Compare commits

...

64 Commits

Author SHA1 Message Date
Eng Zer Jun c93618856a
This closes #2029, use a faster deepcopy library (#2030)
Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
2024-11-22 21:56:38 +08:00
xuri 5f446f25f0
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
2024-11-15 22:03:10 +08:00
Ilia Mirkin 30d3561d0e
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
2024-11-09 18:36:42 +08:00
Ilia Mirkin d2be5cdf8e
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
2024-11-08 16:59:07 +08:00
Ilia Mirkin b7375bc6d4
This closes #1395, add new function SetLegacyDrawingHF support to set graphics in a header/footer (#2018) 2024-11-04 10:39:55 +08:00
xuri 0d5d1c53b2
This closes #2015, fix a v2.9.0 regression bug introduced by commit 7715c1462a
- Fix corrupted workbook generated by open the workbook generated by stream writer
- Update unit tests
2024-10-25 08:52:59 +08:00
xuri af190c7fdc
This closes #2014, fix redundant none type pattern fill generated
- Simplify unit tests
2024-10-23 22:07:25 +08:00
wushiling50 d1937a0cde
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>
2024-10-21 09:36:04 +08:00
xuri f1d1a5dc2b
This closes #2004, support apply number format for time and duration cell value
- Add unit tests
- Update dependencies modules
2024-10-10 22:44:38 +08:00
Jian Yu, Chen b23e5a26df
This closes #1076, add new function MoveSheet to support changing sheet order in the workbook (#1996)
- Add unit tests
2024-09-30 21:00:59 +08:00
liuwangchao bebb802069
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
2024-09-27 14:58:18 +08:00
xuri 41c7dd30ce
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
2024-09-21 15:39:36 +08:00
ArcholSevier 02189fb016
Ref #65, new formula function DOLLAR (#1992)
- Update unit tests
2024-09-12 22:07:18 +08:00
xuri ad8541790d
This closes #1989, fix incorrect result of formula functions XIRR and XNPV
- Require number data type instead of string
- Fix incorrect formula error type
2024-09-08 12:19:58 +08:00
xuri 8c0ef7f90d
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
2024-09-05 21:38:19 +08:00
Ben Smith aca04ecf57
This closes #1987, support absolute paths for pictures (#1988) 2024-09-04 19:47:02 +08:00
Zhang Zhipeng 0447cb22c8
This close #1985, fix incorrect result of cell value after apply general number format (#1986)
- Update unit tests
2024-09-02 09:19:50 +08:00
xuri 29366fd126
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
2024-09-01 12:10:01 +08:00
centurion-hub 8f87131608
This closes #1979, fix decimal value round issue (#1984)
- Updated unit tests
2024-08-31 10:57:26 +08:00
wanghaochen2024 9c460ffe6c
Add support for applying number format expression with language/location tags and ID (#1951)
- Update unit tests for specified date and time format code
2024-08-27 22:41:32 +08:00
zhangyimingdatiancai c805be1f6f
This related for #810, add new functions DeleteSlicer and GetSlicers (#1943)
- Update unit tests
2024-08-23 10:47:47 +08:00
xuri 9a38657515
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
2024-08-18 00:18:02 +08:00
xuri d21b598235
This closes #1968, closes #1969
- Fix missing conditional formatting after remove column
- Fix the SetSheetVisible function panic on none views sheet
- Updated unit tests
2024-07-31 09:10:05 +08:00
xuri 30c4cd70e0
This close #1963, prevent the GetStyle function panic when theme without sysClr 2024-07-23 21:48:17 +08:00
xuri d81b4c8661
This closes #1957, fix missing shape macro missing after adjusted drawing object 2024-07-19 22:22:47 +08:00
xuri 4dd34477f7
This closes #1955, refs #119, support to set cell value with an IEEE 754 "not-a-number" value or infinity 2024-07-18 21:05:36 +08:00
pjh591029530 68a1704900
This fix missing horizontal axis in scatter chart with negative values (#1953)
Co-authored-by: Simmons <1815481@qq.com>
2024-07-17 08:44:16 +08:00
xuri 9c278365f2
This closes #1945, an error will be return if column header cell is empty in pivot table data range
- Update unit tests
2024-07-13 10:41:57 +08:00
wxy 307e533061
This closes #1942, fix percent sign missing in formatted result for zero numeric cell value (#1947)
- Updated unit tests
2024-07-12 08:07:19 +08:00
Patrick Wang 431c31029e
This closes #1944, add new TickLabelPosition field in the ChartAxis data type (#1946)
- Introduce new exported ChartTickLabelPositionType enumeration
- Update unit tests
2024-07-11 14:39:16 +08:00
xuri 53b65150ce
This closes #1940, SetCellHyperLink function now support remove hyperlink by None linkType
- Update unit tests
2024-07-07 17:22:13 +08:00
ShowerBandV 7999a492a4
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
2024-07-06 09:25:09 +08:00
Aybek b18b48099b
Optimize ColumnNumberToName function performance, reduce about 50% memory usage and 50% time cost (#1935)
Co-authored-by: zhayt <zaibek@wtotem.com>
2024-07-05 14:34:02 +08:00
Vovka Morkovka 4e6457accd
This closes #1926, fix secondary vertical axis title is not displayed (#1928) 2024-06-21 08:13:12 +08:00
wangsongyan f04aa8dd31
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
2024-06-19 20:45:25 +08:00
联盟少侠 1a99dd4a23
This closes #1921, fix set axis format doesn't work in combo chart (#1924)
- Fix incorrect primary axis titles position
2024-06-17 21:54:15 +08:00
xuri c349313850
This closes #1910, fix a v2.8.1 regression bug introduced by commit 866f3086cd
- Fix spark lines duplicate when creating spark lines on multiple sheets
2024-05-29 21:05:34 +08:00
xiaokui 08d25006f9
This fixed can not found code coverage on Windows (#1908)
Co-authored-by: Qi Jinkui <qijinkui@kkguan.com>
2024-05-27 20:45:37 +08:00
xuri 0c3dfb1605
This closes #1903, made GetCellStyle function concurrency safe
- Update comment of the function and unit test
2024-05-26 01:25:49 +08:00
xuri 42ad4d6959
This closes #1906, fix a v2.8.1 regression bug introduced by commit d9a0da7b48
- Fix incorrect cell value written if save multiple times
- Update unit tests
2024-05-24 22:05:07 +08:00
nna 5f583549f4
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
2024-05-14 12:06:10 +08:00
xuri a64efca31f
This fixes #1888, read internal media files with absolute path 2024-05-04 20:34:22 +08:00
barlevd 781c38481d
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
2024-05-01 00:05:05 +08:00
xuri 7715c1462a
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
2024-04-27 20:13:43 +08:00
xuri 055349d8a6
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
2024-04-26 00:23:10 +08:00
jianxinhou f8487a68a8
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 <houjianxin.rupert@bytedance.com>
2024-04-18 13:21:46 +08:00
Nima 3e636ae7b2
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
2024-04-11 23:12:56 +08:00
xuri 5f8a5b8690
This closes #1867, breaking changes: change the data type for the ConditionalFormatOptions structure field Format as a pointer 2024-04-03 08:44:46 +08:00
xuri 5dc22e874b
Support get the cell images inserted by IMAGE formula function 2024-04-02 08:47:57 +08:00
xuri 9999221450
This closes #1865, unescape newline character in stream writer
- Update dependencies module
2024-04-01 08:49:21 +08:00
yunkeweb ffad7aecb5
Support get rich data value rels index from rich value part (#1866) 2024-03-28 16:37:35 +08:00
yangyile-yyle88 5e500f5e5d
Introduce new exported PictureInsertType enumeration (#1864) 2024-03-27 15:50:51 +08:00
Matthew Sackman 838232fd27
Add support for get the Microsoft 365 cell images (#1857)
- Update unit tests
2024-03-26 23:19:23 +08:00
xuri 703b73779c
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
2024-03-25 08:33:29 +08:00
realzuojianxiang 5975d87f7e
This closes #1851, and closes #1856 fix formula calculation result round issue (#1860)
- The SetSheetName function now support case sensitivity
- Update unit tests
2024-03-22 16:09:45 +08:00
vic 9e884c798b
This closes #1847, support apply number format with alignment (#1852)
- Update dependencies module
- Update unit tests
2024-03-19 08:58:52 +08:00
hu5ky 4eb088cf73
This fix performance impact introduced in #1692 (#1849)
Co-authored-by: chun.zhang2 <chun.zhang2@geely.com>

- This fix speed slowdown and memory usage increase base on the reverts commit 6220a798fd
- Fix panic on read workbook with internal row element without r attribute
- Update the unit tests
2024-03-15 11:36:34 +08:00
yeahyear 585ebff5b7
Typo fix for the comment of the extractStyleCondFuncs variable (#1846)
Signed-off-by: yetyear <flite@outlook.com>
2024-03-13 14:39:22 +08:00
Evan lu 4ed493819a
This closes #1835, support get data validations which storage in the extension lists (#1834) 2024-03-06 09:26:38 +08:00
xuri f20bbd1f1d
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
2024-03-04 21:40:27 +08:00
Paolo Barbolini 963a058535
Optimize getSharedFormula to avoid runtime.duffcopy (#1837) 2024-03-03 09:39:50 +08:00
陈王 9d4c2e60f6
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 <chenwang@shinsson.com>
2024-03-01 10:12:17 +08:00
岳晨旭 7b4da3906d
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
2024-02-29 09:16:39 +08:00
helloWorld bb603b37d0
Clear slave cells value when merging cells (#1824) 2024-02-27 16:43:47 +08:00
55 changed files with 4407 additions and 1869 deletions

View File

@ -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]
os: [ubuntu-latest, macos-latest, windows-latest]
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]
runs-on: ${{ matrix.os }}
@ -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

138
adjust.go
View File

@ -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)
}
}
@ -237,38 +237,44 @@ 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
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
}
var delete bool
coordinates, err := rangeRefToCoordinates(ref)
if err != nil {
return ref, delete, err
for _, ref := range strings.Split(cellRef, " ") {
if !strings.Contains(ref, ":") {
ref += ":" + ref
}
coordinates, err := rangeRefToCoordinates(ref)
if err != nil {
return "", err
}
if dir == columns {
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] && num == coordinates[1] {
continue
}
coordinates = applyOffset(coordinates, 1, 3, TotalRows)
}
if ref, err = coordinatesToRangeRef(coordinates); err != nil {
return "", err
}
SQRef = append(SQRef, ref)
}
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
}
} else {
if offset < 0 && coordinates[1] == coordinates[3] {
delete = true
}
if coordinates[1] >= num {
coordinates[1] += offset
}
if coordinates[3] >= num {
coordinates[3] += offset
}
}
ref, err = f.coordinatesToRangeRef(coordinates)
return ref, delete, err
return strings.Join(SQRef, " "), nil
}
// adjustFormula provides a function to adjust formula reference and shared
@ -284,7 +290,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 {
@ -320,7 +326,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 +342,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
@ -437,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 {
@ -454,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 {
@ -542,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 {
@ -662,7 +676,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
}
@ -692,7 +706,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
}
}
@ -702,7 +716,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
}
@ -769,7 +783,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
}
}
@ -928,11 +942,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--
@ -963,11 +977,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--
@ -975,15 +989,15 @@ 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)
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 {
formula := unescapeDataValidationFormula(worksheet.DataValidations.DataValidation[i].Formula2.Content)
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
}

View File

@ -4,6 +4,7 @@ import (
"encoding/xml"
"fmt"
"path/filepath"
"strings"
"testing"
_ "image/jpeg"
@ -289,7 +290,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",
@ -463,6 +464,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) {
@ -991,7 +1000,7 @@ func TestAdjustConditionalFormats(t *testing.T) {
{
Type: "cell",
Criteria: "greater than",
Format: formatID,
Format: &formatID,
Value: "0",
},
}
@ -1011,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) {
@ -1059,6 +1119,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 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 A4:A1048576", dvs[3].Sqref)
dv = NewDataValidation(true)
dv.Sqref = "C5:D6"
assert.NoError(t, dv.SetRange("Sheet1!A1048576", "Sheet1!XFD1", DataValidationTypeWhole, DataValidationOperatorBetween))
@ -1081,6 +1151,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)), "\"", "&quot;")
ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Formula1.Content = formula
assert.NoError(t, 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) {

187
calc.go
View File

@ -478,6 +478,7 @@ type formulaFuncs struct {
// DISC
// DMAX
// DMIN
// DOLLAR
// DOLLARDE
// DOLLARFR
// DPRODUCT
@ -831,8 +832,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 +841,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 +4284,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 +4362,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 +11573,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
@ -11602,7 +11603,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 +11967,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
@ -12063,7 +12082,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,
@ -13610,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
@ -13741,34 +13762,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
}
if idx-pre > 1 {
result++
}
pre = idx
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
}
}
return newNumberFormulaArg(float64(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()))
}
mtx = append(mtx, array)
}
return newMatrixFormulaArg(mtx)
}
return find(findTextArg.Value())
}
// LEFT function returns a specified number of characters from the start of a
@ -14427,7 +14462,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
@ -14436,7 +14471,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
}
@ -16309,6 +16344,52 @@ 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: "$",
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))
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:
//
@ -18165,28 +18246,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 arg.Type == ArgNumber {
datesArg = append(datesArg, arg.Number)
if arg.Number < date {
err = newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
return
}
date = arg.Number
continue
}
if dateValue.Number < date {
err = newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
return
}
datesArg = append(datesArg, dateValue.Number)
date = dateValue.Number
err = newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
return
}
if len(valuesArg) != len(datesArg) {
err = newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)

View File

@ -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",
@ -1538,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",
@ -1748,6 +1751,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",
@ -2113,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
@ -3663,7 +3677,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"},
@ -4247,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"},
@ -4773,7 +4792,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)
}
@ -5559,13 +5578,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{
@ -5581,8 +5600,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!"},
@ -5705,15 +5724,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{
@ -5729,9 +5748,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!"},
@ -6308,13 +6327,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})

77
cell.go
View File

@ -15,6 +15,7 @@ import (
"bytes"
"encoding/xml"
"fmt"
"math"
"os"
"reflect"
"strconv"
@ -143,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:
@ -255,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
}
@ -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
@ -505,7 +512,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 {
@ -955,14 +964,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",
@ -1030,6 +1061,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)
}
@ -1082,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()
@ -1426,8 +1459,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 +1470,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 {
@ -1655,8 +1688,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)

View File

@ -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{
@ -88,6 +91,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)
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
assert.NoError(t, f.DeleteDataValidation("Sheet1", dv.Sqref))
wg.Done()
}(i, t)
}
@ -97,6 +108,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())
}
@ -266,7 +281,7 @@ func TestSetCellValue(t *testing.T) {
f.Pkg.Store(defaultXMLPathSharedStrings, []byte(fmt.Sprintf(`<sst xmlns="%s" count="2" uniqueCount="1"><si><t>a</t></si><si><t>a</t></si></sst>`, 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"))
@ -277,6 +292,42 @@ 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)
}
// 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) {
@ -286,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))
@ -375,6 +426,9 @@ func TestGetCellValue(t *testing.T) {
f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="0"><c r="H6" t="inlineStr"><is><t>H6</t></is></c><c r="A1" t="inlineStr"><is><t>r0A6</t></is></c><c r="F4" t="inlineStr"><is><t>F4</t></is></c></row><row><c r="A1" t="inlineStr"><is><t>A6</t></is></c><c r="B1" t="inlineStr"><is><t>B6</t></is></c><c r="C1" t="inlineStr"><is><t>C6</t></is></c></row><row r="3"><c r="A3"><v>100</v></c><c r="B3" t="inlineStr"><is><t>B3</t></is></c></row>`)))
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"},
@ -385,9 +439,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, `<row><c r="A1" t="inlineStr"><is><t>A1</t></is></c></row><row></row><row><c r="A3" t="inlineStr"><is><t>A3</t></is></c></row>`)))
@ -757,6 +808,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)

View File

@ -85,11 +85,25 @@ type ChartLineType byte
// This section defines the currently supported chart line types enumeration.
const (
ChartLineSolid ChartLineType = iota
ChartLineUnset ChartLineType = iota
ChartLineSolid
ChartLineNone
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 +498,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",
}
@ -830,6 +850,7 @@ func (opts *Chart) parseTitle() {
// ReverseOrder
// Maximum
// Minimum
// Alignment
// Font
// NumFmt
// Title
@ -844,6 +865,7 @@ func (opts *Chart) parseTitle() {
// ReverseOrder
// Maximum
// Minimum
// Alignment
// Font
// LogBase
// NumFmt
@ -876,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:
//

View File

@ -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,11 +233,11 @@ 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}},
{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
@ -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)

14
col.go
View File

@ -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.
@ -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.
@ -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)
}

View File

@ -140,7 +140,7 @@ func TestGetColsError(t *testing.T) {
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(`<worksheet xmlns="%s"><sheetData><row r="A"><c r="2" t="inlineStr"><is><t>B</t></is></c></row></sheetData></worksheet>`, 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(`<worksheet xmlns="%s"><sheetData><row r="2"><c r="A" t="inlineStr"><is><t>B</t></is></c></row></sheetData></worksheet>`, NameSpaceSpreadSheet.Value)))
_, err = f.GetCols("Sheet1")

View File

@ -13,6 +13,7 @@ package excelize
import (
"fmt"
"io"
"math"
"strings"
"unicode/utf16"
@ -222,8 +223,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 +258,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)
}
@ -290,46 +294,83 @@ 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 ws.ExtLst != nil {
if err = f.xmlNewDecoder(strings.NewReader("<extLst>" + ws.ExtLst.Ext + "</extLst>")).
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)...)
}
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
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, "<xm:f>"), "</xm:f>")
dataValidation.Formula2 = strings.TrimSuffix(strings.TrimPrefix(dataValidation.Formula2, "<xm:f>"), "</xm:f>")
}
dataValidations = append(dataValidations, dataValidation)
}
return dataValidations
}
// 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 +378,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 +398,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 +414,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 +425,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,13 +435,18 @@ 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])
}
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, "&quot;") && strings.HasSuffix(dv.Content, "&quot;"))
}
// unescapeDataValidationFormula returns unescaped data validation formula.
func unescapeDataValidationFormula(val string) string {
if strings.HasPrefix(val, "\"") { // Text detection

View File

@ -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(`<ext uri="%s" xmlns:x14="%s"><x14:dataValidations><x14:dataValidation type="list" allowBlank="1"><x14:formula1><xm:f>Sheet1!$B$1:$B$5</xm:f></x14:formula1><xm:sqref>A7:B8</xm:sqref></x14:dataValidation></x14:dataValidations></ext>`, 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(`<ext uri="%s" xmlns:x14="%s"><x14:dataValidations></x14:dataValidation></x14:dataValidations></ext>`, ExtURIDataValidations, NameSpaceSpreadSheetX14.Value)}
_, err = f.GetDataValidations("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: element <dataValidations> closed by </dataValidation>")
// Test get validations without validations
assert.Nil(t, getDataValidations(nil))
assert.Nil(t, getDataValidations(&xlsxDataValidations{DataValidation: []*xlsxDataValidation{nil}}))
}
func TestDataValidationError(t *testing.T) {

28
date.go
View File

@ -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
}

View File

@ -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,
@ -176,14 +176,19 @@ 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](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,240 +198,242 @@ 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 {
c := cCharts{
BarDir: &attrValString{
Val: stringPtr("col"),
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,
},
Ser: f.drawChartSeries(opts),
Shape: f.drawChartShape(opts),
DLbls: f.drawChartDLbls(opts),
AxID: f.genAxID(opts),
Overlap: &attrValInt{Val: intPtr(100)},
},
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)},
}
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(opts)
valAx := f.drawPlotAreaValAx(opts)
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,
},
@ -436,233 +443,256 @@ 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
}
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)},
},
}
}
// 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{
Val: stringPtr(plotAreaChartGrouping[opts.Type]),
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),
},
VaryColors: &attrValBool{
Val: boolPtr(false),
},
Ser: f.drawChartSeries(opts),
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{
Val: stringPtr(plotAreaChartGrouping[opts.Type]),
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),
},
VaryColors: &attrValBool{
Val: boolPtr(false),
},
Ser: f.drawChartSeries(opts),
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{
Val: opts.VaryColors,
PieChart: []*cCharts{
{
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
Ser: f.drawChartSeries(opts),
},
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(opts *Chart) *cPlotArea {
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),
},
}
}
// 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)}
}
return &cPlotArea{
OfPieChart: &cCharts{
OfPieType: &attrValString{
Val: stringPtr("pie"),
OfPieChart: []*cCharts{
{
OfPieType: &attrValString{
Val: stringPtr("pie"),
},
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
Ser: f.drawChartSeries(opts),
SplitPos: splitPos,
SerLines: &attrValString{},
},
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
Ser: f.drawChartSeries(opts),
SplitPos: splitPos,
SerLines: &attrValString{},
},
}
}
// 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)}
}
return &cPlotArea{
OfPieChart: &cCharts{
OfPieType: &attrValString{
Val: stringPtr("bar"),
OfPieChart: []*cCharts{
{
OfPieType: &attrValString{
Val: stringPtr("bar"),
},
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
SplitPos: splitPos,
Ser: f.drawChartSeries(opts),
SerLines: &attrValString{},
},
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
SplitPos: splitPos,
Ser: f.drawChartSeries(opts),
SerLines: &attrValString{},
},
}
}
// 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{
Val: stringPtr("marker"),
RadarChart: []*cCharts{
{
RadarStyle: &attrValString{
Val: stringPtr("marker"),
},
VaryColors: &attrValBool{
Val: boolPtr(false),
},
Ser: f.drawChartSeries(opts),
DLbls: f.drawChartDLbls(opts),
AxID: f.genAxID(opts),
},
VaryColors: &attrValBool{
Val: boolPtr(false),
},
Ser: f.drawChartSeries(opts),
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{
Val: stringPtr("smoothMarker"), // line,lineMarker,marker,none,smooth,smoothMarker
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),
},
VaryColors: &attrValBool{
Val: boolPtr(false),
},
Ser: f.drawChartSeries(opts),
DLbls: f.drawChartDLbls(opts),
AxID: f.genAxID(opts),
},
CatAx: f.drawPlotAreaCatAx(opts),
ValAx: f.drawPlotAreaValAx(opts),
ValAx: append(f.drawPlotAreaCatAx(pa, opts), 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),
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(opts),
ValAx: f.drawPlotAreaValAx(opts),
CatAx: f.drawPlotAreaCatAx(pa, opts),
ValAx: f.drawPlotAreaValAx(pa, opts),
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
}
// 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),
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(opts),
ValAx: f.drawPlotAreaValAx(opts),
CatAx: f.drawPlotAreaCatAx(pa, opts),
ValAx: f.drawPlotAreaValAx(pa, opts),
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
}
// 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{
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: []*cAxs{f.drawPlotAreaCatAx(opts)[0], f.drawPlotAreaValAx(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))}
plotArea.BubbleChart[0].BubbleScale = &attrValFloat{Val: float64Ptr(float64(opts.BubbleSize))}
}
return plotArea
}
@ -751,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
@ -979,7 +1005,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 +1014,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(tickLblPosVal[opts.XAxis.TickLabelPosition])},
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 +1073,52 @@ 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(tickLblPosVal[opts.YAxis.TickLabelPosition])},
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)
if pos, ok := tickLblPosNone[opts.Type]; ok {
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.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.
@ -1147,7 +1141,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)},
@ -1262,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
}

View File

@ -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")
@ -97,6 +100,9 @@ var (
// ErrPasswordLengthInvalid defined the error message on invalid password
// length.
ErrPasswordLengthInvalid = errors.New("password length invalid")
// 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")
// ErrSheetIdx defined the error message on receive the invalid worksheet
@ -246,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 {
@ -264,6 +276,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 {

View File

@ -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.
@ -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
@ -305,84 +308,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
@ -452,14 +456,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:
//
// <row r="19" spans="2:2">
// <row r="19">
// <c r="B19">
// <f>SUM(Sheet2!D2,Sheet2!D11)</f>
// <v>100</v>
@ -468,7 +472,7 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int {
//
// to
//
// <row r="19" spans="2:2">
// <row r="19">
// <c r="B19">
// <f>SUM(Sheet2!D2,Sheet2!D11)</f>
// </c>
@ -588,3 +592,77 @@ 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
}
// 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(defaultXMLRdRichValuePart)))).
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) {
var richValueRels xlsxRichValueRels
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValueRel)))).
Decode(&richValueRels); err != nil && err != io.EOF {
return &richValueRels, err
}
return &richValueRels, nil
}
// 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(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 {
if v.ID == rID {
return &v
}
}
}
return nil
}

View File

@ -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) {
@ -365,11 +389,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)
@ -431,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) {
@ -742,11 +778,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 {
@ -834,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: {{"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"}},
} {
f, err := prepareTestBook5(Options{CultureInfo: lang})
assert.NoError(t, err)
@ -850,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: {{"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"}},
} {
f, err := prepareTestBook5(Options{CultureInfo: lang, ShortDatePattern: "yyyy-M-d", LongTimePattern: "hh:mm:ss"})
assert.NoError(t, err)
@ -962,7 +1007,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")))
})
@ -1088,7 +1133,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "cell",
Criteria: "between",
Format: format1,
Format: &format1,
MinValue: "6",
MaxValue: "8",
},
@ -1100,7 +1145,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "cell",
Criteria: ">",
Format: format3,
Format: &format3,
Value: "6",
},
},
@ -1111,7 +1156,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "top",
Criteria: "=",
Format: format3,
Format: &format3,
},
},
))
@ -1121,7 +1166,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "unique",
Criteria: "=",
Format: format2,
Format: &format2,
},
},
))
@ -1131,7 +1176,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "duplicate",
Criteria: "=",
Format: format2,
Format: &format2,
},
},
))
@ -1141,7 +1186,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "top",
Criteria: "=",
Format: format1,
Format: &format1,
Value: "6",
Percent: true,
},
@ -1153,7 +1198,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "average",
Criteria: "=",
Format: format3,
Format: &format3,
AboveAverage: true,
},
},
@ -1164,7 +1209,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "average",
Criteria: "=",
Format: format1,
Format: &format1,
AboveAverage: false,
},
},
@ -1187,7 +1232,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "formula",
Criteria: "L2<3",
Format: format1,
Format: &format1,
},
},
))
@ -1197,7 +1242,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "cell",
Criteria: ">",
Format: format4,
Format: &format4,
Value: "0",
},
},
@ -1610,13 +1655,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 {

View File

@ -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 {

18
go.mod
View File

@ -3,20 +3,20 @@ 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/xuri/efp v0.0.0-20231025114914-d1ff6096ae53
github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05
golang.org/x/crypto v0.19.0
golang.org/x/image v0.14.0
golang.org/x/net v0.21.0
golang.org/x/text v0.14.0
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
golang.org/x/image v0.18.0
golang.org/x/net v0.31.0
golang.org/x/text v0.20.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
)

36
go.sum
View File

@ -1,28 +1,28 @@
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/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/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=
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.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=
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/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=
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/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=
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=

26
lib.go
View File

@ -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
@ -323,7 +329,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 +366,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) {
@ -646,6 +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].Name == NameSpaceSpreadSheet.Name {
ns = true
}
}
if !ns {
startElement.Attr = append(startElement.Attr, NameSpaceSpreadSheet)
}
return startElement.Attr
}
}

View File

@ -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")
}
@ -290,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(`<x:workbook xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main"></x:workbook>`)))
assert.Len(t, getRootElement(d), 3)
}
func TestSetIgnorableNameSpace(t *testing.T) {

View File

@ -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})
@ -128,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)

1117
numfmt.go

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,10 @@ 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"},
{"43528", "y", "19"},
{"43528", "Y", "19"},
{"43528", "yy", "19"},
@ -66,7 +70,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"},
@ -76,22 +83,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."},
@ -242,6 +289,28 @@ 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"},
{"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"},
@ -2667,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日"},
@ -3488,15 +3560,18 @@ 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.125", "0.00", "1234.13"},
{"1234.5678", "0.00", "1234.57"},
{"1234.5678", "#,##0", "1,235"},
{"1234.5678", "#,##0.00", "1,234.57"},
@ -3580,7 +3655,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",

View File

@ -23,6 +23,18 @@ 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
PictureInsertTypeIMAGE
PictureInsertTypeDISPIMG
)
// parseGraphicOptions provides a function to parse the format settings of
// the picture with default value.
func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions {
@ -52,7 +64,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
//
@ -125,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.
//
@ -167,8 +186,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 +232,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 {
@ -271,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) {
@ -473,8 +507,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
@ -497,13 +530,14 @@ 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)
strings.ReplaceAll(drawingXML, "xl/drawings", "xl/drawings/_rels"), ".xml", ".xml.rels")
embeddedImageCells, err := f.getImageCells(sheet)
if err != nil {
return nil, err
}
@ -577,16 +611,23 @@ 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{}}
if buffer, _ := f.Pkg.Load(strings.ReplaceAll(r.Target, "..", "xl")); buffer != nil {
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 {
pic.File = buffer.([]byte)
pic.Format.AltText = a.Pic.NvPicPr.CNvPr.Descr
pics = append(pics, pic)
}
}
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 {
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)
@ -715,6 +756,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
@ -736,14 +780,21 @@ 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 {
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)
}
@ -771,9 +822,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 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 +842,125 @@ func (f *File) getEmbeddedImageCells(sheet string) ([]string, error) {
}
cells = append(cells, c.R)
}
r, err := f.getImageCellRel(&c, &Picture{})
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.
// 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 r, err
}
richValueRel, err := f.richValueRelReader()
if err != nil {
return r, 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
}
richValueWebImages, err := f.richValueWebImageReader()
if err != nil {
return r, err
}
if idx >= len(richValueWebImages.WebImageSrd) {
return r, 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, pic *Picture) (*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
}
richValueIdx := vmd.Bk[*c.Vm-1].Rc[0].V
richValue, err := f.richValueReader()
if err != nil {
return r, err
}
if richValueIdx >= len(richValue.Rv) {
return r, 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
}
// 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) {
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) {
pic := Picture{Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceInCell}
r, err := f.getImageCellRel(c, &pic)
if err != nil || r == nil {
return "", true, err
}
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)
}
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
@ -823,7 +985,7 @@ func (f *File) getCellImages(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

View File

@ -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) {
@ -49,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))
@ -59,6 +59,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"}}))
@ -81,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"))
@ -90,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)
@ -150,6 +152,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) {
@ -216,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
@ -244,11 +260,12 @@ 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(`<etc:cellImages xmlns:etc="http://www.wps.cn/officeDocument/2017/etCustomData"><etc:cellImage><xdr:pic><xdr:nvPicPr><xdr:cNvPr id="1" name="ID_********************************" descr="CellImage1"/></xdr:nvPicPr><xdr:blipFill><a:blip r:embed="rId1"/></xdr:blipFill></xdr:pic></etc:cellImage></etc:cellImages>`))
f.Pkg.Store(defaultXMLPathCellImagesRels, []byte(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.jpeg"/></Relationships>`))
f.Pkg.Store(defaultXMLPathCellImagesRels, []byte(fmt.Sprintf(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="%s" Target="media/image1.jpeg"/></Relationships>`, SourceRelationshipImage)))
pics, err = f.GetPictures("Sheet1", "F21")
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()"))
@ -448,13 +465,134 @@ 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 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(`<metadata><valueMetadata count="1"><bk><rc t="1" v="0"/></bk></valueMetadata></metadata>`))
f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`<rvData count="1"><rv s="0"><v>0</v><v>5</v></rv></rvData>`))
f.Pkg.Store(defaultXMLRdRichValueRel, []byte(`<richValueRels><rel r:id="rId1"/></richValueRels>`))
f.Pkg.Store(defaultXMLRdRichValueRelRels, []byte(fmt.Sprintf(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="%s" Target="../media/image1.png"/></Relationships>`, 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))
assert.Equal(t, PictureInsertTypePlaceInCell, pics[0].InsertType)
cells, err := f.GetPictureCells("Sheet1")
assert.NoError(t, err)
assert.Equal(t, []string{"A1"}, cells)
// Test get the cell images without image relationships parts
f.Relationships.Delete(defaultXMLRdRichValueRelRels)
f.Pkg.Store(defaultXMLRdRichValueRelRels, []byte(fmt.Sprintf(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="%s" Target="../media/image1.png"/></Relationships>`, 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(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(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
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 cell images with rich data rich value relationships
f.Pkg.Store(defaultXMLMetadata, []byte(`<metadata><valueMetadata count="1"><bk><rc t="1" v="0"/></bk></valueMetadata></metadata>`))
f.Pkg.Store(defaultXMLRdRichValueRel, []byte(`<richValueRels/>`))
pics, err = f.GetPictures("Sheet1", "A1")
assert.NoError(t, err)
assert.Empty(t, pics)
// 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 cell images without block of metadata records
f.Pkg.Store(defaultXMLMetadata, []byte(`<metadata><valueMetadata/></metadata>`))
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(defaultXMLRdRichValuePart, []byte(`<rvData count="1"><rv s="0"><v></v><v>5</v></rv></rvData>`))
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(defaultXMLRdRichValuePart, []byte(`<rvData count="1"><rv s="0"><v>0</v><v>1</v></rv></rvData>`))
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(defaultXMLRdRichValuePart, 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(`<metadata><valueMetadata count="1"><bk><rc t="1" v="1"/></bk></valueMetadata></metadata>`))
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(`<rvData count="1"><rv s="1"><v>0</v><v>1</v><v>0</v><v>0</v></rv></rvData>`))
f.Pkg.Store(defaultXMLRdRichValueWebImagePart, []byte(`<webImagesSrd xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><webImageSrd><address r:id="rId1"/><blip r:id="rId2"/></webImageSrd>
</webImagesSrd>`))
f.Pkg.Store(defaultXMLRdRichValueWebImagePartRels, []byte(fmt.Sprintf(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="%s" Target="https://github.com/xuri/excelize" TargetMode="External"/><Relationship Id="rId2" Type="%s" Target="../media/image1.png"/></Relationships>`, 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(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="%s" Target="https://github.com/xuri/excelize" TargetMode="External"/><Relationship Id="rId2" Type="%s" Target="../media/image1.png"/></Relationships>`, 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(`<webImagesSrd xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" />`))
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(`<rvData count="1"><rv s="1"><v></v><v>1</v><v>0</v><v>0</v></rv></rvData>`))
_, err = f.GetPictures("Sheet1", "A1")
assert.EqualError(t, err, "strconv.Atoi: parsing \"\": invalid syntax")
}
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())
}

View File

@ -51,6 +51,7 @@ type PivotTableOptions struct {
UseAutoFormatting bool
PageOverThenDown bool
MergeItem bool
ClassicLayout bool
CompactData bool
ShowError bool
ShowRowHeaders bool
@ -58,10 +59,16 @@ type PivotTableOptions struct {
ShowRowStripes bool
ShowColStripes bool
ShowLastColumn bool
FieldPrintTitles bool
ItemPrintTitles bool
PivotTableStyleName string
}
// 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,15 +85,19 @@ 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
Name string
Outline bool
ShowAll bool
InsertBlankRow bool
Subtotal string
DefaultSubtotal bool
NumFmt int
}
// AddPivotTable provides the method to add pivot table by given pivot table
@ -210,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
}
@ -260,6 +274,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 +289,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{
@ -337,7 +356,10 @@ 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,
DataCaption: "Values",
Location: &xlsxLocation{
Ref: topLeftCell + ":" + bottomRightCell,
@ -370,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)
@ -447,6 +475,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{}
@ -455,6 +484,7 @@ func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opts *PivotTable
Name: dataFieldsName[idx],
Fld: dataField,
Subtotal: dataFieldsSubtotals[idx],
NumFmtID: dataFieldsNumFmtID[idx],
})
}
@ -518,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 {
@ -535,23 +573,26 @@ 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,
Compact: &rowOptions.Compact,
Outline: &rowOptions.Outline,
ShowAll: rowOptions.ShowAll,
InsertBlankRow: rowOptions.InsertBlankRow,
DefaultSubtotal: &rowOptions.DefaultSubtotal,
Items: &xlsxItems{
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),
@ -561,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 {
@ -572,27 +615,35 @@ 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,
Compact: &columnOptions.Compact,
Outline: &columnOptions.Outline,
ShowAll: columnOptions.ShowAll,
InsertBlankRow: columnOptions.InsertBlankRow,
DefaultSubtotal: &columnOptions.DefaultSubtotal,
Items: &xlsxItems{
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
}
@ -682,6 +733,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 {
@ -756,12 +823,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
@ -803,12 +869,15 @@ 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", 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,
ClassicLayout: pt.GridDropZones,
FieldPrintTitles: pt.FieldPrintTitles,
ItemPrintTitles: pt.ItemPrintTitles,
}
if pc.CacheSource.WorksheetSource.Name != "" {
opts.DataRange = pc.CacheSource.WorksheetSource.Name
@ -886,6 +955,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,
})
}
}
@ -895,7 +965,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()
@ -986,8 +1058,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]++
}
@ -1008,3 +1080,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
}

View File

@ -31,17 +31,20 @@ 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}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}},
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,
ShowDrill: true,
ClassicLayout: true,
ShowError: true,
ShowRowHeaders: true,
ShowColHeaders: true,
ShowLastColumn: true,
ShowError: true,
FieldPrintTitles: true,
ItemPrintTitles: true,
PivotTableStyleName: "PivotStyleLight16",
}
assert.NoError(t, f.AddPivotTable(expected))
@ -131,7 +134,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,
@ -139,12 +142,19 @@ 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",
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,
@ -164,7 +174,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,
@ -256,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")
@ -293,6 +310,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{
@ -335,6 +353,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())
}
@ -386,6 +406,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) {
@ -477,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

92
rows.go
View File

@ -20,7 +20,7 @@ import (
"strconv"
"strings"
"github.com/mohae/deepcopy"
"github.com/tiendc/go-deepcopy"
)
// duplicateHelperFunc defines functions to duplicate helper.
@ -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++
@ -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
}
}
@ -392,7 +395,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 +426,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 +581,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,8 +652,8 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
var rowCopy xlsxRow
for i, r := range ws.SheetData.Row {
if *r.R == row {
rowCopy = deepcopy.Copy(ws.SheetData.Row[i]).(xlsxRow)
if r.R == row {
deepcopy.Copy(&rowCopy, ws.SheetData.Row[i])
ok = true
break
}
@ -666,7 +669,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
}
@ -688,26 +691,47 @@ 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 = f.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 {
var cfCopy xlsxConditionalFormatting
deepcopy.Copy(&cfCopy, *cf)
cfCopy.SQRef = strings.Join(SQRef, " ")
cfs = append(cfs, &cfCopy)
}
}
@ -725,20 +749,20 @@ 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 = f.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 {
var dvCopy xlsxDataValidation
deepcopy.Copy(&dvCopy, *dv)
dvCopy.Sqref = strings.Join(SQRef, " ")
dvs = append(dvs, &dvCopy)
}
}

View File

@ -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))

112
sheet.go
View File

@ -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
}
@ -167,7 +168,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()
}
@ -363,7 +364,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()
@ -600,6 +601,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) {
@ -707,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 {
@ -773,6 +818,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
}
@ -1191,7 +1241,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 AddHeaderFooterImage)
// |
// &H | Shadow text format
// |
@ -1561,8 +1611,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
@ -1574,12 +1623,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
}
@ -1588,7 +1640,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)
}
@ -1604,13 +1659,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),
}
@ -1638,6 +1701,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
}
@ -1652,6 +1718,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
@ -1957,7 +2041,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]
@ -2014,7 +2098,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
}

View File

@ -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) {
@ -465,8 +476,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))
@ -544,6 +558,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(defaultXMLPathWorkbook, 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)
@ -567,6 +618,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) {

357
slicer.go
View File

@ -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), "<extLst>"), "</extLst>")}
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("<extLst>" + ws.ExtLst.Ext + "</extLst>")).
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("<decodeCellAnchor>" + anchor.GraphicFrame + "</decodeCellAnchor>")).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("<extLst>" + ws.ExtLst.Ext + "</extLst>")).
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), "<extLst>"), "</extLst>")}
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)
}

View File

@ -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(`<wsDr xmlns="%s"><twoCellAnchor><from><col>-1</col><row>-1</row></from><mc:AlternateContent><mc:Choice xmlns:sle15="%s"><graphicFrame><nvGraphicFramePr><cNvPr id="2" name="Column1"/></nvGraphicFramePr></graphicFrame></mc:Choice></mc:AlternateContent></twoCellAnchor></wsDr>`, 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(`<wsDr xmlns="%s"><twoCellAnchor/></wsDr>`, 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(`<slicerCacheDefinition xmlns="%s" name="Slicer2" sourceName="B1"><extLst><ext uri="%s"/></extLst></slicerCacheDefinition>`, 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(`<slicerCacheDefinition xmlns="%s" name="Slicer1" sourceName="B1"><extLst><ext uri="%s"/></extLst></slicerCacheDefinition>`, 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(`<slicerCacheDefinition xmlns="%s" name="Slicer1" sourceName="B1"><extLst><ext uri="%s"><tableSlicerCache tableId="1" column="2"/></ext></extLst></slicerCacheDefinition>`, 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(`<slicerCacheDefinition xmlns="%s" name="Slicer1" sourceName="B1"></slicerCacheDefinition>`, 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
@ -362,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())
}

View File

@ -18,350 +18,352 @@ 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
// 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"},
@ -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"

View File

@ -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
}
@ -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,11 +527,11 @@ 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)
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:
@ -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.

View File

@ -3,6 +3,8 @@ package excelize
import (
"encoding/xml"
"fmt"
"io"
"math"
"math/rand"
"os"
"path/filepath"
@ -75,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++ {
@ -144,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())
@ -224,6 +228,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 +338,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 +403,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("<worksheet><row r=\"1\"><c r=\"B1\"></c></row><worksheet/>")
_, err = sw.getRowValues(1, 1, 1)
assert.NoError(t, err)
sw.rawData.buf.Reset()
// Test getRowValues with illegal cell reference
sw.rawData.buf.WriteString("<worksheet><row r=\"1\"><c r=\"A\"></c></row><worksheet/>")
_, 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("<worksheet><row r=\"1\"><c></row><worksheet/>")
_, err = sw.getRowValues(1, 1, 1)
assert.EqualError(t, err, "XML syntax error on line 1: element <c> closed by </row>")
sw.rawData.buf.Reset()
}
func TestStreamWriterGetRowElement(t *testing.T) {
// Test get row element without r attribute
dec := xml.NewDecoder(strings.NewReader("<row ht=\"0\" />"))
for {
token, err := dec.Token()
if err == io.EOF {
break
}
_, ok := getRowElement(token, 0)
assert.False(t, ok)
}
}

168
styles.go
View File

@ -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 {
@ -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
}
@ -1891,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
@ -2042,11 +2051,12 @@ func newFills(style *Style, fg bool) *xlsxFill {
if style.Fill.Pattern > 18 || style.Fill.Pattern < 0 {
break
}
if len(style.Fill.Color) < 1 {
break
}
var pattern xlsxPatternFill
pattern.PatternType = styleFillPatterns[style.Fill.Pattern]
if len(style.Fill.Color) < 1 {
fill.PatternFill = &pattern
break
}
if fg {
if pattern.FgColor == nil {
pattern.FgColor = new(xlsxColor)
@ -2186,19 +2196,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
}
@ -2445,7 +2458,7 @@ func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID
// {
// Type: "cell",
// Criteria: ">",
// Format: format,
// Format: &format,
// Value: "6",
// },
// },
@ -2458,7 +2471,7 @@ func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID
// {
// Type: "cell",
// Criteria: ">",
// Format: format,
// Format: &format,
// Value: "$C$1",
// },
// },
@ -2482,7 +2495,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 +2547,7 @@ func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID
// {
// Type: "cell",
// Criteria: "between",
// Format: format,
// Format: &format,
// MinValue: 6",
// MaxValue: 8",
// },
@ -2554,7 +2567,7 @@ func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID
// {
// Type: "average",
// Criteria: "=",
// Format: format1,
// Format: &format1,
// AboveAverage: true,
// },
// },
@ -2566,7 +2579,7 @@ func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID
// {
// Type: "average",
// Criteria: "=",
// Format: format2,
// Format: &format2,
// AboveAverage: false,
// },
// },
@ -2578,7 +2591,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 +2600,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 +2613,7 @@ func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID
// {
// Type: "top",
// Criteria: "=",
// Format: format,
// Format: &format,
// Value: "6",
// },
// },
@ -2613,7 +2626,7 @@ func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID
// {
// Type: "top",
// Criteria: "=",
// Format: format,
// Format: &format,
// Value: "6",
// Percent: true,
// },
@ -2931,10 +2944,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 +2956,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 +2970,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 +2988,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 +3003,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 +3012,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 +3148,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 +3214,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 +3248,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 +3278,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 +3292,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 +3310,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 +3322,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 +3410,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 +3422,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 +3434,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 +3446,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 +3458,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
}

View File

@ -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")
@ -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) {

View File

@ -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 {
@ -350,7 +364,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 +477,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
@ -474,7 +488,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 +504,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
}

View File

@ -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))
}

View File

@ -102,8 +102,9 @@ 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}"
ExtURIDataValidations = "{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}"
ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}"
ExtURIExternalLinkPr = "{FCE6A71B-6B00-49CD-AB44-F6B1AE7CDE65}"
ExtURIIgnoredErrors = "{01252117-D84E-4E92-8308-4BE1C098FCBB}"
@ -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}"
@ -266,19 +270,25 @@ 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"
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
@ -485,9 +495,19 @@ 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"}
// 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"}

154
vml.go
View File

@ -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),
@ -1070,3 +1081,140 @@ func extractVMLFont(font []decodeVMLFont) []RichTextRun {
}
return runs
}
// AddHeaderFooterImage provides a mechanism to set the graphics that can be
// 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".
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)
}
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"},
},
}
// 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)
}
}
}
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(opts.File, ext), "xl")
imageID := f.addRels(drawingVMLRels, SourceRelationshipImage, mediaStr, "")
shape := xlsxShape{
ID: shapeID,
SpID: "_x0000_s1025",
Type: "#_x0000_t75",
Style: style,
}
sp, _ := xml.Marshal(encodeShape{
ImageData: &vImageData{RelID: "rId" + strconv.Itoa(imageID)},
Lock: &oLock{Ext: "edit", Rotation: "t"},
})
shape.Val = string(sp[13 : len(sp)-14])
vml.Shape = append(vml.Shape, shape)
f.VMLDrawing[drawingVML] = vml
if err := f.setContentTypePartImageExtensions(); err != nil {
return err
}
return f.setContentTypePartVMLExtensions()
}

View File

@ -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"`
@ -162,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"`
@ -254,7 +289,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 +338,15 @@ type FormControl struct {
Type FormControlType
Format GraphicOptions
}
// 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
}

View File

@ -412,3 +412,100 @@ func TestExtractFormControl(t *testing.T) {
_, err := extractFormControl(string(MacintoshCyrillicCharset))
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestAddHeaderFooterImage(t *testing.T) {
f, sheet, wb := NewFile(), "Sheet1", filepath.Join("test", "TestAddHeaderFooterImage.xlsx")
headerFooterOptions := HeaderFooterOptions{
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))
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.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
File: images[".jpg"],
FirstPage: true,
Extension: ".jpg",
Width: "50pt",
Height: "32pt",
}))
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.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
Extension: ".png",
File: images[".png"],
Width: "50pt",
Height: "32pt",
}), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
}

View File

@ -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.
@ -530,20 +530,22 @@ 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
Alignment Alignment
Font Font
LogBase float64
NumFmt ChartNumFmt
Title []RichTextRun
axID int
}
// ChartDimension directly maps the dimension of the chart.

View File

@ -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"`
}
@ -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,19 +46,39 @@ 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.
type decodeSp struct {
NvSpPr *decodeNvSpPr `xml:"nvSpPr"`
SpPr *decodeSpPr `xml:"spPr"`
// 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"`
}
// decodeSp (Non-Visual Properties for a Shape) directly maps the nvSpPr
// 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"`
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"`
}
// 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

View File

@ -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"`
@ -409,25 +424,27 @@ 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.
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.

117
xmlMetaData.go Normal file
View File

@ -0,0 +1,117 @@
// 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"`
}
// 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 {
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"`
}
// 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"`
}

View File

@ -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"`
@ -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"`
}

View File

@ -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

View File

@ -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 {

View File

@ -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"`
@ -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"`
@ -916,7 +917,7 @@ type ConditionalFormatOptions struct {
Type string
AboveAverage bool
Percent bool
Format int
Format *int
Criteria string
Value string
MinType string
@ -1005,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.