diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 96b49aba..f993cdba 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,5 +1,6 @@
name: CI
on:
+# workflow_dispatch: # <-- Add this to allow manual execution
pull_request:
branches:
- master
@@ -15,10 +16,10 @@ jobs:
fail-fast: false
matrix:
version:
- - '1.7'
- '1.8'
- '1.9'
- '1' # automatically expands to the latest stable 1.x release of Julia
+ - 'lts'
- 'pre'
- 'nightly'
os:
diff --git a/Project.toml b/Project.toml
index 31eaa8b9..6754c56c 100644
--- a/Project.toml
+++ b/Project.toml
@@ -7,21 +7,34 @@ version = "0.10.5-dev"
[deps]
Artifacts = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"
+Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
+Mmap = "a63ad114-7e13-5084-954f-fe012c677804"
+PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
+UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
+Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"
XML = "72c71f33-b9b6-44de-8c94-c961784809e2"
ZipArchives = "49080126-0e18-4c2a-b176-c102e4b3760c"
[compat]
+Colors = "0.13.0"
+Distributions = "0.25.0"
+Mmap = "1"
+PrecompileTools = "1"
+Random = "1.10.0"
Tables = "1"
+UUIDs = "1.8"
XML = "0.3.5"
ZipArchives = "2"
-julia = "1.7"
+julia = "1.8"
[extras]
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
+Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
+Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[targets]
-test = ["Test", "DataFrames"]
+test = ["Test", "Distributions", "Random", "DataFrames"]
diff --git a/README.md b/README.md
index b4cc42ce..4eb42a27 100644
--- a/README.md
+++ b/README.md
@@ -19,26 +19,70 @@
Excel file reader/writer for the Julia language.
+## Introduction
+
+**XLSX.jl** is a Julia package to read and write
+[Excel](https://products.office.com/excel) spreadsheet files.
+
+Internally, an Excel XLSX file is just a
+[Zip](https://en.wikipedia.org/wiki/Zip_(file_format)) file with a set of
+[XML](https://en.wikipedia.org/wiki/XML) files inside.
+The formats for these XML files are described in
+the [Standard ECMA-376](https://ecma-international.org/publications-and-standards/standards/ecma-376/).
+
+This package follows the EMCA-376 to parse and generate XLSX files.
+
+## Requirements
+
+* Julia v1.8
+
+* Linux, macOS or Windows.
+
## Installation
+From a Julia session, run:
+
```julia
+julia> using Pkg
+
julia> Pkg.add("XLSX")
```
-## Documentation
+## Source Code
-Package documentation is hosted at https://felipenoris.github.io/XLSX.jl/stable.
+The source code for this package is hosted at
+[https://github.com/felipenoris/XLSX.jl](https://github.com/felipenoris/XLSX.jl).
+
+## License
+
+The source code for the package **XLSX.jl** is licensed under
+the [MIT License](https://raw.githubusercontent.com/felipenoris/XLSX.jl/master/LICENSE).
+
+## Getting Help
+
+If you're having any trouble, have any questions about this package
+or want to ask for a new feature,
+just open a new [issue](https://github.com/felipenoris/XLSX.jl/issues).
+
+## Contributing
+
+Contributions are always welcome!
+
+To contribute, fork the project on [GitHub](https://github.com/felipenoris/XLSX.jl)
+and send a Pull Request.
## References
-* [ECMA Open XML White Paper](https://www.ecma-international.org/news/TC45_current_work/OpenXML%20White%20Paper.pdf)
+* [ECMA Open XML White Paper](https://www.ecma-international.org/wp-content/uploads/OpenXML-White-Paper.pdf)
-* [ECMA-376](https://www.ecma-international.org/publications/standards/Ecma-376.htm)
+* [ECMA-376](https://ecma-international.org/publications-and-standards/standards/ecma-376/)
-* [Excel file limits](https://support.office.com/en-gb/article/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3)
+* [Excel file limits](https://support.microsoft.com/en-us/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3)
## Alternative Packages
+* [LibXLSXWriter.jl](https://github.com/jaakkor2/LibXLSXWriter.jl)
+
* [ExcelFiles.jl](https://github.com/davidanthoff/ExcelFiles.jl)
* [ExcelReaders.jl](https://github.com/davidanthoff/ExcelReaders.jl)
diff --git a/data/Borders.xlsx b/data/Borders.xlsx
index 345a63e5..ac409a1d 100644
Binary files a/data/Borders.xlsx and b/data/Borders.xlsx differ
diff --git a/data/NoDim.xlsx b/data/NoDim.xlsx
new file mode 100644
index 00000000..bcd046a9
Binary files /dev/null and b/data/NoDim.xlsx differ
diff --git a/data/Template File.xltx b/data/Template File.xltx
new file mode 100644
index 00000000..a39c0599
Binary files /dev/null and b/data/Template File.xltx differ
diff --git a/data/customXml.xlsx b/data/customXml.xlsx
index 2dc7f9fa..8f521397 100644
Binary files a/data/customXml.xlsx and b/data/customXml.xlsx differ
diff --git a/data/empty_v.xlsx b/data/empty_v.xlsx
new file mode 100644
index 00000000..43b397f7
Binary files /dev/null and b/data/empty_v.xlsx differ
diff --git a/data/general.xlsx b/data/general.xlsx
index a5c92d67..96b6a300 100644
Binary files a/data/general.xlsx and b/data/general.xlsx differ
diff --git a/data/testmerge.xlsx b/data/testmerge.xlsx
new file mode 100644
index 00000000..7dcf33b7
Binary files /dev/null and b/data/testmerge.xlsx differ
diff --git a/docs/src/api.md b/docs/api.md
similarity index 77%
rename from docs/src/api.md
rename to docs/api.md
index 2a2b32bb..e32c281c 100644
--- a/docs/src/api.md
+++ b/docs/api.md
@@ -1,6 +1,9 @@
# API Reference
+```@index
+```
+
```@docs
XLSX.XLSXFile
XLSX.readxlsx
@@ -14,6 +17,8 @@ XLSX.hassheet
XLSX.Worksheet
XLSX.rename!
XLSX.addsheet!
+XLSX.copysheet!
+XLSX.deletesheet!
XLSX.readdata
XLSX.getdata
XLSX.getcell
@@ -23,17 +28,18 @@ XLSX.row_number
XLSX.column_number
XLSX.eachrow
XLSX.readtable
+XLSX.readdf
XLSX.gettable
XLSX.eachtablerow
XLSX.writetable
XLSX.writetable!
+XLSX.setConditionalFormat
XLSX.setFormat
XLSX.setUniformFormat
XLSX.setFont
XLSX.setUniformFont
XLSX.setBorder
XLSX.setUniformBorder
-XLSX.setOutsideBorder
XLSX.setFill
XLSX.setUniformFill
XLSX.setAlignment
@@ -41,4 +47,9 @@ XLSX.setUniformAlignment
XLSX.setUniformStyle
XLSX.setColumnWidth
XLSX.setRowHeight
+XLSX.getMergedCells
+XLSX.isMergedCell
+XLSX.getMergedBaseCell
+XLSX.mergeCells
+XLSX.addDefinedName
```
diff --git a/docs/make.jl b/docs/make.jl
index 5dcb4525..f62c794d 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -7,9 +7,19 @@ makedocs(
pages = [
"Home" => "index.md",
"Tutorial" => "tutorial.md",
- "API Reference" => "api.md",
- "Migration Guides" => "migration.md",
- ],
+ "Formatting Guide" => Any[
+ "Cell formats" => "formatting/cellFormatting.md",
+ "Conditional formats" => "formatting/conditionalFormatting.md",
+ "Working with merged cells" => "formatting/mergedCells.md",
+ "Examples" => "formatting/examples.md"
+ ],
+ "Migration Guide" => "migration.md",
+ "API Reference" => Any[
+ "Files and worksheets" => "api/files.md",
+ "Cells and data" => "api/data.md",
+ "Formats" => "api/formats.md",
+ ]
+ ],
checkdocs=:none,
)
diff --git a/docs/src/api/data.md b/docs/src/api/data.md
new file mode 100644
index 00000000..c3639141
--- /dev/null
+++ b/docs/src/api/data.md
@@ -0,0 +1,31 @@
+# Cells and data
+
+## Cell referencing
+
+```@docs
+XLSX.CellRef
+XLSX.row_number
+XLSX.column_number
+XLSX.eachrow
+XLSX.eachtablerow
+```
+
+## Cell data
+
+```@docs
+XLSX.readdata
+XLSX.getdata
+XLSX.getcell
+XLSX.getcellrange
+XLSX.gettable
+XLSX.readtable
+XLSX.readto
+XLSX.writetable
+XLSX.writetable!
+```
+
+## Defined names
+
+```@docs
+XLSX.addDefinedName
+```
diff --git a/docs/src/api/files.md b/docs/src/api/files.md
new file mode 100644
index 00000000..d6e61fc6
--- /dev/null
+++ b/docs/src/api/files.md
@@ -0,0 +1,26 @@
+# Files and worksheets
+
+## Files
+
+```@docs
+XLSX.XLSXFile
+XLSX.readxlsx
+XLSX.openxlsx
+XLSX.opentemplate
+XLSX.newxlsx
+XLSX.writexlsx
+XLSX.savexlsx
+```
+
+## Worksheets
+
+```@docs
+XLSX.Worksheet
+XLSX.sheetnames
+XLSX.sheetcount
+XLSX.hassheet
+XLSX.rename!
+XLSX.addsheet!
+XLSX.copysheet!
+XLSX.deletesheet!
+```
diff --git a/docs/src/api/formats.md b/docs/src/api/formats.md
new file mode 100644
index 00000000..17adef9a
--- /dev/null
+++ b/docs/src/api/formats.md
@@ -0,0 +1,42 @@
+# Cells and data
+
+## Cell format
+
+```@docs
+XLSX.setFormat
+XLSX.setUniformFormat
+XLSX.setFont
+XLSX.setUniformFont
+XLSX.setBorder
+XLSX.setUniformBorder
+XLSX.setFill
+XLSX.setUniformFill
+XLSX.setAlignment
+XLSX.setUniformAlignment
+XLSX.setUniformStyle
+```
+
+## Conditional format
+
+```@docs
+XLSX.getConditionalFormats
+XLSX.setConditionalFormat
+```
+
+## Column width and row height
+
+```@docs
+XLSX.getColumnWidth
+XLSX.setColumnWidth
+XLSX.getRowHeight
+XLSX.setRowHeight
+```
+
+# Merged Cells
+
+```@docs
+XLSX.getMergedCells
+XLSX.isMergedCell
+XLSX.getMergedBaseCell
+XLSX.mergeCells
+```
diff --git a/docs/src/formatting/cellFormatting.md b/docs/src/formatting/cellFormatting.md
new file mode 100644
index 00000000..12ead670
--- /dev/null
+++ b/docs/src/formatting/cellFormatting.md
@@ -0,0 +1,316 @@
+
+# Cell formats
+
+## Excel formatting
+
+Each cell in an Excel spreadsheet may refer to an Excel `style`. Multiple cells can
+refer to the same `style` and therefore have a uniform appearance. A `style` defines
+the cell's `alignment` directly (as part of the `style` definition), but it may also
+refer to further formatting definitions for `font`, `fill`, `border`, `format`.
+Multiple `style`s may each refer to the same `fill` definition or the same `font`
+definition, etc, and therefore share these formatting characteristics.
+This hierarchy can be shown like this:
+```
+ `Cell`
+ │
+ `Style` => `Alignment`
+ │
+ ┌──────────┬────┴─────┬─────────┐
+ │ │ │ │
+`font` `fill` `border` `format`
+```
+A family of setter functions is provided to set each of the formatting characteristics
+Excel uses. These are applied to cells, and the functions deal with the relationships
+between the individual characteristics, the overarching `style` and the cell(s) themselves.
+
+## Setting format attributes of a cell
+
+Set the font attributes of a cell using [`XLSX.setFont`](@ref). For example, to set cells `A1` and
+`A5` in the `general` sheet of a workbook to specific `font` values, use:
+
+```julia
+
+julia> using XLSX
+
+julia> f=XLSX.opentemplate("general.xlsx")
+XLSXFile("general.xlsx") containing 13 Worksheets
+ sheetname size range
+-------------------------------------------------
+ general 10x6 A1:F10
+ table3 5x6 A2:F6
+ table4 4x3 E12:G15
+ table 12x8 A2:H13
+ table2 5x3 A1:C5
+ empty 1x1 A1:A1
+ table5 6x1 C3:C8
+ table6 8x2 B1:C8
+ table7 7x2 B2:C8
+ lookup 4x9 B2:J5
+ header_error 3x4 B2:E4
+ named_ranges_2 4x5 A1:E4
+ named_ranges 14x6 A2:F15
+
+julia> s=f["general"]
+10×6 XLSX.Worksheet: ["general"](A1:F10)
+
+julia> XLSX.setFont(s, "A1"; name="Arial", size=24, color="blue", bold=true)
+2
+
+julia> XLSX.setFont(s, "A5"; name="Arial", size=24, color="blue", bold=true)
+2
+```
+
+The function returns the `fontId` that has been used to define this combination
+of attributes.
+
+There are more `font` attributes that can be set. Setting attributes for a cell
+that already has some, merges the new attributes with the old. Thus:
+
+```julia
+julia> XLSX.setFont(s, "A5"; italic=true, under="double", bold=false)
+3
+```
+
+will over-ride the `bold` setting that was previously defined and add a double
+underline and make the font italic. However, the color, font name and size will
+all remain unchanged from before. This new combination of attributes is unique,
+so a new `fontId` has been created.
+
+Font colors (and colors in any of the other formatting functions) can be set using a
+hex RGB value or by name using any of the colors provided by [Colors.jl](https://juliagraphics.github.io/Colors.jl/stable/namedcolors/)
+
+The other set attribute functions behave in similar ways. See [`XLSX.setBorder`](@ref),
+[`XLSX.setFill`](@ref), [`XLSX.setFormat`](@ref) and [`XLSX.setAlignment`](@ref).
+
+## Formatting multiple cells at once
+
+### Applying `setAttribute` to multiple cells
+
+Each of the setter functions can be applied to multiple cells at once using cell-ranges,
+row- or column-ranges or non-contiguous ranges. Additionally, indexing can use integer
+indices for rows and columns, vectors of index values, unit- or step-ranges. This makes
+it easy to apply formatting to many cells at once.
+
+Thus, for example:
+
+```julia
+
+julia> using XLSX
+
+julia> f=XLSX.newxlsx()
+XLSXFile("C:\...\blank.xlsx") containing 1 Worksheet
+ sheetname size range
+-------------------------------------------------
+ Sheet1 1x1 A1:A1
+
+julia> s=f[1]
+1×1 XLSX.Worksheet: ["Sheet1"](A1:A1)
+
+julia> s[1:100, 1:100] = "" # Ensure these aren't `EmptyCell`s.
+""
+
+julia> XLSX.setFont(s, "A1:CV100"; name="Arial", size=24, color="blue", bold=true)
+-1 # Returns -1 on a range.
+
+julia> XLSX.setBorder(s, "A1:CV100"; allsides = ["style" => "thin", "color" => "black"])
+-1
+
+julia> XLSX.setAlignment(s, [10, 50, 90], 1:100; wrapText=true) # Wrap text in the specified rows.
+-1
+
+julia> XLSX.setAlignment(s, 1:100, 2:2:100; rotation=90) # Rotate text 90° every second column in the first 100 rows.
+-1
+```
+
+It is even possible to use defined names to index these functions:
+
+```julia
+
+julia> XLSX.addDefinedName(s, "my_name", "A1,B20,C30") # Define a non-contiguous named range.
+XLSX.DefinedNameValue(Sheet1!A1,Sheet1!B20,Sheet1!C30, Bool[1, 1, 1])
+
+julia> XLSX.setFill(s, "my_name"; pattern="solid", fgColor="coral")
+-1
+```
+
+When setting format attributes over a range of cells as decribed, the new attributes are merged
+with existing on a cell by cell basis. If you set the font name on a range of cells that previously
+all had different font colors, the color differences will persist even as the font name is applied
+to the range consistently.
+
+### Setting uniform attributes
+
+Sometimes it is useful to be able to apply a fully consistent set of format attributes to a range of
+cells, over-riding any pre-existing differences. This is the purpose of the `setUniformAttribute`
+family of functions. These functions update the attributes of the first cell in the range and then
+apply the relevant attribute Id to the rest of the cells in the range. Thus:
+
+```julia
+julia> XLSX.setUniformBorder(s, "A1:CV100"; allsides = ["color" => "green"], diagonal = ["direction"=>"both", "color"=>"red"])
+2 # This is the `borderId` that has now been uniformly applied to every cell.
+```
+
+This sets the border color in cell `A1` to be green and adds red diagonal lines across the cell.
+It then applies all the `Border` attributes of cell `A1` uniformly to all the other cells in the range,
+overriding their previous attributes.
+
+All the format setter functions have `setUniformAttribute` versions, too. See [`XLSX.setUniformBorder`](@ref),
+[`XLSX.setUniformFill`](@ref), [`XLSX.setUniformFormat`](@ref) and [`XLSX.setUniformAlignment`](@ref).
+
+### Setting uniform styles
+
+It is possible to use each of the `setUniformAttribute` functions in turn to ensure every possible
+attribute is consistently applied to a range of cells. However, if perfect uniformity is required,
+then `setUniformStyle` is considerably more efficient. It will simply take the `styleId` of the
+first cell in the range and apply it uniformly to each cell in the range. This ensures that all
+of font, fill, border, format, and alignment are all completely consistent across the range:
+
+```julia
+julia> XLSX.setUniformStyle(s, "A1:CV100") # set all formatting attributes to be uniformly tha same as cell A1.
+7 # this is the `styleId` that has now been applied to all cells in the range
+```
+
+### Illustrating the different approaches
+
+To illustrate the differences between applying `setAttribute`, `setUniformAttribute` and `setUinformStyle`,
+consider the following worksheet, which has very hetrogeneous formatting across the three cells:
+
+
+
+We can apply `setBorder()` to add a top border to each cell:
+
+```julia
+julia> XLSX.setBorder(s, "B2,D2,F2"; top=["style"=>"thick", "color"=>"red"])
+-1
+```
+This merges the new top border definition with the other, existing border attributes, to get
+
+
+
+Alternatively, we can apply `setUniformBorder()`, which will update the borders of cell `B2`
+and then apply all the border attributes of `B2` to the other cells, overwriting the previous
+settings:
+
+```julia
+julia> XLSX.setUniformBorder(s, "B2,D2,F2"; top=["style"=>"thick", "color"=>"red"])
+4
+```
+
+This makes the border formatting entirely consistent across the cells but leaves the other formatting
+attributes (font, fill, format, alignment) as they were.
+
+
+
+Finally, we can set `B2` to have the formatting we want, and then apply a uniform style to all three cells.
+
+```julia
+julia> XLSX.setBorder(s, "B2"; top=["style"=>"thick", "color"=>"red"])
+4
+
+julia> XLSX.setUniformStyle(s, "B2,D2,F2")
+19
+```
+Which results in all formatting attributes being entirely consistent across the cells.
+
+
+
+### Performance differences between methods
+
+To illustrtate the relative performance of these three methods, applied to a million cells:
+```julia
+using XLSX
+function setup()
+ f = XLSX.newxlsx()
+ s = f[1]
+ s[1:1000, 1:1000] = pi
+ return f
+end
+do_format(f) = XLSX.setFormat(f[1], 1:1000, 1:1000; format="0.0000")
+do_uniform_format(f) = XLSX.setUniformFormat(f[1], 1:1000, 1:1000; format="0.0000")
+function do_format_styles(f)
+ XLSX.setFormat(f[1], "A1"; format="0.0000")
+ XLSX.setUniformStyle(f[1], 1:1000, 1:1000)
+end
+function timeit()
+ f = setup()
+ do_format(f)
+ do_uniform_format(f)
+ do_format_styles(f)
+ f = setup()
+ print("Using `setFormat` : ")
+ @time do_format(f)
+ f = setup()
+ print("Using `setUniformFormat` : ")
+ @time do_uniform_format(f)
+ f = setup()
+ print("Using `setUniformStyle` : ")
+ @time do_format_styles(f)
+ return f
+end
+f=timeit()
+```
+
+which yields the following timings:
+
+```
+Using `setFormat` : 10.966803 seconds (256.00 M allocations: 19.771 GiB, 18.81% gc time)
+Using `setUniformFormat` : 2.222868 seconds (31.00 M allocations: 1.137 GiB, 19.48% gc time)
+Using `setUniformStyles` : 0.519658 seconds (14.00 M allocations: 416.587 MiB)
+```
+
+The same test, using the more involved `setBorder` function
+
+```julia
+do_format(f) = XLSX.setBorder(f[1], 1:1000, 1:1000;
+ left = ["style" => "dotted", "color" => "FF000FF0"],
+ right = ["style" => "medium", "color" => "firebrick2"],
+ top = ["style" => "thick", "color" => "FF230000"],
+ bottom = ["style" => "medium", "color" => "goldenrod3"],
+ diagonal = ["style" => "dotted", "color" => "FF00D4D4", "direction" => "both"]
+ )
+```
+
+gives
+
+```
+Using `setBorder` : 29.536010 seconds (759.00 M allocations: 64.286 GiB, 22.01% gc time)
+Using `setUniformBorder` : 2.052018 seconds (31.00 M allocations: 1.197 GiB, 13.18% gc time)
+Using `setUniformStyles` : 0.599491 seconds (14.00 M allocations: 416.586 MiB, 15.20% gc time)
+```
+
+If maintaining heterogeneous formatting attributes is not important, it is more efficient to
+apply `setUinformAttribute` functions rather than `setAttribute` functions, especially on large
+cell ranges, and more efficient still to use `setUniformStyle`.
+
+## Copying formatting attributes
+
+It is possible to use non-contiguous ranges to copy format attributes from any cell to any other cells,
+whether you are also updating the source cell's format or not.
+
+```julia
+julia> XLSX.setBorder(s, "BB50"; allsides = ["style" => "medium", "color" => "yellow"])
+3 # Cell BB50 now has the border format I want!
+
+julia> XLSX.setUniformBorder(s, "BB50,A1:CV100") # Make cell BB50 the first (reference) cell in a non-contiguous range.
+3
+
+julia> XLSX.setUniformStyle(s, "BB50,A1:CV100") # Or if I want to apply all formatting attributes from BB50 to the range.
+11
+```
+
+## Setting column width and row height
+
+Two functions offer the ability to set the column width and row height within a worksheet. These can use
+all of the indexing options described above. For example:
+
+```julia
+julia> XLSX.setRowHeight(s, "A2:A5"; height=25) # Rows 1 to 5 (columns ignored)
+
+julia> XLSX.setColumnWidth(s, 5:5:100; width=50) # Every 5th column.
+```
+
+Excel applies some padding to user specified widths and heights. The two functions described here attempt
+to do something similar but it is not an exact match to what Excel does. User specified row heights and
+column widths will therefore differ by a small amount from the values you would see setting the same
+widths in Excel itself.
+
diff --git a/docs/src/formatting/conditionalFormatting.md b/docs/src/formatting/conditionalFormatting.md
new file mode 100644
index 00000000..96d18877
--- /dev/null
+++ b/docs/src/formatting/conditionalFormatting.md
@@ -0,0 +1,1439 @@
+
+# Conditional formats
+
+## Applying conditional formats
+
+In Excel, a conditional format is a format that is applied if the content of a cell meets some criterion
+but not otherwise. Such conditional formatting is generally straightforward to apply using the
+`setAttribute()` functions or the `setConditionalFormat()` function described here.
+
+!!! note
+
+ In Excel, conditional formats are dynamic. If the cell values change, the formats are updated based
+ on application of the condition to the new values.
+
+ The examples of conditional formatting given here a mix of static and dynamic formats.
+
+ Static conditional formats apply formatting based on the current cell values at the time the format
+ is set, but the formats are then static regardless of updates to cell values. They can be updated
+ by re-running the conditional formatting functions described but otherwise remain unchanged. Static
+ formats are created by applying the `setAttribute()` functions described above.
+
+ Dynamic conditional formatting, using the native Excel conditional format functionality, is possible
+ using the `setConditionalFormat()` function, giving access to all of Excel's options.
+
+### Static conditional formats
+
+As an example, a simple function to set true values in a range to use a bold green font color and
+false values to use a bold red color a could be defined as follows:
+
+```julia
+function trueorfalse(sheet, rng) # Use green or red font for true or false respectively
+ for c in rng
+ if !ismissing(sheet[c]) && sheet[c] isa Bool
+ XLSX.setFont(sheet, c, bold=true, color = sheet[c] ? "FF548235" : "FFC00000")
+ end
+ end
+end
+```
+
+Applying this function over any range will conditionally color cells green or red if they are
+true or false respectively:
+
+```julia
+trueorfalse(sheet, XLSX.CellRange("E3:L6"))
+```
+
+Similarly, a function can be defined to fill any cells containing missing values to be filled with a grey
+color and have diagonal borders applied:
+
+```julia
+function blankmissing(sheet, rng) # Fill with grey and apply both diagonal borders on cells
+ for c in rng # with missing values
+ if ismissing(sheet[c])
+ XLSX.setFill(sheet, c; pattern = "solid", fgColor = "lightgrey")
+ XLSX.setBorder(sheet, c; diagonal = ["style" => "thin", "color" => "black"])
+ end
+ end
+end
+```
+
+This can then be applied to a range of cells to conditionally apply the format:
+
+```julia
+blankmissing(sheet, XLSX.CellRange("B3:L6"))
+```
+
+### Dynamic conditional formats
+
+XLSX.jl provides a function to create native Excel conditional formats that will be saved
+as part of an `XLSXFile` and which will update dynamically if the values in the cell range
+to which the formatting is applied are subsequently updated.
+
+`XLSX.setConditionalFormat(sheet, CellRange, :type; kwargs...)`
+
+Excel uses a range of `:type` values to describe these conditional formats and the same values
+are used here, as follows:
+- `:cellIs`
+- `:top10`
+- `:aboveAverage`
+- `:containsText`
+- `:notContainsText`
+- `:beginsWith`
+- `:endsWith`
+- `:timePeriod`
+- `:containsErrors`
+- `:notContainsErrors`
+- `:containsBlanks`
+- `:notContainsBlanks`
+- `:uniqueValues`
+- `:duplicateValues`
+- `:expression`
+- `:dataBar`
+- `:colorScale`
+- `:iconSet`
+
+Use of these different `:type`s is illustrated in the following sections.
+For more details on the range of `:type` values and their associated keyword
+options, refer to [XLSX.setConditionalFormat()](@ref).
+
+#### Cell Value
+
+It is possible to format each cell in a range when the cell's value meets a specified condition using one
+of a number of built-in cell format options or using custom formatting. This group of formatting options
+represents the greatest range of conditional formatting options available in Excel and the most often
+used. All the functions of `Highlight Cells Rules` and `Top/Bottom Rules` are provided.
+
+  
+
+The following `:type` values are used to set conditional formats by making direct comparisons to a cell's value:
+- `:cellIs`
+- `:top10`
+- `:aboveAverage`
+- `:containsText`
+- `:notContainsText`
+- `:beginsWith`
+- `:endsWith`
+- `:timePeriod`
+- `:containsErrors`
+- `:notContainsErrors`
+- `:containsBlanks`
+- `:notContainsBlanks`
+- `:uniqueValues`
+- `:duplicateValues`
+
+Each of these formatting types needs a set of keyword options to fully define its operation.
+This can be exemplified by considering the `:cellIs` type. Like the other conditional formats
+in this group, `:cellIs` needs an `operator` keyword to define the test to make to determine
+whether or not to apply the formatting. Valid `operator` values for `:cellIs` are:
+
+- `greaterThan` (cell > `value`)
+- `greaterEqual` (cell >= `value`)
+- `lessThan` (cell < `value`)
+- `lessEqual` (cell <= `value`)
+- `equal` (cell == `value`)
+- `notEqual` (cell != `value`)
+- `between` (cell between `value` and `value2`)
+- `notBetween` (cell not between `value` and `value2`)
+
+Each of these need the keyword `value` to be specified and, for `between` and `notBetween`, `value2`
+must also be specified.
+
+Like all the cell value formatting types, `:cellIs` can use one of six built-in Excel formats, as
+illustrated here for the `greaterThan` comparison.
+
+
+
+These six built-in formatting options are available by name in XLSX.jl by specifying the `dxStyle`
+keyword with one of the following values:
+* `redfilltext`
+* `yellowfilltext`
+* `greenfilltext`
+* `redfill`
+* `redtext`
+* `redborder`
+
+Thus, for example, to create a simple `XLSXFile` from scratch and then apply some
+`:cellIs` conditional formats to its cells:
+
+```julia
+julia> columns = [ [1, 2, 3, 4], ["Hey", "You", "Out", "There"], [10.2, 20.3, 30.4, 40.5] ]
+3-element Vector{Vector}:
+ [1, 2, 3, 4]
+ ["Hey", "You", "Out", "There"]
+ [10.2, 20.3, 30.4, 40.5]
+
+julia> colnames = [ "integers", "strings", "floats" ]
+3-element Vector{String}:
+ "integers"
+ "strings"
+ "floats"
+
+julia> f=XLSX.newxlsx()
+XLSXFile("C:\...\blank.xlsx") containing 1 Worksheet
+ sheetname size range
+-------------------------------------------------
+ Sheet1 1x1 A1:A1
+
+julia> s=f[1]
+1×1 XLSX.Worksheet: ["Sheet1"](A1:A1)
+
+julia> XLSX.writetable!(s, columns, colnames)
+
+julia> s[1:5, 1:3]
+5×3 Matrix{Any}:
+ "integers" "strings" "floats"
+ 1 "Hey" 10.2
+ 2 "You" 20.3
+ 3 "Out" 30.4
+ 4 "There" 40.5
+
+julia> XLSX.setConditionalFormat(s, "A2:A5", :cellIs; # Cells with a value > 2 to have red text and light red fill.
+ operator="greaterThan",
+ value="2",
+ dxStyle="redfilltext")
+0
+
+julia> XLSX.setConditionalFormat(s, "B2:B5", :containsText; # Cells with text containing "u" to have green text and light green fill.
+ value="u",
+ dxStyle="greenfilltext")
+0
+
+julia> XLSX.setConditionalFormat(s, "C2:C5", :top10; # Cells with values in the top 10% of values in the range to have a red border.
+ operator ="topN%",
+ value="10"
+ dxStyle="redborder")
+0
+
+```
+
+
+
+Alternatively, it is possible to specify custom format options to match the options offered in Excel
+under the `Custom Format...` option:
+
+
+
+!!! note
+
+ In the image above, the font name and size selectors are greyed out. Excel limits
+ the formatting attributes that can be set in a conditional format. It is not
+ possible to set the size or name of a font and neither is it possible to set any
+ of the cell alignment attributes. Diagonal borders cannot be set either.
+
+ Although it is not a limitation of Excel, for simplicity this function sets all the
+ border attributes for each side of a cell to be the same.
+
+For example, starting with the same simple `XLSXFile` as above, we can apply the following custom formats:
+
+```julia
+julia> XLSX.setConditionalFormat(s, "A2:A5", :cellIs;
+ operator="greaterThan",
+ value="2",
+ font=["color" => "coral", "bold"=>"true"],
+ fill=["pattern"=>"solid", "bgColor"=>"cornsilk"],
+ border=["style"=>"dashed", "color"=>"orangered4"],
+ format=["format"=>"0.000"])
+0
+
+julia> XLSX.setConditionalFormat(s, "B2:B5", :containsText;
+ value="u",
+ font=["color" => "steelblue4", "italic"=>"true"],
+ fill=["pattern"=>"darkTrellis", "fgColor"=>"lawngreen", "bgColor"=>"cornsilk"],
+ border=["style"=>"double", "color"=>"magenta3"])
+0
+
+julia> XLSX.setConditionalFormat(s, "C2:C5", :top10;
+ operator ="topN%",
+ value="10",
+ font=["color" => "magenta3", "strike"=>"true"],
+ fill=["pattern"=>"lightVertical", "fgColor"=>"lawngreen", "bgColor"=>"cornsilk"],
+ border=["style"=>"double", "color"=>"cyan"])
+0
+
+julia> XLSX.getConditionalFormats(s)
+3-element Vector{Pair{XLSX.CellRange, NamedTuple}}:
+ C2:C5 => (type = "top10", priority = 3)
+ B2:B5 => (type = "containsText", priority = 2)
+ A2:A5 => (type = "cellIs", priority = 1)
+
+```
+
+
+
+Each of the conditional format `type`s in the cell value group take similar keyword options but
+the specific details vary for each. For more details, refer to [XLSX.setConditionalFormat()](@ref).
+
+#### Expressions
+
+It is possible to use an Excel formula directly to determine whether to apply a conditional format.
+Any expression that evaluates to true or false can be used.
+
+
+
+For example, to compare one column with another and apply a conditional format accordingly:
+
+```julia
+julia> f=XLSX.newxlsx()
+XLSXFile("C:\...\blank.xlsx") containing 1 Worksheet
+ sheetname size range
+-------------------------------------------------
+ Sheet1 1x1 A1:A1
+
+julia> s=f[1]
+1×1 XLSX.Worksheet: ["Sheet1"](A1:A1)
+
+julia> XLSX.writetable!(s, [rand(10), rand(10), rand(10), rand(10)], ["col1", "col2", "col3", "col4"])
+
+julia> s[:]
+11×4 Matrix{Any}:
+ "col1" "col2" "col3" "col4"
+ 0.810579 0.13742 0.0146856 0.654739
+ 0.169043 0.623955 0.713874 0.103253
+ 0.198619 0.19622 0.0818595 0.863316
+ 0.353214 0.0949461 0.961917 0.812889
+ 0.343781 0.0957323 0.061183 0.822921
+ 0.34115 0.243949 0.527914 0.758945
+ 0.161748 0.744446 0.119521 0.52732
+ 0.39707 0.284588 0.501409 0.374944
+ 0.327938 0.191197 0.943983 0.755799
+ 0.0314949 0.560541 0.526068 0.45253
+
+julia> XLSX.setConditionalFormat(s, "A2:A10", :expression; formula = "A2>B2", dxStyle = "redfilltext")
+0
+
+julia> XLSX.setConditionalFormat(s, "C2:D10", :expression; formula = "C2>\$B2", dxStyle = "greenfilltext")
+0
+```
+
+
+Column A uses relative referencing. Columns C and D use an absolute reference for the column but not the
+row of the comparison reference.
+
+The following example uses absolute references on rows and compares the average of each column with the
+average of the preceding column.
+
+```julia
+julia> f=XLSX.newxlsx()
+XLSXFile("C:\...\blank.xlsx") containing 1 Worksheet
+ sheetname size range
+-------------------------------------------------
+ Sheet1 1x1 A1:A1
+
+julia> s=f[1]
+1×1 XLSX.Worksheet: ["Sheet1"](A1:A1)
+
+julia> XLSX.writetable!(s, [rand(10).*1000, rand(10).*1000, rand(10).*1000, rand(10).*1000], ["2022", "2023", "2024", "2025"])
+
+julia> XLSX.setConditionalFormat(s, "B2:D11", :expression; formula = "average(B\$2:B\$11) > average(A\$2:A\$11)", dxStyle = "greenfilltext")
+0
+
+julia> XLSX.setConditionalFormat(s, "B2:D11", :expression; formula = "average(B\$2:B\$11) < average(A\$2:A\$11)", dxStyle = "redfilltext")
+0
+```
+
+
+(Row 13 above is the average of each column, calculated in Excel)
+
+When a formula uses relative references, the relative position (offset) of the reference to the base cell in the
+range to which the condition is applied is used consistently throughout the range.
+This is illustrated in the following example:
+
+```julia
+julia> f=XLSX.newxlsx()
+XLSXFile("C:\...\blank.xlsx") containing 1 Worksheet
+ sheetname size range
+-------------------------------------------------
+ Sheet1 1x1 A1:A1
+
+julia> s=f[1]
+1×1 XLSX.Worksheet: ["Sheet1"](A1:A1)
+
+julia> for i=1:10; for j=1:10; s[i, j] = i*j; end; end
+
+julia> XLSX.setConditionalFormat(s, "A1:E5", :expression; formula = "E5 < 50", dxStyle = "redfilltext")
+0
+```
+
+
+The format applied in cell `A1` is determined by comparison of cell `E5` to the value 50. In `B2` it is
+based on cell `F6`, in `C3`, on cell `G7` and so on throughtout the range.
+
+Text based comparisons in Excel are not case sensitive by default, but can be forced to be so:
+
+```julia
+julia> f=XLSX.newxlsx()
+XLSXFile("C:\...\blank.xlsx") containing 1 Worksheet
+ sheetname size range
+-------------------------------------------------
+ Sheet1 1x1 A1:A1
+
+julia> s=f[1]
+1×1 XLSX.Worksheet: ["Sheet1"](A1:A1)
+
+julia> s[1:3,1:3]="HELLO WORLD"
+"HELLO WORLD"
+
+julia> s["A1"] = "Hello World"
+"Hello World"
+
+julia> s["B2"] = "Hello World"
+"Hello World"
+
+julia> s["C3"] = "Hello World"
+"Hello World"
+
+julia> XLSX.setConditionalFormat(s, "A1:A3", :expression; formula = "A1=\"hello world\"", dxStyle = "redfilltext")
+0
+
+julia> XLSX.setConditionalFormat(s, "B1:B3", :expression; formula = "B1=\"HELLO WORLD\"", dxStyle = "redfilltext")
+0
+
+julia> XLSX.setConditionalFormat(s, "C1:C3", :expression; formula = "exact(\"Hello World\", C1)", dxStyle = "greenfilltext")
+0
+```
+
+
+#### Data Bar
+
+A `:dataBar` conditional format can be applied to a range of cells.
+In Excel there are twelve built-in data bars available, but it is possible
+to customise many elements of these.
+
+
+
+In XLSX.jl, the twelve built-in data bars are named as follows
+(layout follows image)
+
+| | | | |
+|:--------------:|:--------------:|:---------------:|:---------------:|
+| Gradient fill | bluegrad | greengrad | redgrad |
+| | orangegrad | lightbluegrad | purplegrad |
+| Solid fill | blue | green | red |
+| | orange | lightblue | purple |
+
+
+Choose one of these data bars by name using the `databar` keyword. If no `databar`
+is specified, `bluegrad` is the default choice. For example
+
+```julia
+julia> f=XLSX.newxlsx()
+XLSXFile("C:\...\blank.xlsx") containing 1 Worksheet
+ sheetname size range
+-------------------------------------------------
+ Sheet1 1x1 A1:A1
+
+julia> s=f[1]
+1×1 XLSX.Worksheet: ["Sheet1"](A1:A1)
+
+julia> s[1:10, 1]=1:10
+1:10
+
+julia> s[1:10, 3]=1:10
+1:10
+
+julia> XLSX.setConditionalFormat(s, "A1:A10", :dataBar) # Defaults to `databar="bluegrad"`
+0
+
+julia> XLSX.setConditionalFormat(s, "C1:C10", :dataBar; databar="orange")
+0
+
+```
+
+
+All of the options provided by Excel can be adjusted using the provided keyword options.
+
+
+
+
+
+For example, the end points of the bar scale can be defined by setting the `min_type` and `max_type`
+keywords to `num` (for an absolute number value), `percent`, `percentile`, `formula` or `min` or `max`.
+The default type is `automatic`.
+
+For the first three type options, a value must also be given by setting `min_val`, `max_val`.
+The value may be taken from a cell by setting `min_val`, `max_val` to a cell reference. When the type is
+set to `formula`, any valid formula yielding a value can be given. Cell references must use absolute referencing.
+Types `min` and `max` set the scale endpoints to be exactly the minimum and maximum values of the data in the
+cell range whereas using `automatic` allows Excel flexibility to make minor adjustments to these endpoints,
+e.g. to improve appearance.
+
+```julia
+julia> f=XLSX.newxlsx()
+XLSXFile("C:\...\blank.xlsx") containing 1 Worksheet
+ sheetname size range
+-------------------------------------------------
+ Sheet1 1x1 A1:A1
+
+julia> s=f[1]
+1×1 XLSX.Worksheet: ["Sheet1"](A1:A1)
+
+julia> s[1:10, 5]=1:10
+1:10
+
+julia> s[1:10, 1]=1:10
+1:10
+
+julia> s[1:10, 3]=1:10
+1:10
+
+julia> XLSX.setConditionalFormat(s, "A1:A10", :dataBar)
+0
+
+julia> XLSX.setConditionalFormat(s, "C1:C10", :dataBar; databar="purple", min_type="num", max_type="num", min_val="2", max_val="8")
+0
+
+julia> XLSX.setConditionalFormat(s, "E1:E10", :dataBar; databar="greengrad", min_type="percent", max_type="percent", min_val="35", max_val="65")
+0
+```
+
+
+
+Choose whether to hide values using `showVal="false"`, convert a gradient fill to solid (or vice versa)
+with `gradient="false"` (`gradient="true"`) and add borders to data bars with `borders="true"`.
+
+```julia
+julia> XLSX.setConditionalFormat(s, "A1:A10", :dataBar)
+0
+
+julia> XLSX.setConditionalFormat(s, "C1:C10", :dataBar, showVal="false", gradient="false")
+0
+
+julia> XLSX.setConditionalFormat(s, "E1:E10", :dataBar; databar=purple, borders="true", gradient="true")
+0
+```
+
+
+Change bar colors using `fill_col=` and border colors using `border_col=`. Colors are specified using an 8-digit hexadecimal as `"FFRRGGBB"` or using any named color from [Colors.jl](https://juliagraphics.github.io/Colors.jl/stable/namedcolors/).
+
+By default, negative values are shown with red bars and borders. Override these defaults by setting `sameNegFill = "true"`and `sameNegBorders="true"` to use the same colors as positive bars. Alternatively, to use any available color, set `neg_fill_col=` and `neg_border_col=`.
+
+```julia
+julia> XLSX.setConditionalFormat(s, "A1:A11", :dataBar)
+0
+
+julia> XLSX.setConditionalFormat(s, "C1:C11", :dataBar; sameNegFill="true", sameNegBorders="true")
+0
+
+julia> XLSX.setConditionalFormat(s, "E1:E11", :dataBar; fill_col="cyan", border_col="blue", neg_fill_col="lemonchiffon1", neg_border_col="goldenrod4")
+0
+
+```
+
+
+By default, Excel positions the axis automatically, based on the range of the cell data.
+Control the location of the axis using `axis_pos = "middle"` to locate it in the middle
+of the column width or `axis_pos = "none"` to remove the axis. Excel chooses the direction
+of the bars according to the context of the cell data. Force (postive) bars to go `leftToRight`
+or `rightToLeft` using the `direction` key word. Change the color of the axis with `axis_col`.
+
+```julia
+julia> s[1:10, 1]=1:10
+1:10
+
+julia> s[1:10,3]=-5:4
+-5:4
+
+julia> s[1:10,5]=1:10
+1:10
+
+julia> XLSX.setConditionalFormat(s, "A1:A10", :dataBar)
+0
+
+julia> XLSX.setConditionalFormat(s, "C1:C10", :dataBar; direction="rightToLeft", axis_pos="middle", axis_col="magenta")
+0
+
+julia> XLSX.setConditionalFormat(s, "E1:E10", :dataBar; direction="leftToRight", min_type="num", min_val="-5", axis_pos="none")
+0
+
+```
+
+
+#### Color Scale
+
+It is possible to apply a `:colorScale` formatting type to a range of cells.
+In Excel there are twelve built-in color scales available, but it is possible to create
+custom color scales, too.
+
+
+
+In XLSX.jl, the twelve built-in scales are named by their end/mid/start colors as follows
+(layout follows image)
+
+| | | | |
+|:----------------:|:----------------:|:---------------:|:---------------:|
+| greenyellowred | redyellowgreen | greenwhitered | redwhitegreen |
+| bluewhitered | redwhiteblue | whitered | redwhite |
+| greenwhite | whitegreen | greenyellow | yellowgreen |
+
+The default colorscale is `greenyellow`. To use a different built-in color scale,
+specify the name using the keyword `colorscale`, thus:
+
+```julia
+julia> XLSX.setConditionalFormat(f["Sheet1"], "A1:F12", :colorScale) # Defaults to the `greenyellow` built-in scale.
+0
+
+julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:C18", :colorScale; colorscale="whitered")
+0
+
+julia> XLSX.setConditionalFormat(f["Sheet1"], "D13:F18", :colorScale; colorscale="bluewhitered")
+0
+```
+
+A custom color scale may be defined by the colors at each end of the scale and (optionally) by some
+mid-point color, too. Colors can be specified using hex RGB values or by name using any of the colors
+in [Colors.jl](https://juliagraphics.github.io/Colors.jl/stable/namedcolors/).
+
+In Excel, the colorScale options (for a 3 color scale) look like this:
+
+
+
+The end points (and optional mid-point) can be defined using an absolute number (`num`), a `percent`,
+a `percentile` or as a `min` or `max`. For the first three options, a value must also be given.
+The value may be taken from a cell by setting `min_val`, `mid_val` or `max_val` to a cell reference.
+Thus, you can apply a custom 3-color scale using, for example:
+
+```julia
+julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:F22", :colorScale;
+ min_type="num",
+ min_val="2",
+ min_col="tomato",
+ mid_type="num",
+ mid_val="6",
+ mid_col="lawngreen",
+ max_type="num",
+ max_val="10",
+ max_col="cadetblue"
+ )
+0
+```
+
+
+#### Icon Set
+
+It is possible to apply an `:iconSet` formatting type to a range of cells.
+In Excel there are twenty built-in icon sets available, but it is possible to
+create a custom icon set from the 52 built-in icons, too.
+
+
+
+In XLSX.jl, the twenty built-in icon sets are named as follows
+(layout follows image)
+
+| | | |
+|:--------------:|:--------------:|:---------------:|
+| Directional | 3Arrows | 3ArrowsGray |
+| | 3Triangles | 4ArrowsGray |
+| | 4Arrows | 5ArrowsGray |
+| | 5Arrows | |
+| Shapes | 3TrafficLights | 3TrafficLights2 |
+| | 3Signs | 4TrafficLights |
+| | 4BlackToRed | |
+| Indicators | 3Symbols | 3Symbols2 |
+| | 3Flags | |
+| Ratings | 3Stars | 4Ratings |
+| | 5Quarters | 5Ratings |
+| | 5Boxes | |
+
+Choose one of these icon sets by name using the `iconset` keyword. If no `iconset`
+is specified, `3TrafficLights` is the default choice. For example
+
+```julia
+julia> f=XLSX.newxlsx()
+XLSXFile("C:\...\blank.xlsx") containing 1 Worksheet
+ sheetname size range
+-------------------------------------------------
+ Sheet1 1x1 A1:A1
+
+julia> s=f[1]
+1×1 XLSX.Worksheet: ["Sheet1"](A1:A1)
+
+julia> s[1:10, 1]=1:10
+1:10
+
+julia> XLSX.setConditionalFormat(s, "A1:A10", :iconSet)
+0
+```
+
+
+All of the options to control an iconSet in Excel are available. The iconSet options
+(for a 4-icon set) look like this:
+
+
+
+Each icon set includes a default set of thresholds defining which symbol to use. These
+relate the cell value to the range of values in the cell range to which the conditional
+format is being applied. This can be illustrated (for a 4-icon set) as follows:
+
+```
+ Range ┌─────────────────┬─────────────────┬─────────────────┬────────────────┐ Range
+ Minimum ->│ Icon 1 │ Icon 2 │ Icon 3 │ Icon 4 │<- Maximum
+ `min_val` `mid_val` `max_val`
+ threshold threshold threshold
+```
+The starting value for the first icon is always the minimum value of the range, and the stopping
+value for the last icon is always the maximum value in the range. No cells will have values for
+which an icon cannot be assigned. The internal thresholds for transition from one icon to the
+next are defined (in a 3-icon set) by `min_val` and `max_val`. In a 4-icon set, an additional
+threshold, `mid-val`, is required and in a 5-icon set, `mid2_val` is needed as well.
+
+The type of these thresholds can be defined in terms of `percent` (of the range), `percentile`
+or simply with a `num` (number) (e.g. as `min_type="percent"`). For each threshold,
+the value can either be given as a number (as a String) or as a simple cell reference.
+Alternatively, specifying the type as `formula` allows the value to be determined by any
+valid Excel formula.
+
+!!! note
+
+ Cell references used to define threshold values in an iconSet MUST always be given as absolute
+ cell references (e.g. `"\$A\$4"`). Relative references should not be used.
+
+Using the example above, change both the type and value of the thresholds like this:
+
+```julia
+julia> XLSX.setConditionalFormat(s, "A1:A10", :iconSet;
+ min_type="num", max_type="num",
+ min_val="2", max_val="9")
+0
+```
+
+
+To suppress the values in cells and just show the icons, use `showVal="false"`, to reverse the icon ordering
+use `reverse="true"` and to change the default comparison from `>=` to `>` set `min_gte="false"` (and
+equivalent for mid, mid2 and max):
+```julia
+julia> XLSX.writetable!(s, [collect(1:10), collect(1:10), collect(1:10), collect(1:10)],
+ ["normal", "showVal=\"false\"", "reverse=\"true\"", "min_gte=\"false\""])
+
+julia> XLSX.setConditionalFormat(s, "A2:A11", :iconSet;
+ min_type="num", max_type="num",
+ min_val="3", max_val="8")
+0
+
+julia> XLSX.setConditionalFormat(s, "B2:B11", :iconSet;
+ min_type="num", max_type="num",
+ min_val="3", max_val="8",
+ showVal="false")
+0
+
+julia> XLSX.setConditionalFormat(s, "C2:C11", :iconSet;
+ min_type="num", max_type="num",
+ min_val="3", max_val="8",
+ reverse="true")
+0
+
+julia> XLSX.setConditionalFormat(s, "D2:D11", :iconSet;
+ min_type="num", max_type="num",
+ min_val="3", max_val="8",
+ min_gte="false", max_gte="false")
+0
+```
+
+
+
+Create a custom icon set by specifying `iconset="Custom"`. The icons to use in the custom set are
+defined with `icon_list` keyword, which takes a vector of integers defining which of the 52 built
+in icons to use. Use of the val and type keywords dictate the number of icons to use. If `mid_type`
+and `mid_val` are both defined, but not `mid2_val` or `mid2_type`, then a 4-icon set will be used.
+If both sets of keywords are defined, a 5-icon set is used and if neither is set, a 3-icon set will
+be used.
+
+This is illustrated with code below, which produces a key defining which integer to use
+in `icon_list` to represent any desired icon:
+```julia
+using XLSX
+f=XLSX.newxlsx()
+s=f[1]
+for i = 0:3
+ for j=1:13
+ s[i+1,j]=i*13+j
+ end
+end
+for j=1:13
+ XLSX.setConditionalFormat(s, 1:4, j, :iconSet; # Create a custom 4-icon set in each column.
+ iconset="Custom",
+ icon_list=[j, 13+j, 26+j, 39+j],
+ min_type="percent", mid_type="percent", max_type="percent",
+ min_val="25", mid_val="50", max_val="75"
+ )
+end
+XLSX.setColumnWidth(s, 1:13, width=6.4)
+XLSX.setRowHeight(s, 1:4, height=27.75)
+XLSX.setAlignment(s, "A1:M4", horizontal="center", vertical="center")
+XLSX.setBorder(s, "A1:M4", allsides = ["style"=>"thin","color"=>"black"])
+XLSX.writexlsx("iconKey.xlsx", f, overwrite=true)
+```
+
+
+Specifying too few icons in `icon_list` throws an error while any extra will simply be ignored.
+
+#### Specifying cell references in Conditional Formats
+
+##### Cell Ranges
+
+Cell ranges for conditional formats are always absolute refences. The specified range to which a
+conditional format is to be applied is always treated as an absolute cell references so that,
+for example
+```julia
+julia> XLSX.setConditionalFormat(s, "A2:C5", :colorScale; colorscale="greenyellow")
+```
+will be converted automatically to the range "\$A\$2:\$C\$5" by Excel itself. There is therefore no need to specify
+absolute cell ranges when calling `setCondtionalFormat()`
+
+##### Relative and absolute cell references
+
+Cell references used to specify `value` or `value2` or in any `formula` (for `:expression` type
+conditional formats only) may be either absolute or relative. As in Excel, an absolute reference
+is defined using a `$` prefix to either or both the row or the column part of the cell reference
+but here the `$` must be appropriately escaped. Thus:
+
+```julia
+value = "B2" # relative reference
+value = "\$B\$2" # (escaped) absolute reference
+```
+
+The cell used in a comparison is adjusted for each cell in the range if a relative reference is used. This is
+illustrated in the following example. Cells in column A are referenced to column B using a relative reference,
+meaning `A2` is compared with `B2` but `A3` is compared with `B3` and so on until `A5` is compared with `B5`.
+In contrast, column B is referenced to cell `C2` using an absolute reference. Each cell in column B is compared
+with cell `C2`.
+
+```julia
+julia> f=XLSX.newxlsx()
+XLSXFile("C:\...\blank.xlsx") containing 1 Worksheet
+ sheetname size range
+-------------------------------------------------
+ Sheet1 1x1 A1:A1
+
+julia> s=f[1]
+1×1 XLSX.Worksheet: ["Sheet1"](A1:A1)
+
+julia> col1=rand(5)
+5-element Vector{Float64}:
+ 0.6283728884101448
+ 0.7516580026008692
+ 0.2738854683970795
+ 0.13517788102005834
+ 0.4659468387663539
+
+julia> col2=rand(5)
+5-element Vector{Float64}:
+ 0.7582186445697804
+ 0.739539172599636
+ 0.4389109821689414
+ 0.14156225872248773
+ 0.10715394525726485
+
+julia> XLSX.writetable!(s, [col1, col2],["col1", "col2"])
+
+julia> s["C2"]=0.5
+0.5
+
+julia> s[:]
+6×3 Matrix{Any}:
+ "col1" "col2" missing
+ 0.628373 0.758219 0.5
+ 0.751658 0.739539 missing
+ 0.273885 0.438911 missing
+ 0.135178 0.141562 missing
+ 0.465947 0.107154 missing
+
+julia> XLSX.setConditionalFormat(s, "A2:A6", :cellIs; operator="greaterThan", value="B2", dxStyle="redfilltext")
+0
+
+julia> XLSX.setConditionalFormat(s, "B2:B6", :cellIs; operator="greaterThan", value="\$C\$2", dxStyle="greenfilltext")
+0
+
+```
+
+
+!!! note
+
+ It is not possible to use relative cell references in conditional format types `:dataBar`,
+ `:colorScale` or `:iconSet`.
+
+!!! note
+
+ Excel permits cell references to cells in other sheets for comparisons in conditional formats
+ (e.g. "OtherSheet!A1"), but this is handled differently internally than references within the
+ same sheet. This functionality is not universally implemented in XLSX.jl yet.
+
+#### Overlaying conditional formats
+
+It is possible to overlay multiple conditional formats over each other in a
+cell range or even in different, overlapping cell ranges. Starting with a table of
+integers, we can apply three different conditional formats sequentially. Excel applies
+these in priority order (priority 1 is higher priority than priority 2) which is the
+same as the order in which they were defined with `setConditionalFormat`.
+
+```julia
+julia> s[1:5, 1:3]
+5×3 Matrix{Any}:
+ "first" "middle" "last"
+ 1 15 9
+ 12 6 10
+ 3 17 11
+ 14 8 2
+
+julia> XLSX.setConditionalFormat(f["Sheet1"], "A2:C5", :colorScale; colorscale="greenyellowred")
+0
+
+julia> XLSX.setConditionalFormat(s, "A2:C5", :top10;
+ operator ="topN",
+ value="3",
+ font=["color"=>"magenta3", "strike"=>"true"],
+ fill=["pattern"=>"lightVertical", "fgColor"=>"lawngreen", "bgColor"=>"cornsilk"],
+ border=["style"=>"double", "color"=>"cyan"])
+0
+
+julia> XLSX.setConditionalFormat(s, "A2:A5", :cellIs;
+ operator="lessThan",
+ value="2",
+ font=["color"=>"coral", "bold"=>"true"],
+ fill=["pattern"=>"lightHorizontal", "fgColor"=>"cornsilk"],
+ border=["style"=>"dashed", "color"=>"orangered4"])
+0
+
+julia> XLSX.getConditionalFormats(s)
+3-element Vector{Pair{XLSX.CellRange, NamedTuple}}:
+ A2:A5 => (type = "cellIs", priority = 3)
+ A2:C5 => (type = "colorScale", priority = 1)
+ A2:C5 => (type = "top10", priority = 2)
+
+```
+
+
+
+When applying multiple overlayed formats, it is possible to make the formatting stop if any cell meets
+one of the conditions, so that lower proirity conditional formats are not applied to that cell. This is
+achieved with the `stopIfTrue` keyword. It is not possible to apply `stopIfTrue` to `:dataBar`,
+`:colorScale` or `:iconSet` types.
+
+The example below illustrates how `stopIfTrue` is used to stop further conditional formats from being
+applied to cells to which red borders are applied:
+
+```julia
+julia> s[1:5, 1:3]
+5×3 Matrix{Any}:
+ "first" "middle" "last"
+ 1 15 9
+ 12 6 10
+ 3 17 11
+ 14 8 2
+
+julia> XLSX.setConditionalFormat(s, "A2:C5", :cellIs; # No further conditions will be evaluated if this condition is met.
+ operator ="greaterThan",
+ value="9",
+ stopIfTrue="true",
+ dxStyle = "redborder")
+0
+
+julia> XLSX.setConditionalFormat(s, "A2:C5", :top10; # Won't apply if the max value in the range is > 9.
+ operator ="topN",
+ value="1",
+ dxStyle = "redfilltext")
+0
+
+julia> XLSX.setConditionalFormat(s, "A2:C5", :colorScale; colorscale="greenyellow") # Won't apply to any cell with a value > 9
+0
+```
+
+
+
+Overlaying the same three conditional formats without setting the `stopIfTrue` option
+will result in the following, instead:
+
+
+
+It is possible to overlay `:colorScale`s, `:dataBar`s and `:iconSet`s in the same or
+overlapping cell ranges.
+
+```juliaf=XLSX.newxlsx()
+julia>
+XLSXFile("C:\...\blank.xlsx") containing 1 Worksheet
+ sheetname size range
+-------------------------------------------------
+ Sheet1 1x1 A1:A1
+
+julia> s=f[1]
+1×1 XLSX.Worksheet: ["Sheet1"](A1:A1)
+
+julia> XLSX.writetable!(s, [rand(10),rand(10),rand(10),rand(10),rand(10),rand(10),rand(10)],["col1","col2","col3","col4","col5","col6","col7"])
+
+julia> XLSX.setConditionalFormat(s, "A5:E8", :dataBar; direction="rightToLeft")
+0
+
+julia> XLSX.setConditionalFormat(s, "C5:G8", :iconSet; iconset="5Arrows")
+0
+
+julia> XLSX.setConditionalFormat(s, "C2:E11", :colorScale; colorscale="greenyellowred")
+0
+
+julia> XLSX.setFormat(s, "A2:G11"; format="#0.00")
+-1
+
+```
+
+
+## Working with merged cells
+
+Worksheets may contain merged cells. XLSX.jl provides functions to identify the merged cells in a worksheet,
+to determine if a cell is part of a merged range and to determine the value of a merged cell range from any
+cell in that range.
+
+```julia
+
+julia> using XLSX
+
+julia> f=XLSX.opentemplate("customXml.xlsx")
+XLSXFile("customXml.xlsx") containing 2 Worksheets
+ sheetname size range
+-------------------------------------------------
+ Mock-up 116x11 A1:K116
+ Document History 17x3 A1:C17
+
+julia> XLSX.getMergedCells(f[1])
+25-element Vector{XLSX.CellRange}:
+ D49:H49
+ D72:J72
+ F94:J94
+ F96:J96
+ F84:J84
+ F86:J86
+ D62:J63
+ D51:J53
+ D55:J60
+ D92:J92
+ D82:J82
+ D74:J74
+ D67:J68
+ D47:H47
+ D9:H9
+ D11:G11
+ D12:G12
+ D14:E14
+ D16:E16
+ D32:F32
+ D38:J38
+ D34:J34
+ D18:E18
+ D20:E20
+ D13:G13
+
+julia> XLSX.isMergedCell(f[1], "D13")
+true
+
+julia> XLSX.isMergedCell(f[1], "H13")
+false
+
+julia> XLSX.getMergedBaseCell(f[1], "E18") # E18 is a merged cell. The base cell in the merged range is D18.
+(baseCell = D18, baseValue = "Here") # The base cell in the merged range is D18 and it's value is "Here".
+```
+
+It is also possible to create new merged cells:
+
+```julia
+
+julia> XLSX.isMergedCell(f[1], "F5")
+false
+
+julia> XLSX.isMergedCell(f[1], "J8")
+false
+
+julia> XLSX.mergeCells(s, "F5:J8")
+
+julia> s["F5"] = pi
+π = 3.1415926535897...
+
+julia> XLSX.isMergedCell(f[1], "J8")
+true
+
+julia> XLSX.isMergedCell(f[1], "F5")
+true
+
+julia> XLSX.getMergedBaseCell(f[1], "J8")
+(baseCell = F5, baseValue = 3.141592653589793)
+```
+
+It is not allowed to create new merged cells that overlap at all with any existing merged cells.
+
+!!! warning
+
+ It is possible to write into any merged cell using `XLSX.jl`, even those that are not the
+ base cell of the merged range. This is illustrated below:
+
+ ```julia
+
+ julia> using XLSX
+
+ julia> f=XLSX.newxlsx()
+ XLSXFile("C:\...\blank.xlsx") containing 1 Worksheet
+ sheetname size range
+ -------------------------------------------------
+ Sheet1 1x1 A1:A1
+
+
+ julia> s=f[1]
+ 1×1 XLSX.Worksheet: ["Sheet1"](A1:A1)
+
+ julia> s["A1:A3"]=5
+ 5
+ ```
+
+ This produces the simple sheet shown.
+
+ 
+
+ Merging the three cells `A1:A3` sets the cells `A2` and `A3` to missing just as Excel does.
+
+ ```
+ julia> s["A1"]
+ 5
+
+ julia> s["A2"]
+ 5
+
+ julia> s["A3"]
+ 5
+
+ julia> XLSX.mergeCells(s, "A1:A3")
+ 0
+
+ julia> s["A1"]
+ 5
+
+ julia> s["A2"]
+ missing
+
+ julia> s["A3"]
+ missing
+ ```
+
+ 
+
+ However, even after the merge, it is possible to explicitly write into the merged cells.
+ These written values will not be visible in Excel but can still be accessed by reference.
+
+ ```
+ julia> s["A2"]="text here now"
+ "text here now"
+
+ julia> s["A1"]
+ 5
+
+ julia> s["A2"]
+ "text here now"
+
+ julia> s["A3"]
+ missing
+
+ julia> XLSX.getMergedBaseCell(s, "A2")
+ (baseCell = A1, baseValue = 5)
+
+ ```
+
+ The cell `A2` remains merged, and this is how Excel displays it. The assigned cell value
+ won't be visible in Excel, but it can be referenced in a formula as shown here, where
+ cell `B2` references cell `A2` in its formula ("=A2"):
+
+ 
+
+ Assigning values to cells in a merged range like this is prevented in Excel itself by the UI
+ although it is possible using VBA. There is currently no check to prevent this in `XLSX.jl`.
+ See [#241](https://github.com/felipenoris/XLSX.jl/issues/241)
+
+## Examples
+
+### Applying formatting to an existing table
+
+Consider a simple table, created from scratch, like this:
+
+```julia
+using XLSX
+using Dates
+
+# First create some data in an empty XLSXfile
+xf = XLSX.newxlsx()
+sheet = xf["Sheet1"]
+
+col_names = ["Integers", "Strings", "Floats", "Booleans", "Dates", "Times", "DateTimes", "AbstractStrings", "Rational", "Irrationals", "MixedStringNothingMissing"]
+data = Vector{Any}(undef, 11)
+data[1] = [1, 2, missing, UInt8(4)]
+data[2] = ["Hey", "You", "Out", "There"]
+data[3] = [101.5, 102.5, missing, 104.5]
+data[4] = [true, false, missing, true]
+data[5] = [Date(2018, 2, 1), Date(2018, 3, 1), Date(2018, 5, 20), Date(2018, 6, 2)]
+data[6] = [Dates.Time(19, 10), Dates.Time(19, 20), Dates.Time(19, 30), Dates.Time(0, 0)]
+data[7] = [Dates.DateTime(2018, 5, 20, 19, 10), Dates.DateTime(2018, 5, 20, 19, 20), Dates.DateTime(2018, 5, 20, 19, 30), Dates.DateTime(2018, 5, 20, 19, 40)]
+data[8] = SubString.(["Hey", "You", "Out", "There"], 1, 2)
+data[9] = [1 // 2, 1 // 3, missing, 22 // 3]
+data[10] = [pi, sqrt(2), missing, sqrt(5)]
+data[11] = [nothing, "middle", missing, "rotated"]
+
+XLSX.writetable!(
+ sheet,
+ data,
+ col_names;
+ anchor_cell=XLSX.CellRef("B2"),
+ write_columnnames=true,
+)
+
+XLSX.writexlsx("mytable_unformatted.xlsx", xf, overwrite=true)
+```
+
+By default, this table will look like this in Excel:
+
+
+
+We can apply some formatting choices to change the table's appearance:
+
+
+
+This is achieved with the following code:
+
+```julia
+# Cell borders
+XLSX.setUniformBorder(sheet, "B2:L6";
+ top = ["style" => "hair", "color" => "FF000000"],
+ bottom = ["style" => "hair", "color" => "FF000000"],
+ left = ["style" => "thin", "color" => "FF000000"],
+ right = ["style" => "thin", "color" => "FF000000"]
+)
+XLSX.setBorder(sheet, "B2:L2"; bottom = ["style" => "medium", "color" => "FF000000"])
+XLSX.setBorder(sheet, "B6:L6"; top = ["style" => "double", "color" => "FF000000"])
+XLSX.setOutsideBorder(sheet, "B2:L6"; outside = ["style" => "thick", "color" => "FF000000"])
+
+# Cell fill
+XLSX.setFill(sheet, "B2:L2"; pattern = "solid", fgColor = "FF444444")
+
+# Cell fonts
+XLSX.setFont(sheet, "B2:L2"; bold=true, color = "FFFFFFFF")
+XLSX.setFont(sheet, "B3:L6"; color = "FF444444")
+XLSX.setFont(sheet, "C3"; name = "Times New Roman")
+XLSX.setFont(sheet, "C6"; name = "Wingdings", color = "FF2F75B5")
+
+# Cell alignment
+XLSX.setAlignment(sheet, "L2"; wrapText = true)
+XLSX.setAlignment(sheet, "I4"; horizontal="right")
+XLSX.setAlignment(sheet, "I6"; horizontal="right")
+XLSX.setAlignment(sheet, "C4"; indent=2)
+XLSX.setAlignment(sheet, "F4"; vertical="top")
+XLSX.setAlignment(sheet, "G4"; vertical="center")
+XLSX.setAlignment(sheet, "L4"; horizontal="center", vertical="center")
+XLSX.setAlignment(sheet, "G3:G6"; horizontal = "center")
+XLSX.setAlignment(sheet, "H3:H6"; shrink = true)
+XLSX.setAlignment(sheet, "L6"; horizontal = "center", rotation = 90, wrapText=true)
+
+# Row height and column width
+XLSX.setRowHeight(sheet, "B4"; height=50)
+XLSX.setRowHeight(sheet, "B6"; height=15)
+XLSX.setColumnWidth(sheet, "I"; width = 20.5)
+
+# Conditional formatting
+function blankmissing(sheet, rng) # Fill with grey and apply both diagonal borders on cells
+ for c in rng # with missing values
+ if ismissing(sheet[c])
+ XLSX.setFill(sheet, c; pattern = "solid", fgColor = "grey")
+ XLSX.setBorder(sheet, c; diagonal = ["style" => "thin", "color" => "black"])
+ end
+ end
+end
+function trueorfalse(sheet, rng) # Use green or red font for true or false respectively
+ for c in rng
+ if !ismissing(sheet[c]) && sheet[c] isa Bool
+ XLSX.setFont(sheet, c, bold=true, color = sheet[c] ? "FF548235" : "FFC00000")
+ end
+ end
+end
+function redgreenminmax(sheet, rng) # Fill light green / light red the cell with maximum / minimum value
+ mn, mx = extrema(x for x in sheet[rng] if !ismissing(x))
+ for c in rng
+ if !ismissing(sheet[c])
+ if sheet[c] == mx
+ XLSX.setFill(sheet, c; pattern = "solid", fgColor = "FFC6EFCE")
+ elseif sheet[c] == mn
+ XLSX.setFill(sheet, c; pattern = "solid", fgColor = "FFFFC7CE")
+ end
+ end
+ end
+end
+
+blankmissing(sheet, XLSX.CellRange("B3:L6"))
+trueorfalse(sheet, XLSX.CellRange("B2:L6"))
+redgreenminmax(sheet, XLSX.CellRange("D3:D6"))
+redgreenminmax(sheet, XLSX.CellRange("J3:J6"))
+redgreenminmax(sheet, XLSX.CellRange("K3:K6"))
+
+# Number formats
+XLSX.setFormat(sheet, "J3"; format = "Percentage")
+XLSX.setFormat(sheet, "J4"; format = "Currency")
+XLSX.setFormat(sheet, "J6"; format = "Number")
+XLSX.setFormat(sheet, "K3"; format = "0.0")
+XLSX.setFormat(sheet, "K4"; format = "0.000")
+XLSX.setFormat(sheet, "K6"; format = "0.0000")
+
+# Save to an actual XLSX file
+XLSX.writexlsx("mytable_formatted.xlsx", xf, overwrite=true)
+```
+
+### Creating a formatted form
+
+There is a file, customXml.xlsx, in the \data folder of this project that looks like a template
+file - a form to be filled in. The code below creates this form from scratch and makes
+extensive use of vector indexing for rows and columns and of non-contiguous ranges:
+
+```julia
+using XLSX
+
+f = XLSX.newxlsx()
+s = f[1]
+s["A1:K116"] = ""
+
+s["B2"] = "Catalogue Entry Form"
+
+s["B5"] = "User Data"
+s["B7"] = "Recipient ID"
+s["B9"] = "Recipient Name"
+s["B11"] = "Address 1"
+s["B12"] = "Address 2"
+s["B13"] = "Address 3"
+s["B14"] = "Town"
+s["B16"] = "Postcode"
+s["B18"] = "Ward"
+s["B20"] = "Region"
+s["H18"] = "Local Authority"
+s["H20"] = "UK Constituency"
+s["B22"] = "GrantID"
+s["D22"] = "Grant Date"
+s["F22"] = "Grant Amount"
+s["H22"] = "Grant Title"
+s["J22"] = "Distributor"
+s["B32"] = "Distributor"
+
+s["B30"] = "Creator"
+s["B34"] = "Created by"
+s["D36"] = "Email"
+s["H36"] = "Phone"
+s["B38"] = "Grant Manager"
+s["D40"] = "Email"
+s["H40"] = "Phone number"
+
+s["B43"] = "Summary"
+s["B45"] = "Summary ID"
+s["H45"] = "Date Created"
+s["B47"] = "Summary Name"
+s["B49"] = "Headline"
+s["B51"] = "Short Description"
+s["B55"] = "Long Description"
+s["B62"] = "Quote 1"
+s["D65"] = "Quote Attribution"
+s["H65"] = "Quote Date"
+s["B67"] = "Quote 2"
+s["D70"] = "Quote Attribution"
+s["H70"] = "Quote Date"
+s["B72"] = "Keywords"
+s["B74"] = "Website"
+s["B76"] = "Social media handles"
+s["D76"] = "Twitter"
+s["D78"] = "Facebook"
+s["D80"] = "Instagram"
+s["H76"] = "LinkedIn"
+s["H78"] = "TikTok"
+s["H80"] = "YouTube"
+s["B82"] = "Image 1 filename"
+s["D84"] = "Alt-Text"
+s["D86"] = "Image Attribution"
+s["D88"] = "Image Date"
+s["D90"] = "Confirm permission to use image"
+s["B92"] = "Image 2 filename"
+s["D94"] = "Alt-Text"
+s["D96"] = "Image Attribution"
+s["D98"] = "Image Date"
+s["D100"] = "Confirm permission to use image"
+
+s["B103"] = "Penultimate category"
+s["B105"] = "Competition Details"
+s["D105"] = "Last year of entry"
+s["D107"] = "Year of last win"
+s["H105"] = "Categories of entry"
+s["H107"] = "Categories of win"
+
+s["B110"] = "Last category"
+s["B112"] = "Use for Comms"
+s["D112"] = "Comms Priority"
+s["F112"] = "Comms End Date"
+
+XLSX.setColumnWidth(s, 1:2:11; width=1.3)
+XLSX.setColumnWidth(s, 2:2:10; width=18)
+XLSX.setRowHeight(s, :; height=15)
+XLSX.setRowHeight(s, [3, 4, 19, 28, 29, 35, 39, 41, 42, 64, 69, 77, 79, 83, 85, 87, 89, 93, 95, 97, 99, 101, 102, 106, 108, 109, 116]; height=5.5)
+XLSX.setRowHeight(s, [5, 30, 43, 103, 110]; height=18)
+XLSX.setRowHeight(s, 2; height=23)
+
+XLSX.setFont(s, "B2"; size=18, bold=true)
+XLSX.setUniformFont(s, [5, 30, 43, 103, 110], 2; size=14, bold=true)
+
+XLSX.setUniformFill(s, [1, 2, 3, 4, 5, 6, 8, 10, 15, 17, 19, 21, 28, 29, 30, 31, 33, 35, 37, 39, 41, 42, 43, 44, 46, 48, 50, 52, 53, 54, 56, 57, 58, 59, 60, 61, 63, 64, 66, 68, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99, 101, 102, 103, 104, 106, 108, 109, 110, 111, 115, 116], :; pattern="solid", fgColor="lightgrey")
+XLSX.setUniformFill(s, :, [1, 3, 5, 7, 9, 11]; pattern="solid", fgColor="lightgrey")
+XLSX.setFill(s, "F7,H7,J7,J9,H11:J16,F14,F16:F20,H32:J32,B36,B40,F45,J47:J49,B65,B70,B78:B80,B84:B90,B94:B100,H88:J90,H98:J100,B107,F114,H112:J115"; pattern="solid", fgColor="lightgrey")
+XLSX.setFill(s, "D18,D20,J18,J20,D45"; pattern="solid", fgColor="darkgrey")
+XLSX.setFill(s, "B112:B114,D112:D115"; pattern="solid", fgColor="white")
+XLSX.setFill(s, "E90,E100,D115"; pattern="none")
+
+XLSX.mergeCells(s, "D9:H9")
+XLSX.mergeCells(s, "D11:G11,D12:G12,D13:G13")
+XLSX.mergeCells(s, "D32:F32,D34:J34,D38:J38")
+XLSX.mergeCells(s, "D47:H47,D49:H49")
+XLSX.mergeCells(s, "D51:J53,D55:J60")
+XLSX.mergeCells(s, "D62:J63,D67:J68")
+XLSX.mergeCells(s, "D72:J72,D74:J74")
+XLSX.mergeCells(s, "D82:J82,F84:J84,F86:J86")
+XLSX.mergeCells(s, "D92:J92,F94:J94,F96:J96")
+
+XLSX.setAlignment(s, "D51:J53,D55:J60,D62:J63,D67:J68"; vertical="top", wrapText=true)
+
+XLSX.setBorder(s, "A1:K3"; outside = ["style" => "medium", "color" => "black"])
+XLSX.setBorder(s, "A4:K28"; outside = ["style" => "medium", "color" => "black"])
+XLSX.setBorder(s, "A29:K41"; outside = ["style" => "medium", "color" => "black"])
+XLSX.setBorder(s, "A42:K101"; outside = ["style" => "medium", "color" => "black"])
+XLSX.setBorder(s, "A102:K108"; outside = ["style" => "medium", "color" => "black"])
+XLSX.setBorder(s, "A109:K116"; outside = ["style" => "medium", "color" => "black"])
+
+XLSX.setBorder(s, "B7:D7,B9:H9"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "B11:G13,B14:D14,B16:D16"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "B18:D18,B20:D20,H18:J18,H20:J20"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setUniformBorder(s, "B22:J27"; allsides = ["style" => "thin", "color" => "black"])
+
+XLSX.setBorder(s, "B32:F32"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "B34:C34,D34:J34,D36:F36,H36:J36"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "B38:C38,D38:J38,D40:F40,H40:J40"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "D34:J36,D38:J40"; outside = ["style" => "thin", "color" => "black"])
+
+XLSX.setBorder(s, "B45:D45,H45:J45"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "B47:H47,B49:H49"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "B51:C51,B55:C55"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "D51:J53,D55:J60"; outside = ["style" => "thin", "color" => "black"])
+
+XLSX.setBorder(s, "B62:C62,D65:F65,H65:J65"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "B67:C67,D70:F70,H70:J70"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "D62:J63,D67:J68"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "D62:J65,D67:J70"; outside = ["style" => "thin", "color" => "black"])
+
+XLSX.setBorder(s, "B72:J72,B74:J74"; allsides = ["style" => "thin", "color" => "black"])
+
+XLSX.setBorder(s, "B76:F76,H76:J76,D78:F78,H78:J78,D80:F80,H80:J80"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "D76:J80"; outside = ["style" => "thin", "color" => "black"])
+
+XLSX.setBorder(s, "B82:J82,D84:J84,D86:J86,D88:F88,D90:F90"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "D82:J90"; outside = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "B92:J92,D94:J94,D96:J96,D98:F98,D100:F100"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "D92:J100"; outside = ["style" => "thin", "color" => "black"])
+
+XLSX.setBorder(s, "B105:F105,H105:J105,D107:F107,H107:J107"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "D105:J107"; outside = ["style" => "thin", "color" => "black"])
+
+XLSX.setBorder(s, "F112,F113"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "B112:B114,D112:D115"; outside = ["style" => "thin", "color" => "black"])
+
+XLSX.writexlsx("myNewTemplate.xlsx", f, overwrite=true)
+```
\ No newline at end of file
diff --git a/docs/src/formatting/examples.md b/docs/src/formatting/examples.md
new file mode 100644
index 00000000..b59a906b
--- /dev/null
+++ b/docs/src/formatting/examples.md
@@ -0,0 +1,297 @@
+
+
+# Examples
+
+## Applying cell format to an existing table
+
+Consider a simple table, created from scratch, like this:
+
+```julia
+using XLSX
+using Dates
+
+# First create some data in an empty XLSXfile
+xf = XLSX.newxlsx()
+sheet = xf["Sheet1"]
+
+col_names = ["Integers", "Strings", "Floats", "Booleans", "Dates", "Times", "DateTimes", "AbstractStrings", "Rational", "Irrationals", "MixedStringNothingMissing"]
+data = Vector{Any}(undef, 11)
+data[1] = [1, 2, missing, UInt8(4)]
+data[2] = ["Hey", "You", "Out", "There"]
+data[3] = [101.5, 102.5, missing, 104.5]
+data[4] = [true, false, missing, true]
+data[5] = [Date(2018, 2, 1), Date(2018, 3, 1), Date(2018, 5, 20), Date(2018, 6, 2)]
+data[6] = [Dates.Time(19, 10), Dates.Time(19, 20), Dates.Time(19, 30), Dates.Time(0, 0)]
+data[7] = [Dates.DateTime(2018, 5, 20, 19, 10), Dates.DateTime(2018, 5, 20, 19, 20), Dates.DateTime(2018, 5, 20, 19, 30), Dates.DateTime(2018, 5, 20, 19, 40)]
+data[8] = SubString.(["Hey", "You", "Out", "There"], 1, 2)
+data[9] = [1 // 2, 1 // 3, missing, 22 // 3]
+data[10] = [pi, sqrt(2), missing, sqrt(5)]
+data[11] = [nothing, "middle", missing, "rotated"]
+
+XLSX.writetable!(
+ sheet,
+ data,
+ col_names;
+ anchor_cell=XLSX.CellRef("B2"),
+ write_columnnames=true,
+)
+
+XLSX.writexlsx("mytable_unformatted.xlsx", xf, overwrite=true)
+```
+
+By default, this table will look like this in Excel:
+
+
+
+We can apply some formatting choices to change the table's appearance:
+
+
+
+This is achieved with the following code:
+
+```julia
+# Cell borders
+XLSX.setUniformBorder(sheet, "B2:L6";
+ top = ["style" => "hair", "color" => "FF000000"],
+ bottom = ["style" => "hair", "color" => "FF000000"],
+ left = ["style" => "thin", "color" => "FF000000"],
+ right = ["style" => "thin", "color" => "FF000000"]
+)
+XLSX.setBorder(sheet, "B2:L2"; bottom = ["style" => "medium", "color" => "FF000000"])
+XLSX.setBorder(sheet, "B6:L6"; top = ["style" => "double", "color" => "FF000000"])
+XLSX.setOutsideBorder(sheet, "B2:L6"; outside = ["style" => "thick", "color" => "FF000000"])
+
+# Cell fill
+XLSX.setFill(sheet, "B2:L2"; pattern = "solid", fgColor = "FF444444")
+
+# Cell fonts
+XLSX.setFont(sheet, "B2:L2"; bold=true, color = "FFFFFFFF")
+XLSX.setFont(sheet, "B3:L6"; color = "FF444444")
+XLSX.setFont(sheet, "C3"; name = "Times New Roman")
+XLSX.setFont(sheet, "C6"; name = "Wingdings", color = "FF2F75B5")
+
+# Cell alignment
+XLSX.setAlignment(sheet, "L2"; wrapText = true)
+XLSX.setAlignment(sheet, "I4"; horizontal="right")
+XLSX.setAlignment(sheet, "I6"; horizontal="right")
+XLSX.setAlignment(sheet, "C4"; indent=2)
+XLSX.setAlignment(sheet, "F4"; vertical="top")
+XLSX.setAlignment(sheet, "G4"; vertical="center")
+XLSX.setAlignment(sheet, "L4"; horizontal="center", vertical="center")
+XLSX.setAlignment(sheet, "G3:G6"; horizontal = "center")
+XLSX.setAlignment(sheet, "H3:H6"; shrink = true)
+XLSX.setAlignment(sheet, "L6"; horizontal = "center", rotation = 90, wrapText=true)
+
+# Row height and column width
+XLSX.setRowHeight(sheet, "B4"; height=50)
+XLSX.setRowHeight(sheet, "B6"; height=15)
+XLSX.setColumnWidth(sheet, "I"; width = 20.5)
+
+# Conditional formatting
+function blankmissing(sheet, rng) # Fill with grey and apply both diagonal borders on cells
+ for c in rng # with missing values
+ if ismissing(sheet[c])
+ XLSX.setFill(sheet, c; pattern = "solid", fgColor = "grey")
+ XLSX.setBorder(sheet, c; diagonal = ["style" => "thin", "color" => "black"])
+ end
+ end
+end
+function trueorfalse(sheet, rng) # Use green or red font for true or false respectively
+ for c in rng
+ if !ismissing(sheet[c]) && sheet[c] isa Bool
+ XLSX.setFont(sheet, c, bold=true, color = sheet[c] ? "FF548235" : "FFC00000")
+ end
+ end
+end
+function redgreenminmax(sheet, rng) # Fill light green / light red the cell with maximum / minimum value
+ mn, mx = extrema(x for x in sheet[rng] if !ismissing(x))
+ for c in rng
+ if !ismissing(sheet[c])
+ if sheet[c] == mx
+ XLSX.setFill(sheet, c; pattern = "solid", fgColor = "FFC6EFCE")
+ elseif sheet[c] == mn
+ XLSX.setFill(sheet, c; pattern = "solid", fgColor = "FFFFC7CE")
+ end
+ end
+ end
+end
+
+blankmissing(sheet, XLSX.CellRange("B3:L6"))
+trueorfalse(sheet, XLSX.CellRange("B2:L6"))
+redgreenminmax(sheet, XLSX.CellRange("D3:D6"))
+redgreenminmax(sheet, XLSX.CellRange("J3:J6"))
+redgreenminmax(sheet, XLSX.CellRange("K3:K6"))
+
+# Number formats
+XLSX.setFormat(sheet, "J3"; format = "Percentage")
+XLSX.setFormat(sheet, "J4"; format = "Currency")
+XLSX.setFormat(sheet, "J6"; format = "Number")
+XLSX.setFormat(sheet, "K3"; format = "0.0")
+XLSX.setFormat(sheet, "K4"; format = "0.000")
+XLSX.setFormat(sheet, "K6"; format = "0.0000")
+
+# Save to an actual XLSX file
+XLSX.writexlsx("mytable_formatted.xlsx", xf, overwrite=true)
+```
+
+## Creating a formatted form
+
+There is a file, customXml.xlsx, in the \data folder of this project that looks like a template
+file - a form to be filled in. The code below creates this form from scratch and makes
+extensive use of vector indexing for rows and columns and of non-contiguous ranges:
+
+```julia
+using XLSX
+
+f = XLSX.newxlsx()
+s = f[1]
+s["A1:K116"] = ""
+
+s["B2"] = "Catalogue Entry Form"
+
+s["B5"] = "User Data"
+s["B7"] = "Recipient ID"
+s["B9"] = "Recipient Name"
+s["B11"] = "Address 1"
+s["B12"] = "Address 2"
+s["B13"] = "Address 3"
+s["B14"] = "Town"
+s["B16"] = "Postcode"
+s["B18"] = "Ward"
+s["B20"] = "Region"
+s["H18"] = "Local Authority"
+s["H20"] = "UK Constituency"
+s["B22"] = "GrantID"
+s["D22"] = "Grant Date"
+s["F22"] = "Grant Amount"
+s["H22"] = "Grant Title"
+s["J22"] = "Distributor"
+s["B32"] = "Distributor"
+
+s["B30"] = "Creator"
+s["B34"] = "Created by"
+s["D36"] = "Email"
+s["H36"] = "Phone"
+s["B38"] = "Grant Manager"
+s["D40"] = "Email"
+s["H40"] = "Phone number"
+
+s["B43"] = "Summary"
+s["B45"] = "Summary ID"
+s["H45"] = "Date Created"
+s["B47"] = "Summary Name"
+s["B49"] = "Headline"
+s["B51"] = "Short Description"
+s["B55"] = "Long Description"
+s["B62"] = "Quote 1"
+s["D65"] = "Quote Attribution"
+s["H65"] = "Quote Date"
+s["B67"] = "Quote 2"
+s["D70"] = "Quote Attribution"
+s["H70"] = "Quote Date"
+s["B72"] = "Keywords"
+s["B74"] = "Website"
+s["B76"] = "Social media handles"
+s["D76"] = "Twitter"
+s["D78"] = "Facebook"
+s["D80"] = "Instagram"
+s["H76"] = "LinkedIn"
+s["H78"] = "TikTok"
+s["H80"] = "YouTube"
+s["B82"] = "Image 1 filename"
+s["D84"] = "Alt-Text"
+s["D86"] = "Image Attribution"
+s["D88"] = "Image Date"
+s["D90"] = "Confirm permission to use image"
+s["B92"] = "Image 2 filename"
+s["D94"] = "Alt-Text"
+s["D96"] = "Image Attribution"
+s["D98"] = "Image Date"
+s["D100"] = "Confirm permission to use image"
+
+s["B103"] = "Penultimate category"
+s["B105"] = "Competition Details"
+s["D105"] = "Last year of entry"
+s["D107"] = "Year of last win"
+s["H105"] = "Categories of entry"
+s["H107"] = "Categories of win"
+
+s["B110"] = "Last category"
+s["B112"] = "Use for Comms"
+s["D112"] = "Comms Priority"
+s["F112"] = "Comms End Date"
+
+XLSX.setColumnWidth(s, 1:2:11; width=1.3)
+XLSX.setColumnWidth(s, 2:2:10; width=18)
+XLSX.setRowHeight(s, :; height=15)
+XLSX.setRowHeight(s, [3, 4, 19, 28, 29, 35, 39, 41, 42, 64, 69, 77, 79, 83, 85, 87, 89, 93, 95, 97, 99, 101, 102, 106, 108, 109, 116]; height=5.5)
+XLSX.setRowHeight(s, [5, 30, 43, 103, 110]; height=18)
+XLSX.setRowHeight(s, 2; height=23)
+
+XLSX.setFont(s, "B2"; size=18, bold=true)
+XLSX.setUniformFont(s, [5, 30, 43, 103, 110], 2; size=14, bold=true)
+
+XLSX.setUniformFill(s, [1, 2, 3, 4, 5, 6, 8, 10, 15, 17, 19, 21, 28, 29, 30, 31, 33, 35, 37, 39, 41, 42, 43, 44, 46, 48, 50, 52, 53, 54, 56, 57, 58, 59, 60, 61, 63, 64, 66, 68, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99, 101, 102, 103, 104, 106, 108, 109, 110, 111, 115, 116], :; pattern="solid", fgColor="lightgrey")
+XLSX.setUniformFill(s, :, [1, 3, 5, 7, 9, 11]; pattern="solid", fgColor="lightgrey")
+XLSX.setFill(s, "F7,H7,J7,J9,H11:J16,F14,F16:F20,H32:J32,B36,B40,F45,J47:J49,B65,B70,B78:B80,B84:B90,B94:B100,H88:J90,H98:J100,B107,F114,H112:J115"; pattern="solid", fgColor="lightgrey")
+XLSX.setFill(s, "D18,D20,J18,J20,D45"; pattern="solid", fgColor="darkgrey")
+XLSX.setFill(s, "B112:B114,D112:D115"; pattern="solid", fgColor="white")
+XLSX.setFill(s, "E90,E100,D115"; pattern="none")
+
+XLSX.mergeCells(s, "D9:H9")
+XLSX.mergeCells(s, "D11:G11,D12:G12,D13:G13")
+XLSX.mergeCells(s, "D32:F32,D34:J34,D38:J38")
+XLSX.mergeCells(s, "D47:H47,D49:H49")
+XLSX.mergeCells(s, "D51:J53,D55:J60")
+XLSX.mergeCells(s, "D62:J63,D67:J68")
+XLSX.mergeCells(s, "D72:J72,D74:J74")
+XLSX.mergeCells(s, "D82:J82,F84:J84,F86:J86")
+XLSX.mergeCells(s, "D92:J92,F94:J94,F96:J96")
+
+XLSX.setAlignment(s, "D51:J53,D55:J60,D62:J63,D67:J68"; vertical="top", wrapText=true)
+
+XLSX.setBorder(s, "A1:K3"; outside = ["style" => "medium", "color" => "black"])
+XLSX.setBorder(s, "A4:K28"; outside = ["style" => "medium", "color" => "black"])
+XLSX.setBorder(s, "A29:K41"; outside = ["style" => "medium", "color" => "black"])
+XLSX.setBorder(s, "A42:K101"; outside = ["style" => "medium", "color" => "black"])
+XLSX.setBorder(s, "A102:K108"; outside = ["style" => "medium", "color" => "black"])
+XLSX.setBorder(s, "A109:K116"; outside = ["style" => "medium", "color" => "black"])
+
+XLSX.setBorder(s, "B7:D7,B9:H9"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "B11:G13,B14:D14,B16:D16"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "B18:D18,B20:D20,H18:J18,H20:J20"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setUniformBorder(s, "B22:J27"; allsides = ["style" => "thin", "color" => "black"])
+
+XLSX.setBorder(s, "B32:F32"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "B34:C34,D34:J34,D36:F36,H36:J36"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "B38:C38,D38:J38,D40:F40,H40:J40"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "D34:J36,D38:J40"; outside = ["style" => "thin", "color" => "black"])
+
+XLSX.setBorder(s, "B45:D45,H45:J45"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "B47:H47,B49:H49"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "B51:C51,B55:C55"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "D51:J53,D55:J60"; outside = ["style" => "thin", "color" => "black"])
+
+XLSX.setBorder(s, "B62:C62,D65:F65,H65:J65"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "B67:C67,D70:F70,H70:J70"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "D62:J63,D67:J68"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "D62:J65,D67:J70"; outside = ["style" => "thin", "color" => "black"])
+
+XLSX.setBorder(s, "B72:J72,B74:J74"; allsides = ["style" => "thin", "color" => "black"])
+
+XLSX.setBorder(s, "B76:F76,H76:J76,D78:F78,H78:J78,D80:F80,H80:J80"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "D76:J80"; outside = ["style" => "thin", "color" => "black"])
+
+XLSX.setBorder(s, "B82:J82,D84:J84,D86:J86,D88:F88,D90:F90"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "D82:J90"; outside = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "B92:J92,D94:J94,D96:J96,D98:F98,D100:F100"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "D92:J100"; outside = ["style" => "thin", "color" => "black"])
+
+XLSX.setBorder(s, "B105:F105,H105:J105,D107:F107,H107:J107"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "D105:J107"; outside = ["style" => "thin", "color" => "black"])
+
+XLSX.setBorder(s, "F112,F113"; allsides = ["style" => "thin", "color" => "black"])
+XLSX.setBorder(s, "B112:B114,D112:D115"; outside = ["style" => "thin", "color" => "black"])
+
+XLSX.writexlsx("myNewTemplate.xlsx", f, overwrite=true)
+```
\ No newline at end of file
diff --git a/docs/src/formatting/mergedCells.md b/docs/src/formatting/mergedCells.md
new file mode 100644
index 00000000..d1e86e19
--- /dev/null
+++ b/docs/src/formatting/mergedCells.md
@@ -0,0 +1,168 @@
+
+# Working with merged cells
+
+Worksheets may contain merged cells. XLSX.jl provides functions to identify the merged cells in a worksheet,
+to determine if a cell is part of a merged range and to determine the value of a merged cell range from any
+cell in that range.
+
+```julia
+
+julia> using XLSX
+
+julia> f=XLSX.opentemplate("customXml.xlsx")
+XLSXFile("customXml.xlsx") containing 2 Worksheets
+ sheetname size range
+-------------------------------------------------
+ Mock-up 116x11 A1:K116
+ Document History 17x3 A1:C17
+
+julia> XLSX.getMergedCells(f[1])
+25-element Vector{XLSX.CellRange}:
+ D49:H49
+ D72:J72
+ F94:J94
+ F96:J96
+ F84:J84
+ F86:J86
+ D62:J63
+ D51:J53
+ D55:J60
+ D92:J92
+ D82:J82
+ D74:J74
+ D67:J68
+ D47:H47
+ D9:H9
+ D11:G11
+ D12:G12
+ D14:E14
+ D16:E16
+ D32:F32
+ D38:J38
+ D34:J34
+ D18:E18
+ D20:E20
+ D13:G13
+
+julia> XLSX.isMergedCell(f[1], "D13")
+true
+
+julia> XLSX.isMergedCell(f[1], "H13")
+false
+
+julia> XLSX.getMergedBaseCell(f[1], "E18") # E18 is a merged cell. The base cell in the merged range is D18.
+(baseCell = D18, baseValue = "Here") # The base cell in the merged range is D18 and it's value is "Here".
+```
+
+It is also possible to create new merged cells:
+
+```julia
+
+julia> XLSX.isMergedCell(f[1], "F5")
+false
+
+julia> XLSX.isMergedCell(f[1], "J8")
+false
+
+julia> XLSX.mergeCells(s, "F5:J8")
+
+julia> s["F5"] = pi
+π = 3.1415926535897...
+
+julia> XLSX.isMergedCell(f[1], "J8")
+true
+
+julia> XLSX.isMergedCell(f[1], "F5")
+true
+
+julia> XLSX.getMergedBaseCell(f[1], "J8")
+(baseCell = F5, baseValue = 3.141592653589793)
+```
+
+It is not allowed to create new merged cells that overlap at all with any existing merged cells.
+
+!!! warning
+
+ It is possible to write into any merged cell using `XLSX.jl`, even those that are not the
+ base cell of the merged range. This is illustrated below:
+
+ ```julia
+
+ julia> using XLSX
+
+ julia> f=XLSX.newxlsx()
+ XLSXFile("C:\...\blank.xlsx") containing 1 Worksheet
+ sheetname size range
+ -------------------------------------------------
+ Sheet1 1x1 A1:A1
+
+
+ julia> s=f[1]
+ 1×1 XLSX.Worksheet: ["Sheet1"](A1:A1)
+
+ julia> s["A1:A3"]=5
+ 5
+ ```
+
+ This produces the simple sheet shown.
+
+ 
+
+ Merging the three cells `A1:A3` sets the cells `A2` and `A3` to missing just as Excel does.
+
+ ```
+ julia> s["A1"]
+ 5
+
+ julia> s["A2"]
+ 5
+
+ julia> s["A3"]
+ 5
+
+ julia> XLSX.mergeCells(s, "A1:A3")
+ 0
+
+ julia> s["A1"]
+ 5
+
+ julia> s["A2"]
+ missing
+
+ julia> s["A3"]
+ missing
+ ```
+
+ 
+
+ However, even after the merge, it is possible to explicitly write into the merged cells.
+ These written values will not be visible in Excel but can still be accessed by reference.
+
+ ```
+ julia> s["A2"]="text here now"
+ "text here now"
+
+ julia> s["A1"]
+ 5
+
+ julia> s["A2"]
+ "text here now"
+
+ julia> s["A3"]
+ missing
+
+ julia> XLSX.getMergedBaseCell(s, "A2")
+ (baseCell = A1, baseValue = 5)
+
+ ```
+
+ The cell `A2` remains merged, and this is how Excel displays it. The assigned cell value
+ won't be visible in Excel, but it can be referenced in a formula as shown here, where
+ cell `B2` references cell `A2` in its formula ("=A2"):
+
+ 
+
+ Assigning values to cells in a merged range like this is prevented in Excel itself by the UI
+ although it is possible using VBA. There is currently no check to prevent this in `XLSX.jl`.
+ See [#241](https://github.com/felipenoris/XLSX.jl/issues/241)
+
diff --git a/docs/src/images/Written-to-merged-cell.png b/docs/src/images/Written-to-merged-cell.png
new file mode 100644
index 00000000..6490039a
Binary files /dev/null and b/docs/src/images/Written-to-merged-cell.png differ
diff --git a/docs/src/images/after-merge.png b/docs/src/images/after-merge.png
new file mode 100644
index 00000000..b56b8612
Binary files /dev/null and b/docs/src/images/after-merge.png differ
diff --git a/docs/src/images/allIcons.png b/docs/src/images/allIcons.png
new file mode 100644
index 00000000..0aadd892
Binary files /dev/null and b/docs/src/images/allIcons.png differ
diff --git a/docs/src/images/averageComparison.png b/docs/src/images/averageComparison.png
new file mode 100644
index 00000000..ef3ca499
Binary files /dev/null and b/docs/src/images/averageComparison.png differ
diff --git a/docs/src/images/axisOptions.png b/docs/src/images/axisOptions.png
new file mode 100644
index 00000000..1f4f4b5b
Binary files /dev/null and b/docs/src/images/axisOptions.png differ
diff --git a/docs/src/images/basicIconSet.png b/docs/src/images/basicIconSet.png
new file mode 100644
index 00000000..f5fc273d
Binary files /dev/null and b/docs/src/images/basicIconSet.png differ
diff --git a/docs/src/images/blank.png b/docs/src/images/blank.png
new file mode 100644
index 00000000..89ee7cae
Binary files /dev/null and b/docs/src/images/blank.png differ
diff --git a/docs/src/images/borderAndGrad.png b/docs/src/images/borderAndGrad.png
new file mode 100644
index 00000000..f4235949
Binary files /dev/null and b/docs/src/images/borderAndGrad.png differ
diff --git a/docs/src/images/caseSensitiveComparison.png b/docs/src/images/caseSensitiveComparison.png
new file mode 100644
index 00000000..0bc3a267
Binary files /dev/null and b/docs/src/images/caseSensitiveComparison.png differ
diff --git a/docs/src/images/cell1.png b/docs/src/images/cell1.png
new file mode 100644
index 00000000..7d206bfa
Binary files /dev/null and b/docs/src/images/cell1.png differ
diff --git a/docs/src/images/cell2.png b/docs/src/images/cell2.png
new file mode 100644
index 00000000..7d35ea4b
Binary files /dev/null and b/docs/src/images/cell2.png differ
diff --git a/docs/src/images/cellvalue-formats.png b/docs/src/images/cellvalue-formats.png
new file mode 100644
index 00000000..f34cd791
Binary files /dev/null and b/docs/src/images/cellvalue-formats.png differ
diff --git a/docs/src/images/colorScaleOptions.png b/docs/src/images/colorScaleOptions.png
new file mode 100644
index 00000000..70db1a1b
Binary files /dev/null and b/docs/src/images/colorScaleOptions.png differ
diff --git a/docs/src/images/colorScales.png b/docs/src/images/colorScales.png
new file mode 100644
index 00000000..5123a78f
Binary files /dev/null and b/docs/src/images/colorScales.png differ
diff --git a/docs/src/images/containsText.png b/docs/src/images/containsText.png
new file mode 100644
index 00000000..ab103a2e
Binary files /dev/null and b/docs/src/images/containsText.png differ
diff --git a/docs/src/images/custom-cellvalue-example.png b/docs/src/images/custom-cellvalue-example.png
new file mode 100644
index 00000000..b4df58db
Binary files /dev/null and b/docs/src/images/custom-cellvalue-example.png differ
diff --git a/docs/src/images/custom-colorscale.png b/docs/src/images/custom-colorscale.png
new file mode 100644
index 00000000..e8fb2042
Binary files /dev/null and b/docs/src/images/custom-colorscale.png differ
diff --git a/docs/src/images/custom-formats.png b/docs/src/images/custom-formats.png
new file mode 100644
index 00000000..3807698e
Binary files /dev/null and b/docs/src/images/custom-formats.png differ
diff --git a/docs/src/images/customColors.png b/docs/src/images/customColors.png
new file mode 100644
index 00000000..13b60cf8
Binary files /dev/null and b/docs/src/images/customColors.png differ
diff --git a/docs/src/images/dataBarOptions.png b/docs/src/images/dataBarOptions.png
new file mode 100644
index 00000000..a06fc418
Binary files /dev/null and b/docs/src/images/dataBarOptions.png differ
diff --git a/docs/src/images/dataBars.png b/docs/src/images/dataBars.png
new file mode 100644
index 00000000..230d5dc0
Binary files /dev/null and b/docs/src/images/dataBars.png differ
diff --git a/docs/src/images/errorBlank.png b/docs/src/images/errorBlank.png
new file mode 100644
index 00000000..24d4dbd5
Binary files /dev/null and b/docs/src/images/errorBlank.png differ
diff --git a/docs/src/images/expression.png b/docs/src/images/expression.png
new file mode 100644
index 00000000..8d23fb6a
Binary files /dev/null and b/docs/src/images/expression.png differ
diff --git a/docs/src/images/formatted-table.png b/docs/src/images/formatted-table.png
new file mode 100644
index 00000000..fbd0edf3
Binary files /dev/null and b/docs/src/images/formatted-table.png differ
diff --git a/docs/src/images/iconKey.png b/docs/src/images/iconKey.png
new file mode 100644
index 00000000..29e73f66
Binary files /dev/null and b/docs/src/images/iconKey.png differ
diff --git a/docs/src/images/iconSetOptions.png b/docs/src/images/iconSetOptions.png
new file mode 100644
index 00000000..d6aabd24
Binary files /dev/null and b/docs/src/images/iconSetOptions.png differ
diff --git a/docs/src/images/iconSets.png b/docs/src/images/iconSets.png
new file mode 100644
index 00000000..0971748e
Binary files /dev/null and b/docs/src/images/iconSets.png differ
diff --git a/docs/src/images/minmaxDataBar.png b/docs/src/images/minmaxDataBar.png
new file mode 100644
index 00000000..87b511bf
Binary files /dev/null and b/docs/src/images/minmaxDataBar.png differ
diff --git a/docs/src/images/moreMixed.png b/docs/src/images/moreMixed.png
new file mode 100644
index 00000000..ffd6562b
Binary files /dev/null and b/docs/src/images/moreMixed.png differ
diff --git a/docs/src/images/multicell.png b/docs/src/images/multicell.png
new file mode 100644
index 00000000..6c952f3f
Binary files /dev/null and b/docs/src/images/multicell.png differ
diff --git a/docs/src/images/multicell2.png b/docs/src/images/multicell2.png
new file mode 100644
index 00000000..d6013b72
Binary files /dev/null and b/docs/src/images/multicell2.png differ
diff --git a/docs/src/images/multicell3.png b/docs/src/images/multicell3.png
new file mode 100644
index 00000000..5639e68b
Binary files /dev/null and b/docs/src/images/multicell3.png differ
diff --git a/docs/src/images/multicell4.png b/docs/src/images/multicell4.png
new file mode 100644
index 00000000..990b1af8
Binary files /dev/null and b/docs/src/images/multicell4.png differ
diff --git a/docs/src/images/multiple-cellvalue-example.png b/docs/src/images/multiple-cellvalue-example.png
new file mode 100644
index 00000000..85da5cb2
Binary files /dev/null and b/docs/src/images/multiple-cellvalue-example.png differ
diff --git a/docs/src/images/negAndAxisOptions.png b/docs/src/images/negAndAxisOptions.png
new file mode 100644
index 00000000..47b27a84
Binary files /dev/null and b/docs/src/images/negAndAxisOptions.png differ
diff --git a/docs/src/images/newValIconSet.png b/docs/src/images/newValIconSet.png
new file mode 100644
index 00000000..80f89311
Binary files /dev/null and b/docs/src/images/newValIconSet.png differ
diff --git a/docs/src/images/no-stop-if-true.png b/docs/src/images/no-stop-if-true.png
new file mode 100644
index 00000000..0c68da28
Binary files /dev/null and b/docs/src/images/no-stop-if-true.png differ
diff --git a/docs/src/images/relative-CellRef.png b/docs/src/images/relative-CellRef.png
new file mode 100644
index 00000000..fa6e9a0d
Binary files /dev/null and b/docs/src/images/relative-CellRef.png differ
diff --git a/docs/src/images/relativeComparison.png b/docs/src/images/relativeComparison.png
new file mode 100644
index 00000000..f23ea8c8
Binary files /dev/null and b/docs/src/images/relativeComparison.png differ
diff --git a/docs/src/images/showValIcons.png b/docs/src/images/showValIcons.png
new file mode 100644
index 00000000..b7988618
Binary files /dev/null and b/docs/src/images/showValIcons.png differ
diff --git a/docs/src/images/simple-cellvalue-example.png b/docs/src/images/simple-cellvalue-example.png
new file mode 100644
index 00000000..5f1c6d57
Binary files /dev/null and b/docs/src/images/simple-cellvalue-example.png differ
diff --git a/docs/src/images/simple-unmerged.png b/docs/src/images/simple-unmerged.png
new file mode 100644
index 00000000..aab765e4
Binary files /dev/null and b/docs/src/images/simple-unmerged.png differ
diff --git a/docs/src/images/simpleComparison.png b/docs/src/images/simpleComparison.png
new file mode 100644
index 00000000..1c5ca1de
Binary files /dev/null and b/docs/src/images/simpleComparison.png differ
diff --git a/docs/src/images/simpleDataBar.png b/docs/src/images/simpleDataBar.png
new file mode 100644
index 00000000..9f64fd8b
Binary files /dev/null and b/docs/src/images/simpleDataBar.png differ
diff --git a/docs/src/images/stop-if-true.png b/docs/src/images/stop-if-true.png
new file mode 100644
index 00000000..370143c6
Binary files /dev/null and b/docs/src/images/stop-if-true.png differ
diff --git a/docs/src/images/timePeriod-9thMay2025.png b/docs/src/images/timePeriod-9thMay2025.png
new file mode 100644
index 00000000..a6ae35d1
Binary files /dev/null and b/docs/src/images/timePeriod-9thMay2025.png differ
diff --git a/docs/src/images/topN.png b/docs/src/images/topN.png
new file mode 100644
index 00000000..14d0e94a
Binary files /dev/null and b/docs/src/images/topN.png differ
diff --git a/docs/src/images/unformatted-table.png b/docs/src/images/unformatted-table.png
new file mode 100644
index 00000000..335554b6
Binary files /dev/null and b/docs/src/images/unformatted-table.png differ
diff --git a/docs/src/index.md b/docs/src/index.md
index c1358408..2d131cf1 100644
--- a/docs/src/index.md
+++ b/docs/src/index.md
@@ -10,13 +10,13 @@ Internally, an Excel XLSX file is just a
[Zip](https://en.wikipedia.org/wiki/Zip_(file_format)) file with a set of
[XML](https://en.wikipedia.org/wiki/XML) files inside.
The formats for these XML files are described in
-the [Standard ECMA-376](https://www.ecma-international.org/publications/standards/Ecma-376.htm).
+the [Standard ECMA-376](https://ecma-international.org/publications-and-standards/standards/ecma-376/).
This package follows the EMCA-376 to parse and generate XLSX files.
## Requirements
-* Julia v1.6
+* Julia v1.8
* Linux, macOS or Windows.
@@ -63,6 +63,8 @@ and send a Pull Request.
## Alternative Packages
+* [LibXLSXWriter.jl](https://github.com/jaakkor2/LibXLSXWriter.jl)
+
* [ExcelFiles.jl](https://github.com/davidanthoff/ExcelFiles.jl)
* [ExcelReaders.jl](https://github.com/davidanthoff/ExcelReaders.jl)
diff --git a/docs/src/migration.md b/docs/src/migration.md
index d0f0921f..aeffb3d1 100644
--- a/docs/src/migration.md
+++ b/docs/src/migration.md
@@ -1,4 +1,11 @@
-# Migration Guides
+# Migration Guide
+
+!!! note
+
+ This migration guide was introduced to describe migrating from a pre v0.8 version
+ of XLSX.jl to v0.8.
+
+ It is a largely historic document now.
## Migrating Legacy Code to v0.8
diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md
index f50c135e..4aa0edda 100644
--- a/docs/src/tutorial.md
+++ b/docs/src/tutorial.md
@@ -102,18 +102,31 @@ that implements [`Tables.jl`](https://github.com/JuliaData/Tables.jl) interface.
You can use it to create a `DataFrame` from [DataFrames.jl](https://github.com/JuliaData/DataFrames.jl).
Check the docstring for `gettable` method for more advanced options.
-There's also a helper method [`XLSX.readtable`](@ref) to read from file directly, as shown in the following example.
+There are also two helper methods [`XLSX.readtable`](@ref) and [`XLSX.readto`](@ref) to read from file
+directly, as shown in the following examples.
```julia
julia> using DataFrames, XLSX
-julia> df = DataFrame(XLSX.readtable("myfile.xlsx", "mysheet"))
-3×2 DataFrames.DataFrame
-│ Row │ HeaderA │ HeaderB │
-├─────┼─────────┼──────────┤
-│ 1 │ 1 │ "first" │
-│ 2 │ 2 │ "second" │
-│ 3 │ 3 │ "third" │
+julia> df = DataFrame(XLSX.readtable("myfile.xlsx", "mysheet")) # Returns a `Tables.jl` table that `DataFrame` can accept
+3×2 DataFrame
+ Row │ HeaderA HeaderB
+ │ Int64 String
+─────┼──────────────────
+ 1 │ 1 first
+ 2 │ 2 second
+ 3 │ 3 third
+
+julia> df = XLSX.readto("myfile.xlsx", "mysheet", DataFrame) # Returns a `DataFrame` directly.
+3×2 DataFrame
+ Row │ HeaderA HeaderB
+ │ Int64 String
+─────┼──────────────────
+ 1 │ 1 first
+ 2 │ 2 second
+ 3 │ 3 third
+
+
```
## Reading Cells as a Julia Matrix
@@ -185,8 +198,10 @@ The method `XLSX.openxlsx` has a `enable_cache` option to control worksheet cell
Cache is enabled by default, so if you read a worksheet cell twice it will use the cached value instead of reading from disk
in the second time.
-If `enable_cache=false`, worksheet cells will always be read from disk.
-This is useful when you want to read a spreadsheet that doesn't fit into memory.
+If `enable_cache=false`, worksheet cells will always be read from disk. In addition, if `enable_cache=false`
+and `openxlsx` is used with do-syntax, the xlsx file itself will be opened usning `Mmap.mmap` so that the
+zip archives themselves are not read into memory. This is useful when you want to read a spreadsheet that
+doesn't fit into memory.
The following example shows how you would read worksheet cells, one row at a time,
where `myfile.xlsx` is a spreadsheet that doesn't fit into memory.
@@ -258,7 +273,7 @@ end
### Edit Existing Files
Opening a file in `read-write` mode with `XLSX.openxlsx` will open an existing Excel file for editing.
-This will preserve existing data in the original file.
+This will preserve existing data and formatting in the original file.
```julia
XLSX.openxlsx("my_new_file.xlsx", mode="rw") do xf
diff --git a/src/XLSX.jl b/src/XLSX.jl
index 59309249..3c3d6fed 100644
--- a/src/XLSX.jl
+++ b/src/XLSX.jl
@@ -7,7 +7,14 @@ import Printf.@printf
import ZipArchives
import XML
import Tables
+import Unicode
+import Colors
import Base.convert
+import UUIDs
+import Mmap
+
+import PrecompileTools as PCT # this is a small dependency. PCT avoids namespace conflict with ZipArchives (I think)
+
const SPREADSHEET_NAMESPACE_XPATH_ARG = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
const EXCEL_MAX_COLS = 16_384 # total columns supported by Excel per sheet
@@ -26,7 +33,33 @@ include("workbook.jl")
include("worksheet.jl")
include("cell.jl")
include("styles.jl")
+include("cellformat-helpers.jl")
include("cellformats.jl")
+include("conditional-format-helpers.jl") # must load before conditional-formats.jl
+include("conditional-formats.jl")
include("write.jl")
+PCT.@setup_workload begin
+ # Putting some things in `@setup_workload` instead of `@compile_workload` can reduce the size of the
+ # precompile file and potentially make loading faster.
+ s=IOBuffer()
+ t=IOBuffer()
+ PCT.@compile_workload begin
+ # all calls in this block will be precompiled, regardless of whether
+ # they belong to your package or not (on Julia 1.8 and higher)
+ f=openxlsx(joinpath(_relocatable_data_path(), "blank.xlsx"), mode="rw")
+ f[1]["A1:Z26"] = "hello World"
+ openxlsx(s, mode="w") do xf
+ xf[1][1:26, 1:26] = pi
+ end
+ _ = XLSX.readtable(seekstart(s), 1, "A:Z")
+ f= openxlsx(seekstart(s), mode="rw")
+ setConditionalFormat(f[1], :, :cellIs)
+ setConditionalFormat(f[1], "A1:Z26", :colorScale)
+ setBorder(f[1], collect(1:26), 1:26, allsides=["style"=>"thin", "color"=>"black"])
+ _ = getdata(f[1], "A1:A20")
+ writexlsx(t, f)
+ end
+end
+
end # module XLSX
diff --git a/src/cell.jl b/src/cell.jl
index 113b6907..24a52981 100644
--- a/src/cell.jl
+++ b/src/cell.jl
@@ -38,7 +38,9 @@ function Cell(c::XML.LazyNode)
# t (Cell Data Type) is an enumeration representing the cell's data type. The possible values for this attribute are defined by the ST_CellType simple type (§18.18.11).
# s (Style Index) is the index of this cell's style. Style records are stored in the Styles Part.
- @assert XML.tag(c) == "c" "`Cell` Expects a `c` (cell) XML node."
+ if XML.tag(c) != "c"
+ throw(XLSXError("`Cell` Expects a `c` (cell) XML node."))
+ end
a = XML.attributes(c) # Dict of cell attributes
@@ -75,7 +77,7 @@ function Cell(c::XML.LazyNode)
elseif length(c) == 1
v= XML.value(c[1])
else
- error("Too amny children in `t` node. Expected >=1, found: $(length(c))")
+ throw(XLSXError("Too amny children in `t` node. Expected >=1, found: $(length(c))"))
end
end
end
@@ -85,18 +87,18 @@ function Cell(c::XML.LazyNode)
# we should have only one v element
if found_v
- error("Unsupported: cell $(ref) has more than 1 `v` elements.")
+ throw(XLSXError("Unsupported: cell $(ref) has more than 1 `v` elements."))
else
found_v = true
end
- v = XML.unescape(XML.simple_value(c_child_element))
+ v = length(c_child_element)==0 ? "" : XML.unescape(XML.simple_value(c_child_element))
elseif XML.tag(c_child_element) == "f"
# we should have only one f element
if found_f
- error("Unsupported: cell $(ref) has more than 1 `f` elements.")
+ throw(XLSXError("Unsupported: cell $(ref) has more than 1 `f` elements."))
else
found_f = true
end
@@ -111,17 +113,17 @@ end
function parse_formula_from_element(c_child_element) :: AbstractFormula
if XML.tag(c_child_element) != "f"
- error("Expected nodename `f`. Found: `$(XML.tag(c_child_element))`")
+ throw(XLSXError("Expected nodename `f`. Found: `$(XML.tag(c_child_element))`"))
end
if XML.is_simple(c_child_element)
- formula_string = XML.simple_value(c_child_element)
+ formula_string = XML.unescape(XML.simple_value(c_child_element))
else
fs = [x for x in XML.children(c_child_element) if XML.nodetype(x) == XML.Text]
if length(fs)==0
formula_string=""
else
- formula_string=XML.value(fs[1])
+ formula_string=XML.unescape(XML.value(fs[1]))
end
end
@@ -131,7 +133,7 @@ function parse_formula_from_element(c_child_element) :: AbstractFormula
if haskey(a, "ref") && haskey(a, "t") && a["t"] == "shared"
- haskey(a, "si") || error("Expected shared formula to have an index. `si` attribute is missing: $c_child_element")
+ haskey(a, "si") || throw(XLSXError("Expected shared formula to have an index. `si` attribute is missing: $c_child_element"))
return ReferencedFormula(
formula_string,
@@ -141,7 +143,7 @@ function parse_formula_from_element(c_child_element) :: AbstractFormula
elseif haskey(a, "t") && a["t"] == "shared"
- haskey(a, "si") || error("Expected shared formula to have an index. `si` attribute is missing: $c_child_element")
+ haskey(a, "si") || throw(XLSXError("Expected shared formula to have an index. `si` attribute is missing: $c_child_element"))
return FormulaReference(
parse(Int, a["si"]),
@@ -238,7 +240,7 @@ function getdata(ws::Worksheet, cell::Cell) :: CellValueType
elseif cell.value == "1"
return true
else
- error("Unknown boolean value: $(cell.value).")
+ throw(XLSXError("Unknown boolean value: $(cell.value)."))
end
elseif cell.datatype == "str"
# plain string
@@ -249,17 +251,21 @@ function getdata(ws::Worksheet, cell::Cell) :: CellValueType
end
end
- error("Couldn't parse data for $cell.")
+ throw(XLSXError("Couldn't parse data for $cell."))
end
function _celldata_datetime(v::AbstractString, _is_date_1904::Bool) :: Union{Dates.DateTime, Dates.Date, Dates.Time}
# does not allow empty string
- @assert !isempty(v) "Cannot convert an empty string into a datetime value."
+ if isempty(v)
+ throw(XLSXError("Cannot convert an empty string into a datetime value."))
+ end
if occursin(".", v) || v == "0"
time_value = parse(Float64, v)
- @assert time_value >= 0
+ if time_value < 0
+ throw(XLSXError("Cannot have a datetime value < 0. Got $time_value"))
+ end
if time_value <= 1
# Time
@@ -279,8 +285,11 @@ end
# To represent Time, Excel uses the decimal part
# of a floating point number. `1` equals one day.
function excel_value_to_time(x::Float64) :: Dates.Time
- @assert x >= 0 && x <= 1
- return Dates.Time(Dates.Nanosecond(round(Int, x * 86400) * 1E9 ))
+ if x >= 0 && x <= 1
+ return Dates.Time(Dates.Nanosecond(round(Int, x * 86400) * 1E9 ))
+ else
+ throw(XLSXError("A value must be between 0 and 1 to be converted to time. Got $x"))
+ end
end
time_to_excel_value(x::Dates.Time) :: Float64 = Dates.value(x) / ( 86400 * 1E9 )
@@ -307,7 +316,9 @@ end
# The integer part represents the Date.
# See also XLSX.isdate1904.
function excel_value_to_datetime(x::Float64, _is_date_1904::Bool) :: Dates.DateTime
- @assert x >= 0
+ if x < 0
+ throw(XLSXError("Cannot have a datetime value < 0. Got $x"))
+ end
local dt::Dates.Date
local hr::Dates.Time
diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl
new file mode 100644
index 00000000..d29bf600
--- /dev/null
+++ b/src/cellformat-helpers.jl
@@ -0,0 +1,978 @@
+
+const font_tags = ["b", "i", "u", "strike", "outline", "shadow", "condense", "extend", "sz", "color", "name", "scheme"]
+const border_tags = ["left", "right", "top", "bottom", "diagonal"]
+const fill_tags = ["patternFill"]
+const builtinFormats = Dict(
+ "0" => "General",
+ "1" => "0",
+ "2" => "0.00",
+ "3" => "#,##0",
+ "4" => "#,##0.00",
+ "5" => "\$#,##0_);(\$#,##0)",
+ "6" => "\$#,##0_);Red",
+ "7" => "\$#,##0.00_);(\$#,##0.00)",
+ "8" => "\$#,##0.00_);Red",
+ "9" => "0%",
+ "10" => "0.00%",
+ "11" => "0.00E+00",
+ "12" => "# ?/?",
+ "13" => "# ??/??",
+ "14" => "m/d/yyyy",
+ "15" => "d-mmm-yy",
+ "16" => "d-mmm",
+ "17" => "mmm-yy",
+ "18" => "h:mm AM/PM",
+ "19" => "h:mm:ss AM/PM",
+ "20" => "h:mm",
+ "21" => "h:mm:ss",
+ "22" => "m/d/yyyy h:mm",
+ "37" => "#,##0_);(#,##0)",
+ "38" => "#,##0_);Red",
+ "39" => "#,##0.00_);(#,##0.00)",
+ "40" => "#,##0.00_);Red",
+ "45" => "mm:ss",
+ "46" => "[h]:mm:ss",
+ "47" => "mmss.0",
+ "48" => "##0.0E+0",
+ "49" => "@"
+)
+const builtinFormatNames = Dict(
+ "General" => 0,
+ "Number" => 2,
+ "Currency" => 7,
+ "Percentage" => 9,
+ "ShortDate" => 14,
+ "LongDate" => 15,
+ "Time" => 21,
+ "Scientific" => 48
+)
+const floatformats = r"""
+\.[0#?]|
+[0#?]e[+-]?[0#?]|
+[0#?]/[0#?]|
+%
+"""ix
+
+#
+# -- A bunch of helper functions ...
+#
+
+function copynode(o::XML.Node)
+ n = XML.Node(o.nodetype, o.tag, o.attributes, o.value, isnothing(o.children) ? nothing : [copynode(x) for x in o.children])
+# n = deepcopy(o)
+ return n
+end
+function do_sheet_names_match(ws::Worksheet, rng::T) where {T<:Union{SheetCellRef,AbstractSheetCellRange}}
+ if ws.name == rng.sheet
+ return true
+ else
+ throw(XLSXError("Worksheet `$(ws.name)` does not match sheet in cell reference: `$(rng.sheet)`"))
+ end
+end
+function buildNode(tag::String, attributes::Dict{String,Union{Nothing,Dict{String,String}}})::XML.Node
+ if tag == "font"
+ attribute_tags = font_tags
+ elseif tag == "border"
+ attribute_tags = border_tags
+ elseif tag == "fill"
+ attribute_tags = fill_tags
+ else
+ throw(XLSXError("Unknown tag: $tag"))
+ end
+ new_node = XML.Element(tag)
+ for a in attribute_tags # Use this as a device to keep ordering constant for Excel
+ if tag == "font"
+ if haskey(attributes, a)
+ if isnothing(attributes[a])
+ cnode = XML.Element(a)
+ else
+ cnode = XML.Node(XML.Element, a, XML.OrderedDict{String,String}(), nothing, tag ∈ ["border", "fill"] ? Vector{XML.Node}() : nothing)
+ for (k, v) in attributes[a]
+ cnode[k] = v
+ end
+ end
+ push!(new_node, cnode)
+ end
+ elseif tag == "border"
+ if haskey(attributes, a)
+ if isnothing(attributes[a])
+ cnode = XML.Element(a)
+ else
+ cnode = XML.Node(XML.Element, a, XML.OrderedDict{String,String}(), nothing, tag ∈ ["border", "fill"] ? Vector{XML.Node}() : nothing)
+ color = XML.Element("color")
+ for (k, v) in attributes[a]
+ if k == "style" && v != "none"
+ cnode[k] = v
+ elseif k == "direction"
+ if v in ["up", "both"]
+ new_node["diagonalUp"] = "1"
+ end
+ if v in ["down", "both"]
+ new_node["diagonalDown"] = "1"
+ end
+ else
+ color[k] = v
+ end
+ end
+ if length(XML.attributes(color)) > 0 # Don't push an empty color.
+ push!(cnode, color)
+ end
+ end
+ push!(new_node, cnode)
+ end
+ elseif tag == "fill"
+ if haskey(attributes, a)
+ if isnothing(attributes[a])
+ cnode = XML.Element(a)
+ else
+ cnode = XML.Node(XML.Element, a, XML.OrderedDict{String,String}(), nothing, tag ∈ ["border", "fill"] ? Vector{XML.Node}() : nothing)
+ patternfill = XML.Element("patternFill")
+ fgcolor = XML.Element("fgColor")
+ bgcolor = XML.Element("bgColor")
+ for (k, v) in attributes[a]
+ if k == "patternType"
+ patternfill[k] = v
+ elseif first(k, 2) == "fg"
+ fgcolor[k[3:end]] = v
+ elseif first(k, 2) == "bg"
+ bgcolor[k[3:end]] = v
+ end
+ end
+ if !haskey(patternfill, "patternType")
+ throw(XLSXError("No `patternType` attribute found."))
+ end
+ length(XML.attributes(fgcolor)) > 0 && push!(patternfill, fgcolor)
+ length(XML.attributes(bgcolor)) > 0 && push!(patternfill, bgcolor)
+ end
+ push!(new_node, patternfill)
+ end
+ #else
+ end
+ end
+ return new_node
+end
+function isInDim(ws::Worksheet, dim::CellRange, rng::CellRange)
+ if !issubset(rng, dim)
+ throw(XLSXError("Cell range $rng is out of bounds. Worksheet `$(ws.name)` only has dimension `$dim`."))
+ end
+ return true
+end
+function isInDim(ws::Worksheet, dim::CellRange, row, col)
+ if maximum(row) > dim.stop.row_number || minimum(row) < dim.start.row_number
+ throw(XLSXError("Row range $row is out of bounds. Worksheet `$(ws.name)` only has dimension `$dim`."))
+ end
+ if maximum(col) > dim.stop.column_number || minimum(col) < dim.start.column_number
+ throw(XLSXError("Column range $col is out of bounds. Worksheet `$(ws.name)` only has dimension `$dim`."))
+ end
+ return true
+end
+function get_new_formatId(wb::Workbook, format::String)::Int
+ if haskey(builtinFormatNames, uppercasefirst(format)) # User specified a format by name
+ return builtinFormatNames[format]
+ else # user specified a format code
+ code = lowercase(format)
+ code = remove_formatting(code)
+ if !occursin(floatformats, code) && !any(map(x -> occursin(x, code), DATETIME_CODES)) # Only a very weak test!
+ throw(XLSXError("Specified format is not a valid numFmt: $format"))
+ end
+
+ xroot = styles_xmlroot(wb)
+ i, j = get_idces(xroot, "styleSheet", "numFmts")
+ if isnothing(j) # There are no existing custom formats
+ return styles_add_numFmt(wb, format)
+ else
+ existing_elements_count = length(XML.children(xroot[i][j]))
+ if parse(Int, xroot[i][j]["count"]) != existing_elements_count
+ throw(XLSXError("Wrong number of font elements found: $existing_elements_count. Expected $(parse(Int, xroot[i][j]["count"]))."))
+ end
+
+ format_node = XML.Element("numFmt";
+ numFmtId=string(existing_elements_count + PREDEFINED_NUMFMT_COUNT),
+ formatCode=XML.escape(format)
+ )
+
+ return styles_add_cell_attribute(wb, format_node, "numFmts") + PREDEFINED_NUMFMT_COUNT
+ end
+ end
+end
+function update_template_xf(ws::Worksheet, existing_style::CellDataFormat, attributes::Vector{String}, vals::Vector{String})::CellDataFormat
+ old_cell_xf = styles_cell_xf(ws.package.workbook, Int(existing_style.id))
+ new_cell_xf = copynode(old_cell_xf)
+ if length(attributes) != length(vals)
+ throw(XLSXError("Attributes and values must be of the same length."))
+ end
+ for (a, v) in zip(attributes, vals)
+ new_cell_xf[a] = v
+ end
+ return styles_add_cell_xf(ws.package.workbook, new_cell_xf)
+end
+function update_template_xf(ws::Worksheet, allXfNodes::Vector{XML.Node}, existing_style::CellDataFormat, attributes::Vector{String}, vals::Vector{String})::CellDataFormat
+ old_cell_xf = styles_cell_xf(allXfNodes, Int(existing_style.id))
+ new_cell_xf = copynode(old_cell_xf)
+ if length(attributes) != length(vals)
+ throw(XLSXError("Attributes and values must be of the same length."))
+ end
+ for (a, v) in zip(attributes, vals)
+ new_cell_xf[a] = v
+ end
+ return styles_add_cell_xf(ws.package.workbook, new_cell_xf)
+end
+function update_template_xf(ws::Worksheet, existing_style::CellDataFormat, alignment::XML.Node)::CellDataFormat
+ old_cell_xf = styles_cell_xf(ws.package.workbook, Int(existing_style.id))
+ new_cell_xf = copynode(old_cell_xf)
+ if isnothing(new_cell_xf.children)
+ new_cell_xf=XML.Node(new_cell_xf, alignment)
+ elseif length(XML.children(new_cell_xf)) == 0
+ push!(new_cell_xf, alignment)
+ else
+ new_cell_xf[1] = alignment
+ end
+ return styles_add_cell_xf(ws.package.workbook, new_cell_xf)
+end
+function update_template_xf(ws::Worksheet, allXfNodes::Vector{XML.Node}, existing_style::CellDataFormat, alignment::XML.Node)::CellDataFormat
+ old_cell_xf = styles_cell_xf(allXfNodes, Int(existing_style.id))
+ new_cell_xf = copynode(old_cell_xf)
+ if isnothing(new_cell_xf.children)
+ new_cell_xf=XML.Node(new_cell_xf, alignment)
+ elseif length(XML.children(new_cell_xf)) == 0
+ push!(new_cell_xf, alignment)
+ else
+ new_cell_xf[1] = alignment
+ end
+ return styles_add_cell_xf(ws.package.workbook, new_cell_xf)
+end
+
+# Only used in testing!
+function styles_add_cell_font(wb::Workbook, attributes::Dict{String,Union{Dict{String,String},Nothing}})::Int
+ new_font = buildNode("font", attributes)
+ return styles_add_cell_attribute(wb, new_font, "fonts")
+end
+
+# Used by setFont(), setBorder(), setFill(), setAlignment() and setNumFmt()
+function styles_add_cell_attribute(wb::Workbook, new_att::XML.Node, att::String)::Int
+ xroot = styles_xmlroot(wb)
+ i, j = get_idces(xroot, "styleSheet", att)
+ existing_elements_count = length(XML.children(xroot[i][j]))
+ if parse(Int, xroot[i][j]["count"]) != existing_elements_count
+ throw(XLSXError("Wrong number of elements elements found: $existing_elements_count. Expected $(parse(Int, xroot[i][j]["count"]))."))
+ end
+
+ # Check new_att doesn't duplicate any existing att. If yes, use that rather than create new.
+ for (k, node) in enumerate(XML.children(xroot[i][j]))
+ if XML.tag(new_att) == "numFmt" # mustn't compare numFmtId attribute for formats
+ if node["formatCode"] == new_att["formatCode"]
+ return k - 1 # CellDataFormat is zero-indexed
+ end
+ else
+ if node == new_att
+ return k - 1 # CellDataFormat is zero-indexed
+ end
+ end
+ end
+
+ push!(xroot[i][j], new_att)
+ xroot[i][j]["count"] = string(existing_elements_count + 1)
+
+ return existing_elements_count # turns out this is the new index (because it's zero-based)
+end
+function process_sheetcell(f::Function, xl::XLSXFile, sheetcell::String; kw...)::Int
+ if is_workbook_defined_name(xl, sheetcell)
+ v = get_defined_name_value(xl.workbook, sheetcell)
+ if is_defined_name_value_a_constant(v)
+ throw(XLSXError("Can only assign attributes to cells but `$(sheetcell)` is a constant: $(sheetcell)=$v."))
+ elseif is_defined_name_value_a_reference(v)
+ newid = process_sheetcell(f, xl, string(v); kw...)
+ else
+ throw(XLSXError("Unexpected defined name value: $v."))
+ end
+ elseif is_valid_non_contiguous_sheetcellrange(sheetcell)
+ sheetncrng = NonContiguousRange(sheetcell)
+ !hassheet(xl, sheetncrng.sheet) && throw(XLSXError("Sheet $(sheetncrng.sheet) not found."))
+ newid = f(xl[sheetncrng.sheet], sheetncrng; kw...)
+ elseif is_valid_sheet_column_range(sheetcell)
+ sheetcolrng = SheetColumnRange(sheetcell)
+ !hassheet(xl, sheetcolrng.sheet) && throw(XLSXError("Sheet $(sheetcolrng.sheet) not found."))
+ newid = f(xl[sheetcolrng.sheet], sheetcolrng.colrng; kw...)
+ elseif is_valid_sheet_row_range(sheetcell)
+ sheetrowrng = SheetRowRange(sheetcell)
+ !hassheet(xl, sheetrowrng.sheet) && throw(XLSXError("Sheet $(sheetrowrng.sheet) not found."))
+ newid = f(xl[sheetrowrng.sheet], sheetrowrng.rowrng; kw...)
+ elseif is_valid_sheet_cellrange(sheetcell)
+ sheetcellrng = SheetCellRange(sheetcell)
+ !hassheet(xl, sheetcellrng.sheet) && throw(XLSXError("Sheet $(sheetcellrng.sheet) not found."))
+ newid = f(xl[sheetcellrng.sheet], sheetcellrng.rng; kw...)
+ elseif is_valid_sheet_cellname(sheetcell)
+ ref = SheetCellRef(sheetcell)
+ !hassheet(xl, ref.sheet) && throw(XLSXError("Sheet $(ref.sheet) not found."))
+ newid = f(getsheet(xl, ref.sheet), ref.cellref; kw...)
+ else
+ throw(XLSXError("Invalid sheet cell reference: $sheetcell"))
+ end
+ return newid
+end
+function process_ranges(f::Function, ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int
+ # Moved the tests for defined names to be first in case a name looks like a column name (e.g. "ID")
+ if is_worksheet_defined_name(ws, ref_or_rng)
+ v = get_defined_name_value(ws, ref_or_rng)
+ if is_defined_name_value_a_constant(v)
+ throw(XLSXError("Can only assign attributes to cells but `$(ref_or_rng)` is a constant: $(ref_or_rng)=$v."))
+ elseif is_defined_name_value_a_reference(v)
+ wb = get_workbook(ws)
+ newid = f(get_xlsxfile(wb), string(v); kw...)
+ else
+ throw(XLSXError("Unexpected defined name value: $v."))
+ end
+ elseif is_workbook_defined_name(get_workbook(ws), ref_or_rng)
+ wb = get_workbook(ws)
+ v = get_defined_name_value(wb, ref_or_rng)
+ if is_defined_name_value_a_constant(v)
+ throw(XLSXError("Can only assign attributes to cells but `$(ref_or_rng)` is a constant: $(ref_or_rng)=$v."))
+ elseif is_defined_name_value_a_reference(v)
+ if is_valid_non_contiguous_range(string(v))
+ _ = f.(Ref(get_xlsxfile(wb)), replace.(split(string(v), ","), "'" => "", "\$" => ""); kw...)
+ newid = -1
+ else
+ newid = f(get_xlsxfile(wb), replace(string(v), "'" => "", "\$" => ""); kw...)
+ end
+ else
+ throw(XLSXError("Unexpected defined name value: $v."))
+ end
+ elseif is_valid_column_range(ref_or_rng)
+ newid = f(ws, ColumnRange(ref_or_rng); kw...)
+ elseif is_valid_row_range(ref_or_rng)
+ newid = f(ws, RowRange(ref_or_rng); kw...)
+ elseif is_valid_cellrange(ref_or_rng)
+ newid = f(ws, CellRange(ref_or_rng); kw...)
+ elseif is_valid_cellname(ref_or_rng)
+ newid = f(ws, CellRef(ref_or_rng); kw...)
+ elseif is_valid_sheet_cellname(ref_or_rng)
+ newid = f(ws, SheetCellRef(ref_or_rng); kw...)
+ elseif is_valid_sheet_cellrange(ref_or_rng)
+ newid = f(ws, SheetCellRange(ref_or_rng); kw...)
+ elseif is_valid_sheet_column_range(ref_or_rng)
+ newid = f(ws, SheetColumnRange(ref_or_rng); kw...)
+ elseif is_valid_sheet_row_range(ref_or_rng)
+ newid = f(ws, SheetRowRange(ref_or_rng); kw...)
+ elseif is_valid_non_contiguous_cellrange(ref_or_rng)
+ newid = f(ws, NonContiguousRange(ws, ref_or_rng); kw...)
+ elseif is_valid_non_contiguous_sheetcellrange(ref_or_rng)
+ nc = NonContiguousRange(ref_or_rng)
+ newid = do_sheet_names_match(ws, nc) && f(ws, nc; kw...)
+ else
+ throw(XLSXError("Invalid cell reference or range: $ref_or_rng"))
+ end
+ return newid
+end
+function process_columnranges(f::Function, ws::Worksheet, colrng::ColumnRange; kw...)::Int
+ bounds = column_bounds(colrng)
+ dim = (get_dimension(ws))
+ left = bounds[begin]
+ right = bounds[end]
+ top = dim.start.row_number
+ bottom = dim.stop.row_number
+
+ OK = dim.start.column_number <= left
+ OK &= dim.stop.column_number >= right
+ OK &= dim.start.row_number <= top
+ OK &= dim.stop.row_number >= bottom
+
+ if OK
+ rng = CellRange(top, left, bottom, right)
+ return f(ws, rng; kw...)
+ else
+ throw(XLSXError("Column range $colrng is out of bounds. Worksheet `$(ws.name)` only has dimension `$dim`."))
+ end
+end
+function process_rowranges(f::Function, ws::Worksheet, rowrng::RowRange; kw...)::Int
+ bounds = row_bounds(rowrng)
+ dim = (get_dimension(ws))
+ top = bounds[begin]
+ bottom = bounds[end]
+ left = dim.start.column_number
+ right = dim.stop.column_number
+
+ OK = dim.start.column_number <= left
+ OK &= dim.stop.column_number >= right
+ OK &= dim.start.row_number <= top
+ OK &= dim.stop.row_number >= bottom
+
+ if OK
+ rng = CellRange(top, left, bottom, right)
+ return f(ws, rng; kw...)
+ else
+ throw(XLSXError("Row range $rowrng is out of bounds. Worksheet `$(ws.name)` only has dimension `$dim`."))
+ end
+end
+function process_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int
+ bounds = nc_bounds(ncrng)
+ if length(ncrng) == 1
+ single = true
+ else
+ single = false
+ end
+ dim = (get_dimension(ws))
+ OK = dim.start.column_number <= bounds.start.column_number
+ OK &= dim.stop.column_number >= bounds.stop.column_number
+ OK &= dim.start.row_number <= bounds.start.row_number
+ OK &= dim.stop.row_number >= bounds.stop.row_number
+ if OK
+ for r in ncrng.rng
+ if r isa CellRef && getcell(ws, r) isa EmptyCell
+ single && throw(XLSXError("Cannot set format for an `EmptyCell`: $(r.name). Set the value first."))
+ continue
+ end
+ _ = f(ws, r; kw...)
+ end
+ return -1
+ else
+ throw(XLSXError("Non-contiguous range $ncrng is out of bounds. Worksheet `$(ws.name)` only has dimension `$dim`."))
+ end
+end
+function process_cellranges(f::Function, ws::Worksheet, rng::CellRange; kw...)::Int
+ if length(rng) == 1
+ single = true
+ else
+ single = false
+ end
+ isInDim(ws, get_dimension(ws), rng)
+ for cellref in rng
+ if getcell(ws, cellref) isa EmptyCell
+ single && throw(XLSXError("Cannot set format for an `EmptyCell`: $(cellref.name). Set the value first."))
+ continue
+ end
+ _ = f(ws, cellref; kw...)
+ end
+ return -1 # Each cell may have a different attribute Id so we can't return a single value.
+end
+function process_get_sheetcell(f::Function, xl::XLSXFile, sheetcell::String; kw...)
+ ref = SheetCellRef(sheetcell)
+ !hassheet(xl, ref.sheet) && throw(XLSXError("Sheet $(ref.sheet) not found."))
+ ws = getsheet(xl, ref.sheet)
+ d = get_dimension(ws)
+ if ref.cellref ∉ d
+ throw(XLSXError("Cell specified is outside sheet dimension \"$d\""))
+ end
+ return f(ws, ref.cellref; kw...)
+end
+function process_get_cellref(f::Function, ws::Worksheet, cellref::CellRef; kw...)
+ wb = get_workbook(ws)
+ cell = getcell(ws, cellref)
+ d = get_dimension(ws)
+ if cellref ∉ d
+ throw(XLSXError("Cell specified is outside sheet dimension \"$d\""))
+ end
+ if cell isa EmptyCell || cell.style == ""
+ return nothing
+ end
+ cell_style = styles_cell_xf(wb, parse(Int, cell.style))
+ return f(wb, cell_style; kw...)
+end
+function process_get_cellname(f::Function, ws::Worksheet, ref_or_rng::AbstractString; kw...)
+ if is_workbook_defined_name(get_workbook(ws), ref_or_rng)
+ wb = get_workbook(ws)
+ v = get_defined_name_value(wb, ref_or_rng)
+ if is_defined_name_value_a_constant(v)
+ throw(XLSXError("Can only assign attributes to cells but `$(ref_or_rng)` is a constant: $(ref_or_rng)=$v."))
+ elseif is_defined_name_value_a_reference(v)
+ new_att = f(get_xlsxfile(wb), replace(string(v), "'" => ""); kw...)
+ else
+ throw(XLSXError("Unexpected defined name value: $v."))
+ end
+ elseif is_valid_cellname(ref_or_rng)
+ new_att = f(ws, CellRef(ref_or_rng); kw...)
+ else
+ throw(XLSXError("Invalid cell reference: $ref_or_rng"))
+ end
+ return new_att
+end
+
+#
+# - Used for indexing `setAttribute` family of functions
+#
+function process_colon(f::Function, ws::Worksheet, row, col; kw...)
+ dim = get_dimension(ws)
+ @assert isnothing(row) || isnothing(col) "Something wrong here!"
+ if isnothing(row) && isnothing(col)
+ return f(ws, dim; kw...)
+ elseif isnothing(col)
+ rng = CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number))
+ else
+ rng = CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col)))
+# else
+# throw(XLSXError("Something wrong here!"))
+ end
+
+ return f(ws, rng; kw...)
+end
+function process_veccolon(f::Function, ws::Worksheet, row, col; kw...)
+ dim = get_dimension(ws)
+ @assert isnothing(row) || isnothing(col) "Something wrong here!"
+ if isnothing(col)
+ col = dim.start.column_number:dim.stop.column_number
+ else
+ row = dim.start.row_number:dim.stop.row_number
+# else
+# throw(XLSXError("Something wrong here!"))
+ end
+ isInDim(ws, dim, row, col)
+ if length(row) == 1 && length(col) == 1
+ single = true
+ else
+ single = false
+ end
+ for a in row
+ for b in col
+ cellref = CellRef(a, b)
+ if getcell(ws, cellref) isa EmptyCell
+ single && throw(XLSXError("Cannot set attribute for an `EmptyCell`: $(cellref.name). Set the value first."))
+ continue
+ end
+ f(ws, cellref; kw...)
+ end
+ end
+ return -1
+end
+function process_vecint(f::Function, ws::Worksheet, row, col; kw...)
+ if length(col) == 1 && length(row) == 1
+ single = true
+ else
+ single = false
+ end
+ dim = get_dimension(ws)
+ isInDim(ws, dim, row, col)
+ for a in row, b in col
+ cellref = CellRef(a, b)
+ if getcell(ws, cellref) isa EmptyCell
+ single && throw(XLSXError("Cannot set format for an `EmptyCell`: $(cellref.name). Set the value first."))
+ continue
+ end
+ f(ws, cellref; kw...)
+ end
+ return -1
+end
+
+#
+# - Used for indexing `setUniformAttribute` family of functions
+#
+
+#
+# Most setUniform functions (but not Style or Alignment - see below)
+#
+function process_uniform_core(f::Function, ws::Worksheet, allXfNodes::Vector{XML.Node}, cellref::CellRef, atts::Vector{String}, newid::Union{Int,Nothing}, first::Bool; kw...)
+ cell = getcell(ws, cellref)
+ if cell isa EmptyCell # Can't add a attribute to an empty cell.
+ return newid, first
+ end
+ if first # Get the attribute of the first cell in the range.
+ newid = f(ws, cellref; kw...)
+ first = false
+ else # Apply the same attribute to the rest of the cells in the range.
+ if cell.style == ""
+ cell.style = string(get_num_style_index(ws, allXfNodes, 0).id)
+ end
+ cell.style = string(update_template_xf(ws, allXfNodes, CellDataFormat(parse(Int, cell.style)), atts, [string(newid), "1"]).id)
+ end
+ return newid, first
+end
+function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange, atts::Vector{String}; kw...)
+ if !get_xlsxfile(ws).use_cache_for_sheet_data
+ throw(XLSXError("Cannot set uniform attributes because cache is not enabled."))
+ end
+ allXfNodes=find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":cellXfs/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":xf", styles_xmlroot(get_workbook(ws)))
+ let newid::Union{Int,Nothing}, first::Bool
+ newid = nothing
+ first = true
+ isInDim(ws, get_dimension(ws), rng)
+ for cellref in rng
+ newid, first = process_uniform_core(f, ws, allXfNodes, cellref, atts, newid, first; kw...)
+ end
+ if first
+ newid = -1
+ end
+ return newid
+ end
+end
+function process_uniform_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguousRange, atts::Vector{String}; kw...)::Int
+ allXfNodes=find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":cellXfs/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":xf", styles_xmlroot(get_workbook(ws)))
+ bounds = nc_bounds(ncrng)
+ if length(ncrng) == 1
+ single = true
+ else
+ single = false
+ end
+ dim = (get_dimension(ws))
+ OK = dim.start.column_number <= bounds.start.column_number
+ OK &= dim.stop.column_number >= bounds.stop.column_number
+ OK &= dim.start.row_number <= bounds.start.row_number
+ OK &= dim.stop.row_number >= bounds.stop.row_number
+ if OK
+ let newid::Union{Int,Nothing}, first::Bool
+ newid = nothing
+ first = true
+ for r in ncrng.rng
+ @assert r isa CellRef || r isa CellRange "Something wrong here"
+ if r isa CellRef
+ if getcell(ws, r) isa EmptyCell
+ single && throw(XLSXError("Cannot set format for an `EmptyCell`: $(r.name). Set the value first."))
+ continue
+ end
+ newid, first = process_uniform_core(f, ws, allXfNodes, r, atts, newid, first; kw...)
+ else
+ for c in r
+ newid, first = process_uniform_core(f, ws, allXfNodes, c, atts, newid, first; kw...)
+ end
+# else
+# throw(XLSXError("Something wrong here!"))
+ end
+ end
+ if first
+ newid = -1
+ end
+ return newid
+ end
+ else
+ throw(XLSXError("Non-contiguous range $ncrng is out of bounds. Worksheet `$(ws.name)` only has dimension `$dim`."))
+ end
+end
+function process_uniform_veccolon(f::Function, ws::Worksheet, row, col, atts::Vector{String}; kw...)
+ dim = get_dimension(ws)
+ @assert isnothing(row) || isnothing(col) "Something wrong here!"
+ allXfNodes=find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":cellXfs/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":xf", styles_xmlroot(get_workbook(ws)))
+ if isnothing(col)
+ col = dim.start.column_number:dim.stop.column_number
+ else
+ row = dim.start.row_number:dim.stop.row_number
+# else
+# throw(XLSXError("Something wrong here!"))
+ end
+ isInDim(ws, dim, row, col)
+ let newid::Union{Int,Nothing}, first::Bool
+ newid = nothing
+ first = true
+ for a in row
+ for b in col
+ cellref = CellRef(a, b)
+ if getcell(ws, cellref) isa EmptyCell
+ continue
+ end
+ newid, first = process_uniform_core(f, ws, allXfNodes, cellref, atts, newid, first; kw...)
+ end
+ end
+ if first
+ newid = -1
+ end
+ return newid
+ end
+end
+function process_uniform_vecint(f::Function, ws::Worksheet, row, col, atts::Vector{String}; kw...)
+ allXfNodes=find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":cellXfs/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":xf", styles_xmlroot(get_workbook(ws)))
+ let newid::Union{Int,Nothing}, first::Bool
+ dim = get_dimension(ws)
+ newid = nothing
+ first = true
+ isInDim(ws, dim, row, col)
+ for a in row, b in col
+ cellref = CellRef(a, b)
+ if getcell(ws, cellref) isa EmptyCell
+ continue
+ end
+ newid, first = process_uniform_core(f, ws, allXfNodes, cellref, atts, newid, first; kw...)
+ end
+ if first
+ newid = -1
+ end
+ return newid
+ end
+end
+
+#
+# UniformStyles
+#
+function process_uniform_core(ws::Worksheet, cellref::CellRef, newid::Union{Int,Nothing}, first::Bool)
+ cell = getcell(ws, cellref)
+ if cell isa EmptyCell # Can't add a attribute to an empty cell.
+ return newid, first
+ end
+ if first # Get the style of the first cell in the range.
+ newid = parse(Int, cell.style)
+ first = false
+ else # Apply the same style to the rest of the cells in the range.
+ cell.style = string(newid)
+ end
+ return newid, first
+end
+function process_uniform_ncranges(ws::Worksheet, ncrng::NonContiguousRange)::Int
+ bounds = nc_bounds(ncrng)
+ if length(ncrng) == 1
+ single = true
+ else
+ single = false
+ end
+ dim = (get_dimension(ws))
+ OK = dim.start.column_number <= bounds.start.column_number
+ OK &= dim.stop.column_number >= bounds.stop.column_number
+ OK &= dim.start.row_number <= bounds.start.row_number
+ OK &= dim.stop.row_number >= bounds.stop.row_number
+ if OK
+ let newid::Union{Int,Nothing}, first::Bool
+ newid = nothing
+ first = true
+ for r in ncrng.rng
+ @assert r isa CellRef || r isa CellRange "Something wrong here"
+ if r isa CellRef
+ if getcell(ws, r) isa EmptyCell
+ single && throw(XLSXError("Cannot set format for an `EmptyCell`: $(r.name). Set the value first."))
+ continue
+ end
+ newid, first = process_uniform_core(ws, r, newid, first)
+ else
+ for c in r
+ newid, first = process_uniform_core(ws, c, newid, first)
+ end
+# else
+# throw(XLSXError("Something wrong here!"))
+ end
+ end
+ if first
+ newid = -1
+ end
+ return newid
+ end
+ else
+ throw(XLSXError("Non-contiguous range $ncrng is out of bounds. Worksheet `$(ws.name)` only has dimension `$dim`."))
+ end
+end
+function process_colon(ws::Worksheet, row, col)
+ dim = get_dimension(ws)
+ @assert isnothing(row) || isnothing(col) "Something wrong here!"
+ if isnothing(row) && isnothing(col)
+ return setUniformStyle(ws, dim)
+ elseif isnothing(col)
+ rng = CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number))
+ else
+ rng = CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col)))
+# else
+# throw(XLSXError("Something wrong here!"))
+ end
+
+ return setUniformStyle(ws, rng)
+end
+function process_uniform_veccolon(ws::Worksheet, row, col)
+ dim = get_dimension(ws)
+ @assert isnothing(row) || isnothing(col) "Something wrong here!"
+ if isnothing(col)
+ col = dim.start.column_number:dim.stop.column_number
+ else
+ row = dim.start.row_number:dim.stop.row_number
+# else
+# throw(XLSXError("Something wrong here!"))
+ end
+ isInDim(ws, dim, row, col)
+ let newid::Union{Int,Nothing}, first::Bool
+ newid = nothing
+ first = true
+ for a in row
+ for b in col
+ cellref = CellRef(a, b)
+ if getcell(ws, cellref) isa EmptyCell
+ continue
+ end
+ newid, first = process_uniform_core(ws, cellref, newid, first)
+ end
+ end
+ if first
+ newid = -1
+ end
+ return newid
+ end
+end
+function process_uniform_vecint(ws::Worksheet, row, col)
+ let newid::Union{Int,Nothing}, first::Bool
+ dim = get_dimension(ws)
+ newid = nothing
+ first = true
+ isInDim(ws, dim, row, col)
+ for a in row, b in col
+ cellref = CellRef(a, b)
+ if getcell(ws, cellref) isa EmptyCell
+ continue
+ end
+ newid, first = process_uniform_core(ws, cellref, newid, first)
+ end
+ if first
+ newid = -1
+ end
+ return newid
+ end
+end
+
+#
+# Alignment is different
+#
+function process_uniform_core(f::Function, ws::Worksheet, allXfNodes::Vector{XML.Node}, cellref::CellRef, newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing}; kw...) # setUniformAlignment is different
+ cell = getcell(ws, cellref)
+ if cell isa EmptyCell # Can't add a attribute to an empty cell.
+ return newid, first, alignment_node
+ end
+ if first # Get the attribute of the first cell in the range.
+ newid = f(ws, cellref; kw...)
+ new_alignment = getAlignment(ws, cellref).alignment["alignment"]
+ alignment_node = XML.Node(XML.Element, "alignment", new_alignment, nothing, nothing)
+ first = false
+ else # Apply the same attribute to the rest of the cells in the range.
+ if cell.style == ""
+ cell.style = string(get_num_style_index(ws, allXfNodes, 0).id)
+ end
+ cell.style = string(update_template_xf(ws, allXfNodes, CellDataFormat(parse(Int, cell.style)), alignment_node).id)
+ end
+ return newid, first, alignment_node
+end
+function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange; kw...)
+ if !get_xlsxfile(ws).use_cache_for_sheet_data
+ throw(XLSXError("Cannot set uniform attributes because cache is not enabled."))
+ end
+ allXfNodes=find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":cellXfs/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":xf", styles_xmlroot(get_workbook(ws)))
+ let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing}
+ newid = nothing
+ first = true
+ alignment_node = nothing
+ isInDim(ws, get_dimension(ws), rng)
+ for cellref in rng
+ if getcell(ws, cellref) isa EmptyCell
+ continue
+ end
+ newid, first, alignment_node = process_uniform_core(f, ws, allXfNodes, cellref, newid, first, alignment_node; kw...)
+ end
+ if first
+ newid = -1
+ end
+ return newid
+ end
+end
+function process_uniform_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int
+ allXfNodes=find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":cellXfs/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":xf", styles_xmlroot(get_workbook(ws)))
+ bounds = nc_bounds(ncrng)
+ if length(ncrng) == 1
+ single = true
+ else
+ single = false
+ end
+ dim = (get_dimension(ws))
+ OK = dim.start.column_number <= bounds.start.column_number
+ OK &= dim.stop.column_number >= bounds.stop.column_number
+ OK &= dim.start.row_number <= bounds.start.row_number
+ OK &= dim.stop.row_number >= bounds.stop.row_number
+ if OK
+ let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing}
+ newid = nothing
+ first = true
+ alignment_node = nothing
+ for r in ncrng.rng
+ @assert r isa CellRef || r isa CellRange "Something wrong here"
+ if r isa CellRef && getcell(ws, r) isa EmptyCell
+ single && throw(XLSXError("Cannot set format for an `EmptyCell`: $(r.name). Set the value first."))
+ continue
+ end
+ if r isa CellRef
+ if getcell(ws, r) isa EmptyCell
+ single && throw(XLSXError("Cannot set format for an `EmptyCell`: $(r.name). Set the value first."))
+ continue
+ end
+ newid, first, alignment_node = process_uniform_core(f, ws, allXfNodes, r, newid, first, alignment_node; kw...)
+ else
+ for c in r
+ newid, first, alignment_node = process_uniform_core(f, ws, allXfNodes, c, newid, first, alignment_node; kw...)
+ end
+# else
+# throw(XLSXError("Something wrong here!"))
+ end
+ end
+ if first
+ newid = -1
+ end
+ return newid
+ end
+ else
+ throw(XLSXError("Non-contiguous range $ncrng is out of bounds. Worksheet `$(ws.name)` only has dimension `$dim`."))
+ end
+end
+function process_uniform_veccolon(f::Function, ws::Worksheet, row, col; kw...)
+ dim = get_dimension(ws)
+ if dim === nothing
+ throw(XLSXError("No worksheet dimension found"))
+ else
+ @assert isnothing(row) || isnothing(col) "Something wrong here!"
+ allXfNodes=find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":cellXfs/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":xf", styles_xmlroot(get_workbook(ws)))
+ if isnothing(col)
+ col = dim.start.column_number:dim.stop.column_number
+ else
+ row = dim.start.row_number:dim.stop.row_number
+# else
+# throw(XLSXError("Something wrong here!"))
+ end
+ isInDim(ws, dim, row, col)
+ let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing}
+ newid = nothing
+ first = true
+ alignment_node = nothing
+ for a in row
+ for b in dim.start.column_number:dim.stop.column_number
+ cellref = CellRef(a, b)
+ if getcell(ws, cellref) isa EmptyCell
+ continue
+ end
+ newid, first, alignment_node = process_uniform_core(f, ws, allXfNodes, cellref, newid, first, alignment_node; kw...)
+ end
+ end
+ if first
+ newid = -1
+ end
+ return newid
+ end
+ end
+end
+function process_uniform_vecint(f::Function, ws::Worksheet, row, col; kw...)
+ let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing}
+ dim = get_dimension(ws)
+ if dim === nothing
+ throw(XLSXError("No worksheet dimension found"))
+ end
+ allXfNodes=find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":cellXfs/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":xf", styles_xmlroot(get_workbook(ws)))
+ newid = nothing
+ first = true
+ alignment_node = nothing
+ isInDim(ws, dim, row, col)
+ for a in row, b in col
+ cellref = CellRef(a, b)
+ if getcell(ws, cellref) isa EmptyCell
+ continue
+ end
+ newid, first, alignment_node = process_uniform_core(f, ws, allXfNodes, cellref, newid, first, alignment_node; kw...)
+ end
+ if first
+ newid = -1
+ end
+ return newid
+ end
+end
+
+# Check if a string is a valid named color in Colors.jl and convert to "FFRRGGBB" if it is.
+function get_colorant(color_string::String)
+ try
+ c = Colors.parse(Colors.Colorant, color_string)
+ rgb = Colors.hex(c, :RRGGBB)
+ return "FF" * rgb
+ catch
+ return nothing
+ end
+end
+function get_color(s::String)::String
+ if occursin(r"^[0-9A-F]{8}$", s) # is a valid 8 digit hexadecimal color
+ return s
+ end
+ c = get_colorant(s)
+ if isnothing(c)
+ throw(XLSXError("Invalid color specified: $s. Either give a valid color name (from Colors.jl) or an 8-digit rgb color in the form FFRRGGBB"))
+ end
+ return c
+end
diff --git a/src/cellformats.jl b/src/cellformats.jl
index 0c80d10b..4c3ca225 100644
--- a/src/cellformats.jl
+++ b/src/cellformats.jl
@@ -1,401 +1,4 @@
-const font_tags = ["b", "i", "u", "strike", "outline", "shadow", "condense", "extend", "sz", "color", "name", "scheme"]
-const border_tags = ["left", "right", "top", "bottom", "diagonal"]
-const fill_tags = ["patternFill"]
-const builtinFormats = Dict(
- "0" => "General",
- "1" => "0",
- "2" => "0.00",
- "3" => "#,##0",
- "4" => "#,##0.00",
- "5" => "\$#,##0_);(\$#,##0)",
- "6" => "\$#,##0_);Red",
- "7" => "\$#,##0.00_);(\$#,##0.00)",
- "8" => "\$#,##0.00_);Red",
- "9" => "0%",
- "10" => "0.00%",
- "11" => "0.00E+00",
- "12" => "# ?/?",
- "13" => "# ??/??",
- "14" => "m/d/yyyy",
- "15" => "d-mmm-yy",
- "16" => "d-mmm",
- "17" => "mmm-yy",
- "18" => "h:mm AM/PM",
- "19" => "h:mm:ss AM/PM",
- "20" => "h:mm",
- "21" => "h:mm:ss",
- "22" => "m/d/yyyy h:mm",
- "37" => "#,##0_);(#,##0)",
- "38" => "#,##0_);Red",
- "39" => "#,##0.00_);(#,##0.00)",
- "40" => "#,##0.00_);Red",
- "45" => "mm:ss",
- "46" => "[h]:mm:ss",
- "47" => "mmss.0",
- "48" => "##0.0E+0",
- "49" => "@"
- )
- const builtinFormatNames = Dict(
- "General" => 0,
- "Number" => 2,
- "Currency" => 7,
- "Percentage" => 9,
- "ShortDate" => 14,
- "LongDate" => 15,
- "Time" => 21,
- "Scientific" => 48
- )
-const floatformats = r"""
-\.[0#?]|
-[0#?]e[+-]?[0#?]|
-[0#?]/[0#?]|
-%
-"""ix
-
-#
-# -- A bunch of helper functions first...
-#
-
-function copynode(o::XML.Node)
- n = XML.parse(XML.Node, XML.write(o))[1]
- n = XML.Node(n.nodetype, n.tag, isnothing(n.attributes) ? XML.OrderedDict{String,String}() : n.attributes, n.value, isnothing(n.children) ? Vector{XML.Node}() : n.children)
- return n
-end
-function buildNode(tag::String, attributes::Dict{String,Union{Nothing,Dict{String,String}}})::XML.Node
- if tag == "font"
- attribute_tags = font_tags
- elseif tag == "border"
- attribute_tags = border_tags
- elseif tag == "fill"
- attribute_tags = fill_tags
- else
- error("Unknown tag: $tag")
- end
- new_node = XML.Element(tag)
- for a in attribute_tags # Use this as a device to keep ordering constant for Excel
- if tag == "font"
- if haskey(attributes, a)
- if isnothing(attributes[a])
- cnode = XML.Element(a)
- else
- cnode = XML.Node(XML.Element, a, XML.OrderedDict{String,String}(), nothing, tag ∈ ["border", "fill"] ? Vector{XML.Node}() : nothing)
- for (k, v) in attributes[a]
- cnode[k] = v
- end
- end
- push!(new_node, cnode)
- end
- elseif tag == "border"
- if haskey(attributes, a)
- if isnothing(attributes[a])
- cnode = XML.Element(a)
- else
- cnode = XML.Node(XML.Element, a, XML.OrderedDict{String,String}(), nothing, tag ∈ ["border", "fill"] ? Vector{XML.Node}() : nothing)
- color = XML.Element("color")
- for (k, v) in attributes[a]
- if k == "style" && v != "none"
- cnode[k] = v
- elseif k == "direction"
- if v in ["up", "both"]
- new_node["diagonalUp"] = "1"
- end
- if v in ["down", "both"]
- new_node["diagonalDown"] = "1"
- end
- else#if k == "rgb"
- color[k] = v
- #else
- #error("Incorect border attribute found: $k") # shouldn't happen!
- end
- end
- if length(XML.attributes(color)) > 0 # Don't push an empty color.
- push!(cnode, color)
- end
- end
- push!(new_node, cnode)
- end
- elseif tag == "fill"
- if haskey(attributes, a)
- if isnothing(attributes[a])
- cnode = XML.Element(a)
- else
- cnode = XML.Node(XML.Element, a, XML.OrderedDict{String,String}(), nothing, tag ∈ ["border", "fill"] ? Vector{XML.Node}() : nothing)
- patternfill = XML.Element("patternFill")
- fgcolor = XML.Element("fgColor")
- bgcolor = XML.Element("bgColor")
- for (k, v) in attributes[a]
- if k == "patternType"
- patternfill[k] = v
- elseif first(k, 2) == "fg"
- fgcolor[k[3:end]] = v
- elseif first(k, 2) == "bg"
- bgcolor[k[3:end]] = v
- end
- end
- @assert haskey(patternfill, "patternType") "No `patternType` attribute found."
- length(XML.attributes(fgcolor)) > 0 && push!(patternfill, fgcolor)
- length(XML.attributes(bgcolor)) > 0 && push!(patternfill, bgcolor)
- end
- push!(new_node, patternfill)
- end
- else
- end
- end
- return new_node
-end
-function unlink_cols(node::XML.Node) # removes each `col` from a `cols` XML node.
- new_cols = XML.Element("cols")
- a = XML.attributes(node)
- if !isnothing(a) # Copy attributes across to new node
- for (k, v) in XML.attributes(node)
- new_cols[k] = v
- end
- end
- for child in XML.children(node) # Copy any child nodes that are not cols across to new node
- if XML.tag(child) != "col" # Shouldn't be any.
- push!(new_cols, child)
- end
- end
- return new_cols
-end
-function update_template_xf(ws::Worksheet, existing_style::CellDataFormat, attributes::Vector{String}, vals::Vector{String})::CellDataFormat
- old_cell_xf = styles_cell_xf(ws.package.workbook, Int(existing_style.id))
- new_cell_xf = copynode(old_cell_xf)
- @assert length(attributes) == length(vals) "Attributes and values must be of the same length."
- for (a, v) in zip(attributes, vals)
- new_cell_xf[a] = v
- end
- return styles_add_cell_xf(ws.package.workbook, new_cell_xf)
-end
-function update_template_xf(ws::Worksheet, existing_style::CellDataFormat, alignment::XML.Node)::CellDataFormat
- old_cell_xf = styles_cell_xf(ws.package.workbook, Int(existing_style.id))
- new_cell_xf = copynode(old_cell_xf)
- if length(XML.children(new_cell_xf))==0
- push!(new_cell_xf, alignment)
- else
- new_cell_xf[1] = alignment
- end
- return styles_add_cell_xf(ws.package.workbook, new_cell_xf)
-end
-
-# Only used in testing!
-function styles_add_cell_font(wb::Workbook, attributes::Dict{String,Union{Dict{String,String},Nothing}})::Int
- new_font = buildNode("font", attributes)
- return styles_add_cell_attribute(wb, new_font, "fonts")
-end
-
-# Used by setFont(), setBorder(), setFill(), setAlignment() and setNumFmt()
-function styles_add_cell_attribute(wb::Workbook, new_att::XML.Node, att::String)::Int
- xroot = styles_xmlroot(wb)
- i, j = get_idces(xroot, "styleSheet", att)
- existing_elements_count = length(XML.children(xroot[i][j]))
- @assert parse(Int, xroot[i][j]["count"]) == existing_elements_count "Wrong number of elements elements found: $existing_elements_count. Expected $(parse(Int, xroot[i][j]["count"]))."
-
- # Check new_att doesn't duplicate any existing att. If yes, use that rather than create new.
- for (k, node) in enumerate(XML.children(xroot[i][j]))
- if XML.tag(new_att) == "numFmt" # mustn't compare numFmtId attribute for formats
- if XML.parse(XML.Node, XML.write(node))[1]["formatCode"] == XML.parse(XML.Node, XML.write(new_att))[1]["formatCode"] # XML.jl defines `Base.:(==)`
- return k - 1 # CellDataFormat is zero-indexed
- end
- else
- if XML.parse(XML.Node, XML.write(node))[1] == XML.parse(XML.Node, XML.write(new_att))[1] # XML.jl defines `Base.:(==)`
- return k - 1 # CellDataFormat is zero-indexed
- end
- end
- end
-
- push!(xroot[i][j], new_att)
- xroot[i][j]["count"] = string(existing_elements_count + 1)
-
- return existing_elements_count # turns out this is the new index (because it's zero-based)
-end
-function process_sheetcell(f::Function, xl::XLSXFile, sheetcell::String; kw...)::Int
- if is_workbook_defined_name(xl, sheetcell)
- v = get_defined_name_value(xl.workbook, sheetcell)
- if is_defined_name_value_a_constant(v)
- error("Can only assign attributes to cells but `$(sheetcell)` is a constant: $(sheetcell)=$v.")
- elseif is_defined_name_value_a_reference(v)
- newid = process_ranges(f, xl, string(v); kw...)
- else
- error("Unexpected defined name value: $v.")
- end
- elseif is_valid_sheet_column_range(sheetcell)
- sheetcolrng = SheetColumnRange(sheetcell)
- newid = f(xl[sheetcolrng.sheet], sheetcolrng.colrng; kw...)
- elseif is_valid_sheet_cellrange(sheetcell)
- sheetcellrng = SheetCellRange(sheetcell)
- newid = f(xl[sheetcellrng.sheet], sheetcellrng.rng; kw...)
- elseif is_valid_sheet_cellname(sheetcell)
- ref = SheetCellRef(sheetcell)
- @assert hassheet(xl, ref.sheet) "Sheet $(ref.sheet) not found."
- newid = f(getsheet(xl, ref.sheet), ref.cellref; kw...)
- else
- error("Invalid sheet cell reference: $sheetcell")
- end
- return newid
-end
-function process_ranges(f::Function, ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int
- # Moved the tests for defined names to be first in case a name looks like a column name (e.g. "ID")
- if is_worksheet_defined_name(ws, ref_or_rng)
- v = get_defined_name_value(ws, ref_or_rng)
- if is_defined_name_value_a_constant(v)
- error("Can only assign attributes to cells but `$(ref_or_rng)` is a constant: $(ref_or_rng)=$v.")
- elseif is_defined_name_value_a_reference(v)
- wb = get_workbook(ws)
- newid = f(get_xlsxfile(wb), string(v); kw...)
- else
- error("Unexpected defined name value: $v.")
- end
- elseif is_workbook_defined_name(get_workbook(ws), ref_or_rng)
- wb = get_workbook(ws)
- v = get_defined_name_value(wb, ref_or_rng)
- if is_defined_name_value_a_constant(v)
- error("Can only assign attributes to cells but `$(ref_or_rng)` is a constant: $(ref_or_rng)=$v.")
- elseif is_defined_name_value_a_reference(v)
- if is_non_contiguous_range(v)
- _ = f.(Ref(get_xlsxfile(wb)), replace.(split(string(v), ","), "'" => "", "\$" => ""); kw...)
- newid = -1
- else
- newid = f(get_xlsxfile(wb), replace(string(v), "'" => "", "\$" => ""); kw...)
- end
- else
- error("Unexpected defined name value: $v.")
- end
- elseif is_valid_column_range(ref_or_rng)
- colrng = ColumnRange(ref_or_rng)
- newid = f(ws, colrng; kw...)
- elseif is_valid_cellrange(ref_or_rng)
- rng = CellRange(ref_or_rng)
- newid = f(ws, rng; kw...)
- elseif is_valid_cellname(ref_or_rng)
- newid = f(ws, CellRef(ref_or_rng); kw...)
- else
- error("Invalid cell reference or range: $ref_or_rng")
- end
- return newid
-end
-function process_columnranges(f::Function, ws::Worksheet, colrng::ColumnRange; kw...)::Int
- bounds = column_bounds(colrng)
- dim = (get_dimension(ws))
-
- left = bounds[begin]
- right = bounds[end]
- top = dim.start.row_number
- bottom = dim.stop.row_number
-
- OK = dim.start.column_number <= left
- OK &= dim.stop.column_number >= right
- OK &= dim.start.row_number <= top
- OK &= dim.stop.row_number >= bottom
-
- if OK
- rng = CellRange(top, left, bottom, right)
- return f(ws, rng; kw...)
- else
- error("Column range $colrng is out of bounds. Worksheet `$(ws.name)` only has dimension `$dim`.")
- end
-end
-function process_cellranges(f::Function, ws::Worksheet, rng::CellRange; kw...)::Int
- for cellref in rng
- if getcell(ws, cellref) isa EmptyCell
- continue
- end
- _ = f(ws, cellref; kw...)
- end
- return -1 # Each cell may have a different attribute Id so we can't return a single value.
-end
-function process_get_sheetcell(f::Function, xl::XLSXFile, sheetcell::String)
- ref = SheetCellRef(sheetcell)
- @assert hassheet(xl, ref.sheet) "Sheet $(ref.sheet) not found."
- return f(getsheet(xl, ref.sheet), ref.cellref)
-end
-function process_get_cellref(f::Function, ws::Worksheet, cellref::CellRef)
- wb = get_workbook(ws)
- cell = getcell(ws, cellref)
-
- if cell isa EmptyCell || cell.style == ""
- return nothing
- end
-
- cell_style = styles_cell_xf(wb, parse(Int, cell.style))
- return f(wb, cell_style)
-end
-function process_get_cellname(f::Function, ws::Worksheet, ref_or_rng::AbstractString)
- if is_workbook_defined_name(get_workbook(ws), ref_or_rng)
- wb = get_workbook(ws)
- v = get_defined_name_value(wb, ref_or_rng)
- if is_defined_name_value_a_constant(v) # Can these have fonts?
- error("Can only assign borderds to cells but `$(ref_or_rng)` is a constant: $(ref_or_rng)=$v.")
- elseif is_defined_name_value_a_reference(v)
- new_att = f(get_xlsxfile(wb), replace(string(v), "'" => ""))
- else
- error("Unexpected defined name value: $v.")
- end
- elseif is_valid_cellname(ref_or_rng)
- new_att = f(ws, CellRef(ref_or_rng))
- else
- error("Invalid cell reference or range: $ref_or_rng")
- end
- return new_att
-end
-function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange, atts::Vector{String}; kw...)
-
- @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot set uniform attributes because cache is not enabled."
-
- let newid
- first = true
- for cellref in rng
- cell = getcell(ws, cellref)
- if cell isa EmptyCell # Can't add a attribute to an empty cell.
- continue
- end
- if first # Get the attribute of the first cell in the range.
- newid = f(ws, cellref; kw...)
- first = false
- else # Apply the same attribute to the rest of the cells in the range.
- if cell.style == ""
- cell.style = string(get_num_style_index(ws, 0).id)
- end
- cell.style = string(update_template_xf(ws, CellDataFormat(parse(Int, cell.style)), atts, ["$newid", "1"]).id)
- end
- end
- if first
- newid = -1
- end
- return newid
- end
-end
-function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange; kw...)
-
- @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot set uniform attributes because cache is not enabled."
-
- let newid, alignment_node
- first = true
- for cellref in rng
- cell = getcell(ws, cellref)
- if cell isa EmptyCell # Can't add a attribute to an empty cell.
- continue
- end
- if first # Get the attribute of the first cell in the range.
- newid = f(ws, cellref; kw...)
- new_alignment = getAlignment(ws, cellref).alignment["alignment"]
- alignment_node = XML.Node(XML.Element, "alignment", new_alignment, nothing, nothing)
- first = false
- else # Apply the same attribute to the rest of the cells in the range.
- if cell.style == ""
- cell.style = string(get_num_style_index(ws, 0).id)
- end
- cell.style = string(update_template_xf(ws, CellDataFormat(parse(Int, cell.style)), alignment_node).id)
- end
- end
- if first
- newid = -1
- end
- return newid
- end
-end
-
# ==========================================================================================
#
# -- Get and set font attributes
@@ -405,8 +8,12 @@ end
setFont(sh::Worksheet, cr::String; kw...) -> ::Int
setFont(xf::XLSXFile, cr::String, kw...) -> ::Int
+ setFont(sh::Worksheet, row, col; kw...) -> ::Int
+
Set the font used by a single cell, a cell range, a column range or
-a named cell or named range in a worksheet or XLSXfile.
+row range or a named cell or named range in a worksheet or XLSXfile.
+Alternatively, specify the row and column using any combination of
+Integer, UnitRange, Vector{Integer} or `:`.
Font attributes are specified using keyword arguments:
- `bold::Bool = nothing` : set to `true` to make the font bold.
@@ -414,7 +21,7 @@ Font attributes are specified using keyword arguments:
- `under::String = nothing` : set to `single`, `double` or `none`.
- `strike::Bool = nothing` : set to `true` to strike through the font.
- `size::Int = nothing` : set the font size (0 < size < 410).
-- `color::String = nothing` : set the font color using an 8-digit hexadecimal RGB value.
+- `color::String = nothing` : set the font color.
- `name::String = nothing` : set the font name.
Only the attributes specified will be changed. If an attribute is not specified, the current
@@ -422,18 +29,21 @@ value will be retained. These are the only attributes supported currently.
No validation of the font names specified is performed. Available fonts will depend
on what your system has installed. If you specify, for example, `name = "badFont"`,
-that value will be written to the XLSXfile.
+that value will be written to the XLSXFile.
As an expedient to get fonts to work, the `scheme` attribute is simply dropped from
new font definitions.
-The `color` attribute can only be defined as rgb values.
-- The first two digits represent transparency (α). FF is fully opaque, while 00 is fully transparent.
+The `color` attribute can be defined using 8-digit rgb values.
+- The first two digits represent transparency (α). Excel ignores transparency.
- The next two digits give the red component.
- The next two digits give the green component.
- The next two digits give the blue component.
So, FF000000 means a fully opaque black color.
+Alternatively, you can use the name of any named color from Colors.jl
+([here](https://juliagraphics.github.io/Colors.jl/stable/namedcolors/)).
+
Font attributes cannot be set for `EmptyCell`s. Set a cell value first.
If a cell range or column range includes any `EmptyCell`s, they will be
quietly skipped and the font will be set for the remaining cells.
@@ -447,49 +57,82 @@ For cell ranges, column ranges and named ranges, the value returned is -1.
```julia
julia> setFont(sh, "A1"; bold=true, italic=true, size=12, name="Arial") # Single cell
-julia> setFont(xf, "Sheet1!A1"; bold=false, size=14, color="FFB3081F") # Single cell
+julia> setFont(xf, "Sheet1!A1"; bold=false, size=14, color="yellow") # Single cell
julia> setFont(sh, "A1:B7"; name="Aptos", under="double", strike=true) # Cell range
julia> setFont(xf, "Sheet1!A1:B7"; size=24, name="Berlin Sans FB Demi") # Cell range
-julia> setFont(sh, "A:B"; italic=true, color="FF8888FF", under="single") # Column range
+julia> setFont(sh, "A:B"; italic=true, color="green", under="single") # Column range
+
+julia> setFont(xf, "Sheet1!A:B"; italic=true, color="red", under="single") # Column range
-julia> setFont(xf, "Sheet1!A:B"; italic=true, color="FF8888FF", under="single") # Column range
+julia> setFont(xf, "Sheet1!6:12"; italic=false, color="FF8888FF", under="none") # Row range
julia> setFont(sh, "bigred"; size=48, color="FF00FF00") # Named cell or range
-julia> setFont(xf, "bigred"; size=48, color="FF00FF00") # Named cell or range
-
+julia> setFont(xf, "bigred"; size=48, color="magenta") # Named cell or range
+
+julia> setFont(sh, 1, 2; size=48, color="magenta") # row and column as integers
+
+julia> setFont(sh, 1:3, 2; size=48, color="magenta") # row as unit range
+
+julia> setFont(sh, 6, [2, 3, 8, 12]; size=48, color="magenta") # column as vector of indices
+
+julia> setFont(sh, :, 2:6; size=48, color="lightskyblue2") # all rows, columns 2 to 6
+
```
"""
function setFont end
+setFont(ws::Worksheet, ref::SheetCellRef; kw...) = do_sheet_names_match(ws, ref) && setFont(ws, ref.cellref; kw...)
+setFont(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setFont(ws, rng.rng; kw...)
+setFont(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setFont(ws, rng.colrng; kw...)
+setFont(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setFont(ws, rng.rowrng; kw...)
setFont(ws::Worksheet, rng::CellRange; kw...)::Int = process_cellranges(setFont, ws, rng; kw...)
setFont(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setFont, ws, colrng; kw...)
+setFont(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(setFont, ws, rowrng; kw...)
+setFont(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setFont, ws, ncrng; kw...)
setFont(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setFont, ws, ref_or_rng; kw...)
setFont(xl::XLSXFile, sheetcell::String; kw...)::Int = process_sheetcell(setFont, xl, sheetcell; kw...)
-function setFont(sh::Worksheet, cellref::CellRef;
- bold::Union{Nothing,Bool}=nothing,
- italic::Union{Nothing,Bool}=nothing,
- under::Union{Nothing,String}=nothing,
- strike::Union{Nothing,Bool}=nothing,
- size::Union{Nothing,Int}=nothing,
- color::Union{Nothing,String}=nothing,
- name::Union{Nothing,String}=nothing
- )::Int
-
- @assert get_xlsxfile(sh).use_cache_for_sheet_data "Cannot set font because cache is not enabled."
+setFont(ws::Worksheet, row::Integer, col::Integer; kw...) = setFont(ws, CellRef(row, col); kw...)
+setFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setFont, ws, row, nothing; kw...)
+setFont(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setFont, ws, nothing, col; kw...)
+setFont(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setFont, ws, nothing, nothing; kw...)
+setFont(ws::Worksheet, ::Colon; kw...) = process_colon(setFont, ws, nothing, nothing; kw...)
+setFont(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setFont, ws, row, nothing; kw...)
+setFont(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_veccolon(setFont, ws, nothing, col; kw...)
+setFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_vecint(setFont, ws, row, col; kw...)
+setFont(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFont, ws, row, col; kw...)
+setFont(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_vecint(setFont, ws, row, col; kw...)
+setFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setFont(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
+function setFont(sh::Worksheet, cellref::CellRef;
+ bold::Union{Nothing,Bool}=nothing,
+ italic::Union{Nothing,Bool}=nothing,
+ under::Union{Nothing,String}=nothing,
+ strike::Union{Nothing,Bool}=nothing,
+ size::Union{Nothing,Int}=nothing,
+ color::Union{Nothing,String}=nothing,
+ name::Union{Nothing,String}=nothing
+)::Int
+
+ if !get_xlsxfile(sh).use_cache_for_sheet_data
+ throw(XLSXError("Cannot set font because cache is not enabled."))
+ end
wb = get_workbook(sh)
cell = getcell(sh, cellref)
- @assert !(cell isa EmptyCell) "Cannot set attribute for an `EmptyCell`: $(cellref.name). Set the value first."
+ if cell isa EmptyCell
+ throw(XLSXError("Cannot set attribute for an `EmptyCell`: $(cellref.name). Set the value first."))
+ end
+
+ allXfNodes=find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":cellXfs/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":xf", styles_xmlroot(wb))
if cell.style == ""
- cell.style = string(get_num_style_index(sh, 0).id)
+ cell.style = string(get_num_style_index(sh, allXfNodes, 0).id)
end
- cell_style = styles_cell_xf(wb, parse(Int, cell.style))
+ cell_style = styles_cell_xf(allXfNodes, parse(Int, cell.style))
new_font_atts = Dict{String,Union{Dict{String,String},Nothing}}()
cell_font = getFont(wb, cell_style)
@@ -506,7 +149,9 @@ function setFont(sh::Worksheet, cellref::CellRef;
new_font_atts["i"] = nothing
end
elseif a == "u"
- @assert isnothing(under) || under ∈ ["none", "single", "double"] "Invalid value for under: $under. Must be one of: `none`, `single`, `double`."
+ if !isnothing(under) && under ∉ ["none", "single", "double"]
+ throw(XLSXError("Invalid value for under: $under. Must be one of: `none`, `single`, `double`."))
+ end
if isnothing(under) && haskey(old_font_atts, "u")
new_font_atts["u"] = old_font_atts["u"]
elseif !isnothing(under)
@@ -521,14 +166,13 @@ function setFont(sh::Worksheet, cellref::CellRef;
new_font_atts["strike"] = nothing
end
elseif a == "color"
- @assert isnothing(color) || occursin(r"^[0-9A-F]{8}$", color) "Invalid color value: $color. Must be an 8-digit hexadecimal RGB value."
if isnothing(color) && haskey(old_font_atts, "color")
new_font_atts["color"] = old_font_atts["color"]
elseif !isnothing(color)
- new_font_atts["color"] = Dict("rgb" => color)
+ new_font_atts["color"] = Dict("rgb" => get_color(color))
end
elseif a == "sz"
- @assert isnothing(size) || (size > 0 && size < 410) "Invalid size value: $size. Must be between 1 and 409."
+ (!isnothing(size) && (size < 1 || size > 409)) && throw(XLSXError("Invalid size value: $size. Must be between 1 and 409."))
if isnothing(size) && haskey(old_font_atts, "sz")
new_font_atts["sz"] = old_font_atts["sz"]
elseif !isnothing(size)
@@ -550,7 +194,7 @@ function setFont(sh::Worksheet, cellref::CellRef;
new_fontid = styles_add_cell_attribute(wb, font_node, "fonts")
- newstyle = string(update_template_xf(sh, CellDataFormat(parse(Int, cell.style)), ["fontId", "applyFont"], ["$new_fontid", "1"]).id)
+ newstyle = string(update_template_xf(sh, allXfNodes, CellDataFormat(parse(Int, cell.style)), ["fontId", "applyFont"], [string(new_fontid), "1"]).id)
cell.style = newstyle
return new_fontid
end
@@ -559,8 +203,12 @@ end
setUniformFont(sh::Worksheet, cr::String; kw...) -> ::Int
setUniformFont(xf::XLSXFile, cr::String, kw...) -> ::Int
-Set the font used by a cell range, a column range or a named range in a
-worksheet or XLSXfile to be uniformly the same font.
+ setUniformFont(sh::Worksheet, rows, cols; kw...) -> ::Int
+
+Set the font used by a cell range, a column range or row range or
+a named range in a worksheet or XLSXfile to be uniformly the same font.
+Alternatively, specify the rows and columns using any combination of
+Integer, UnitRange, Vector{Integer} or `:`.
First, the font attributes of the first cell in the range (the top-left cell) are
updated according to the given `kw...` (using `setFont()`). The resultant font is
@@ -580,6 +228,9 @@ etc) to all the other cells in the range.
This can be more efficient when setting the same font for a large number of cells.
+Applying `setUniformFont()` without any keyword arguments simply copies the `Font`
+attributes from the first cell specified to all the others.
+
The value returned is the `fontId` of the font uniformly applied to the cells.
If all cells in the range are `EmptyCells` the returned value is -1.
@@ -595,24 +246,46 @@ julia> setUniformFont(sh, "A:B"; italic=true, color="FF8888FF", under="single")
julia> setUniformFont(xf, "Sheet1!A:B"; italic=true, color="FF8888FF", under="single") # Column range
+julia> setUniformFont(sh, "33"; italic=true, color="FF8888FF", under="single") # Row
+
julia> setUniformFont(sh, "bigred"; size=48, color="FF00FF00") # Named range
-julia> setUniformFont(xf, "bigred"; size=48, color="FF00FF00") # Named range
+julia> setUniformFont(sh, 1, [2, 4, 6]; size=48, color="lightskyblue2") # vector of column indices
+
+julia> setUniformFont(sh, "B2,A5:D22") # Copy `Font` from B2 to cells in A5:D22
```
"""
function setUniformFont end
+setUniformFont(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setUniformFont(ws, rng.rng; kw...)
+setUniformFont(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setUniformFont(ws, rng.colrng; kw...)
+setUniformFont(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setUniformFont(ws, rng.rowrng; kw...)
setUniformFont(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setUniformFont, ws, colrng; kw...)
+setUniformFont(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(setUniformFont, ws, rowrng; kw...)
+setUniformFont(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_uniform_ncranges(setFont, ws, ncrng, ["fontId", "applyFont"]; kw...)
setUniformFont(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetcell(setUniformFont, xl, sheetcell; kw...)
setUniformFont(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setUniformFont, ws, ref_or_rng; kw...)
+setUniformFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setFont, ws, row, nothing; kw...)
+setUniformFont(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setFont, ws, nothing, col; kw...)
+setUniformFont(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setUniformFont, ws, nothing, nothing; kw...)
+setUniformFont(ws::Worksheet, ::Colon; kw...) = process_colon(setUniformFont, ws, nothing, nothing; kw...)
+setUniformFont(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_uniform_veccolon(setFont, ws, row, nothing, ["fontId", "applyFont"]; kw...)
+setUniformFont(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_veccolon(setFont, ws, nothing, col, ["fontId", "applyFont"]; kw...)
+setUniformFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_vecint(setFont, ws, row, col, ["fontId", "applyFont"]; kw...)
+setUniformFont(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFont, ws, row, col, ["fontId", "applyFont"]; kw...)
+setUniformFont(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_vecint(setFont, ws, row, col, ["fontId", "applyFont"]; kw...)
+setUniformFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setUniformFont(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
setUniformFont(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attribute(setFont, ws, rng, ["fontId", "applyFont"]; kw...)
"""
getFont(sh::Worksheet, cr::String) -> ::Union{Nothing, CellFont}
getFont(xf::XLSXFile, cr::String) -> ::Union{Nothing, CellFont}
-
-Get the font used by a single cell at reference `cr` in a worksheet `sh` or XLSXfile `xf`.
+
+ getFont(sh::Worksheet, row::Int, col::Int) -> ::Union{Nothing, CellFont}
+
+Get the font used by a single cell reference in a worksheet `sh` or XLSXfile `xf`.
+The specified cell must be within the sheet dimension.
Return a `CellFont` object containing:
- `fontId` : a 0-based index of the font in the workbook
@@ -645,6 +318,10 @@ e.g. `"color" => ("theme" => "1")`.
julia> getFont(sh, "A1")
julia> getFont(xf, "Sheet1!A1")
+
+julia> getFont(sh, "Sheet1!A1")
+
+julia> getFont(sh, 1, 1)
```
"""
@@ -652,24 +329,28 @@ function getFont end
getFont(ws::Worksheet, cr::String) = process_get_cellname(getFont, ws, cr)
getFont(xl::XLSXFile, sheetcell::String)::Union{Nothing,CellFont} = process_get_sheetcell(getFont, xl, sheetcell)
getFont(ws::Worksheet, cellref::CellRef)::Union{Nothing,CellFont} = process_get_cellref(getFont, ws, cellref)
+getFont(ws::Worksheet, row::Integer, col::Integer) = getFont(ws, CellRef(row, col))
getDefaultFont(ws::Worksheet) = getFont(get_workbook(ws), styles_cell_xf(get_workbook(ws), 0))
function getFont(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellFont}
- @assert get_xlsxfile(wb).use_cache_for_sheet_data "Cannot get font because cache is not enabled."
+ if !get_xlsxfile(wb).use_cache_for_sheet_data
+ throw(XLSXError("Cannot get font because cache is not enabled."))
+ end
if haskey(cell_style, "fontId")
fontid = cell_style["fontId"]
applyfont = haskey(cell_style, "applyFont") ? cell_style["applyFont"] : "0"
xroot = styles_xmlroot(wb)
- font_elements = find_all_nodes("/$SPREADSHEET_NAMESPACE_XPATH_ARG:styleSheet/$SPREADSHEET_NAMESPACE_XPATH_ARG:fonts", xroot)[begin]
- @assert parse(Int, font_elements["count"]) == length(XML.children(font_elements)) "Unexpected number of font definitions found : $(length(XML.children(font_elements))). Expected $(parse(Int, font_elements["count"]))"
+ font_elements = find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":fonts", xroot)[begin]
+ if parse(Int, font_elements["count"]) != length(XML.children(font_elements))
+ throw(XLSXError("Unexpected number of font definitions found : $(length(XML.children(font_elements))). Expected $(parse(Int, font_elements["count"]))"))
+ end
current_font = XML.children(font_elements)[parse(Int, fontid)+1] # Zero based!
font_atts = Dict{String,Union{Dict{String,String},Nothing}}()
for c in XML.children(current_font)
if isnothing(XML.attributes(c)) || length(XML.attributes(c)) == 0
font_atts[XML.tag(c)] = nothing
else
- # @assert length(XML.attributes(c)) == 1 "Too many font attributes found for $(XML.tag(c)) Expected 1, found $(length(XML.attributes(c)))."
for (k, v) in XML.attributes(c)
font_atts[XML.tag(c)] = Dict(k => v)
end
@@ -688,8 +369,11 @@ end
"""
getBorder(sh::Worksheet, cr::String) -> ::Union{Nothing, CellBorder}
getBorder(xf::XLSXFile, cr::String) -> ::Union{Nothing, CellBorder}
+
+ getBorder(sh::Worksheet, row::Int, col::Int) -> ::Union{Nothing, CellBorder}
-Get the borders used by a single cell at reference `cr` in a worksheet or XLSXfile.
+Get the borders used by a single cell at reference in a worksheet or XLSXfile.
+The specified cell must be within the sheet dimension.
Return a `CellBorder` object containing:
- `borderId` : a 0-based index of the border in the workbook
@@ -736,12 +420,12 @@ The `color` element can have the following attributes:
`tint` can only be used in conjunction with the theme attribute to derive different shades of the theme color.
For example: .
-Only the `rgb` attribute can be used in `setBorder()` to define a border color.
-
# Examples:
```julia
julia> getBorder(sh, "A1")
+julia> getBorder(sh, 3, 6)
+
julia> getBorder(xf, "Sheet1!A1")
```
@@ -750,17 +434,22 @@ function getBorder end
getBorder(xl::XLSXFile, sheetcell::String)::Union{Nothing,CellBorder} = process_get_sheetcell(getBorder, xl, sheetcell)
getBorder(ws::Worksheet, cellref::CellRef)::Union{Nothing,CellBorder} = process_get_cellref(getBorder, ws, cellref)
getBorder(ws::Worksheet, cr::String) = process_get_cellname(getBorder, ws, cr)
+getBorder(ws::Worksheet, row::Integer, col::Integer) = getBorder(ws, CellRef(row, col))
getDefaultBorders(ws::Worksheet) = getBorder(get_workbook(ws), styles_cell_xf(get_workbook(ws), 0))
function getBorder(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellBorder}
- @assert get_xlsxfile(wb).use_cache_for_sheet_data "Cannot get border because cache is not enabled."
+ if !get_xlsxfile(wb).use_cache_for_sheet_data
+ throw(XLSXError("Cannot get border because cache is not enabled."))
+ end
if haskey(cell_style, "borderId")
borderid = cell_style["borderId"]
applyborder = haskey(cell_style, "applyBorder") ? cell_style["applyBorder"] : "0"
xroot = styles_xmlroot(wb)
- border_elements = find_all_nodes("/$SPREADSHEET_NAMESPACE_XPATH_ARG:styleSheet/$SPREADSHEET_NAMESPACE_XPATH_ARG:borders", xroot)[begin]
- @assert parse(Int, border_elements["count"]) == length(XML.children(border_elements)) "Unexpected number of border definitions found : $(length(XML.children(border_elements))). Expected $(parse(Int, border_elements["count"]))"
+ border_elements = find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":borders", xroot)[begin]
+ if parse(Int, border_elements["count"]) != length(XML.children(border_elements))
+ throw(XLSXError("Unexpected number of border definitions found : $(length(XML.children(border_elements))). Expected $(parse(Int, border_elements["count"]))"))
+ end
current_border = XML.children(border_elements)[parse(Int, borderid)+1] # Zero based!
diag_atts = XML.attributes(current_border)
border_atts = Dict{String,Union{Dict{String,String},Nothing}}()
@@ -768,10 +457,12 @@ function getBorder(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellBorder
if isnothing(XML.attributes(side)) || length(XML.attributes(side)) == 0
border_atts[XML.tag(side)] = nothing
else
- @assert length(XML.attributes(side)) == 1 "Too many border attributes found for $(XML.tag(side)) Expected 1, found $(length(XML.attributes(side)))."
+ if length(XML.attributes(side)) != 1
+ throw(XLSXError("Too many border attributes found for $(XML.tag(side)) Expected 1, found $(length(XML.attributes(side)))."))
+ end
for (k, v) in XML.attributes(side) # style is the only possible attribute of a side
border_atts[XML.tag(side)] = Dict(k => v)
- if side == "diagonal" && !isnothing(diag_atts)
+ if XML.tag(side) == "diagonal" && !isnothing(diag_atts)
if haskey(diag_atts, "diagonalUp") && haskey(diag_atts, "diagonalDown")
border_atts[XML.tag(side)]["direction"] = "both"
elseif haskey(diag_atts, "diagonalUp")
@@ -779,7 +470,7 @@ function getBorder(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellBorder
elseif haskey(diag_atts, "diagonalDown")
border_atts[XML.tag(side)]["direction"] = "down"
else
- @assert false "No direction set for `diagonal` border"
+ throw(XLSXError("No direction set for `diagonal` border"))
end
end
for subc in XML.children(side) # color is a child of a border element
@@ -799,27 +490,38 @@ end
"""
setBorder(sh::Worksheet, cr::String; kw...) -> ::Int}
setBorder(xf::XLSXFile, cr::String; kw...) -> ::Int
+
+ setBorder(sh::Worksheet, row, col; kw...) -> ::Int}
Set the borders used used by a single cell, a cell range, a column range or
-a named cell or named range in a worksheet or XLSXfile.
+row range or a named cell or named range in a worksheet or XLSXfile.
+Alternatively, specify the row and column using any combination of
+Integer, UnitRange, Vector{Integer} or `:`.
Borders are independently defined for the keywords:
-- `left::Vector{Pair{String,String} = nothing`
-- `right::Vector{Pair{String,String} = nothing`
-- `top::Vector{Pair{String,String} = nothing`
-- `bottom::Vector{Pair{String,String} = nothing`
-- `diagonal::Vector{Pair{String,String} = nothing`
-- `[allsides::Vector{Pair{String,String} = nothing]`
-
-These represent each of the sides of a cell . The keyword `diagonal` defines diagonal lines running
-across the cell. These lines must share the same style and color in any cell.
+- `left::Vector{Pair{String,String}} = nothing`
+- `right::Vector{Pair{String,String}} = nothing`
+- `top::Vector{Pair{String,String}} = nothing`
+- `bottom::Vector{Pair{String,String}} = nothing`
+- `diagonal::Vector{Pair{String,String}} = nothing`
+- `[allsides::Vector{Pair{String,String}} = nothing]`
+- `[outside::Vector{Pair{String,String}} = nothing]`
+
+These represent each of the sides of a cell . The keyword `diagonal` defines
+diagonal lines running across the cell. These lines must share the same style
+and color in any cell.
An additional keyword, `allsides`, is provided for convenience. It can be used
in place of the four side keywords to apply the same border setting to all four
sides at once. It cannot be used in conjunction with any of the side-specific
-keywords but it can be used together with `diagonal`.
+keywords or with `outside` but it can be used together with `diagonal`.
-The two attributes that can be set for each keyword are `style` and `rgb`.
+A further keyword, `outside`, can be used to set the outside border around a
+range. Any internal borders will remain unchanged. An outside border cannot be
+set for any non-contiguous/non-rectangular range, cannot be indexed with
+vectors and cannot be used in conjunction with any other keywords.
+
+The two attributes that can be set for each keyword are `style` and `color`.
Additionally, for diagonal borders, a third keyword, `direction` can be used.
Allowed values for `style` are:
@@ -838,8 +540,11 @@ Allowed values for `style` are:
- `mediumDashDotDot`
- `slantDashDot`
-The `color` attribute is set by specifying an 8-digit hexadecimal value.
-No other color attributes can be applied.
+The `color` attribute can be set by specifying an 8-digit hexadecimal value
+in the format "FFRRGGBB". The transparency ("FF") is ignored by Excel but
+is required.
+Alternatively, you can use the name of any named color from Colors.jl
+([here](https://juliagraphics.github.io/Colors.jl/stable/namedcolors/)).
Valid values for the `direction` keyword (for diagonal borders) are:
- `up` : diagonal border runs bottom-left to top-right
@@ -865,30 +570,126 @@ For cell ranges, column ranges and named ranges, the value returned is -1.
```julia
Julia> setBorder(sh, "D6"; allsides = ["style" => "thick"], diagonal = ["style" => "hair", "direction" => "up"])
+Julia> setBorder(sh, 2:45, 2:12; outside = ["style" => "thick", "color" => "lightskyblue2"])
+
Julia> setBorder(xf, "Sheet1!D4"; left = ["style" => "dotted", "color" => "FF000FF0"],
- right = ["style" => "medium", "color" => "FF765000"],
+ right = ["style" => "medium", "color" => "firebrick2"],
top = ["style" => "thick", "color" => "FF230000"],
- bottom = ["style" => "medium", "color" => "FF0000FF"],
- diagonal = ["style" => "dotted", "color" => "FF00D4D4"]
+ bottom = ["style" => "medium", "color" => "goldenrod3"],
+ diagonal = ["style" => "dotted", "color" => "FF00D4D4", "direction" => "both"]
)
```
"""
function setBorder end
-setBorder(ws::Worksheet, rng::CellRange; kw...)::Int = process_cellranges(setBorder, ws, rng; kw...)
-setBorder(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setBorder, ws, colrng; kw...)
+setBorder(ws::Worksheet, ref::SheetCellRef; kw...) = do_sheet_names_match(ws, ref) && setBorder(ws, ref.cellref; kw...)
+setBorder(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setBorder(ws, rng.rng; kw...)
+setBorder(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setBorder(ws, rng.colrng; kw...)
+setBorder(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setBorder(ws, rng.rowrng; kw...)
+setBorder(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setBorder, ws, ncrng; kw...)
setBorder(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setBorder, ws, ref_or_rng; kw...)
-setBorder(xl::XLSXFile, sheetcell::String; kw...)::Int = process_sheetcell(setBorder, xl, sheetcell; kw...)
+setBorder(ws::Worksheet, row::Integer, col::Integer; kw...) = setBorder(ws, CellRef(row, col); kw...)
+setBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setBorder, ws, row, nothing; kw...)
+setBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setBorder, ws, nothing, col; kw...)
+setBorder(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setBorder, ws, nothing, nothing; kw...)
+setBorder(ws::Worksheet, ::Colon; kw...) = process_colon(setBorder, ws, nothing, nothing; kw...)
+setBorder(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setBorder, ws, row, nothing; kw...)
+setBorder(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_veccolon(setBorder, ws, nothing, col; kw...)
+setBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_vecint(setBorder, ws, row, col; kw...)
+setBorder(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setBorder, ws, row, col; kw...)
+setBorder(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_vecint(setBorder, ws, row, col; kw...)
+setBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setBorder(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
+function setBorder(ws::Worksheet, rng::CellRange;
+ outside::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ allsides::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ left::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ right::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ top::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ bottom::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ diagonal::Union{Nothing,Vector{Pair{String,String}}}=nothing
+)::Int
+ if isnothing(outside)
+ return process_cellranges(setBorder, ws, rng; allsides, left, right, top, bottom, diagonal)
+ else
+ if !all(isnothing, [left, right, top, bottom, diagonal, allsides])
+ throw(XLSXError("Keyword `outside` is incompatible with any other keywords."))
+ end
+ return setOutsideBorder(ws, rng; outside)
+ end
+end
+function setBorder(ws::Worksheet, colrng::ColumnRange;
+ outside::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ allsides::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ left::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ right::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ top::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ bottom::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ diagonal::Union{Nothing,Vector{Pair{String,String}}}=nothing
+)::Int
+ if isnothing(outside)
+ return process_columnranges(setBorder, ws, colrng; allsides, left, right, top, bottom, diagonal)
+ else
+ if !all(isnothing, [left, right, top, bottom, diagonal, allsides])
+ throw(XLSXError("Keyword `outside` is incompatible with any other keywords"))
+ end
+ return process_columnranges(setOutsideBorder, ws, colrng; outside)
+ end
+end
+function setBorder(ws::Worksheet, rowrng::RowRange;
+ outside::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ allsides::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ left::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ right::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ top::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ bottom::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ diagonal::Union{Nothing,Vector{Pair{String,String}}}=nothing
+)::Int
+ if isnothing(outside)
+ return process_rowranges(setBorder, ws, rowrng; allsides, left, right, top, bottom, diagonal)
+ else
+ if !all(isnothing, [left, right, top, bottom, diagonal, allsides])
+ throw(XLSXError("Keyword `outside` is incompatible with any other keywords except `diagonal`."))
+ end
+ return process_rowranges(setOutsideBorder, ws, rowrng; outside)
+ end
+end
+function setBorder(xl::XLSXFile, sheetcell::String;
+ outside::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ allsides::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ left::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ right::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ top::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ bottom::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ diagonal::Union{Nothing,Vector{Pair{String,String}}}=nothing
+)::Int
+ if isnothing(outside)
+ return process_sheetcell(setBorder, xl, sheetcell; allsides, left, right, top, bottom, diagonal)
+ else
+ if !all(isnothing, [left, right, top, bottom, diagonal, allsides])
+ throw(XLSXError("Keyword `outside` is incompatible with any other keywords except `diagonal`."))
+ end
+ return process_sheetcell(setOutsideBorder, xl, sheetcell; outside)
+ end
+end
function setBorder(sh::Worksheet, cellref::CellRef;
- allsides::Union{Nothing,Vector{Pair{String,String}}}=nothing,
- left::Union{Nothing,Vector{Pair{String,String}}}=nothing,
- right::Union{Nothing,Vector{Pair{String,String}}}=nothing,
- top::Union{Nothing,Vector{Pair{String,String}}}=nothing,
- bottom::Union{Nothing,Vector{Pair{String,String}}}=nothing,
- diagonal::Union{Nothing,Vector{Pair{String,String}}}=nothing
- )::Int
+ allsides::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ left::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ right::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ top::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ bottom::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+ diagonal::Union{Nothing,Vector{Pair{String,String}}}=nothing
+)::Int
+
+ if !get_xlsxfile(sh).use_cache_for_sheet_data
+ throw(XLSXError("Cannot set borders because cache is not enabled."))
+ end
- @assert get_xlsxfile(sh).use_cache_for_sheet_data "Cannot set borders because cache is not enabled."
+ if !isnothing(allsides)
+ if !all(isnothing, [left, right, top, bottom])
+ throw(XLSXError("Keyword `allsides` is incompatible with any other keywords except `diagonal`."))
+ end
+ return setBorder(sh, cellref; left=allsides, right=allsides, top=allsides, bottom=allsides, diagonal=diagonal)
+ end
kwdict = Dict{String,Union{Dict{String,String},Nothing}}()
kwdict["allsides"] = isnothing(allsides) ? nothing : Dict{String,String}(p for p in allsides)
@@ -898,21 +699,20 @@ function setBorder(sh::Worksheet, cellref::CellRef;
kwdict["bottom"] = isnothing(bottom) ? nothing : Dict{String,String}(p for p in bottom)
kwdict["diagonal"] = isnothing(diagonal) ? nothing : Dict{String,String}(p for p in diagonal)
- if !isnothing(allsides)
- @assert all(isnothing, [left, right, top, bottom]) "Keyword `allsides` is incompatible with any other keywords except `diagonal`."
- return setBorder(sh, cellref; left=allsides, right=allsides, top=allsides, bottom=allsides, diagonal=diagonal)
- end
-
wb = get_workbook(sh)
cell = getcell(sh, cellref)
- @assert !(cell isa EmptyCell) "Cannot set border for an `EmptyCell`: $(cellref.name). Set the value first."
+ if cell isa EmptyCell
+ throw(XLSXError("Cannot set border for an `EmptyCell`: $(cellref.name). Set the value first."))
+ end
+
+ allXfNodes=find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":cellXfs/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":xf", styles_xmlroot(wb))
if cell.style == ""
- cell.style = string(get_num_style_index(sh, 0).id)
+ cell.style = string(get_num_style_index(sh, allXfNodes, 0).id)
end
- cell_style = styles_cell_xf(wb, parse(Int, cell.style))
+ cell_style = styles_cell_xf(allXfNodes, parse(Int, cell.style))
new_border_atts = Dict{String,Union{Dict{String,String},Nothing}}()
cell_borders = getBorder(wb, cell_style)
@@ -921,38 +721,45 @@ function setBorder(sh::Worksheet, cellref::CellRef;
for a in ["left", "right", "top", "bottom", "diagonal"]
new_border_atts[a] = Dict{String,String}()
- if isnothing(kwdict[a]) && haskey(old_border_atts, a)
- new_border_atts[a] = old_border_atts[a]
- elseif !isnothing(kwdict[a])
- if !haskey(kwdict[a], "style") && haskey(old_border_atts, a) && haskey(old_border_atts[a], "style")
- new_border_atts[a]["style"] = old_border_atts[a]["style"]
- elseif haskey(kwdict[a], "style")
- @assert kwdict[a]["style"] ∈ ["none", "thin", "medium", "dashed", "dotted", "thick", "double", "hair", "mediumDashed", "dashDot", "mediumDashDot", "dashDotDot", "mediumDashDotDot", "slantDashDot"] "Invalid style: $v. Must be one of: `none`, `thin`, `medium`, `dashed`, `dotted`, `thick`, `double`, `hair`, `mediumDashed`, `dashDot`, `mediumDashDot`, `dashDotDot`, `mediumDashDotDot`, `slantDashDot`."
- new_border_atts[a]["style"] = kwdict[a]["style"]
- end
- if a == "diagonal"
- if !haskey(kwdict[a], "direction")
- if haskey(old_border_atts, a) && !isnothing(old_border_atts[a]) && haskey(old_border_atts[a], "direction")
- new_border_atts[a]["direction"] = old_border_atts[a]["direction"]
- else
- new_border_atts[a]["direction"] = "both" # default if direction not specified or inherited
+ if !isnothing(old_border_atts) # Need to merge new into old atts
+ if isnothing(kwdict[a]) && haskey(old_border_atts, a)
+ new_border_atts[a] = old_border_atts[a]
+ elseif !isnothing(kwdict[a])
+ if !haskey(kwdict[a], "style") && haskey(old_border_atts, a) && !isnothing(old_border_atts[a]) && haskey(old_border_atts[a], "style")
+ new_border_atts[a]["style"] = old_border_atts[a]["style"]
+ elseif haskey(kwdict[a], "style")
+ if kwdict[a]["style"] ∉ ["none", "thin", "medium", "dashed", "dotted", "thick", "double", "hair", "mediumDashed", "dashDot", "mediumDashDot", "dashDotDot", "mediumDashDotDot", "slantDashDot"]
+ throw(XLSXError("Invalid style: $v. Must be one of: `none`, `thin`, `medium`, `dashed`, `dotted`, `thick`, `double`, `hair`, `mediumDashed`, `dashDot`, `mediumDashDot`, `dashDotDot`, `mediumDashDotDot`, `slantDashDot`."))
end
- elseif haskey(kwdict[a], "direction")
- @assert kwdict[a]["direction"] ∈ ["up", "down", "both"] "Invalid direction: $v. Must be one of: `up`, `down`, `both`."
- new_border_atts[a]["direction"] = kwdict[a]["direction"]
+ new_border_atts[a]["style"] = kwdict[a]["style"]
end
- end
- if !haskey(kwdict[a], "color") && haskey(old_border_atts, a) && !isnothing(old_border_atts[a])
- for (k, v) in old_border_atts[a]
- if k != "style"
- new_border_atts[a][k] = v
+ if a == "diagonal"
+ if !haskey(kwdict[a], "direction")
+ if haskey(old_border_atts, a) && !isnothing(old_border_atts[a]) && haskey(old_border_atts[a], "direction")
+ new_border_atts[a]["direction"] = old_border_atts[a]["direction"]
+ else
+ new_border_atts[a]["direction"] = "both" # default if direction not specified or inherited
+ end
+ elseif haskey(kwdict[a], "direction")
+ if kwdict[a]["direction"] ∉ ["up", "down", "both"]
+ throw(XLSXError("Invalid direction: $v. Must be one of: `up`, `down`, `both`."))
+ end
+ new_border_atts[a]["direction"] = kwdict[a]["direction"]
+ end
+ end
+ if !haskey(kwdict[a], "color") && haskey(old_border_atts, a) && !isnothing(old_border_atts[a])
+ for (k, v) in old_border_atts[a]
+ if k != "style"
+ new_border_atts[a][k] = v
+ end
end
+ elseif haskey(kwdict[a], "color")
+ v = kwdict[a]["color"]
+ new_border_atts[a]["rgb"] = get_color(v)
end
- elseif haskey(kwdict[a], "color")
- v = kwdict[a]["color"]
- @assert occursin(r"^[0-9A-F]{8}$", v) "Invalid color value: $v. Must be an 8-digit hexadecimal RGB value."
- new_border_atts[a]["rgb"] = v
end
+ else
+ new_border_atts = kwdict
end
end
@@ -960,7 +767,7 @@ function setBorder(sh::Worksheet, cellref::CellRef;
new_borderid = styles_add_cell_attribute(wb, border_node, "borders")
- newstyle = string(update_template_xf(sh, CellDataFormat(parse(Int, cell.style)), ["borderId", "applyBorder"], ["$new_borderid", "1"]).id)
+ newstyle = string(update_template_xf(sh, allXfNodes, CellDataFormat(parse(Int, cell.style)), ["borderId", "applyBorder"], [string(new_borderid), "1"]).id)
cell.style = newstyle
return new_borderid
end
@@ -969,8 +776,12 @@ end
setUniformBorder(sh::Worksheet, cr::String; kw...) -> ::Int
setUniformBorder(xf::XLSXFile, cr::String, kw...) -> ::Int
-Set the border used by a cell range, a column range or a named range in a
-worksheet or XLSXfile to be uniformly the same border.
+ setUniformBorder(sh::Worksheet, rows, cols; kw...) -> ::Int
+
+Set the border used by a cell range, a column range or row range or
+a named range in a worksheet or XLSXfile to be uniformly the same border.
+Alternatively, specify the rows and columns using any combination of
+Integer, UnitRange, Vector{Integer} or `:`.
First, the border attributes of the first cell in the range (the top-left cell) are
updated according to the given `kw...` (using `setBorder()`). The resultant border is
@@ -980,7 +791,7 @@ As a result, every cell in the range will have a uniform border setting.
This differs from `setBorder()` which merges the attributes defined by `kw...` into
the border definition used by each cell individually. For example, if you set the
-border style to `thin` for a range of cells, but these cells all use different border
+border style to `thin` for a range of cells, but these cells all use different border
colors, `setBorder()` will change the border style but leave the border color unchanged
for each cell individually.
@@ -990,90 +801,127 @@ and `color`) to all the other cells in the range.
This can be more efficient when setting the same border for a large number of cells.
+Applying `setUniformBorder()` without any keyword arguments simply copies the `Border`
+attributes from the first cell specified to all the others.
+
The value returned is the `borderId` of the border uniformly applied to the cells.
If all cells in the range are `EmptyCells` the returned value is -1.
For keyword definitions see [`setBorder()`](@ref).
+Note: `setUniformBorder` cannot be used with the `outside` keyword.
+
# Examples:
```julia
Julia> setUniformBorder(sh, "B2:D6"; allsides = ["style" => "thick"], diagonal = ["style" => "hair"])
+Julia> setUniformBorder(sh, [1, 2, 3], [3, 5, 9]; allsides = ["style" => "thick"], diagonal = ["style" => "hair", "color" => "yellow2"])
+
Julia> setUniformBorder(xf, "Sheet1!A1:F20"; left = ["style" => "dotted", "color" => "FF000FF0"],
right = ["style" => "medium", "color" => "FF765000"],
top = ["style" => "thick", "color" => "FF230000"],
bottom = ["style" => "medium", "color" => "FF0000FF"],
diagonal = ["style" => "none"]
)
+
+julia> setUniformBorder(sh, "B2,A5:D22") # Copy `Border` from B2 to cells in A5:D22
+
```
"""
function setUniformBorder end
+setUniformBorder(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setUniformBorder(ws, rng.rng; kw...)
+setUniformBorder(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setUniformBorder(ws, rng.colrng; kw...)
+setUniformBorder(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setUniformBorder(ws, rng.rowrng; kw...)
setUniformBorder(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setUniformBorder, ws, colrng; kw...)
+setUniformBorder(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(setUniformBorder, ws, rowrng; kw...)
+setUniformBorder(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_uniform_ncranges(setBorder, ws, ncrng, ["borderId", "applyBorder"]; kw...)
setUniformBorder(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetcell(setUniformBorder, xl, sheetcell; kw...)
setUniformBorder(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setUniformBorder, ws, ref_or_rng; kw...)
+setUniformBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setBorder, ws, row, nothing; kw...)
+setUniformBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setBorder, ws, nothing, col; kw...)
+setUniformBorder(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setUniformBorder, ws, nothing, nothing; kw...)
+setUniformBorder(ws::Worksheet, ::Colon; kw...) = process_colon(setUniformBorder, ws, nothing, nothing; kw...)
+setUniformBorder(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_uniform_veccolon(setBorder, ws, row, nothing, ["borderId", "applyBorder"]; kw...)
+setUniformBorder(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_veccolon(setBorder, ws, nothing, col, ["borderId", "applyBorder"]; kw...)
+setUniformBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_vecint(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...)
+setUniformBorder(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...)
+setUniformBorder(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_vecint(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...)
+setUniformBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setUniformBorder(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
setUniformBorder(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attribute(setBorder, ws, rng, ["borderId", "applyBorder"]; kw...)
"""
setOutsideBorder(sh::Worksheet, cr::String; kw...) -> ::Int
setOutsideBorder(xf::XLSXFile, cr::String, kw...) -> ::Int
-Set the border around the outside of a cell range, a column range or a named
-range in a worksheet or XLSXfile.
+ setOutsideBorder(sh::Worksheet, rows, cols; kw...) -> ::Int
+
+Set the border around the outside of a cell range, a column range or row range
+or a named range in a worksheet or XLSXfile.
+Alternatively, specify the rows and columns using integers, UnitRanges or `:`.
-Two key words can be defined:
-- `style::String = nothing` : defines the style of the outside border
-- `color::String = nothing` : defines the color of the outside border
+There is one key word:
+- `outside::Vector{Pair{String,String} = nothing`
+
+For keyword definition see [`setBorder()`](@ref).
Only the border definitions for the sides of boundary cells that are on the
ouside edge of the range will be set to the specified style and color. The
-borders of internal edges and any diagonal will remain unchanged. Border
+borders of internal edges and any diagonals will remain unchanged. Border
settings for all internal cells in the range will remain unchanged.
-The value returned is is -1.
+Top and bottom borders for column ranges and left and right borders for
+row ranges are taken from the worksheet `dimension`.
-For keyword definitions see [`setBorder()`](@ref).
+An outside border cannot be set for a non-contiguous range.
+
+The value returned is is -1.
# Examples:
```julia
-Julia> setOutsideBorder(sh, "B2:D6"; style = "thick")
+Julia> setOutsideBorder(sh, "B2:D6"; outside = ["style" => "thick")
-Julia> setOutsideBorder(xf, "Sheet1!A1:F20"; style = "dotted", color = "FF000FF0")
-
+Julia> setOutsideBorder(xf, "Sheet1!A1:F20"; outside = ["style" => "dotted", "color" => "FF000FF0"])
```
+This function is equivalent to `setBorder()` called with the same arguments and keywords.
+
"""
function setOutsideBorder end
+setOutsideBorder(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setOutsideBorder(ws, rng.rng; kw...)
+setOutsideBorder(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setOutsideBorder(ws, rng.colrng; kw...)
+setOutsideBorder(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setOutsideBorder(ws, rng.rowrng; kw...)
setOutsideBorder(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setOutsideBorder, ws, colrng; kw...)
+setOutsideBorder(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(setOutsideBorder, ws, rowrng; kw...)
setOutsideBorder(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetcell(setOutsideBorder, xl, sheetcell; kw...)
setOutsideBorder(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setOutsideBorder, ws, ref_or_rng; kw...)
-function setOutsideBorder(ws::Worksheet, rng::CellRange;
- style::Union{String, Nothing}=nothing,
- color::Union{String, Nothing}=nothing
- )::Int
-
- @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot set borders because cache is not enabled."
-
- topLeft = CellRef(rng.start.row_number, rng.start.column_number)
- topRight = CellRef(rng.start.row_number, rng.stop.column_number)
- bottomLeft = CellRef(rng.stop.row_number, rng.start.column_number)
- bottomRight = CellRef(rng.stop.row_number, rng.stop.column_number)
- if !isnothing(style) && !isnothing(color)
- setBorder(ws, CellRange(topLeft, topRight); top= ["style" => style, "color" => color])
- setBorder(ws, CellRange(topLeft, bottomLeft); left= ["style" => style, "color" => color])
- setBorder(ws, CellRange(topRight, bottomRight); right= ["style" => style, "color" => color])
- setBorder(ws, CellRange(bottomLeft, bottomRight); bottom= ["style" => style, "color" => color])
- elseif !isnothing(style)
- setBorder(ws, CellRange(topLeft, topRight); top= ["style" => style])
- setBorder(ws, CellRange(topLeft, bottomLeft); left= ["style" => style])
- setBorder(ws, CellRange(topRight, bottomRight); right= ["style" => style])
- setBorder(ws, CellRange(bottomLeft, bottomRight); bottom= ["style" => style])
- elseif !isnothing(color)
- setBorder(ws, CellRange(topLeft, topRight); top= ["color" => color])
- setBorder(ws, CellRange(topLeft, bottomLeft); left= ["color" => color])
- setBorder(ws, CellRange(topRight, bottomRight); right= ["color" => color])
- setBorder(ws, CellRange(bottomLeft, bottomRight); bottom= ["color" => color])
+setOutsideBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setOutsideBorder, ws, row, nothing; kw...)
+setOutsideBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setOutsideBorder, ws, nothing, col; kw...)
+setOutsideBorder(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setOutsideBorder, ws, nothing, nothing; kw...)
+setOutsideBorder(ws::Worksheet, ::Colon; kw...) = process_colon(setOutsideBorder, ws, nothing, nothing; kw...)
+setOutsideBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setOutsideBorder(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
+function setOutsideBorder(ws::Worksheet, rng::CellRange;
+ outside::Union{Nothing,Vector{Pair{String,String}}}=nothing,
+)::Int
+
+ if !get_xlsxfile(ws).use_cache_for_sheet_data
+ throw(XLSXError("Cannot set borders because cache is not enabled."))
end
-
+
+ # length(rng) <= 1 && throw(XLSXError("Cannot set outside border for a single cell."))
+
+ kwdict = Dict{String,Union{Dict{String,String},Nothing}}()
+ kwdict["outside"] = Dict{String,String}(p for p in outside)
+
+
+ topLeft = CellRef(rng.start.row_number, rng.start.column_number)
+ topRight = CellRef(rng.start.row_number, rng.stop.column_number)
+ bottomLeft = CellRef(rng.stop.row_number, rng.start.column_number)
+ bottomRight = CellRef(rng.stop.row_number, rng.stop.column_number)
+
+ setBorder(ws, CellRange(topLeft, topRight); top=outside)
+ setBorder(ws, CellRange(topLeft, bottomLeft); left=outside)
+ setBorder(ws, CellRange(topRight, bottomRight); right=outside)
+ setBorder(ws, CellRange(bottomLeft, bottomRight); bottom=outside)
return -1
@@ -1086,8 +934,11 @@ end
"""
getFill(sh::Worksheet, cr::String) -> ::Union{Nothing, CellFill}
getFill(xf::XLSXFile, cr::String) -> ::Union{Nothing, CellFill}
+
+ getFill(sh::Worksheet, row::Int, col::Int) -> ::Union{Nothing, CellFill}
Get the fill used by a single cell at reference `cr` in a worksheet or XLSXfile.
+The specified cell must be within the sheet dimension.
Return a `CellFill` object containing:
- `fillId` : a 0-based index of the fill in the workbook
@@ -1152,6 +1003,8 @@ needed, they will simply be ignored by Excel, and the default appearance will be
```julia
julia> getFill(sh, "A1")
+julia> getFill(sh, 3, 4)
+
julia> getFill(xf, "Sheet1!A1")
```
@@ -1160,29 +1013,40 @@ function getFill end
getFill(xl::XLSXFile, sheetcell::String)::Union{Nothing,CellFill} = process_get_sheetcell(getFill, xl, sheetcell)
getFill(ws::Worksheet, cellref::CellRef)::Union{Nothing,CellFill} = process_get_cellref(getFill, ws, cellref)
getFill(ws::Worksheet, cr::String) = process_get_cellname(getFill, ws, cr)
+getFill(ws::Worksheet, row::Integer, col::Integer) = getFill(ws, CellRef(row, col))
getDefaultFill(ws::Worksheet) = getFill(get_workbook(ws), styles_cell_xf(get_workbook(ws), 0))
function getFill(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellFill}
- @assert get_xlsxfile(wb).use_cache_for_sheet_data "Cannot get fill because cache is not enabled."
+ if !get_xlsxfile(wb).use_cache_for_sheet_data
+ throw(XLSXError("Cannot get fill because cache is not enabled."))
+ end
if haskey(cell_style, "fillId")
fillid = cell_style["fillId"]
applyfill = haskey(cell_style, "applyFill") ? cell_style["applyFill"] : "0"
xroot = styles_xmlroot(wb)
- fill_elements = find_all_nodes("/$SPREADSHEET_NAMESPACE_XPATH_ARG:styleSheet/$SPREADSHEET_NAMESPACE_XPATH_ARG:fills", xroot)[begin]
- @assert parse(Int, fill_elements["count"]) == length(XML.children(fill_elements)) "Unexpected number of font definitions found : $(length(XML.children(fill_elements))). Expected $(parse(Int, fill_elements["count"]))"
+ fill_elements = find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":fills", xroot)[begin]
+ if parse(Int, fill_elements["count"]) != length(XML.children(fill_elements))
+ throw(XLSXError("Unexpected number of font definitions found : $(length(XML.children(fill_elements))). Expected $(parse(Int, fill_elements["count"]))"))
+ end
current_fill = XML.children(fill_elements)[parse(Int, fillid)+1] # Zero based!
fill_atts = Dict{String,Union{Dict{String,String},Nothing}}()
for pattern in XML.children(current_fill)
if isnothing(XML.attributes(pattern)) || length(XML.attributes(pattern)) == 0
fill_atts[XML.tag(pattern)] = nothing
else
- @assert length(XML.attributes(pattern)) == 1 "Too many fill attributes found for $(XML.tag(pattern)) Expected 1, found $(length(XML.attributes(pattern)))."
+ if length(XML.attributes(pattern)) != 1
+ throw(XLSXError("Too many fill attributes found for $(XML.tag(pattern)) Expected 1, found $(length(XML.attributes(pattern)))."))
+ end
for (k, v) in XML.attributes(pattern) # patternType is the only possible attribute of a fill
fill_atts[XML.tag(pattern)] = Dict(k => v)
for subc in XML.children(pattern) # foreground and background colors are children of a patternFill element
- @assert !isnothing(XML.children(subc)) && length(XML.attributes(subc)) > 0 "Too few children found for $(XML.tag(subc)) Expected 1, found 0."
- @assert length(XML.children(subc)) < 3 "Too many children found for $(XML.tag(subc)) Expected < 3, found $(length(XML.attributes(subc)))."
+ if !isnothing(XML.children(subc)) && length(XML.attributes(subc)) <= 0
+ throw(XLSXError("Too few children found for $(XML.tag(subc)) Expected 1, found 0."))
+ end
+ if length(XML.children(subc)) > 2
+ throw(XLSXError("Too many children found for $(XML.tag(subc)) Expected < 3, found $(length(XML.attributes(subc)))."))
+ end
tag = first(XML.tag(subc), 2)
for (k, v) in XML.attributes(subc)
fill_atts[XML.tag(pattern)][tag*k] = v
@@ -1200,9 +1064,13 @@ end
"""
setFill(sh::Worksheet, cr::String; kw...) -> ::Int}
setFill(xf::XLSXFile, cr::String; kw...) -> ::Int
-
+
+ setFill(sh::Worksheet, row, col; kw...) -> ::Int}
+
Set the fill used used by a single cell, a cell range, a column range or
-a named cell or named range in a worksheet or XLSXfile.
+row range or a named cell or named range in a worksheet or XLSXfile.
+Alternatively, specify the row and column using any combination of
+Integer, UnitRange, Vector{Integer} or `:`.
The following keywords are used to define a fill:
- `pattern::String = nothing` : Sets the patternType for the fill.
@@ -1230,8 +1098,10 @@ Here is a list of the available `pattern` values (thanks to Copilot!):
- `gray125`
- `gray0625`
-The two colors are set by specifying an 8-digit hexadecimal value for the `fgColor`
-and/or `bgColor` keywords. No other color attributes can be applied.
+The two colors may be set by specifying an 8-digit hexadecimal value for the `fgColor`
+and/or `bgColor` keywords.
+Alternatively, you can use the name of any named color from Colors.jl
+([here](https://juliagraphics.github.io/Colors.jl/stable/namedcolors/)).
Setting only one or two of the attributes leaves the other attribute(s) unchanged
for that cell's fill.
@@ -1249,34 +1119,59 @@ For cell ranges, column ranges and named ranges, the value returned is -1.
```julia
Julia> setFill(sh, "B2"; pattern="gray125", bgColor = "FF000000")
-Julia> setFill(xf, "Sheet1!A1:F20"; pattern="none", fgColor = "88FF8800")
+Julia> setFill(xf, "Sheet1!A1:F20"; pattern="none", fgColor = "darkseagreen3")
+
+Julia> setFill(sh, "11:24"; pattern="none", fgColor = "yellow2")
```
"""
function setFill end
+setFill(ws::Worksheet, ref::SheetCellRef; kw...) = do_sheet_names_match(ws, ref) && setFill(ws, ref.cellref; kw...)
+setFill(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setFill(ws, rng.rng; kw...)
+setFill(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setFill(ws, rng.colrng; kw...)
+setFill(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setFill(ws, rng.rowrng; kw...)
setFill(ws::Worksheet, rng::CellRange; kw...)::Int = process_cellranges(setFill, ws, rng; kw...)
+setFill(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(setFill, ws, rowrng; kw...)
+setFill(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setFill, ws, ncrng; kw...)
setFill(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setFill, ws, colrng; kw...)
setFill(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setFill, ws, ref_or_rng; kw...)
setFill(xl::XLSXFile, sheetcell::String; kw...)::Int = process_sheetcell(setFill, xl, sheetcell; kw...)
+setFill(ws::Worksheet, row::Integer, col::Integer; kw...) = setFill(ws, CellRef(row, col); kw...)
+setFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setFill, ws, row, nothing; kw...)
+setFill(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setFill, ws, nothing, nothing; kw...)
+setFill(ws::Worksheet, ::Colon; kw...) = process_colon(setFill, ws, nothing, nothing; kw...)
+setFill(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setFill, ws, nothing, col; kw...)
+setFill(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setFill, ws, row, nothing; kw...)
+setFill(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_veccolon(setFill, ws, nothing, col; kw...)
+setFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_vecint(setFill, ws, row, col; kw...)
+setFill(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFill, ws, row, col; kw...)
+setFill(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_vecint(setFill, ws, row, col; kw...)
+setFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setFill(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
function setFill(sh::Worksheet, cellref::CellRef;
- pattern::Union{Nothing,String}=nothing,
- fgColor::Union{Nothing,String}=nothing,
- bgColor::Union{Nothing,String}=nothing,
- )::Int
+ pattern::Union{Nothing,String}=nothing,
+ fgColor::Union{Nothing,String}=nothing,
+ bgColor::Union{Nothing,String}=nothing,
+)::Int
- @assert get_xlsxfile(sh).use_cache_for_sheet_data "Cannot set fill because cache is not enabled."
+ if !get_xlsxfile(sh).use_cache_for_sheet_data
+ throw(XLSXError("Cannot set fill because cache is not enabled."))
+ end
wb = get_workbook(sh)
cell = getcell(sh, cellref)
- @assert !(cell isa EmptyCell) "Cannot set fill for an `EmptyCell`: $(cellref.name). Set the value first."
+ if cell isa EmptyCell
+ throw(XLSXError("Cannot set fill for an `EmptyCell`: $(cellref.name). Set the value first."))
+ end
+
+ allXfNodes=find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":cellXfs/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":xf", styles_xmlroot(wb))
if cell.style == ""
- cell.style = string(get_num_style_index(sh, 0).id)
+ cell.style = string(get_num_style_index(sh, allXfNodes, 0).id)
end
- cell_style = styles_cell_xf(wb, parse(Int, cell.style))
-
+ cell_style = styles_cell_xf(allXfNodes, parse(Int, cell.style))
+
new_fill_atts = Dict{String,Union{Dict{String,String},Nothing}}()
patternFill = Dict{String,String}()
@@ -1289,6 +1184,9 @@ function setFill(sh::Worksheet, cellref::CellRef;
if isnothing(pattern) && haskey(old_fill_atts, "patternType")
patternFill["patternType"] = old_fill_atts["patternType"]
elseif !isnothing(pattern)
+ if pattern ∉ ["none", "solid", "mediumGray", "darkGray", "lightGray", "darkHorizontal", "darkVertical", "darkDown", "darkUp", "darkGrid", "darkTrellis", "lightHorizontal", "lightVertical", "lightDown", "lightUp", "lightGrid", "lightTrellis", "gray125", "gray0625"]
+ throw(XLSXError("Invalid style: $pattern. Must be one of: `none`, `solid`, `mediumGray`, `darkGray`, `lightGray`, `darkHorizontal`, `darkVertical`, `darkDown`, `darkUp`, `darkGrid`, `darkTrellis`, `lightHorizontal`, `lightVertical`, `lightDown`, `lightUp`, `lightGrid`, `lightTrellis`, `gray125`, `gray0625`."))
+ end
patternFill["patternType"] = pattern
end
elseif a == "fg"
@@ -1299,8 +1197,7 @@ function setFill(sh::Worksheet, cellref::CellRef;
end
end
else
- @assert occursin(r"^[0-9A-F]{8}$", fgColor) "Invalid color value: $fgColor. Must be an 8-digit hexadecimal RGB value."
- patternFill["fgrgb"] = fgColor
+ patternFill["fgrgb"] = get_color(fgColor)
end
elseif a == "bg"
if isnothing(bgColor)
@@ -1310,8 +1207,7 @@ function setFill(sh::Worksheet, cellref::CellRef;
end
end
else
- @assert occursin(r"^[0-9A-F]{8}$", bgColor) "Invalid color value: $bgColor. Must be an 8-digit hexadecimal RGB value."
- patternFill["bgrgb"] = bgColor
+ patternFill["bgrgb"] = get_color(bgColor)
end
end
end
@@ -1321,7 +1217,7 @@ function setFill(sh::Worksheet, cellref::CellRef;
new_fillid = styles_add_cell_attribute(wb, fill_node, "fills")
- newstyle = string(update_template_xf(sh, CellDataFormat(parse(Int, cell.style)), ["fillId", "applyFill"], ["$new_fillid", "1"]).id)
+ newstyle = string(update_template_xf(sh, allXfNodes, CellDataFormat(parse(Int, cell.style)), ["fillId", "applyFill"], [string(new_fillid), "1"]).id)
cell.style = newstyle
return new_fillid
end
@@ -1330,8 +1226,12 @@ end
setUniformFill(sh::Worksheet, cr::String; kw...) -> ::Int
setUniformFill(xf::XLSXFile, cr::String, kw...) -> ::Int
-Set the fill used by a cell range, a column range or a named range in a
-worksheet or XLSXfile to be uniformly the same fill.
+ setUniformFill(sh::Worksheet, rows, cols; kw...) -> ::Int
+
+Set the fill used by a cell range, a column range or row range or a
+named range in a worksheet or XLSXfile to be uniformly the same fill.
+Alternatively, specify the rows and columns using any combination of
+Integer, UnitRange, Vector{Integer} or `:`.
First, the fill attributes of the first cell in the range (the top-left cell) are
updated according to the given `kw...` (using `setFill()`). The resultant fill is
@@ -1351,6 +1251,9 @@ and both foreground and background colors) to all the other cells in the range.
This can be more efficient when setting the same fill for a large number of cells.
+Applying `setUniformFill()` without any keyword arguments simply copies the `Fill`
+attributes from the first cell specified to all the others.
+
The value returned is the `fillId` of the fill uniformly applied to the cells.
If all cells in the range are `EmptyCells` the returned value is -1.
@@ -1360,14 +1263,31 @@ For keyword definitions see [`setFill()`](@ref).
```julia
Julia> setUniformFill(sh, "B2:D4"; pattern="gray125", bgColor = "FF000000")
-Julia> setUniformFill(xf, "Sheet1!A1:F20"; pattern="none", fgColor = "88FF8800")
+Julia> setUniformFill(xf, "Sheet1!A1:F20"; pattern="none", fgColor = "darkseagreen3")
+
+julia> setUniformFill(sh, "B2,A5:D22") # Copy `Fill` from B2 to cells in A5:D22
```
"""
function setUniformFill end
+setUniformFill(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setUniformFill(ws, rng.rng; kw...)
+setUniformFill(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setUniformFill(ws, rng.colrng; kw...)
+setUniformFill(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setUniformFill(ws, rng.rowrng; kw...)
setUniformFill(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setUniformFill, ws, colrng; kw...)
+setUniformFill(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(setUniformFill, ws, rowrng; kw...)
+setUniformFill(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_uniform_ncranges(setFill, ws, ncrng, ["fillId", "applyFill"]; kw...)
setUniformFill(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetcell(setUniformFill, xl, sheetcell; kw...)
setUniformFill(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setUniformFill, ws, ref_or_rng; kw...)
+setUniformFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setFill, ws, row, nothing; kw...)
+setUniformFill(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setFill, ws, nothing, col; kw...)
+setUniformFill(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setUniformFill, ws, nothing, nothing; kw...)
+setUniformFill(ws::Worksheet, ::Colon; kw...) = process_colon(setUniformFill, ws, nothing, nothing; kw...)
+setUniformFill(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_uniform_veccolon(setFill, ws, row, nothing, ["fillId", "applyFill"]; kw...)
+setUniformFill(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_veccolon(setFill, ws, nothing, col, ["fillId", "applyFill"]; kw...)
+setUniformFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_vecint(setFill, ws, row, col, ["fillId", "applyFill"]; kw...)
+setUniformFill(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFill, ws, row, col, ["fillId", "applyFill"]; kw...)
+setUniformFill(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_vecint(setFill, ws, row, col, ["fillId", "applyFill"]; kw...)
+setUniformFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setUniformFill(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
setUniformFill(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attribute(setFill, ws, rng, ["fillId", "applyFill"]; kw...)
#
@@ -1377,8 +1297,11 @@ setUniformFill(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attr
"""
getAlignment(sh::Worksheet, cr::String) -> ::Union{Nothing, CellAlignment}
getAlignment(xf::XLSXFile, cr::String) -> ::Union{Nothing, CellAlignment}
+
+ getAlignment(sh::Worksheet, row::Int, col::Int) -> ::Union{Nothing, CellAlignment}
Get the alignment used by a single cell at reference `cr` in a worksheet or XLSXfile.
+The specified cell must be within the sheet dimension.
Return a `CellAlignment` object containing:
- `alignment` : a dictionary of alignment attributes: alignmentAttribute -> (attribute -> value)
@@ -1418,6 +1341,8 @@ Excel supports the following values for the vertical alignment:
```julia
julia> getAlignment(sh, "A1")
+julia> getAlignment(sh, 2, 5) # Cell E2
+
julia> getAlignment(xf, "Sheet1!A1")
```
@@ -1426,19 +1351,24 @@ function getAlignment end
getAlignment(xl::XLSXFile, sheetcell::String)::Union{Nothing,CellAlignment} = process_get_sheetcell(getAlignment, xl, sheetcell)
getAlignment(ws::Worksheet, cellref::CellRef)::Union{Nothing,CellAlignment} = process_get_cellref(getAlignment, ws, cellref)
getAlignment(ws::Worksheet, cr::String) = process_get_cellname(getAlignment, ws, cr)
+getAlignment(ws::Worksheet, row::Integer, col::Integer) = getAlignment(ws, CellRef(row, col))
#getDefaultAlignment(ws::Worksheet) = getAlignment(get_workbook(ws), styles_cell_xf(get_workbook(ws), 0))
function getAlignment(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellAlignment}
- @assert get_xlsxfile(wb).use_cache_for_sheet_data "Cannot get alignment because cache is not enabled."
+ if !get_xlsxfile(wb).use_cache_for_sheet_data
+ throw(XLSXError("Cannot get alignment because cache is not enabled."))
+ end
if length(XML.children(cell_style)) == 0 # `alignment` is a child node of the cell `xf`.
return nothing
end
- @assert length(XML.children(cell_style)) == 1 "Expected cell `xf` to have 1 child node, found $(length(XML.children(cell_style)))"
- @assert XML.tag(cell_style[1]) == "alignment" "Error cell alignment found but it has no attributes!"
+ if length(XML.children(cell_style)) != 1
+ throw(XLSXError("Expected cell style to have 1 child node, found $(length(XML.children(cell_style)))"))
+ end
+ XML.tag(cell_style[1]) != "alignment" && throw(XLSXError("Cell style has a child node but it is not for alignment!"))
atts = Dict{String,String}()
for (k, v) in XML.attributes(cell_style[1])
- atts[k]=v
+ atts[k] = v
end
alignment_atts = Dict{String,Union{Dict{String,String},Nothing}}()
alignment_atts["alignment"] = atts
@@ -1449,9 +1379,14 @@ end
"""
setAlignment(sh::Worksheet, cr::String; kw...) -> ::Int}
setAlignment(xf::XLSXFile, cr::String; kw...) -> ::Int}
+
+ setAlignment(sh::Worksheet, row, col; kw...) -> ::Int}
+
Set the alignment used used by a single cell, a cell range, a column range or
-a named cell or named range in a worksheet or XLSXfile.
+row range or a named cell or named range in a worksheet or XLSXfile.
+Alternatively, specify the row and column using any combination of
+Integer, UnitRange, Vector{Integer} or `:`.
The following keywords are used to define an alignment:
- `horizontal::String = nothing` : Sets the horizontal alignment.
@@ -1493,35 +1428,60 @@ julia> setAlignment(sh, "D18"; horizontal="center", wrapText=true)
julia> setAlignment(xf, "sheet1!D18"; horizontal="right", vertical="top", wrapText=true)
julia> setAlignment(sh, "L6"; horizontal="center", rotation="90", shrink=true, indent="2")
+
+julia> setAlignment(sh, 1:3, 3:6; horizontal="center", rotation="90", shrink=true, indent="2")
```
"""
function setAlignment end
+setAlignment(ws::Worksheet, ref::SheetCellRef; kw...) = do_sheet_names_match(ws, ref) && setAlignment(ws, ref.cellref; kw...)
+setAlignment(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setAlignment(ws, rng.rng; kw...)
+setAlignment(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setAlignment(ws, rng.colrng; kw...)
+setAlignment(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setAlignment(ws, rng.rowrng; kw...)
setAlignment(ws::Worksheet, rng::CellRange; kw...)::Int = process_cellranges(setAlignment, ws, rng; kw...)
setAlignment(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setAlignment, ws, colrng; kw...)
+setAlignment(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(setAlignment, ws, rowrng; kw...)
+setAlignment(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setAlignment, ws, ncrng; kw...)
setAlignment(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setAlignment, ws, ref_or_rng; kw...)
setAlignment(xl::XLSXFile, sheetcell::String; kw...)::Int = process_sheetcell(setAlignment, xl, sheetcell; kw...)
-function setAlignment(sh::Worksheet, cellref::CellRef;
+setAlignment(ws::Worksheet, row::Integer, col::Integer; kw...) = setAlignment(ws, CellRef(row, col); kw...)
+setAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setAlignment, ws, row, nothing; kw...)
+setAlignment(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setAlignment, ws, nothing, nothing; kw...)
+setAlignment(ws::Worksheet, ::Colon; kw...) = process_colon(setAlignment, ws, nothing, nothing; kw...)
+setAlignment(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setAlignment, ws, nothing, col; kw...)
+setAlignment(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setAlignment, ws, row, nothing; kw...)
+setAlignment(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_veccolon(setAlignment, ws, nothing, col; kw...)
+setAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_vecint(setAlignment, ws, row, col; kw...)
+setAlignment(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setAlignment, ws, row, col; kw...)
+setAlignment(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_vecint(setAlignment, ws, row, col; kw...)
+setAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setAlignment(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
+function setAlignment(sh::Worksheet, cellref::CellRef;
horizontal::Union{Nothing,String}=nothing,
vertical::Union{Nothing,String}=nothing,
wrapText::Union{Nothing,Bool}=nothing,
shrink::Union{Nothing,Bool}=nothing,
indent::Union{Nothing,Int}=nothing,
rotation::Union{Nothing,Int}=nothing
- )::Int
+)::Int
- @assert get_xlsxfile(sh).use_cache_for_sheet_data "Cannot set alignment because cache is not enabled."
+ if !get_xlsxfile(sh).use_cache_for_sheet_data
+ throw(XLSXError("Cannot set alignment because cache is not enabled."))
+ end
wb = get_workbook(sh)
cell = getcell(sh, cellref)
- @assert !(cell isa EmptyCell) "Cannot set fill for an `EmptyCell`: $(cellref.name). Set the value first."
+ if cell isa EmptyCell
+ throw(XLSXError("Cannot set alignment for an `EmptyCell`: $(cellref.name). Set the value first."))
+ end
+
+ allXfNodes=find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":cellXfs/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":xf", styles_xmlroot(wb))
if cell.style == ""
- cell.style = string(get_num_style_index(sh, 0).id)
+ cell.style = string(get_num_style_index(sh, allXfNodes, 0).id)
end
- cell_style = styles_cell_xf(wb, parse(Int, cell.style))
+ cell_style = styles_cell_xf(allXfNodes, parse(Int, cell.style))
atts = XML.OrderedDict{String,String}()
cell_alignment = getAlignment(wb, cell_style)
@@ -1531,12 +1491,12 @@ function setAlignment(sh::Worksheet, cellref::CellRef;
old_applyAlignment = cell_alignment.applyAlignment
end
- @assert isnothing(horizontal) || horizontal ∈ ["left", "center", "right", "fill", "justify", "centerContinuous", "distributed"] "Invalid horizontal alignment: $horizontal. Must be one of: `left`, `center`, `right`, `fill`, `justify`, `centerContinuous`, `distributed`."
- @assert isnothing(vertical) || vertical ∈ ["top", "center", "bottom", "justify", "distributed"] "Invalid vertical aligment: $vertical. Must be one of: `top`, `center`, `bottom`, `justify`, `distributed`."
- @assert isnothing(wrapText) || wrapText ∈ [true, false] "Invalid wrap option: $wrapText. Must be one of: `true`, `false`."
- @assert isnothing(shrink) || shrink ∈ [true, false] "Invalid shrink option: $shrink. Must be one of: `true`, `false`."
- @assert isnothing(indent) || indent > 0 "Invalid indent value specified: $indent. Must be a postive integer."
- @assert isnothing(rotation) || rotation ∈ -90:90 "Invalid rotation value specified: $rotation. Must be an integer between -90 and 90."
+ !isnothing(horizontal) && horizontal ∉ ["left", "center", "right", "fill", "justify", "centerContinuous", "distributed"] && throw(XLSXError("Invalid horizontal alignment: $horizontal. Must be one of: `left`, `center`, `right`, `fill`, `justify`, `centerContinuous`, `distributed`."))
+ !isnothing(vertical) && vertical ∉ ["top", "center", "bottom", "justify", "distributed"] && throw(XLSXError("Invalid vertical aligment: $vertical. Must be one of: `top`, `center`, `bottom`, `justify`, `distributed`."))
+ !isnothing(wrapText) && wrapText ∉ [true, false] && throw(XLSXError("Invalid wrap option: $wrapText. Must be one of: `true`, `false`."))
+ !isnothing(shrink) && shrink ∉ [true, false] && throw(XLSXError("Invalid shrink option: $shrink. Must be one of: `true`, `false`."))
+ !isnothing(indent) && indent < 0 && throw(XLSXError("Invalid indent value specified: $indent. Must be a postive integer."))
+ !isnothing(rotation) && rotation ∉ -90:90 && throw(XLSXError("Invalid rotation value specified: $rotation. Must be an integer between -90 and 90."))
if isnothing(horizontal) && !isnothing(cell_alignment) && haskey(old_alignment_atts, "horizontal")
atts["horizontal"] = old_alignment_atts["horizontal"]
@@ -1571,7 +1531,7 @@ function setAlignment(sh::Worksheet, cellref::CellRef;
alignment_node = XML.Node(XML.Element, "alignment", atts, nothing, nothing)
- newstyle = string(update_template_xf(sh, CellDataFormat(parse(Int, cell.style)), alignment_node).id)
+ newstyle = string(update_template_xf(sh, allXfNodes, CellDataFormat(parse(Int, cell.style)), alignment_node).id)
cell.style = newstyle
return parse(Int, newstyle)
@@ -1581,8 +1541,12 @@ end
setUniformAlignment(sh::Worksheet, cr::String; kw...) -> ::Int
setUniformAlignment(xf::XLSXFile, cr::String, kw...) -> ::Int
-Set the alignment used by a cell range, a column range or a named range in a
-worksheet or XLSXfile to be uniformly the same alignment.
+ setUniformAlignment(sh::Worksheet, rows, cols; kw...) -> ::Int
+
+Set the alignment used by a cell range, a column range or row range or a
+named range in a worksheet or XLSXfile to be uniformly the same alignment.
+Alternatively, specify the rows and columns using any combination of
+Integer, UnitRange, Vector{Integer} or `:`.
First, the alignment attributes of the first cell in the range (the top-left cell) are
updated according to the given `kw...` (using `setAlignment()`). The resultant alignment
@@ -1602,6 +1566,9 @@ cell to all the other cells in the range.
This can be more efficient when setting the same alignment for a large number of cells.
+Applying `setUniformAlignment()` without any keyword arguments simply copies the `Alignment`
+attributes from the first cell specified to all the others.
+
The value returned is the `styleId` of the reference (top-left) cell, from which the
alignment uniformly applied to the cells was taken.
If all cells in the range are `EmptyCells`, the returned value is -1.
@@ -1613,13 +1580,32 @@ For keyword definitions see [`setAlignment()`](@ref).
Julia> setUniformAlignment(sh, "B2:D4"; horizontal="center", wrap = true)
Julia> setUniformAlignment(xf, "Sheet1!A1:F20"; horizontal="center", vertical="top")
+
+Julia> setUniformAlignment(sh, :, 1:24; horizontal="center", vertical="top")
+
+julia> setUniformAlignment(sh, "B2,A5:D22") # Copy `Alignment` from B2 to cells in A5:D22
```
"""
function setUniformAlignment end
+setUniformAlignment(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setUniformAlignment(ws, rng.rng; kw...)
+setUniformAlignment(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setUniformAlignment(ws, rng.colrng; kw...)
+setUniformAlignment(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setUniformAlignment(ws, rng.rowrng; kw...)
setUniformAlignment(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setUniformAlignment, ws, colrng; kw...)
+setUniformAlignment(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(setUniformAlignment, ws, rowrng; kw...)
+setUniformAlignment(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_uniform_ncranges(setAlignment, ws, ncrng; kw...)
setUniformAlignment(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetcell(setUniformAlignment, xl, sheetcell; kw...)
setUniformAlignment(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setUniformAlignment, ws, ref_or_rng; kw...)
+setUniformAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setAlignment, ws, row, nothing; kw...)
+setUniformAlignment(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setAlignment, ws, nothing, col; kw...)
+setUniformAlignment(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setUniformAlignment, ws, nothing, nothing; kw...)
+setUniformAlignment(ws::Worksheet, ::Colon; kw...) = process_colon(setUniformAlignment, ws, nothing, nothing; kw...)
+setUniformAlignment(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_uniform_veccolon(setAlignment, ws, row, nothing; kw...)
+setUniformAlignment(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_veccolon(setAlignment, ws, nothing, col; kw...)
+setUniformAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_vecint(setAlignment, ws, row, col; kw...)
+setUniformAlignment(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setAlignment, ws, row, col; kw...)
+setUniformAlignment(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_vecint(setAlignment, ws, row, col; kw...)
+setUniformAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setUniformAlignment(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
setUniformAlignment(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attribute(setAlignment, ws, rng; kw...)
#
@@ -1629,8 +1615,11 @@ setUniformAlignment(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform
"""
getFormat(sh::Worksheet, cr::String) -> ::Union{Nothing, CellFormat}
getFormat(xf::XLSXFile, cr::String) -> ::Union{Nothing, CellFormat}
+
+ getFormat(sh::Worksheet, row::Int, col::int) -> ::Union{Nothing, CellFormat}
Get the format (numFmt) used by a single cell at reference `cr` in a worksheet or XLSXfile.
+The specified cell must be within the sheet dimension.
Return a `CellFormat` object containing:
- `numFmtId` : a 0-based index of the formats in the workbook. Values below 164 are
@@ -1649,6 +1638,8 @@ the format for built-in formats, too.
julia> getFormat(sh, "A1")
julia> getFormat(xf, "Sheet1!A1")
+
+julia> getFormat(sh, 1, 1)
```
"""
@@ -1656,10 +1647,13 @@ function getFormat end
getFormat(xl::XLSXFile, sheetcell::String)::Union{Nothing,CellFormat} = process_get_sheetcell(getFormat, xl, sheetcell)
getFormat(ws::Worksheet, cellref::CellRef)::Union{Nothing,CellFormat} = process_get_cellref(getFormat, ws, cellref)
getFormat(ws::Worksheet, cr::String) = process_get_cellname(getFormat, ws, cr)
+getFormat(ws::Worksheet, row::Integer, col::Integer) = getFormat(ws, CellRef(row, col))
#getDefaultFill(ws::Worksheet) = getFormat(get_workbook(ws), styles_cell_xf(get_workbook(ws), 0))
function getFormat(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellFormat}
- @assert get_xlsxfile(wb).use_cache_for_sheet_data "Cannot get number formats because cache is not enabled."
+ if !get_xlsxfile(wb).use_cache_for_sheet_data
+ throw(XLSXError("Cannot get number formats because cache is not enabled."))
+ end
if haskey(cell_style, "numFmtId")
numfmtid = cell_style["numFmtId"]
@@ -1667,17 +1661,22 @@ function getFormat(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellFormat
format_atts = Dict{String,Union{Dict{String,String},Nothing}}()
if parse(Int, numfmtid) >= PREDEFINED_NUMFMT_COUNT
xroot = styles_xmlroot(wb)
- format_elements = find_all_nodes("/$SPREADSHEET_NAMESPACE_XPATH_ARG:styleSheet/$SPREADSHEET_NAMESPACE_XPATH_ARG:numFmts", xroot)[begin]
- @assert parse(Int, format_elements["count"]) == length(XML.children(format_elements)) "Unexpected number of format definitions found : $(length(XML.children(format_elements))). Expected $(parse(Int, format_elements["count"]))"
- current_format = XML.children(format_elements)[parse(Int, numfmtid)+1-PREDEFINED_NUMFMT_COUNT] # Zero based!
- @assert length(XML.attributes(current_format)) == 2 "Wrong number of attributes found for $(XML.tag(current_format)) Expected 2, found $(length(XML.attributes(current_format)))."
+ format_elements = find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":numFmts", xroot)[begin]
+ if parse(Int, format_elements["count"]) != length(XML.children(format_elements))
+ throw(XLSXError("Unexpected number of format definitions found : $(length(XML.children(format_elements))). Expected $(parse(Int, format_elements["count"]))"))
+ end
+ current_format = [x for x in XML.children(format_elements) if x["numFmtId"] == numfmtid][1]
+ if length(XML.attributes(current_format)) != 2
+ throw(XLSXError("Wrong number of attributes found for $(XML.tag(current_format)) Expected 2, found $(length(XML.attributes(current_format)))."))
+ end
for (k, v) in XML.attributes(current_format)
format_atts[XML.tag(current_format)] = Dict(k => XML.unescape(v))
end
else
-# any(num in r for r in ranges)
ranges = [0:22, 37:40, 45:49]
- @assert any(parse(Int, numfmtid) == n for r ∈ ranges for n ∈ r) "Expected a built in format ID in the following ranges: 1:22, 37:40, 45:49. Got $numfmtid."
+ if !any(parse(Int, numfmtid) == n for r ∈ ranges for n ∈ r)
+ throw(XLSXError("Expected a built in format ID in the following ranges: 1:22, 37:40, 45:49. Got $numfmtid."))
+ end
if haskey(builtinFormats, numfmtid)
format_atts["numFmt"] = Dict("numFmtId" => numfmtid, "formatCode" => builtinFormats[numfmtid])
end
@@ -1691,9 +1690,13 @@ end
"""
setFormat(sh::Worksheet, cr::String; kw...) -> ::Int
setFormat(xf::XLSXFile, cr::String; kw...) -> ::Int
+
+ setFormat(sh::Worksheet, row, col; kw...) -> ::Int
-Set the format used used by a single cell, a cell range, a column range or
-a named cell or named range in a worksheet or XLSXfile.
+Set the number format used used by a single cell, a cell range, a column
+range or row range or a named cell or named range in a worksheet or
+XLSXfile. Alternatively, specify the row and column using any combination
+of Integer, UnitRange, Vector{Integer} or `:`.
The function uses one keyword used to define a format:
- `format::String = nothing` : Defines a built-in or custom number format
@@ -1725,33 +1728,58 @@ julia> XLSX.setFormat(xf, "Sheet1!A2"; format = "# ??/??")
julia> XLSX.setFormat(sh, "F1:F5"; format = "Currency")
+julia> XLSX.setFormat(sh, "named_range"; format = "Percentage")
+
julia> XLSX.setFormat(sh, "A2"; format = "_-£* #,##0.00_-;-£* #,##0.00_-;_-£* \\\"-\\\"??_-;_-@_-")
```
"""
function setFormat end
+setFormat(ws::Worksheet, ref::SheetCellRef; kw...) = do_sheet_names_match(ws, ref) && setFormat(ws, ref.cellref; kw...)
+setFormat(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setFormat(ws, rng.rng; kw...)
+setFormat(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setFormat(ws, rng.colrng; kw...)
+setFormat(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setFormat(ws, rng.rowrng; kw...)
setFormat(ws::Worksheet, rng::CellRange; kw...)::Int = process_cellranges(setFormat, ws, rng; kw...)
setFormat(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setFormat, ws, colrng; kw...)
+setFormat(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setFormat, ws, ncrng; kw...)
+setFormat(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(setFormat, ws, rowrng; kw...)
setFormat(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setFormat, ws, ref_or_rng; kw...)
setFormat(xl::XLSXFile, sheetcell::String; kw...)::Int = process_sheetcell(setFormat, xl, sheetcell; kw...)
+setFormat(ws::Worksheet, row::Integer, col::Integer; kw...) = setFormat(ws, CellRef(row, col); kw...)
+setFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setFormat, ws, row, nothing; kw...)
+setFormat(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setFormat, ws, nothing, col; kw...)
+setFormat(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setFormat, ws, nothing, nothing; kw...)
+setFormat(ws::Worksheet, ::Colon; kw...) = process_colon(setFormat, ws, nothing, nothing; kw...)
+setFormat(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setFormat, ws, row, nothing; kw...)
+setFormat(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_veccolon(setFormat, ws, nothing, col; kw...)
+setFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_vecint(setFormat, ws, row, col; kw...)
+setFormat(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFormat, ws, row, col; kw...)
+setFormat(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_vecint(setFormat, ws, row, col; kw...)
+setFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setFormat(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
function setFormat(sh::Worksheet, cellref::CellRef;
- format::Union{Nothing,String}=nothing,
- )::Int
+ format::Union{Nothing,String}=nothing,
+)::Int
- @assert get_xlsxfile(sh).use_cache_for_sheet_data "Cannot set number formats because cache is not enabled."
+ if !get_xlsxfile(sh).use_cache_for_sheet_data
+ throw(XLSXError("Cannot set number formats because cache is not enabled."))
+ end
wb = get_workbook(sh)
cell = getcell(sh, cellref)
- @assert !(cell isa EmptyCell) "Cannot set format for an `EmptyCell`: $(cellref.name). Set the value first."
+ if cell isa EmptyCell
+ throw(XLSXError("Cannot set format for an `EmptyCell`: $(cellref.name). Set the value first."))
+ end
+
+ allXfNodes=find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":cellXfs/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":xf", styles_xmlroot(wb))
if cell.style == ""
- cell.style = string(get_num_style_index(sh, 0).id)
+ cell.style = string(get_num_style_index(sh, allXfNodes, 0).id)
end
- cell_style = styles_cell_xf(wb, parse(Int, cell.style))
-
-# new_format_atts = Dict{String,Union{Dict{String,String},Nothing}}()
+ cell_style = styles_cell_xf(allXfNodes, parse(Int, cell.style))
+
+ # new_format_atts = Dict{String,Union{Dict{String,String},Nothing}}()
new_format = XML.OrderedDict{String,String}()
cell_format = getFormat(wb, cell_style)
@@ -1759,43 +1787,21 @@ function setFormat(sh::Worksheet, cellref::CellRef;
old_applyNumberFormat = cell_format.applyNumberFormat
if isnothing(format) # User didn't specify any format so this is a no-op
- return cell_format.formatId
+ return cell_format.numFmtId
end
- if haskey(builtinFormatNames, uppercasefirst(format)) # User specified a format by name
- new_formatid = builtinFormatNames[uppercasefirst(format)]
- else # user specified a format code
- code = lowercase(format)
- code = remove_formatting(code)
- @assert occursin(floatformats, code) || any(map(x->occursin(x, code), DATETIME_CODES)) "Specified format is not a valid numFmt: $format"
-
- xroot = styles_xmlroot(wb)
- i, j = get_idces(xroot, "styleSheet", "numFmts")
- if isnothing(j) # There are no existing custom formats
- new_formatid = styles_add_numFmt(wb, format)
- else
- existing_elements_count = length(XML.children(xroot[i][j]))
- @assert parse(Int, xroot[i][j]["count"]) == existing_elements_count "Wrong number of font elements found: $existing_elements_count. Expected $(parse(Int, xroot[i][j]["count"]))."
-
- format_node = XML.Element("numFmt";
- numFmtId = string(existing_elements_count + PREDEFINED_NUMFMT_COUNT),
- formatCode = xlsx_escape(format)
- )
-
- new_formatid = styles_add_cell_attribute(wb, format_node, "numFmts") + PREDEFINED_NUMFMT_COUNT
- end
- end
+ new_formatid = get_new_formatId(wb, format)
if new_formatid == 0
atts = ["numFmtId"]
- vals = ["$new_formatid"]
+ vals = [string(new_formatid)]
else
atts = ["numFmtId", "applyNumberFormat"]
- vals = ["$new_formatid", "1"]
+ vals = [string(new_formatid), "1"]
end
- newstyle = string(update_template_xf(sh, CellDataFormat(parse(Int, cell.style)), atts, vals).id)
+ newstyle = string(update_template_xf(sh, allXfNodes, CellDataFormat(parse(Int, cell.style)), atts, vals).id)
cell.style = newstyle
-
+
return new_formatid
end
@@ -1803,8 +1809,12 @@ end
setUniformFormat(sh::Worksheet, cr::String; kw...) -> ::Int
setUniformFormat(xf::XLSXFile, cr::String, kw...) -> ::Int
-Set the number format used by a cell range, a column range or a named range in a
-worksheet or XLSXfile to be to be uniformly the same format.
+ setUniformFormat(sh::Worksheet, rows, cols; kw...) -> ::Int
+
+Set the number format used by a cell range, a column range or row range or a
+named range in a worksheet or XLSXfile to be to be uniformly the same format.
+Alternatively, specify the rows and columns using any combination of
+Integer, UnitRange, Vector{Integer} or `:`.
First, the number format of the first cell in the range (the top-left cell) is
updated according to the given `kw...` (using `setFormat()`). The resultant format is
@@ -1815,6 +1825,9 @@ As a result, every cell in the range will have a uniform number format.
This is functionally equivalent to applying `setFormat()` to each cell in the range
but may be very marginally more efficient.
+Applying `setUniformFormat()` without any keyword arguments simply copies the `Format`
+attributes from the first cell specified to all the others.
+
The value returned is the `numfmtId` of the format uniformly applied to the cells.
If all cells in the range are `EmptyCells`, the returned value is -1.
@@ -1825,14 +1838,31 @@ For keyword definitions see [`setFormat()`](@ref).
julia> XLSX.setUniformFormat(xf, "Sheet1!A2:L6"; format = "# ??/??")
julia> XLSX.setUniformFormat(sh, "F1:F5"; format = "Currency")
+
+julia> setUniformFormat(sh, "B2,A5:D22") # Copy `Format` from B2 to cells in A5:D22
```
"""
function setUniformFormat end
+setUniformFormat(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setUniformFormat(ws, rng.rng; kw...)
+setUniformFormat(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setUniformFormat(ws, rng.colrng; kw...)
+setUniformFormat(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setUniformFormat(ws, rng.rowrng; kw...)
setUniformFormat(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setUniformFormat, ws, colrng; kw...)
+setUniformFormat(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(setUniformFormat, ws, rowrng; kw...)
+setUniformFormat(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_uniform_ncranges(setFormat, ws, ncrng, ["numFmtId", "applyNumberFormat"]; kw...)
setUniformFormat(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetcell(setUniformFormat, xl, sheetcell; kw...)
setUniformFormat(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setUniformFormat, ws, ref_or_rng; kw...)
-setUniformFormat(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attribute(setFormat, ws, rng; kw...)
+setUniformFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setFormat, ws, row, nothing; kw...)
+setUniformFormat(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setFormat, ws, nothing, col; kw...)
+setUniformFormat(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setUniformFormat, ws, nothing, nothing; kw...)
+setUniformFormat(ws::Worksheet, ::Colon; kw...) = process_colon(setUniformFormat, ws, nothing, nothing; kw...)
+setUniformFormat(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_uniform_veccolon(setFormat, ws, row, nothing, ["numFmtId", "applyNumberFormat"]; kw...)
+setUniformFormat(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_veccolon(setFormat, ws, nothing, col, ["numFmtId", "applyNumberFormat"]; kw...)
+setUniformFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_vecint(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...)
+setUniformFormat(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...)
+setUniformFormat(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_vecint(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...)
+setUniformFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setUniformFormat(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
+setUniformFormat(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attribute(setFormat, ws, rng, ["numFmtId", "applyNumberFormat"]; kw...)
#
# -- Set uniform styles
@@ -1842,9 +1872,13 @@ setUniformFormat(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_at
setUniformStyle(sh::Worksheet, cr::String) -> ::Int
setUniformStyle(xf::XLSXFile, cr::String) -> ::Int
-Set the cell `style` used by a cell range, a column range or a named range in a
-worksheet or XLSXfile to be the same as that of the first cell in the range
-that is not an `EmptyCell`.
+ setUniformStyle(sh::Worksheet, rows, cols) -> ::Int
+
+Set the cell `style` used by a cell range, a column range or row range
+or a named range in a worksheet or XLSXfile to be the same as that of
+the first cell in the range that is not an `EmptyCell`.
+Alternatively, specify the rows and columns using any combination of
+Integer, UnitRange, Vector{Integer} or `:`.
As a result, every cell in the range will have a uniform `style`.
@@ -1863,37 +1897,52 @@ If all cells in the range are `EmptyCells`, the returned value is -1.
julia> XLSX.setUniformStyle(xf, "Sheet1!A2:L6")
julia> XLSX.setUniformStyle(sh, "F1:F5")
+
+julia> XLSX.setUniformStyle(sh, 2:5, 5)
+
+julia> XLSX.setUniformStyle(sh, 2, :)
```
"""
function setUniformStyle end
+setUniformStyle(ws::Worksheet, rng::SheetCellRange) = do_sheet_names_match(ws, rng) && setUniformStyle(ws, rng.rng)
+setUniformStyle(ws::Worksheet, rng::SheetColumnRange) = do_sheet_names_match(ws, rng) && setUniformStyle(ws, rng.colrng)
+setUniformStyle(ws::Worksheet, rng::SheetRowRange) = do_sheet_names_match(ws, rng) && setUniformStyle(ws, rng.rowrng)
setUniformStyle(ws::Worksheet, colrng::ColumnRange)::Int = process_columnranges(setUniformStyle, ws, colrng)
+setUniformStyle(ws::Worksheet, rowrng::RowRange)::Int = process_rowranges(setUniformStyle, ws, rowrng)
+setUniformStyle(ws::Worksheet, ncrng::NonContiguousRange)::Int = process_uniform_ncranges(ws, ncrng)
setUniformStyle(xl::XLSXFile, sheetcell::AbstractString)::Int = process_sheetcell(setUniformStyle, xl, sheetcell)
setUniformStyle(ws::Worksheet, ref_or_rng::AbstractString)::Int = process_ranges(setUniformStyle, ws, ref_or_rng)
-function setUniformStyle(ws::Worksheet, rng::CellRange)::Union{Nothing, Int}
+setUniformStyle(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) = process_colon(ws, row, nothing)
+setUniformStyle(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}) = process_colon(ws, nothing, col)
+setUniformStyle(ws::Worksheet, ::Colon, ::Colon) = process_colon(ws, nothing, nothing)
+setUniformStyle(ws::Worksheet, ::Colon) = process_colon(ws, nothing, nothing)
+setUniformStyle(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon) = process_uniform_veccolon(ws, row, nothing)
+setUniformStyle(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}) = process_uniform_veccolon(ws, nothing, col)
+setUniformStyle(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}) = process_uniform_vecint(ws, row, col)
+setUniformStyle(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}) = process_uniform_vecint(ws, row, col)
+setUniformStyle(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}) = process_uniform_vecint(ws, row, col)
+setUniformStyle(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}) = setUniformStyle(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))))
+function setUniformStyle(ws::Worksheet, rng::CellRange)::Union{Nothing,Int}
+
+ if !get_xlsxfile(ws).use_cache_for_sheet_data
+ throw(XLSXError("Cannot set styles because cache is not enabled."))
+ end
- @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot set styles because cache is not enabled."
+ let newid::Union{Nothing,Int},
+ newid = nothing
- let newid::Union{Nothing, Int},
first = true
+
for cellref in rng
- cell = getcell(ws, cellref)
- if cell isa EmptyCell # Can't add a attribute to an empty cell.
- continue
- end
- if first # Get the style of the first cell in the range.
- newid = cell.style
- first = false
- else # Apply the same style to the rest of the cells in the range.
- cell.style = newid
- end
+ newid, first = process_uniform_core(ws, cellref, newid, first)
end
if first
newid = -1
end
- return isnothing(newID) ? nothing : newid
+ return isnothing(newid) ? nothing : newid
end
-end
+end
#
# -- Get and set column width
@@ -1903,11 +1952,16 @@ end
setColumnWidth(sh::Worksheet, cr::String; kw...) -> ::Int
setColumnWidth(xf::XLSXFile, cr::String, kw...) -> ::Int
+ setColumnWidth(sh::Worksheet, row, col; kw...) -> ::Int
+
Set the width of a column or column range.
A standard cell reference or cell range can be used to define the column range.
The function will use the columns and ignore the rows. Named cells and named
ranges can similarly be used.
+Alternatively, specify the row and column using any combination of
+Integer, UnitRange, Vector{Integer} or `:`, but only the columns will be used.
+
The function uses one keyword used to define a column width:
- `width::Real = nothing` : Defines width in Excel's own (internal) units
@@ -1925,7 +1979,8 @@ You can set a column width to 0.
The function returns a value of 0.
NOTE: Unlike the other `set` and `get` XLSX functions, working with `ColumnWidth` requires
-a file to be open for writing as well as reading (`mode="rw"` or open as a template)
+a file to be open for writing as well as reading (`mode="rw"` or open as a template) but
+it can work on empty cells.
# Examples:
```julia
@@ -1938,33 +1993,53 @@ julia> XLSX.setColumnWidth(sh, "I"; width = 24.37)
```
"""
function setColumnWidth end
+setColumnWidth(ws::Worksheet, ref::SheetCellRef; kw...) = do_sheet_names_match(ws, ref) && setColumnWidth(ws, ref.cellref; kw...)
+setColumnWidth(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setColumnWidth(ws, rng.rng; kw...)
+setColumnWidth(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setColumnWidth(ws, rng.colrng; kw...)
+setColumnWidth(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setColumnWidth(ws, rng.rowrng; kw...)
setColumnWidth(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setColumnWidth, ws, colrng; kw...)
+setColumnWidth(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(setColumnWidth, ws, rowrng; kw...)
+setColumnWidth(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setColumnWidth, ws, ncrng; kw...)
setColumnWidth(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setColumnWidth, ws, ref_or_rng; kw...)
setColumnWidth(xl::XLSXFile, sheetcell::String; kw...)::Int = process_sheetcell(setColumnWidth, xl, sheetcell; kw...)
setColumnWidth(ws::Worksheet, cr::CellRef; kw...)::Int = setColumnWidth(ws::Worksheet, CellRange(cr, cr); kw...)
+setColumnWidth(ws::Worksheet, row::Integer, col::Integer; kw...) = setColumnWidth(ws, CellRef(row, col); kw...)
+setColumnWidth(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setColumnWidth, ws, row, nothing; kw...)
+setColumnWidth(ws::Worksheet, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setColumnWidth, ws, nothing, col; kw...)
+setColumnWidth(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setColumnWidth, ws, nothing, col; kw...)
+setColumnWidth(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setColumnWidth, ws, nothing, nothing; kw...)
+setColumnWidth(ws::Worksheet, ::Colon; kw...) = process_colon(setColumnWidth, ws, nothing, nothing; kw...)
+setColumnWidth(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setColumnWidth, ws, row, nothing; kw...)
+setColumnWidth(ws::Worksheet, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_veccolon(setColumnWidth, ws, nothing, col; kw...)
+setColumnWidth(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_veccolon(setColumnWidth, ws, nothing, col; kw...)
+setColumnWidth(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_vecint(setColumnWidth, ws, row, col; kw...)
+setColumnWidth(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setColumnWidth, ws, row, col; kw...)
+setColumnWidth(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_vecint(setColumnWidth, ws, row, col; kw...)
+setColumnWidth(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setColumnWidth(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
function setColumnWidth(ws::Worksheet, rng::CellRange; width::Union{Nothing,Real}=nothing)::Int
-
- @assert get_xlsxfile(ws).is_writable "Cannot set column widths: `XLSXFile` is not writable."
- # Because we are working on worksheet data directly, we need to update the xml file using the worksheet cache first.
- update_worksheets_xml!(get_xlsxfile(ws))
+ if !get_xlsxfile(ws).is_writable
+ throw(XLSXError("Cannot set column widths: `XLSXFile` is not writable."))
+ end
- left = rng.start.column_number
+ left = rng.start.column_number
right = rng.stop.column_number
padded_width = isnothing(width) ? -1 : width + 0.7109375 # Excel adds cell padding to a user specified width
- @assert isnothing(width) || width >= 0 "Invalid value specified for width: $width. Width must be >= 0."
+ if !isnothing(width) && width < 0
+ throw(XLSXError("Invalid value specified for width: $width. Width must be >= 0."))
+ end
if isnothing(width) # No-op
return 0
end
- sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet$(ws.sheetId).xml") # find the block in the worksheet's xml file
+ sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet" * string(ws.sheetId) * ".xml") # find the block in the worksheet's xml file
i, j = get_idces(sheetdoc, "worksheet", "cols")
if isnothing(j) # There are no existing column formats. Insert before the block and push everything else down one.
k, l = get_idces(sheetdoc, "worksheet", "sheetData")
len = length(sheetdoc[k])
- @assert i==k "Some problem here!"
+ i != k && throw(XLSXError("Some problem here!"))
push!(sheetdoc[k], sheetdoc[k][end])
if l < len
for pos = len-1:-1:l
@@ -1972,7 +2047,7 @@ function setColumnWidth(ws::Worksheet, rng::CellRange; width::Union{Nothing,Real
end
end
sheetdoc[k][l] = XML.Element("Cols")
- j=l
+ j = l
end
child_list = Dict{String,Union{Dict{String,String},Nothing}}()
@@ -1988,13 +2063,13 @@ function setColumnWidth(ws::Worksheet, rng::CellRange; width::Union{Nothing,Real
end
else
if padded_width >= 0 # Add new definitions where there is not one extant
- scol=string(col)
+ scol = string(col)
push!(child_list, scol => Dict("max" => scol, "min" => scol, "width" => string(padded_width), "customWidth" => "1"))
end
end
end
- new_cols = unlink_cols(sheetdoc[i][j]) # Create the new Node
+ new_cols = unlink(sheetdoc[i][j], ("cols", "col")) # Create the new Node
for atts in values(child_list)
new_col = XML.Element("col")
for (k, v) in atts
@@ -2005,6 +2080,9 @@ function setColumnWidth(ws::Worksheet, rng::CellRange; width::Union{Nothing,Real
sheetdoc[i][j] = new_cols # Update the worksheet with the new cols.
+ # Because we are working on worksheet data directly, we need to update the xml file using the worksheet cache.
+ update_worksheets_xml!(get_xlsxfile(ws))
+
return 0 # meaningless return value. Int required to comply with reference decoding structure.
end
@@ -2012,7 +2090,10 @@ end
getColumnWidth(sh::Worksheet, cr::String) -> ::Union{Nothing, Real}
getColumnWidth(xf::XLSXFile, cr::String) -> ::Union{Nothing, Real}
+ getColumnWidth(sh::Worksheet, row::Int, col::Int) -> ::Union{Nothing, Real}
+
Get the width of a column defined by a cell reference or named cell.
+The specified cell must be within the sheet dimension.
A standard cell reference or defined name may be used to define the column.
The function will use the column number and ignore the row.
@@ -2025,23 +2106,28 @@ does not have an explicitly defined width.
julia> XLSX.getColumnWidth(xf, "Sheet1!A2")
julia> XLSX.getColumnWidth(sh, "F1")
+
+julia> XLSX.getColumnWidth(sh, 1, 6)
```
"""
function getColumnWidth end
getColumnWidth(xl::XLSXFile, sheetcell::String)::Union{Nothing,Float64} = process_get_sheetcell(getColumnWidth, xl, sheetcell)
getColumnWidth(ws::Worksheet, cr::String) = process_get_cellname(getColumnWidth, ws, cr)
+getColumnWidth(ws::Worksheet, row::Integer, col::Integer) = getColumnWidth(ws, CellRef(row, col))
function getColumnWidth(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real}
+ # May be better if column width were part of ws.cache?
- @assert get_xlsxfile(ws).is_writable "Cannot get column width: `XLSXFile` is not writable."
+ if !get_xlsxfile(ws).is_writable
+ throw(XLSXError("Cannot get column width: `XLSXFile` is not writable."))
+ end
d = get_dimension(ws)
- @assert cellref.row_number >= d.start.row_number && cellref.row_number <= d.stop.row_number "Cell specified is outside sheet dimension \"$d\""
-
- # Because we are working on worksheet data directly, we need to update the xml file using the worksheet cache first.
- update_worksheets_xml!(get_xlsxfile(ws))
+ if cellref ∉ d
+ throw(XLSXError("Cell specified is outside sheet dimension `$d`"))
+ end
- sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet$(ws.sheetId).xml") # find the block in the worksheet's xml file
+ sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet" * string(ws.sheetId) * ".xml") # find the block in the worksheet's xml file
i, j = get_idces(sheetdoc, "worksheet", "cols")
if isnothing(j) # There are no existing column formats defined.
@@ -2069,11 +2155,15 @@ end
setRowHeight(sh::Worksheet, cr::String; kw...) -> ::Int
setRowHeight(xf::XLSXFile, cr::String, kw...) -> ::Int
+ setRowHeight(sh::Worksheet, row, col; kw...) -> ::Int
+
Set the height of a row or row range.
A standard cell reference or cell range must be used to define the row range.
The function will use the rows and ignore the columns. Named cells and named
ranges can similarly be used.
+Alternatively, specify the row and column using any combination of
+Integer, UnitRange, Vector{Integer} or `:`, but only the rows will be used.
The function uses one keyword used to define a row height:
- `height::Real = nothing` : Defines height in Excel's own (internal) units.
@@ -2098,50 +2188,78 @@ it returns a value of -1.
```julia
julia> XLSX.setRowHeight(xf, "Sheet1!A2"; height = 50)
-julia> XLSX.setRowHeight(sh, "F1:F5"; heighth = 0)
+julia> XLSX.setRowHeight(sh, "F1:F5"; height = 0)
julia> XLSX.setRowHeight(sh, "I"; height = 24.56)
```
"""
function setRowHeight end
+setRowHeight(ws::Worksheet, ref::SheetCellRef; kw...) = do_sheet_names_match(ws, ref) && setRowHeight(ws, ref.cellref; kw...)
+setRowHeight(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setRowHeight(ws, rng.rng; kw...)
+setRowHeight(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setRowHeight(ws, rng.colrng; kw...)
+setRowHeight(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setRowHeight(ws, rng.rowrng; kw...)
setRowHeight(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setRowHeight, ws, colrng; kw...)
+setRowHeight(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(setRowHeight, ws, rowrng; kw...)
+setRowHeight(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setRowHeight, ws, ncrng; kw...)
setRowHeight(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setRowHeight, ws, ref_or_rng; kw...)
setRowHeight(xl::XLSXFile, sheetcell::String; kw...)::Int = process_sheetcell(setRowHeight, xl, sheetcell; kw...)
setRowHeight(ws::Worksheet, cr::CellRef; kw...)::Int = setRowHeight(ws::Worksheet, CellRange(cr, cr); kw...)
+setRowHeight(ws::Worksheet, row::Integer, col::Integer; kw...) = setRowHeight(ws, CellRef(row, col); kw...)
+setRowHeight(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setRowHeight, ws, row, nothing; kw...)
+setRowHeight(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setRowHeight, ws, row, nothing; kw...)
+setRowHeight(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setRowHeight, ws, nothing, col; kw...)
+setRowHeight(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setRowHeight, ws, nothing, nothing; kw...)
+setRowHeight(ws::Worksheet, ::Colon; kw...) = process_colon(setRowHeight, ws, nothing, nothing; kw...)
+setRowHeight(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_veccolon(setRowHeight, ws, row, nothing; kw...)
+setRowHeight(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setRowHeight, ws, row, nothing; kw...)
+setRowHeight(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_veccolon(setRowHeight, ws, nothing, col; kw...)
+setRowHeight(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_vecint(setRowHeight, ws, row, col; kw...)
+setRowHeight(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setRowHeight, ws, row, col; kw...)
+setRowHeight(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_vecint(setRowHeight, ws, row, col; kw...)
+setRowHeight(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setRowHeight(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
function setRowHeight(ws::Worksheet, rng::CellRange; height::Union{Nothing,Real}=nothing)::Int
- @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot set row heights because cache is not enabled."
+ if !get_xlsxfile(ws).use_cache_for_sheet_data
+ throw(XLSXError("Cannot set row heights because cache is not enabled."))
+ end
- top = rng.start.row_number
+ top = rng.start.row_number
bottom = rng.stop.row_number
padded_height = isnothing(height) ? -1 : height + 0.2109375 # Excel adds cell padding to a user specified width
- @assert isnothing(height) || height >= 0 "Invalid value specified for height: $height. Height must be >= 0."
+ if !isnothing(height) && height < 0
+ throw(XLSXError("Invalid value specified for height: $height. Height must be >= 0."))
+ end
if isnothing(height) # No-op
return 0
end
+
first = true
- for r in eachrow(ws)
- if r.row in top:bottom
- if haskey(ws.cache.row_ht, r.row)
- ws.cache.row_ht[r.row] = padded_height
- first = false
- end
+
+ for r in top:bottom
+ if haskey(ws.cache.row_ht, r)
+ ws.cache.row_ht[r] = padded_height
+ first=false
end
end
- if first == true
- return -1 # All rows were empty
+ if first == true
+ return -1
end
- return 0 # meaningless return value. Int required to comply with reference decoding structure.
+
+ return 0
+
end
"""
getRowHeight(sh::Worksheet, cr::String) -> ::Union{Nothing, Real}
getRowHeight(xf::XLSXFile, cr::String) -> ::Union{Nothing, Real}
+ getRowHeight(sh::Worksheet, row::Int, col::Int) -> ::Union{Nothing, Real}
+
Get the height of a row defined by a cell reference or named cell.
+The specified cell must be within the sheet dimension.
A standard cell reference or defined name must be used to define the row.
The function will use the row number and ignore the column.
@@ -2156,27 +2274,350 @@ If the row is not found (an empty row), returns -1.
julia> XLSX.getRowHeight(xf, "Sheet1!A2")
julia> XLSX.getRowHeight(sh, "F1")
+
+julia> XLSX.getRowHeight(sh, 1, 6)
```
"""
function getRowHeight end
getRowHeight(xl::XLSXFile, sheetcell::String)::Union{Nothing,Real} = process_get_sheetcell(getRowHeight, xl, sheetcell)
getRowHeight(ws::Worksheet, cr::String) = process_get_cellname(getRowHeight, ws, cr)
+getRowHeight(ws::Worksheet, row::Integer, col::Integer) = getRowHeight(ws, CellRef(row, col))
function getRowHeight(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real}
- @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot get row height because cache is not enabled."
+ if !get_xlsxfile(ws).use_cache_for_sheet_data
+ throw(XLSXError("Cannot get row height because cache is not enabled."))
+ end
d = get_dimension(ws)
- @assert cellref.row_number >= d.start.row_number && cellref.row_number <= d.stop.row_number "Cell specified is outside sheet dimension \"$d\""
+ if cellref ∉ d
+ throw(XLSXError("Cell specified is outside sheet dimension `$d`"))
+ end
+
+ if haskey(ws.cache.row_ht, cellref.row_number)
+ return ws.cache.row_ht[cellref.row_number]
+ end
+
+ return -1 # Row specified not found (is empty)
- for r in eachrow(ws)
- if r.row == cellref.row_number
- if haskey(ws.cache.row_ht, r.row)
- return ws.cache.row_ht[r.row]
+end
+
+#
+# -- Get merged cells
+#
+
+"""
+ getMergedCells(ws::Worksheet) -> Union{Vector{CellRange}, Nothing}
+
+Return a vector of the `CellRange` of all merged cells in the specified worksheet.
+Return nothing if the worksheet contains no merged cells.
+
+The Excel file must be opened in write mode to work with merged cells.
+
+# Examples:
+```julia
+julia> f = XLSX.readxlsx("test.xlsx")
+XLSXFile("C:\\Users\\tim\\Downloads\\test.xlsx") containing 1 Worksheet
+ sheetname size range
+-------------------------------------------------
+ Sheet1 2x2 A1:B2
+
+julia> s = f["Sheet1"]
+2×2 XLSX.Worksheet: ["Sheet1"](A1:B2)
+
+julia> XLSX.getMergedCells(s)
+1-element Vector{XLSX.CellRange}:
+ B1:B2
+
+```
+"""
+function getMergedCells(ws::Worksheet)::Union{Vector{CellRange},Nothing}
+ # May be better if merged cells were part of ws.cache?
+
+ if !get_xlsxfile(ws).is_writable
+ throw(XLSXError("Cannot get merged cells: `XLSXFile` is not writable."))
+ end
+
+ if !get_xlsxfile(ws).use_cache_for_sheet_data
+ throw(XLSXError("Cannot get merged cells because cache is not enabled."))
+ end
+
+ sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet" * string(ws.sheetId) * ".xml") # find the block in the worksheet's xml file
+ i, j = get_idces(sheetdoc, "worksheet", "mergeCells")
+
+ if isnothing(j) # There are no existing merged cells.
+ return nothing
+ end
+
+ c = XML.children(sheetdoc[i][j])
+ if length(c) != parse(Int, sheetdoc[i][j]["count"])
+ throw(XLSXError("Unexpected number of mergeCells found: $(length(c)). Expected $(sheetdoc[i][j]["count"])."))
+ end
+
+ mergedCells = Vector{CellRange}()
+ for cell in c
+ !haskey(cell, "ref") && throw(XLSXError("No `ref` attribute found in `mergeCell` element."))
+ push!(mergedCells, CellRange(cell["ref"]))
+ end
+
+ return mergedCells
+end
+
+"""
+ isMergedCell(ws::Worksheet, cr::String) -> Bool
+ isMergedCell(xf::XLSXFile, cr::String) -> Bool
+
+ isMergedCell(ws::Worksheet, row::Int, col::Int) -> Bool
+
+Return `true` if a cell is part of a merged cell range and `false` if not.
+The specified cell must be within the sheet dimension.
+
+Alternatively, if you have already obtained the merged cells for the worksheet,
+you can avoid repeated determinations and pass them as a keyword argument to
+the function:
+
+ isMergedCell(ws::Worksheet, cr::String; mergedCells::Union{Vector{CellRange}, Nothing, Missing}=missing) -> Bool
+ isMergedCell(xf::XLSXFile, cr::String; mergedCells::Union{Vector{CellRange}, Nothing, Missing}=missing) -> Bool
+
+ isMergedCell(ws::Worksheet, row:Int, col::Int; mergedCells::Union{Vector{CellRange}, Nothing, Missing}=missing) -> Bool
+
+The Excel file must be opened in write mode to work with merged cells.
+
+# Examples:
+```julia
+julia> XLSX.isMergedCell(xf, "Sheet1!A1")
+
+julia> XLSX.isMergedCell(sh, "A1")
+
+julia> XLSX.isMergedCell(sh, 2, 4) # cell D2
+
+julia> mc = XLSX.getMergedCells(sh)
+
+julia> XLSX.isMergedCell(sh, XLSX.CellRef("A1"), mc)
+
+```
+"""
+function isMergedCell end
+isMergedCell(xl::XLSXFile, sheetcell::String; kw...)::Bool = process_get_sheetcell(isMergedCell, xl, sheetcell; kw...)
+isMergedCell(ws::Worksheet, cr::String; kw...)::Bool = process_get_cellname(isMergedCell, ws, cr; kw...)
+isMergedCell(ws::Worksheet, row::Integer, col::Integer; kw...) = isMergedCell(ws, CellRef(row, col); kw...)
+function isMergedCell(ws::Worksheet, cellref::CellRef; mergedCells::Union{Vector{CellRange},Nothing,Missing}=missing)::Bool
+
+ if !get_xlsxfile(ws).is_writable
+ throw(XLSXError("Cannot get merged cells: `XLSXFile` is not writable."))
+ end
+
+ if !get_xlsxfile(ws).use_cache_for_sheet_data
+ throw(XLSXError("Cannot get merged cells because cache is not enabled."))
+ end
+
+ d = get_dimension(ws)
+ if cellref ∉ d
+ throw(XLSXError("Cell specified is outside sheet dimension `$d`"))
+ end
+
+ if ismissing(mergedCells) # Get mergedCells if missing
+ mergedCells = getMergedCells(ws)
+ end
+ if isnothing(mergedCells) # No merged cells in sheet
+ return false
+ end
+ for rng in mergedCells
+ if cellref ∈ rng
+ return true
+ end
+ end
+
+ return false
+end
+
+"""
+ getMergedBaseCell(ws::Worksheet, cr::String) -> Union{Nothing, NamedTuple{CellRef, Any}}
+ getMergedBaseCell(xf::XLSXFile, cr::String) -> Union{Nothing, NamedTuple{CellRef, Any}}
+
+ getMergedBaseCell(ws::Worksheet, row::Int, col::Int) -> Union{Nothing, NamedTuple{CellRef, Any}}
+
+Return the cell reference and cell value of the base cell of a merged cell range in a worksheet as a named tuple.
+The specified cell must be within the sheet dimension.
+If the specified cell is not part of a merged cell range, return `nothing`.
+
+The base cell is the top-left cell of the merged cell range and is the reference cell for the range.
+
+The tuple returned contains:
+- `baseCell` : the reference (`CellRef`) of the base cell
+- `baseValue` : the value of the base cell
+
+Additionally, if you have already obtained the merged cells for the worksheet,
+you can avoid repeated determinations and pass them as a keyword argument to
+the function:
+
+ getMergedBaseCell(ws::Worksheet, cr::String; mergedCells::Union{Vector{CellRange}, Nothing, Missing}=missing) -> Union{Nothing, NamedTuple{CellRef, Any}}
+ getMergedBaseCell(xf::XLSXFile, cr::String; mergedCells::Union{Vector{CellRange}, Nothing, Missing}=missing) -> Union{Nothing, NamedTuple{CellRef, Any}}
+
+ getMergedBaseCell(ws::Worksheet, row::Int, col::Int; mergedCells::Union{Vector{CellRange}, Nothing, Missing}=missing) -> Union{Nothing, NamedTuple{CellRef, Any}}
+
+The Excel file must be opened in write mode to work with merged cells.
+
+# Examples:
+```julia
+julia> XLSX.getMergedBaseCell(xf, "Sheet1!B2")
+(baseCell = B1, baseValue = 3)
+
+julia> XLSX.getMergedBaseCell(sh, "B2")
+(baseCell = B1, baseValue = 3)
+
+julia> XLSX.getMergedBaseCell(sh, 2, 2)
+(baseCell = B1, baseValue = 3)
+
+```
+"""
+function getMergedBaseCell end
+getMergedBaseCell(xl::XLSXFile, sheetcell::String; kw...) = process_get_sheetcell(getMergedBaseCell, xl, sheetcell; kw...)
+getMergedBaseCell(ws::Worksheet, cr::String; kw...) = process_get_cellname(getMergedBaseCell, ws, cr; kw...)
+getMergedBaseCell(ws::Worksheet, row::Integer, col::Integer; kw...) = getMergedBaseCell(ws, CellRef(row, col); kw...)
+function getMergedBaseCell(ws::Worksheet, cellref::CellRef; mergedCells::Union{Vector{CellRange},Nothing,Missing}=missing)
+
+ if !get_xlsxfile(ws).is_writable
+ throw(XLSXError("Cannot get merged cells: `XLSXFile` is not writable."))
+ end
+
+ if !get_xlsxfile(ws).use_cache_for_sheet_data
+ throw(XLSXError("Cannot get merged cells because cache is not enabled."))
+ end
+
+ d = get_dimension(ws)
+ if cellref ∉ d
+ throw(XLSXError("Cell specified is outside sheet dimension `$d`"))
+ end
+
+ if ismissing(mergedCells) # Get mergedCells if missing
+ mergedCells = getMergedCells(ws)
+ end
+ if isnothing(mergedCells) # No merged cells in sheet
+ return nothing
+ end
+ for rng in mergedCells
+ if cellref ∈ rng
+ return (; baseCell=rng.start, baseValue=ws[rng.start])
+ end
+ end
+ return nothing
+end
+
+"""
+ mergeCells(ws::Worksheet, cr::String) -> 0
+ mergeCells(xf::XLSXFile, cr::String) -> 0
+
+ mergeCells(ws::Worksheet, row::Int, col::Int) -> 0
+
+Merge the cells in the range given by `cr`. The value of the merged cell
+will be the value of the first cell in the range (the base cell) prior
+to the merge. All other cells in the range will be set to `missing`,
+reflecting the behaviour of Excel itself.
+
+Merging is limited to the extent of the worksheet dimension.
+
+The specified range must not overlap with any previously merged cells.
+
+It is not possible to merge a single cell!
+
+A non-contiguous range composed of multiple cell ranges will be processed as a
+list of separate ranges. Each range will be merged separately. No range within
+a non-contiguous range may be a single cell.
+
+The Excel file must be opened in write mode to work with merged cells.
+
+# Examples:
+```julia
+julia> XLSX.mergeCells(xf, "Sheet1!B2:D3") # Merge a cell range.
+
+julia> XLSX.mergeCells(sh, 1:3, :) # Merge rows to the extent of the dimension.
+
+julia> XLSX.mergeCells(sh, "A:D") # Merge columns to the extent of the dimension.
+
+```
+"""
+function mergeCells end
+mergeCells(ws::Worksheet, rng::SheetCellRange) = do_sheet_names_match(ws, rng) && mergeCells(ws, rng.rng)
+mergeCells(ws::Worksheet, rng::SheetColumnRange) = do_sheet_names_match(ws, rng) && mergeCells(ws, rng.colrng)
+mergeCells(ws::Worksheet, rng::SheetRowRange) = do_sheet_names_match(ws, rng) && mergeCells(ws, rng.rowrng)
+mergeCells(ws::Worksheet, colrng::ColumnRange)::Int = process_columnranges(mergeCells, ws, colrng)
+mergeCells(ws::Worksheet, rowrng::RowRange)::Int = process_rowranges(mergeCells, ws, rowrng)
+mergeCells(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(mergeCells, ws, ncrng; kw...)
+mergeCells(xl::XLSXFile, sheetcell::AbstractString)::Int = process_sheetcell(mergeCells, xl, sheetcell)
+mergeCells(ws::Worksheet, ref_or_rng::AbstractString)::Int = process_ranges(mergeCells, ws, ref_or_rng)
+mergeCells(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) = process_colon(mergeCells, ws, row, nothing)
+mergeCells(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}) = process_colon(mergeCells, ws, nothing, col)
+mergeCells(ws::Worksheet, ::Colon, ::Colon) = process_colon(mergeCells, ws, nothing, nothing)
+mergeCells(ws::Worksheet, ::Colon) = process_colon(mergeCells, ws, nothing, nothing)
+mergeCells(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}) = mergeCells(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))))
+function mergeCells(ws::Worksheet, cr::CellRange)
+ # May be better if merged cells were part of ws.cache?
+
+ # !is_valid_cell_range(cr) && throw(XLSXError("\"$cr\" is not a valid cell range."))
+
+ !issubset(cr, get_dimension(ws)) && throw(XLSXError("Range `$cr` goes outside worksheet dimension."))
+
+ length(cr) == 1 && throw(XLSXError("Cannot merge a single cell: `$cr`"))
+
+ if !get_xlsxfile(ws).is_writable
+ throw(XLSXError("Cannot merge cells: `XLSXFile` is not writable."))
+ end
+
+ if !get_xlsxfile(ws).use_cache_for_sheet_data
+ throw(XLSXError("Cannot get merged cells because cache is not enabled."))
+ end
+
+ sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet" * string(ws.sheetId) * ".xml") # find the block in the worksheet's xml file
+ i, j = get_idces(sheetdoc, "worksheet", "mergeCells")
+
+ if isnothing(j) # There are no existing merged cells. Insert immediately after the block and push everything else down one.
+ k, l = get_idces(sheetdoc, "worksheet", "sheetData")
+ len = length(sheetdoc[k])
+ i != k && throw(XLSXError("Some problem here!"))
+ if l != len
+ push!(sheetdoc[k], sheetdoc[k][end])
+ if l + 1 < len
+ for pos = len-1:-1:l+1
+ sheetdoc[k][pos+1] = sheetdoc[k][pos]
+ end
+ end
+ sheetdoc[k][l+1] = XML.Element("mergeCells")
+ else
+ push!(sheetdoc[k], XML.Element("mergeCells"))
+ end
+ j = l + 1
+ count = 0
+ else # There are already some existing merged cells
+ c = XML.children(sheetdoc[i][j])
+ count = length(c)
+ if count != parse(Int, sheetdoc[i][j]["count"])
+ throw(XLSXError("Unexpected number of mergeCells found: $(length(c)). Expected $(sheetdoc[i][j]["count"])."))
+ end
+ for child in c
+ if intersects(cr, CellRange(child["ref"]))
+ throw(XLSXError("Merged range (`$cr`) cannot overlap with existing merged range (`" * child["ref"] * "`)."))
end
end
end
- return -1 # Row specified not found (is empty)
+ push!(sheetdoc[i][j], XML.Element("mergeCell", ref=string(cr))) # Add the new merged cell range.
+ count += 1
+ sheetdoc[i][j]["count"] = count
+ # All cells except the base cell are set to missing.
+ let first = true
+ for cell in cr
+ if first
+ first = false
+ continue
+ else
+ ws[cell] = ""
+ end
+ end
+ end
+
+ update_worksheets_xml!(get_xlsxfile(ws))
+
+ return 0 # meaningless return value. Int required to comply with reference decoding structure.
end
diff --git a/src/cellref.jl b/src/cellref.jl
index 07cac399..de632249 100644
--- a/src/cellref.jl
+++ b/src/cellref.jl
@@ -1,6 +1,6 @@
function CellRef(n::AbstractString)
- @assert is_valid_cellname(n) "$n is not a valid CellRef."
+ !is_valid_cellname(n) && throw(XLSXError("$n is not a valid CellRef."))
column_name, row_number = split_cellname(n)
return CellRef(n, row_number, decode_column_number(column_name))
end
@@ -15,7 +15,7 @@ end
function decode_column_number(column_name::AbstractString) :: Int
local result::Int = 0
- @assert isascii(column_name) "$column_name is not a valid column name."
+ !isascii(column_name) && throw(XLSXError("$column_name is not a valid column name."))
num_characters = length(column_name) # this is safe, since `column_name` is encoded as ASCII
iteration = 1
@@ -30,7 +30,9 @@ end
# Converts column number to a column name. See also XLSX.decode_column_number.
function encode_column_number(column_number::Int) :: String
- @assert column_number > 0 && column_number <= EXCEL_MAX_COLS "Column number should be in the range from 1 to $EXCEL_MAX_COLS."
+ if column_number <= 0 && column_number > EXCEL_MAX_COLS
+ throw(XLSXError("Column number should be in the range from 1 to $EXCEL_MAX_COLS."))
+ end
third_letter_sequence = div(column_number - 26 - 1, 26*26)
column_number = column_number - third_letter_sequence*(26*26)
@@ -58,8 +60,10 @@ Base.string(c::CellRef) = c.name
Base.show(io::IO, c::CellRef) = print(io, string(c))
Base.:(==)(c1::CellRef, c2::CellRef) = c1.name == c2.name
Base.hash(c::CellRef) = hash(c.name)
+Base.isless(c1::CellRef, c2::CellRef) = Base.isless(string(c1), string(c2))
const RGX_COLUMN_NAME = r"^[A-Z]?[A-Z]?[A-Z]$"
+const RGX_ROW_NAME = r"^[1-9][0-9]*$"
const RGX_CELLNAME = r"^[A-Z]+[0-9]+$"
const RGX_CELLRANGE = r"^[A-Z]+[0-9]+:[A-Z]+[0-9]+$"
@@ -75,28 +79,43 @@ function is_valid_column_name(n::AbstractString) :: Bool
return true
end
+function is_valid_row_name(n::AbstractString) :: Bool
+ if !occursin(RGX_ROW_NAME, n)
+ return false
+ end
+
+ row_number = parse(Int, n)
+ if row_number < 1 || row_number > EXCEL_MAX_ROWS
+ return false
+ end
+
+ return true
+end
const RGX_CELLNAME_LEFT = r"^[A-Z]+"
const RGX_CELLNAME_RIGHT = r"[0-9]+$"
# Splits a string representing a cell name to its column name and row number.
@inline function split_cellname(n::AbstractString)
- @assert isascii(n) "$n is not a valid cell name."
+ !isascii(n) && throw(XLSXError("$n is not a valid cell name."))
for (i, c) in enumerate(n)
if isdigit(c) # this block is safe since n is encoded as ASCII
column_name = SubString(n, 1, i-1)
row = parse(Int, SubString(n, i, length(n)))
-
return column_name, row
end
end
- error("Couldn't split (column_name, row) for cellname $n.")
+ throw(XLSXError("Couldn't split (column_name, row) for cellname $n."))
end
# Checks whether `n` is a valid name for a cell.
function is_valid_cellname(n::AbstractString) :: Bool
+ if is_valid_non_contiguous_range(n) # Non-contiguous ranges are comma separated `CellRef-like` or `CellRange-like` strings
+ return false
+ end
+
if !occursin(RGX_CELLNAME, n)
return false
end
@@ -131,11 +150,14 @@ julia> XLSX.split_cellrange("AB12:CD24")
=#
@inline function split_cellrange(n::AbstractString)
s = split(n, ":")
- @assert length(s) == 2 "$n is not a valid cell range."
+ length(s) != 2 && throw(XLSXError("$n is not a valid cell range."))
return s[1], s[2]
end
function is_valid_cellrange(n::AbstractString) :: Bool
+ if is_valid_non_contiguous_range(n) # Non-contiguous ranges are comma separated `CellRef-like` or `CellRange-like` strings
+ return false
+ end
if !occursin(RGX_CELLRANGE, n)
return false
@@ -159,17 +181,18 @@ macro ref_str(ref)
end
function CellRange(r::AbstractString)
- @assert occursin(RGX_CELLRANGE, r) "Invalid cell range: $r."
+ !occursin(RGX_CELLRANGE, r) && throw(XLSXError("Invalid cell range: $r."))
start_name, stop_name = split_cellrange(r)
return CellRange(CellRef(start_name), CellRef(stop_name))
end
CellRange(start_row::Integer, start_column::Integer, stop_row::Integer, stop_column::Integer) = CellRange(CellRef(start_row, start_column), CellRef(stop_row, stop_column))
-Base.string(cr::CellRange) = "$(string(cr.start)):$(string(cr.stop))"
+Base.string(cr::CellRange) = string(cr.start)*":"*string(cr.stop)
Base.show(io::IO, cr::CellRange) = print(io, string(cr))
Base.:(==)(cr1::CellRange, cr2::CellRange) = cr1.start == cr2.start && cr2.stop == cr2.stop
Base.hash(cr::CellRange) = hash(cr.start) + hash(cr.stop)
+Base.isless(cr1::CellRange, cr2::CellRange) = Base.isless(string(cr1), string(cr2)) # needed for tests
macro range_str(cellrange)
CellRange(cellrange)
@@ -196,6 +219,43 @@ end
# Checks whether `subrng` is a cell range contained in `rng`.
Base.issubset(subrng::CellRange, rng::CellRange) :: Bool = in(subrng.start, rng) && in(subrng.stop, rng)
+
+function intersects(rng1::CellRange, rng2::CellRange) :: Bool
+ if row_number(rng1.start) <= row_number(rng2.stop) && row_number(rng1.start) >= row_number(rng2.start) &&
+ column_number(rng1.stop) <= column_number(rng2.stop) && column_number(rng1.stop) >= column_number(rng2.start)
+# println("offset 1")
+ return true
+ end
+ if row_number(rng2.start) <= row_number(rng1.stop) && row_number(rng2.start) >= row_number(rng1.start) &&
+ column_number(rng2.stop) <= column_number(rng1.stop) && column_number(rng2.stop) >= column_number(rng1.start)
+# println("offset 2")
+ return true
+ end
+ if row_number(rng1.start)>=row_number(rng2.start) && column_number(rng1.start)<=column_number(rng2.start) &&
+ row_number(rng1.stop)<=row_number(rng2.stop) && column_number(rng1.stop)>=column_number(rng2.stop)
+# println("cruciform 1")
+ return true
+ end
+ if row_number(rng2.start)>=row_number(rng1.start) && column_number(rng2.start)<=column_number(rng1.start) &&
+ row_number(rng2.stop)<=row_number(rng1.stop) && column_number(rng2.stop)>=column_number(rng1.stop)
+# println("cruciform 2")
+ return true
+ end
+ if in(rng1.start, rng2) || in(rng1.stop, rng2)
+# println("inside corner 1")
+ return true
+ end
+ if in(rng2.start, rng1) || in(rng2.stop, rng1)
+# println("inside corner 2")
+ return true
+ end
+ if issubset(rng1, rng2) || issubset(rng2, rng1)
+# println("subset")
+ return true
+ end
+ return false
+end
+
function Base.size(rng::CellRange)
top = row_number(rng.start)
@@ -235,8 +295,9 @@ For example, for a range "B2:D4", we have:
* "D4" relative position is (3, 3)
=#
+
function relative_cell_position(ref::CellRef, rng::CellRange)
- @assert ref ∈ rng "$ref is outside range $rng."
+ ref ∉ rng && throw(XLSXError("$ref is outside range $rng."))
top = row_number(rng.start)
left = column_number(rng.start)
@@ -247,17 +308,23 @@ function relative_cell_position(ref::CellRef, rng::CellRange)
end
#
-# ColumnRange
+# ColumnRange and RowRange
#
-Base.string(cr::ColumnRange) = "$(encode_column_number(cr.start)):$(encode_column_number(cr.stop))"
+Base.string(cr::ColumnRange) = encode_column_number(cr.start)*":"*encode_column_number(cr.stop)
Base.show(io::IO, cr::ColumnRange) = print(io, string(cr))
Base.:(==)(cr1::ColumnRange, cr2::ColumnRange) = cr1.start == cr2.start && cr2.stop == cr2.stop
Base.hash(cr::ColumnRange) = hash(cr.start) + hash(cr.stop)
Base.in(column_number::Integer, rng::ColumnRange) = rng.start <= column_number && column_number <= rng.stop
+Base.string(cr::RowRange) = string(cr.start)*":"*string(cr.stop)
+Base.show(io::IO, cr::RowRange) = print(io, string(cr))
+Base.:(==)(cr1::RowRange, cr2::RowRange) = cr1.start == cr2.start && cr2.stop == cr2.stop
+Base.hash(cr::RowRange) = hash(cr.start) + hash(cr.stop)
+Base.in(row_number::Integer, rng::RowRange) = rng.start <= row_number && row_number <= rng.stop
+
function relative_column_position(column_number::Integer, rng::ColumnRange)
- @assert column_number ∈ rng "Column $column_number is outside range $rng."
+ column_number ∉ rng && throw(XLSXError("Column $column_number is outside range $rng."))
return column_number - rng.start + 1
end
@@ -267,9 +334,14 @@ const RGX_COLUMN_RANGE = r"^[A-Z]?[A-Z]?[A-Z]:[A-Z]?[A-Z]?[A-Z]$"
const RGX_COLUMN_RANGE_START = r"^[A-Z]+"
const RGX_COLUMN_RANGE_STOP = r"[A-Z]+$"
const RGX_SINGLE_COLUMN = r"^[A-Z]+$"
+const RGX_ROW_RANGE = r"^[1-9][0-9]*:[1-9][0-9]*$"
+const RGX_ROW_RANGE_START = r"^[1-9][0-9]*"
+const RGX_ROW_RANGE_STOP = r"[1-9][0-9]*$"
+const RGX_SINGLE_ROW = r"^[1-9][0-9]*$"
# Returns tuple (column_name_start, column_name_stop).
-@inline function split_column_range(n::AbstractString)
+# Also works for row ranges (row_name_start, row_name_stop)!
+@inline function split_sheet_range(n::AbstractString)
if !occursin(":", n)
return n, n
else
@@ -279,35 +351,56 @@ const RGX_SINGLE_COLUMN = r"^[A-Z]+$"
end
function is_valid_column_range(r::AbstractString) :: Bool
-
if occursin(RGX_SINGLE_COLUMN, r)
return true
end
-
if !occursin(RGX_COLUMN_RANGE, r)
return false
end
-
- start_name, stop_name = split_column_range(r)
-
+ start_name, stop_name = split_sheet_range(r)
if !is_valid_column_name(start_name) || !is_valid_column_name(stop_name)
return false
end
-
+ return true
+end
+function is_valid_row_range(r::AbstractString) :: Bool
+ if occursin(RGX_SINGLE_ROW, r)
+ row_number = parse(Int, r)
+ if row_number <= 0 && row_number > EXCEL_MAX_ROWS
+ throw(XLSXError("Row number should be in the range from 1 to $EXCEL_MAX_ROWS."))
+ end
+ return true
+ end
+ if !occursin(RGX_ROW_RANGE, r)
+ return false
+ end
+ start_name, stop_name = split_sheet_range(r) # Function works for row ranges too.
+ if !is_valid_row_name(start_name) || !is_valid_row_name(stop_name)
+ return false
+ end
return true
end
+function RowRange(r::AbstractString)
+ !is_valid_row_range(r) && throw(XLSXError("Invalid row range: $r."))
+ start_name, stop_name = split_sheet_range(r)
+ return RowRange(parse(Int, start_name), parse(Int, stop_name))
+end
function ColumnRange(r::AbstractString)
- @assert is_valid_column_range(r) "Invalid column range: $r."
- start_name, stop_name = split_column_range(r)
+ !is_valid_column_range(r) && throw(XLSXError("Invalid column range: $r."))
+ start_name, stop_name = split_sheet_range(r)
return ColumnRange(decode_column_number(start_name), decode_column_number(stop_name))
end
convert(::Type{ColumnRange}, str::AbstractString) = ColumnRange(str)
convert(::Type{ColumnRange}, column_range::ColumnRange) = column_range
+convert(::Type{RowRange}, str::AbstractString) = RowRange(str)
+convert(::Type{RowRange}, row_range::RowRange) = row_range
column_bounds(r::ColumnRange) = (r.start, r.stop)
Base.length(r::ColumnRange) = r.stop - r.start + 1
+row_bounds(r::RowRange) = (r.start, r.stop)
+Base.length(r::RowRange) = r.stop - r.start + 1
# ColumnRange iterator: element is a String with the column name, the state is the column number.
function Base.iterate(itr::ColumnRange, state::Int=itr.start)
@@ -318,6 +411,15 @@ function Base.iterate(itr::ColumnRange, state::Int=itr.start)
return encode_column_number(state), state + 1
end
+# RowRange iterator: element is a String with the row name (e.g. "1"). The state is the row number.
+function Base.iterate(itr::RowRange, state::Int=itr.start)
+ if state > itr.stop
+ return nothing
+ end
+
+ return string(state), state + 1
+end
+
# CellRange iterator: element is a CellRef, the state is a CellPosition.
function Base.iterate(rng::CellRange, state::CellPosition=CellPosition(rng.start))
@@ -340,33 +442,109 @@ function Base.length(rng::CellRange)
end
#
-# SheetCellRef, SheetCellRange, SheetColumnRange
+# SheetCellRef, SheetCellRange, SheetColumnRange, SheetRowRange, NonContiguousRange
#
-Base.string(cr::SheetCellRef) = string(cr.sheet, "!", cr.cellref)
+Base.string(cr::SheetCellRef) = string(quoteit(cr.sheet), "!", cr.cellref)
Base.show(io::IO, cr::SheetCellRef) = print(io, string(cr))
Base.:(==)(cr1::SheetCellRef, cr2::SheetCellRef) = cr1.sheet == cr2.sheet && cr2.cellref == cr2.cellref
Base.hash(cr::SheetCellRef) = hash(cr.sheet) + hash(cr.cellref)
-Base.string(cr::SheetCellRange) = string(cr.sheet, "!", cr.rng)
+Base.string(cr::SheetCellRange) = string(quoteit(cr.sheet), "!", cr.rng)
Base.show(io::IO, cr::SheetCellRange) = print(io, string(cr))
Base.:(==)(cr1::SheetCellRange, cr2::SheetCellRange) = cr1.sheet == cr2.sheet && cr2.rng == cr2.rng
Base.hash(cr::SheetCellRange) = hash(cr.sheet) + hash(cr.rng)
-Base.string(cr::SheetColumnRange) = string(cr.sheet, "!", cr.colrng)
+Base.string(cr::SheetColumnRange) = string(quoteit(cr.sheet), "!", cr.colrng)
Base.show(io::IO, cr::SheetColumnRange) = print(io, string(cr))
Base.:(==)(cr1::SheetColumnRange, cr2::SheetColumnRange) = cr1.sheet == cr2.sheet && cr2.colrng == cr2.colrng
Base.hash(cr::SheetColumnRange) = hash(cr.sheet) + hash(cr.colrng)
+Base.string(cr::SheetRowRange) = string(quoteit(cr.sheet), "!", cr.rowrng)
+Base.show(io::IO, cr::SheetRowRange) = print(io, string(cr))
+Base.:(==)(cr1::SheetRowRange, cr2::SheetRowRange) = cr1.sheet == cr2.sheet && cr2.rowrng == cr2.rowrng
+Base.hash(cr::SheetRowRange) = hash(cr.sheet) + hash(cr.colrng)
+
+Base.string(cr::NonContiguousRange) = join([string(quoteit(cr.sheet), "!", x) for x in cr.rng],",")
+Base.show(io::IO, cr::NonContiguousRange) = print(io, string(cr))
+Base.:(==)(cr1::NonContiguousRange, cr2::NonContiguousRange) = cr1.sheet == cr2.sheet && cr2.rng == cr2.rng
+Base.hash(cr::NonContiguousRange) = hash(cr.sheet) + hash(cr.rng)
+
+function Base.in(ref::SheetCellRef, ncrng::NonContiguousRange) :: Bool
+ if ref.sheet != ncrng.sheet
+ return false
+ end
+ for r in ncrng.rng
+ if r isa CellRef
+ if ref.cellref == r
+ return true
+ end
+ else
+ if ref.cellref in r
+ return true
+ end
+ end
+ end
+ return false
+end
+
+function nc_bounds(r::NonContiguousRange)::CellRange # Smallest rectangualar `CellRange` that contains all the elements in `r`.
+ top = EXCEL_MAX_ROWS
+ bottom = 0
+ left = EXCEL_MAX_COLS
+ right = 0
+ for rng in r.rng
+ if isa(rng, CellRef)
+ top = min(top, row_number(rng))
+ bottom = max(bottom, row_number(rng))
+ left = min(left, column_number(rng))
+ right = max(right, column_number(rng))
+ else
+ top = min(top, row_number(rng.start))
+ bottom = max(bottom, row_number(rng.stop))
+ left = min(left, column_number(rng.start))
+ right = max(right, column_number(rng.stop))
+ end
+ end
+ return CellRange(CellRef(top, left), CellRef(bottom, right))
+end
+function Base.length(r::NonContiguousRange)::Int # Number of cells in `r`, eliminating duplicates.
+# s = 0
+ allcells= Vector{String}()
+ for rng in r.rng
+ if rng isa CellRef
+ push!(allcells, rng.name)
+ else
+ for cell in rng
+ push!(allcells, cell.name)
+ end
+ end
+ end
+# if rng isa CellRef
+# s += 1
+# else
+# s += length(rng)
+# end
+# end
+ return length(unique(allcells))
+end
+
const RGX_SHEET_CELLNAME = r"^.+![A-Z]+[0-9]+$"
const RGX_SHEET_CELLRANGE = r"^.+![A-Z]+[0-9]+:[A-Z]+[0-9]+$"
const RGX_SHEET_COLUMN_RANGE = r"^.+![A-Z]?[A-Z]?[A-Z]:[A-Z]?[A-Z]?[A-Z]$"
+const RGX_SHEET_ROW_RANGE = r"^.+![1-9][0-9]*:[1-9][0-9]*$"
const RGX_SHEET_CELLNAME_RIGHT = r"[A-Z]+[0-9]+$"
const RGX_SHEET_CELLRANGE_RIGHT = r"[A-Z]+[0-9]+:[A-Z]+[0-9]+$"
const RGX_SHEET_COLUMN_RANGE_RIGHT = r"[A-Z]?[A-Z]?[A-Z]:[A-Z]?[A-Z]?[A-Z]$"
+const RGX_SHEET_ROW_RANGE_RIGHT = r"[1-9][0-9]*:[1-9][0-9]*$"
function is_valid_sheet_cellname(n::AbstractString) :: Bool
+
+ if is_valid_non_contiguous_range(n) # Non-contiguous ranges are comma separated `CellRef-like` or `CellRange-like` strings
+ return false
+ end
+
if !occursin(RGX_SHEET_CELLNAME, n)
return false
end
@@ -380,6 +558,11 @@ function is_valid_sheet_cellname(n::AbstractString) :: Bool
end
function is_valid_sheet_cellrange(n::AbstractString) :: Bool
+
+ if is_valid_non_contiguous_range(n) # Non-contiguous ranges are comma separated `CellRef-like` or `CellRange-like` strings
+ return false
+ end
+
if !occursin(RGX_SHEET_CELLRANGE, n)
return false
end
@@ -404,13 +587,25 @@ function is_valid_sheet_column_range(n::AbstractString) :: Bool
return true
end
+function is_valid_sheet_row_range(n::AbstractString) :: Bool
+ if !occursin(RGX_SHEET_ROW_RANGE, n)
+ return false
+ end
+
+ row_range = match(RGX_SHEET_ROW_RANGE_RIGHT, n).match
+ if !is_valid_row_range(row_range)
+ return false
+ end
+
+ return true
+end
const RGX_SHEET_PREFIX = r"^.+!"
const RGX_CELLNAME_RIGHT_FIXED = r"\$[A-Z]+\$[0-9]+$"
-const RGX_SHEET_CELNAME_RIGHT_FIXED = r"\$[A-Z]+\$[0-9]+:\$[A-Z]+\$[0-9]+$"
+const RGX_SHEET_CELLNAME_RIGHT_FIXED = r"\$[A-Z]+\$[0-9]+:\$[A-Z]+\$[0-9]+$"
function parse_sheetname_from_sheetcell_name(n::AbstractString) :: SubString
- @assert occursin(RGX_SHEET_PREFIX, n) "$n is not a SheetCell reference."
+ !occursin(RGX_SHEET_PREFIX, n) && throw(XLSXError("$n is not a SheetCell reference."))
sheetname = match(RGX_SHEET_PREFIX, n).match
sheetname = SubString(sheetname, firstindex(sheetname), prevind(sheetname, lastindex(sheetname)))
return sheetname
@@ -423,7 +618,7 @@ function SheetCellRef(n::AbstractString)
fixed_cellname = match(RGX_CELLNAME_RIGHT_FIXED, n).match
cellref = CellRef(replace(fixed_cellname, "\$" => ""))
else
- @assert is_valid_sheet_cellname(n) "$n is not a valid SheetCellRef."
+ !is_valid_sheet_cellname(n) && throw(XLSXError("$n is not a valid SheetCellRef."))
cellref = CellRef(match(RGX_SHEET_CELLNAME_RIGHT, n).match)
end
sheetname = parse_sheetname_from_sheetcell_name(n)
@@ -434,10 +629,10 @@ function SheetCellRange(n::AbstractString)
local cellrange::CellRange
if is_valid_fixed_sheet_cellrange(n)
- fixed_cellrange = match(RGX_SHEET_CELNAME_RIGHT_FIXED, n).match
+ fixed_cellrange = match(RGX_SHEET_CELLNAME_RIGHT_FIXED, n).match
cellrange = CellRange(replace(fixed_cellrange, "\$" => ""))
else
- @assert is_valid_sheet_cellrange(n) "$n is not a valid SheetCellRange."
+ !is_valid_sheet_cellrange(n) && throw(XLSXError("$n is not a valid SheetCellRange."))
cellrange = CellRange(match(RGX_SHEET_CELLRANGE_RIGHT, n).match)
end
@@ -446,17 +641,111 @@ function SheetCellRange(n::AbstractString)
end
function SheetColumnRange(n::AbstractString)
- @assert is_valid_sheet_column_range(n) "$n is not a valid SheetColumnRange."
+ !is_valid_sheet_column_range(n) && throw(XLSXError("$n is not a valid SheetColumnRange."))
column_range = match(RGX_SHEET_COLUMN_RANGE_RIGHT, n).match
sheetname = parse_sheetname_from_sheetcell_name(n)
return SheetColumnRange(sheetname, ColumnRange(column_range))
end
+function SheetRowRange(n::AbstractString)
+ !is_valid_sheet_row_range(n) && throw(XLSXError("$n is not a valid SheetRowRange."))
+ row_range = match(RGX_SHEET_ROW_RANGE_RIGHT, n).match
+ sheetname = parse_sheetname_from_sheetcell_name(n)
+ return SheetRowRange(sheetname, RowRange(row_range))
+end
# Named ranges
+const RGX_FIXED_CELLNAME = r"^\$[A-Z]+\$[0-9]+$"
+const RGX_FIXED_CELLRANGE = r"^\$[A-Z]+\$[0-9]+:\$[A-Z]+\$[0-9]+$"
const RGX_FIXED_SHEET_CELLNAME = r"^.+!\$[A-Z]+\$[0-9]+$"
const RGX_FIXED_SHEET_CELLRANGE = r"^.+!\$[A-Z]+\$[0-9]+:\$[A-Z]+\$[0-9]+$"
+is_valid_fixed_cellname(s::AbstractString) = occursin(RGX_FIXED_CELLNAME, s)
+is_valid_fixed_cellrange(s::AbstractString) = occursin(RGX_FIXED_CELLRANGE, s)
is_valid_fixed_sheet_cellname(s::AbstractString) = occursin(RGX_FIXED_SHEET_CELLNAME, s)
is_valid_fixed_sheet_cellrange(s::AbstractString) = occursin(RGX_FIXED_SHEET_CELLRANGE, s)
-is_non_contiguous_range(v) = occursin(",", string(v)) # Non-contiguous ranges are comma separated `SheetCellRef-like` or `SheetCellRange-like` strings
+is_valid_non_contiguous_range(v::AbstractString) :: Bool = is_valid_non_contiguous_cellrange(v) || is_valid_non_contiguous_sheetcellrange(v)
+
+function is_valid_non_contiguous_sheetcellrange(v::AbstractString) :: Bool
+
+ if !occursin(",", string(v)) # Non-contiguous ranges are comma separated `SheetCellRef-like` or `SheetCellRange-like` strings
+ return false
+ end
+
+ ranges = split(v, ",")
+ for r in ranges
+ if !is_valid_sheet_cellname(r) && !is_valid_sheet_cellrange(r) && !is_valid_fixed_sheet_cellname(r) && !is_valid_fixed_sheet_cellrange(r)
+ return false
+ end
+ end
+
+ firstsheet = parse_sheetname_from_sheetcell_name(ranges[begin])
+
+ if any(parse_sheetname_from_sheetcell_name(r) != firstsheet for r in ranges) # All `SheetCellRef`s and `SheetCellRange`s should have the same sheet name
+ return false
+ end
+
+ return true
+end
+
+function is_valid_non_contiguous_cellrange(v::AbstractString) :: Bool
+
+ if !occursin(",", string(v)) # Non-contiguous ranges are comma separated `SheetCellRef-like` or `SheetCellRange-like` strings
+
+ return false
+ end
+
+ ranges = split(v, ",")
+
+ for r in ranges
+ if !is_valid_cellname(r) && !is_valid_cellrange(r) &&!is_valid_fixed_cellname(r) && !is_valid_fixed_cellrange(r)
+ return false
+ end
+ end
+
+ return true
+end
+
+NonContiguousRange(s::Worksheet, v::AbstractString)::NonContiguousRange = nCR(s.name, string.(split(v, ",")))
+function NonContiguousRange(v::AbstractString)::NonContiguousRange
+
+ !is_valid_non_contiguous_range(v) && throw(XLSXError("$v is not a valid non-contiguous range."))
+
+ ranges = string.(split(v, ","))
+ firstsheet = parse_sheetname_from_sheetcell_name(ranges[1])
+ if !all(parse_sheetname_from_sheetcell_name(r) == firstsheet for r in ranges)
+ throw(XLSXError("All `CellRef`s and `CellRange`s should have the same sheet name."))
+ end
+
+ return nCR(unquoteit(firstsheet), ranges)
+end
+
+function nCR(s::AbstractString, ranges::Vector{String}) :: NonContiguousRange
+ noncontig = Vector{Union{CellRef, CellRange}}()
+
+ for n in ranges
+ if is_valid_fixed_sheet_cellname(n)
+ fixed_cellname = match(RGX_CELLNAME_RIGHT_FIXED, n).match
+ push!(noncontig, CellRef(replace(fixed_cellname, "\$" => "")))
+ elseif is_valid_sheet_cellname(n)
+ push!(noncontig, CellRef(match(RGX_SHEET_CELLNAME_RIGHT, n).match))
+ elseif is_valid_fixed_sheet_cellrange(n)
+ fixed_cellrange = match(RGX_SHEET_CELLNAME_RIGHT_FIXED, n).match
+ push!(noncontig, CellRange(replace(fixed_cellrange, "\$" => "")))
+ elseif is_valid_sheet_cellrange(n)
+ push!(noncontig, CellRange(match(RGX_SHEET_CELLRANGE_RIGHT, n).match))
+ elseif is_valid_fixed_cellname(n)
+ push!(noncontig, CellRef(replace(n, "\$" => "")))
+ elseif is_valid_fixed_cellrange(n)
+ push!(noncontig, CellRange(replace(n, "\$" => "")))
+ elseif is_valid_cellname(n)
+ push!(noncontig, CellRef(n))
+ elseif is_valid_cellrange(n)
+ push!(noncontig, CellRange(n))
+ else
+ throw(XLSXError("Invalid non-contiguous range: $n."))
+ end
+ end
+
+ return NonContiguousRange(s, noncontig)
+end
\ No newline at end of file
diff --git a/src/conditional-format-helpers.jl b/src/conditional-format-helpers.jl
new file mode 100644
index 00000000..ab4c6c29
--- /dev/null
+++ b/src/conditional-format-helpers.jl
@@ -0,0 +1,328 @@
+#
+# ---- Some random helper functions
+#
+#function convertref(c)
+# if !isnothing(c)
+# if is_valid_cellname(c)
+# c = abscell(CellRef(c))
+# elseif is_valid_sheet_cellname(c)
+# c = mkabs(SheetCellRef(c))
+# end
+# end
+# return c
+#end
+function isValidKw(kw::String, val::Union{String, Nothing}, valid::Vector{String})
+ if isnothing(val) || val ∈ valid
+ return true
+ else
+ throw(XLSXError("Invalid keyword $kw: $val. Valid values are $valid"))
+ end
+end
+function uppercase_unquoted(s::AbstractString)
+ result = IOBuffer()
+ i = firstindex(s)
+ inside_quote = false
+ while i <= lastindex(s)
+ c = s[i]
+ if c == '\\' && nextind(s, i) <= lastindex(s)
+ # Handle escaped character
+ next_i = nextind(s, i)
+ print(result, s[i:next_i])
+ i = nextind(s, next_i)
+ elseif c == '"'
+ inside_quote = !inside_quote
+ print(result, c)
+ i = nextind(s, i)
+ else
+ if inside_quote
+ print(result, c)
+ else
+ print(result, uppercase(c))
+ end
+ i = nextind(s, i)
+ end
+ end
+ return String(take!(result))
+end
+
+#
+# --- Standard conditional formats
+#
+function allCfs(ws::Worksheet)::Vector{XML.Node}
+ sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet" * string(ws.sheetId) * ".xml") # find all the blocks in the worksheet's xml file
+ return find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":worksheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":conditionalFormatting", sheetdoc)
+end
+function add_cf_to_XML(ws, new_cf) # Add a new conditional formatting to the worksheet XML.
+ sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet" * string(ws.sheetId) * ".xml") # The blocks come after the
+ k, l = get_idces(sheetdoc, "worksheet", "sheetData")
+ len = length(sheetdoc[k])
+ if l != len
+ push!(sheetdoc[k], sheetdoc[k][end])
+ if l + 1 < len
+ for pos = len-1:-1:l+1
+ sheetdoc[k][pos+1] = sheetdoc[k][pos]
+ end
+ end
+ sheetdoc[k][l+1] = new_cf
+ else
+ push!(sheetdoc[k], new_cf)
+ end
+end
+function update_worksheet_cfx!(allcfs, cfx, ws, rng)
+ matchcfs = filter(x -> x["sqref"] == string(rng), allcfs) # Match range with existing conditional formatting blocks.
+ l = length(matchcfs)
+ if l == 0 # No existing conditional formatting blocks for this range so create a new one.
+ new_cf = XML.Element("conditionalFormatting"; sqref=rng)
+ push!(new_cf, cfx)
+ add_cf_to_XML(ws, new_cf) # Add the new conditional formatting block to the worksheet XML.
+ elseif l == 1 # Existing conditional formatting block found for this range so add new rule to that block.
+ push!(matchcfs[1], cfx)
+ else
+ throw(XLSXError("Too many conditional formatting blocks for range `$rng`. Must be one or none, found `$l`."))
+ end
+ update_worksheets_xml!(get_xlsxfile(ws))
+end
+
+#
+# --- Conditional formats relying on Excel 2010 extensions
+#
+function allExtCfs(ws::Worksheet)::Vector{XML.Node}
+ sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet" * string(ws.sheetId) * ".xml")
+ i, j = get_idces(sheetdoc, "worksheet", "extLst")
+ if isnothing(j)
+ return Vector{XML.Node}()
+ end
+ extlst = sheetdoc[i][j]
+ exts = XML.children(extlst)
+ let cfs = nothing
+ for ext in exts
+ for c in XML.children(ext)
+ if XML.tag(c)=="x14:conditionalFormattings"
+ cfs = c
+ break
+ end
+ end
+ end
+ return isnothing(cfs) ? Vector{XML.Node}() : XML.children(cfs)
+ end
+end
+function make_extLst!(s)
+ ext_list = XML.Element("extLst")
+ ext_element = XML.Element("ext")
+ ext_element["xmlns:x14"] = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"
+ ext_element["uri"] = "{78C0D931-6437-407d-A8EE-F0AAD7539E65}"
+ push!(ext_list, ext_element)
+ push!(s, ext_list)
+end
+function make_extCfsBlock()
+ extCf = XML.Element("x14:conditionalFormatting")
+ extCf["xmlns:xm"] = "http://schemas.microsoft.com/office/excel/2006/main"
+ return extCf
+end
+function update_worksheet_ext_cfx!(allcfs, cfx, ws, rng)
+ sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet" * string(ws.sheetId) * ".xml")
+ i, j = get_idces(sheetdoc, "worksheet", "extLst")
+ if isnothing(j)
+ make_extLst!(sheetdoc[i])
+ j = length(XML.children(sheetdoc[i]))
+ end
+ m, n = get_idces(sheetdoc[i], "extLst", "ext")
+ @assert m==j
+ if length(allcfs)==0 # No block. Need to create one.
+ extcfs=XML.Element("x14:conditionalFormattings")
+ push!(sheetdoc[i][j][n], extcfs)
+ end
+ matchcfs = filter(x -> XML.simple_value(x[end]) == string(rng), allcfs) # Match range with existing conditional formatting blocks.
+ o, p = get_idces(sheetdoc[i][j], "ext", "x14:conditionalFormattings")
+ @assert o==n
+ l = length(matchcfs)
+ if l == 0 # No existing conditional formatting blocks for this range so create a new one.
+ new_cf = make_extCfsBlock()
+ push!(new_cf, cfx)
+ push!(new_cf, XML.Element("xm:sqref", XML.Text(string(rng))))
+ push!(sheetdoc[i][j][n][p], new_cf) # Add the new conditional formatting block to the worksheet XML.
+ elseif l == 1 # Existing conditional formatting block found for this range so add new rule to that block.
+ pushfirst!(matchcfs[1], cfx)
+ else
+ throw(XLSXError("Too many conditional formatting blocks for range `$rng`. Must be one or none, found `$l`."))
+ end
+ update_worksheets_xml!(get_xlsxfile(ws))
+end
+function get_x14_icon(x14set)
+ rule = XML.Element("x14:cfRule", type="iconSet", priority="1", id="XXXX-xxxx-XXXX") # replace id with UUID generated at time of use.
+ if x14set == "Custom"
+ icon = XML.Element("x14:iconSet", iconSet="3Arrows", custom="1")
+ else
+ icon = XML.Element("x14:iconSet", iconSet=x14set)
+ end
+ if x14set=="5Boxes"
+ vals=[0, 20, 40, 60, 80]
+ elseif x14set=="Custom"
+ vals=[0]
+ else
+ vals=[0, 33, 67]
+ end
+ for v in vals
+ cfvo = XML.Element("x14:cfvo", type="percent")
+ push!(cfvo, XML.Element("xm:f", XML.Text(v)))
+ push!(icon, cfvo)
+ end
+ push!(rule, icon)
+ return rule
+end
+
+#
+# ---- Formatting (styles) definitions for conditional formats
+#
+function Add_Cf_Dx(wb::Workbook, new_dx::XML.Node)::DxFormat
+ # Check if the workbook already has a dxfs element. If not, add one.
+ xroot = styles_xmlroot(wb)
+ i, j = get_idces(xroot, "styleSheet", "dxfs")
+
+ if isnothing(j) # No existing conditional formats so need to add a block (is this even possible?). Push everything lower down one.
+ throw(XLSXError("No block found in the styles.xml file. Please submit an issue to report this and attach the Excel file you were working with."))
+ #= I don't think this can ever happen, so I've commented it out to improve coverage.
+ k, l = get_idces(xroot, "styleSheet", "cellStyles")
+ l += 1 # The dxfs block comes after the cellXfs block.
+ len = length(xroot[k])
+ i != k && throw(XLSXError("Some problem here!"))
+ push!(xroot[k], xroot[k][end]) # duplicate last element then move everything else down one
+ if l < len
+ for pos = len-1:-1:l
+ xroot[k][pos+1] = xroot[k][pos]
+ end
+ end
+ xroot[k][l] = XML.Element("dxsf", count="0")
+ j = l
+ println(XML.write(xroot[i][j]))
+ =#
+ else
+ existing_dxf_elements_count = length(XML.children(xroot[i][j]))
+
+ if parse(Int, xroot[i][j]["count"]) != existing_dxf_elements_count
+ throw(XLSXError("Wrong number of xf elements found: $existing_cellxf_elements_count. Expected $(parse(Int, xroot[i][j]["count"]))."))
+ end
+ end
+
+ # Don't reuse duplicates here. Always create new!
+ existingdx = XML.children(xroot[i][j])
+ dxfs = unlink(xroot[i][j], ("dxfs", "dxf")) # Create the new Node
+ if length(existingdx) > 0
+ for c in existingdx
+ push!(dxfs, c) # Copy each existing into the new Node
+ end
+ end
+ push!(dxfs, new_dx)
+
+ xroot[i][j] = dxfs # Update the worksheet with the new cols.
+
+ xroot[i][j]["count"] = string(existing_dxf_elements_count + 1)
+
+ return DxFormat(existing_dxf_elements_count) # turns out this is the new index (because it's zero-based)
+
+end
+function get_dx(dxStyle::Union{Nothing,String}, format::Union{Nothing,Vector{Pair{String,String}}}, font::Union{Nothing,Vector{Pair{String,String}}}, border::Union{Nothing,Vector{Pair{String,String}}}, fill::Union{Nothing,Vector{Pair{String,String}}})::Dict{String,Dict{String,String}}
+ if isnothing(dxStyle)
+ if all(isnothing.([border, fill, font, format]))
+ dx = highlights["redfilltext"]
+ else
+ dx = Dict{String,Dict{String,String}}()
+ for att in ["font" => font, "fill" => fill, "border" => border, "format" => format]
+ if !isnothing(last(att))
+ dxx = Dict{String,String}()
+ for i in last(att)
+ push!(dxx, first(i) => last(i))
+ end
+ push!(dx, first(att) => dxx)
+ end
+ end
+ end
+ elseif haskey(highlights, dxStyle)
+ dx = highlights[dxStyle]
+ else
+ throw(XLSXError("Invalid dxStyle: $dxStyle. Valid options are: $(keys(highlights))."))
+ end
+ return dx
+end
+function get_new_dx(wb::Workbook, dx::Dict{String,Dict{String,String}})::XML.Node
+ new_dx = XML.Element("dxf")
+ for k in ["font", "format", "fill", "border"] # Order seems to be important to Excel.
+ if haskey(dx, k)
+ v = dx[k]
+ if k == "fill"
+ if !isnothing(v)
+ filldx = XML.Element("fill")
+ patterndx = XML.Element("patternFill")
+ for (y, z) in v
+ y in ["pattern", "bgColor", "fgColor"] || throw(XLSXError("Invalid fill attribute: $k. Valid options are: `pattern`, `bgColor`, `fgColor`."))
+ if y in ["fgColor", "bgColor"]
+ push!(patterndx, XML.Element(y, rgb=get_color(z)))
+ elseif y == "pattern" && z != "none"
+ patterndx["patternType"] = z
+ end
+ end
+ push!(filldx, patterndx)
+ end
+ push!(new_dx, filldx)
+ elseif k == "font"
+ if !isnothing(v)
+ fontdx = XML.Element("font")
+ for (y, z) in v
+ y in ["color", "bold", "italic", "under", "strike"] || throw(XLSXError("Invalid font attribute: $y. Valid options are: `color`, `bold`, `italic`, `under`, `strike`."))
+ if y == "color"
+ push!(fontdx, XML.Element(y, rgb=get_color(z)))
+ elseif y == "bold"
+ z == "true" && push!(fontdx, XML.Element("b", val="0"))
+ elseif y == "italic"
+ z == "true" && push!(fontdx, XML.Element("i", val="0"))
+ elseif y == "under"
+ z != "none" && push!(fontdx, XML.Element("u"; val="v"))
+ elseif y == "strike"
+ z == "true" && push!(fontdx, XML.Element(y))
+ end
+ end
+ end
+ push!(new_dx, fontdx)
+ elseif k == "border"
+ if !isnothing(v)
+ all([y in ["color", "style"] for y in keys(v)]) || throw(XLSXError("Invalid border attribute. Valid options are: `color`, `style`."))
+ borderdx = XML.Element("border")
+ cdx = haskey(v, "color") ? XML.Element("color", rgb=get_color(v["color"])) : nothing
+ sdx = haskey(v, "style") ? v["style"] : nothing
+ leftdx = XML.Element("left")
+ rightdx = XML.Element("right")
+ topdx = XML.Element("top")
+ bottomdx = XML.Element("bottom")
+ if !isnothing(sdx)
+ leftdx["style"] = sdx
+ rightdx["style"] = sdx
+ topdx["style"] = sdx
+ bottomdx["style"] = sdx
+ end
+ if !isnothing(cdx)
+ push!(leftdx, cdx)
+ push!(rightdx, cdx)
+ push!(topdx, cdx)
+ push!(bottomdx, cdx)
+ end
+ end
+ push!(borderdx, leftdx)
+ push!(borderdx, rightdx)
+ push!(borderdx, topdx)
+ push!(borderdx, bottomdx)
+ push!(new_dx, borderdx)
+ elseif k == "format"
+ if !isnothing(v)
+ if haskey(v, "format")
+ fmtCode = v["format"]
+ new_formatId = get_new_formatId(wb, fmtCode)
+ new_fmtCode = styles_numFmt_formatCode(wb, new_formatId)
+ fmtdx = XML.Element("numFmt"; numFmtId=string(new_formatId), formatCode=new_fmtCode)
+ push!(new_dx, fmtdx)
+ end
+ end
+ end
+ end
+ end
+ return new_dx
+end
\ No newline at end of file
diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl
new file mode 100644
index 00000000..94fb4151
--- /dev/null
+++ b/src/conditional-formats.jl
@@ -0,0 +1,2525 @@
+#const needsValue2::Vector{String} = ["between", "notBetween"]
+const highlights::Dict{String,Dict{String,Dict{String,String}}} = Dict(
+ "redfilltext" => Dict(
+ "font" => Dict("color" => "FF9C0006"),
+ "fill" => Dict("pattern" => "solid", "bgColor" => "FFFFC7CE")
+ ),
+ "yellowfilltext" => Dict(
+ "font" => Dict("color" => "FF9C5700"),
+ "fill" => Dict("pattern" => "solid", "bgColor" => "FFFFEB9C")
+ ),
+ "greenfilltext" => Dict(
+ "font" => Dict("color" => "FF006100"),
+ "fill" => Dict("pattern" => "solid", "bgColor" => "FFC6EFCE")
+ ),
+ "redfill" => Dict(
+ "fill" => Dict("pattern" => "solid", "bgColor" => "FFFFC7CE")
+ ),
+ "redtext" => Dict(
+ "font" => Dict("color" => "FF9C0006"),
+ ),
+ "redborder" => Dict(
+ "border" => Dict("color" => "FF9C0006", "style" => "thin")
+ )
+)
+const databars::Dict{String,Dict{String,String}} = Dict(
+ "bluegrad" => Dict(
+ "min_type" => "automatic",
+ "max_type" => "automatic",
+ "fill_col" => "FF638EC6",
+ "borders" => "true",
+ "sameNegBorders" => "false",
+ "border_col" => "FF638EC6",
+ "neg_fill_col" => "FFFF0000",
+ "neg_border_col" => "FFFF0000",
+ "axis_col" => "FF000000"
+ ),
+ "greengrad" => Dict(
+ "min_type" => "automatic",
+ "max_type" => "automatic",
+ "fill_col" => "FF63C384",
+ "borders" => "true",
+ "sameNegBorders" => "false",
+ "border_col" => "FF63C384",
+ "neg_fill_col" => "FFFF0000",
+ "neg_border_col" => "FFFF0000",
+ "axis_col" => "FF000000"
+ ),
+ "redgrad" => Dict(
+ "min_type" => "automatic",
+ "max_type" => "automatic",
+ "fill_col" => "FFFF555A",
+ "borders" => "true",
+ "sameNegBorders" => "false",
+ "border_col" => "FFFF555A",
+ "neg_fill_col" => "FFFF0000",
+ "neg_border_col" => "FFFF0000",
+ "axis_col" => "FF000000"
+ ),
+ "orangegrad" => Dict(
+ "min_type" => "automatic",
+ "max_type" => "automatic",
+ "fill_col" => "FFFFB628",
+ "borders" => "true",
+ "sameNegBorders" => "false",
+ "border_col" => "FFFFB628",
+ "neg_fill_col" => "FFFF0000",
+ "neg_border_col" => "FFFF0000",
+ "axis_col" => "FF000000"
+ ),
+ "lightbluegrad" => Dict(
+ "min_type" => "automatic",
+ "max_type" => "automatic",
+ "fill_col" => "FF008AEF",
+ "borders" => "true",
+ "sameNegBorders" => "false",
+ "border_col" => "FF008AEF",
+ "neg_fill_col" => "FFFF0000",
+ "neg_border_col" => "FFFF0000",
+ "axis_col" => "FF000000"
+ ),
+ "purplegrad" => Dict(
+ "min_type" => "automatic",
+ "max_type" => "automatic",
+ "fill_col" => "FFD6007B",
+ "borders" => "true",
+ "sameNegBorders" => "false",
+ "border_col" => "FFD6007B",
+ "neg_fill_col" => "FFFF0000",
+ "neg_border_col" => "FFFF0000",
+ "axis_col" => "FF000000"
+ ),
+ "blue" => Dict(
+ "min_type" => "automatic",
+ "max_type" => "automatic",
+ "fill_col" => "FF638EC6",
+ "gradient" => "false",
+ "neg_fill_col" => "FFFF0000",
+ "axis_col" => "FF000000"
+ ),
+ "green" => Dict(
+ "min_type" => "automatic",
+ "max_type" => "automatic",
+ "fill_col" => "FF63C384",
+ "gradient" => "false",
+ "neg_fill_col" => "FFFF0000",
+ "axis_col" => "FF000000"
+ ),
+ "red" => Dict(
+ "min_type" => "automatic",
+ "max_type" => "automatic",
+ "fill_col" => "FFFF555A",
+ "gradient" => "false",
+ "neg_fill_col" => "FFFF0000",
+ "axis_col" => "FF000000"
+ ),
+ "orange" => Dict(
+ "min_type" => "automatic",
+ "max_type" => "automatic",
+ "fill_col" => "FFFFB628",
+ "gradient" => "false",
+ "neg_fill_col" => "FFFF0000",
+ "axis_col" => "FF000000"
+ ),
+ "lightblue" => Dict(
+ "min_type" => "automatic",
+ "max_type" => "automatic",
+ "fill_col" => "FF008AEF",
+ "gradient" => "false",
+ "neg_fill_col" => "FFFF0000",
+ "axis_col" => "FF000000"
+ ),
+ "purple" => Dict(
+ "min_type" => "automatic",
+ "max_type" => "automatic",
+ "fill_col" => "FFD6007B",
+ "gradient" => "false",
+ "neg_fill_col" => "FFFF0000",
+ "axis_col" => "FF000000"
+ ),
+)
+const colorscales::Dict{String,XML.Node} = Dict( # Defines the 12 standard, built-in Excel color scales for conditional formatting.
+ "greenyellowred" => XML.h.cfRule(type="colorScale", priority="1",
+ XML.h.colorScale(
+ XML.h.cfvo(type="min"),
+ XML.h.cfvo(type="percentile", val="50"),
+ XML.h.cfvo(type="max"),
+ XML.h.color(rgb="FFF8696B"),
+ XML.h.color(rgb="FFFFEB84"),
+ XML.h.color(rgb="FF63BE7B")
+ )
+ ),
+ "redyellowgreen" => XML.h.cfRule(type="colorScale", priority="1",
+ XML.h.colorScale(
+ XML.h.cfvo(type="min"),
+ XML.h.cfvo(type="percentile", val="50"),
+ XML.h.cfvo(type="max"),
+ XML.h.color(rgb="FF63BE7B"),
+ XML.h.color(rgb="FFFFEB84"),
+ XML.h.color(rgb="FFF8696B")
+ )
+ ),
+ "greenwhitered" => XML.h.cfRule(type="colorScale", priority="1",
+ XML.h.colorScale(
+ XML.h.cfvo(type="min"),
+ XML.h.cfvo(type="percentile", val="50"),
+ XML.h.cfvo(type="max"),
+ XML.h.color(rgb="FFF8696B"),
+ XML.h.color(rgb="FFFCFCFF"),
+ XML.h.color(rgb="FF63BE7B")
+ )
+ ),
+ "redwhitegreen" => XML.h.cfRule(type="colorScale", priority="1",
+ XML.h.colorScale(
+ XML.h.cfvo(type="min"),
+ XML.h.cfvo(type="percentile", val="50"),
+ XML.h.cfvo(type="max"),
+ XML.h.color(rgb="FF63BE7B"),
+ XML.h.color(rgb="FFFCFCFF"),
+ XML.h.color(rgb="FFF8696B")
+ )
+ ),
+ "bluewhitered" => XML.h.cfRule(type="colorScale", priority="1",
+ XML.h.colorScale(
+ XML.h.cfvo(type="min"),
+ XML.h.cfvo(type="percentile", val="50"),
+ XML.h.cfvo(type="max"),
+ XML.h.color(rgb="FFF8696B"),
+ XML.h.color(rgb="FFFCFCFF"),
+ XML.h.color(rgb="FF5A8AC6")
+ )
+ ),
+ "redwhiteblue" => XML.h.cfRule(type="colorScale", priority="1",
+ XML.h.colorScale(
+ XML.h.cfvo(type="min"),
+ XML.h.cfvo(type="percentile", val="50"),
+ XML.h.cfvo(type="max"),
+ XML.h.color(rgb="FF5A8AC6"),
+ XML.h.color(rgb="FFFCFCFF"),
+ XML.h.color(rgb="FFF8696B")
+ )
+ ),
+ "whitered" => XML.h.cfRule(type="colorScale", priority="1",
+ XML.h.colorScale(
+ XML.h.cfvo(type="min"),
+ XML.h.cfvo(type="max"),
+ XML.h.color(rgb="FFF8696B"),
+ XML.h.color(rgb="FFFCFCFF")
+ )
+ ),
+ "redwhite" => XML.h.cfRule(type="colorScale", priority="1",
+ XML.h.colorScale(
+ XML.h.cfvo(type="min"),
+ XML.h.cfvo(type="max"),
+ XML.h.color(rgb="FFFCFCFF"),
+ XML.h.color(rgb="FFF8696B")
+ )
+ ),
+ "greenwhite" => XML.h.cfRule(type="colorScale", priority="1",
+ XML.h.colorScale(
+ XML.h.cfvo(type="min"),
+ XML.h.cfvo(type="max"),
+ XML.h.color(rgb="FFFCFCFF"),
+ XML.h.color(rgb="FF63BE7B")
+ )
+ ),
+ "whitegreen" => XML.h.cfRule(type="colorScale", priority="1",
+ XML.h.colorScale(
+ XML.h.cfvo(type="min"),
+ XML.h.cfvo(type="max"),
+ XML.h.color(rgb="FF63BE7B"),
+ XML.h.color(rgb="FFFCFCFF")
+ )
+ ),
+ "greenyellow" => XML.h.cfRule(type="colorScale", priority="1",
+ XML.h.colorScale(
+ XML.h.cfvo(type="min"),
+ XML.h.cfvo(type="max"),
+ XML.h.color(rgb="FFFFEF9C"),
+ XML.h.color(rgb="FF63BE7B")
+ )
+ ),
+ "yellowgreen" => XML.h.cfRule(type="colorScale", priority="1",
+ XML.h.colorScale(
+ XML.h.cfvo(type="min"),
+ XML.h.cfvo(type="max"),
+ XML.h.color(rgb="FF63BE7B"),
+ XML.h.color(rgb="FFFFEF9C")
+ )
+ )
+)
+const iconsets::Dict{String,XML.Node} = Dict( # Defines the 20 standard, built-in Excel icon sets for conditional formatting.
+ "3Arrows" => XML.h.cfRule(type="iconSet", priority="1",
+ XML.h.iconSet(iconSet="3Arrows",
+ XML.h.cfvo(type="percent", val="0"),
+ XML.h.cfvo(type="percent", val="33"),
+ XML.h.cfvo(type="percent", val="67"),
+ )
+ ),
+ "4Arrows" => XML.h.cfRule(type="iconSet", priority="1",
+ XML.h.iconSet(iconSet="4Arrows",
+ XML.h.cfvo(type="percent", val="0"),
+ XML.h.cfvo(type="percent", val="25"),
+ XML.h.cfvo(type="percent", val="50"),
+ XML.h.cfvo(type="percent", val="75"),
+ )
+ ),
+ "5Arrows" => XML.h.cfRule(type="iconSet", priority="1",
+ XML.h.iconSet(iconSet="5Arrows",
+ XML.h.cfvo(type="percent", val="0"),
+ XML.h.cfvo(type="percent", val="20"),
+ XML.h.cfvo(type="percent", val="40"),
+ XML.h.cfvo(type="percent", val="60"),
+ XML.h.cfvo(type="percent", val="80"),
+ )
+ ),
+ "3ArrowsGray" => XML.h.cfRule(type="iconSet", priority="1",
+ XML.h.iconSet(iconSet="3ArrowsGray",
+ XML.h.cfvo(type="percent", val="0"),
+ XML.h.cfvo(type="percent", val="33"),
+ XML.h.cfvo(type="percent", val="67"),
+ )
+ ),
+ "4ArrowsGray" => XML.h.cfRule(type="iconSet", priority="1",
+ XML.h.iconSet(iconSet="4ArrowsGray",
+ XML.h.cfvo(type="percent", val="0"),
+ XML.h.cfvo(type="percent", val="25"),
+ XML.h.cfvo(type="percent", val="50"),
+ XML.h.cfvo(type="percent", val="75"),
+ )
+ ),
+ "5ArrowsGray" => XML.h.cfRule(type="iconSet", priority="1",
+ XML.h.iconSet(iconSet="5ArrowsGray",
+ XML.h.cfvo(type="percent", val="0"),
+ XML.h.cfvo(type="percent", val="20"),
+ XML.h.cfvo(type="percent", val="40"),
+ XML.h.cfvo(type="percent", val="60"),
+ XML.h.cfvo(type="percent", val="80"),
+ )
+ ),
+ "3TrafficLights" => XML.h.cfRule(type="iconSet", priority="1",
+ XML.h.iconSet(iconSet="3TrafficLights",
+ XML.h.cfvo(type="percent", val="0"),
+ XML.h.cfvo(type="percent", val="33"),
+ XML.h.cfvo(type="percent", val="67"),
+ )
+ ),
+ "3Signs" => XML.h.cfRule(type="iconSet", priority="1",
+ XML.h.iconSet(iconSet="3Signs",
+ XML.h.cfvo(type="percent", val="0"),
+ XML.h.cfvo(type="percent", val="33"),
+ XML.h.cfvo(type="percent", val="67"),
+ )
+ ),
+ "4RedToBlack" => XML.h.cfRule(type="iconSet", priority="1",
+ XML.h.iconSet(iconSet="4RedToBlack",
+ XML.h.cfvo(type="percent", val="0"),
+ XML.h.cfvo(type="percent", val="25"),
+ XML.h.cfvo(type="percent", val="50"),
+ XML.h.cfvo(type="percent", val="75"),
+ )
+ ),
+ "3TrafficLights2" => XML.h.cfRule(type="iconSet", priority="1",
+ XML.h.iconSet(iconSet="3TrafficLights2",
+ XML.h.cfvo(type="percent", val="0"),
+ XML.h.cfvo(type="percent", val="33"),
+ XML.h.cfvo(type="percent", val="67"),
+ )
+ ),
+ "4TrafficLights" => XML.h.cfRule(type="iconSet", priority="1",
+ XML.h.iconSet(iconSet="4TraficLights",
+ XML.h.cfvo(type="percent", val="0"),
+ XML.h.cfvo(type="percent", val="25"),
+ XML.h.cfvo(type="percent", val="50"),
+ XML.h.cfvo(type="percent", val="75"),
+ )
+ ),
+ "3Symbols" => XML.h.cfRule(type="iconSet", priority="1",
+ XML.h.iconSet(iconSet="3Symbols",
+ XML.h.cfvo(type="percent", val="0"),
+ XML.h.cfvo(type="percent", val="33"),
+ XML.h.cfvo(type="percent", val="67"),
+ )
+ ),
+ "3Symbols2" => XML.h.cfRule(type="iconSet", priority="1",
+ XML.h.iconSet(iconSet="3Symbols2",
+ XML.h.cfvo(type="percent", val="0"),
+ XML.h.cfvo(type="percent", val="33"),
+ XML.h.cfvo(type="percent", val="67"),
+ )
+ ),
+ "3Flags" => XML.h.cfRule(type="iconSet", priority="1",
+ XML.h.iconSet(iconSet="3Flags",
+ XML.h.cfvo(type="percent", val="0"),
+ XML.h.cfvo(type="percent", val="33"),
+ XML.h.cfvo(type="percent", val="67"),
+ )
+ ),
+ "5Quarters" => XML.h.cfRule(type="iconSet", priority="1",
+ XML.h.iconSet(iconSet="5Quarters",
+ XML.h.cfvo(type="percent", val="0"),
+ XML.h.cfvo(type="percent", val="20"),
+ XML.h.cfvo(type="percent", val="40"),
+ XML.h.cfvo(type="percent", val="60"),
+ XML.h.cfvo(type="percent", val="80"),
+ )
+ ),
+ "4Rating" => XML.h.cfRule(type="iconSet", priority="1",
+ XML.h.iconSet(iconSet="4Rating",
+ XML.h.cfvo(type="percent", val="0"),
+ XML.h.cfvo(type="percent", val="25"),
+ XML.h.cfvo(type="percent", val="50"),
+ XML.h.cfvo(type="percent", val="75"),
+ )
+ ),
+ "5Rating" => XML.h.cfRule(type="iconSet", priority="1",
+ XML.h.iconSet(iconSet="5Rating",
+ XML.h.cfvo(type="percent", val="0"),
+ XML.h.cfvo(type="percent", val="20"),
+ XML.h.cfvo(type="percent", val="40"),
+ XML.h.cfvo(type="percent", val="60"),
+ XML.h.cfvo(type="percent", val="80"),
+ )
+ ),
+ # These three require Excel 2010 extensions and will be ignored by earlier versions of Excel.
+ "3Triangles" => get_x14_icon("3Triangles"),
+ "3Stars" => get_x14_icon("3Stars"),
+ "5Boxes" => get_x14_icon("5Boxes"),
+ "Custom" => get_x14_icon("Custom")
+)
+const allIcons::Dict{String,Tuple{String,String}} = Dict(
+ "1" => ("3Arrows", "0"),
+ "2" => ("3Arrows", "1"),
+ "3" => ("3Arrows", "2"),
+ "4" => ("4Arrows", "1"),
+ "5" => ("4Arrows", "2"),
+ "6" => ("3ArrowsGray", "0"),
+ "7" => ("3ArrowsGray", "1"),
+ "8" => ("3ArrowsGray", "2"),
+ "9" => ("4ArrowsGray", "1"),
+ "10" => ("4ArrowsGray", "2"),
+ "11" => ("3Flags", "0"),
+ "12" => ("3Flags", "1"),
+ "13" => ("3Flags", "2"),
+ "14" => ("3TrafficLights1", "0"),
+ "15" => ("3TrafficLights1", "1"),
+ "16" => ("3TrafficLights1", "2"),
+ "17" => ("3TrafficLights2", "0"),
+ "18" => ("3TrafficLights2", "1"),
+ "19" => ("3TrafficLights2", "2"),
+ "20" => ("4TrafficLights", "0"),
+ "21" => ("3Signs", "0"),
+ "22" => ("3Signs", "1"),
+ "23" => ("3Symbols", "0"),
+ "24" => ("3Symbols", "1"),
+ "25" => ("3Symbols", "2"),
+ "26" => ("3Symbols2", "0"),
+ "27" => ("3Symbols2", "1"),
+ "28" => ("3Symbols2", "2"),
+ "29" => ("4RedToBlack", "0"),
+ "30" => ("4RedToBlack", "1"),
+ "31" => ("4RedToBlack", "2"),
+ "32" => ("4RedToBlack", "3"),
+ "33" => ("5Quarters", "0"),
+ "34" => ("5Quarters", "1"),
+ "35" => ("5Quarters", "2"),
+ "36" => ("5Quarters", "3"),
+ "37" => ("5Rating", "0"),
+ "38" => ("5Rating", "1"),
+ "39" => ("5Rating", "2"),
+ "40" => ("5Rating", "3"),
+ "41" => ("5Rating", "4"),
+ "42" => ("3Stars", "0"),
+ "43" => ("3Stars", "1"),
+ "44" => ("3Stars", "2"),
+ "45" => ("3Triangles", "0"),
+ "46" => ("3Triangles", "1"),
+ "47" => ("3Triangles", "2"),
+ "48" => ("5Boxes", "0"),
+ "49" => ("5Boxes", "1"),
+ "50" => ("5Boxes", "2"),
+ "51" => ("5Boxes", "3"),
+ "52" => ("5Boxes", "4")
+)
+const timeperiods::Dict{String,String} = Dict(
+ "last7Days" => "AND(TODAY()-FLOOR(__CR__,1)<=6,FLOOR(__CR__,1)<=TODAY())",
+ "yesterday" => "FLOOR(__CR__,1)=TODAY()-1",
+ "today" => "FLOOR(__CR__,1)=TODAY()",
+ "tomorrow" => "FLOOR(__CR__,1)=TODAY()+1",
+ "lastWeek" => "AND(TODAY()-ROUNDDOWN(__CR__,0)>=(WEEKDAY(TODAY())),TODAY()-ROUNDDOWN(__CR__,0)<(WEEKDAY(TODAY())+7))",
+ "thisWeek" => "AND(TODAY()-ROUNDDOWN(__CR__,0)<=WEEKDAY(TODAY())-1,ROUNDDOWN(__CR__,0)-TODAY()<=7-WEEKDAY(TODAY()))",
+ "nextWeek" => "AND(ROUNDDOWN(__CR__,0)-TODAY()>(7-WEEKDAY(TODAY())),ROUNDDOWN(__CR__,0)-TODAY()<(15-WEEKDAY(TODAY())))",
+ "lastMonth" => "AND(MONTH(__CR__)=MONTH(EDATE(TODAY(),0-1)),YEAR(__CR__)=YEAR(EDATE(TODAY(),0-1)))",
+ "thisMonth" => "AND(MONTH(__CR__)=MONTH(TODAY()),YEAR(__CR__)=YEAR(TODAY()))",
+ "nextMonth" => "AND(MONTH(__CR__)=MONTH(EDATE(TODAY(),0+1)),YEAR(__CR__)=YEAR(EDATE(TODAY(),0+1)))"
+)
+
+
+"""
+ getConditionalFormats(ws::Worksheet)
+
+Get the conditional formats for a worksheet.
+
+# Arguments
+- `ws::Worksheet`: The worksheet for which to get the conditional formats.
+
+Return a vector of pairs: CellRange => NamedTuple{type::String, priority::Int}}.
+
+
+"""
+getConditionalFormats(ws::Worksheet) = append!(getConditionalFormats(allCfs(ws)), getConditionalExtFormats(allExtCfs(ws)))
+function getConditionalFormats(allcfnodes::Vector{XML.Node})::Vector{Pair{CellRange,NamedTuple{(:type, :priority),Tuple{String,Int64}}}}
+ allcfs = Vector{Pair{CellRange,NamedTuple{(:type, :priority),Tuple{String,Int64}}}}()
+ for cf in allcfnodes
+ for child in XML.children(cf)
+ if XML.tag(child) == "cfRule"
+ push!(allcfs, CellRange(cf["sqref"]) => (type=child["type"], priority=parse(Int, child["priority"])))
+ end
+ end
+ end
+ return allcfs
+end
+function getConditionalExtFormats(allcfnodes::Vector{XML.Node})::Vector{Pair{CellRange,NamedTuple{(:type, :priority),Tuple{String,Int64}}}}
+ allcfs = Vector{Pair{CellRange,NamedTuple{(:type, :priority),Tuple{String,Int64}}}}()
+ for cf in allcfnodes
+ let t, p, r, rule = false, ref = false
+ @assert XML.tag(cf) == "x14:conditionalFormatting" "Something wrong here"
+ sqref = cf[end]
+ if XML.tag(sqref) == "xm:sqref"
+ r = XML.simple_value(sqref)
+ ref = true
+ end
+ for child in XML.children(cf)
+ if XML.tag(child) == "x14:cfRule"
+ t = child["type"]
+ if t != "dataBar" # This is the other half of a dataBar definition - don't count twice!
+ p = parse(Int, child["priority"])
+ rule = true
+ end
+ end
+ if rule && ref
+ push!(allcfs, CellRange(r) => (type=t, priority=p))
+ rule = false
+ end
+ end
+ end
+ end
+ return allcfs
+end
+
+"""
+ setConditionalFormat(ws::Worksheet, cr::String, type::Symbol; kw...) -> ::Int
+ setConditionalFormat(xf::XLSXFile, cr::String, type::Symbol; kw...) -> ::Int
+
+ setConditionalFormat(ws::Worksheet, rows, cols, type::Symbol; kw...) -> ::Int
+
+Add a new conditional format to a cell range, row range or column range in a
+worksheet or `XLSXFile`. Alternatively, ranges can be specified by giving rows
+and columns separately.
+
+There are many options for applying differnt types of custom format. For a basic guide,
+refer to the section on [Conditional formats](@ref) in the Formatting Guide.
+
+The `type` argument specifies which of Excel's conditional format types will be applied.
+
+Valid options for `type` are:
+- `:cellIs`
+- `:top10`
+- `:aboveAverage`
+- `:containsText`
+- `:notContainsText`
+- `:beginsWith`
+- `:endsWith`
+- `:timePeriod`
+- `:containsErrors`
+- `:notContainsErrors`
+- `:containsBlanks`
+- `:notContainsBlanks`
+- `:uniqueValues`
+- `:duplicateValues`
+- `:dataBar`
+- `:colorScale`
+- `:iconSet`
+
+Keyword options differ according to the `type` specified, as set out below.
+
+# type = :cellIs
+
+Defines a conditional format based on the value of each cell in a range.
+
+Valid keywords are:
+- `operator` : Defines the comparison to make.
+- `value` : defines the first value to compare against. This can be a cell reference (e.g. `"A1"`) or a number.
+- `value2` : defines the second value to compare against. This can be a cell reference (e.g. `"A1"`) or a number.
+- `stopIfTrue` : Stops evaluating the conditional formats for this cell if this one is true.
+- `dxStyle` : Used optionally to select one of the built-in Excel formats to apply
+- `format` : defines the numFmt to apply if opting for a custom format.
+- `font` : defines the font to apply if opting for a custom format.
+- `border` : defines the border to apply if opting for a custom format.
+- `fill` : defines the fill to apply if opting for a custom format.
+
+All keywords are defined using Strings (e.g. `value = "2"` or `value = "A2"`).
+
+The keyword `operator` defines the comparison to use in the conditional formatting.
+If the condition is met, the format is applied.
+Valid options are:
+
+- `greaterThan` (cell > `value`) (default)
+- `greaterEqual` (cell >= `value`)
+- `lessThan` (cell < `value`)
+- `lessEqual` (cell <= `value`)
+- `equal` (cell == `value`)
+- `notEqual` (cell != `value`)
+- `between` (cell between `value` and `value2`)
+- `notBetween` (cell not between `value` and `value2`)
+
+If not specified (when required), `value` will be the arithmetic average of the
+(non-missing) cell values in the range if values are numeric. If the cell values
+are non-numeric, an error is thrown.
+
+Formatting to be applied if the condition is met can be defined in one of two ways.
+Use the keyword `dxStyle` to select one of the built-in Excel formats.
+Valid options are:
+
+- `redfilltext` (light red fill, dark red text) (default)
+- `yellowfilltext` (light yellow fill, dark yellow text)
+- `greenfilltext` (light green fill, dark green text)
+- `redfill` (light red fill)
+- `redtext` (dark red text)
+- `redborder` (dark red cell borders)
+
+Alternatively, you can define a custom format by using the keywords `format`, `font`,
+`border`, and `fill` which each take a vector of pairs of strings. The first string
+is the name of the attribute to set and the second is the value to set it to.
+Valid attributes for each keyword are:
+
+- `format` : `format`
+- `font` : `color`, `bold`, `italic`, `under`, `strike`
+- `fill` : `pattern`, `bgColor`, `fgColor`
+- `border` : `style`, `color`
+
+Refer to [`setFormat()`](@ref), [`setFont()`](@ref), [`setFill()`](@ref) and [`setBorder()`](@ref) for
+more details on the valid attributes and values.
+
+!!! note
+
+ Excel limits the formatting attributes that can be set in a conditional format.
+ It is not possible to set the size or name of a font and neither is it possible to set
+ any of the cell alignment attributes. Diagonal borders cannot be set either.
+
+ Although it is not a limitation of Excel, this function sets all the border attributes
+ for each side of a cell to be the same.
+
+If both `dxStyle` and custom formatting keywords are specified, `dxStyle` will be used
+and the custom formatting will be ignored.
+If neither `dxStyle` nor custom formatting keywords are specified, the default
+is `dxStyle="redfilltext"`.
+
+# Examples
+
+```julia
+julia> XLSX.setConditionalFormat(s, "B1:B5", :cellIs) # Defaults to `operator="greaterThan"`, `dxStyle="redfilltext"` and `value` set to the arithmetic agverage of cell values in `rng`.
+
+julia> XLSX.setConditionalFormat(s, "B1:B5", :cellIs;
+ operator="between",
+ value="2",
+ value2="3",
+ fill = ["pattern" => "none", "bgColor"=>"FFFFC7CE"],
+ format = ["format"=>"0.00%"],
+ font = ["color"=>"blue", "bold"=>"true"]
+ )
+
+julia> XLSX.setConditionalFormat(s, "B1:B5", :cellIs;
+ operator="greaterThan",
+ value="4",
+ fill = ["pattern" => "none", "bgColor"=>"green"],
+ format = ["format"=>"0.0"],
+ font = ["color"=>"red", "italic"=>"true"]
+ )
+
+julia> XLSX.setConditionalFormat(s, "B1:B5", :cellIs;
+ operator="lessThan",
+ value="2",
+ fill = ["pattern" => "none", "bgColor"=>"yellow"],
+ format = ["format"=>"0.0"],
+ font = ["color"=>"green"],
+ border = ["style"=>"thick", "color"=>"coral"]
+ )
+
+```
+
+# type = :top10
+
+This conditional format can be used to highlight cells in the top (bottom) n within the
+range or in the top (bottom) n% (ie in the top 5 or in the top 5% of values in the range).
+
+The available keywords are:
+
+- `operator` : Defines the comparison to make.
+- `value` : Gives the for comparison or a cell reference (e.g. `"A1"`).
+- `stopIfTrue` : Stops evaluating the conditional formats if this one is true.
+- `dxStyle` : Used optionally to select one of the built-in Excel formats to apply.
+- `format` : defines the numFmt to apply if opting for a custom format.
+- `font` : defines the font to apply if opting for a custom format.
+- `border` : defines the border to apply if opting for a custom format.
+- `fill` : defines the fill to apply if opting for a custom format.
+
+Valid values for the `operator` keyword are the following:
+
+- `topN` (cell is in the top n (= `value`) values of the range)
+- `bottomN` (cell is in the bottom n (= `value`) values of the range)
+- `topN%` (cell is in the top n% (= `value`) values of the range)
+- `bottomN%` (cell is in the bottom n% (= `value`) values of the range)
+
+Default keyowrds are `operator="TopN"` and `value="10"`.
+
+Multiple conditional formats may be applied to the smae or overlapping cell ranges.
+If `stopIfTrue=true` the first condition that is met will be applied but all subsequent
+conditional formats for that cell will be skipped. If `stopIfTrue=false` (default) all
+relevant conditional formats will be applied to the cell in turn.
+
+For example usage of the `stopIfTrue` keyword, refer to [Overlaying conditional formats](@ref)
+in the Formatting Guide.
+
+The remaining keywords are defined as above for `type = :cellIs`.
+
+# Examples
+
+```julia
+julia> f=XLSX.newxlsx()
+XLSXFile("C:\\...\\blank.xlsx") containing 1 Worksheet
+ sheetname size range
+-------------------------------------------------
+ Sheet1 1x1 A1:A1
+
+
+julia> s=f[1]
+1×1 XLSX.Worksheet: ["Sheet1"](A1:A1)
+
+julia> for i=1:10;for j=1:10; s[i,j]=i*j;end;end
+
+julia> s[:]
+10×10 Matrix{Any}:
+ 1 2 3 4 5 6 7 8 9 10
+ 2 4 6 8 10 12 14 16 18 20
+ 3 6 9 12 15 18 21 24 27 30
+ 4 8 12 16 20 24 28 32 36 40
+ 5 10 15 20 25 30 35 40 45 50
+ 6 12 18 24 30 36 42 48 54 60
+ 7 14 21 28 35 42 49 56 63 70
+ 8 16 24 32 40 48 56 64 72 80
+ 9 18 27 36 45 54 63 72 81 90
+ 10 20 30 40 50 60 70 80 90 100
+
+julia> XLSX.setConditionalFormat(s, "A1:J10", :top10; operator="bottomN", value="1", stopIfTrue="true", dxStyle="redfilltext")
+0
+
+julia> XLSX.setConditionalFormat(s, "A1:J10", :top10; operator="topN", value="1", stopIfTrue="true", dxStyle="greenfilltext")
+0
+
+julia> XLSX.setConditionalFormat(s, "A1:J10", :top10;
+ operator="topN%",
+ value="20",
+ fill=["pattern"=>"solid", "bgColor"=>"cyan"])
+0
+
+julia> XLSX.setConditionalFormat(s, "A1:J10", :top10;
+ operator="bottomN%",
+ value="20",
+ fill=["pattern"=>"solid", "bgColor"=>"yellow"])
+0
+
+```
+
+
+# type = :aboveAverage
+
+This conditional format can be used to compare cell values in the range with the
+average value for the range.
+
+The available keywords are:
+
+- `operator` : Defines the comparison to make.
+- `stopIfTrue` : Stops evaluating the conditional formats if this one is true.
+- `dxStyle` : Used optionally to select one of the built-in Excel formats to apply.
+- `format` : defines the numFmt to apply if opting for a custom format.
+- `font` : defines the font to apply if opting for a custom format.
+- `border` : defines the border to apply if opting for a custom format.
+- `fill` : defines the fill to apply if opting for a custom format.
+
+Valid values for the `operator` keyword are the following:
+
+- `aboveAverage` (cell is above the average of the range) (default)
+- `aboveEqAverage` (cell is above or equal to the average of the range)
+- `plus1StdDev` (cell is above the average of the range + 1 standard deviation)
+- `plus2StdDev` (cell is above the average of the range + 2 standard deviations)
+- `plus3StdDev` (cell is above the average of the range + 3 standard deviations)
+- `belowAverage` (cell is below the average of the range)
+- `belowEqAverage` (cell is below or equal to the average of the range)
+- `minus1StdDev` (cell is below the average of the range - 1 standard deviation)
+- `minus2StdDev` (cell is below the average of the range - 2 standard deviations)
+- `minus3StdDev` (cell is below the average of the range - 3 standard deviations)
+
+The remaining keywords are defined as above for `type = :cellIs`.
+
+# Examples
+
+```julia
+julia> using Random, Distributions
+
+julia> d=Normal()
+Normal{Float64}(μ=0.0, σ=1.0)
+
+julia> columns=rand(d,1000)
+1000-element Vector{Float64}:
+-1.5515478694605092
+ 0.36859583733587165
+ 1.5349535865662158
+ -0.2352610551087202
+ 0.12355875388105911
+ 0.5859222303845908
+ -0.6326662651426166
+ 1.0610118292961683
+ -0.7891578831398097
+ 0.031022172414689787
+ -0.5534440118018843
+ -2.3538883599955023
+ ⋮
+ 0.4813001892130465
+ 0.03871017417416217
+ 0.7224728281160403
+ -1.1265372949908539
+ 1.5714393857211955
+ 0.31438739499933255
+ 0.4852591013082452
+ 0.5363388236349432
+ 1.1268430910133729
+ 0.7691442442244849
+ 1.0061732938516454
+
+julia> f=XLSX.newxlsx()
+XLSXFile("C:\\...\\blank.xlsx") containing 1 Worksheet
+ sheetname size range
+-------------------------------------------------
+ Sheet1 1x1 A1:A1
+
+
+julia> s=f[1]
+1×1 XLSX.Worksheet: ["Sheet1"](A1:A1)
+
+julia> XLSX.writetable!(s, [columns], ["normal"])
+
+julia> XLSX.setConditionalFormat(s, "A2:A1001", :aboveAverage ;
+ operator="plus3StdDev",
+ stopIfTrue="true",
+ fill = ["pattern"=>"solid", "bgColor"=>"red"],
+ font = ["color"=>"white", "bold"=>"true"])
+0
+
+julia> XLSX.setConditionalFormat(s, "A2:A1001", :aboveAverage ;
+ operator="minus3StdDev",
+ stopIfTrue="true",
+ fill = ["pattern"=>"solid", "bgColor"=>"red"],
+ font = ["color"=>"white", "bold"=>"true"])
+0
+
+julia> XLSX.setConditionalFormat(s, "A2:A1001", :aboveAverage ;
+ operator="plus2StdDev",
+ stopIfTrue="true",
+ fill = ["pattern"=>"solid", "bgColor"=>"tomato"],
+ font = ["color"=>"white", "bold"=>"true"])
+0
+
+julia> XLSX.setConditionalFormat(s, "A2:A1001", :aboveAverage ;
+ operator="minus2StdDev",
+ stopIfTrue="true",
+ fill = ["pattern"=>"solid", "bgColor"=>"tomato"],
+ font = ["color"=>"white", "bold"=>"true"])
+0
+
+julia> XLSX.setConditionalFormat(s, "A2:A1001", :aboveAverage ;
+ operator="minus1StdDev",
+ stopIfTrue="true",
+ fill = ["pattern"=>"solid", "bgColor"=>"pink"],
+ font = ["color"=>"white", "bold"=>"true"])
+0
+
+julia> XLSX.setConditionalFormat(s, "A2:A1001", :aboveAverage ;
+ operator="plus1StdDev",
+ stopIfTrue = "true",
+ fill = ["pattern"=>"solid", "bgColor"=>"pink"],
+ font = ["color"=>"white", "bold"=>"true"])
+0
+
+julia> XLSX.setConditionalFormat(s, "A2:A1001", :aboveAverage ;
+ operator="belowEqAverage",
+ fill = ["pattern"=>"solid", "bgColor"=>"green"],
+ font = ["color"=>"white", "bold"=>"true"])
+0
+
+julia> XLSX.setConditionalFormat(s, "A2:A1001", :aboveAverage ;
+ operator="aboveEqAverage",
+ fill = ["pattern"=>"solid", "bgColor"=>"green"],
+ font = ["color"=>"white", "bold"=>"true"])
+0
+
+```
+
+# type = :containsText, :notContainsText, :beginsWith or :endsWith
+
+Highlight cells in the range that contain (or do not contain), begin or end with
+a specific text string. The default is `containsText`.
+
+Valid keywords are:
+
+- `value` : Gives the literal text to match or provides a cell reference (e.g. `"A1"`).
+- `stopIfTrue` : Stops evaluating the conditional formats if this one is true.
+- `dxStyle` : Used optionally to select one of the built-in Excel formats to apply.
+- `format` : defines the numFmt to apply if opting for a custom format.
+- `font` : defines the font to apply if opting for a custom format.
+- `border` : defines the border to apply if opting for a custom format.
+- `fill` : defines the fill to apply if opting for a custom format.
+
+The keyword `value` gives the literal text to compare (eg. "Hello World") or provides a cell reference
+(e.g. `"A1"`). It is a required keyword with no default value.
+
+The remaining keywords are optional and are defined as above for `type = :cellIs`.
+
+# Examples
+
+```julia
+julia> s[:]
+4×1 Matrix{Any}:
+ "Hello World"
+ "Life the universe and everything"
+ "Once upon a time"
+ "In America"
+
+julia> XLSX.setConditionalFormat(s, "A1:A4", :containsText;
+ value="th",
+ fill = ["pattern"=>"solid", "bgColor"=>"cyan"],
+ font = ["color"=>"black", "bold"=>"true"])
+0
+
+julia> XLSX.setConditionalFormat(s, "A1:A4", :notContainsText;
+ value="i",
+ fill = ["pattern"=>"solid", "bgColor"=>"green"],
+ font = ["color"=>"white", "bold"=>"true"])
+0
+
+julia> XLSX.setConditionalFormat(s, "A1:A4", :beginsWith ;
+ value="On",
+ fill = ["pattern"=>"solid", "bgColor"=>"red"],
+ font = ["color"=>"white", "bold"=>"true"])
+0
+
+julia> XLSX.setConditionalFormat(s, "A1:A4", :endsWith ;
+ value="ica",
+ fill = ["pattern"=>"solid", "bgColor"=>"blue"],
+ font = ["color"=>"white", "bold"=>"true"])
+0
+
+```
+
+
+# type = :timePeriod
+
+When cells contain dates, this conditional format can be used to highlight cells.
+The available keywords are:
+
+- `operator` : Defines the comparison to make.
+- `stopIfTrue` : Stops evaluating the conditional formats if this one is true.
+- `dxStyle` : Used optionally to select one of the built-in Excel formats to apply
+- `format` : defines the numFmt to apply if opting for a custom format.
+- `font` : defines the font to apply if opting for a custom format.
+- `border` : defines the border to apply if opting for a custom format.
+- `fill` : defines the fill to apply if opting for a custom format.
+
+Valid values for the keyword `operator` are the following:
+
+- `yesterday`
+- `today`
+- `tomorrow`
+- `last7Days` (default)
+- `lastWeek`
+- `thisWeek`
+- `nextWeek`
+- `lastMonth`
+- `thisMonth`
+- `nextMonth`
+
+The remaining keywords are defined as above for `type = :cellIs`.
+
+# Examples
+
+```julia
+julia> s[1:13, 1]
+13×1 Matrix{Any}:
+ "Dates"
+ 2024-11-20
+ 2024-12-20
+ 2025-01-08
+ 2025-02-08
+ 2025-03-08
+ 2025-04-08
+ 2025-05-08
+ 2025-05-09
+ 2025-05-10
+ 2025-05-14
+ 2025-06-08
+ 2025-07-08
+
+julia> XLSX.setConditionalFormat(s, "A1:A13", :timePeriod; operator="today", dxStyle = "greenfilltext")
+0
+
+julia> XLSX.setConditionalFormat(s, "A1:A13", :timePeriod; operator="tomorrow", dxStyle = "yellowfilltext")
+0
+
+julia> XLSX.setConditionalFormat(s, "A1:A13", :timePeriod; operator="nextMonth", dxStyle = "redfilltext")
+0
+
+julia> XLSX.setConditionalFormat(s, "A1:A13", :timePeriod;
+ operator="lastMonth",
+ fill = ["pattern"=>"solid", "bgColor"=>"blue"],
+ font = ["color"=>"yellow", "bold"=>"true"])
+0
+
+```
+
+
+
+# type = :containsErrors, :notContainsErrors, :containsBlanks, :notContainsBlanks, :uniqueValues or :duplicateValues
+
+These conditional formatting options highlight cells that contain or don't contain errors,
+are blank (default) or not blank, are unique in the range or are duplicates within the range.
+The available keywords are:
+
+- `stopIfTrue` : Stops evaluating the conditional formats if this one is true.
+- `dxStyle` : Used optionally to select one of the built-in Excel formats to apply
+- `format` : defines the numFmt to apply if opting for a custom format.
+- `font` : defines the font to apply if opting for a custom format.
+- `border` : defines the border to apply if opting for a custom format.
+- `fill` : defines the fill to apply if opting for a custom format.
+
+These keywords are defined as above for the `:cellIs` conditional format type.
+
+# Examples
+
+```julia
+julia> XLSX.setConditionalFormat(s, "A1:A7", :containsErrors;
+ stopIfTrue="true",
+ fill = ["pattern"=>"solid", "bgColor"=>"blue"],
+ font = ["color"=>"white", "bold"=>"true"])
+0
+
+julia> XLSX.setConditionalFormat(s, "A1:A7", :containsBlanks;
+ stopIfTrue="true",
+ fill = ["pattern"=>"solid", "bgColor"=>"green"],
+ font = ["color"=>"black", "bold"=>"true"])
+0
+
+julia> XLSX.setConditionalFormat(s, "A1:A7", :uniqueValues;
+ stopIfTrue="true",
+ fill = ["pattern"=>"solid", "bgColor"=>"yellow"],
+ font = ["color"=>"black", "bold"=>"true"])
+0
+
+julia> XLSX.setConditionalFormat(s, "A1:A7", :duplicateValues;
+ fill = ["pattern"=>"solid", "bgColor"=>"cyan"],
+ font = ["color"=>"black", "bold"=>"true"])
+0
+
+```
+
+
+# type = :expressiom
+
+Set a conditional format when an expression evaluated in each cell is `true`.
+
+The available keywords are:
+
+- `formula` : Specifies the formula to use. This must be a valid Excel formula.
+- `stopIfTrue` : Stops evaluating the conditional formats if this one is true.
+- `dxStyle` : Used optionally to select one of the built-in Excel formats to apply
+- `format` : defines the numFmt to apply if opting for a custom format.
+- `font` : defines the font to apply if opting for a custom format.
+- `border` : defines the border to apply if opting for a custom format.
+- `fill` : defines the fill to apply if opting for a custom format.
+
+The keyword `formula` is required and there is no default value. Formulae must be valid
+Excel formulae and written in US english with comma separators. Cell references may be
+absolute or relative references in either the row or the column or both.
+
+The remaining keywords are defined as above for `type = :cellIs`.
+
+# Examples
+
+```julia
+julia> XLSX.setConditionalFormat(s, "A1:C4", :expression; formula = "A1 < 16", dxStyle="greenfilltext")
+
+julia> XLSX.setConditionalFormat(s, 1:5, 1:4, :expression;
+ formula="A1=1",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "thick", "color" => "coral"]
+ )
+
+julia> XLSX.setConditionalFormat(s, "B2:D11", :expression; formula = "average(B\$2:B\$11) > average(A\$2:A\$11)", dxStyle = "greenfilltext")
+
+julia> XLSX.setConditionalFormat(s, "A1:E5", :expression; formula = "E5<50", dxStyle = "redfilltext")
+
+```
+
+# type = :dataBar
+
+Apply data bars to cells in a range depending on their values. The keyword `databar`
+can be used to select one of 12 built-in databars Excel provides by name. Valid names are:
+- `bluegrad` (default)
+- `greengrad`
+- `redgrad`
+- `orangegrad`
+- `lightbluegrad`
+- `purplegrad`
+- `blue`
+- `green`
+- `red`
+- `orange`
+- `lightblue`
+- `purple`
+
+The first six (with a `grad` suffix) yield bars with a color gradient while the remainder
+yield bars of solid color. By default, all built in data bars define their range from the
+minumum and maximum values in the range and negative values are given a red bar. These default
+settings can each be modified using the other keyword options available.
+
+Remaining keyword options provided are:
+- `showVal` - set to "false" to show databars only and hide cell values
+- `gradient` - set to "false" to use a solid color bar rather than a gradient fill
+- `borders` - set to "true" to show borders around each bar
+- `sameNegFill` - set to "true" to use the same fill color on negative bars as positive.
+- `sameNegBorders` - set to "false" to use the same border color on negative bars as positive
+- `direction` - determines the direction of the bars from the axis, "leftToRight" or "rightToLeft"
+- `min_type` - Defines how the minimum of the bar scale is defined ("num", "min", "percent", percentile", "formula" or "automatic")
+- `min_val` - Defines the minimum value for the data bar scale. May be a number(as a string), a cell reference or a formula (if type="formula").
+- `max_type` - Defines how the maximum of the bar scale is defined ("num", "max", "percent", percentile", "formula" or "automatic")
+- `max_val` - Defines the maximum value for the data bar scale. May be a number(as a string), a cell reference or a formula (if type="formula").
+- `fill_col` - Defines the color of the fill for positive bars (8 digit hex or by name)
+- `border_col` - Defines the color of the border for positive bars (8 digit hex or by name)
+- `neg_fill_col` - Defines the color of the fill for negative bars (8 digit hex or by name)
+- `neg_border_col` - Defines the color of the border for negative bars (8 digit hex or by name)
+- `axis_pos` - Defines the position of the axis ("middle" or "none")
+- `axis_col` - Defines the color of the axis (8 digit hex or by name)
+
+# Examples
+```julia
+julia> XLSX.setConditionalFormat(s, "A1:A11", :dataBar)
+
+julia> XLSX.setConditionalFormat(s, "B1:B11", :dataBar; databar="purple")
+
+julia> XLSX.setConditionalFormat(s, "D1:D11", :dataBar;
+ gradient="true",
+ direction="rightToLeft",
+ axis_pos="none",
+ showVal="false"
+ )
+
+jjulia> XLSX.setConditionalFormat(s, "F1:F11", :dataBar;
+ gradient="false",
+ sameNegFill="true",
+ sameNegBorders="true"
+ )
+
+julia> XLSX.setConditionalFormat(f, "Sheet1!G1:G11", :dataBar;
+ fill_col="coral", border_col = "cyan",
+ neg_fill_col="cyan", neg_border_col = "coral"
+ )
+
+julia> XLSX.setConditionalFormat(f, "Sheet1!J1:J11", :dataBar; axis_col="magenta")
+
+julia> XLSX.setConditionalFormat(s, 15:25, 1, :dataBar;
+ min_type="least", max_type="highest"
+ )
+
+julia> XLSX.setConditionalFormat(s, 15:25, 2, :dataBar;
+ databar="purple",
+ min_type="percent", max_type="percent",
+ min_val="20", max_val="60"
+ )
+
+julia> XLSX.setConditionalFormat(s, "C15:C25", :dataBar;
+ databar="blue",
+ min_type="num", max_type="num",
+ min_val="-1", max_val="6",
+ gradient="true",
+ direction="leftToRight",
+ axis_pos="none"
+ )
+
+julia> XLSX.setConditionalFormat(s, "E15:E25", :dataBar;
+ gradient="true",
+ min_type="percentile", max_type="percentile",
+ min_val="20", max_val="80",
+ direction="rightToLeft",
+ axis_pos="middle"
+ )
+
+julia> XLSX.setConditionalFormat(s, "G15:G25", :dataBar;
+ min_type="num", max_type="formula",
+ min_val="\$L\$1", max_val="\$M\$1 * \$N\$1 + 3",
+ fill_col="coral", border_col = "cyan",
+ neg_fill_col="cyan", neg_border_col = "coral"
+ )
+
+```
+
+# type = :colorScale
+
+Define a 2-color or 3-color colorscale conditional format.
+
+Use the keyword `colorscale` to choose one of the 12 built-in Excel colorscales:
+
+- `"redyellowgreen"`: Red, Yellow, Green 3-color scale.
+- `"greenyellowred"`: Green, Yellow, Red 3-color scale.
+- `"redwhitegreen"` : Red, White, Green 3-color scale.
+- `"greenwhitered"` : Green, White, Red 3-color scale.
+- `"redwhiteblue"` : Red, White, Blue 3-color scale.
+- `"bluewhitered"` : Blue, White, Red 3-color scale.
+- `"redwhite"` : Red, White 2-color scale.
+- `"whitered"` : White, Red 2-color scale.
+- `"whitegreen"` : White, Green 2-color scale.
+- `"greenwhite"` : Green, White 2-color scale.
+- `"yellowgreen"` : Yellow, Green 2-color scale.
+- `"greenyellow"` : Green, Yellow 2-color scale (default).
+
+Alternatively, you can define a custom color scale by omitting the `colorscale` keyword and
+instead using the following keywords:
+
+- `min_type`: The type of the minimum value. Valid values are: `min`, `percentile`, `percent`, `num` or `formula`.
+- `min_val` : The value of the minimum. Omit if `min_type="min"`.
+- `min_col` : The color of the minimum value.
+- `mid_type`: Valid values are: `percentile`, `percent`, `num` or `formula`. Omit for a 2-color scale.
+- `mid_val` : The value of the scale mid point. Omit for a 2-color scale.
+- `mid_col` : The color of the mid point. Omit for a 2-color scale.
+- `max_type`: The type of the maximum value. Valid values are: `max`, `percentile`, `percent`, `num` or `formula`.
+- `max_val` : The value of the maximum value. Omit if `max_type="max"`.
+- `max_col` : The color of the maximum value.
+
+The keywords `min_val`, `mid_val`, and `max_val` can be a number or cell reference (e.g. `"\$A\$1"`) for any value
+of the related type keyword or, if the related type keyword is set to `formula`, may be a valid Excel formula that
+calculates a number. Cell references is used in a formula must be specified as absolute references.
+
+Colors can be specified using an 8-digit hex string (e.g. `FF0000FF` for blue) or any named
+color from [Colors.jl](https://juliagraphics.github.io/Colors.jl/stable/namedcolors/).
+
+# Examples
+
+```julia
+julia> XLSX.setConditionalFormat(f["Sheet1"], "A1:F12", :colorScale) # Defaults to the `greenyellow` built-in scale.
+0
+
+julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:C18", :colorScale; colorscale="whitered")
+0
+
+julia> XLSX.setConditionalFormat(f["Sheet1"], "D13:F18", :colorScale; colorscale="bluewhitered")
+0
+
+julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:F22", :colorScale;
+ min_type="num",
+ min_val="2",
+ min_col="tomato",
+ mid_type="num",
+ mid_val="6",
+ mid_col="lawngreen",
+ max_type="num",
+ max_val="10",
+ max_col="cadetblue"
+ )
+0
+
+```
+
+# type = :iconSet
+
+Apply a set of icons to cells in a range depending on their values. The keyword `iconset`
+can be used to select one of 20 built-in icon sets Excel provides by name. Valid names are:
+- `3Arrows`
+- `5ArrowsGray`
+- `3TrafficLights` (default)
+- `3Flags`
+- `5Quarters`
+- `4Rating`
+- `5Rating`
+- `3Symbols`
+- `3Symbols2`
+- `3Signs`
+- `3TrafficLights2`
+- `4TrafficLights`
+- `4BlackToRed`
+- `4Arrows`
+- `5Arrows`
+- `3ArrowsGray`
+- `4ArrowsGray`
+- `3Triangles`
+- `3Stars`
+- `5Boxes`
+
+The digit prefix to the name indicates how many icons there are in a set, and therefore
+how the cell values with be binned by value. Bin boundaries may optionally be specified
+by the following keywords to override the default values for each icon set:
+
+- `min_type` = "percent" (default), "percentile", "num" or "formula"
+- `min_val` (default: "33" (3 icons), "25" (4 icons) or "20" (5 icons))
+- `mid_type` = "percent" (default), "percentile", "num" or "formula"
+- `mid_val` (default: "50" (4 icons), "40" (5 icons))
+- `mid2_type` = "percent" (default), "percentile", "num" or "formula"
+- `mid2_val` (default: "60" (5 icons))
+- `max_type` = "percent" (default), "percentile", "num" or "formula"
+- `max_val` (default: "67" (3 icons), "75" (4 icons) or "80" (5 icons))
+
+The keywords `min_val`, `mid_val`, `mid2_val` and `max_val` may contain numbers (as strings)
+or valid cell references. If `formula` is specified for the related type keyword, a valid
+Excel formula can be provided to evaluate to the bin threshold value to be used.
+Three-icon sets require two thresholds (`min_type`/`min_val` and `max_type`/`max_val`),
+four-icon sets require three thresholds (with the addition of `mid_type`/`mid_val`) and
+five-icon sets require four thresholds (adding `mid2_type`/`mid2_val`). Thresholds defined
+(using val and type keywords) that are unnecessary are simply ignored.
+
+Each value can be tested using `>=` (default) or `>`. To change from the default,
+optionally set `min_gte`, `mid_gte`, `mid2_gte` and/or `max_gte` to `"false"` to
+use `>` in the comparison. Any other value for these gte keywords will be ignored
+and the default `>=` comparison used.
+
+The built-in icon sets Excel provides are composed of 52 individual icons. It is
+possible to mix and match any of these to make a custom 3-icon, 4-icon or 5-icon
+set by specifying `iconset = "Custom"`. The number of icons in the set will be
+determined by whether the `mid_val`/`mid_type` keywords and `mid2_val`/`mid2_type`
+keywords are provided.
+
+The icons that will be used in a `Custom` iconset are defined using the `icon_list`
+keyword which takes a vector of integers in the range from 1 to 52. For a key relating
+integers to the icons they represent, see the [Icon Set](@ref) section in the Formatting
+Guide.
+
+The order in which the symbols are appiled can be reversed from the default order (or, for
+`Custom` icon sets, the order given in `icon_list`), by optionally setting `reverse = "true"`.
+Any other value provided for `reverse` will be ignored, and the default order applied.
+
+The cell value can be suppressed, so that only the icon is shown in the Excel cell by
+optionally specifying `showVal = "false"`. Any other value provided for `showVal` will be
+ignored, and the cell value will be displayed with the icon.
+
+# Examples
+```julia
+XLSX.setConditionalFormat(s, "F2:F11", :iconSet; iconset="3Arrows")
+
+XLSX.setConditionalFormat(s, 2, :, :iconSet; iconset = "5Boxes",
+ reverse = "true",
+ showVal = "false",
+ min_type="num", mid_type="percentile", mid2_type="percentile", max_type="num",
+ min_val="3", mid_val="45", mid2_val="65", max_val="8",
+ min_gte="false", mid_gte="false", mid2_gte="false", max_gte="false")
+
+XLSX.setConditionalFormat(s, "A2:A11", :iconSet;
+ iconset = "Custom",
+ icon_list = [31,24],
+ min_type="num", max_type="formula",
+ min_val="3", max_val="if(\$G\$4=\"y\", \$G\$1+5, 10)")
+
+```
+
+"""
+function setConditionalFormat(f, r, type::Symbol; kw...)
+ _allkws = Dict{Symbol,Any}(k => v for (k, v) in kw)
+ if type == :colorScale
+ setCfColorScale(f, r; allkws=_allkws)
+ elseif type == :cellIs
+ setCfCellIs(f, r; allkws=_allkws)
+ elseif type == :top10
+ setCfTop10(f, r; allkws=_allkws)
+ elseif type == :aboveAverage
+ setCfAboveAverage(f, r; allkws=_allkws)
+ elseif type == :timePeriod
+ setCfTimePeriod(f, r; allkws=_allkws)
+ elseif type ∈ [:containsText, :notContainsText, :beginsWith, :endsWith]
+ setCfContainsText(f, r; allkws=_allkws)
+ elseif type ∈ [:containsBlanks, :notContainsBlanks, :containsErrors, :notContainsErrors, :duplicateValues, :uniqueValues]
+ push!(_allkws, :operator => string(type))
+ setCfContainsBlankErrorUniqDup(f, r; allkws=_allkws)
+ elseif type == :expression
+ setCfFormula(f, r; allkws=_allkws)
+ elseif type == :iconSet
+ setCfIconSet(f, r; allkws=_allkws)
+ elseif type == :dataBar
+ setCfDataBar(f, r; allkws=_allkws)
+ else
+ throw(XLSXError("Invalid conditional format type: $type."))
+ end
+end
+
+function setConditionalFormat(f, r, c, type::Symbol; kw...)
+ _allkws = Dict{Symbol,Any}(k => v for (k, v) in kw)
+ if type == :colorScale
+ setCfColorScale(f, r, c; allkws=_allkws)
+ elseif type == :cellIs
+ setCfCellIs(f, r, c; allkws=_allkws)
+ elseif type == :top10
+ setCfTop10(f, r, c; allkws=_allkws)
+ elseif type == :aboveAverage
+ setCfAboveAverage(f, r, c; allkws=_allkws)
+ elseif type == :timePeriod
+ setCfTimePeriod(f, r, c; allkws=_allkws)
+ elseif type ∈ [:containsText, :notContainsText, :beginsWith, :endsWith]
+ setCfContainsText(f, r, c; allkws=_allkws)
+ elseif type ∈ [:containsBlanks, :notContainsBlanks, :containsErrors, :notContainsErrors, :duplicateValues, :uniqueValues]
+ push!(_allkws, :operator => string(type))
+ setCfContainsBlankErrorUniqDup(f, r, c; allkws=_allkws)
+ elseif type == :expression
+ setCfFormula(f, r, c; allkws=_allkws)
+ elseif type == :iconSet
+ setCfIconSet(f, r, c; allkws=_allkws)
+ elseif type == :dataBar
+ setCfDataBar(f, r, c; allkws=_allkws)
+ else
+ throw(XLSXError("Invalid conditional format type: $type."))
+ end
+end
+
+setCfCellIs(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setCfCellIs, ws, row, nothing; kw...)
+setCfCellIs(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setCfCellIs, ws, nothing, col; kw...)
+setCfCellIs(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setCfCellIs, ws, nothing, nothing; kw...)
+setCfCellIs(ws::Worksheet, ::Colon; kw...) = process_colon(setCfCellIs, ws, nothing, nothing; kw...)
+setCfCellIs(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setCfCellIs(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
+setCfCellIs(ws::Worksheet, cell::CellRef; kw...) = setCfCellIs(ws, CellRange(cell, cell); kw...)
+setCfCellIs(ws::Worksheet, cell::SheetCellRef; kw...) = do_sheet_names_match(ws, cell) && setCfCellIs(ws, CellRange(cell.cellref, cell.cellref); kw...)
+setCfCellIs(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setCfCellIs(ws, rng.rng; kw...)
+setCfCellIs(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setCfCellIs(ws, rng.colrng; kw...)
+setCfCellIs(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setCfCellIs(ws, rng.rowrng; kw...)
+setCfCellIs(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCfCellIs, ws, rng; kw...)
+setCfCellIs(ws::Worksheet, rng::ColumnRange; kw...) = process_columnranges(setCfCellIs, ws, rng; kw...)
+setCfCellIs(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetcell(setCfCellIs, xl, sheetcell; kw...)
+setCfCellIs(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setCfCellIs, ws, ref_or_rng; kw...)
+function setCfCellIs(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}=())::Int
+
+ operator::Union{Nothing,String}="greaterThan"
+ value::Union{Nothing,String}=nothing
+ value2::Union{Nothing,String}=nothing
+ stopIfTrue::Union{Nothing,String}=nothing
+ dxStyle::Union{Nothing,String}=nothing
+ format::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ font::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ border::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ fill::Union{Nothing,Vector{Pair{String,String}}}=nothing
+
+ for (k, v) in allkws
+ if k == :operator
+ operator = v
+ elseif k == :value
+ value = v
+ elseif k == :value2
+ value2 = v
+ elseif k == :stopIfTrue
+ stopIfTrue = v
+ elseif k == :dxStyle
+ dxStyle = v
+ elseif k == :format
+ format = v
+ elseif k == :font
+ font = v
+ elseif k == :border
+ border = v
+ elseif k == :fill
+ fill = v
+ else
+ throw(XLSXError("Invalid keyword argument: $k. Valid keywords are: `operator`, `value`, `value2`, `stopIfTrue`, `dxStyle`, `format`, `font`, `border` and `fill`."))
+ end
+ end
+
+ !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension."))
+
+ allcfs = allCfs(ws) # get all conditional format blocks
+ old_cf = getConditionalFormats(ws) # extract conditional format info
+
+ !isnothing(value) && !is_valid_cellname(value) && !is_valid_fixed_cellname(value) && isnothing(tryparse(Float64, value)) && throw(XLSXError("Invalid `value`: $value. Must be a number or a CellRef."))
+ !isnothing(value2) && !is_valid_cellname(value2) && !is_valid_fixed_cellname(value2) && isnothing(tryparse(Float64, value2)) && throw(XLSXError("Invalid `value2`: $value2. Must be a number or a CellRef."))
+ !isnothing(operator) && operator ∉ ["greaterThan", "greaterEqual", "lessThan", "lessEqual", "equal", "notEqual", "between", "notBetween"] && throw(XLSXError("Invalid `operator`: $operator. Valid options are: `greaterThan`, `greaterEqual`, `lessThan`, `lessEqual`, `equal`, `notEqual`, `between`, `notBetween`"))
+ wb = get_workbook(ws)
+ dx = get_dx(dxStyle, format, font, border, fill)
+ new_dx = get_new_dx(wb, dx)
+ dxid = Add_Cf_Dx(wb, new_dx)
+
+ if isnothing(value)
+ value = all(ismissing.(ws[rng])) ? nothing : string(sum(skipmissing(ws[rng])) / count(!ismissing, ws[rng]))
+ end
+ cfx = XML.Element("cfRule"; type="cellIs", dxfId=Int(dxid.id))
+ cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : 1
+ if !isnothing(stopIfTrue) && stopIfTrue == "true"
+ cfx["stopIfTrue"] = "1"
+ end
+ cfx["operator"] = operator
+
+ push!(cfx, XML.Element("formula", XML.Text(XML.escape(value))))
+ if !isnothing(value2) && operator ∈ ["between", "notBetween"]
+
+ push!(cfx, XML.Element("formula", XML.Text(XML.escape(value2))))
+ end
+
+ update_worksheet_cfx!(allcfs, cfx, ws, rng)
+
+ return 0
+end
+
+setCfContainsText(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setCfContainsText, ws, row, nothing; kw...)
+setCfContainsText(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setCfContainsText, ws, nothing, col; kw...)
+setCfContainsText(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setCfContainsText, ws, nothing, nothing; kw...)
+setCfContainsText(ws::Worksheet, ::Colon; kw...) = process_colon(setCfContainsText, ws, nothing, nothing; kw...)
+setCfContainsText(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setCfContainsText(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
+setCfContainsText(ws::Worksheet, cell::CellRef; kw...) = setCfContainsText(ws, CellRange(cell, cell); kw...)
+setCfContainsText(ws::Worksheet, cell::SheetCellRef; kw...) = do_sheet_names_match(ws, cell) && setCfContainsText(ws, CellRange(cell.cellref, cell.cellref); kw...)
+setCfContainsText(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setCfContainsText(ws, rng.rng; kw...)
+setCfContainsText(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setCfContainsText(ws, rng.colrng; kw...)
+setCfContainsText(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setCfContainsText(ws, rng.rowrng; kw...)
+setCfContainsText(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCfContainsText, ws, rng; kw...)
+setCfContainsText(ws::Worksheet, rng::ColumnRange; kw...) = process_columnranges(setCfContainsText, ws, rng; kw...)
+setCfContainsText(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetcell(setCfContainsText, xl, sheetcell; kw...)
+setCfContainsText(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setCfContainsText, ws, ref_or_rng; kw...)
+function setCfContainsText(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}=())::Int
+
+ operator::Union{Nothing,String}="containsText"
+ value::Union{Nothing,String}=nothing
+ stopIfTrue::Union{Nothing,String}=nothing
+ dxStyle::Union{Nothing,String}=nothing
+ format::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ font::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ border::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ fill::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ for (k, v) in allkws
+ if k == :operator
+ operator = String(v)
+ elseif k == :value
+ value = String(v)
+ elseif k == :stopIfTrue
+ stopIfTrue = String(v)
+ elseif k == :dxStyle
+ dxStyle = String(v)
+ elseif k == :format
+ format = v
+ elseif k == :font
+ font = v
+ elseif k == :border
+ border = v
+ elseif k == :fill
+ fill = v
+ else
+ throw(XLSXError("Invalid keyword argument: $k. Valid options are: `operator`, `value`, `stopIfTrue`, `dxStyle`, `format`, `font`, `border`, `fill`."))
+ end
+ end
+
+ !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension."))
+
+ allcfs = allCfs(ws) # get all conditional format blocks
+ old_cf = getConditionalFormats(ws) # extract conditional format info
+
+ isnothing(value) && throw(XLSXError("Invalid `value`: $value. Must contain text or a CellRef."))
+
+ wb = get_workbook(ws)
+ dx = get_dx(dxStyle, format, font, border, fill)
+ new_dx = get_new_dx(wb, dx)
+ dxid = Add_Cf_Dx(wb, new_dx)
+
+ type = operator
+ if operator == "containsText"
+ formula = "NOT(ISERROR(SEARCH(\"__txt__\",__CR__)))"
+ elseif operator == "notContainsText"
+ operator = "notContains"
+ formula = "ISERROR(SEARCH(\"__txt__\",__CR__))"
+ elseif operator == "beginsWith"
+ # operator = "beginsWith"
+ formula = "LEFT(__CR__,LEN(\"__txt__\"))=\"__txt__\""
+ elseif operator == "endsWith"
+ # operator = "endsWith"
+ formula = "RIGHT(__CR__,LEN(\"__txt__\"))=\"__txt__\""
+ else
+ throw(XLSXError("Invalid operator: $type. Valid options are: `containsText`, `notContainsText`, `beginsWith`, `endsWith`."))
+ end
+ formula = replace(formula, "__txt__" => value, "__CR__" => string(first(rng)))
+
+ cfx = XML.Element("cfRule"; type=type, dxfId=Int(dxid.id))
+ cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : 1
+ if !isnothing(stopIfTrue) && stopIfTrue == "true"
+ cfx["stopIfTrue"] = "1"
+ end
+ cfx["operator"] = operator
+ cfx["text"] = value
+ push!(cfx, XML.Element("formula", XML.Text(XML.escape(formula))))
+
+ update_worksheet_cfx!(allcfs, cfx, ws, rng)
+
+ return 0
+end
+
+setCfTop10(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setCfTop10, ws, row, nothing; kw...)
+setCfTop10(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setCfTop10, ws, nothing, col; kw...)
+setCfTop10(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setCfTop10, ws, nothing, nothing; kw...)
+setCfTop10(ws::Worksheet, ::Colon; kw...) = process_colon(setCfTop10, ws, nothing, nothing; kw...)
+setCfTop10(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setCfTop10(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
+setCfTop10(ws::Worksheet, cell::CellRef; kw...) = setCfTop10(ws, CellRange(cell, cell); kw...)
+setCfTop10(ws::Worksheet, cell::SheetCellRef; kw...) = do_sheet_names_match(ws, cell) && setCfTop10(ws, CellRange(cell.cellref, cell.cellref); kw...)
+setCfTop10(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setCfTop10(ws, rng.rng; kw...)
+setCfTop10(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setCfTop10(ws, rng.colrng; kw...)
+setCfTop10(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setCfTop10(ws, rng.rowrng; kw...)
+setCfTop10(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCfTop10, ws, rng; kw...)
+setCfTop10(ws::Worksheet, rng::ColumnRange; kw...) = process_columnranges(setCfTop10, ws, rng; kw...)
+setCfTop10(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetcell(setCfTop10, xl, sheetcell; kw...)
+setCfTop10(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setCfTop10, ws, ref_or_rng; kw...)
+function setCfTop10(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}=())::Int
+
+ operator::Union{Nothing,String}="topN"
+ value::Union{Nothing,String}="10"
+ stopIfTrue::Union{Nothing,String}=nothing
+ dxStyle::Union{Nothing,String}=nothing
+ format::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ font::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ border::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ fill::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ for (k, v) in allkws
+ if k == :operator
+ operator = v
+ elseif k == :value
+ value = v
+ elseif k == :stopIfTrue
+ stopIfTrue = v
+ elseif k == :dxStyle
+ dxStyle = v
+ elseif k == :format
+ format = v
+ elseif k == :font
+ font = v
+ elseif k == :border
+ border = v
+ elseif k == :fill
+ fill = v
+ else
+ throw(XLSXError("Invalid keyword argument: $k. Valid options are: `operator`, `value`, `stopIfTrue`, `dxStyle`, `format`, `font`, `border`, `fill`."))
+ end
+ end
+
+ !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension."))
+
+ allcfs = allCfs(ws) # get all conditional format blocks
+ old_cf = getConditionalFormats(ws) # extract conditional format info
+
+ !isnothing(value) && !is_valid_cellname(value) && !is_valid_fixed_cellname(value) && isnothing(tryparse(Float64, value)) && throw(XLSXError("Invalid `value`: $value. Must be a number or a CellRef."))
+
+ wb = get_workbook(ws)
+ dx = get_dx(dxStyle, format, font, border, fill)
+ new_dx = get_new_dx(wb, dx)
+ dxid = Add_Cf_Dx(wb, new_dx)
+
+ percent = ""
+ bottom = ""
+ cfx = XML.Element("cfRule"; type="top10", dxfId=Int(dxid.id))
+ if operator == "topN"
+ elseif operator == "topN%"
+ percent = "1"
+ elseif operator == "bottomN"
+ bottom = "1"
+ elseif operator == "bottomN%"
+ percent = "1"
+ bottom = "1"
+ else
+ throw(XLSXError("Invalid operator: $operator. Valid options are: `topN`, `topN%`, `bottomN`, `bottomN%`."))
+ end
+
+ cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : 1
+ if !isnothing(stopIfTrue) && stopIfTrue == "true"
+ cfx["stopIfTrue"] = "1"
+ end
+ if percent != ""
+ cfx["percent"] = percent
+ end
+ if bottom != ""
+ cfx["bottom"] = bottom
+ end
+ cfx["rank"] = value
+
+ update_worksheet_cfx!(allcfs, cfx, ws, rng)
+
+ return 0
+end
+
+setCfAboveAverage(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setCfAboveAverage, ws, row, nothing; kw...)
+setCfAboveAverage(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setCfAboveAverage, ws, nothing, col; kw...)
+setCfAboveAverage(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setCfAboveAverage, ws, nothing, nothing; kw...)
+setCfAboveAverage(ws::Worksheet, ::Colon; kw...) = process_colon(setCfAboveAverage, ws, nothing, nothing; kw...)
+setCfAboveAverage(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setCfAboveAverage(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
+setCfAboveAverage(ws::Worksheet, cell::CellRef; kw...) = setCfAboveAverage(ws, CellRange(cell, cell); kw...)
+setCfAboveAverage(ws::Worksheet, cell::SheetCellRef; kw...) = do_sheet_names_match(ws, cell) && setCfAboveAverage(ws, CellRange(cell.cellref, cell.cellref); kw...)
+setCfAboveAverage(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setCfAboveAverage(ws, rng.rng; kw...)
+setCfAboveAverage(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setCfAboveAverage(ws, rng.colrng; kw...)
+setCfAboveAverage(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setCfAboveAverage(ws, rng.rowrng; kw...)
+setCfAboveAverage(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCfAboveAverage, ws, rng; kw...)
+setCfAboveAverage(ws::Worksheet, rng::ColumnRange; kw...) = process_columnranges(setCfAboveAverage, ws, rng; kw...)
+setCfAboveAverage(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetcell(setCfAboveAverage, xl, sheetcell; kw...)
+setCfAboveAverage(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setCfAboveAverage, ws, ref_or_rng; kw...)
+function setCfAboveAverage(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}=())::Int
+
+ operator::Union{Nothing,String}="aboveAverage"
+ stopIfTrue::Union{Nothing,String}=nothing
+ dxStyle::Union{Nothing,String}=nothing
+ format::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ font::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ border::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ fill::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ for (k, v) in allkws
+ if k == :operator
+ operator = v
+ elseif k == :stopIfTrue
+ stopIfTrue = v
+ elseif k == :dxStyle
+ dxStyle = v
+ elseif k == :format
+ format = v
+ elseif k == :font
+ font = v
+ elseif k == :border
+ border = v
+ elseif k == :fill
+ fill = v
+ else
+ throw(XLSXError("Invalid keyword argument: $k. Valid options are: `operator`, `stopIfTrue`, `dxStyle`, `format`, `font`, `border`, `fill`."))
+ end
+ end
+
+ !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension."))
+
+ allcfs = allCfs(ws) # get all conditional format blocks
+ old_cf = getConditionalFormats(ws) # extract conditional format info
+
+ wb = get_workbook(ws)
+ dx = get_dx(dxStyle, format, font, border, fill)
+ new_dx = get_new_dx(wb, dx)
+ dxid = Add_Cf_Dx(wb, new_dx)
+
+ if operator == "aboveAverage"
+ cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1")
+ elseif operator == "aboveEqAverage"
+ cfx = XML.Element("cfRule"; type="aboveAverage", dxfId=Int(dxid.id), priority="1", equalAverage="1")
+ elseif operator == "plus1StdDev"
+ cfx = XML.Element("cfRule"; type="aboveAverage", dxfId=Int(dxid.id), priority="1", stdDev="1")
+ elseif operator == "plus2StdDev"
+ cfx = XML.Element("cfRule"; type="aboveAverage", dxfId=Int(dxid.id), priority="1", stdDev="2")
+ elseif operator == "plus3StdDev"
+ cfx = XML.Element("cfRule"; type="aboveAverage", dxfId=Int(dxid.id), priority="1", stdDev="3")
+ elseif operator == "belowAverage"
+ cfx = XML.Element("cfRule"; type="aboveAverage", dxfId=Int(dxid.id), priority="1", aboveAverage="0")
+ elseif operator == "belowEqAverage"
+ cfx = XML.Element("cfRule"; type="aboveAverage", dxfId=Int(dxid.id), priority="1", aboveAverage="0", equalAverage="1")
+ elseif operator == "minus1StdDev"
+ cfx = XML.Element("cfRule"; type="aboveAverage", dxfId=Int(dxid.id), priority="1", aboveAverage="0", stdDev="1")
+ elseif operator == "minus2StdDev"
+ cfx = XML.Element("cfRule"; type="aboveAverage", dxfId=Int(dxid.id), priority="1", aboveAverage="0", stdDev="2")
+ elseif operator == "minus3StdDev"
+ cfx = XML.Element("cfRule"; type="aboveAverage", dxfId=Int(dxid.id), priority="1", aboveAverage="0", stdDev="3")
+ else
+ throw(XLSXError("Invalid operator: $operator. Valid options are: `aboveAverage`, `aboveEqAverage`, `plus1sStdDev`, `plus2StdDev`, `plus3StdDev`, `belowAverage`, `belowEqAverage`, `minus1StdDev`, `minus2StdDev`, `minus3StdDev`."))
+ end
+
+ cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : 1
+ if !isnothing(stopIfTrue) && stopIfTrue == "true"
+ cfx["stopIfTrue"] = "1"
+ end
+
+ update_worksheet_cfx!(allcfs, cfx, ws, rng)
+
+ return 0
+end
+
+setCfTimePeriod(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setCfTimePeriod, ws, row, nothing; kw...)
+setCfTimePeriod(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setCfTimePeriod, ws, nothing, col; kw...)
+setCfTimePeriod(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setCfTimePeriod, ws, nothing, nothing; kw...)
+setCfTimePeriod(ws::Worksheet, ::Colon; kw...) = process_colon(setCfTimePeriod, ws, nothing, nothing; kw...)
+setCfTimePeriod(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setCfTimePeriod(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
+setCfTimePeriod(ws::Worksheet, cell::CellRef; kw...) = setCfTimePeriod(ws, CellRange(cell, cell); kw...)
+setCfTimePeriod(ws::Worksheet, cell::SheetCellRef; kw...) = do_sheet_names_match(ws, cell) && setCfTimePeriod(ws, CellRange(cell.cellref, cell.cellref); kw...)
+setCfTimePeriod(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setCfTimePeriod(ws, rng.rng; kw...)
+setCfTimePeriod(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setCfTimePeriod(ws, rng.colrng; kw...)
+setCfTimePeriod(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setCfTimePeriod(ws, rng.rowrng; kw...)
+setCfTimePeriod(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCfTimePeriod, ws, rng; kw...)
+setCfTimePeriod(ws::Worksheet, rng::ColumnRange; kw...) = process_columnranges(setCfTimePeriod, ws, rng; kw...)
+setCfTimePeriod(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetcell(setCfTimePeriod, xl, sheetcell; kw...)
+setCfTimePeriod(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setCfTimePeriod, ws, ref_or_rng; kw...)
+function setCfTimePeriod(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}=())::Int
+
+ operator::Union{Nothing,String}="last7Days"
+ stopIfTrue::Union{Nothing,String}=nothing
+ dxStyle::Union{Nothing,String}=nothing
+ format::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ font::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ border::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ fill::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ for (k, v) in allkws
+ if k == :operator
+ operator = v
+ elseif k == :stopIfTrue
+ stopIfTrue = v
+ elseif k == :dxStyle
+ dxStyle = v
+ elseif k == :format
+ format = v
+ elseif k == :font
+ font = v
+ elseif k == :border
+ border = v
+ elseif k == :fill
+ fill = v
+ else
+ throw(XLSXError("Invalid keyword argument: $k. Valid options are: `operator`, `stopIfTrue`, `dxStyle`, `format`, `font`, `border`, `fill`."))
+ end
+ end
+
+ !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension."))
+
+ allcfs = allCfs(ws) # get all conditional format blocks
+ old_cf = getConditionalFormats(ws) # extract conditional format info
+
+ if operator == "yesterday"
+ formula = "FLOOR(__CR__,1)=TODAY()-1"
+ elseif operator == "today"
+ formula = "FLOOR(__CR__,1)=TODAY()"
+ elseif operator == "tomorrow"
+ formula = "FLOOR(__CR__,1)=TODAY()+1"
+ elseif operator == "last7Days"
+ formula = "AND(TODAY()-FLOOR(__CR__,1)<=6,FLOOR(__CR__,1)<=TODAY())"
+ elseif operator == "lastWeek"
+ formula = "AND(TODAY()-ROUNDDOWN(__CR__,0)>=(WEEKDAY(TODAY())),TODAY()-ROUNDDOWN(__CR__,0)<(WEEKDAY(TODAY())+7))"
+ elseif operator == "thisWeek"
+ formula = "AND(TODAY()-ROUNDDOWN(__CR__,0)<=WEEKDAY(TODAY())-1,ROUNDDOWN(__CR__,0)-TODAY()<=7-WEEKDAY(TODAY()))"
+ elseif operator == "nextWeek"
+ formula = "AND(ROUNDDOWN(__CR__,0)-TODAY()>(7-WEEKDAY(TODAY())),ROUNDDOWN(__CR__,0)-TODAY()<(15-WEEKDAY(TODAY())))"
+ elseif operator == "lastMonth"
+ formula = "AND(MONTH(__CR__)=MONTH(EDATE(TODAY(),0-1)),YEAR(__CR__)=YEAR(EDATE(TODAY(),0-1)))"
+ elseif operator == "thisMonth"
+ formula = "AND(MONTH(__CR__)=MONTH(TODAY()),YEAR(__CR__)=YEAR(TODAY()))"
+ elseif operator == "nextMonth"
+ formula = "AND(MONTH(__CR__)=MONTH(EDATE(TODAY(),0+1)),YEAR(__CR__)=YEAR(EDATE(TODAY(),0+1)))"
+ else
+ throw(XLSXError("Invalid operator: $operator. Valid options are: `yesterday`, `today`, `tomorrow`, `last7Days`, `lastWeek`, `thisWeek`, `nextWeek`, `lastMonth`, `thisMonth`, `nextMonth`."))
+ end
+ formula = replace(formula, "__CR__" => string(first(rng)))
+
+ wb = get_workbook(ws)
+ dx = get_dx(dxStyle, format, font, border, fill)
+ new_dx = get_new_dx(wb, dx)
+ dxid = Add_Cf_Dx(wb, new_dx)
+
+ cfx = XML.Element("cfRule"; type="timePeriod", dxfId=Int(dxid.id))
+ cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : 1
+ if !isnothing(stopIfTrue) && stopIfTrue == "true"
+ cfx["stopIfTrue"] = "1"
+ end
+ cfx["timePeriod"] = operator
+
+ push!(cfx, XML.Element("formula", XML.Text(XML.escape(formula))))
+
+ update_worksheet_cfx!(allcfs, cfx, ws, rng)
+
+ return 0
+end
+
+setCfContainsBlankErrorUniqDup(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setCfContainsBlankErrorUniqDup, ws, row, nothing; kw...)
+setCfContainsBlankErrorUniqDup(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setCfContainsBlankErrorUniqDup, ws, nothing, col; kw...)
+setCfContainsBlankErrorUniqDup(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setCfContainsBlankErrorUniqDup, ws, nothing, nothing; kw...)
+setCfContainsBlankErrorUniqDup(ws::Worksheet, ::Colon; kw...) = process_colon(setCfContainsBlankErrorUniqDup, ws, nothing, nothing; kw...)
+setCfContainsBlankErrorUniqDup(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setCfContainsBlankErrorUniqDup(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
+setCfContainsBlankErrorUniqDup(ws::Worksheet, cell::CellRef; kw...) = setCfContainsBlankErrorUniqDup(ws, CellRange(cell, cell); kw...)
+setCfContainsBlankErrorUniqDup(ws::Worksheet, cell::SheetCellRef; kw...) = do_sheet_names_match(ws, cell) && setCfContainsBlankErrorUniqDup(ws, CellRange(cell.cellref, cell.cellref); kw...)
+setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setCfContainsBlankErrorUniqDup(ws, rng.rng; kw...)
+setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setCfContainsBlankErrorUniqDup(ws, rng.colrng; kw...)
+setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setCfContainsBlankErrorUniqDup(ws, rng.rowrng; kw...)
+setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCfContainsBlankErrorUniqDup, ws, rng; kw...)
+setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::ColumnRange; kw...) = process_columnranges(setCfContainsBlankErrorUniqDup, ws, rng; kw...)
+setCfContainsBlankErrorUniqDup(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetcell(setCfContainsBlankErrorUniqDup, xl, sheetcell; kw...)
+setCfContainsBlankErrorUniqDup(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setCfContainsBlankErrorUniqDup, ws, ref_or_rng; kw...)
+function setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}=())::Int
+
+ operator::Union{Nothing,String}="containsBlanks"
+ stopIfTrue::Union{Nothing,String}=nothing
+ dxStyle::Union{Nothing,String}=nothing
+ format::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ font::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ border::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ fill::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ for (k, v) in allkws
+ if k == :operator
+ operator = v
+ elseif k == :stopIfTrue
+ stopIfTrue = v
+ elseif k == :dxStyle
+ dxStyle = v
+ elseif k == :format
+ format = v
+ elseif k == :font
+ font = v
+ elseif k == :border
+ border = v
+ elseif k == :fill
+ fill = v
+ else
+ throw(XLSXError("Invalid keyword argument: $k. Valid options are: `operator`, `stopIfTrue`, `dxStyle`, `format`, `font`, `border`, `fill`."))
+ end
+ end
+
+ !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension."))
+
+ allcfs = allCfs(ws) # get all conditional format blocks
+ old_cf = getConditionalFormats(ws) # extract conditional format info
+ if operator == "containsBlanks"
+ formula = "LEN(TRIM(__CR__))=0"
+ elseif operator == "notContainsBlanks"
+ formula = "LEN(TRIM(__CR__))>0"
+ elseif operator == "containsErrors"
+ formula = "ISERROR(__CR__)"
+ elseif operator == "notContainsErrors"
+ formula = "NOT(ISERROR(__CR__))"
+ elseif operator == "uniqueValues"
+ formula = ""
+ elseif operator == "duplicateValues"
+ formula = ""
+ end
+ formula = replace(formula, "__CR__" => string(first(rng)))
+
+ wb = get_workbook(ws)
+ dx = get_dx(dxStyle, format, font, border, fill)
+ new_dx = get_new_dx(wb, dx)
+ dxid = Add_Cf_Dx(wb, new_dx)
+
+ cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id))
+ cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : 1
+ if !isnothing(stopIfTrue) && stopIfTrue == "true"
+ cfx["stopIfTrue"] = "1"
+ end
+ formula != "" && push!(cfx, XML.Element("formula", XML.Text(XML.escape(formula))))
+
+ update_worksheet_cfx!(allcfs, cfx, ws, rng)
+
+ return 0
+end
+
+setCfFormula(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setCfFormula, ws, row, nothing; kw...)
+setCfFormula(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setCfFormula, ws, nothing, col; kw...)
+setCfFormula(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setCfFormula, ws, nothing, nothing; kw...)
+setCfFormula(ws::Worksheet, ::Colon; kw...) = process_colon(setCfFormula, ws, nothing, nothing; kw...)
+setCfFormula(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setCfFormula(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
+setCfFormula(ws::Worksheet, cell::CellRef; kw...) = setCfFormula(ws, CellRange(cell, cell); kw...)
+setCfFormula(ws::Worksheet, cell::SheetCellRef; kw...) = do_sheet_names_match(ws, cell) && setCfFormula(ws, CellRange(cell.cellref, cell.cellref); kw...)
+setCfFormula(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setCfFormula(ws, rng.rng; kw...)
+setCfFormula(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setCfFormula(ws, rng.colrng; kw...)
+setCfFormula(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setCfFormula(ws, rng.rowrng; kw...)
+setCfFormula(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCfFormula, ws, rng; kw...)
+setCfFormula(ws::Worksheet, rng::ColumnRange; kw...) = process_columnranges(setCfFormula, ws, rng; kw...)
+setCfFormula(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetcell(setCfFormula, xl, sheetcell; kw...)
+setCfFormula(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setCfFormula, ws, ref_or_rng; kw...)
+function setCfFormula(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}=())::Int
+
+ formula::Union{Nothing,String}=nothing
+ stopIfTrue::Union{Nothing,String}=nothing
+ dxStyle::Union{Nothing,String}=nothing
+ format::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ font::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ border::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ fill::Union{Nothing,Vector{Pair{String,String}}}=nothing
+ for (k, v) in allkws
+ if k == :formula
+ formula = v
+ elseif k == :stopIfTrue
+ stopIfTrue = v
+ elseif k == :dxStyle
+ dxStyle = v
+ elseif k == :format
+ format = v
+ elseif k == :font
+ font = v
+ elseif k == :border
+ border = v
+ elseif k == :fill
+ fill = v
+ else
+ throw(XLSXError("Invalid keyword argument: $k. Valid options are: `formula`, `stopIfTrue`, `dxStyle`, `format`, `font`, `border`, `fill`."))
+ end
+ end
+ isnothing(formula) && throw(XLSXError("A `formula` must be provided as a keyword argument."))
+
+ !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension."))
+
+ allcfs = allCfs(ws) # get all conditional format blocks
+ old_cf = getConditionalFormats(ws) # extract conditional format info
+
+ wb = get_workbook(ws)
+ dx = get_dx(dxStyle, format, font, border, fill)
+ new_dx = get_new_dx(wb, dx)
+ dxid = Add_Cf_Dx(wb, new_dx)
+
+ cfx = XML.Element("cfRule"; type="expression", dxfId=Int(dxid.id))
+ cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : 1
+ if !isnothing(stopIfTrue) && stopIfTrue == "true"
+ cfx["stopIfTrue"] = "1"
+ end
+
+ push!(cfx, XML.Element("formula", XML.Text("(" * XML.escape(uppercase_unquoted(formula)) * ")")))
+
+ update_worksheet_cfx!(allcfs, cfx, ws, rng)
+
+ return 0
+end
+
+setCfColorScale(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setCfColorScale, ws, row, nothing; kw...)
+setCfColorScale(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setCfColorScale, ws, nothing, col; kw...)
+setCfColorScale(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setCfColorScale, ws, nothing, nothing; kw...)
+setCfColorScale(ws::Worksheet, ::Colon; kw...) = process_colon(setCfColorScale, ws, nothing, nothing; kw...)
+setCfColorScale(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setCfColorScale(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
+setCfColorScale(ws::Worksheet, cell::CellRef; kw...) = setCfColorScale(ws, CellRange(cell, cell); kw...)
+setCfColorScale(ws::Worksheet, cell::SheetCellRef; kw...) = do_sheet_names_match(ws, cell) && setCfColorScale(ws, CellRange(cell.cellref, cell.cellref); kw...)
+setCfColorScale(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setCfColorScale(ws, rng.rng; kw...)
+setCfColorScale(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setCfColorScale(ws, rng.colrng; kw...)
+setCfColorScale(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setCfColorScale(ws, rng.rowrng; kw...)
+setCfColorScale(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCfColorScale, ws, rng; kw...)
+setCfColorScale(ws::Worksheet, rng::ColumnRange; kw...) = process_columnranges(setCfColorScale, ws, rng; kw...)
+setCfColorScale(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetcell(setCfColorScale, xl, sheetcell; kw...)
+setCfColorScale(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setCfColorScale, ws, ref_or_rng; kw...)
+function setCfColorScale(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}=())::Int
+ colorscale::Union{Nothing,String}=nothing
+ min_type::Union{Nothing,String}="min"
+ min_val::Union{Nothing,String}=nothing
+ min_col::Union{Nothing,String}="FFF8696B"
+ mid_type::Union{Nothing,String}=nothing
+ mid_val::Union{Nothing,String}=nothing
+ mid_col::Union{Nothing,String}=nothing
+ max_type::Union{Nothing,String}="max"
+ max_val::Union{Nothing,String}=nothing
+ max_col::Union{Nothing,String}="FFFFEB84"
+
+ for (k, v) in allkws
+ if k == :colorscale
+ colorscale = v
+ elseif k == :min_type
+ min_type = v
+ elseif k == :min_val
+ min_val = v
+ elseif k == :min_col
+ min_col = v
+ elseif k == :mid_type
+ mid_type = v
+ elseif k == :mid_val
+ mid_val = v
+ elseif k == :mid_col
+ mid_col = v
+ elseif k == :max_type
+ max_type = v
+ elseif k == :max_val
+ max_val = v
+ elseif k == :max_col
+ max_col = v
+ else
+ throw(XLSXError("Invalid keyword argument: $k. Valid options are: `colorscale`, `min_type`, `min_val`, `min_col`, `mid_type`, `mid_val`, `mid_col`, `max_type`, `max_val`, `max_col`."))
+ end
+ end
+
+ !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension ($(get_dimension(ws)))."))
+
+ allcfs = allCfs(ws) # get all conditional format blocks
+ old_cf = getConditionalFormats(ws) # extract conditional format info
+
+ let new_pr, new_cf
+
+ new_pr = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : 1
+
+ if isnothing(colorscale)
+
+ min_type in ["min", "percentile", "percent", "num", "formula"] || throw(XLSXError("Invalid min_type: $min_type. Valid options are: min, percentile, percent, num, formula."))
+ if min_type == "min"
+ min_val = nothing
+ end
+ min_type == "formula" || isnothing(min_val) || is_valid_fixed_cellname(min_val) || is_valid_fixed_sheet_cellname(min_val) || !isnothing(tryparse(Float64, min_val)) || throw(XLSXError("Invalid min_val: `$min_val`. Valid options (unless min_type is `formula`) are a CellRef (e.g. `\$A\$1`) or a number."))
+ isnothing(mid_type) || mid_type in ["percentile", "percent", "num", "formula"] || throw(XLSXError("Invalid mid_type: $mid_type. Valid options are: percentile, percent, num, formula."))
+ (!isnothing(mid_type) && mid_type == "formula") || isnothing(mid_val) || is_valid_fixed_cellname(mid_val) || is_valid_fixed_sheet_cellname(mid_val) || !isnothing(tryparse(Float64, mid_val)) || throw(XLSXError("Invalid mid_val: `$mid_val`. Valid options (unless mid_type is `formula`) are a CellRef (e.g. `\$A\$1`) or a number."))
+ max_type in ["max", "percentile", "percent", "num", "formula"] || throw(XLSXError("Invalid max_type: $max_type. Valid options are: max, percentile, percent, num, formula."))
+ if max_type == "max"
+ max_val = nothing
+ end
+ max_type == "formula" || isnothing(max_val) || is_valid_fixed_cellname(max_val) || is_valid_fixed_sheet_cellname(max_val) || !isnothing(tryparse(Float64, max_val)) || throw(XLSXError("Invalid max_val: `$max_val`. Valid options (unless max_type is `formula`) are a CellRef (e.g. `\$A\$1`) or a number."))
+
+ for val in [min_val, mid_val, max_val]
+ if !isnothing(val)
+ if is_valid_fixed_sheet_cellname(val)
+ do_sheet_names_match(ws, SheetCellRef(val))
+ val = string(SheetCellRef(val).cellref)
+ end
+ val = XML.escape(uppercase_unquoted(val))
+ end
+ end
+
+ cfx = XML.h.cfRule(type="colorScale", priority=new_pr,
+ XML.h.colorScale(
+ isnothing(min_val) ? XML.h.cfvo(type=min_type) : XML.h.cfvo(type=min_type, val=min_val),
+ isnothing(mid_type) ? "" : XML.h.cfvo(type=mid_type, val=mid_val),
+ isnothing(max_val) ? XML.h.cfvo(type=max_type) : XML.h.cfvo(type=max_type, val=max_val),
+ XML.h.color(rgb=get_color(min_col)),
+ isnothing(mid_type) ? "" : XML.h.color(rgb=get_color(mid_col)),
+ XML.h.color(rgb=get_color(max_col))
+ )
+ )
+
+ else
+ if !haskey(colorscales, colorscale)
+ throw(XLSXError("Invalid colorscale option chosen: $colorscale. Valid options are: $(keys(colorscales))."))
+ end
+ cfx = copynode(colorscales[colorscale])
+ cfx["priority"] = new_pr
+ end
+
+ update_worksheet_cfx!(allcfs, cfx, ws, rng)
+
+ end
+
+ return 0
+end
+
+setCfIconSet(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setCfIconSet, ws, row, nothing; kw...)
+setCfIconSet(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setCfIconSet, ws, nothing, col; kw...)
+setCfIconSet(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setCfIconSet, ws, nothing, nothing; kw...)
+setCfIconSet(ws::Worksheet, ::Colon; kw...) = process_colon(setCfIconSet, ws, nothing, nothing; kw...)
+setCfIconSet(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setCfIconSet(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
+setCfIconSet(ws::Worksheet, cell::CellRef; kw...) = setCfIconSet(ws, CellRange(cell, cell); kw...)
+setCfIconSet(ws::Worksheet, cell::SheetCellRef; kw...) = do_sheet_names_match(ws, cell) && setCfIconSet(ws, CellRange(cell.cellref, cell.cellref); kw...)
+setCfIconSet(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setCfIconSet(ws, rng.rng; kw...)
+setCfIconSet(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setCfIconSet(ws, rng.colrng; kw...)
+setCfIconSet(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setCfIconSet(ws, rng.rowrng; kw...)
+setCfIconSet(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCfIconSet, ws, rng; kw...)
+setCfIconSet(ws::Worksheet, rng::ColumnRange; kw...) = process_columnranges(setCfIconSet, ws, rng; kw...)
+setCfIconSet(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetcell(setCfIconSet, xl, sheetcell; kw...)
+setCfIconSet(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setCfIconSet, ws, ref_or_rng; kw...)
+function setCfIconSet(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}=())::Int
+
+ iconset::Union{Nothing,String} = "3TrafficLights"
+ reverse::Union{Nothing,String} = nothing
+ showVal::Union{Nothing,String} = nothing
+ min_type::Union{Nothing,String} = nothing
+ min_val::Union{Nothing,String} = nothing
+ min_gte::Union{Nothing,String} = nothing
+ mid_type::Union{Nothing,String} = nothing
+ mid_val::Union{Nothing,String} = nothing
+ mid_gte::Union{Nothing,String} = nothing
+ mid2_type::Union{Nothing,String} = nothing
+ mid2_val::Union{Nothing,String} = nothing
+ mid2_gte::Union{Nothing,String} = nothing
+ max_type::Union{Nothing,String} = nothing
+ max_val::Union{Nothing,String} = nothing
+ max_gte::Union{Nothing,String} = nothing
+ icon_list::Union{Nothing,Vector{Int64}}=nothing
+
+ for (k, v) in allkws
+ if k == :iconset
+ iconset = v
+ elseif k == :reverse
+ reverse = v
+ elseif k == :showVal
+ showVal = v
+ elseif k == :min_type
+ min_type = v
+ elseif k == :min_val
+ min_val = v
+ elseif k == :min_gte
+ min_gte = v
+ elseif k == :mid_type
+ mid_type = v
+ elseif k == :mid_val
+ mid_val = v
+ elseif k == :mid_gte
+ mid_gte = v
+ elseif k == :mid2_type
+ mid2_type = v
+ elseif k == :mid2_val
+ mid2_val = v
+ elseif k == :mid2_gte
+ mid2_gte = v
+ elseif k == :max_type
+ max_type = v
+ elseif k == :max_val
+ max_val = v
+ elseif k == :max_gte
+ max_gte = v
+ elseif k == :icon_list
+ icon_list = v
+ else
+ throw(XLSXError("Invalid keyword argument: $k. Valid options are: `iconset`, `reverse`, `showVal`, `min_type`, `min_val`, `min_gte`, `mid_type`, `mid_val`, `mid_gte`, `mid2_type`, `mid2_val`, `mid2_gte`, `max_type`, `max_val`, `max_gte`, `icon_list`."))
+ end
+ end
+
+ !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension ($(get_dimension(ws)))."))
+
+ allcfs = allCfs(ws) # get all conditional format blocks
+ old_cf = getConditionalFormats(ws) # extract conditional format info
+ allextcfs = allExtCfs(ws) # get all extended conditional format blocks
+
+ let new_pr, new_cf
+
+ new_pr = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : 1
+
+ isnothing(min_type) || min_type in ["percentile", "percent", "num", "formula"] || throw(XLSXError("Invalid min_type: $min_type. Valid options are: percentile, percent, num, formula."))
+ (!isnothing(min_type) && min_type == "formula") || isnothing(min_val) || is_valid_fixed_cellname(min_val) || is_valid_fixed_sheet_cellname(min_val) || !isnothing(tryparse(Float64, min_val)) || throw(XLSXError("Invalid min_val: `$min_val`. Valid options (unless min_type is `formula`) are a CellRef (e.g. `\$A\$1`) or a number."))
+ isnothing(mid_type) || mid_type in ["percentile", "percent", "num", "formula"] || throw(XLSXError("Invalid mid_type: $mid_type. Valid options are: percentile, percent, num, formula."))
+ (!isnothing(mid_type) && mid_type == "formula") || isnothing(mid_val) || is_valid_fixed_cellname(mid_val) || !is_valid_fixed_sheet_cellname(mid_val) || !isnothing(tryparse(Float64, mid_val)) || throw(XLSXError("Invalid mid_val: `$mid_val`. Valid options (unless mid_type is `formula`) are a CellRef (e.g. `\$A\$1`) or a number."))
+ isnothing(mid2_type) || mid2_type in ["percentile", "percent", "num", "formula"] || throw(XLSXError("Invalid mid_type: $mid2_type. Valid options are: percentile, percent, num, formula."))
+ (!isnothing(mid2_type) && mid2_type == "formula") || isnothing(mid2_val) || is_valid_fixed_cellname(mid2_val) || is_valid_fixed_sheet_cellname(mid2_val) || !isnothing(tryparse(Float64, mid2_val)) || throw(XLSXError("Invalid mid2_type: `$mid2_val`. Valid options (unless mid2_type is `formula`) are a CellRef (e.g. `\$A\$1`) or a number."))
+ isnothing(max_type) || max_type in ["percentile", "percent", "num", "formula"] || throw(XLSXError("Invalid max_type: $max_type. Valid options are: percentile, percent, num, formula."))
+ (!isnothing(max_type) && max_type == "formula") || isnothing(max_val) || is_valid_fixed_cellname(max_val) || is_valid_fixed_sheet_cellname(max_val) || !isnothing(tryparse(Float64, max_val)) || throw(XLSXError("Invalid max_val: `$max_val`. Valid options (unless max_type is `formula`) are a CellRef (e.g. `\$A\$1`) or a number."))
+
+ for val in [min_val, mid_val, mid2_val, max_val]
+ if !isnothing(val)
+ if is_valid_fixed_sheet_cellname(val)
+ do_sheet_names_match(ws, SheetCellRef(val))
+ val = string(SheetCellRef(val).cellref)
+ end
+ val = XML.escape(uppercase_unquoted(val))
+ end
+ end
+ if !haskey(iconsets, iconset)
+ throw(XLSXError("Invalid iconset option chosen: $iconset. Valid options are: $(keys(iconsets))"))
+ end
+ l = first(iconset)
+ cfx = copynode(iconsets[iconset])
+ if l == 'C'
+ cfvo = XML.Element("x14:cfvo", type="percent")
+ push!(cfvo, XML.Element("xm:f", XML.Text("dummy")))
+ push!(cfx[1], copynode(cfvo)) # for min_val
+ push!(cfx[1], copynode(cfvo)) # for max_val
+ if isnothing(min_type) || isnothing(min_val) || isnothing(max_type) || isnothing(max_val)
+ throw(XLSXError("No type or val keywords defined. Must define at least `min_type`, `min_val`, `max_type` and `max_val` for a custom iconSet"))
+ elseif isnothing(mid_type) || isnothing(mid_val)
+ list = [(min_type, min_val, min_gte), (max_type, max_val, max_gte)]
+ nicons = 3
+ elseif isnothing(mid2_type) || isnothing(mid2_val)
+ push!(cfx[1], copynode(cfvo)) # for mid_val
+ cfx[1]["iconSet"] = "4Arrows"
+ nicons = 4
+ list = [(min_type, min_val, min_gte), (mid_type, mid_val, mid_gte), (max_type, max_val, max_gte)]
+ else
+ push!(cfx[1], copynode(cfvo)) # for mid_val
+ push!(cfx[1], copynode(cfvo)) # for mid2_val
+ cfx[1]["iconSet"] = "5Quarters"
+ nicons = 5
+ list = [(min_type, min_val, min_gte), (mid_type, mid_val, mid_gte), (mid2_type, mid2_val, mid2_gte), (max_type, max_val, max_gte)]
+ end
+ elseif l == '5'
+ list = [(min_type, min_val, min_gte), (mid_type, mid_val, mid_gte), (mid2_type, mid2_val, mid2_gte), (max_type, max_val, max_gte)]
+ elseif l == '4'
+ list = [(min_type, min_val, min_gte), (mid_type, mid_val, mid_gte), (max_type, max_val, max_gte)]
+ else
+ list = [(min_type, min_val, min_gte), (max_type, max_val, max_gte)]
+ end
+ if iconset in ["3Triangles", "3Stars", "5Boxes", "Custom"]
+ cfx["id"] = "{" * uppercase(string(UUIDs.uuid4())) * "}"
+ cfx["priority"] = new_pr
+ if !isnothing(showVal) && showVal == "false"
+ cfx[1]["showValue"] = "0"
+ end
+ if !isnothing(reverse) && reverse == "true"
+ if iconset == "Custom"
+ reverse!(icon_list)
+ else
+ cfx[1]["reverse"] = "1"
+ end
+ end
+ for (i, (type, val, gte)) in enumerate(list)
+ if !isnothing(type)
+ cfx[1][i+1]["type"] = type # Need +1 because the first is always 0 percent.
+ end
+ if !isnothing(val)
+ if !isnothing(type) && type == "formula"
+ c = XML.Element("xm:f", XML.Text("(" * val * ")"))
+ else
+ c = XML.Element("xm:f", XML.Text(val))
+ end
+ if isnothing(XML.children(cfx[1][i+1]))
+ cfx[1][i+1] = XML.Node(cfx[1][i+1], c)
+ else
+ cfx[1][i+1][1] = c
+ end
+ end
+ if !isnothing(gte) && gte == "false"
+ cfx[1][i+1]["gte"] = "0"
+ end
+ end
+ if iconset == "Custom"
+ if isnothing(icon_list)
+ throw(XLSXError("No custom icons specified. Must specify between two and four icons."))
+ elseif length(icon_list) < nicons
+ throw(XLSXError("Too few custom icons specified: $(length(icon_list)). Expected $nicons"))
+ end
+ for (count, icon) in enumerate(string.(icon_list))
+ if !isnothing(icon)
+ if !haskey(allIcons, icon)
+ throw(XLSXError("Invalid custom icon specified: $icon. Valid values are \"1\" to \"52\"."))
+ end
+ i = allIcons[icon]
+ push!(cfx[1], XML.Element("x14:cfIcon", iconSet=first(i), iconId=last(i)))
+ count == nicons && break
+ end
+ end
+ end
+ update_worksheet_ext_cfx!(allextcfs, cfx, ws, rng)
+ else
+ cfx["priority"] = new_pr
+ if !isnothing(showVal) && showVal == "false"
+ cfx[1]["showValue"] = "0"
+ end
+ if !isnothing(reverse) && reverse == "true"
+ cfx[1]["reverse"] = "1"
+ end
+ for (i, (type, val, gte)) in enumerate(list)
+ if !isnothing(val)
+ if !isnothing(type) && type == "formula"
+ cfx[1][i+1]["val"] = "(" * val * ")"
+ else
+ cfx[1][i+1]["val"] = val
+ end
+ end
+ if !isnothing(type)
+ cfx[1][i+1]["type"] = type
+ end
+ if !isnothing(gte) && gte == "false"
+ cfx[1][i+1]["gte"] = "0"
+ end
+ end
+ update_worksheet_cfx!(allcfs, cfx, ws, rng)
+ end
+
+ end
+
+ return 0
+end
+
+setCfDataBar(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setCfDataBar, ws, row, nothing; kw...)
+setCfDataBar(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setCfDataBar, ws, nothing, col; kw...)
+setCfDataBar(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setCfDataBar, ws, nothing, nothing; kw...)
+setCfDataBar(ws::Worksheet, ::Colon; kw...) = process_colon(setCfDataBar, ws, nothing, nothing; kw...)
+setCfDataBar(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setCfDataBar(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...)
+setCfDataBar(ws::Worksheet, cell::CellRef; kw...) = setCfDataBar(ws, CellRange(cell, cell); kw...)
+setCfDataBar(ws::Worksheet, cell::SheetCellRef; kw...) = do_sheet_names_match(ws, cell) && setCfDataBar(ws, CellRange(cell.cellref, cell.cellref); kw...)
+setCfDataBar(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setCfDataBar(ws, rng.rng; kw...)
+setCfDataBar(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setCfDataBar(ws, rng.colrng; kw...)
+setCfDataBar(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setCfDataBar(ws, rng.rowrng; kw...)
+setCfDataBar(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCfDataBar, ws, rng; kw...)
+setCfDataBar(ws::Worksheet, rng::ColumnRange; kw...) = process_columnranges(setCfDataBar, ws, rng; kw...)
+setCfDataBar(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetcell(setCfDataBar, xl, sheetcell; kw...)
+setCfDataBar(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setCfDataBar, ws, ref_or_rng; kw...)
+function setCfDataBar(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}=())::Int
+
+ databar::Union{Nothing,String}="bluegrad"
+ showVal::Union{Nothing,String}=nothing
+ gradient::Union{Nothing,String}=nothing
+ borders::Union{Nothing,String}=nothing
+ sameNegFill::Union{Nothing,String}=nothing
+ sameNegBorders::Union{Nothing,String}=nothing
+ direction::Union{Nothing,String}=nothing
+ axis_pos::Union{Nothing,String}=nothing
+ axis_col::Union{Nothing,String}=nothing
+ min_type::Union{Nothing,String}=nothing
+ min_val::Union{Nothing,String}=nothing
+ max_type::Union{Nothing,String}=nothing
+ max_val::Union{Nothing,String}=nothing
+ fill_col::Union{Nothing,String}=nothing
+ border_col::Union{Nothing,String}=nothing
+ neg_fill_col::Union{Nothing,String}=nothing
+ neg_border_col::Union{Nothing,String}=nothing
+
+ for (k, v) in allkws
+ if k == :databar
+ databar = v
+ elseif k == :showVal
+ showVal = v
+ elseif k == :gradient
+ gradient = v
+ elseif k == :borders
+ borders = v
+ elseif k == :sameNegFill
+ sameNegFill = v
+ elseif k == :sameNegBorders
+ sameNegBorders = v
+ elseif k == :direction
+ direction = v
+ elseif k == :axis_pos
+ axis_pos = v
+ elseif k == :axis_col
+ axis_col = v
+ elseif k == :min_type
+ min_type = v
+ elseif k == :min_val
+ min_val = v
+ elseif k == :max_type
+ max_type = v
+ elseif k == :max_val
+ max_val = v
+ elseif k == :fill_col
+ fill_col = v
+ elseif k == :border_col
+ border_col = v
+ elseif k == :neg_fill_col
+ neg_fill_col = v
+ elseif k == :neg_border_col
+ neg_border_col = v
+ else
+ throw(XLSXError("Invalid keyword argument: $k. Valid options are: `databar`, `showVal`, `gradient`, `borders`, `sameNegFill`, `sameNegBorders`, `direction`, `axis_pos`, `axis_col`, `min_type`, `min_val`, `max_type`, `max_val`, `fill_col`, `border_col`, `neg_fill_col`, `neg_border_col`."))
+ end
+ end
+
+ !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension ($(get_dimension(ws)))."))
+
+ allcfs = allCfs(ws) # get all conditional format blocks
+ old_cf = getConditionalFormats(ws) # extract conditional format info
+ allextcfs = allExtCfs(ws) # get all extended conditional format blocks
+
+ let new_pr, new_cf
+
+ new_pr = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : "1"
+ isnothing(min_type) || min_type in ["least", "percentile", "percent", "num", "formula", "automatic"] || throw(XLSXError("Invalid min_type: $min_type. Valid options are: least, percentile, percent, num, formula."))
+ if min_type in ["least", "automatic"]
+ min_val = nothing
+ end
+ (!isnothing(min_type) && min_type == "formula") || isnothing(min_val) || is_valid_fixed_cellname(min_val) || is_valid_fixed_sheet_cellname(min_val) || !isnothing(tryparse(Float64, min_val)) || throw(XLSXError("Invalid min_val: `$min_val`. Valid options (unless min_type is `formula`) are a CellRef (e.g. `\$A\$1`) or a number."))
+ isnothing(max_type) || max_type in ["highest", "percentile", "percent", "num", "formula", "automatic"] || throw(XLSXError("Invalid max_type: $max_type. Valid options are: highest, percentile, percent, num, formula."))
+ if min_type in ["highest", "automatic"]
+ max_val = nothing
+ end
+ (!isnothing(max_type) && max_type == "formula") || isnothing(max_val) || is_valid_fixed_cellname(max_val) || is_valid_fixed_sheet_cellname(max_val) || !isnothing(tryparse(Float64, max_val)) || throw(XLSXError("Invalid max_val: `$max_val`. Valid options (unless max_type is `formula`) are a CellRef (e.g. `\$A\$1`) or a number."))
+
+ for val in [min_val, max_val]
+ if !isnothing(val)
+ if is_valid_fixed_sheet_cellname(val)
+ do_sheet_names_match(ws, SheetCellRef(val))
+ val = string(SheetCellRef(val).cellref)
+ end
+ val = XML.escape(uppercase_unquoted(val))
+ end
+ end
+ if !haskey(databars, databar)
+ throw(XLSXError("Invalid dataBar option chosen: $databar. Valid options are: $(keys(databars))"))
+ end
+
+ allkws::Dict{String,Union{String,Nothing}} = Dict(
+ "showVal" => showVal,
+ "gradient" => gradient,
+ "borders" => borders,
+ "sameNegFill" => sameNegFill,
+ "sameNegBorders" => sameNegBorders,
+ "direction" => direction,
+ "min_type" => min_type,
+ "min_val" => min_val,
+ "max_type" => max_type,
+ "max_val" => max_val,
+ "fill_col" => fill_col,
+ "border_col" => border_col,
+ "neg_fill_col" => neg_fill_col,
+ "neg_border_col" => neg_border_col,
+ "axis_pos" => axis_pos,
+ "axis_col" => axis_col
+ )
+
+ for (k, w) in allkws # Allow user input to override any default value
+ if isnothing(w)
+ if haskey(databars[databar], k)
+ allkws[k] = databars[databar][k]
+ end
+ end
+ end
+ for kw in ["showVal", "gradient", "borders", "sameNegFill", "sameNegBorders"]
+ haskey(allkws, kw) && isValidKw(kw, allkws[kw], ["true", "false"])
+ end
+ haskey(allkws, "direction") && isValidKw("direction", allkws["direction"], ["leftToRight", "rightToLeft"])
+ haskey(allkws, "axis_pos") && isValidKw("axis_pos", allkws["axis_pos"], ["middle", "none"])
+
+ # Define basic elements of dataBar definition
+ id = "{" * uppercase(string(UUIDs.uuid4())) * "}"
+ mnt = allkws["min_type"] ∈ ["automatic", "least"] ? "min" : allkws["min_type"]
+ mxt = allkws["max_type"] ∈ ["automatic", "highest"] ? "max" : allkws["max_type"]
+ cfx = XML.h.cfRule(type="dataBar", priority=new_pr,
+ XML.h.dataBar(
+ isnothing(allkws["min_val"]) ? XML.h.cfvo(type=mnt) : XML.h.cfvo(type=mnt, val=allkws["min_val"]),
+ isnothing(allkws["max_val"]) ? XML.h.cfvo(type=mxt) : XML.h.cfvo(type=mxt, val=allkws["max_val"]),
+ XML.h.color(rgb=get_color(allkws["fill_col"]))),
+ XML.h.extLst()
+ )
+ if haskey(allkws, "showVal") && !isnothing(allkws["showVal"]) && allkws["showVal"] == "false"
+ cfx[1]["showValue"] = "0"
+ end
+ cfx_ext = XML.Element("ext") # This establishes link (via id) to the extension elements
+ cfx_ext["xmlns:x14"] = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"
+ cfx_ext["uri"] = "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}"
+ push!(cfx_ext, XML.Element("x14:id", XML.Text(id)))
+ push!(cfx[end], cfx_ext)
+
+ # Define extension elements of dataBar definition
+ emnt = allkws["min_type"] == "automatic" ? "autoMin" : allkws["min_type"] == "least" ? "min" : allkws["min_type"]
+ emxt = allkws["max_type"] == "automatic" ? "autoMax" : allkws["max_type"] == "highest" ? "max" : allkws["max_type"]
+ emnv = allkws["min_type"] == "formula" ? "(" * allkws["min_val"] * ")" : allkws["min_val"]
+ emxv = allkws["max_type"] == "formula" ? "(" * allkws["max_val"] * ")" : allkws["max_val"]
+ ext_cfx = XML.Element("x14:cfRule", type="dataBar", id=id)
+ ext_db = XML.Element("x14:dataBar", minLength="0", maxLength="100")
+ valmin = XML.Element("x14:cfvo", type=emnt)
+ !isnothing(emnv) && push!(valmin, XML.Element("xm:f", XML.Text(emnv)))
+ valmax = XML.Element("x14:cfvo", type=emxt)
+ !isnothing(emxv) && push!(valmax, XML.Element("xm:f", XML.Text(emxv)))
+ push!(ext_db, valmin)
+ push!(ext_db, valmax)
+ if allkws["gradient"] == "false"
+ ext_db["gradient"] = "0"
+ end
+ do_borders = haskey(allkws, "borders") && allkws["borders"] == "true"
+ if do_borders
+ ext_db["border"] = "1"
+ if haskey(allkws, "border_col") && !isnothing(allkws["border_col"])
+ push!(ext_db, XML.Element("x14:borderColor", rgb=get_color(allkws["border_col"])))
+ else
+ push!(ext_db, XML.Element("x14:borderColor", rgb=get_color("FF638EC6"))) # Default colour
+ end
+ end
+ if haskey(allkws, "direction")
+ if allkws["direction"] == "leftToRight"
+ ext_db["direction"] = "leftToRight"
+ elseif allkws["direction"] == "rightToLeft"
+ ext_db["direction"] = "rightToLeft"
+ end
+ end
+ if haskey(allkws, "sameNegFill") && allkws["sameNegFill"] == "true"
+ ext_db["negativeBarColorSameAsPositive"] = "1"
+ else
+ if haskey(allkws, "neg_fill_col") && !isnothing(allkws["neg_fill_col"])
+ push!(ext_db, XML.Element("x14:negativeFillColor", rgb=get_color(allkws["neg_fill_col"])))
+ else
+ push!(ext_db, XML.Element("x14:negativeFillColor", rgb=get_color("FFFF0000"))) # Default colour
+ end
+ end
+ if do_borders && haskey(allkws, "sameNegBorders") && allkws["sameNegBorders"] == "false"
+ ext_db["negativeBarBorderColorSameAsPositive"] = "0"
+ if haskey(allkws, "neg_border_col") && !isnothing(allkws["neg_border_col"])
+ push!(ext_db, XML.Element("x14:negativeBorderColor", rgb=get_color(allkws["neg_border_col"])))
+ else
+ push!(ext_db, XML.Element("x14:negativeBorderColor", rgb=get_color("FFFF0000"))) # Default colour
+ end
+ end
+ if haskey(allkws, "axis_pos")
+ if allkws["axis_pos"] == "none"
+ ext_db["axisPosition"] = "none"
+ elseif allkws["axis_pos"] == "middle"
+ ext_db["axisPosition"] = "middle"
+ end
+ end
+ haskey(allkws, "axis_col") && push!(ext_db, XML.Element("x14:axisColor", rgb=get_color(allkws["axis_col"])))
+ push!(ext_cfx, ext_db)
+
+ update_worksheet_cfx!(allcfs, cfx, ws, rng) # Add basic elements to worksheet xml file
+ update_worksheet_ext_cfx!(allextcfs, ext_cfx, ws, rng) # Add extension elements to worksheet xml file
+ end
+ return 0
+end
\ No newline at end of file
diff --git a/src/read.jl b/src/read.jl
index 51a77c07..15744738 100644
--- a/src/read.jl
+++ b/src/read.jl
@@ -17,20 +17,81 @@ function check_for_xlsx_file_format(source::IO, label::AbstractString="input")
if header == ZIP_FILE_HEADER # valid Zip file header
return
elseif header == XLS_FILE_HEADER # old XLS file
- error("$label looks like an old XLS file (not XLSX). This package does not support XLS file format.")
+ throw(XLSXError("$label looks like an old XLS file (not XLSX). This package does not support XLS file format."))
else
- error("$label is not a valid XLSX file.")
+ throw(XLSXError("$label is not a valid XLSX file."))
end
end
function check_for_xlsx_file_format(filepath::AbstractString)
- @assert isfile(filepath) "File $filepath not found."
+ !isfile(filepath) && throw(XLSXError("File $filepath not found."))
open(filepath, "r") do io
check_for_xlsx_file_format(io, filepath)
end
end
+
+"""
+ opentemplate(source::Union{AbstractString, IO}) :: XLSXFile
+
+Read an existing Excel (`.xlsx`) file as a template and return as a writable `XLSXFile` for editing
+and saving to another file with [XLSX.writexlsx](@ref).
+
+A convenience function equivalent to `openxlsx(source; mode="rw", enable_cache=true)`
+
+!!! note
+ XLSX.jl only works with `.xlsx` files and cannot work with Excel `.xltx` template files.
+ Reading as a template in this package merely means opening a `.xlsx` file to edit, update
+ and then write as an updated `.xlsx` file (e.g. using `XLSX.writexlsx()`). Doing so retains
+ the formatting and layout of the opened file, but this is not the same as using a `.xltx` file.
+
+# Examples
+```julia
+julia> xf = opentemplate("myExcelFile.xlsx")
+```
+
+"""
+opentemplate(source::Union{AbstractString,IO})::XLSXFile = open_or_read_xlsx(source, true, true, true)
+
+@inline open_xlsx_template(source::Union{AbstractString,IO})::XLSXFile = open_or_read_xlsx(source, true, true, true)
+
+function _relocatable_data_path(; path::AbstractString=Artifacts.artifact"XLSX_relocatable_data")
+ return path
+end
+
+"""
+ newxlsx() :: XLSXFile
+
+Return an empty, writable `XLSXFile` with 1 worksheet for editing and
+subsequent saving to a file with [XLSX.writexlsx](@ref).
+By default, the worksheet is `Sheet1`. Specify `sheetname` to give the worksheet a different name.
+
+# Examples
+```julia
+julia> xf = XLSX.newxlsx()
+```
+
+"""
+newxlsx(sheetname::AbstractString=""; path::AbstractString=_relocatable_data_path())::XLSXFile = open_empty_template(sheetname; path)
+
+function open_empty_template(
+ sheetname::AbstractString="";
+ path::AbstractString=_relocatable_data_path()
+)::XLSXFile
+
+ empty_excel_template = joinpath(path, "blank.xlsx")
+ !isfile(empty_excel_template) && throw(XLSXError("Couldn't find template file $empty_excel_template."))
+ xf = open_xlsx_template(empty_excel_template)
+ xf[1].cache.is_empty = false
+
+ if sheetname != ""
+ rename!(xf[1], sheetname)
+ end
+ xf.source="blank.xlsx"
+ return xf
+end
+
"""
readxlsx(source::Union{AbstractString, IO}) :: XLSXFile
@@ -100,7 +161,7 @@ where `myfile.xlsx` is a spreadsheet that doesn't fit into memory.
```julia
julia> XLSX.openxlsx("myfile.xlsx", enable_cache=false) do xf
- for r in XLSX.eachrow(xf["mysheet"])
+ for r in eachrow(xf["mysheet"])
# read something from row `r`
end
end
@@ -128,24 +189,39 @@ See also [`XLSX.readxlsx`](@ref).
"""
function openxlsx(f::F, source::Union{AbstractString, IO};
mode::AbstractString="r", enable_cache::Bool=true) where {F<:Function}
+ xf = _openxlsx(f, source; mode, enable_cache)
+ if !enable_cache
+ GC.gc() # GC to clean-up after mmap
+ end
+ return xf
+end
+
+function _openxlsx(f::F, source::Union{AbstractString, IO};
+ mode::AbstractString="r", enable_cache::Bool=true) where {F<:Function}
_read, _write = parse_file_mode(mode)
if _read
- @assert source isa IO || isfile(source) "File $source not found."
- xf = open_or_read_xlsx(source, _write, enable_cache, _write) # Why _write, _write here???
+ if !(source isa IO || isfile(source))
+ throw(XLSXError("File $source not found."))
+ end
+ xf = open_or_read_xlsx(source, _read, enable_cache, _write; use_stream=!enable_cache)
else
xf = open_empty_template()
+ xf.source = source
end
try
f(xf)
- finally
+ finally
+ if !isnothing(xf.stream) # close the mmap stream if it was opened
+ close(xf.stream)
+ xf.stream = nothing
+ end
if _write
writexlsx(source, xf, overwrite=true)
end
-
end
end
@@ -153,8 +229,6 @@ end
openxlsx(source::Union{AbstractString, IO}; mode="r", enable_cache=true) :: XLSXFile
Supports opening a XLSX file without using do-syntax.
-In this case, the user is responsible for closing the `XLSXFile`
-using `close` or writing it to file using `XLSX.writexlsx`.
See also [`XLSX.writexlsx`](@ref).
"""
@@ -165,10 +239,14 @@ function openxlsx(source::Union{AbstractString, IO};
_read, _write = parse_file_mode(mode)
if _read
- @assert source isa IO || isfile(source) "File $source not found."
- return open_or_read_xlsx(source, _write, enable_cache, _write) # Why _write, _write here???
+ if !(source isa IO || isfile(source))
+ throw(XLSXError("File $source not found."))
+ end
+ return open_or_read_xlsx(source, _read, enable_cache, _write)
else
- return open_empty_template()
+ xf = open_empty_template()
+ xf.source = source
+ return xf
end
end
@@ -180,17 +258,17 @@ function parse_file_mode(mode::AbstractString) :: Tuple{Bool, Bool}
elseif mode == "rw" || mode == "wr"
return (true, true)
else
- error("Couldn't parse file mode $mode.")
+ throw(XLSXError("Couldn't parse file mode $mode."))
end
end
-function open_or_read_xlsx(source::Union{IO, AbstractString}, read_files::Bool, enable_cache::Bool, read_as_template::Bool) :: XLSXFile
+function open_or_read_xlsx(source::Union{IO, AbstractString}, read_files::Bool, enable_cache::Bool, read_as_template::Bool; use_stream::Bool=false) :: XLSXFile
# sanity check
if read_as_template
- @assert read_files && enable_cache
+ !(read_files && enable_cache) && throw(XLSXError("Cache must be enabled for files in `write` mode."))
end
-
- xf = XLSXFile(source, enable_cache, read_as_template)
+ xf = XLSXFile(source, enable_cache, read_as_template; use_stream)
+
for f in ZipArchives.zip_names(xf.io)
@@ -225,17 +303,17 @@ function open_or_read_xlsx(source::Union{IO, AbstractString}, read_files::Bool,
parse_workbook!(xf)
# read data from Worksheet streams
- if read_files
+ if enable_cache
for sheet_name in sheetnames(xf)
sheet = getsheet(xf, sheet_name)
- # to read sheet content, we just need to iterate a SheetRowIterator and the data will be stored in cache
- for r in eachrow(sheet)
+ # to read sheet content, we just need to iterate a SheetRowIterator and the data will be stored in cache
+ for _ in eachrow(sheet)
nothing
end
+ isnothing(sheet.dimension) && get_dimension(sheet) # Get sheet dimension from the cell cache if not specified in the `xlsx` file.
end
end
-
if read_as_template
wb = get_workbook(xf)
if has_sst(wb)
@@ -273,7 +351,7 @@ function get_default_namespace(r::XML.Node) :: String
end
end
- error("No default namespace found.")
+ throw(XLSXError("No default namespace found."))
end
# See section 12.2 - Package Structure
@@ -285,7 +363,23 @@ function check_minimum_requirements(xf::XLSXFile)
]
for f in mandatory_files
- @assert in(f, filenames(xf)) "Malformed XLSX File. Couldn't find file $f in the package."
+ !in(f, filenames(xf)) && throw(XLSXError("Malformed XLSX File. Couldn't find file $f in the package."))
+ end
+
+ # Further check if this is a valid `.xlsx` file.
+ f = "[Content_Types].xml"
+ if internal_xml_file_isread(xf, f)
+ content_types = XML.write(xf.data[f])
+ else
+ content_types = ZipArchives.zip_readentry(xf.io, f, String)
+ end
+
+ if occursin("spreadsheetml.sheet", content_types)
+ return nothing
+ elseif occursin("spreadsheetml.template", content_types)
+ throw(XLSXError("XLSX.jl does not support Excel template files (`.xltx` files).\nSave template as an `xlsx` file type first."))
+ else
+ throw(XLSXError("Unknown Excel file type."))
end
nothing
@@ -300,7 +394,7 @@ function parse_relationships!(xf::XLSXFile)
for el in XML.children(xroot)
push!(xf.relationships, Relationship(el))
end
- @assert !isempty(xf.relationships) "Relationships not found in _rels/.rels!"
+ isempty(xf.relationships) && throw(XLSXError("Relationships not found in _rels/.rels!"))
# workbook level relationships
wb = get_workbook(xf)
@@ -308,7 +402,7 @@ function parse_relationships!(xf::XLSXFile)
for el in XML.children(xroot)
push!(wb.relationships, Relationship(el))
end
- @assert !isempty(wb.relationships) "Relationships not found in xl/_rels/workbook.xml.rels"
+ isempty(wb.relationships) && throw(XLSXError("Relationships not found in xl/_rels/workbook.xml.rels"))
nothing
end
@@ -317,7 +411,7 @@ end
function parse_workbook!(xf::XLSXFile)
xroot = xmlroot(xf, "xl/workbook.xml")[end]
chn=XML.children(xroot)
- @assert XML.tag(xroot) == "workbook" "Malformed xl/workbook.xml. Root node name should be 'workbook'. Got '$(XML.tag(xroot))'."
+ XML.tag(xroot) != "workbook" && throw(XLSXError("Malformed xl/workbook.xml. Root node name should be 'workbook'. Got '$(XML.tag(xroot))'."))
# workbook to be parsed
workbook = get_workbook(xf)
@@ -340,7 +434,7 @@ function parse_workbook!(xf::XLSXFile)
elseif attribute_value_date1904 == "0" || attribute_value_date1904 == "false"
workbook.date1904 = false
else
- error("Could not parse xl/workbook -> workbookPr -> date1904 = $(attribute_value_date1904).")
+ throw(XLSXError("Could not parse xl/workbook -> workbookPr -> date1904 = $(attribute_value_date1904)."))
end
end
end
@@ -356,7 +450,7 @@ function parse_workbook!(xf::XLSXFile)
if XML.tag(node) == "sheets"
for sheet_node in XML.children(node)
- @assert XML.tag(sheet_node) == "sheet" "Unsupported node $(XML.tag(sheet_node)) in node $(XML.tag(node)) in 'xl/workbook.xml'."
+ XML.tag(sheet_node) != "sheet" && throw(XLSXError("Unsupported node $(XML.tag(sheet_node)) in node $(XML.tag(node)) in 'xl/workbook.xml'."))
worksheet = Worksheet(xf, sheet_node)
push!(sheets, worksheet)
end
@@ -377,31 +471,52 @@ function parse_workbook!(xf::XLSXFile)
name = XML.attributes(defined_name_node)["name"]
local defined_value::DefinedNameValueTypes
-
- if is_valid_fixed_sheet_cellname(defined_value_string) || is_valid_sheet_cellname(defined_value_string)
- defined_value = SheetCellRef(defined_value_string)
- elseif is_valid_fixed_sheet_cellrange(defined_value_string) || is_valid_sheet_cellrange(defined_value_string)
- defined_value = SheetCellRange(defined_value_string)
+ if is_valid_non_contiguous_range(defined_value_string)
+ defined_value = NonContiguousRange(unquoteit(defined_value_string))
+ isabs=Vector{Bool}(undef,length(defined_value.rng))
+ for (i, d) in enumerate(split(defined_value_string, ","))
+ isabs[i]=is_valid_fixed_sheet_cellname(d) || is_valid_fixed_sheet_cellrange(d)
+ end
+ length(isabs) != length(defined_value.rng) && throw(XLSXError("Error parsing absolute references in non-contiguous range."))
+ elseif is_valid_fixed_sheet_cellname(defined_value_string)
+ defined_value = SheetCellRef(unquoteit(defined_value_string))
+ isabs=true
+ elseif is_valid_sheet_cellname(defined_value_string)
+ defined_value = SheetCellRef(unquoteit(defined_value_string))
+ isabs=false
+ elseif is_valid_fixed_sheet_cellrange(defined_value_string)
+ defined_value = SheetCellRange(unquoteit(defined_value_string))
+ isabs=true
+ elseif is_valid_sheet_cellrange(defined_value_string)
+ defined_value = SheetCellRange(unquoteit(defined_value_string))
+ isabs=false
elseif occursin(r"^\".*\"$", defined_value_string) # is enclosed by quotes
defined_value = defined_value_string[2:end-1] # remove enclosing quotes
if isempty(defined_value)
defined_value = missing
end
+ isabs=false
elseif tryparse(Int, defined_value_string) !== nothing
defined_value = parse(Int, defined_value_string)
+ isabs=false
elseif tryparse(Float64, defined_value_string) !== nothing
defined_value = parse(Float64, defined_value_string)
+ isabs=false
elseif isempty(defined_value_string)
defined_value = missing
+ isabs=false
else
# Couldn't parse definedName. Will silently ignore it, since this is not a critical feature.
- continue
+ # Actually is just interpreted as a string anyway and added to the defined names (is this true?).
+ defined_value = string(defined_value_string)
+ isabs=false
+ #continue
- # debug
- #error("Could not parse value $(defined_value_string) for definedName $name.")
+ # debug - Now more important since we are writing updated defined names to back to output file.
+ # throw(XLSXError("Could not parse value $(defined_value_string) for definedName $name."))
end
- a = XML.attributes(defined_name_node)
+ a = XML.attributes(defined_name_node)
if haskey(a,"localSheetId")
# is a Worksheet level name
@@ -410,10 +525,10 @@ function parse_workbook!(xf::XLSXFile)
# Which is the order of the elements under element in workbook.xml .
localSheetId = parse(Int, a["localSheetId"])+1
sheetId = workbook.sheets[localSheetId].sheetId
- workbook.worksheet_names[(sheetId, name)] = defined_value
+ workbook.worksheet_names[(sheetId, name)] = DefinedNameValue(defined_value, isabs)
else
# is a Workbook level name
- workbook.workbook_names[name] = defined_value
+ workbook.workbook_names[name] = DefinedNameValue(defined_value, isabs)
end
end
@@ -435,26 +550,40 @@ end
@inline internal_xml_file_exists(xl::XLSXFile, filename::String) :: Bool = haskey(xl.files, filename)
function internal_xml_file_add!(xl::XLSXFile, filename::String)
- @assert endswith(filename, ".xml") || endswith(filename, ".rels")
+ !(endswith(filename, ".xml") || endswith(filename, ".rels")) && throw(XLSXError("Something wrong here!"))
xl.files[filename] = false
nothing
end
+function strip_bom_and_lf!(bytes::Vector{UInt8})
+ # Issue 243 - Need to remove BOM characters that precede the XML declaration.
+ bom = UInt8[0xEF, 0xBB, 0xBF]
+ l=length(bytes)
+ if l >= 3 && bytes[1:3] == bom
+ if l > 3 && bytes[4] == 0x0A
+ deleteat!(bytes, 1:4)
+ else
+ deleteat!(bytes, 1:3)
+ end
+ end
+end
function internal_xml_file_read(xf::XLSXFile, filename::String) :: XML.Node
- @assert internal_xml_file_exists(xf, filename) "Couldn't find $filename in $(xf.source)."
+
+ !internal_xml_file_exists(xf, filename) && throw(XLSXError("Couldn't find $filename in $(xf.source)."))
if !internal_xml_file_isread(xf, filename)
try
- xf.data[filename] = XML.Node(XML.Raw(ZipArchives.zip_readentry(xf.io, filename)))
+ bytes = ZipArchives.zip_readentry(xf.io, filename)
+ strip_bom_and_lf!(bytes)
+ xf.data[filename] = XML.Node(XML.Raw(bytes))
xf.files[filename] = true # set file as read
catch err
- @error("Failed to parse internal XML file `$filename`")
- rethrow()
-
+ throw(XLSXError("Failed to parse internal XML file `$filename`"))
end
end
+
return xf.data[filename]
end
@@ -473,7 +602,10 @@ end
readdata(source, sheet, ref)
readdata(source, sheetref)
-Returns a scalar or matrix with values from a spreadsheet.
+Return a scalar, vector or matrix with values from a spreadsheet file.
+'ref' can be a defined name, a cell reference or a cell, column, row
+or non-contiguous range.
+
See also [`XLSX.getdata`](@ref).
@@ -500,11 +632,23 @@ julia> XLSX.readdata("myfile.xlsx", "mysheet!A2:B4")
2 "second"
3 "third"
```
+
+Non-contiguous ranges return vectors.
+
+```julia
+julia> XLSX.readdata("customXml.xlsx", "Mock-up", "Location") # `Location` is a `definedName` for a non-contiguous range
+4-element Vector{Any}:
+ "Here"
+ missing
+ missing
+ missing
+```
"""
function readdata(source::Union{AbstractString, IO}, sheet::Union{AbstractString, Int}, ref)
c = openxlsx(source, enable_cache=false) do xf
getdata(getsheet(xf, sheet), ref)
end
+# GC.gc()
return c
end
@@ -512,30 +656,35 @@ function readdata(source::Union{AbstractString, IO}, sheetref::AbstractString)
c = openxlsx(source, enable_cache=false) do xf
getdata(xf, sheetref)
end
+# GC.gc()
return c
end
"""
readtable(
source,
- sheet,
- [columns];
+ [sheet,
+ [columns]];
[first_row],
[column_labels],
[header],
[infer_eltypes],
[stop_in_empty_row],
[stop_in_row_function],
- [keep_empty_rows]
+ [keep_empty_rows],
+ [normalizenames]
) -> DataTable
Returns tabular data from a spreadsheet as a struct `XLSX.DataTable`.
Use this function to create a `DataFrame` from package `DataFrames.jl`.
+If `sheet` is not given, the first sheet in the `XLSXFile` will be used.
+
Use `columns` argument to specify which columns to get.
For example, `"B:D"` will select columns `B`, `C` and `D`.
If `columns` is not given, the algorithm will find the first sequence
-of consecutive non-empty cells.
+of consecutive non-empty cells. A valid `sheet` must be specified
+when specifying `columns`.
Use `first_row` to indicate the first row from the table.
`first_row=5` will look for a table starting at sheet row `5`.
@@ -550,27 +699,36 @@ will generate column labels. The default value is `header=true`.
Use `column_labels` to specify names for the header of the table.
+Use `normalizenames=true` to normalize column names to valid Julia identifiers.
+
Use `infer_eltypes=true` to get `data` as a `Vector{Any}` of typed vectors.
-The default value is `infer_eltypes=false`.
+The default value is `infer_eltypes=true`.
-`stop_in_empty_row` is a boolean indicating whether an empty row marks the end of the table.
-If `stop_in_empty_row=false`, the `TableRowIterator` will continue to fetch rows until there's no more rows in the Worksheet.
+`stop_in_empty_row` is a boolean indicating whether an empty row marks the
+end of the table. If `stop_in_empty_row=false`, the `TableRowIterator` will
+continue to fetch rows until there's no more rows in the Worksheet or range.
The default behavior is `stop_in_empty_row=true`.
-`stop_in_row_function` is a Function that receives a `TableRow` and returns a `Bool` indicating if the end of the table was reached.
+`stop_in_row_function` is a Function that receives a `TableRow` and returns
+ a `Bool` indicating if the end of the table was reached.
Example for `stop_in_row_function`:
-```
+```julia
function stop_function(r)
v = r[:col_label]
return !ismissing(v) && v == "unwanted value"
end
```
-`keep_empty_rows` determines whether rows where all column values are equal to `missing` are kept (`true`) or dropped (`false`) from the resulting table.
-`keep_empty_rows` never affects the *bounds* of the table; the number of rows read from a sheet is only affected by, `first_row`, `stop_in_empty_row` and `stop_in_row_function` (if specified).
-`keep_empty_rows` is only checked once the first and last row of the table have been determined, to see whether to keep or drop empty rows between the first and the last row.
+`keep_empty_rows` determines whether rows where all column values are equal
+to `missing` are kept (`true`) or dropped (`false`) from the resulting table.
+`keep_empty_rows` never affects the *bounds* of the table; the number of
+rows read from a sheet is only affected by `first_row`, `stop_in_empty_row`
+and `stop_in_row_function` (if specified).
+`keep_empty_rows` is only checked once the first and last row of the table
+have been determined, to see whether to keep or drop empty rows between the
+first and the last row.
# Example
@@ -582,16 +740,87 @@ julia> df = DataFrame(XLSX.readtable("myfile.xlsx", "mysheet"))
See also: [`XLSX.gettable`](@ref).
"""
-function readtable(source::Union{AbstractString, IO}, sheet::Union{AbstractString, Int}; first_row::Union{Nothing, Int} = nothing, column_labels=nothing, header::Bool=true, infer_eltypes::Bool=false, stop_in_empty_row::Bool=true, stop_in_row_function::Union{Nothing, Function}=nothing, enable_cache::Bool=false, keep_empty_rows::Bool=false)
- c = openxlsx(source, enable_cache=enable_cache) do xf
- gettable(getsheet(xf, sheet); first_row=first_row, column_labels=column_labels, header=header, infer_eltypes=infer_eltypes, stop_in_empty_row=stop_in_empty_row, stop_in_row_function=stop_in_row_function, keep_empty_rows=keep_empty_rows)
+function readtable(source::Union{AbstractString, IO}; first_row::Union{Nothing, Int} = nothing, column_labels=nothing, header::Bool=true, infer_eltypes::Bool=true, stop_in_empty_row::Bool=true, stop_in_row_function::Union{Nothing, Function}=nothing, enable_cache::Bool=false, keep_empty_rows::Bool=false, normalizenames::Bool=false)
+ c = openxlsx(source; enable_cache) do xf
+ gettable(getsheet(xf, 1); first_row, column_labels, header, infer_eltypes, stop_in_empty_row, stop_in_row_function, keep_empty_rows, normalizenames)
+ end
+ return c
+end
+function readtable(source::Union{AbstractString, IO}, sheet::Union{AbstractString, Int}; first_row::Union{Nothing, Int} = nothing, column_labels=nothing, header::Bool=true, infer_eltypes::Bool=true, stop_in_empty_row::Bool=true, stop_in_row_function::Union{Nothing, Function}=nothing, enable_cache::Bool=false, keep_empty_rows::Bool=false, normalizenames::Bool=false)
+ c = openxlsx(source; enable_cache) do xf
+ gettable(getsheet(xf, sheet); first_row, column_labels, header, infer_eltypes, stop_in_empty_row, stop_in_row_function, keep_empty_rows, normalizenames)
end
return c
end
-function readtable(source::Union{AbstractString, IO}, sheet::Union{AbstractString, Int}, columns::Union{ColumnRange, AbstractString}; first_row::Union{Nothing, Int} = nothing, column_labels=nothing, header::Bool=true, infer_eltypes::Bool=false, stop_in_empty_row::Bool=true, stop_in_row_function::Union{Nothing, Function}=nothing, enable_cache::Bool=false, keep_empty_rows::Bool=false)
- c = openxlsx(source, enable_cache=enable_cache) do xf
- gettable(getsheet(xf, sheet), columns; first_row=first_row, column_labels=column_labels, header=header, infer_eltypes=infer_eltypes, stop_in_empty_row=stop_in_empty_row, stop_in_row_function=stop_in_row_function, keep_empty_rows=keep_empty_rows)
+function readtable(source::Union{AbstractString, IO}, sheet::Union{AbstractString, Int}, columns::ColumnRange; first_row::Union{Nothing, Int} = nothing, column_labels=nothing, header::Bool=true, infer_eltypes::Bool=true, stop_in_empty_row::Bool=true, stop_in_row_function::Union{Nothing, Function}=nothing, enable_cache::Bool=false, keep_empty_rows::Bool=false, normalizenames::Bool=false)
+ c = openxlsx(source; enable_cache) do xf
+ gettable(getsheet(xf, sheet), columns; first_row, column_labels, header, infer_eltypes, stop_in_empty_row, stop_in_row_function, keep_empty_rows, normalizenames)
end
return c
end
+
+function readtable(source::Union{AbstractString, IO}, sheet::Union{AbstractString, Int}, range::AbstractString; first_row::Union{Nothing, Int} = nothing, column_labels=nothing, header::Bool=true, infer_eltypes::Bool=true, stop_in_empty_row::Bool=true, stop_in_row_function::Union{Nothing, Function}=nothing, enable_cache::Bool=false, keep_empty_rows::Bool=false, normalizenames::Bool=false)
+ if is_valid_column_range(range)
+ range = ColumnRange(range)
+ else
+ throw(XLSXError("The columns argument must be a valid column range."))
+ end
+ return readtable(source, sheet, range; first_row, column_labels, header, infer_eltypes, stop_in_empty_row, stop_in_row_function, enable_cache, keep_empty_rows, normalizenames)
+end
+
+"""
+ readto(
+ source,
+ [sheet,
+ [columns]],
+ sink;
+ [first_row],
+ [column_labels],
+ [header],
+ [infer_eltypes],
+ [stop_in_empty_row],
+ [stop_in_row_function],
+ [keep_empty_rows],
+ [normalizenames]
+ ) -> sink
+
+Read and parse an Excel worksheet, materializing directly using
+the `sink` function (e.g. `DataFrame` or `StructArray`).
+
+Takes the same keyword arguments as [`XLSX.readtable`](@ref)
+
+# Example
+
+```julia
+julia> using DataFrames, StructArrays, XLSX
+
+julia> df = XLSX.readto("myfile.xlsx", DataFrame)
+
+julia> df = XLSX.readto("myfile.xlsx", StructArray)
+
+julia> df = XLSX.readto("myfile.xlsx", "mysheet", DataFrame)
+
+julia> df = XLSX.readto("myfile.xlsx", "mysheet", "A:C", DataFrame)
+```
+
+See also: [`XLSX.gettable`](@ref).
+"""
+function readto(source::Union{AbstractString, IO}, sheet::Union{AbstractString, Int}, range::AbstractString, sink=nothing; kw...)
+ if sink === nothing
+ throw(XLSXError("provide a valid sink argument, like `using DataFrames; XLSX.readdf(source, sheet, columns, DataFrame)`"))
+ end
+ return Tables.CopiedColumns(readtable(source, sheet, range; kw...)) |> sink
+end
+function readto(source::Union{AbstractString, IO}, sheet::Union{AbstractString, Int}, sink=nothing; kw...)
+ if sink === nothing
+ throw(XLSXError("provide a valid sink argument, like `using DataFrames; XLSX.readdf(source, sheet, DataFrame)`"))
+ end
+ return Tables.CopiedColumns(readtable(source, sheet; kw...)) |> sink
+end
+function readto(source::Union{AbstractString, IO}, sink=nothing; kw...)
+ if sink === nothing
+ throw(XLSXError("provide a valid sink argument, like `using DataFrames; XLSX.readdf(source, sheet, DataFrame)`"))
+ end
+ return Tables.CopiedColumns(readtable(source; kw...)) |> sink
+end
diff --git a/src/relationship.jl b/src/relationship.jl
index b65285d8..d232ddcd 100644
--- a/src/relationship.jl
+++ b/src/relationship.jl
@@ -1,6 +1,6 @@
-function Relationship(e::XML.Node) :: Relationship
- @assert XML.tag(e) == "Relationship" "Unexpected XMLElement: $(XML.tag(e)). Expected: \"Relationship\"."
+function Relationship(e::XML.Node)::Relationship
+ XML.tag(e) != "Relationship" && throw(XLSXError("Unexpected XMLElement: $(XML.tag(e)). Expected: \"Relationship\"."))
a = XML.attributes(e)
return Relationship(
a["Id"],
@@ -9,36 +9,35 @@ function Relationship(e::XML.Node) :: Relationship
)
end
-function parse_relationship_target(prefix::String, target::String) :: String
- @assert !isempty(prefix) && !isempty(target)
-
+function parse_relationship_target(prefix::String, target::String)::String
+ isempty(prefix) || isempty(target) && throw(XLSXError("Something wrong here!"))
if target[1] == '/'
- @assert sizeof(target) > 1 "Incomplete target path $target."
+ sizeof(target) <= 1 && throw(XLSXError("Incomplete target path $target."))
return target[2:end]
else
return prefix * '/' * target
end
end
-function get_relationship_target_by_id(prefix::String, wb::Workbook, Id::String) :: String
+function get_relationship_target_by_id(prefix::String, wb::Workbook, Id::String)::String
for r in wb.relationships
if Id == r.Id
return parse_relationship_target(prefix, r.Target)
end
end
- error("Relationship Id=$(Id) not found")
+ throw(XLSXError("Relationship Id=$(Id) not found"))
end
-function get_relationship_target_by_type(prefix::String, wb::Workbook, _type_::String) :: String
+function get_relationship_target_by_type(prefix::String, wb::Workbook, _type_::String)::String
for r in wb.relationships
if _type_ == r.Type
return parse_relationship_target(prefix, r.Target)
end
end
- error("Relationship Type=$(_type_) not found")
+ throw(XLSXError("Relationship Type=$(_type_) not found"))
end
-function has_relationship_by_type(wb::Workbook, _type_::String) :: Bool
+function has_relationship_by_type(wb::Workbook, _type_::String)::Bool
for r in wb.relationships
if _type_ == r.Type
return true
@@ -47,25 +46,29 @@ function has_relationship_by_type(wb::Workbook, _type_::String) :: Bool
false
end
-function get_package_relationship_root(xf::XLSXFile) :: XML.Node
+function get_package_relationship_root(xf::XLSXFile)::XML.Node
xroot = xmlroot(xf, "_rels/.rels")[end]
- @assert XML.tag(xroot) == "Relationships" "Malformed XLSX file $(xf.source). _rels/.rels root node name should be `Relationships`. Found $(XML.tag(xroot))."
- @assert (""=>"http://schemas.openxmlformats.org/package/2006/relationships") ∈ get_namespaces(xroot) "Unexpected namespace at workbook relationship file: `$(get_namespaces(xroot))`."
+ XML.tag(xroot) != "Relationships" && throw(XLSXError("Malformed XLSX file $(xf.source). _rels/.rels root node name should be `Relationships`. Found $(XML.tag(xroot))."))
+ if ("" => "http://schemas.openxmlformats.org/package/2006/relationships") ∉ get_namespaces(xroot)
+ throw(XLSXError("Unexpected namespace at workbook relationship file: `$(get_namespaces(xroot))`."))
+ end
return xroot
end
-function get_workbook_relationship_root(xf::XLSXFile) :: XML.Node
+function get_workbook_relationship_root(xf::XLSXFile)::XML.Node
xroot = xmlroot(xf, "xl/_rels/workbook.xml.rels")[end]
- @assert XML.tag(xroot) == "Relationships" "Malformed XLSX file $(xf.source). xl/_rels/workbook.xml.rels root node name should be `Relationships`. Found $(XML.tag(xroot))."
- @assert (""=>"http://schemas.openxmlformats.org/package/2006/relationships") ∈ get_namespaces(xroot) "Unexpected namespace at workbook relationship file: `$(get_namespaces(xroot))`."
+ XML.tag(xroot) != "Relationships" && throw(XLSXError("Malformed XLSX file $(xf.source). xl/_rels/workbook.xml.rels root node name should be `Relationships`. Found $(XML.tag(xroot))."))
+ if ("" => "http://schemas.openxmlformats.org/package/2006/relationships") ∉ get_namespaces(xroot)
+ throw(XLSXError("Unexpected namespace at workbook relationship file: `$(get_namespaces(xroot))`."))
+ end
return xroot
end
# Adds new relationship. Returns new generated rId.
-function add_relationship!(wb::Workbook, target::String, _type::String) :: String
+function add_relationship!(wb::Workbook, target::String, _type::String)::String
xf = get_xlsxfile(wb)
- @assert is_writable(xf) "XLSXFile instance is not writable."
- local rId :: String
+ !is_writable(xf) && throws(XLSXError("XLSXFile instance is not writable."))
+ local rId::String
let
got_unique_id = false
@@ -90,7 +93,7 @@ function add_relationship!(wb::Workbook, target::String, _type::String) :: Strin
# adds to XML tree
xroot = get_workbook_relationship_root(xf)
- el = XML.Element("Relationship"; Id = rId, Target = target, Type = _type)
+ el = XML.Element("Relationship"; Id=rId, Type=_type, Target=target)
push!(xroot, el)
return rId
diff --git a/src/sst.jl b/src/sst.jl
index 87ae973c..a5e62d6b 100644
--- a/src/sst.jl
+++ b/src/sst.jl
@@ -10,7 +10,7 @@ SharedStringTable() = SharedStringTable(Vector{String}(), Vector{String}(), Dict
# Returns `nothing` if it's not in the shared string table.
# Returns the index of the string in the shared string table. The index is 0-based.
function get_shared_string_index(sst::SharedStringTable, str_formatted::AbstractString) :: Union{Nothing, Int}
- @assert sst.is_loaded "Can't query shared string table because it's not loaded into memory."
+ !sst.is_loaded && throw(XLSXError("Can't query shared string table because it's not loaded into memory."))
#using a Dict is much more efficient than the findfirst approach especially on large datasets
if haskey(sst.index, str_formatted)
@@ -31,15 +31,17 @@ function add_shared_string!(sst::SharedStringTable, str_unformatted::AbstractStr
push!(sst.formatted_strings, str_formatted)
sst.index[str_formatted] = length(sst.formatted_strings)
new_index = length(sst.formatted_strings) - 1 # 0-based
- @assert new_index == get_shared_string_index(sst, str_formatted) "Inconsistent state after adding a string to the Shared String Table."
+ if new_index != get_shared_string_index(sst, str_formatted)
+ throw(XLSXError("Inconsistent state after adding a string to the Shared String Table."))
+ end
return new_index
end
end
# Adds a string to shared string table. Returns the 0-based index of the shared string in the shared string table.
function add_shared_string!(wb::Workbook, str_unformatted::AbstractString, str_formatted::AbstractString) :: Int
- @assert is_writable(get_xlsxfile(wb)) "XLSXFile instance is not writable."
- @assert !(isempty(str_unformatted) || isempty(str_formatted)) "Can't add empty string to Shared String Table."
+ !is_writable(get_xlsxfile(wb)) && throw(XLSXError("XLSXFile instance is not writable."))
+ (isempty(str_unformatted) || isempty(str_formatted)) && throw(XLSXError("Can't add empty string to Shared String Table."))
sst = get_sst(wb)
if !sst.is_loaded
@@ -53,7 +55,7 @@ function add_shared_string!(wb::Workbook, str_unformatted::AbstractString, str_f
# add Content Type
ctype_root = xmlroot(get_xlsxfile(wb), "[Content_Types].xml")[end]
- @assert XML.tag(ctype_root) == "Types"
+ XML.tag(ctype_root) != "Types" && throw(XLSXError("Something wrong here!"))
override_node = XML.Element("Override";
ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
PartName = "/xl/sharedStrings.xml"
@@ -66,7 +68,11 @@ function add_shared_string!(wb::Workbook, str_unformatted::AbstractString, str_f
end
function add_shared_string!(wb::Workbook, str_unformatted::AbstractString) :: Int
- str_formatted = string("", str_unformatted, "")
+ if startswith(str_unformatted, " ") || endswith(str_unformatted, " ")
+ str_formatted = string("", XML.escape(str_unformatted), "")
+ else
+ str_formatted = string("", XML.escape(str_unformatted), "")
+ end
return add_shared_string!(wb, str_unformatted, str_formatted)
end
@@ -78,14 +84,13 @@ function sst_load!(workbook::Workbook)
relationship_type = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
if has_relationship_by_type(workbook, relationship_type)
sst_root = xmlroot(get_xlsxfile(workbook), get_relationship_target_by_type("xl", workbook, relationship_type))[end]
- @assert XML.tag(sst_root) == "sst"
+ XML.tag(sst_root) != "sst" && throw(XLSXError("Something wrong here!"))
for el in XML.children(sst_root)
XML.nodetype(el) == XML.Text && continue
- @assert XML.tag(el) == "si" "Unsupported node $(XML.tag(el)) in sst table."
+ XML.tag(el) != "si" && throw(XLSXError("Unsupported node $(XML.tag(el)) in sst table."))
push!(sst.unformatted_strings, unformatted_text(el))
push!(sst.formatted_strings, XML.write(el))
-
end
init_sst_index(sst)
@@ -93,7 +98,7 @@ function sst_load!(workbook::Workbook)
return
end
- error("Shared Strings Table not found for this workbook.")
+ throw(XLSXError("Shared Strings Table not found for this workbook."))
end
end
@@ -103,6 +108,13 @@ function has_sst(workbook::Workbook) :: Bool
return has_relationship_by_type(workbook, relationship_type)
end
+
+# work around issue 43 in XML.jl (https://github.com/JuliaComputing/XML.jl/issues/43)
+# Can now write cells containing only whitespace characters or with leading or trailing whitespace.
+# Cannot see these things in cells read in from existing Excel files - XML removes leading whitespace
+# even if `xml:space="preserve"` is specified.
+# Thus a cell containing " " will be read in as missing. A cell containing " hello" will become "hello".
+
# Helper function to gather unformatted text from Excel data files.
# It looks at all children of `el` for tag name `t` and returns
# a join of all the strings found.
@@ -112,9 +124,12 @@ function unformatted_text(el::XML.Node) :: String
if XML.tag(e) == "t"
c = XML.children(e)
if length(c) == 1
- push!(v, XML.value(c[1]))
+ push!(v, XML.is_simple(c[1]) ? XML.simple_value(c[1]) : XML.value(c[1]))
+ elseif length(c) == 0
+ push!(v, isnothing(XML.value(e)) ? "" : XML.is_simple(e) ? XML.simple_value(e) : XML.value(e))
else
- push!(v, XML.write(e))
+# println([i, " => ", XML.write(x) for (i, x) in enumerate(c)])
+ throw(XLSXError("Unexpected number of children in node: $(length(c)). Expected 0 or 1."))
end
end
@@ -129,8 +144,10 @@ function unformatted_text(el::XML.Node) :: String
v_string = Vector{String}()
gather_strings!(v_string, el)
-# println("sst131 : ",join(v_string))
- return join(v_string)
+
+ gatheredstrings=XML.unescape(join(v_string))
+
+ return gatheredstrings
end
# Looks for a string inside the Shared Strings Table (sst).
diff --git a/src/stream.jl b/src/stream.jl
index 6c286291..b1f21c0f 100644
--- a/src/stream.jl
+++ b/src/stream.jl
@@ -20,11 +20,11 @@ end
# About Iterators
* `SheetRowIterator` is an abstract iterator that has `SheetRow` as its elements. `SheetRowStreamIterator` and `WorksheetCache` implements `SheetRowIterator` interface.
-* `SheetRowStreamIterator` is a dumb iterator for row elements in sheetData XML tag of a worksheet.
+* `SheetRowStreamIterator` is a dumb iterator for row elements in sheetData XML tag of a worksheet. Empty rows are not represented in the XML file so cannot be seen by the iterator.
* `WorksheetCache` has a `SheetRowStreamIterator` and caches all values read from the stream.
* `TableRowIterator` is a smart iterator that looks for tabular data, but uses a SheetRowIterator under the hood.
-The implementation of `SheetRowIterator` will be chosen automatically by `XLSX.eachrow` method,
+The implementation of `SheetRowIterator` will be chosen automatically by `eachrow` method,
based on the `enable_cache` option used in `XLSX.openxlsx` method.
=#
@@ -48,9 +48,15 @@ Base.show(io::IO, state::SheetRowStreamIteratorState) = print(io, "SheetRowStrea
# Opens a file for streaming.
@inline function open_internal_file_stream(xf::XLSXFile, filename::String) :: XML.LazyNode
- @assert internal_xml_file_exists(xf, filename) "Couldn't find $filename in $(xf.source)."
- @assert xf.source isa IO || isfile(xf.source) "Can't open internal file $filename for streaming because the XLSX file $(xf.filepath) was not found."
- XML.LazyNode(XML.Raw(ZipArchives.zip_readentry(xf.io, filename)))
+
+ !internal_xml_file_exists(xf, filename) && throw(XLSXError("Couldn't find $filename in $(xf.source)."))
+ if !(xf.source isa IO || isfile(xf.source))
+ throw(XLSXError("Can't open internal file $filename for streaming because the XLSX file $(xf.source) was not found."))
+ end
+
+ lznode=XML.LazyNode(XML.Raw(ZipArchives.zip_readentry(xf.io, filename)))
+
+ return lznode
end
# Creates a reader for row elements in the Worksheet's XML.
@@ -73,8 +79,8 @@ function Base.iterate(itr::SheetRowStreamIterator, state::Union{Nothing, SheetRo
target_file = get_relationship_target_by_id("xl", get_workbook(ws), ws.relationship_id)
reader = open_internal_file_stream(get_xlsxfile(ws), target_file)
- @assert length(reader) > 0 "Couldn't open reader for Worksheet $(ws.name)."
- @assert XML.tag(reader[end]) == "worksheet" "Expecting to find a worksheet node.: Found a $(XML.tag(reader[end]))."
+ length(reader) <= 0 && throw(XLSXError("Couldn't open reader for Worksheet $(ws.name)."))
+ XML.tag(reader[end]) != "worksheet" && throw(XLSXError("Expecting to find a worksheet node.: Found a $(XML.tag(reader[end]))."))
ws_elements = XML.children(reader[end])
idx = findfirst(y -> y=="sheetData", [XML.tag(x) for x in ws_elements])
next_element= idx===nothing ? "" : (ws_elements[idx+1])
@@ -88,7 +94,9 @@ function Base.iterate(itr::SheetRowStreamIterator, state::Union{Nothing, SheetRo
if nrows == 0
return nothing
end
- @assert XML.depth(lznode) == 2 "Malformed Worksheet \"$(ws.name)\": unexpected node depth for sheetData node: $(XML.depth(lznode))."
+
+ XML.depth(lznode) != 2 && throw(XLSXError("Malformed Worksheet \"$(ws.name)\": unexpected node depth for sheetData node: $(XML.depth(lznode))."))
+
break
end
@@ -117,7 +125,7 @@ function Base.iterate(itr::SheetRowStreamIterator, state::Union{Nothing, SheetRo
end
# given that the first iteration case is done in the code above, we shouldn't get it again in here
- @assert state !== nothing "Error processing Worksheet $(ws.name): shouldn't get first iteration case again."
+ state === nothing && throw(XLSXError("Error processing Worksheet $(ws.name): shouldn't get first iteration case again."))
reader = state.itr
lzstate = state.itr_state
@@ -141,7 +149,9 @@ function Base.iterate(itr::SheetRowStreamIterator, state::Union{Nothing, SheetRo
elseif XML.nodetype(lznode) == XML.Element && XML.tag(lznode) == "c" # This is a cell
cell_no += 1
cell = Cell(lznode)
- @assert row_number(cell) == current_row "Error processing Worksheet $(ws.name): Inconsistent state: expected row number $(current_row), but cell has row number $(row_number(cell))"
+ if row_number(cell) != current_row
+ throw(XLSXError("Error processing Worksheet $(ws.name): Inconsistent state: expected row number $(current_row), but cell has row number $(row_number(cell))"))
+ end
rowcells[column_number(cell)] = cell
if cell_no == nc # when all cells found
@@ -162,8 +172,6 @@ function Base.iterate(itr::SheetRowStreamIterator, state::Union{Nothing, SheetRo
state.ht = current_row_ht
state.itr_state = lzstate
-
-
return sheet_row, state
end
@@ -193,39 +201,56 @@ end
#
# WorksheetCache iterator
#
-# The state is the row number. The element is a SheetRow.
+# The state is the row number and a flag for if the cache is full or being filled. The element is a SheetRow.
#
function WorksheetCache(ws::Worksheet)
itr = SheetRowStreamIterator(ws)
- return WorksheetCache(CellCache(), Vector{Int}(), Dict{Int, Union{Float64, Nothing}}(), Dict{Int, Int}(), itr, nothing, true)
+ return WorksheetCache(true, CellCache(), Vector{Int}(), Dict{Int, Union{Float64, Nothing}}(), Dict{Int, Int}(), itr, nothing, true)
end
@inline get_worksheet(r::SheetRow) = r.sheet
@inline get_worksheet(itr::WorksheetCache) = get_worksheet(itr.stream_iterator)
-# In the WorksheetCache iterator, the element is a SheetRow, the state is the row number
-function Base.iterate(ws_cache::WorksheetCache, row_from_last_iteration::Int=0)
+# In the WorksheetCache iterator, the element is a SheetRow, the state is the row number and a flag on whether the cache is already full or not
+function Base.iterate(ws_cache::WorksheetCache, state::Union{Nothing, WorksheetCacheIteratorState}=nothing)
+
+ # If first iteration, check if cache is full
+ if isnothing(state)
+ if is_cache_enabled(ws_cache.stream_iterator.sheet) && ws_cache.is_empty
+ state=WorksheetCacheIteratorState(0, false)
+ ws_cache.is_empty = false
+ else
+ state=WorksheetCacheIteratorState(0, true)
+ end
+ end
- #the sorting operation is very costly when adding row and only needed if we use the row iterator
+ # the sorting operation is very costly when adding row and only needed if we use the row iterator
if ws_cache.dirty
sort!(ws_cache.rows_in_cache)
ws_cache.row_index = Dict{Int, Int}(ws_cache.rows_in_cache[i] => i for i in 1:length(ws_cache.rows_in_cache))
ws_cache.dirty = false
- end
-
- if row_from_last_iteration == 0 && !isempty(ws_cache.rows_in_cache)
- # the next row is in cache, and it's the first one
- current_row_number = ws_cache.rows_in_cache[1]
- current_row_ht = ws_cache.row_ht[current_row_number]
- sheet_row_cells = ws_cache.cells[current_row_number]
- return SheetRow(get_worksheet(ws_cache), current_row_number, current_row_ht, sheet_row_cells), current_row_number
-
- elseif row_from_last_iteration != 0 && ws_cache.row_index[row_from_last_iteration] < length(ws_cache.rows_in_cache)
- # the next row is in cache
- current_row_number = ws_cache.rows_in_cache[ws_cache.row_index[row_from_last_iteration] + 1]
- current_row_ht = ws_cache.row_ht[current_row_number]
- sheet_row_cells = ws_cache.cells[current_row_number]
- return SheetRow(get_worksheet(ws_cache), current_row_number, current_row_ht, sheet_row_cells), current_row_number
+ end
+
+
+ if state.full_cache
+ if state.row_from_last_iteration == 0 && !isempty(ws_cache.rows_in_cache)
+ # the next row is in cache, and it's the first one
+ current_row_number = ws_cache.rows_in_cache[1]
+ current_row_ht = ws_cache.row_ht[current_row_number]
+ sheet_row_cells = ws_cache.cells[current_row_number]
+ state.row_from_last_iteration=current_row_number
+ return SheetRow(get_worksheet(ws_cache), current_row_number, current_row_ht, sheet_row_cells), state
+
+ elseif state.row_from_last_iteration != 0 && ws_cache.row_index[state.row_from_last_iteration] < length(ws_cache.rows_in_cache)
+ # the next row is in cache
+ current_row_number = ws_cache.rows_in_cache[ws_cache.row_index[state.row_from_last_iteration] + 1]
+ current_row_ht = ws_cache.row_ht[current_row_number]
+ sheet_row_cells = ws_cache.cells[current_row_number]
+ state.row_from_last_iteration=current_row_number
+ return SheetRow(get_worksheet(ws_cache), current_row_number, current_row_ht, sheet_row_cells), state
+ else
+ return nothing
+ end
else
next = iterate(ws_cache.stream_iterator, ws_cache.stream_state)
@@ -241,7 +266,9 @@ function Base.iterate(ws_cache::WorksheetCache, row_from_last_iteration::Int=0)
# update stream state
ws_cache.stream_state = next_stream_state
- return sheet_row, row_number(sheet_row)
+ state.row_from_last_iteration=row_number(sheet_row)
+
+ return sheet_row, state
end
end
@@ -251,7 +278,7 @@ function find_row(itr::SheetRowIterator, row::Int) :: SheetRow
return r
end
end
- error("Row $row not found.")
+ throw(XLSXError("Row $row not found."))
end
@@ -276,10 +303,11 @@ function getcell(r::SheetRow, column_index::Int) :: AbstractCell
end
function getcell(r::SheetRow, column_name::AbstractString)
- @assert is_valid_column_name(column_name) "$column_name is not a valid column name."
+ !is_valid_column_name(column_name) && throw(XLSXError("$column_name is not a valid column name."))
return getcell(r, decode_column_number(column_name))
end
+getdata(r::SheetRow, column::Union{Vector{T}, UnitRange{T}}) where {T<:Integer} = [getdata(get_worksheet(r), getcell(r, x)) for x in column]
getdata(r::SheetRow, column) = getdata(get_worksheet(r), getcell(r, column))
Base.getindex(r::SheetRow, x) = getdata(r, x)
@@ -293,7 +321,7 @@ Example: Query all cells from columns 1 to 4.
```julia
left = 1 # 1st column
right = 4 # 4th column
-for sheetrow in XLSX.eachrow(sheet)
+for sheetrow in eachrow(sheet)
for column in left:right
cell = XLSX.getcell(sheetrow, column)
@@ -301,8 +329,15 @@ for sheetrow in XLSX.eachrow(sheet)
end
end
```
+
+Note: The `eachrow` row iterator will not return any row that
+consists entirely of `EmptyCell`s. These are simply not seen
+by the iterator. The `length(eachrow(sheet))` function therefore
+defines the number of rows that are not entirely empty and will,
+in any case, only succeed if the worksheet cache is in use.
"""
function eachrow(ws::Worksheet) :: SheetRowIterator
+
if is_cache_enabled(ws)
if ws.cache === nothing
ws.cache = WorksheetCache(ws)
@@ -316,3 +351,5 @@ end
function Base.isempty(sr::SheetRow)
return isempty(sr.rowcells)
end
+
+Base.length(r::XLSX.WorksheetCache)=length(r.cells)
\ No newline at end of file
diff --git a/src/styles.jl b/src/styles.jl
index 89b42c72..1ca22338 100644
--- a/src/styles.jl
+++ b/src/styles.jl
@@ -42,13 +42,25 @@ default_cell_format(ws::Worksheet, ::Dates.DateTime) = get_num_style_index(ws, D
# Attempts to get CellDataFormat associated with a numFmtId and sets a default style if it is not found
# Use for ensuring default formats exist
function get_num_style_index(ws::Worksheet, numformatid::Integer)
- @assert numformatid >= 0 "Invalid number format id"
+ numformatid < 0 && throw(XLSXError("Invalid number format id"))
wb = get_workbook(ws)
style_index = styles_get_cellXf_with_numFmtId(wb, numformatid)
if isempty(style_index)
# adds default style
- style_index = styles_add_cell_xf(wb, Dict("applyNumberFormat"=>"1", "borderId"=>"0", "fillId"=>"0", "fontId"=>"0", "numFmtId"=>string(numformatid), "xfId"=>"0"))
+ style_index = styles_add_cell_xf(wb, Dict("applyNumberFormat" => "1", "borderId" => "0", "fillId" => "0", "fontId" => "0", "numFmtId" => string(numformatid), "xfId" => "0"))
+ end
+
+ return style_index
+end
+function get_num_style_index(ws::Worksheet, allXfNodes::Vector{XML.Node}, numformatid::Integer)
+ numformatid < 0 && throw(XLSXError("Invalid number format id"))
+
+ wb = get_workbook(ws)
+ style_index = styles_get_cellXf_with_numFmtId(allXfNodes, numformatid)
+ if isempty(style_index)
+ # adds default style
+ style_index = styles_add_cell_xf(wb, Dict("applyNumberFormat" => "1", "borderId" => "0", "fillId" => "0", "fontId" => "0", "numFmtId" => string(numformatid), "xfId" => "0"))
end
return style_index
@@ -63,53 +75,60 @@ function styles_xmlroot(workbook::Workbook)
styles_root = xmlroot(get_xlsxfile(workbook), styles_target)
# check root node name for styles.xml
- @assert get_default_namespace(styles_root[end]) == SPREADSHEET_NAMESPACE_XPATH_ARG "Unsupported styles XML namespace $(get_default_namespace(styles_root[end]))."
- @assert XML.tag(styles_root[end]) == "styleSheet" "Malformed package. Expected root node named `styleSheet` in `styles.xml`."
+ if get_default_namespace(styles_root[end]) != SPREADSHEET_NAMESPACE_XPATH_ARG
+ throw(XLSXError("Unsupported styles XML namespace $(get_default_namespace(styles_root[end]))."))
+ end
+ XML.tag(styles_root[end]) != "styleSheet" && throw(XLSXError("Malformed package. Expected root node named `styleSheet` in `styles.xml`."))
workbook.styles_xroot = styles_root
else
- error("Styles not found for this workbook.")
+ throw(XLSXError("Styles not found for this workbook."))
end
end
return workbook.styles_xroot
end
-
+
# Returns the xf XML node element for style `index`.
# `index` is 0-based.
-function styles_cell_xf(wb::Workbook, index::Int) :: XML.Node
+function styles_cell_xf(wb::Workbook, index::Int)::XML.Node
xroot = styles_xmlroot(wb)
- xf_elements = find_all_nodes("/$SPREADSHEET_NAMESPACE_XPATH_ARG:styleSheet/$SPREADSHEET_NAMESPACE_XPATH_ARG:cellXfs/$SPREADSHEET_NAMESPACE_XPATH_ARG:xf", xroot)
+ xf_elements = find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":cellXfs/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":xf", xroot)
return xf_elements[index+1]
end
+function styles_cell_xf(allXfNodes::Vector{XML.Node}, index::Int)::XML.Node
+ return allXfNodes[index+1]
+end
# Queries numFmtId from cellXfs -> xf nodes."
-function styles_cell_xf_numFmtId(wb::Workbook, index::Int) :: Int
+function styles_cell_xf_numFmtId(wb::Workbook, index::Int)::Int
el = styles_cell_xf(wb, index)
if !haskey(el, "numFmtId")
return 0
end
return parse(Int, el["numFmtId"])
end
+function styles_cell_xf_numFmtId(allXfNodes::Vector{XML.Node}, index::Int)::Int
+ el = styles_cell_xf(allXfNodes, index)
+ if !haskey(el, "numFmtId")
+ return 0
+ end
+ return parse(Int, el["numFmtId"])
+end
# Defines a custom number format to render numbers, dates or text.
# Returns the index to be used as the `numFmtId` in a cellXf definition.
-function styles_add_numFmt(wb::Workbook, format_code::AbstractString) :: Integer
+function styles_add_numFmt(wb::Workbook, format_code::AbstractString)::Integer
xroot = styles_xmlroot(wb)
- numfmts = find_all_nodes("/$SPREADSHEET_NAMESPACE_XPATH_ARG:styleSheet/$SPREADSHEET_NAMESPACE_XPATH_ARG:numFmts", xroot)
+ numfmts = find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":numFmts", xroot)
if isempty(numfmts)
- stylesheet = find_all_nodes("/$SPREADSHEET_NAMESPACE_XPATH_ARG:styleSheet", xroot)[begin] # find first
+ stylesheet = find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet", xroot)[begin] # find first
# We need to add the numFmts node directly after the styleSheet node
# Move everything down one and then insert the new node at the top
- nchildren = length(XML.children(stylesheet))
- numfmts = XML.Element("numFmts", count="1")
- push!(stylesheet, stylesheet[end])
- for i in nchildren-1:-1:1
- stylesheet[i+1]=stylesheet[i]
- end
- stylesheet[1]=numfmts
+ numfmts = XML.Element("numFmts", count="1")
+ XML.pushfirst!(stylesheet, numfmts)
else
numfmts = numfmts[1]
end
@@ -117,21 +136,24 @@ function styles_add_numFmt(wb::Workbook, format_code::AbstractString) :: Integer
existing_numFmt_elements_count = length(XML.children(numfmts))
fmt_code = existing_numFmt_elements_count + PREDEFINED_NUMFMT_COUNT
new_fmt = XML.Element("numFmt";
- numFmtId = fmt_code,
- formatCode = xlsx_escape(format_code)
+ numFmtId=fmt_code,
+ formatCode=XML.escape(format_code)
)
push!(numfmts, new_fmt)
return fmt_code
end
-const FontAttribute = Union{String, Pair{String, Pair{String, String}}}
+const FontAttribute = Union{String,Pair{String,Pair{String,String}}}
# Queries numFmt formatCode field by numFmtId.
-function styles_numFmt_formatCode(wb::Workbook, numFmtId::AbstractString) :: String
+function styles_numFmt_formatCode(wb::Workbook, numFmtId::AbstractString)::String
+ if haskey(builtinFormats, numFmtId)
+ return builtinFormats[numFmtId]
+ end
xroot = styles_xmlroot(wb)
- nodes_found = find_all_nodes("/$SPREADSHEET_NAMESPACE_XPATH_ARG:styleSheet/$SPREADSHEET_NAMESPACE_XPATH_ARG:numFmts/$SPREADSHEET_NAMESPACE_XPATH_ARG:numFmt", xroot)
- elements_found = filter(x->XML.attributes(x)["numFmtId"] == numFmtId, nodes_found)
- @assert length(elements_found) == 1 "numFmtId $numFmtId not found."
+ nodes_found = find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":numFmts/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":numFmt", xroot)
+ elements_found = filter(x -> XML.attributes(x)["numFmtId"] == numFmtId, nodes_found)
+ length(elements_found) != 1 && throw(XLSXError("numFmtId $numFmtId not found."))
return XML.attributes(elements_found[1])["formatCode"]
end
@@ -143,10 +165,10 @@ function remove_formatting(code::AbstractString)
# this regex should cover all the formatting cases found here(colors/conditionals/quotes/spacing):
# https://support.office.com/en-us/article/create-or-delete-a-custom-number-format-78f2a361-936b-4c03-8772-09fab54be7f4
ignoredformatting = r"""\[.{2,}?\]|".+?"|_.|\\.|\*."""x # Had to add ? to "".+"" to make it work. Don't understand what made this necessary!
- replace(code, ignoredformatting => "")
+ replace(code, ignoredformatting => "")
end
-function styles_is_datetime(wb::Workbook, index::Int) :: Bool
+function styles_is_datetime(wb::Workbook, index::Int)::Bool
if !haskey(wb.buffer_styles_is_datetime, index)
isdatetime = false
@@ -157,7 +179,7 @@ function styles_is_datetime(wb::Workbook, index::Int) :: Bool
elseif numFmtId > 81
code = lowercase(styles_numFmt_formatCode(wb, numFmtId))
code = remove_formatting(code)
- if any(map(x->occursin(x, code), DATETIME_CODES))
+ if any(map(x -> occursin(x, code), DATETIME_CODES))
isdatetime = true
end
end
@@ -171,13 +193,13 @@ end
styles_is_datetime(wb::Workbook, fmt::CellDataFormat) = styles_is_datetime(wb, Int(fmt.id))
function styles_is_datetime(wb::Workbook, index::AbstractString)
- @assert !isempty(index)
+ isempty(index) && throw(XLSXError("Something wrong here!"))
styles_is_datetime(wb, parse(Int, index))
end
styles_is_datetime(ws::Worksheet, index) = styles_is_datetime(get_workbook(ws), index)
-function styles_is_float(wb::Workbook, index::Int) :: Bool
+function styles_is_float(wb::Workbook, index::Int)::Bool
if !haskey(wb.buffer_styles_is_float, index)
isfloat = false
numFmtId = styles_cell_xf_numFmtId(wb, index)
@@ -206,7 +228,7 @@ function styles_is_float(wb::Workbook, index::Int) :: Bool
end
function styles_is_float(wb::Workbook, index::AbstractString)
- @assert !isempty(index)
+ isempty(index) && throw(XLSXError("Something wrong here!"))
styles_is_float(wb, parse(Int, index))
end
@@ -227,18 +249,20 @@ Returns -1 if not found.
```
=#
-function styles_get_cellXf_with_numFmtId(wb::Workbook, numFmtId::Int) :: AbstractCellDataFormat
+function styles_get_cellXf_with_numFmtId(wb::Workbook, numFmtId::Int)::AbstractCellDataFormat
xroot = styles_xmlroot(wb)
- elements_found = find_all_nodes("/$SPREADSHEET_NAMESPACE_XPATH_ARG:styleSheet/$SPREADSHEET_NAMESPACE_XPATH_ARG:cellXfs/$SPREADSHEET_NAMESPACE_XPATH_ARG:xf", xroot)
-
- if isempty(elements_found)
+ allXfNodes = find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":styleSheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":cellXfs/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":xf", xroot)
+ return styles_get_cellXf_with_numFmtId(allXfNodes, numFmtId)
+end
+function styles_get_cellXf_with_numFmtId(allXfNodes::Vector{XML.Node}, numFmtId::Int)::AbstractCellDataFormat
+ if isempty(allXfNodes)
return EmptyCellDataFormat()
else
- for i in 1:length(elements_found)
- el = XML.attributes(elements_found[i])
+ for i in 1:length(allXfNodes)
+ el = XML.attributes(allXfNodes[i])
if !isnothing(el) && haskey(el, "numFmtId")
if parse(Int, el["numFmtId"]) == numFmtId
- return CellDataFormat(i-1) # CellDataFormat is zero-indexed
+ return CellDataFormat(i - 1) # CellDataFormat is zero-indexed
end
end
end
@@ -249,23 +273,23 @@ function styles_get_cellXf_with_numFmtId(wb::Workbook, numFmtId::Int) :: Abstrac
end
function styles_add_cell_xf(wb::Workbook, attributes::Dict{String,String})::CellDataFormat
- new_xf = XML.Node(XML.Element, "xf", XML.OrderedDict{String, String}(), nothing, nothing)
+ new_xf = XML.Node(XML.Element, "xf", XML.OrderedDict{String,String}(), nothing, nothing)
for k in keys(attributes)
new_xf[k] = attributes[k]
end
return styles_add_cell_xf(wb, new_xf)
end
-function styles_add_cell_xf(wb::Workbook, new_xf::XML.Node) :: CellDataFormat
+function styles_add_cell_xf(wb::Workbook, new_xf::XML.Node)::CellDataFormat
xroot = styles_xmlroot(wb)
i, j = get_idces(xroot, "styleSheet", "cellXfs")
existing_cellxf_elements_count = length(XML.children(xroot[i][j]))
- @assert parse(Int, xroot[i][j]["count"]) == existing_cellxf_elements_count "Wrong number of xf elements found: $existing_cellxf_elements_count. Expected $(parse(Int, xroot[i][j]["count"]))."
+ if parse(Int, xroot[i][j]["count"]) != existing_cellxf_elements_count
+ throw(XLSXError("Wrong number of xf elements found: $existing_cellxf_elements_count. Expected $(parse(Int, xroot[i][j]["count"]))."))
+ end
# Check new_xf doesn't duplicate any existing xf. If yes, use that rather than create new.
- # Need to work around XML.jl issue # 33
for (k, node) in enumerate(XML.children(xroot[i][j]))
- #if XML.nodetype(node) == XML.nodetype(new_xf) && XML.parse(XML.Node, XML.write(node)) == XML.parse(XML.Node, XML.write(new_xf)) # XML.jl defines `Base.:(==)`
- if XML.parse(XML.Node, XML.write(node))[1] == XML.parse(XML.Node, XML.write(new_xf))[1] # XML.jl defines `Base.:(==)`
+ if node == new_xf
return CellDataFormat(k - 1) # CellDataFormat is zero-indexed
end
end
diff --git a/src/table.jl b/src/table.jl
index e0fbe436..8c776ce7 100644
--- a/src/table.jl
+++ b/src/table.jl
@@ -6,7 +6,7 @@
# Returns a tuple with the first and last index of the columns for a `SheetRow`.
function column_bounds(sr::SheetRow)
- @assert !isempty(sr) "Can't get column bounds from an empty row."
+ isempty(sr) && throw(XLSXError("Can't get column bounds from an empty row."))
local first_column_index::Int = first(keys(sr.rowcells))
local last_column_index::Int = first_column_index
@@ -27,7 +27,7 @@ end
# anchor_column will be the leftmost column of the column_bounds
function last_column_index(sr::SheetRow, anchor_column::Int) :: Int
- @assert !isempty(getcell(sr, anchor_column)) "Can't get column bounds based on an empty anchor cell."
+ isempty(getcell(sr, anchor_column)) && throw(XLSXError("Can't get column bounds based on an empty anchor cell."))
local first_column_index::Int = anchor_column
local last_column_index::Int = first_column_index
@@ -51,24 +51,24 @@ function last_column_index(sr::SheetRow, anchor_column::Int) :: Int
return last_column_index
end
-function _colname_prefix_symbol(sheet::Worksheet, cell::Cell)
+function _colname_prefix_string(sheet::Worksheet, cell::Cell)
d = getdata(sheet, cell)
if d isa String
- return Symbol(XML.unescape(d))
+ return XML.unescape(d)
else
- return Symbol(d)
+ return string(d)
end
end
-_colname_prefix_symbol(sheet::Worksheet, ::EmptyCell) = Symbol("#Empty")
+_colname_prefix_string(sheet::Worksheet, ::EmptyCell) = "#Empty"
# helper function to manage problematic column labels
# Empty cell -> "#Empty"
# No_unique_label -> No_unique_label_2
-function push_unique!(vect::Vector{Symbol}, sheet::Worksheet, cell::AbstractCell, iter::Int=1)
- name = _colname_prefix_symbol(sheet, cell)
+function push_unique!(vect::Vector{String}, sheet::Worksheet, cell::AbstractCell, iter::Int=1)
+ name = _colname_prefix_string(sheet, cell)
if iter > 1
- name = Symbol(name, '_', iter)
+ name = name*"_"*string(iter)
end
if name in vect
@@ -80,6 +80,20 @@ function push_unique!(vect::Vector{Symbol}, sheet::Worksheet, cell::AbstractCell
nothing
end
+# Issue 260
+const RESERVED = Set(["local", "global", "export", "let",
+ "for", "struct", "while", "const", "continue", "import",
+ "function", "if", "else", "try", "begin", "break", "catch",
+ "return", "using", "baremodule", "macro", "finally",
+ "module", "elseif", "end", "quote", "do"])
+normalizename(name::Symbol) = name
+function normalizename(name::String)::Symbol
+ uname = strip(Unicode.normalize(name))
+ id = Base.isidentifier(uname) ? uname : map(c->Base.is_id_char(c) ? c : '_', uname)
+ cleansed = string((isempty(id) || !Base.is_id_start_char(id[1]) || id in RESERVED) ? "_" : "", id)
+ return Symbol(replace(cleansed, r"(_)\1+"=>"_"))
+end
+
"""
eachtablerow(sheet, [columns]; [first_row], [column_labels], [header], [stop_in_empty_row], [stop_in_row_function], [keep_empty_rows])
@@ -134,37 +148,48 @@ function eachtablerow(
stop_in_empty_row::Bool=true,
stop_in_row_function::Union{Nothing, Function}=nothing,
keep_empty_rows::Bool=false,
+ normalizenames::Bool=false
) :: TableRowIterator
- if first_row === nothing
- first_row = _find_first_row_with_data(sheet, convert(ColumnRange, cols).start)
- end
+ #let col_lab
+
+ if first_row === nothing
+ first_row = _find_first_row_with_data(sheet, convert(ColumnRange, cols).start)
+ end
- itr = eachrow(sheet)
- column_range = convert(ColumnRange, cols)
-
- if column_labels === nothing
- column_labels = Vector{Symbol}()
- if header
- # will use getdata to get column names
- for column_index in column_range.start:column_range.stop
- sheet_row = find_row(itr, first_row)
- cell = getcell(sheet_row, column_index)
- push_unique!(column_labels, sheet, cell)
+ itr = eachrow(sheet)
+ column_range = convert(ColumnRange, cols)
+ col_lab = Vector{String}()
+
+ if column_labels === nothing
+ if header
+ # will use getdata to get column names
+ for column_index in column_range.start:column_range.stop
+ sheet_row = find_row(itr, first_row)
+ cell = getcell(sheet_row, column_index)
+ push_unique!(col_lab, sheet, cell)
+ end
+ else
+ # generate column_labels if there's no header information anywhere
+ for c in column_range
+ push!(col_lab, string(c))
+ end
end
else
- # generate column_labels if there's no header information anywhere
- for c in column_range
- push!(column_labels, Symbol(c))
+ # check consistency for column_range and column_labels
+ if length(column_labels) != length(column_range)
+ throw(XLSXError("`column_range` (length=$(length(column_range))) and `column_labels` (length=$(length(column_labels))) must have the same length."))
end
end
- else
- # check consistency for column_range and column_labels
- @assert length(column_labels) == length(column_range) "`column_range` (length=$(length(column_range))) and `column_labels` (length=$(length(column_labels))) must have the same length."
- end
-
- first_data_row = header ? first_row + 1 : first_row
- return TableRowIterator(sheet, Index(column_range, column_labels), first_data_row, stop_in_empty_row, stop_in_row_function, keep_empty_rows)
+ if normalizenames
+ column_labels = normalizename.(column_labels===nothing ? col_lab : column_labels)
+ else
+ column_labels = Symbol.(column_labels===nothing ? col_lab : column_labels)
+ end
+
+ first_data_row = header ? first_row + 1 : first_row
+ return TableRowIterator(sheet, Index(column_range, column_labels), first_data_row, stop_in_empty_row, stop_in_row_function, keep_empty_rows)
+ # end
end
function TableRowIterator(sheet::Worksheet, index::Index, first_data_row::Int, stop_in_empty_row::Bool=true, stop_in_row_function::Union{Nothing, Function}=nothing, keep_empty_rows::Bool=false)
@@ -179,6 +204,7 @@ function eachtablerow(
stop_in_empty_row::Bool=true,
stop_in_row_function::Union{Nothing, Function}=nothing,
keep_empty_rows::Bool=false,
+ normalizenames::Bool=false
) :: TableRowIterator
if first_row === nothing
@@ -224,12 +250,12 @@ function eachtablerow(
# if got here, it's because all columns are non-empty
column_range = ColumnRange(column_start, column_stop)
- return eachtablerow(sheet, column_range; first_row=first_row, column_labels=column_labels, header=header, stop_in_empty_row=stop_in_empty_row, stop_in_row_function=stop_in_row_function, keep_empty_rows=keep_empty_rows)
+ return eachtablerow(sheet, column_range; first_row=first_row, column_labels=column_labels, header=header, stop_in_empty_row=stop_in_empty_row, stop_in_row_function=stop_in_row_function, keep_empty_rows=keep_empty_rows, normalizenames=normalizenames)
end
end
end
- error("Couldn't find a table in sheet $(sheet.name)")
+ throw(XLSXError("Couldn't find a table in sheet $(sheet.name)"))
end
function _find_first_row_with_data(sheet::Worksheet, column_number::Int)
@@ -239,7 +265,7 @@ function _find_first_row_with_data(sheet::Worksheet, column_number::Int)
return row_number(r)
end
end
- error("Column $(encode_column_number(column_number)) has no data.")
+ throw(XLSXError("Column $(encode_column_number(column_number)) has no data."))
end
@inline get_worksheet(tri::TableRowIterator) = get_worksheet(tri.itr)
@@ -300,13 +326,14 @@ function TableRow(table_row::Int, index::Index, sheet_row::SheetRow)
end
getdata(r::TableRow, table_column_number::Int) = r.cell_values[table_column_number]
+getdata(r::TableRow, table_column_numbers::Union{Vector{T}, UnitRange{T}}) where {T<:Integer} = [r.cell_values[x] for x in table_column_numbers]
function getdata(r::TableRow, column_label::Symbol)
index = r.index
if haskey(index.lookup, column_label)
return getdata(r, index.lookup[column_label])
else
- error("Invalid column label: $column_label.")
+ throw(XLSXError("Invalid column label: $column_label."))
end
end
@@ -324,7 +351,7 @@ function Base.iterate(itr::TableRowIterator)
table_row_index = 1
return TableRow(table_row_index, itr.index, sheet_row), TableRowIteratorState(table_row_index, row_number(sheet_row), sheet_row_iterator_state)
else
- next = iterate(itr.itr, sheet_row_iterator_state)
+ next = iterate(itr.itr, sheet_row_iterator_state)
end
end
@@ -369,7 +396,7 @@ function Base.iterate(itr::TableRowIterator, state::TableRowIteratorState)
end
if is_empty_table_row(sheet_row)
- if itr.stop_in_empty_row
+ if itr.stop_in_empty_row
# user asked to stop fetching table rows if we find an empty row
return nothing
elseif !itr.keep_empty_rows
@@ -391,10 +418,11 @@ function Base.iterate(itr::TableRowIterator, state::TableRowIteratorState)
end
# if the `is_empty_table_row` check above was successful, we can't get empty sheet_row here
- @assert !is_empty_table_row(sheet_row) || itr.keep_empty_rows
+ @assert !is_empty_table_row(sheet_row) || itr.keep_empty_rows "Something wrong here!"
+
table_row = TableRow(table_row_index, itr.index, sheet_row)
- # user asked to stop
+ # user asked to stop (or end of row range)
if itr.stop_in_row_function !== nothing && itr.stop_in_row_function(table_row)
return nothing
end
@@ -447,7 +475,9 @@ function check_table_data_dimension(data::Vector)
# all columns should be vectors
for (colindex, colvec) in enumerate(data)
- @assert isa(colvec, Vector) "Data type at index $colindex is not a vector. Found: $(typeof(colvec))."
+ if !isa(colvec, Vector)
+ throw(XLSXError("Data type at index $colindex is not a vector. Found: $(typeof(colvec))."))
+ end
end
# no need to check row count
@@ -457,13 +487,15 @@ function check_table_data_dimension(data::Vector)
col_count = length(data)
row_count = length(data[1])
for colindex in 2:col_count
- @assert length(data[colindex]) == row_count "Not all columns have the same number of rows. Check column $colindex."
+ if length(data[colindex]) != row_count
+ throw(XLSXError("Not all columns have the same number of rows. Check column $colindex."))
+ end
end
nothing
end
-function gettable(itr::TableRowIterator; infer_eltypes::Bool=false) :: DataTable
+function gettable(itr::TableRowIterator; infer_eltypes::Bool=true) :: DataTable
column_labels = get_column_labels(itr)
columns_count = table_columns_count(itr)
data = Vector{Any}(undef, columns_count)
@@ -516,7 +548,8 @@ end
[infer_eltypes],
[stop_in_empty_row],
[stop_in_row_function],
- [keep_empty_rows]
+ [keep_empty_rows],
+ [normalizenames]
) -> DataTable
Returns tabular data from a spreadsheet as a struct `XLSX.DataTable`.
@@ -540,8 +573,10 @@ will generate column labels. The default value is `header=true`.
Use `column_labels` as a vector of symbols to specify names for the header of the table.
+Use `normalizenames=true` to normalize column names to valid Julia identifiers.
+
Use `infer_eltypes=true` to get `data` as a `Vector{Any}` of typed vectors.
-The default value is `infer_eltypes=false`.
+The default value is `infer_eltypes=true`.
`stop_in_empty_row` is a boolean indicating whether an empty row marks the end of the table.
If `stop_in_empty_row=false`, the `TableRowIterator` will continue to fetch rows until there's no more rows in the Worksheet.
@@ -571,15 +606,15 @@ julia> df = XLSX.openxlsx("myfile.xlsx") do xf
DataFrame(XLSX.gettable(xf["mysheet"]))
end
```
-
+
See also: [`XLSX.readtable`](@ref).
"""
-function gettable(sheet::Worksheet, cols::Union{ColumnRange, AbstractString}; first_row::Union{Nothing, Int}=nothing, column_labels=nothing, header::Bool=true, infer_eltypes::Bool=false, stop_in_empty_row::Bool=true, stop_in_row_function::Union{Function, Nothing}=nothing, keep_empty_rows::Bool=false)
- itr = eachtablerow(sheet, cols; first_row=first_row, column_labels=column_labels, header=header, stop_in_empty_row=stop_in_empty_row, stop_in_row_function=stop_in_row_function, keep_empty_rows=keep_empty_rows)
- return gettable(itr; infer_eltypes=infer_eltypes)
+function gettable(sheet::Worksheet, cols::Union{ColumnRange, AbstractString}; first_row::Union{Nothing, Int}=nothing, column_labels=nothing, header::Bool=true, infer_eltypes::Bool=true, stop_in_empty_row::Bool=true, stop_in_row_function::Union{Function, Nothing}=nothing, keep_empty_rows::Bool=false, normalizenames::Bool=false)
+ itr = eachtablerow(sheet, cols; first_row, column_labels, header, stop_in_empty_row, stop_in_row_function, keep_empty_rows, normalizenames)
+ return gettable(itr; infer_eltypes)
end
-function gettable(sheet::Worksheet; first_row::Union{Nothing, Int}=nothing, column_labels=nothing, header::Bool=true, infer_eltypes::Bool=false, stop_in_empty_row::Bool=true, stop_in_row_function::Union{Function, Nothing}=nothing, keep_empty_rows::Bool=false)
- itr = eachtablerow(sheet; first_row=first_row, column_labels=column_labels, header=header, stop_in_empty_row=stop_in_empty_row, stop_in_row_function=stop_in_row_function, keep_empty_rows=keep_empty_rows)
- return gettable(itr; infer_eltypes=infer_eltypes)
+function gettable(sheet::Worksheet; first_row::Union{Nothing, Int}=nothing, column_labels=nothing, header::Bool=true, infer_eltypes::Bool=true, stop_in_empty_row::Bool=true, stop_in_row_function::Union{Function, Nothing}=nothing, keep_empty_rows::Bool=false, normalizenames::Bool=false)
+ itr = eachtablerow(sheet; first_row, column_labels, header, stop_in_empty_row, stop_in_row_function, keep_empty_rows, normalizenames)
+ return gettable(itr; infer_eltypes)
end
diff --git a/src/tables_interface.jl b/src/tables_interface.jl
index b41ec25c..a38444bd 100644
--- a/src/tables_interface.jl
+++ b/src/tables_interface.jl
@@ -18,7 +18,7 @@ function _table_to_arrays(x)
colnames = collect(Symbol, Tables.columnnames(x))
return columns, colnames
else
- error("$(typeof(x)) does not implement Tables.jl interface.")
+ throw(XLSXError("$(typeof(x)) does not implement Tables.jl interface."))
end
end
@@ -59,7 +59,7 @@ Tables.getcolumn(dt::DataTable, i::Int) = dt.data[i]
function Tables.getcolumn(dt::DataTable, column_label::Symbol)
if !haskey(dt.column_label_index, column_label)
- error("Column `$column_label` not found.")
+ throw(XLSXError("Column `$column_label` not found."))
end
column_index = dt.column_label_index[column_label]
diff --git a/src/types.jl b/src/types.jl
index ae9ba8c3..a8b21318 100644
--- a/src/types.jl
+++ b/src/types.jl
@@ -147,6 +147,11 @@ struct CellDataFormat <: AbstractCellDataFormat
id::UInt
end
+# Keeps track of conditional formatting information.
+struct DxFormat <: AbstractCellDataFormat
+ id::UInt
+end
+
"""
CellValueType
@@ -175,7 +180,13 @@ As a convenience, `@range_str` macro is provided.
cr = XLSX.range"A1:C4"
```
=#
-struct CellRange
+
+abstract type AbstractCellRange end
+abstract type ContiguousCellRange <: AbstractCellRange end
+abstract type AbstractSheetCellRange <: AbstractCellRange end
+abstract type ContiguousSheetCellRange <: AbstractSheetCellRange end
+
+struct CellRange <: ContiguousCellRange
start::CellRef
stop::CellRef
@@ -186,18 +197,33 @@ struct CellRange
left = column_number(a)
right = column_number(b)
- @assert left <= right && top <= bottom "Invalid CellRange. Start cell should be at the top left corner of the range."
+ if left > right || top > bottom
+ throw(XLSXError("Invalid CellRange. Start cell should be at the top left corner of the range."))
+ end
return new(a, b)
end
end
-struct ColumnRange
+struct ColumnRange <: ContiguousCellRange
start::Int # column number
stop::Int # column number
function ColumnRange(a::Int, b::Int)
- @assert a <= b "Invalid ColumnRange. Start column must be located before end column."
+ if a > b
+ throw(XLSXError("Invalid ColumnRange. Start column must be located before end column."))
+ end
+ return new(a, b)
+ end
+end
+struct RowRange <: ContiguousCellRange
+ start::Int # row number
+ stop::Int # row number
+
+ function RowRange(a::Int, b::Int)
+ if a > b
+ throw(XLSXError("Invalid RowRange. Start row must be located before end row."))
+ end
return new(a, b)
end
end
@@ -207,15 +233,24 @@ struct SheetCellRef
cellref::CellRef
end
-struct SheetCellRange
+struct SheetCellRange <: ContiguousSheetCellRange
sheet::String
rng::CellRange
end
-struct SheetColumnRange
+struct NonContiguousRange <: AbstractSheetCellRange
+ sheet::String
+ rng::Vector{Union{CellRef, CellRange}}
+end
+
+struct SheetColumnRange <: ContiguousSheetCellRange
sheet::String
colrng::ColumnRange
end
+struct SheetRowRange <: ContiguousSheetCellRange
+ sheet::String
+ rowrng::RowRange
+end
abstract type MSOfficePackage end
@@ -258,7 +293,13 @@ mutable struct SheetRowStreamIteratorState
ht::Union{Float64, Nothing} # row height
end
+mutable struct WorksheetCacheIteratorState
+ row_from_last_iteration::Int
+ full_cache::Bool # is the cache full (true) or does it need filling (false)
+end
+
mutable struct WorksheetCache{I<:SheetRowIterator} <: SheetRowIterator
+ is_empty::Bool # true before first read from file, then false
cells::CellCache # SheetRowNumber -> Dict{column_number, Cell}
rows_in_cache::Vector{Int} # ordered vector with row numbers that are stored in cache
row_ht::Dict{Int, Union{Float64, Nothing}} # Maps a row number to a row height
@@ -309,9 +350,18 @@ mutable struct SharedStringTable
is_loaded::Bool # for lazy-loading of sst XML file (implies that this struct must be mutable)
end
-const DefinedNameValueTypes = Union{SheetCellRef, SheetCellRange, Int, Float64, String, Missing}
+const DefinedNameValueTypes = Union{SheetCellRef, SheetCellRange, NonContiguousRange, Int, Float64, String, Missing}
+const DefinedNameRangeTypes = Union{SheetCellRef, SheetCellRange, NonContiguousRange}
+
+struct DefinedNameValue
+ value::DefinedNameValueTypes
+ isabs::Union{Bool, Vector{Bool}}
+end
# Workbook is the result of parsing file `xl/workbook.xml`.
+# The `xl/workbook.xml` will need to be updated using the Workbook_names and
+# worksheet_names from here when a workbook is saved in case any new defined
+# names have been created.
mutable struct Workbook
package::MSOfficePackage # parent XLSXFile
sheets::Vector{Worksheet} # workbook -> sheets -> . sheetId determines the index of the WorkSheet in this vector.
@@ -320,15 +370,16 @@ mutable struct Workbook
sst::SharedStringTable # shared string table
buffer_styles_is_float::Dict{Int, Bool} # cell style -> true if is float
buffer_styles_is_datetime::Dict{Int, Bool} # cell style -> true if is datetime
- workbook_names::Dict{String, DefinedNameValueTypes} # definedName
- worksheet_names::Dict{Tuple{Int, String}, DefinedNameValueTypes} # definedName. (sheetId, name) -> value.
+ workbook_names::Dict{String, DefinedNameValue} # definedName
+ worksheet_names::Dict{Tuple{Int, String}, DefinedNameValue} # definedName. (sheetId, name) -> value.
styles_xroot::Union{XML.Node, Nothing}
end
"""
`XLSXFile` represents a reference to an Excel file.
-It is created by using [`XLSX.readxlsx`](@ref) or [`XLSX.openxlsx`](@ref).
+It is created by using [`XLSX.readxlsx`](@ref) or [`XLSX.openxlsx`](@ref)
+or [`XLSX.opentemplate`](@ref) or [`XLSX.newxlsx`](@ref).
From a `XLSXFile` you can navigate to a `XLSX.Worksheet` reference
as shown in the example below.
@@ -343,6 +394,7 @@ sh = xf["mysheet"] # get a reference to a Worksheet
mutable struct XLSXFile <: MSOfficePackage
source::Union{AbstractString, IO}
use_cache_for_sheet_data::Bool # indicates whether Worksheet.cache will be fed while reading worksheet cells.
+ stream::Union{Nothing, IOStream} # mmap stream for reading XLSX file
io::ZipArchives.ZipReader
files::Dict{String, Bool} # maps filename => isread bool
data::Dict{String, XML.Node} # maps filename => XMLDocument
@@ -351,10 +403,16 @@ mutable struct XLSXFile <: MSOfficePackage
relationships::Vector{Relationship} # contains package level relationships
is_writable::Bool # indicates whether this XLSX file can be edited
- function XLSXFile(source::Union{AbstractString, IO}, use_cache::Bool, is_writable::Bool)
+ function XLSXFile(source::Union{AbstractString, IO}, use_cache::Bool, is_writable::Bool; use_stream::Bool = false)
check_for_xlsx_file_format(source)
- io = ZipArchives.ZipReader(read(source))
- xl = new(source, use_cache, io, Dict{String, Bool}(), Dict{String, XML.Node}(), Dict{String, Vector{UInt8}}(), EmptyWorkbook(), Vector{Relationship}(), is_writable)
+ if !use_stream && use_cache || (source isa IOBuffer)
+ stream = nothing
+ io = ZipArchives.ZipReader(read(source))
+ else
+ stream = open(source, "r")
+ io = ZipArchives.ZipReader(Mmap.mmap(stream))
+ end
+ xl = new(source, use_cache, stream, io, Dict{String, Bool}(), Dict{String, XML.Node}(), Dict{String, Vector{UInt8}}(), EmptyWorkbook(), Vector{Relationship}(), is_writable)
xl.workbook.package = xl
return xl
end
@@ -379,7 +437,9 @@ struct Index # based on DataFrames.jl
function Index(column_range::Union{ColumnRange, AbstractString}, column_labels)
column_labels_as_syms = [ Symbol(i) for i in column_labels ]
column_range = convert(ColumnRange, column_range)
- @assert length(unique(column_labels_as_syms)) == length(column_labels_as_syms) "Column labels must be unique."
+ if length(unique(column_labels_as_syms)) != length(column_labels_as_syms)
+ throw(XLSXError("Column labels must be unique."))
+ end
lookup = Dict{Symbol, Int}()
for (i, n) in enumerate(column_labels_as_syms)
@@ -425,11 +485,15 @@ struct DataTable
column_labels::Vector{Symbol},
)
- @assert length(data) == length(column_labels) "data has $(length(data)) columns but $(length(column_labels)) column labels."
+ if length(data) != length(column_labels)
+ throw(XLSXError("Data has $(length(data)) columns but $(length(column_labels)) column labels."))
+ end
column_label_index = Dict{Symbol, Int}()
for (i, sym) in enumerate(column_labels)
- @assert !haskey(column_label_index, sym) "DataTable has repeated label for column `$sym`"
+ if haskey(column_label_index, sym)
+ throw(XLSXError("DataTable has repeated label for column `$sym`"))
+ end
column_label_index[sym] = i
end
@@ -444,4 +508,9 @@ struct xpath
function xpath(node::XML.Node, path::String)
new(node, path)
end
-end
\ No newline at end of file
+end
+
+struct XLSXError <: Exception
+ msg::String
+end
+Base.showerror(io::IO, e::XLSXError) = print(io, "XLSXError: ",e.msg)
\ No newline at end of file
diff --git a/src/workbook.jl b/src/workbook.jl
index db0375c2..9e44840a 100644
--- a/src/workbook.jl
+++ b/src/workbook.jl
@@ -1,7 +1,7 @@
EmptyWorkbook() = Workbook(EmptyMSOfficePackage(), Vector{Worksheet}(), false,
- Vector{Relationship}(), SharedStringTable(), Dict{Int, Bool}(), Dict{Int, Bool}(),
- Dict{String, DefinedNameValueTypes}(), Dict{Tuple{Int, String}, DefinedNameValueTypes}(), nothing)
+ Vector{Relationship}(), SharedStringTable(), Dict{Int,Bool}(), Dict{Int,Bool}(),
+ Dict{String,DefinedNameValueTypes}(), Dict{Tuple{Int,String},DefinedNameValueTypes}(), nothing)
#=
Indicates whether this XLSX file can be edited.
@@ -14,20 +14,20 @@ is_writable(xl::XLSXFile) = xl.is_writable
sheetnames(xl::XLSXFile)
sheetnames(wb::Workbook)
-Returns a vector with Worksheet names for this Workbook.
+Return a vector with Worksheet names for this Workbook.
"""
-sheetnames(wb::Workbook) = [ s.name for s in wb.sheets ]
+sheetnames(wb::Workbook) = [s.name for s in wb.sheets]
@inline sheetnames(xl::XLSXFile) = sheetnames(xl.workbook)
"""
hassheet(wb::Workbook, sheetname::AbstractString)
hassheet(xl::XLSXFile, sheetname::AbstractString)
-Returns `true` if `wb` contains a sheet named `sheetname`.
+Return `true` if `wb` contains a sheet named `sheetname`.
"""
-function hassheet(wb::Workbook, sheetname::AbstractString) :: Bool
+function hassheet(wb::Workbook, sheetname::AbstractString)::Bool
for s in wb.sheets
- if s.name == sheetname
+ if s.name == unquoteit(sheetname)
return true
end
end
@@ -39,27 +39,27 @@ end
"""
sheetcount(xlsfile) :: Int
-Counts the number of sheets in the Workbook.
+Count the number of sheets in the Workbook.
"""
@inline sheetcount(wb::Workbook) = length(wb.sheets)
@inline sheetcount(xl::XLSXFile) = sheetcount(xl.workbook)
# Returns true if workbook follows date1904 convention.
-@inline isdate1904(wb::Workbook) :: Bool = wb.date1904
-@inline isdate1904(xf::XLSXFile) :: Bool = isdate1904(get_workbook(xf))
+@inline isdate1904(wb::Workbook)::Bool = wb.date1904
+@inline isdate1904(xf::XLSXFile)::Bool = isdate1904(get_workbook(xf))
-function getsheet(wb::Workbook, sheetname::String) :: Worksheet
+function getsheet(wb::Workbook, sheetname::String)::Worksheet
for ws in wb.sheets
- if ws.name == xlsx_escape(sheetname)
+ if ws.name == unquoteit(sheetname)
return ws
end
end
- error("$(get_xlsxfile(wb).source) does not have a Worksheet named $sheetname.")
+ throw(XLSXError("$(get_xlsxfile(wb).source) does not have a Worksheet named `$sheetname`."))
end
-@inline getsheet(wb::Workbook, sheet_index::Int) :: Worksheet = wb.sheets[sheet_index]
-@inline getsheet(xl::XLSXFile, sheetname::String) :: Worksheet = getsheet(xl.workbook, sheetname)
-@inline getsheet(xl::XLSXFile, sheet_index::Int) :: Worksheet = getsheet(xl.workbook, sheet_index)
+@inline getsheet(wb::Workbook, sheet_index::Int)::Worksheet = wb.sheets[sheet_index]
+@inline getsheet(xl::XLSXFile, sheetname::String)::Worksheet = getsheet(xl.workbook, sheetname)
+@inline getsheet(xl::XLSXFile, sheet_index::Int)::Worksheet = getsheet(xl.workbook, sheet_index)
function Base.show(io::IO, xf::XLSXFile)
@@ -74,13 +74,13 @@ function Base.show(io::IO, xf::XLSXFile)
wb = xf.workbook
print(io, "XLSXFile(\"$(xf.source)\") ",
- "containing $(sheetcountstr(wb))\n")
+ "containing $(sheetcountstr(wb))\n")
@printf(io, "%21s %-13s %-13s\n", "sheetname", "size", "range")
- println(io, "-"^(21+1+13+1+13))
+ println(io, "-"^(21 + 1 + 13 + 1 + 13))
for s in wb.sheets
- sheetname = s.name
- if textwidth(sheetname) > 20
+ sheetname = s.name
+ if textwidth(sheetname) > 20
sheetname = sheetname[collect(eachindex(s.name))[1:20]] * "…"
end
@@ -105,81 +105,274 @@ function Base.getindex(xl::XLSXFile, s::AbstractString)
end
function getdata(xl::XLSXFile, ref::SheetCellRef)
- @assert hassheet(xl, ref.sheet) "Sheet $(ref.sheet) not found."
+ !hassheet(xl, ref.sheet) && throw(XLSXError("Sheet `$(ref.sheet)` not found."))
return getdata(getsheet(xl, ref.sheet), ref.cellref)
end
function getdata(xl::XLSXFile, rng::SheetCellRange)
- @assert hassheet(xl, rng.sheet) "Sheet $(rng.sheet) not found."
+ !hassheet(xl, rng.sheet) && throw(XLSXError("Sheet `$(rng.sheet)` not found."))
return getdata(getsheet(xl, rng.sheet), rng.rng)
end
function getdata(xl::XLSXFile, rng::SheetColumnRange)
- @assert hassheet(xl, rng.sheet) "Sheet $(rng.sheet) not found."
+ !hassheet(xl, rng.sheet) && throw(XLSXError("Sheet `$(rng.sheet)` not found."))
return getdata(getsheet(xl, rng.sheet), rng.colrng)
end
+function getdata(xl::XLSXFile, rng::SheetRowRange)
+ !hassheet(xl, rng.sheet) && throw(XLSXError("Sheet `$(rng.sheet)` not found."))
+ return getdata(getsheet(xl, rng.sheet), rng.rowrng)
+end
+
+function getdata(xl::XLSXFile, rng::NonContiguousRange)
+ !hassheet(xl, rng.sheet) && throw(XLSXError("Sheet `$(rng.sheet)` not found."))
+ return getdata(getsheet(xl, rng.sheet), rng)
+end
+
function getdata(xl::XLSXFile, s::AbstractString)
- if is_valid_sheet_cellname(s)
- return getdata(xl, SheetCellRef(s))
- elseif is_valid_sheet_cellrange(s)
- return getdata(xl, SheetCellRange(s))
- elseif is_valid_sheet_column_range(s)
- return getdata(xl, SheetColumnRange(s))
- elseif is_workbook_defined_name(xl, s)
+ if is_workbook_defined_name(xl, s)
v = get_defined_name_value(xl.workbook, s)
if is_defined_name_value_a_constant(v)
return v
elseif is_defined_name_value_a_reference(v)
return getdata(xl, v)
else
- error("Unexpected defined name value: $v.")
+ throw(XLSXError("Unexpected Workbook defined name value: $v."))
end
+ elseif is_valid_sheet_cellname(s)
+ return getdata(xl, SheetCellRef(s))
+ elseif is_valid_sheet_cellrange(s)
+ return getdata(xl, SheetCellRange(s))
+ elseif is_valid_sheet_column_range(s)
+ return getdata(xl, SheetColumnRange(s))
+ elseif is_valid_sheet_row_range(s)
+ return getdata(xl, SheetRowRange(s))
+ elseif is_valid_non_contiguous_sheetcellrange(s)
+ return getdata(xl, NonContiguousRange(s))
end
- error("$s is not a valid sheetname or cell/range reference.")
+ throw(XLSXError("`$s` is not a valid sheetname, definedName or cell/range reference."))
end
function getcell(xl::XLSXFile, ref::SheetCellRef)
- @assert hassheet(xl, ref.sheet) "Sheet $(ref.sheet) not found."
+ !hassheet(xl, ref.sheet) && throw(XLSXError("Sheet `$(ref.sheet)` not found."))
return getcell(getsheet(xl, ref.sheet), ref.cellref)
end
-getcell(xl::XLSXFile, ref_str::AbstractString) = getcell(xl, SheetCellRef(ref_str))
+function getcell(xl::XLSXFile, ref_str::AbstractString)
+ if is_workbook_defined_name(xl, ref_str)
+ v = get_defined_name_value(xl.workbook, ref_str)
+ if is_defined_name_value_a_reference(v)
+ return isa(v, SheetCellRef) ? getcell(xl, v) : getcellrange(xl, v)
+ else
+ throw(XLSXError("`$ref_str` is not a valid Workbook definedName reference."))
+ end
+ elseif is_valid_sheet_cellname(ref_str)
+ return getcell(xl, SheetCellRef(ref_str))
+ elseif is_valid_sheet_cellrange(ref_str)
+ return getcellrange(xl, SheetCellRange(ref_str))
+ elseif is_valid_sheet_column_range(ref_str)
+ return getcellrange(xl, SheetColumnRange(ref_str))
+ elseif is_valid_sheet_row_range(ref_str)
+ return getcellrange(xl, SheetRowRange(ref_str))
+ elseif is_valid_non_contiguous_range(ref_str)
+ return getcellrange(xl, NonContiguousRange(ref_str))
+ end
+ throw(XLSXError("`$ref_str` is not a valid SheetCellRef."))
+end
function getcellrange(xl::XLSXFile, rng::SheetCellRange)
- @assert hassheet(xl, rng.sheet) "Sheet $(rng.sheet) not found."
+ !hassheet(xl, rng.sheet) && throw(XLSXError("Sheet `$(rng.sheet)` not found."))
return getcellrange(getsheet(xl, rng.sheet), rng.rng)
end
function getcellrange(xl::XLSXFile, rng::SheetColumnRange)
- @assert hassheet(xl, rng.sheet) "Sheet $(rng.sheet) not found."
+ !hassheet(xl, rng.sheet) && throw(XLSXError("Sheet `$(rng.sheet)` not found."))
return getcellrange(getsheet(xl, rng.sheet), rng.colrng)
end
+function getcellrange(xl::XLSXFile, rng::SheetRowRange)
+ !hassheet(xl, rng.sheet) && throw(XLSXError("Sheet `$(rng.sheet)` not found."))
+ return getcellrange(getsheet(xl, rng.sheet), rng.rowrng)
+end
+
+function getcellrange(xl::XLSXFile, rng::NonContiguousRange)
+ !hassheet(xl, rng.sheet) && throw(XLSXError("Sheet `$(rng.sheet)` not found."))
+ return getcellrange(getsheet(xl, rng.sheet), rng)
+end
+
function getcellrange(xl::XLSXFile, rng_str::AbstractString)
- if is_valid_sheet_cellrange(rng_str)
+ wb = get_workbook(xl)
+ if is_workbook_defined_name(wb, rng_str)
+ v = get_defined_name_value(wb, rng_str)
+ if is_defined_name_value_a_reference(v)
+ return getcellrange(xl, v)
+ else
+ throw(XLSXError("`$rng_str` is not a valid Workbook definedName reference."))
+ end
+ elseif is_valid_sheet_cellrange(rng_str)
return getcellrange(xl, SheetCellRange(rng_str))
elseif is_valid_sheet_column_range(rng_str)
return getcellrange(xl, SheetColumnRange(rng_str))
+ elseif is_valid_sheet_row_range(rng_str)
+ return getcellrange(xl, SheetRowRange(rng_str))
+ elseif is_valid_non_contiguous_range(rng_str)
+ return getcellrange(xl, NonContiguousRange(rng_str))
end
-
- error("$rng_str is not a valid range reference.")
+ throw(XLSXError("`$rng_str` is not a valid SheetCellRange."))
end
-@inline is_workbook_defined_name(wb::Workbook, name::AbstractString) :: Bool = haskey(wb.workbook_names, name)
-@inline is_workbook_defined_name(xl::XLSXFile, name::AbstractString) :: Bool = is_workbook_defined_name(get_workbook(xl), name)
-@inline is_worksheet_defined_name(ws::Worksheet, name::AbstractString) :: Bool = is_worksheet_defined_name(get_workbook(ws), ws.sheetId, name)
-@inline is_worksheet_defined_name(wb::Workbook, sheetId::Int, name::AbstractString) :: Bool = haskey(wb.worksheet_names, (sheetId, name))
-@inline is_worksheet_defined_name(wb::Workbook, sheet_name::AbstractString, name::AbstractString) :: Bool = is_worksheet_defined_name(wb, getsheet(wb, sheet_name).sheetId, name)
+# Defined names are case-insensitive in Excel. Need to check on this basis (haskey is insufficient).
+@inline is_workbook_defined_name(wb::Workbook, name::AbstractString)::Bool = !isnothing(findfirst(x -> uppercase(x)==uppercase(name), collect(keys(wb.workbook_names))))
+@inline is_worksheet_defined_name(wb::Workbook, sheetId::Int, name::AbstractString)::Bool = !isnothing(findfirst(x -> uppercase(last(x))==uppercase(name) && first(x)==sheetId, collect(keys(wb.worksheet_names))))
+@inline is_workbook_defined_name(xl::XLSXFile, name::AbstractString)::Bool = is_workbook_defined_name(get_workbook(xl), name)
+@inline is_worksheet_defined_name(ws::Worksheet, name::AbstractString)::Bool = is_worksheet_defined_name(get_workbook(ws), ws.sheetId, name)
+@inline is_worksheet_defined_name(wb::Workbook, sheet_name::AbstractString, name::AbstractString)::Bool = is_worksheet_defined_name(wb, getsheet(wb, sheet_name).sheetId, name)
-@inline get_defined_name_value(wb::Workbook, name::AbstractString) :: DefinedNameValueTypes = wb.workbook_names[name]
+@inline get_defined_name_value(wb::Workbook, name::AbstractString)::DefinedNameValueTypes = wb.workbook_names[name].value
-function get_defined_name_value(ws::Worksheet, name::AbstractString) :: DefinedNameValueTypes
+function get_defined_name_value(ws::Worksheet, name::AbstractString)::DefinedNameValueTypes
wb = get_workbook(ws)
sheetId = ws.sheetId
- return wb.worksheet_names[(sheetId, name)]
+ dn = wb.worksheet_names[(sheetId, name)]
+ return dn.value
end
-@inline is_defined_name_value_a_reference(v::DefinedNameValueTypes) = isa(v, SheetCellRef) || isa(v, SheetCellRange)
+@inline is_defined_name_value_a_reference(v::DefinedNameValueTypes) = isa(v, SheetCellRef) || isa(v, SheetCellRange) || isa(v, NonContiguousRange)
@inline is_defined_name_value_a_constant(v::DefinedNameValueTypes) = !is_defined_name_value_a_reference(v)
+
+function is_valid_defined_name(name::AbstractString)::Bool
+ if isempty(name)
+ return false
+ end
+ if is_valid_cellname(name) || is_valid_cellrange(name) || is_valid_non_contiguous_cellrange(name)
+ return false
+ end
+ if is_valid_sheet_cellname(name) || is_valid_sheet_cellrange(name) || is_valid_non_contiguous_sheetcellrange(name)
+ return false
+ end
+ if !isletter(name[1]) && name[1] != '_'
+ return false
+ end
+ for c in name
+ if !isletter(c) && !isdigit(c) && c != '_' && c != '\\'
+ return false
+ end
+ end
+ return true
+end
+
+function addDefName(xf::XLSXFile, name::AbstractString, value::DefinedNameValueTypes; absolute=true)
+ if !is_valid_defined_name(name)
+ throw(XLSXError("Invalid defined name: `$name`."))
+ end
+ if is_workbook_defined_name(xf, name)
+ throw(XLSXError("Workbook already has a defined name called `$name`."))
+ end
+ if value isa NonContiguousRange
+ abs = absolute ? fill(true, length(value.rng)) : fill(false, length(value.rng))
+ else
+ abs = absolute ? true : false
+ end
+ xf.workbook.workbook_names[name] = DefinedNameValue(value, abs)
+end
+function addDefName(ws::Worksheet, name::AbstractString, value::DefinedNameValueTypes; absolute=true)
+ wb = get_workbook(ws)
+ if !is_valid_defined_name(name)
+ throw(XLSXError("Invalid defined name: `$name`."))
+ end
+ if is_worksheet_defined_name(ws, name)
+ throw(XLSXError("Worksheet `$(ws.name)` already has a defined name called `$name`."))
+ end
+
+ if value isa NonContiguousRange || value isa SheetCellRange
+ value.sheet != ws.name && throw(XLSXError("Range $value is not in the given worksheet ($(ws.name))."))
+ end
+ if value isa NonContiguousRange
+ abs = absolute ? fill(true, length(value.rng)) : fill(false, length(value.rng))
+ else
+ abs = absolute ? true : false
+ end
+ wb.worksheet_names[(ws.sheetId, name)] = DefinedNameValue(value, abs)
+end
+
+quoteit(x::AbstractString) = occursin(r"[^\w]|\s", x) ? "'$x'" : x
+unquoteit(x::AbstractString) = replace(x, "'" => "")
+
+"""
+ addDefinedName(xf::XLSXFile, name::AbstractString, value::Union{Int, Float64, String}; absolute=true)
+ addDefinedName(xf::XLSXFile, name::AbstractString, value::AbstractString; absolute=true)
+ addDefinedName(sh::Worksheet, name::AbstractString, value::Union{Int, Float64, String}; absolute=true)
+ addDefinedName(sh::Worksheet, name::AbstractString, value::AbstractString; absolute=true)
+
+Add a defined name to the Workbook or Worksheet. If an `XLSXFile` is passed, the defined name
+is added to the Workbook. If a `Worksheet` is passed, the defined name is added to the Worksheet.
+
+When adding defined name referring to a cell or range to a workbook, `value` must include the sheet
+name (e.g. `Sheet1!A1:B2`).
+
+If the new `definedName` is a cell reference or range, by default, it will be an absolute
+reference (e.g. `\$A\$1:\$C\$6`). If `absolute=false` is specified, the new `definedName` will be
+a relative reference (e.g. `A1:C6`). Any `absolute` argument specified is ignored if the
+`definedName` is not a cell reference or range.
+
+In the context of `XLSX.jl` there is no difference between an absolute reference and a relative
+reference. However, Excel treats them differently. When `definedNames` are read in as part of
+an XLSXFile, we keep track of whether they are absolute or not. If the XLSXFile is subsequently
+written out again, the status of the `definedNames` is preserved.
+
+# Examples
+```julia
+julia> XLSX.addDefinedName(sh, "ID", "C21")
+
+julia> XLSX.addDefinedName(sh, "NEW", "A1:B2")
+
+julia> XLSX.addDefinedName(sh, "my_name", "A1,B2,C3")
+
+julia> XLSX.addDefinedName(xf, "New", "'Mock-up'!A1:B2")
+
+julia> XLSX.addDefinedName(xf, "Life_the_universe_and_everything", 42)
+
+julia> XLSX.addDefinedName(xf, "first_name", "Hello World")
+
+```
+"""
+function addDefinedName end
+addDefinedName(xf::XLSXFile, name::AbstractString, value::Union{Int,Float64}; absolute=true) = addDefName(xf, name, value; absolute)
+addDefinedName(ws::Worksheet, name::AbstractString, value::Union{Int,Float64}; absolute=true) = addDefName(ws, name, value; absolute)
+function addDefinedName(xf::XLSXFile, name::AbstractString, value::AbstractString; absolute=true)
+ if value == ""
+ throw(XLSXError("Defined name value cannot be an empty string."))
+ end
+ if is_valid_cellname(value) || is_valid_cellrange(value) || is_valid_non_contiguous_cellrange(value)
+ throw(XLSXError("Workbook defined name reference `$value` incomplete. Must contain sheet name (e.g. `Sheet1!A1:B2`)."))
+ elseif is_valid_sheet_cellname(value)
+ return addDefName(xf, name, SheetCellRef(value); absolute)
+ elseif is_valid_sheet_cellrange(value)
+ return addDefName(xf, name, SheetCellRange(value); absolute)
+ elseif is_valid_non_contiguous_sheetcellrange(value)
+ return addDefName(xf, name, NonContiguousRange(value); absolute)
+ else
+ return addDefName(xf, name, value; absolute)
+ end
+end
+function addDefinedName(ws::Worksheet, name::AbstractString, value::AbstractString; absolute=true)
+ if value == ""
+ throw(XLSXError("Defined name value cannot be an empty string."))
+ end
+ if is_valid_cellname(value)
+ return addDefName(ws, name, SheetCellRef(ws.name, CellRef(value)); absolute)
+ elseif is_valid_sheet_cellname(value)
+ return addDefName(ws, name, SheetCellRef(value); absolute)
+ elseif is_valid_cellrange(value)
+ return addDefName(ws, name, SheetCellRange(ws.name, CellRange(value)); absolute)
+ elseif is_valid_sheet_cellrange(value)
+ return addDefName(ws, name, SheetCellRange(value); absolute)
+ elseif is_valid_non_contiguous_cellrange(value)
+ return addDefName(ws, name, NonContiguousRange(ws, value); absolute)
+ elseif is_valid_non_contiguous_sheetcellrange(value)
+ return addDefName(ws, name, NonContiguousRange(value); absolute)
+ else
+ return addDefName(ws, name, value; absolute)
+ end
+end
diff --git a/src/worksheet.jl b/src/worksheet.jl
index 0526f653..124321bd 100644
--- a/src/worksheet.jl
+++ b/src/worksheet.jl
@@ -1,10 +1,10 @@
function Worksheet(xf::XLSXFile, sheet_element::XML.Node)
- @assert XML.tag(sheet_element) == "sheet"
+ XML.tag(sheet_element) != "sheet" && throw(XLSXError("Something wrong here!"))
a = XML.attributes(sheet_element)
sheetId = parse(Int, a["sheetId"])
relationship_id = a["r:id"]
- name = a["name"]
+ name = XML.unescape(a["name"])
is_hidden = haskey(a, "state") && a["state"] in ["hidden", "veryHidden"]
dim = read_worksheet_dimension(xf, relationship_id, name)
@@ -13,9 +13,7 @@ end
function Base.axes(ws::Worksheet, d)
dim = get_dimension(ws)
- if dim === nothing
- throw(DimensionMismatch("Worksheet $ws has no dimension"))
- elseif d == 1
+ if d == 1
return dim.start.row_number:dim.stop.row_number
elseif d == 2
return dim.start.column_number:dim.stop.column_number
@@ -25,39 +23,78 @@ function Base.axes(ws::Worksheet, d)
end
# 18.3.1.35 - dimension (Worksheet Dimensions). This is optional, and not required.
-function read_worksheet_dimension(xf::XLSXFile, relationship_id, name) :: Union{Nothing, CellRange}
- local result::Union{Nothing, CellRange} = nothing
+function read_worksheet_dimension(xf::XLSXFile, relationship_id, name)::Union{Nothing,CellRange}
wb = get_workbook(xf)
target_file = get_relationship_target_by_id("xl", wb, relationship_id)
- zip_io, doc = open_internal_file_stream(xf, target_file)
-
- reader = iterate(doc)
- # Now let's look for a row element, if it exists
- while reader !== nothing # go next node
- (sheet_row, state) = reader
- if XML.nodetype(sheet_row) == XML.Element && XML.tag(sheet_row) == "dimension"
- @assert XML.depth(sheet_row) == 2 "Malformed Worksheet \"$name\": unexpected node depth for dimension node: $(XML.depth(sheet_row))."
- ref_str = XML.attributes(sheet_row)["ref"]
- if is_valid_cellname(ref_str)
- result = CellRange("$(ref_str):$(ref_str)")
- else
- result = CellRange(ref_str)
- end
- break
+ if xf.is_writable # read from cached file
+ !haskey(xf.files, target_file) && throw(XLSXError("Worksheet \"$name\" not found in the XLSX file."))
+ i, j = get_idces(xf.data[target_file], "worksheet", "dimension")
+ if isnothing(i) || isnothing(j)
+ # No dimension element found, return nothing
+ return nothing
+ end
+ ref= xf.data[target_file][i][j]["ref"]
+ if is_valid_cellname(ref)
+ return CellRange(ref*":"*ref)
+ elseif is_valid_cellrange(ref)
+ return CellRange(ref)
+ else
+ throw(XLSXError("Malformed Worksheet \"$name\": unexpected dimension reference: $ref."))
end
- reader = iterate(doc, state)
- end
- return result
+ else #read from xlsx file
+ local result::Union{Nothing,CellRange} = nothing
+ doc = open_internal_file_stream(xf, target_file)
+ reader = iterate(doc)
+ # Now let's look for a row element, if it exists
+ while reader !== nothing # go next node
+ (sheet_row, state) = reader
+ if XML.nodetype(sheet_row) == XML.Element && XML.tag(sheet_row) == "dimension"
+
+ XML.depth(sheet_row) != 2 && throw(XLSXError("Malformed Worksheet \"$name\": unexpected node depth for dimension node: $(XML.depth(sheet_row))."))
+
+ ref_str = XML.attributes(sheet_row)["ref"]
+ if is_valid_cellname(ref_str)
+ result = CellRange("$(ref_str):$(ref_str)")
+ else
+ result = CellRange(ref_str)
+ end
+
+ break
+ end
+ reader = iterate(doc, state)
+ end
+
+ return result
+ end
end
@inline isdate1904(ws::Worksheet) = isdate1904(get_workbook(ws))
# Returns the dimension of this worksheet as a CellRange.
-# Returns `nothing` if the dimension is unknown.
-@inline get_dimension(ws::Worksheet) :: Union{Nothing, CellRange} = ws.dimension
+# If the dimension is unknown, computes a dimension from cells in cache.
+# If the cache is empty, set dimension to A1:A1.
+# If cache is not being used, throw an error.
+function get_dimension(ws::Worksheet)::Union{Nothing,CellRange}
+ !isnothing(ws.dimension) && return ws.dimension
+ # if (isnothing(ws.cache) || length(ws.cache.cells) < 1)
+ if isnothing(ws.cache)
+ throw(XLSXError("No worksheet dimension found for $(ws.name)."))
+ elseif length(ws.cache.cells) < 1
+ set_dimension!(ws, CellRange(CellRef(1, 1), CellRef(1, 1)))
+ else
+ row_extr = extrema(keys(ws.cache.cells))
+ row_min = first(row_extr)
+ row_max = last(row_extr)
+ col_extr = [extrema(y) for y in [keys(x) for x in values(ws.cache.cells)]]
+ col_min = minimum([x for x in first.(col_extr)])
+ col_max = maximum([x for x in last.(col_extr)])
+ set_dimension!(ws, CellRange(CellRef(row_min, col_min), CellRef(row_max, col_max)))
+ end
+ return ws.dimension
+end
function set_dimension!(ws::Worksheet, rng::CellRange)
ws.dimension = rng
@@ -68,8 +105,8 @@ end
getdata(sheet, ref)
getdata(sheet, row, column)
-Returns a scalar or a matrix with values from a spreadsheet.
-`ref` can be a cell reference or a range.
+Returns a scalar, vector or a matrix with values from a spreadsheet.
+`ref` can be a cell reference or a range or a valid defined name.
Indexing in a `Worksheet` will dispatch to `getdata` method.
@@ -78,39 +115,61 @@ Indexing in a `Worksheet` will dispatch to `getdata` method.
```julia
julia> f = XLSX.readxlsx("myfile.xlsx")
-julia> sheet = f["mysheet"]
+julia> sheet = f["mysheet"] # Worksheet
+
+julia> matrix = sheet["A1:B4"] # CellRange
+
+julia> matrix = sheet["A:B"] # Column range
-julia> matrix = sheet["A1:B4"]
+julia> matrix = sheet["1:4"] # Row range
-julia> single_value = sheet[2, 2] # B2
+julia> matrix = sheet["Contiguous"] # Named range
+
+julia> matrix = sheet[1:30, 1] # use unit ranges to define rows and/or columns
+
+julia> matrix = sheet[[1, 2, 3], 1] # vectors of integers to define rows and/or columns
+
+julia> vector = sheet["A1:A4,C1:C4,G5"] # Non-contiguous range
+
+julia> vector = sheet["Location"] # Non-contiguous named range
+
+julia> single_value = sheet[2, 2] # Cell "B2"
```
See also [`XLSX.readdata`](@ref).
"""
getdata(ws::Worksheet, single::CellRef) = getdata(ws, getcell(ws, single))
getdata(ws::Worksheet, row::Integer, col::Integer) = getdata(ws, CellRef(row, col))
+getdata(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}) = [getdata(ws, a, b) for a in row, b in col]
+getdata(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}) = [getdata(ws, a, b) for a in row, b in col]
+getdata(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}) = [getdata(ws, a, b) for a in row, b in col]
getdata(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}) = getdata(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))))
+getdata(ws::Worksheet, ::Colon, ::Colon) = getdata(ws)
+function getdata(ws::Worksheet, ::Colon)
+ dim = get_dimension(ws)
+ getdata(ws, dim)
+end
function getdata(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon)
dim = get_dimension(ws)
- return if dim === nothing
- @warn "No worksheet dimension found"
- []
- else
- getdata(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)))
- end
+ getdata(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)))
end
function getdata(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}})
dim = get_dimension(ws)
- return if dim === nothing
- @warn "No worksheet dimension found"
- []
- else
- getdata(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))))
- end
+ getdata(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))))
+end
+function getdata(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon)
+ dim = get_dimension(ws)
+ col = dim.start.column_number:dim.stop.column_number
+ return getdata(ws, row, col)
+end
+function getdata(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}})
+ dim = get_dimension(ws)
+ row = dim.start.row_number:dim.stop.row_number
+ return getdata(ws, row, col)
end
-function getdata(ws::Worksheet, rng::CellRange) :: Array{Any,2}
- result = Array{Any, 2}(undef, size(rng))
+function getdata(ws::Worksheet, rng::CellRange)::Array{Any,2}
+ result = Array{Any,2}(undef, size(rng))
fill!(result, missing)
top = row_number(rng.start)
@@ -118,66 +177,84 @@ function getdata(ws::Worksheet, rng::CellRange) :: Array{Any,2}
left = column_number(rng.start)
right = column_number(rng.stop)
- for sheetrow in eachrow(ws)
- if top <= sheetrow.row && sheetrow.row <= bottom
- for column in left:right
- cell = getcell(sheetrow, column)
- if !isempty(cell)
- (r, c) = relative_cell_position(cell, rng)
- result[r, c] = getdata(ws, cell)
+ # if cache is in use, look-up data direct rather than iterating
+ if is_cache_enabled(ws) && ws.cache !== nothing && !ws.cache.is_empty
+ for row in top:bottom
+ if haskey(ws.cache.cells, row)
+ for column in left:right
+ if haskey(ws.cache.cells, row) && haskey(ws.cache.cells[row], column)
+ cell = ws.cache.cells[row][column]
+ (r, c) = relative_cell_position(cell, rng)
+ result[r, c] = getdata(ws, cell)
+ end
end
end
end
- # don't need to read new rows
- if sheetrow.row > bottom
- break
+ else # no cache, need to iterate rows
+ for sheetrow in eachrow(ws)
+ if top <= sheetrow.row && sheetrow.row <= bottom
+ for column in left:right
+ cell = getcell(sheetrow, column)
+ if !isempty(cell)
+ (r, c) = relative_cell_position(cell, rng)
+ result[r, c] = getdata(ws, cell)
+ end
+ end
+ end
+
+ # don't need to read new rows
+ if sheetrow.row > bottom
+ break
+ end
end
end
-
return result
end
-function getdata(ws::Worksheet, rng::ColumnRange) :: Array{Any,2}
- columns_count = length(rng)
- columns = Vector{Vector{Any}}(undef, columns_count)
- for i in 1:columns_count
- columns[i] = Vector{Any}()
- end
-
- left, right = column_bounds(rng)
+function getdata(ws::Worksheet, rng::ColumnRange)::Array{Any,2}
+ dim = get_dimension(ws)
+ start = CellRef(dim.start.row_number, rng.start)
+ stop = CellRef(dim.stop.row_number, rng.stop)
+ return getdata(ws, CellRange(start, stop))
+end
+function getdata(ws::Worksheet, rng::RowRange)::Array{Any,2}
+ dim = get_dimension(ws)
+ start = CellRef(rng.start, dim.start.column_number,)
+ stop = CellRef(rng.stop, dim.stop.column_number)
+ return getdata(ws, CellRange(start, stop))
+end
- for sheetrow in eachrow(ws)
- for column in left:right
- cell = getcell(sheetrow, column)
- c = relative_column_position(cell, rng) # r will be ignored
- push!(columns[c], getdata(ws, cell))
+function getdata(ws::Worksheet, rng::NonContiguousRange)::Vector{Any}
+ do_sheet_names_match(ws, rng)
+ results = Vector{Any}()
+ for r in rng.rng
+ if r isa CellRef
+ push!(results, getdata(ws, r))
+ else
+ for cell in r
+ push!(results, getdata(ws, cell))
+ end
end
end
-
- rows = length(columns[1])
- for i in 1:columns_count
- @assert length(columns[i]) == rows "Inconsistent state: Each column should have the same number of rows."
- end
-
- return hcat(columns...)
+ return results
end
-function getdata(ws::Worksheet, ref::AbstractString) :: Union{Array{Any,2}, Any}
- if is_valid_cellname(ref)
- return getdata(ws, CellRef(ref))
- elseif is_valid_cellrange(ref)
- return getdata(ws, CellRange(ref))
- elseif is_valid_column_range(ref)
- return getdata(ws, ColumnRange(ref))
- elseif is_worksheet_defined_name(ws, ref)
+# Needed for definedName references
+getdata(ws::Worksheet, s::SheetCellRef) = do_sheet_names_match(ws, s) && getdata(ws, s.cellref)
+getdata(ws::Worksheet, s::SheetCellRange) = do_sheet_names_match(ws, s) && getdata(ws, s.rng)
+getdata(ws::Worksheet, s::SheetColumnRange) = do_sheet_names_match(ws, s) && getdata(ws, s.colrng)
+getdata(ws::Worksheet, s::SheetRowRange) = do_sheet_names_match(ws, s) && getdata(ws, s.rowrng)
+
+function getdata(ws::Worksheet, ref::AbstractString)
+ if is_worksheet_defined_name(ws, ref)
v = get_defined_name_value(ws, ref)
if is_defined_name_value_a_constant(v)
return v
elseif is_defined_name_value_a_reference(v)
return getdata(ws, v)
else
- error("Unexpected defined name value: $v.")
+ throw(XLSXError("Unexpected defined name value: $v."))
end
elseif is_workbook_defined_name(get_workbook(ws), ref)
wb = get_workbook(ws)
@@ -187,22 +264,35 @@ function getdata(ws::Worksheet, ref::AbstractString) :: Union{Array{Any,2}, Any}
elseif is_defined_name_value_a_reference(v)
return getdata(get_xlsxfile(ws), v)
else
- error("Unexpected defined name value: $v.")
+ throw(XLSXError("Unexpected defined name value: $v."))
end
+ elseif is_valid_cellname(ref)
+ return getdata(ws, CellRef(ref))
+ elseif is_valid_cellrange(ref)
+ return getdata(ws, CellRange(ref))
+ elseif is_valid_column_range(ref)
+ return getdata(ws, ColumnRange(ref))
+ elseif is_valid_row_range(ref)
+ return getdata(ws, RowRange(ref))
+ elseif is_valid_sheet_cellname(ref)
+ return getdata(ws, SheetCellRef(ref))
+ elseif is_valid_sheet_cellrange(ref)
+ return getdata(ws, SheetCellRange(ref))
+ elseif is_valid_sheet_column_range(ref)
+ return getdata(ws, SheetColumnRange(ref))
+ elseif is_valid_sheet_row_range(ref)
+ return getdata(ws, SheetRowRange(ref))
+ elseif is_valid_non_contiguous_cellrange(ref)
+ return getdata(ws, NonContiguousRange(ws, ref))
+ elseif is_valid_non_contiguous_sheetcellrange(ref)
+ nc = NonContiguousRange(ref)
+ return do_sheet_names_match(ws, nc) && getdata(ws, nc)
else
- error("$ref is not a valid cell or range reference.")
+ throw(XLSXError("`$ref` is not a valid cell or range reference."))
end
end
-getdata(ws::Worksheet, rng::SheetCellRange) = getdata(get_xlsxfile(ws), rng)
-
-function getdata(ws::Worksheet)
- if ws.dimension !== nothing
- return getdata(ws, get_dimension(ws))
- else
- error("Worksheet dimension is unknown.")
- end
-end
+getdata(ws::Worksheet) = getdata(ws, get_dimension(ws))
Base.getindex(ws::Worksheet, r) = getdata(ws, r)
Base.getindex(ws::Worksheet, r, c) = getdata(ws, r, c)
@@ -210,8 +300,8 @@ Base.getindex(ws::Worksheet, ::Colon) = getdata(ws)
function Base.show(io::IO, ws::Worksheet)
hidden_string = ws.is_hidden ? "(hidden)" : ""
- if get_dimension(ws) !== nothing
- rg = get_dimension(ws)
+ rg = get_dimension(ws)
+ if rg !== nothing
nrow, ncol = size(rg)
@printf(io, "%d×%d %s: [\"%s\"](%s) %s", nrow, ncol, typeof(ws), ws.name, rg, hidden_string)
else
@@ -221,8 +311,13 @@ end
"""
getcell(sheet, ref)
+ getcell(sheet, row, col)
+
+Return an `AbstractCell` that represents a cell in the spreadsheet.
+Return a matrix with cells as `Array{AbstractCell, 2}` if called
+with a reference tomore than one cell.
-Returns an `AbstractCell` that represents a cell in the spreadsheet.
+If `ref` is a range, `getcell` dispatches to `getcellrange`.
Example:
@@ -232,20 +327,24 @@ julia> xf = XLSX.readxlsx("myfile.xlsx")
julia> sheet = xf["mysheet"]
julia> cell = XLSX.getcell(sheet, "A1")
+
+julia> cell = XLSX.getcell(sheet, 1:3, [2,4,6])
+
```
+
+Other examples are as [`getdata()`](@ref).
+
"""
-function getcell(ws::Worksheet, single::CellRef) :: AbstractCell
+function getcell(ws::Worksheet, single::CellRef)::AbstractCell
# Access cache directly if it exists and if file `isread` - much faster!
- if is_cache_enabled(ws) && ws.cache !== nothing
- if haskey(get_xlsxfile(ws).files, "xl/worksheets/sheet$(ws.sheetId).xml") && get_xlsxfile(ws).files["xl/worksheets/sheet$(ws.sheetId).xml"] == true
- if haskey(ws.cache.cells, single.row_number)
- if haskey(ws.cache.cells[single.row_number], single.column_number)
- return ws.cache.cells[single.row_number][single.column_number]
- end
+ if is_cache_enabled(ws) && ws.cache !== nothing && !ws.cache.is_empty
+ if haskey(ws.cache.cells, single.row_number)
+ if haskey(ws.cache.cells[single.row_number], single.column_number)
+ return ws.cache.cells[single.row_number][single.column_number]
end
- return EmptyCell(single)
end
+ return EmptyCell(single)
end
# If can't use cache then iterate sheetrows
@@ -258,87 +357,162 @@ function getcell(ws::Worksheet, single::CellRef) :: AbstractCell
return EmptyCell(single)
end
+getcell(ws::Worksheet, s::SheetCellRef) = do_sheet_names_match(ws, s) && getcell(ws, s.cellref)
+getcell(ws::Worksheet, s::SheetCellRange) = do_sheet_names_match(ws, s) && getcellrange(ws, s.rng)
+getcell(ws::Worksheet, s::SheetColumnRange) = do_sheet_names_match(ws, s) && getcellrange(ws, s.colrng)
+getcell(ws::Worksheet, s::SheetRowRange) = do_sheet_names_match(ws, s) && getcellrange(ws, s.rowrng)
+getcell(ws::Worksheet, s::CellRange) = getcellrange(ws, s)
+getcell(ws::Worksheet, s::ColumnRange) = getcellrange(ws, s.colrng)
+getcell(ws::Worksheet, s::RowRange) = getcellrange(ws, s.rowrng)
+
+getcell(ws::Worksheet, row::Integer, col::Integer) = getcell(ws, CellRef(row, col))
+getcell(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}) = getcellrange(ws, row, col)
+getcell(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}) = getcellrange(ws, row, col)
+getcell(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}) = getcellrange(ws, row, col)
+getcell(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}) = getcellrange(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))))
+function getcell(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon)
+ dim = get_dimension(ws)
+ getcellrange(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)))
+end
+function getcell(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}})
+ dim = get_dimension(ws)
+ getcellrange(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))))
+end
+
function getcell(ws::Worksheet, ref::AbstractString)
- if is_valid_cellname(ref)
+ if is_worksheet_defined_name(ws, ref)
+ v = get_defined_name_value(ws, ref)
+ if is_defined_name_value_a_reference(v)
+ return getcell(ws, v)
+ else
+ throw(XLSXError("`$ref` is not a valid cell or range reference."))
+ end
+ elseif is_workbook_defined_name(get_workbook(ws), ref)
+ wb = get_workbook(ws)
+ v = get_defined_name_value(wb, ref)
+ if is_defined_name_value_a_reference(v)
+ return isa(v, SheetCellRef) ? getcell(get_xlsxfile(ws), v) : getcellrange(get_xlsxfile(ws), v)
+ else
+ throw(XLSXError("`$ref` is not a valid cell or range reference."))
+ end
+ elseif is_valid_cellname(ref)
return getcell(ws, CellRef(ref))
- else
- error("$ref is not a valid cell reference.")
+ elseif is_valid_sheet_cellname(ref)
+ return getcell(ws, SheetCellRef(ref))
+ elseif is_valid_cellrange(ref)
+ return getcellrange(ws, CellRange(ref))
+ elseif is_valid_column_range(ref)
+ return getcellrange(ws, ColumnRange(ref))
+ elseif is_valid_row_range(ref)
+ return getcellrange(ws, RowRange(ref))
+ elseif is_valid_non_contiguous_range(ref)
+ return getcellrange(ws, NonContiguousRange(ws, ref))
+ elseif is_valid_sheet_cellrange(ref)
+ return getcellrange(ws, SheetCellRange(ref))
+ elseif is_valid_sheet_column_range(ref)
+ return getcellrange(ws, SheetColumnRange(ref))
+ elseif is_valid_sheet_row_range(ref)
+ return getcellrange(ws, SheetRowRange(ref))
+ elseif is_valid_non_contiguous_range(ref)
+ return getcellrange(ws, NonContiguousRange(ref))
end
+ throw(XLSXError("`$ref` is not a valid cell or range reference."))
end
-getcell(ws::Worksheet, row::Integer, col::Integer) = getcell(ws, CellRef(row, col))
-
"""
getcellrange(sheet, rng)
-Returns a matrix with cells as `Array{AbstractCell, 2}`.
-`rng` must be a valid cell range, as in `"A1:B2"`.
+Return a matrix with cells as `Array{AbstractCell, 2}`.
+`rng` must be a valid cell range, column range or row range,
+as in `"A1:B2"`, `"A:B"` or `"1:2"`, or a non-contiguous range.
+For row and column ranges, the extent of the range in the other
+dimension is determined by the worksheet's dimension.
+A non-contiguous range (which is not rectangular) will return a vector.
+
+For example usage, see [`getdata()`](@ref).
+
"""
-function getcellrange(ws::Worksheet, rng::CellRange) :: Array{AbstractCell,2}
- result = Array{AbstractCell, 2}(undef, size(rng))
+function getcellrange(ws::Worksheet, rng::CellRange)::Array{AbstractCell,2}
+ result = Array{AbstractCell,2}(undef, size(rng))
for cellref in rng
(r, c) = relative_cell_position(cellref, rng)
- result[r, c] = EmptyCell(cellref)
+ cell = getcell(ws, cellref)
+ result[r, c] = isempty(cell) ? EmptyCell(cellref) : cell
end
-
- top = row_number(rng.start)
- bottom = row_number(rng.stop)
- left = column_number(rng.start)
- right = column_number(rng.stop)
-
- for sheetrow in eachrow(ws)
- if top <= sheetrow.row && sheetrow.row <= bottom
- for column in left:right
- cell = getcell(sheetrow, column)
- if !isempty(cell)
- (r, c) = relative_cell_position(cell, rng)
- result[r, c] = cell
- end
- end
- end
-
- # don't need to read new rows
- if sheetrow.row > bottom
- break
- end
- end
-
return result
end
-function getcellrange(ws::Worksheet, rng::ColumnRange) :: Array{AbstractCell,2}
- columns_count = length(rng)
- columns = Vector{Vector{AbstractCell}}(undef, columns_count)
- for i in 1:columns_count
- columns[i] = Vector{AbstractCell}()
- end
+getcellrange(ws::Worksheet, s::SheetCellRange) = do_sheet_names_match(ws, s) && getcellrange(ws, s.rng)
+getcellrange(ws::Worksheet, s::SheetColumnRange) = do_sheet_names_match(ws, s) && getcellrange(ws, s.colrng)
+getcellrange(ws::Worksheet, s::SheetRowRange) = do_sheet_names_match(ws, s) && getcellrange(ws, s.rowrng)
- let
- left, right = column_bounds(rng)
+getcellrange(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}) = [getcell(ws, a, b) for a in row, b in col]
+getcellrange(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}) = [getcell(ws, a, b) for a in row, b in col]
+getcellrange(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}) = [getcell(ws, a, b) for a in row, b in col]
+getcellrange(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}) = getcell(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))))
+getcellrange(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) = getcell(ws, row, :)
+getcellrange(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}) = getcell(ws, :, col)
- for sheetrow in eachrow(ws)
- for column in left:right
- cell = getcell(sheetrow, column)
- c = relative_column_position(cell, rng) # r will be ignored
- push!(columns[c], cell)
+function getcellrange(ws::Worksheet, rng::ColumnRange)::Array{AbstractCell,2}
+ dim = get_dimension(ws)
+ start = CellRef(dim.start.row_number, rng.start)
+ stop = CellRef(dim.stop.row_number, rng.stop)
+ return getcellrange(ws, CellRange(start, stop))
+end
+function getcellrange(ws::Worksheet, rng::RowRange)::Array{AbstractCell,2}
+ dim = get_dimension(ws)
+ start = CellRef(rng.start, dim.start.column_number,)
+ stop = CellRef(rng.stop, dim.stop.column_number)
+ return getcellrange(ws, CellRange(start, stop))
+end
+
+function getcellrange(ws::Worksheet, rng::NonContiguousRange)::Vector{AbstractCell}
+ results = Vector{AbstractCell}()
+ for r in rng.rng
+ if r isa CellRef
+ push!(results, getcell(ws, r))
+ else
+ for cell in r
+ push!(results, getcell(ws, cell))
end
end
end
-
- rows = length(columns[1])
- for i in 1:columns_count
- @assert length(columns[i]) == rows "Inconsistent state: Each column should have the same number of rows."
- end
-
- return hcat(columns...)
+ return results
end
function getcellrange(ws::Worksheet, rng::AbstractString)
- if is_valid_cellrange(rng)
+ if is_worksheet_defined_name(ws, rng)
+ v = get_defined_name_value(ws, rng)
+ if is_defined_name_value_a_reference(v)
+ return getcellrange(ws, v)
+ else
+ throw(XLSXError("$rng is not a valid cell range."))
+ end
+ elseif is_workbook_defined_name(get_workbook(ws), rng)
+ wb = get_workbook(ws)
+ v = get_defined_name_value(wb, rng)
+ if is_defined_name_value_a_reference(v)
+ isa(v, SheetCellRef) && throw(XLSXError("`$rng` is not a valid cell range."))
+ return getcellrange(get_xlsxfile(ws), v)
+ else
+ throw(XLSXError("`$rng` is not a valid cell range."))
+ end
+ elseif is_valid_cellrange(rng)
return getcellrange(ws, CellRange(rng))
elseif is_valid_column_range(rng)
return getcellrange(ws, ColumnRange(rng))
- else
- error("$rng is not a valid cell range.")
+ elseif is_valid_row_range(rng)
+ return getcellrange(ws, RowRange(rng))
+ elseif is_valid_non_contiguous_range(rng)
+ return getcellrange(ws, NonContiguousRange(ws, rng))
+ elseif is_valid_sheet_cellrange(rng)
+ return getcellrange(s, SheetCellRange(rng))
+ elseif is_valid_sheet_column_range(rng)
+ return getcellrange(s, SheetColumnRange(rng))
+ elseif is_valid_sheet_row_range(rng)
+ return getcellrange(s, SheetRowRange(rng))
+ elseif is_valid_non_contiguous_range(rng)
+ return getcellrange(s, NonContiguousRange(rng))
end
+ throw(XLSXError("`$rng` is not a valid cell range."))
end
diff --git a/src/write.jl b/src/write.jl
index 94613f39..964f747f 100644
--- a/src/write.jl
+++ b/src/write.jl
@@ -1,70 +1,46 @@
"""
- opentemplate(source::Union{AbstractString, IO}) :: XLSXFile
+ savexlsx(f::XLSXFile)
-Read an existing Excel file as a template and return as a writable `XLSXFile` for editing
-and saving to another file with `XLSX.writexlsx`.
+Save an `XLSXFile` instance back to the file from which it was opened, overwriting original content.
-# Examples
-```julia
-julia> xf = opentemplate("myExcelFile")
-```
+A new `XLSXFile` created with `XLSX.newxlsx` (or using `openxlsx` without specifying a filename) will
+have `source` set to `"blank.xlsx"` and cannot be saved with this function. Use `writexlsx` instead to
+specify a file name for the saved file.
-"""
-opentemplate(source::Union{AbstractString, IO}) :: XLSXFile = open_or_read_xlsx(source, true, true, true)
-
-@inline open_xlsx_template(source::Union{AbstractString, IO}) :: XLSXFile = open_or_read_xlsx(source, true, true, true)
-
-function _relocatable_data_path(; path::AbstractString=Artifacts.artifact"XLSX_relocatable_data")
- return path
-end
-
-"""
- newxlsx() :: XLSXFile
-
-Return an empty, writable `XLSXFile` with 1 worksheet (`Sheet1`) for editing and
-saving to a file with `XLSX.writexlsx`.
-
-# Examples
-```julia
-julia> xf = newxlsx()
-```
+Returns the filepath of the written file if a filename is supplied, or `nothing` if writing to an `IO`.
"""
-newxlsx() = open_empty_template()
-
-function open_empty_template(
- sheetname::AbstractString="";
- path::AbstractString=_relocatable_data_path()
- ) :: XLSXFile
-
- empty_excel_template = joinpath(path, "blank.xlsx")
- @assert isfile(empty_excel_template) "Couldn't find template file $empty_excel_template."
- xf = open_xlsx_template(empty_excel_template)
-
- if sheetname != ""
- rename!(xf[1], sheetname)
- end
-
- return xf
+function savexlsx(f::XLSXFile)
+ f.source == "blank.xlsx" && throw(XLSXError("Can't save a blank `XLSXFile` instance. Use `writexlsx` instead to specify a file name."))
+ return writexlsx(f.source, f; overwrite=true)
end
+
"""
writexlsx(output_source, xlsx_file; [overwrite=false])
Write an Excel file given by `xlsx_file::XLSXFile` to IO or filepath `output_source`.
+The source attribute of the `XLSXFile` will be updated to the `output_source` if it is a filepath.
+
+Returns the filepath of the written file if a filename is supplied, or `nothing` if writing to an `IO`.
+
If `overwrite=true`, `output_source` (when a filepath) will be overwritten if it exists.
"""
-function writexlsx(output_source::Union{AbstractString, IO}, xf::XLSXFile; overwrite::Bool=false)
+function writexlsx(output_source::Union{AbstractString,IO}, xf::XLSXFile; overwrite::Bool=false)
- @assert is_writable(xf) "XLSXFile instance is not writable."
- @assert all(values(xf.files)) "Some internal files were not loaded into memory. Did you use `XLSX.open_xlsx_template` to open this file?"
+ !is_writable(xf) && throw(XLSXError("XLSXFile instance is not writable."))
+ if !all(values(xf.files))
+ throw(XLSXError("Some internal files were not loaded into memory. Did you use `XLSX.open_xlsx_template` to open this file?"))
+ end
if output_source isa AbstractString && !overwrite
- @assert !isfile(output_source) "Output file $output_source already exists."
+ isfile(output_source) && throw(XLSXError("Output file $output_source already exists."))
end
- update_worksheets_xml!(xf)
+ update_worksheets_xml!(xf)
+ update_workbook_xml!(xf)
+ # update_relationships(xf)
ZipArchives.ZipWriter(output_source) do xlsx
# write XML files
@@ -89,34 +65,43 @@ function writexlsx(output_source::Union{AbstractString, IO}, xf::XLSXFile; overw
print(xlsx, generate_sst_xml_string(get_sst(xf)))
end
end
- nothing
+
+ if !(output_source isa IO)
+ (xf.source = output_source) # update source if output_source is a file path
+ return isabspath(xf.source) ? xf.source : abspath(xf.source)
+ else
+ return nothing
+ end
+
end
get_worksheet_internal_file(ws::Worksheet) = get_relationship_target_by_id("xl", get_workbook(ws), ws.relationship_id)
-get_worksheet_xml_document(ws::Worksheet) = get_xlsxfile(ws).data[ get_worksheet_internal_file(ws) ]
+get_worksheet_xml_document(ws::Worksheet) = get_xlsxfile(ws).data[get_worksheet_internal_file(ws)]
function set_worksheet_xml_document!(ws::Worksheet, xdoc::XML.Node)
- @assert XML.nodetype(xdoc) == XML.Document "Expected an XML Document node, got $(XML.nodetype(xdoc))."
+ XML.nodetype(xdoc) != XML.Document && throw(XLSXError("Expected an XML Document node, got $(XML.nodetype(xdoc))."))
xf = get_xlsxfile(ws)
filename = get_worksheet_internal_file(ws)
- @assert haskey(xf.data, filename) "Internal file not found for $(ws.name)."
+ !haskey(xf.data, filename) && throw(XLSXError("Internal file not found for $(ws.name)."))
xf.data[filename] = xdoc
-
end
-function generate_sst_xml_string(sst::SharedStringTable) :: String
- @assert sst.is_loaded "Can't generate XML string from a Shared String Table that is not loaded."
+function generate_sst_xml_string(sst::SharedStringTable)::String
+ !sst.is_loaded && throw(XLSXError("Can't generate XML string from a Shared String Table that is not loaded."))
buff = IOBuffer()
# TODO:
-""")
+"""
+ )
for s in sst.formatted_strings
print(buff, s)
end
-
+
print(buff, "")
return String(take!(buff))
end
@@ -127,17 +112,17 @@ function add_node_formula!(node, f::Formula)
end
function add_node_formula!(node, f::FormulaReference)
- f_node = XML.Element("f"; t = "shared", si = string(f.id))
+ f_node = XML.Element("f"; t="shared", si=string(f.id))
push!(node, f_node)
end
function add_node_formula!(node, f::ReferencedFormula)
- f_node = XML.Element("f", Text(f.formula); t = "shared", si = string(f.id), ref = f.ref)
+ f_node = XML.Element("f", Text(f.formula); t="shared", si=string(f.id), ref=f.ref)
push!(node, f_node)
end
function find_all_nodes(givenpath::String, doc::XML.Node)::Vector{XML.Node}
- @assert XML.nodetype(doc) == XML.Document
+ XML.nodetype(doc) != XML.Document && throw(XLSXError("Something wrong here!"))
found_nodes = Vector{XML.Node}()
for xp in get_node_paths(doc)
if xp.path == givenpath
@@ -147,7 +132,7 @@ function find_all_nodes(givenpath::String, doc::XML.Node)::Vector{XML.Node}
return found_nodes
end
function get_node_paths(node::XML.Node)
- @assert XML.nodetype(node) == XML.Document
+ XML.nodetype(node) != XML.Document && throw(XLSXError("Something wrong here!"))
default_ns = get_default_namespace(node[end])
xpaths = Vector{xpath}()
get_node_paths!(xpaths, node, default_ns, "")
@@ -158,45 +143,47 @@ function get_node_paths!(xpaths::Vector{xpath}, node::XML.Node, default_ns, path
for c in XML.children(node)
if XML.nodetype(c) ∉ [XML.Declaration, XML.Comment, XML.Text]
node_tag = XML.tag(c)
- if !occursin(":", node_tag)
+ if !occursin(":", node_tag)
node_tag = default_ns * ":" * node_tag
end
npath = path * "/" * node_tag
push!(xpaths, xpath(c, npath))
- if length(XML.children(c))>0
+ if length(XML.children(c)) > 0
get_node_paths!(xpaths, c, default_ns, npath)
end
end
- end
+ end
return nothing
end
-function unlink_rows(node::XML.Node) # removes all rows from a sheetData XML node.
- new_worksheet = XML.Element("sheetData")
+
+# Remove all children with tag given by att[2] from a parent XML node with a tag given by att[1].
+function unlink(node::XML.Node, att::Tuple{String,String})
+ new_node = XML.Element(first(att))
a = XML.attributes(node)
if !isnothing(a) # Copy attributes across to new node
for (k, v) in XML.attributes(node)
- new_worksheet[k] = v
+ new_node[k] = v
end
end
- for child in XML.children(node) # Copy any child nodes that are not rows across to new node
- if XML.tag(child) != "row"
- push!(new_worksheet, child)
+ for child in XML.children(node) # Copy any child nodes with tags that are not att[2] across to new node
+ if XML.tag(child) != last(att)
+ push!(new_node, child)
end
end
- return new_worksheet
+ return new_node
end
function get_idces(doc, t, b)
- i=1
- j=1
+ i = 1
+ j = 1
while XML.tag(doc[i]) != t
- i+=1
+ i += 1
if i > length(XML.children(doc))
return nothing, nothing
end
end
while XML.tag(doc[i][j]) != b
- j+=1
+ j += 1
if j > length(XML.children(doc[i]))
return i, nothing
end
@@ -213,13 +200,13 @@ function update_worksheets_xml!(xl::XLSXFile)
xroot = doc[end]
# check namespace and root node name
- @assert get_default_namespace(xroot) == SPREADSHEET_NAMESPACE_XPATH_ARG "Unsupported Spreadsheet XML namespace $(get_default_namespace(xroot))."
- @assert XML.tag(xroot) == "worksheet" "Malformed Excel file. Expected root node named `worksheet` in worksheet XML file."
+ get_default_namespace(xroot) != SPREADSHEET_NAMESPACE_XPATH_ARG && throw(XLSXError("Unsupported Spreadsheet XML namespace $(get_default_namespace(xroot))."))
+ XML.tag(xroot) != "worksheet" && throw(XLSXError("Malformed Excel file. Expected root node named `worksheet` in worksheet XML file."))
# Since we do not at the moment track changes, we need to delete all data and re-write it, but this could entail losses.
# |- Column formatting is preserved in the subtree.
# |- Row formatting would get lost if we simply remove the children of storing the rows
- unhandled_attributes = Dict{Int, Dict{String,String}}() # from row number to attribute and its value
+ unhandled_attributes = Dict{Int,Dict{String,String}}() # from row number to attribute and its value
# The following attributes will be overwritten by us and need not be preserved
handled_attributes = Set{String}([
@@ -229,7 +216,7 @@ function update_worksheets_xml!(xl::XLSXFile)
])
let
- child_nodes = find_all_nodes("/$SPREADSHEET_NAMESPACE_XPATH_ARG:worksheet/$SPREADSHEET_NAMESPACE_XPATH_ARG:sheetData/$SPREADSHEET_NAMESPACE_XPATH_ARG:row", doc)
+ child_nodes = find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":worksheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":sheetData/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":row", doc)
i, j = get_idces(doc, "worksheet", "sheetData")
parent = doc[i][j]
@@ -241,17 +228,17 @@ function update_worksheets_xml!(xl::XLSXFile)
attributes = XML.attributes(c)
if !isnothing(attributes)
unhandled_attributes_ = filter(attribute -> !in(first(attribute), handled_attributes), attributes)
- if length(unhandled_attributes_)>0
+ if length(unhandled_attributes_) > 0
row_nr = parse(Int, c["r"])
unhandled_attributes[row_nr] = unhandled_attributes_
end
end
else
@warn("Unexpected node under sheetData: $(XML.tag(c))")
- end
+ end
end
- doc[i][j] = unlink_rows(parent)
+ doc[i][j] = unlink(parent, ("sheetData", "row"))
end
# updates sheetData
@@ -270,16 +257,15 @@ function update_worksheets_xml!(xl::XLSXFile)
local spans_str::String = ""
# Every row has the `spans=1:` property. Set it to the whole range of columns by default
- if !isnothing(get_dimension(sheet))
- spans_str = string(column_number(get_dimension(sheet).start), ":", column_number(get_dimension(sheet).stop))
- end
+ d = get_dimension(sheet)
+ spans_str = string(column_number(d.start), ":", column_number(d.stop))
# iterates over WorksheetCache cells and write the XML
for r in eachrow(sheet)
row_nr = row_number(r)
ordered_column_indexes = sort(collect(keys(r.rowcells)))
- row_node = XML.Element("row"; r = string(row_nr))
+ row_node = XML.Element("row"; r=string(row_nr))
if spans_str != ""
row_node["spans"] = spans_str
end
@@ -297,7 +283,7 @@ function update_worksheets_xml!(xl::XLSXFile)
# add cells to row
for c in ordered_column_indexes
cell = getcell(r, c)
- c_element = XML.Element("c"; r = cell.ref.name)
+ c_element = XML.Element("c"; r=cell.ref.name)
if cell.datatype != ""
c_element["t"] = cell.datatype
@@ -317,16 +303,16 @@ function update_worksheets_xml!(xl::XLSXFile)
end
push!(row_node, c_element)
end
-
push!(sheetData_node, row_node)
end
- doc[i][j]=sheetData_node
+ doc[i][j] = sheetData_node
# updates worksheet dimension
- if get_dimension(sheet) !== nothing
- i, j = get_idces(doc, "worksheet", "dimension")
+
+ i, j = get_idces(doc, "worksheet", "dimension")
+ if !isnothing(j)
dimension_node = doc[i][j]
- dimension_node["ref"] = string(get_dimension(sheet))
+ dimension_node["ref"] = string(d)
doc[i][j] = dimension_node
end
@@ -336,15 +322,92 @@ function update_worksheets_xml!(xl::XLSXFile)
nothing
end
+function abscell(c::CellRef)
+ col, row = split_cellname(c.name)
+ return "\$" * col * "\$" * string(row)
+end
+
+mkabs(c::SheetCellRef) = abscell(c.cellref)
+mkabs(c::SheetCellRange) = abscell(c.rng.start) * ":" * abscell(c.rng.stop)
+function make_absolute(dn::DefinedNameValue)
+ if dn.value isa NonContiguousRange
+ v = ""
+ for (i, r) in enumerate(dn.value.rng)
+ cr = r isa CellRange ? SheetCellRange(dn.value.sheet, r) : SheetCellRef(dn.value.sheet, r) # need to separate and handle separately
+ if dn.isabs[i]
+ v *= quoteit(cr.sheet) * "!" * mkabs(cr) * ","
+ else
+ v *= string(cr) * ","
+ end
+ end
+ return v[1:end-1]
+ else
+ return dn.isabs ? quoteit(dn.value.sheet) * "!" * mkabs(dn.value) : string(dn.value)
+ end
+end
+
+function update_workbook_xml!(xl::XLSXFile) # Need to update and .
+ wb = get_workbook(xl)
+
+ #update defined names
+ if length(wb.workbook_names) > 0 || length(wb.worksheet_names) > 0 # skip if no defined names present
+ wbdoc = xmlroot(xl, "xl/workbook.xml") # find the block in the workbook's xml file
+ i, j = get_idces(wbdoc, "workbook", "definedNames")
+ if isnothing(j)
+ # there is no block in the workbook's xml file, so we'll need to create one
+ # The block goes after the block. Need to move everything down one to make room.
+ m, n = get_idces(wbdoc, "workbook", "sheets")
+ nchildren = length(XML.children(wbdoc[m]))
+ push!(wbdoc[m], wbdoc[m][end])
+ for c in nchildren-1:-1:n+1
+ wbdoc[m][c+1] = wbdoc[m][c]
+ end
+ definedNames = XML.Element("definedNames")
+ j = n + 1
+ else
+ definedNames = unlink(wbdoc[i][j], ("definedNames", "definedName")) # Remove old defined names
+ end
+ for (k, v) in wb.workbook_names
+ if typeof(v.value) <: DefinedNameRangeTypes
+ v = make_absolute(v)
+ else
+ v = string(v.value)
+ end
+ dn_node = XML.Element("definedName", name=k, XML.Text(v))
+ push!(definedNames, dn_node)
+ end
+ for (k, v) in wb.worksheet_names
+ if typeof(v.value) <: DefinedNameRangeTypes
+ v = make_absolute(v)
+ else
+ v = string(v.value)
+ end
+ dn_node = XML.Element("definedName", name=last(k), localSheetId=first(k) - 1, XML.Text(v))
+ push!(definedNames, dn_node)
+ end
+ wbdoc[i][j] = definedNames # Add the new definedNames block to the workbook's xml file
+ end
+
+ #update sheets
+ doc = xmlroot(xl, "xl/workbook.xml")
+ i, j = get_idces(doc, "workbook", "sheets")
+ unlink(doc[i][j], ("sheets", "sheet"))
+ sheets_element = XML.Element("sheets")
+ for s in wb.sheets
+ sheet_element = XML.Element("sheet"; name=XML.escape(s.name))
+ sheet_element["sheetId"] = s.sheetId
+ sheet_element["r:id"] = s.relationship_id
+ push!(sheets_element, sheet_element)
+ end
+ doc[i][j] = sheets_element
+
+ return nothing
+end
+
function add_cell_to_worksheet_dimension!(ws::Worksheet, cell::Cell)
# update worksheet dimension
ws_dimension = get_dimension(ws)
- if ws_dimension === nothing
- set_dimension!(ws, CellRange(cell.ref, cell.ref))
- return
- end
-
top = row_number(ws_dimension.start)
left = column_number(ws_dimension.start)
@@ -368,8 +431,8 @@ function add_cell_to_worksheet_dimension!(ws::Worksheet, cell::Cell)
end
function setdata!(ws::Worksheet, cell::Cell)
- @assert is_writable(get_xlsxfile(ws)) "XLSXFile instance is not writable."
- @assert ws.cache !== nothing "Can't write data to a Worksheet with empty cache."
+ !is_writable(get_xlsxfile(ws)) && throw(XLSXError("XLSXFile instance is not writable."))
+ ws.cache === nothing && throw(XLSXError("Can't write data to a Worksheet with empty cache."))
cache = ws.cache
r = row_number(cell)
@@ -377,7 +440,7 @@ function setdata!(ws::Worksheet, cell::Cell)
if !haskey(cache.cells, r)
push!(cache.rows_in_cache, r)
- cache.cells[r] = Dict{Int, Cell}()
+ cache.cells[r] = Dict{Int,Cell}()
cache.row_ht[r] = nothing
cache.dirty = true
end
@@ -412,33 +475,47 @@ function strip_illegal_chars(x::String)
return result
end
-const ESCAPE_CHARS = ('&' => "&", '<' => "<", '>' => ">", "'" => "'", '"' => """)
-function xlsx_escape(x::String)# Adaped from XML.escape()
- result = replace(x, r"&(?!amp;|quot;|apos;|gt;|lt;)" => "&") # This is a change from the XML.escape function, which uses r"&(?=\s)"
- for (pat, r) in ESCAPE_CHARS[2:end]
- result = replace(result, pat => r)
- end
- return result
-end
-
# Returns the datatype and value for `val` to be inserted into `ws`.
function xlsx_encode(ws::Worksheet, val::AbstractString)
if isempty(val)
return ("", "")
end
- sst_ind = add_shared_string!(get_workbook(ws), strip_illegal_chars(xlsx_escape(val)))
+ sst_ind = add_shared_string!(get_workbook(ws), strip_illegal_chars(val))
return ("s", string(sst_ind))
end
xlsx_encode(::Worksheet, val::Missing) = ("", "")
xlsx_encode(::Worksheet, val::Bool) = ("b", val ? "1" : "0")
-xlsx_encode(::Worksheet, val::Union{Int, Float64}) = ("", string(val))
+xlsx_encode(::Worksheet, val::Union{Int,Float64}) = ("", string(val))
xlsx_encode(ws::Worksheet, val::Dates.Date) = ("", string(date_to_excel_value(val, isdate1904(get_xlsxfile(ws)))))
xlsx_encode(ws::Worksheet, val::Dates.DateTime) = ("", string(datetime_to_excel_value(val, isdate1904(get_xlsxfile(ws)))))
xlsx_encode(::Worksheet, val::Dates.Time) = ("", string(time_to_excel_value(val)))
+Base.setindex!(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}) = setdata!(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))), v)
+Base.setindex!(ws::Worksheet, v::AbstractVector, r::Union{Integer,UnitRange{<:Integer}}, c::UnitRange{T}) where {T<:Integer} = setdata!(ws, r, c, v)
+Base.setindex!(ws::Worksheet, v::AbstractVector, r::UnitRange{T}, c::Union{Integer,UnitRange{<:Integer}}) where {T<:Integer} = setdata!(ws, r, c, v)
+Base.setindex!(ws::Worksheet, v::AbstractVector, ref; dim::Integer=2) = setdata!(ws, ref, v, dim)
+Base.setindex!(ws::Worksheet, v::AbstractVector, r, c; dim::Integer=2) = setdata!(ws, r, c, v, dim)
+Base.setindex!(ws::Worksheet, v, ref) = setdata!(ws, ref, v)
+Base.setindex!(ws::Worksheet, v, r, c) = setdata!(ws, r, c, v)
+function Base.setindex!(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}})
+ for a in row, b in col
+ setdata!(ws, CellRef(a, b), v)
+ end
+end
+function Base.setindex!(ws::Worksheet, v, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}})
+ for a in row, b in col
+ setdata!(ws, CellRef(a, b), v)
+ end
+end
+function Base.setindex!(ws::Worksheet, v, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}})
+ for a in row, b in col
+ setdata!(ws, CellRef(a, b), v)
+ end
+end
+
function setdata!(ws::Worksheet, ref::CellRef, val::CellValue)
t, v = xlsx_encode(ws, val.value)
cell = Cell(ref, t, id(val.styleid), v, Formula(""))
@@ -455,11 +532,7 @@ setdata!(ws::Worksheet, ref::CellRef, ::Nothing) = setdata!(ws, ref, CellValue(w
setdata!(ws::Worksheet, row::Integer, col::Integer, val::CellValue) = setdata!(ws, CellRef(row, col), val)
-Base.setindex!(ws::Worksheet, v, ref) = setdata!(ws, ref, v)
-Base.setindex!(ws::Worksheet, v, r, c) = setdata!(ws, r, c, v)
-
-Base.setindex!(ws::Worksheet, v::AbstractVector, ref; dim::Integer=2) = setdata!(ws, ref, v, dim)
-Base.setindex!(ws::Worksheet, v::AbstractVector, r, c; dim::Integer=2) = setdata!(ws, r, c, v, dim)
+setdata!(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}, v) = setdata!(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))), v)
function setdata!(ws::Worksheet, ref::CellRef, val::CellValueType) # use existing cell format if it exists
@@ -471,49 +544,152 @@ function setdata!(ws::Worksheet, ref::CellRef, val::CellValueType) # use existin
isa_dt = styles_is_datetime(ws.package.workbook, existing_style)
if val isa Dates.Date
if isa_dt == false
- c.style = string(update_template_xf(ws, existing_style, "numFmtId", DEFAULT_DATE_numFmtId).id)
+ c.style = string(update_template_xf(ws, existing_style, ["numFmtId", "applyNumberFormat"], [string(DEFAULT_DATE_numFmtId), "1"]).id)
end
elseif val isa Dates.Time
if isa_dt == false
- c.style = string(update_template_xf(ws, existing_style, "numFmtId", DEFAULT_TIME_numFmtId).id)
+ c.style = string(update_template_xf(ws, existing_style, ["numFmtId", "applyNumberFormat"], [string(DEFAULT_TIME_numFmtId), "1"]).id)
end
elseif val isa Dates.DateTime
if isa_dt == false
- c.style = string(update_template_xf(ws, existing_style, "numFmtId", DEFAULT_DATETIME_numFmtId).id)
+ c.style = string(update_template_xf(ws, existing_style, ["numFmtId", "applyNumberFormat"], [string(DEFAULT_DATETIME_numFmtId), "1"]).id)
end
elseif val isa Float64 || val isa Int
if styles_is_float(ws.package.workbook, existing_style) == false && Int(existing_style.id) ∉ [0, 1]
- c.style = string(update_template_xf(ws, existing_style, "numFmtId", DEFAULT_NUMBER_numFmtId).id)
+ c.style = string(update_template_xf(ws, existing_style, ["numFmtId"], [string(DEFAULT_NUMBER_numFmtId)]).id)
end
elseif val isa Bool # Now rerouted here rather than assigning an EmptyCellDataFormat.
- # Change any style to General (0) and retiain other formatting.
- c.style = string(update_template_xf(ws, existing_style, "numFmtId", DEFAULT_BOOL_numFmtId).id)
+ # Change any style to General (0) and retain other formatting.
+ c.style = string(update_template_xf(ws, existing_style, ["numFmtId"], [string(DEFAULT_BOOL_numFmtId)]).id)
end
return setdata!(ws, ref, CellValue(val, CellDataFormat(parse(Int, c.style))))
end
end
-# setdata!(ws::Worksheet, ref::CellRef, val::CellValueType) = setdata!(ws, ref, CellValue(ws, val))
-setdata!(ws::Worksheet, ref_str::AbstractString, value) = setdata!(ws, CellRef(ref_str), value)
+function setdata!(ws::Worksheet, ref::AbstractString, value)
+ if is_worksheet_defined_name(ws, ref)
+ v = get_defined_name_value(ws, ref)
+ if is_defined_name_value_a_reference(v)
+ return setdata!(ws, v, value)
+ else
+ throw(XLSXError("`$ref` is not a valid cell or range reference."))
+ end
+ elseif is_workbook_defined_name(get_workbook(ws), ref)
+ wb = get_workbook(ws)
+ v = get_defined_name_value(wb, ref)
+ if is_defined_name_value_a_reference(v)
+ return setdata!(ws, v, value)
+ else
+ throw(XLSXError("`$ref` is not a valid cell or range reference."))
+ end
+ elseif is_valid_cellname(ref)
+ return setdata!(ws, CellRef(ref), value)
+ elseif is_valid_sheet_cellname(ref)
+ return setdata!(ws, SheetCellRef(ref), value)
+ elseif is_valid_cellrange(ref)
+ return setdata!(ws, CellRange(ref), value)
+ elseif is_valid_column_range(ref)
+ return setdata!(ws, ColumnRange(ref), value)
+ elseif is_valid_row_range(ref)
+ return setdata!(ws, RowRange(ref), value)
+ elseif is_valid_non_contiguous_range(ref)
+ return setdata!(ws, NonContiguousRange(ws, ref), value)
+ elseif is_valid_sheet_cellrange(ref)
+ return setdata!(ws, SheetCellRange(ref), value)
+ elseif is_valid_sheet_column_range(ref)
+ return setdata!(ws, SheetColumnRange(ref), value)
+ elseif is_valid_sheet_row_range(ref)
+ return setdata!(ws, SheetRowRange(ref), value)
+ elseif is_valid_non_contiguous_cellrange(ref)
+ return setdata!(ws, NonContiguousRange(ws, ref), value)
+ elseif is_valid_non_contiguous_sheetcellrange(ref)
+ nc = NonContiguousRange(ref)
+ return do_sheet_names_match(ws, nc) && setdata!(ws, nc, value)
+ end
+ throw(XLSXError("`$ref` is not a valid cell or range reference."))
+end
+function setdata!(ws::Worksheet, rng::CellRange, value)
+ for row in rng.start.row_number:rng.stop.row_number
+ for col in rng.start.column_number:rng.stop.column_number
+ setdata!(ws, row, col, value)
+ end
+ end
+end
+function setdata!(ws::Worksheet, rng::RowRange, value)
+ dim = get_dimension(ws)
+ start = CellRef(rng.start, dim.start.column_number,)
+ stop = CellRef(rng.stop, dim.stop.column_number)
+ setdata!(ws, CellRange(start, stop), value)
+end
+function setdata!(ws::Worksheet, rng::ColumnRange, value)
+ dim = get_dimension(ws)
+ start = CellRef(dim.start.row_number, rng.start)
+ stop = CellRef(dim.stop.row_number, rng.stop)
+ setdata!(ws, CellRange(start, stop), value)
+end
+function setdata!(ws::Worksheet, rng::NonContiguousRange, value)
+ for r in rng.rng
+ if r isa CellRef
+ setdata!(ws, r, value)
+ else
+ for cell in r
+ setdata!(ws, cell, value)
+ end
+ end
+ end
+end
+setdata!(ws::Worksheet, ::Colon, ::Colon, v) = setdata!(ws::Worksheet, :, v)
+function setdata!(ws::Worksheet, ::Colon, v)
+ dim = get_dimension(ws)
+ setdata!(ws, dim, v)
+end
+function setdata!(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon, v)
+ dim = get_dimension(ws)
+ setdata!(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)), v)
+end
+function setdata!(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}, v)
+ dim = get_dimension(ws)
+ setdata!(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))), v)
+end
+function setdata!(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon, v)
+ dim = get_dimension(ws)
+ for a in row
+ for b in dim.start.column_number:dim.stop.column_number
+ setdata!(ws, CellRef(a, b), v)
+ end
+ end
+end
+function setdata!(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}, v)
+ dim = get_dimension(ws)
+ for b in col
+ for a in dim.start.row_number:dim.stop.row_number
+ setdata!(ws, CellRef(a, b), v)
+ end
+ end
+end
+setdata!(ws::Worksheet, ref::SheetCellRef, value) = setdata!(ws, ref.cellref, value)
+setdata!(ws::Worksheet, rng::SheetCellRange, value) = setdata!(ws, rng.rng, value)
+setdata!(ws::Worksheet, rng::SheetColumnRange, value) = setdata!(ws, rng.colrng, value)
+setdata!(ws::Worksheet, rng::SheetRowRange, value) = setdata!(ws, rng.rowrng, value)
setdata!(ws::Worksheet, ref_str::AbstractString, value::Vector, dim::Integer) = setdata!(ws, CellRef(ref_str), value, dim)
setdata!(ws::Worksheet, row::Integer, col::Integer, data) = setdata!(ws, CellRef(row, col), data)
-setdata!(ws::Worksheet, ref::CellRef, value) = error("Unsupported datatype $(typeof(value)) for writing data to Excel file. Supported data types are $(CellValueType) or $(CellValue).")
+setdata!(ws::Worksheet, ref::CellRef, value) = throw(XLSXError("Unsupported datatype $(typeof(value)) for writing data to Excel file. Supported data types are $(CellValueType) or $(CellValue)."))
setdata!(ws::Worksheet, row::Integer, col::Integer, data::AbstractVector, dim::Integer) = setdata!(ws, CellRef(row, col), data, dim)
function setdata!(sheet::Worksheet, ref::CellRef, data::AbstractVector, dim::Integer)
for (i, val) in enumerate(data)
- target_cell_ref = target_cell_ref_from_offset(ref, i-1, dim)
+ target_cell_ref = target_cell_ref_from_offset(ref, i - 1, dim)
setdata!(sheet, target_cell_ref, val)
end
end
-Base.setindex!(ws::Worksheet, v::AbstractVector, r::Union{Colon, UnitRange{T}}, c) where {T<:Integer} = setdata!(ws, r, c, v)
-Base.setindex!(ws::Worksheet, v::AbstractVector, r, c::Union{Colon, UnitRange{T}}) where {T<:Integer} = setdata!(ws, r, c, v)
+Base.setindex!(ws::Worksheet, v::AbstractVector, r::Union{Colon,UnitRange{T}}, c) where {T<:Integer} = setdata!(ws, r, c, v)
+Base.setindex!(ws::Worksheet, v::AbstractVector, r, c::Union{Colon,UnitRange{T}}) where {T<:Integer} = setdata!(ws, r, c, v)
setdata!(sheet::Worksheet, ::Colon, col::Integer, data::AbstractVector) = setdata!(sheet, 1, col, data, 1)
setdata!(sheet::Worksheet, row::Integer, ::Colon, data::AbstractVector) = setdata!(sheet, row, 1, data, 2)
function setdata!(sheet::Worksheet, row::Integer, cols::UnitRange{T}, data::AbstractVector) where {T<:Integer}
- @assert length(data) == length(cols) "Column count mismatch between `data` ($(length(data)) columns) and column range $cols ($(length(cols)) columns)."
+ length(data) != length(cols) && throw(XLSXError("Column count mismatch between `data` ($(length(data)) columns) and column range $cols ($(length(cols)) columns)."))
anchor_cell_ref = CellRef(row, first(cols))
# since cols is the unit range, this is a column-based operation
@@ -521,24 +697,24 @@ function setdata!(sheet::Worksheet, row::Integer, cols::UnitRange{T}, data::Abst
end
function setdata!(sheet::Worksheet, rows::UnitRange{T}, col::Integer, data::AbstractVector) where {T<:Integer}
- @assert length(data) == length(rows) "Row count mismatch between `data` ($(length(data)) rows) and row range $rows ($(length(rows)) rows)."
+ length(data) != length(rows) && throw(XLSXError("Row count mismatch between `data` ($(length(data)) rows) and row range $rows ($(length(rows)) rows)."))
anchor_cell_ref = CellRef(first(rows), col)
# since rows is the unit range, this is a row-based operation
setdata!(sheet, anchor_cell_ref, data, 1)
end
-function setdata!(sheet::Worksheet, ref_or_rng::AbstractString, matrix::Array{T, 2}) where {T}
+function setdata!(sheet::Worksheet, ref_or_rng::AbstractString, matrix::Array{T,2}) where {T}
if is_valid_cellrange(ref_or_rng)
setdata!(sheet, CellRange(ref_or_rng), matrix)
elseif is_valid_cellname(ref_or_rng)
setdata!(sheet, CellRef(ref_or_rng), matrix)
else
- error("Invalid cell reference or range: $ref_or_rng")
+ throw(XLSXError("Invalid cell reference or range: $ref_or_rng"))
end
end
-function setdata!(sheet::Worksheet, ref::CellRef, matrix::Array{T, 2}) where {T}
+function setdata!(sheet::Worksheet, ref::CellRef, matrix::Array{T,2}) where {T}
rows, cols = size(matrix)
anchor_row = row_number(ref)
anchor_col = column_number(ref)
@@ -548,8 +724,8 @@ function setdata!(sheet::Worksheet, ref::CellRef, matrix::Array{T, 2}) where {T}
end
end
-function setdata!(sheet::Worksheet, rng::CellRange, matrix::Array{T, 2}) where {T}
- @assert size(rng) == size(matrix) "Target range $rng size ($(size(rng))) must be equal to the input matrix size ($(size(matrix))) "
+function setdata!(sheet::Worksheet, rng::CellRange, matrix::Array{T,2}) where {T}
+ size(rng) != size(matrix) && throw(XLSXError("Target range $rng size ($(size(rng))) must be equal to the input matrix size ($(size(matrix)))"))
setdata!(sheet, rng.start, matrix)
end
@@ -557,20 +733,34 @@ end
# Returns a CellRef at:
# - (anchor_row + offset, anchol_col) if dim = 1 (operates on rows)
# - (anchor_row, anchor_col + offset) if dim = 2 (operates on cols)
-function target_cell_ref_from_offset(anchor_row::Integer, anchor_col::Integer, offset::Integer, dim::Integer) :: CellRef
+function target_cell_ref_from_offset(anchor_row::Integer, anchor_col::Integer, offset::Integer, dim::Integer)::CellRef
if dim == 1
return CellRef(anchor_row + offset, anchor_col)
elseif dim == 2
return CellRef(anchor_row, anchor_col + offset)
else
- error("Invalid dimension: $dim.")
+ throw(XLSXError("Invalid dimension: $dim."))
end
end
-function target_cell_ref_from_offset(anchor_cell::CellRef, offset::Integer, dim::Integer) :: CellRef
+function target_cell_ref_from_offset(anchor_cell::CellRef, offset::Integer, dim::Integer)::CellRef
return target_cell_ref_from_offset(row_number(anchor_cell), column_number(anchor_cell), offset, dim)
end
+const ALLOWED_TYPES = Union{Number,String,Bool,Dates.Date,Dates.Time,Dates.DateTime,Missing,Nothing}
+function process_vector(col) # Convert any disallowed types to strings. #239.
+ if eltype(col) <: ALLOWED_TYPES
+ # Case 1: All elements are of allowed types
+ return col
+ elseif eltype(col) <: Any && all(x -> !(typeof(x) <: ALLOWED_TYPES), col)
+ # Case 2: All elements are of disallowed types
+ return map(x -> string(x), col)
+ else
+ # Case 3: Mixed types, process each element
+ return [typeof(x) <: ALLOWED_TYPES ? x : string(x) for x in col]
+ end
+end
+
"""
writetable!(
sheet::Worksheet,
@@ -586,47 +776,56 @@ starting at `anchor_cell`.
`data` must be a vector of columns.
`columnnames` must be a vector of column labels.
+Column labels that are not of type `String` will be converted
+to strings before writing. Any data columns that are not of
+type `String`, `Float64`, `Int64`, `Bool`, `Date`, `Time`,
+`DateTime`, `Missing`, or `Nothing` will be converted to strings
+before writing.
+
+
See also: [`XLSX.writetable`](@ref).
"""
function writetable!(
- sheet::Worksheet,
- data,
- columnnames;
- anchor_cell::CellRef=CellRef("A1"),
- write_columnnames::Bool=true,
- )
+ sheet::Worksheet,
+ data,
+ columnnames;
+ anchor_cell::CellRef=CellRef("A1"),
+ write_columnnames::Bool=true,
+)
# read dimensions
col_count = length(data)
- @assert col_count == length(columnnames) "Column count mismatch between `data` ($col_count columns) and `columnnames` ($(length(columnnames)) columns)."
- @assert col_count > 0 "Can't write table with no columns."
- @assert col_count <= EXCEL_MAX_COLS "`data` contains $col_count columns, but Excel only supports up to $EXCEL_MAX_COLS; must reduce `data` size"
+ col_count != length(columnnames) && throw(XLSXError("Column count mismatch between `data` ($col_count columns) and `columnnames` ($(length(columnnames)) columns)."))
+ col_count <= 0 && throw(XLSXError("Can't write table with no columns."))
+ col_count > EXCEL_MAX_COLS && throw(XLSXError("`data` contains $col_count columns, but Excel only supports up to $EXCEL_MAX_COLS; must reduce `data` size"))
row_count = length(data[1])
- @assert row_count <= EXCEL_MAX_ROWS-1 "`data` contains $row_count rows, but Excel only supports up to $(EXCEL_MAX_ROWS-1); must reduce `data` size"
+ row_count > EXCEL_MAX_ROWS - 1 && throw(XLSXError("`data` contains $row_count rows, but Excel only supports up to $(EXCEL_MAX_ROWS-1); must reduce `data` size"))
if col_count > 1
for c in 2:col_count
- @assert length(data[c]) == row_count "Row count mismatch between column 1 ($row_count rows) and column $c ($(length(data[c])) rows)."
+ length(data[c]) != row_count && throw(XLSXError("Row count mismatch between column 1 ($row_count rows) and column $c ($(length(data[c])) rows)."))
end
end
anchor_row = row_number(anchor_cell)
anchor_col = column_number(anchor_cell)
start_from_anchor = 1
+
# write table header
if write_columnnames
for c in 1:col_count
target_cell_ref = CellRef(anchor_row, c + anchor_col - 1)
- sheet[target_cell_ref] = strip_illegal_chars(xlsx_escape(string(columnnames[c])))
+ sheet[target_cell_ref] = strip_illegal_chars(XML.escape(string(columnnames[c])))
end
start_from_anchor = 0
end
# write table data
+ data = [process_vector(col) for col in data] # Address issue #239
for c in 1:col_count
for r in 1:row_count
target_cell_ref = CellRef(r + anchor_row - start_from_anchor, c + anchor_col - 1)
v = data[c][r]
- sheet[target_cell_ref] = v isa String ? strip_illegal_chars(xlsx_escape(v)) : v
+ sheet[target_cell_ref] = v isa String ? strip_illegal_chars(XML.escape(v)) : v
end
end
end
@@ -645,8 +844,8 @@ function rename!(ws::Worksheet, name::AbstractString)
end
xf = get_xlsxfile(ws)
- @assert is_writable(xf) "XLSXFile instance is not writable."
- @assert name ∉ sheetnames(xf) "Sheetname $name is already in use."
+ !is_writable(xf) && throw(XLSXError("XLSXFile instance is not writable."))
+ name ∈ sheetnames(xf) && throw(XLSXError("Sheetname $name is already in use."))
# updates XML
xroot = xmlroot(xf, "xl/workbook.xml")[end]
@@ -670,67 +869,188 @@ function rename!(ws::Worksheet, name::AbstractString)
nothing
end
-addsheet!(xl::XLSXFile, name::AbstractString="") :: Worksheet = addsheet!(get_workbook(xl), name)
-
"""
- addsheet!(workbook, [name]) :: Worksheet
+ addsheet!(wb::Workbook, [name::AbstractString=""]) --> ::Worksheet
+ addsheet!(xf::XLSXFile, [name::AbstractString=""]) --> ::Worksheet
Create a new worksheet named `name`.
If `name` is not provided, a unique name is created.
-"""
-function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path::String = _relocatable_data_path()) :: Worksheet
- xf = get_xlsxfile(wb)
- @assert is_writable(xf) "XLSXFile instance is not writable."
+See also [copysheet!](@ref), [deletesheet!](@ref)
+"""
+addsheet!(xl::XLSXFile, name::AbstractString="")::Worksheet = addsheet!(get_workbook(xl), name) ::Worksheet
+function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path::String=_relocatable_data_path())::Worksheet
file_sheet_template = joinpath(relocatable_data_path, "sheet_template.xml")
- @assert isfile(file_sheet_template) "Couldn't find template file $file_sheet_template."
+ !isfile(file_sheet_template) && throw(XLSXError("Couldn't find template file $file_sheet_template."))
+ xdoc = XML.read(file_sheet_template, XML.Node)
+ new_ws = insertsheet!(wb, xdoc, name)
+ new_ws.cache = XLSX.WorksheetCache(
+ false,
+ Dict{Int64,Dict{Int64,XLSX.Cell}}(),
+ Int64[],
+ Dict{Int,Union{Float64,Nothing}}(),
+ Dict{Int64,Int64}(),
+ SheetRowStreamIterator(new_ws),
+ nothing,
+ false
+ )
+ return new_ws
+end
+
+"""
+ copysheet!(ws::Worksheet, [name::AbstractString=""]) --> ::Worksheet
+
+Create a copy of the worksheet `ws` and add it to the end of the workbook with the
+specified worksheet name.
+Return the new worksheet object.
+If `name` is not provided, a new name is generated by appending " (copy)" to the original
+worksheet name, with a further numerical suffix to guarantee uniqueness if necessary.
+To copy worksheets, the `XLSXFile` must be writable (opened with `mode="rw"` or as a template).
+See also [`XLSX.openxlsx`](@ref) and [XLSX.opentemplate](@ref).
+
+!!! warning "Experimental"
+ This function is experimental is not guaranteed to work with all XLSX files,
+ especially those with complex features. However, cell formats, conditional formats
+ and worksheet defined names should all copy OK.
+
+See also [addsheet!](@ref), [deletesheet!](@ref)
+
+# Examples
+```julia
+julia> f=XLSX.openxlsx("general.xlsx", mode="rw")
+XLSXFile("C:\\...\\general.xlsx") containing 13 Worksheets
+ sheetname size range
+-------------------------------------------------
+ general 10x6 A1:F10
+ table3 5x6 A2:F6
+ table4 4x3 E12:G15
+ table 12x8 A2:H13
+ table2 5x3 A1:C5
+ empty 1x1 A1:A1
+ table5 6x1 C3:C8
+ table6 8x2 B1:C8
+ table7 7x2 B2:C8
+ lookup 4x9 B2:J5
+ header_error 3x4 B2:E4
+ named_ranges_2 4x5 A1:E4
+ named_ranges 14x6 A2:F15
+
+julia> XLSX.copysheet!(f[4])
+12×8 XLSX.Worksheet: ["table (copy)"](A2:H13)
+
+julia> f
+XLSXFile("C:\\...\\general.xlsx") containing 14 Worksheets
+ sheetname size range
+-------------------------------------------------
+ general 10x6 A1:F10
+ table3 5x6 A2:F6
+ table4 4x3 E12:G15
+ table 12x8 A2:H13
+ table2 5x3 A1:C5
+ empty 1x1 A1:A1
+ table5 6x1 C3:C8
+ table6 8x2 B1:C8
+ table7 7x2 B2:C8
+ lookup 4x9 B2:J5
+ header_error 3x4 B2:E4
+ named_ranges_2 4x5 A1:E4
+ named_ranges 14x6 A2:F15
+ table (copy) 12x8 A2:H13
+
+```
+
+"""
+function copysheet!(ws::Worksheet, name::AbstractString="")::Worksheet
+ wb = get_workbook(ws)
+ xl=get_xlsxfile(ws)
+ !is_writable(get_xlsxfile(ws)) && throw(XLSXError("XLSXFile instance is not writable."))
+ dim=get_dimension(ws)
+
+ # make sure cache and XML are consistent
+ update_worksheets_xml!(xl)
+
+ # create a copy of the XML document
+ xdoc = copynode(get_worksheet_xml_document(ws))
+
+ # generate a new name for the copied sheet
+ name = name=="" ? ws.name * " (copy)" : name
+
+ # insert the copied sheet into the workbook
+ new_ws = insertsheet!(wb, xdoc, name, dim=dim)
+
+ # copy the original worksheet cache to the new worksheet
+ new_ws.cache = XLSX.WorksheetCache(
+ false,
+ ws.cache.cells,
+ ws.cache.rows_in_cache,
+ ws.cache.row_ht,
+ ws.cache.row_index,
+ SheetRowStreamIterator(new_ws),
+ nothing,
+ ws.cache.dirty,
+ )
+
+ # copy defined names from the original worksheet to the new worksheet
+ ws_keys=[x for x in keys(wb.worksheet_names) if first(x) == ws.sheetId]
+ for k in ws_keys
+ val=wb.worksheet_names[k].value
+ val = val isa CellRange ? string(val) :
+ val isa SheetCellRange ? new_ws.name*"!"*string(val.rng) :
+ val
+ addDefinedName(new_ws, last(k), val; absolute=wb.worksheet_names[k].isabs)
+ end
+
+ return new_ws
+end
+
+function insertsheet!(wb::Workbook, xdoc::XML.Node, name::AbstractString=""; dim=CellRange("A1:A1"))::Worksheet
+ xf = get_xlsxfile(wb)
+ !is_writable(xf) && throw(XLSXError("XLSXFile instance is not writable."))
if name == ""
- # name was not provided. Will find a unique name.
- i = 1
- current_sheet_names = sheetnames(wb)
- while true
- name = "Sheet$i"
- if !in(name, current_sheet_names)
- # found a unique name
- break
- end
- i += 1
- end
- else
+ new_name=""
+ else
+ new_name = name
+ end
+ # ensure name is unique.
+ i = 1
+ current_sheet_names = sheetnames(wb)
+ while new_name ∈ current_sheet_names || new_name == ""
+ new_name = (name=="" ? "Sheet" : name * " ") * string(i)
+ i += 1
end
- @assert name != ""
+ new_name == "" && throw(XLSXError("Something wrong here!"))
# checks if name is a unique sheet name
- @assert name ∉ sheetnames(wb) "A sheet named `$name` already exists in this workbook."
-
function check_valid_sheetname(n::AbstractString)
max_length = 31
- @assert(length(n) <= max_length,
- "Invalid sheetname $n: must have at most $max_length characters. Found $(length(n))"
- )
+ if length(n) > max_length
+ throw(XLSXError("Invalid sheetname $n: must have at most $max_length characters. Found $(length(n))"))
+ end
- @assert(!occursin(r"[:\\/\?\*\[\]]+", n),
- "Sheetname cannot contain characters: ':', '\\', '/', '?', '*', '[', ']'."
- )
+ if occursin(r"[:\\/\?\*\[\]]+", n)
+ throw(XLSXError("Sheetname cannot contain characters: ':', '\\', '/', '?', '*', '[', ']'."))
+ end
end
- check_valid_sheetname(name)
+ check_valid_sheetname(new_name)
# generate sheetId
- current_sheet_ids = [ ws.sheetId for ws in wb.sheets ]
+ current_sheet_ids = [ws.sheetId for ws in wb.sheets]
sheetId = max(current_sheet_ids...) + 1
- xdoc = XML.read(file_sheet_template, XML.Node)
+ # generate a unique ID for the new sheet
+ xdoc[2]["xr:uid"] = "{" * string(UUIDs.uuid4()) * "}"
# generate a unique name for the XML
local xml_filename::String
i = 1
while true
- xml_filename = "xl/worksheets/sheet$i.xml"
- if !in(xml_filename, keys(xf.files))
+ xml_filename = "xl/worksheets/sheet" * string(i) * ".xml"
+# if !in(xml_filename, keys(xf.files))
+ if !haskey(xf.files, xml_filename)
break
end
i += 1
@@ -739,55 +1059,199 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path:
# adds doc do XLSXFile
xf.files[xml_filename] = true # is read
xf.data[xml_filename] = xdoc
-
# adds workbook-level relationship
#
rId = add_relationship!(wb, xml_filename[4:end], "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet")
# creates Worksheet instance
- ws = Worksheet(xf, sheetId, rId, name, CellRange("A1:A1"), false)
- # creates a mock WorksheetCache
- # because we can't write to sheet with empty cache (see setdata!(ws::Worksheet, cell::Cell))
- # and the stream should be closed
- # to indicate that no more rows will be fetched from SheetRowStreamIterator in Base.iterate(ws_cache::WorksheetCache, row_from_last_iteration::Int)
- reader = open_internal_file_stream(xf, "xl/worksheets/sheet1.xml") # could be any file
- state = SheetRowStreamIteratorState(reader, nothing, 0, nothing)
- ws.cache = XLSX.WorksheetCache(
- Dict{Int64, Dict{Int64, XLSX.Cell}}(),
- Int64[],
- Dict{Int, Union{Float64, Nothing}}(),
- Dict{Int64, Int64}(),
- SheetRowStreamIterator(ws),
- state,
- false
- )
+ ws = Worksheet(xf, sheetId, rId, new_name, dim, false)
# adds the new sheet to the list of sheets in the workbook
push!(wb.sheets, ws)
# update [Content_Types].xml (fix for issue #275)
ctype_root = xmlroot(get_xlsxfile(wb), "[Content_Types].xml")[end]
- @assert XML.tag(ctype_root) == "Types"
+ XML.tag(ctype_root) != "Types" && throw(XLSXError("Something wrong here!"))
override_node = XML.Element("Override";
- ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
- PartName = "/xl/worksheets/sheet$sheetId.xml"
+ PartName="/xl/worksheets/sheet" * string(sheetId) * ".xml",
+ ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
)
push!(ctype_root, override_node)
- # updates workbook xml
- xroot = xmlroot(xf, "xl/workbook.xml")[end]
- for node in XML.children(xroot)
- if XML.tag(node) == "sheets"
- sheet_element = XML.Element("sheet"; name = name)
- sheet_element["r:id"] = rId
- sheet_element["sheetId"] = string(sheetId)
- push!(node, sheet_element)
- break
+ update_workbook_xml!(xf)
+
+ return ws
+end
+
+"""
+ deletesheet!(ws::Worksheet) -> ::XLSXFile
+ deletesheet!(wb::Workbook, name::AbstractString) -> ::XLSXFile
+ deletesheet!(xf::XLSXFile, name::AbstractString) -> ::XLSXFile
+ deletesheet!(xf::XLSXFile, sheetId::Integer) -> ::XLSXFile
+
+Delete the given worksheet, the worksheet with the given name or the worksheet with the given `sheetId` from its `XLSXFile`
+(`sheetId` is a 1-based integer representing the order in which worksheet tabs are displayed in Excel).
+
+See also [addsheet!](@ref), [copysheet!](@ref)
+
+# Examples
+
+```julia
+julia> f = XLSX.opentemplate("general.xlsx")
+XLSXFile("C:\\...\\general.xlsx") containing 13 Worksheets
+ sheetname size range
+-------------------------------------------------
+ general 10x6 A1:F10
+ table3 5x6 A2:F6
+ table4 4x3 E12:G15
+ table 12x8 A2:H13
+ table2 5x3 A1:C5
+ empty 1x1 A1:A1
+ table5 6x1 C3:C8
+ table6 8x2 B1:C8
+ table7 7x2 B2:C8
+ lookup 4x9 B2:J5
+ header_error 3x4 B2:E4
+ named_ranges_2 4x5 A1:E4
+ named_ranges 14x6 A2:F15
+
+
+julia> XLSX.deletesheet!(f[4])
+XLSXFile("C:\\...\\general.xlsx") containing 12 Worksheets
+ sheetname size range
+-------------------------------------------------
+ general 10x6 A1:F10
+ table3 5x6 A2:F6
+ table4 4x3 E12:G15
+ table2 5x3 A1:C5
+ empty 1x1 A1:A1
+ table5 6x1 C3:C8
+ table6 8x2 B1:C8
+ table7 7x2 B2:C8
+ lookup 4x9 B2:J5
+ header_error 3x4 B2:E4
+ named_ranges_2 4x5 A1:E4
+ named_ranges 14x6 A2:F15
+
+
+julia> XLSX.deletesheet!(f, "table5")
+XLSXFile("C:\\...\\general.xlsx") containing 11 Worksheets
+ sheetname size range
+-------------------------------------------------
+ general 10x6 A1:F10
+ table3 5x6 A2:F6
+ table4 4x3 E12:G15
+ table2 5x3 A1:C5
+ empty 1x1 A1:A1
+ table6 8x2 B1:C8
+ table7 7x2 B2:C8
+ lookup 4x9 B2:J5
+ header_error 3x4 B2:E4
+ named_ranges_2 4x5 A1:E4
+ named_ranges 14x6 A2:F15
+
+
+julia> XLSX.deletesheet!(f, 1)
+XLSXFile("C:\\...\\general.xlsx") containing 10 Worksheets
+ sheetname size range
+-------------------------------------------------
+ table3 5x6 A2:F6
+ table4 4x3 E12:G15
+ table2 5x3 A1:C5
+ empty 1x1 A1:A1
+ table6 8x2 B1:C8
+ table7 7x2 B2:C8
+ lookup 4x9 B2:J5
+ header_error 3x4 B2:E4
+ named_ranges_2 4x5 A1:E4
+ named_ranges 14x6 A2:F15
+```
+
+"""
+deletesheet!(ws::Worksheet) = deletesheet!(get_workbook(ws), ws.name)
+deletesheet!(xl::XLSXFile, sheetId::Integer) = deletesheet!(get_workbook(xl), xl[sheetId].name)
+deletesheet!(xl::XLSXFile, name::AbstractString) = deletesheet!(get_workbook(xl), name)
+function deletesheet!(wb::Workbook, name::AbstractString)::XLSXFile
+ hassheet(wb, name) || throw(XLSXError("Worksheet `$name` not found in workbook."))
+ sheetcount(wb) > 1 || throw(XLSXError("`$name` is this workbook's only sheet. Cannot delete the only sheet!"))
+
+ # Worksheets and relationships
+ s = (findfirst(s -> s.name == name, wb.sheets))
+ sId = wb.sheets[s].sheetId
+ rId = wb.sheets[s].relationship_id
+ r = findfirst(y -> occursin("worksheet", y.Type) && y.Id == rId, wb.relationships)
+ deleteat!(wb.sheets, s)
+
+ # Defined Names
+ found_wbnames = Vector{String}()
+ for (k, v) in wb.workbook_names
+ wbn = v.value
+ if typeof(wbn) <: DefinedNameRangeTypes
+ if wbn.sheet == name
+ push!(found_wbnames, k)
+ end
end
end
+ found_wsnames = Vector{Tuple{Int64,String}}()
+ for (k, v) in wb.worksheet_names
+ wbn = v.value
+ if first(k) == sId
+ push!(found_wsnames, k)
+ end
+ end
+ for key in found_wbnames
+ delete!(wb.workbook_names, key)
+ end
+ for key in found_wsnames
+ delete!(wb.worksheet_names, key)
+ end
+ renumber_keys = Vector{Pair{Tuple{Int64,String},Tuple{Int64,String}}}()
+ for (k, v) in wb.worksheet_names
+ wbn = v.value
+ first(k) == sId && throw(XLSXError("Something wrong here!"))
+ if first(k) > sId
+ push!(renumber_keys, k => (sId, last(k)))
+ end
+ end
+ for (oldkey, newkey) in renumber_keys
+ wb.worksheet_names[newkey] = wb.worksheet_names[oldkey]
+ delete!(wb.worksheet_names, oldkey)
+ end
- return ws
+ xf = get_xlsxfile(wb)
+
+ # Files
+ xml_filename = "xl/worksheets/sheet" * rId[4:end] * ".xml"
+ if in(xml_filename, keys(xf.files))
+ delete!(xf.files, xml_filename)
+ end
+ if in(xml_filename, keys(xf.data))
+ delete!(xf.data, xml_filename)
+ end
+ if in(xml_filename, keys(xf.binary_data))
+ delete!(xf.binary_data, xml_filename)
+ end
+
+ # update [Content_Types].xml
+ ctype_root = xmlroot(get_xlsxfile(wb), "[Content_Types].xml")[end]
+ XML.tag(ctype_root) != "Types" && throw(XLSXError("Something wrong here!"))
+ cont = XML.children(ctype_root)
+ let idx = 0
+ for (i, c) in enumerate(cont)
+ if haskey(c, "PartName") && c["PartName"] == "/xl/worksheets/sheet" * rId[4:end] * ".xml"
+ idx = i
+ break
+ end
+ end
+ if idx > 0
+ deleteat!(cont, idx)
+ end
+ end
+
+ update_workbook_xml!(xf)
+
+ return xf
end
#
@@ -802,6 +1266,8 @@ end
- `overwrite` is a `Bool` to control if `filename` should be overwritten if already exists.
- `sheetname` is the name for the worksheet.
+Returns the filepath of the written file if a filename is supplied, or `nothing` if writing to an `IO`.
+
# Example
```julia
@@ -813,10 +1279,10 @@ XLSX.writetable("table.xlsx", columns, colnames)
See also: [`XLSX.writetable!`](@ref).
"""
-function writetable(filename::Union{AbstractString, IO}, data, columnnames; overwrite::Bool=false, sheetname::AbstractString="", anchor_cell::Union{String, CellRef}=CellRef("A1"))
+function writetable(filename::Union{AbstractString,IO}, data, columnnames; overwrite::Bool=false, sheetname::AbstractString="", anchor_cell::Union{String,CellRef}=CellRef("A1"))
if filename isa AbstractString && !overwrite
- @assert !isfile(filename) "$filename already exists."
+ isfile(filename) && throw(XLSXError("$filename already exists."))
end
xf = open_empty_template(sheetname)
@@ -830,7 +1296,6 @@ function writetable(filename::Union{AbstractString, IO}, data, columnnames; over
# write output file
writexlsx(filename, xf, overwrite=overwrite)
- nothing
end
"""
@@ -842,6 +1307,8 @@ Write multiple tables.
`kw` is a variable keyword argument list. Each element should be in this format: `sheetname=( data, column_names )`,
where `data` is a vector of columns and `column_names` is a vector of column labels.
+Returns the filepath of the written file if a filename is supplied, or `nothing` if writing to an `IO`.
+
Example:
```julia
@@ -854,10 +1321,10 @@ julia> df2 = DataFrames.DataFrame(AA=["aa", "bb"], AB=[10.1, 10.2])
julia> XLSX.writetable("report.xlsx", "REPORT_A" => df1, "REPORT_B" => df2)
```
"""
-function writetable(filename::Union{AbstractString, IO}; overwrite::Bool=false, kw...)
+function writetable(filename::Union{AbstractString,IO}; overwrite::Bool=false, kw...)
if filename isa AbstractString && !overwrite
- @assert !isfile(filename) "$filename already exists."
+ isfile(filename) && throw(XLSXError("$filename already exists."))
end
xf = open_empty_template()
@@ -879,13 +1346,13 @@ function writetable(filename::Union{AbstractString, IO}; overwrite::Bool=false,
# write output file
writexlsx(filename, xf, overwrite=overwrite)
- nothing
+
end
-function writetable(filename::Union{AbstractString, IO}, tables::Vector{Tuple{String, S, Vector{T}}}; overwrite::Bool=false) where {S<:Vector{U} where U, T<:Union{String, Symbol}}
+function writetable(filename::Union{AbstractString,IO}, tables::Vector{Tuple{String,S,Vector{T}}}; overwrite::Bool=false) where {S<:Vector{U} where {U},T<:Union{String,Symbol}}
if filename isa AbstractString && !overwrite
- @assert !isfile(filename) "$filename already exists."
+ isfile(filename) && throw(XLSXError("$filename already exists."))
end
xf = open_empty_template()
diff --git a/test/runtests.jl b/test/runtests.jl
index 11b81d40..e55f03d8 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -2,7 +2,8 @@
import XLSX
import Tables
using Test, Dates, XML
-import DataFrames
+import DataFrames, Random
+import Distributions as Dist
const SPREADSHEET_NAMESPACE_XPATH_ARG = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
struct xpath
@@ -123,24 +124,60 @@ data_directory = joinpath(dirname(pathof(XLSX)), "..", "data")
@test XLSX.isdate1904(ef_Book1["Sheet1"]) == false
@testset "Read XLS file error" begin
- @test_throws ErrorException XLSX.readxlsx(joinpath(data_directory, "old.xls"))
+ @test_throws XLSX.XLSXError XLSX.readxlsx(joinpath(data_directory, "old.xls"))
try
XLSX.readxlsx(joinpath(data_directory, "old.xls"))
@test false # didn't throw exception
catch e
@test occursin("This package does not support XLS file format", "$e")
end
+
end
@testset "Read invalid XLSX error" begin
- @test_throws ErrorException XLSX.readxlsx(joinpath(data_directory, "sheet_template.xml"))
+ @test_throws XLSX.XLSXError XLSX.readxlsx(joinpath(data_directory, "sheet_template.xml"))
try
XLSX.readxlsx(joinpath(data_directory, "sheet_template.xml"))
@test false # didn't throw exception
catch e
@test occursin("is not a valid XLSX file", "$e")
end
+ @test_throws XLSX.XLSXError XLSX.readxlsx(joinpath(data_directory, "Template File.xltx"))
+ try
+ XLSX.readxlsx(joinpath(data_directory, "Template File.xltx"))
+ @test false # didn't throw exception
+ catch e
+ @test occursin("does not support Excel template files", "$e")
+ end
+ end
+
+ @testset "missing file or bad `mode`" begin
+ @test_throws XLSX.XLSXError XLSX.openxlsx("noSuchFile.xlsx")
+ @test_throws XLSX.XLSXError XLSX.openxlsx(joinpath(data_directory, "Book1.xlsx"); mode="tg")
end
+
+ @testset "write-only mode" begin
+ XLSX.openxlsx("mytest.xlsx", mode="w") do f
+ f[1]["A1"]=1
+ @test f.source == "mytest.xlsx"
+ end
+ ef = XLSX.readxlsx("mytest.xlsx")
+ @test ef["Sheet1"]["A1"] == 1
+ f=XLSX.openxlsx("mytest.xlsx", mode="w")
+ @test f.source == "mytest.xlsx"
+ f[1]["A1"]=1
+ XLSX.writexlsx("mytest.xlsx", f, overwrite=true)
+ ef = XLSX.readxlsx("mytest.xlsx")
+ @test ef["Sheet1"]["A1"] == 1
+ f=XLSX.newxlsx()
+ @test f.source == "blank.xlsx"
+ f[1]["A1"]=1
+ XLSX.writexlsx("mytest.xlsx", f, overwrite=true)
+ ef = XLSX.readxlsx("mytest.xlsx")
+ @test ef["Sheet1"]["A1"] == 1
+ isfile("mytest.xlsx") && rm("mytest.xlsx")
+ end
+
end
@testset "Cell names" begin
@@ -158,6 +195,12 @@ end
@test !XLSX.is_valid_column_name("AAAZ")
@test !XLSX.is_valid_column_name(":")
@test !XLSX.is_valid_column_name("É")
+ @test XLSX.is_valid_row_name("1")
+ @test XLSX.is_valid_row_name("12")
+ @test XLSX.is_valid_row_name("123")
+ @test !XLSX.is_valid_row_name("012")
+ @test !XLSX.is_valid_row_name(":")
+ @test !XLSX.is_valid_row_name("A")
@test XLSX.is_valid_sheet_cellname("Sheet1!A2")
@test !XLSX.is_valid_sheet_cellname("Sheet1!A2:B3")
@@ -177,13 +220,29 @@ end
@test !XLSX.is_valid_sheet_cellrange("Sheet1!")
@test !XLSX.is_valid_sheet_cellrange("Sheet1")
@test !XLSX.is_valid_sheet_cellrange("mysheet!A1")
- @test XLSX.is_valid_sheet_cellrange("mysheet!A1:A4")
@test XLSX.is_valid_sheet_column_range("Sheet1!A:B")
@test XLSX.is_valid_sheet_column_range("Sheet1!AB:BC")
@test !XLSX.is_valid_sheet_column_range("A:B")
@test !XLSX.is_valid_sheet_column_range("Sheet1!")
@test !XLSX.is_valid_sheet_column_range("Sheet1")
+ @test XLSX.is_valid_sheet_row_range("Sheet1!1:2")
+ @test XLSX.is_valid_sheet_row_range("Sheet1!12:23")
+ @test !XLSX.is_valid_sheet_row_range("1:2")
+ @test !XLSX.is_valid_sheet_row_range("Sheet1!")
+ @test !XLSX.is_valid_sheet_row_range("Sheet1")
+
+ @test XLSX.is_valid_non_contiguous_range("Sheet1!B1,Sheet1!B3")
+ @test XLSX.is_valid_non_contiguous_range("Sheet1!B1,Sheet1!GZ75:HB127")
+ @test XLSX.is_valid_non_contiguous_range("B2,B5")
+ @test XLSX.is_valid_non_contiguous_range("C3:C5,D6,G7:G8")
+ @test !XLSX.is_valid_non_contiguous_range("Sheet1!C3,Sheet2!C3")
+ @test !XLSX.is_valid_non_contiguous_range("Sheet1!B3")
+ @test !XLSX.is_valid_non_contiguous_range("Sheet1!B3:C6")
+
+ @test in(XLSX.SheetCellRef("Sheet1!A1"), XLSX.NonContiguousRange("Sheet1!A1,Sheet1!B2")) == true
+ @test in(XLSX.SheetCellRef("Sheet1!B2"), XLSX.NonContiguousRange("Sheet1!A1,Sheet1!B2")) == true
+ @test in(XLSX.SheetCellRef("Sheet1!A2"), XLSX.NonContiguousRange("Sheet1!A1,Sheet1!B2")) == false
cn = XLSX.CellRef("A1")
@test string(cn) == "A1"
@@ -215,7 +274,7 @@ end
@assert length(v_column_names) == length(v_column_numbers) "Test script is wrong."
- for i in 1:length(v_column_names)
+ for i in axes(v_column_names, 1)
@test XLSX.encode_column_number(v_column_numbers[i]) == v_column_names[i]
@test XLSX.decode_column_number(v_column_names[i]) == v_column_numbers[i]
end
@@ -276,8 +335,8 @@ end
@test XLSX.is_valid_cellrange("B2:C8")
@test !XLSX.is_valid_cellrange("A:B")
- @test_throws AssertionError XLSX.CellRange("Z10:A1")
- @test_throws AssertionError XLSX.CellRange("Z1:A1")
+ @test_throws XLSX.XLSXError XLSX.CellRange("Z10:A1")
+ @test_throws XLSX.XLSXError XLSX.CellRange("Z1:A1")
# hashing and equality
@test XLSX.CellRef("AMI1") == XLSX.CellRef("AMI1")
@@ -305,7 +364,7 @@ end
ref = XLSX.SheetCellRange("Sheet1!A1:B4")
@test ref.sheet == "Sheet1"
@test ref.rng == XLSX.CellRange("A1:B4")
- @test_throws AssertionError XLSX.SheetCellRange("Sheet1!B4:A1")
+ @test_throws XLSX.XLSXError XLSX.SheetCellRange("Sheet1!B4:A1")
@test XLSX.SheetCellRange("Sheet1!A1:B4") == XLSX.SheetCellRange("Sheet1!A1:B4")
@test hash(XLSX.SheetCellRange("Sheet1!A1:B4")) == hash(XLSX.SheetCellRange("Sheet1!A1:B4"))
show(IOBuffer(), ref)
@@ -355,7 +414,12 @@ end
@test XLSX.getcell(sheet1, "B2") == XLSX.Cell(XLSX.CellRef("B2"), "s", "", "0", "")
XLSX.getcellrange(sheet1, "B2:C3")
XLSX.getcellrange(f, "Sheet1!B2:C3")
- @test_throws ErrorException XLSX.getcellrange(f, "B2:C3")
+ XLSX.getcellrange(sheet1, 2, 2)
+ XLSX.getcellrange(sheet1, 2, :)
+ XLSX.getcellrange(sheet1, :, 3)
+ XLSX.getcellrange(sheet1, 3, :)
+ XLSX.getcellrange(sheet1, "B2:C3")
+ @test_throws XLSX.XLSXError XLSX.getcellrange(f, "B2:C3")
# a cell can be put in a dict
c = XLSX.getcell(sheet1, "B2")
@@ -372,6 +436,136 @@ end
@test sheet2_data == sheet2["A1:C3"]
@test sheet2_data == sheet2[:]
@test sheet2[:] == XLSX.getdata(sheet2)
+ @test sheet2[:] == XLSX.getdata(sheet2, :)
+ @test XLSX.getdata(sheet2, :, [1, 2]) == sheet2["A1:B3"]
+end
+
+@testset "setindex" begin
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:A3"] = "Hello world"
+ s[2, 1:3] = 42
+ s[[1, 3], 2:3] = true
+ @test s[1:3, [1, 2, 3]] == Any["Hello world" true true; 42 42 42; "Hello world" true true]
+ s[2, :] = 44
+ @test s[[1, 2, 3], 1:3] == Any["Hello world" true true; 44 44 44; "Hello world" true true]
+ @test s["Sheet1!A1:C3"] == Any["Hello world" true true; 44 44 44; "Hello world" true true]
+ @test s["Sheet1!A:C"] == Any["Hello world" true true; 44 44 44; "Hello world" true true]
+ @test s["Sheet1!1:3"] == Any["Hello world" true true; 44 44 44; "Hello world" true true]
+ s[:, :] = 0
+ @test s[:, :] == Any[0 0 0; 0 0 0; 0 0 0]
+ s[:] = 1
+ @test s[:, 1:3] == Any[1 1 1; 1 1 1; 1 1 1]
+ @test s[1:3, :] == Any[1 1 1; 1 1 1; 1 1 1]
+ @test s[1:2:3, :] == Any[1 1 1; 1 1 1]
+ @test s[1:2:3, 1] == Any[1, 1]
+ s["A1,B2,C3"] = "non-contiguous"
+ @test s["Sheet1!A1,Sheet1!B2,Sheet1!C3"] == Any["non-contiguous", "non-contiguous", "non-contiguous"]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s[[1, 2, 3], :] = "Hello world"
+ s[:, [1, 2, 3, 4]] = 42
+ s[:, 1:3] = true
+ @test s["Sheet1!1:3"] == Any[true true true 42; true true true 42; true true true 42]
+ s["Sheet1!A1"] = "Goodbye world"
+ @test s["Sheet1!A1"] == "Goodbye world"
+ s["Sheet1!A1:A3"] = "Goodbye cruel world"
+ @test s["Sheet1!A1:A3"] == ["Goodbye cruel world"; "Goodbye cruel world"; "Goodbye cruel world";;]
+ s["Sheet1!1:2"] = "Bright Lights"
+ @test s["A1,B2,C3"] == ["Bright Lights", "Bright Lights", true]
+ s["Sheet1!C:D"] = "Beat my Retreat"
+ @test s["B1,C2,D3"] == ["Bright Lights", "Beat my Retreat", "Beat my Retreat"]
+ s["Sheet1!B1,Sheet1!C2,Sheet1!D3"] = "Night Comes In"
+ @test s["Sheet1!B1,Sheet1!C2,Sheet1!D3"] == ["Night Comes In", "Night Comes In", "Night Comes In"]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s[[1, 2, 3], :] = "Hello world"
+ s[:, [1, 2, 3, 4]] = 42
+ s[:, 1:3] = true
+ @test f["Sheet1!1:3"] == Any[true true true 42; true true true 42; true true true 42]
+ s["Sheet1!A1"] = "Goodbye world"
+ @test f["Sheet1!A1"] == "Goodbye world"
+ s["Sheet1!A1:A3"] = "Goodbye cruel world"
+ @test s["Sheet1!A1:A3"] == ["Goodbye cruel world"; "Goodbye cruel world"; "Goodbye cruel world";;]
+ s["Sheet1!1:2"] = "Bright Lights"
+ @test s["A1,B2,C3"] == ["Bright Lights", "Bright Lights", true]
+ s["Sheet1!C:D"] = "Beat my Retreat"
+ @test s["B1,C2,D3"] == ["Bright Lights", "Beat my Retreat", "Beat my Retreat"]
+ s["Sheet1!B1,Sheet1!C2,Sheet1!D3"] = "Night Comes In"
+ @test s["Sheet1!B1,Sheet1!C2,Sheet1!D3"] == ["Night Comes In", "Night Comes In", "Night Comes In"]
+ @test_throws XLSX.XLSXError s["Sheet1!garbage"] = 1
+ @test_throws XLSX.XLSXError s["garbage"] = 1
+ @test_throws XLSX.XLSXError s["garbage1:garbage2"] = 1
+
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5
+ for j in 1:5
+ s[i, j] = i + j
+ end
+ end
+ @test s[1:5, 1:5] == [2 3 4 5 6; 3 4 5 6 7; 4 5 6 7 8; 5 6 7 8 9; 6 7 8 9 10]
+ s[1:3, 1:2:5] = 99
+ @test s[1:5, 1:5] == [99 3 99 5 99; 99 4 99 6 99; 99 5 99 7 99; 5 6 7 8 9; 6 7 8 9 10]
+ s[1:2:5, 4:5] = -99
+ @test s[1:5, 1:5] == [99 3 99 -99 -99; 99 4 99 6 99; 99 5 99 -99 -99; 5 6 7 8 9; 6 7 8 -99 -99]
+ s[[2, 4], [3, 5]] = 0
+ @test s[1:5, 1:5] == [99 3 99 -99 -99; 99 4 0 6 0; 99 5 99 -99 -99; 5 6 0 8 0; 6 7 8 -99 -99]
+ @test s[[2, 4], [3, 5]] == [0 0; 0 0]
+
+end
+
+@testset "getcell" begin
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:3
+ for j in 1:3
+ s[i, j] = i + j
+ end
+ end
+ @test XLSX.getcell(s, "A1") == XLSX.Cell(XLSX.CellRef("A1"), "", "", "2", "")
+ @test XLSX.getcell(s, "Sheet1!A1") == XLSX.Cell(XLSX.CellRef("A1"), "", "", "2", "")
+ @test XLSX.getcell(f, "Sheet1!A1") == XLSX.Cell(XLSX.CellRef("A1"), "", "", "2", "")
+ @test XLSX.getcell(s, XLSX.SheetCellRef("Sheet1!A1")) == XLSX.Cell(XLSX.CellRef("A1"), "", "", "2", "")
+ @test XLSX.getcell(f, XLSX.SheetCellRef("Sheet1!A1")) == XLSX.Cell(XLSX.CellRef("A1"), "", "", "2", "")
+ @test XLSX.getcell(s, "B1:B3") == [XLSX.Cell(XLSX.CellRef("B1"), "", "", "3", ""); XLSX.Cell(XLSX.CellRef("B2"), "", "", "4", ""); XLSX.Cell(XLSX.CellRef("B3"), "", "", "5", "");;]
+ @test XLSX.getcell(s, "Sheet1!B1:B3") == [XLSX.Cell(XLSX.CellRef("B1"), "", "", "3", ""); XLSX.Cell(XLSX.CellRef("B2"), "", "", "4", ""); XLSX.Cell(XLSX.CellRef("B3"), "", "", "5", "");;]
+ @test XLSX.getcell(f, "Sheet1!B1:B3") == [XLSX.Cell(XLSX.CellRef("B1"), "", "", "3", ""); XLSX.Cell(XLSX.CellRef("B2"), "", "", "4", ""); XLSX.Cell(XLSX.CellRef("B3"), "", "", "5", "");;]
+ @test XLSX.getcell(s, XLSX.SheetCellRange("Sheet1!B1:B3")) == [XLSX.Cell(XLSX.CellRef("B1"), "", "", "3", ""); XLSX.Cell(XLSX.CellRef("B2"), "", "", "4", ""); XLSX.Cell(XLSX.CellRef("B3"), "", "", "5", "");;]
+ @test XLSX.getcell(s, "B1,B3") == [XLSX.Cell(XLSX.CellRef("B1"), "", "", "3", ""), XLSX.Cell(XLSX.CellRef("B3"), "", "", "5", "")]
+ @test XLSX.getcell(s, "Sheet1!B1,Sheet1!B3") == [XLSX.Cell(XLSX.CellRef("B1"), "", "", "3", ""), XLSX.Cell(XLSX.CellRef("B3"), "", "", "5", "")]
+ @test XLSX.getcell(f, "Sheet1!B1,Sheet1!B3") == [XLSX.Cell(XLSX.CellRef("B1"), "", "", "3", ""), XLSX.Cell(XLSX.CellRef("B3"), "", "", "5", "")]
+ @test XLSX.getcell(s, "B:B") == [XLSX.Cell(XLSX.CellRef("B1"), "", "", "3", ""); XLSX.Cell(XLSX.CellRef("B2"), "", "", "4", ""); XLSX.Cell(XLSX.CellRef("B3"), "", "", "5", "");;]
+ @test XLSX.getcell(s, "Sheet1!B:B") == [XLSX.Cell(XLSX.CellRef("B1"), "", "", "3", ""); XLSX.Cell(XLSX.CellRef("B2"), "", "", "4", ""); XLSX.Cell(XLSX.CellRef("B3"), "", "", "5", "");;]
+ @test XLSX.getcell(f, "Sheet1!B:B") == [XLSX.Cell(XLSX.CellRef("B1"), "", "", "3", ""); XLSX.Cell(XLSX.CellRef("B2"), "", "", "4", ""); XLSX.Cell(XLSX.CellRef("B3"), "", "", "5", "");;]
+ @test XLSX.getcell(s, XLSX.SheetColumnRange("Sheet1!B:B")) == [XLSX.Cell(XLSX.CellRef("B1"), "", "", "3", ""); XLSX.Cell(XLSX.CellRef("B2"), "", "", "4", ""); XLSX.Cell(XLSX.CellRef("B3"), "", "", "5", "");;]
+ @test XLSX.getcell(s, "Sheet1!2:2") == [XLSX.Cell(XLSX.CellRef("A2"), "", "", "3", "") XLSX.Cell(XLSX.CellRef("B2"), "", "", "4", "") XLSX.Cell(XLSX.CellRef("C2"), "", "", "5", "")]
+ @test XLSX.getcell(f, "Sheet1!2:2") == [XLSX.Cell(XLSX.CellRef("A2"), "", "", "3", "") XLSX.Cell(XLSX.CellRef("B2"), "", "", "4", "") XLSX.Cell(XLSX.CellRef("C2"), "", "", "5", "")]
+ @test XLSX.getcell(s, XLSX.SheetRowRange("Sheet1!2:2")) == [XLSX.Cell(XLSX.CellRef("A2"), "", "", "3", "") XLSX.Cell(XLSX.CellRef("B2"), "", "", "4", "") XLSX.Cell(XLSX.CellRef("C2"), "", "", "5", "")]
+ @test XLSX.getcell(s, "2:2") == [XLSX.Cell(XLSX.CellRef("A2"), "", "", "3", "") XLSX.Cell(XLSX.CellRef("B2"), "", "", "4", "") XLSX.Cell(XLSX.CellRef("C2"), "", "", "5", "")]
+ @test XLSX.getcell(s, :, 2) == [XLSX.Cell(XLSX.CellRef("B1"), "", "", "3", ""); XLSX.Cell(XLSX.CellRef("B2"), "", "", "4", ""); XLSX.Cell(XLSX.CellRef("B3"), "", "", "5", "");;]
+ @test XLSX.getcell(s, 2, :) == [XLSX.Cell(XLSX.CellRef("A2"), "", "", "3", "") XLSX.Cell(XLSX.CellRef("B2"), "", "", "4", "") XLSX.Cell(XLSX.CellRef("C2"), "", "", "5", "")]
+ @test XLSX.getcell(s, 2, 1:2:3) == [XLSX.Cell(XLSX.CellRef("A2"), "", "", "3", ""), XLSX.Cell(XLSX.CellRef("C2"), "", "", "5", "")]
+ @test XLSX.getcell(s, 2, [1, 3]) == [XLSX.Cell(XLSX.CellRef("A2"), "", "", "3", ""), XLSX.Cell(XLSX.CellRef("C2"), "", "", "5", "")]
+ @test XLSX.getcell(s, [2], 1) == [XLSX.Cell(XLSX.CellRef("A2"), "", "", "3", "")]
+ @test XLSX.getcell(s, [2], [1, 3]) == [XLSX.Cell(XLSX.CellRef("A2"), "", "", "3", "") XLSX.Cell(XLSX.CellRef("C2"), "", "", "5", "")]
+ @test_throws XLSX.XLSXError XLSX.getcell(f, "Sheet1!garbage")
+ @test_throws XLSX.XLSXError XLSX.getcell(s, "Sheet1!garbage")
+ @test_throws XLSX.XLSXError XLSX.getcell(s, "garbage")
+ @test_throws XLSX.XLSXError XLSX.getcell(s, "garbage1:garbage2")
+
+ XLSX.addDefinedName(f, "MyName1", "Sheet1!A1")
+ XLSX.addDefinedName(s, "MyName2", "Sheet1!A2:A3")
+ s["MyName1"] = 12.9
+ @test s["MyName1"] == 12.9
+ s["MyName2"] = 42
+ @test s["MyName2"] == [42; 42;;]
+ @test XLSX.getcell(s, "MyName1") == XLSX.Cell(XLSX.CellRef("A1"), "", "", "12.9", "")
+ @test XLSX.getcell(s, "MyName2") == [XLSX.Cell(XLSX.CellRef("A2"), "", "", "42", ""); XLSX.Cell(XLSX.CellRef("A3"), "", "", "42", "");;]
+
end
@testset "Time and DateTime" begin
@@ -408,7 +602,7 @@ end
end
end
-@testset "Defined Names" begin
+@testset "Defined Names" begin # Issue #148
@test XLSX.is_defined_name_value_a_reference(XLSX.SheetCellRef("Sheet1!A1"))
@test XLSX.is_defined_name_value_a_reference(XLSX.SheetCellRange("Sheet1!A1:B2"))
@test !XLSX.is_defined_name_value_a_reference(1)
@@ -416,28 +610,98 @@ end
@test !XLSX.is_defined_name_value_a_reference("Hey")
@test !XLSX.is_defined_name_value_a_reference(missing)
- XLSX.openxlsx(joinpath(data_directory, "general.xlsx")) do f
- @test f["SINGLE_CELL"] == "single cell A2"
- @test f["RANGE_B4C5"] == Any["range B4:C5" "range B4:C5"; "range B4:C5" "range B4:C5"]
- @test f["CONST_DATE"] == 43383
- @test isapprox(f["CONST_FLOAT"], 10.2)
- @test f["CONST_INT"] == 100
- @test f["LOCAL_INT"] == 2000
- @test f["named_ranges_2"]["LOCAL_INT"] == 2000
- @test f["named_ranges"]["LOCAL_INT"] == 1000
- @test f["named_ranges"]["LOCAL_NAME"] == "Hey You"
- @test f["named_ranges_2"]["LOCAL_NAME"] == "out there in the cold"
- @test f["named_ranges"]["SINGLE_CELL"] == "single cell A2"
-
- @test_throws ErrorException f["header_error"]["LOCAL_REF"]
- @test f["named_ranges"]["LOCAL_REF"][1] == 10
- @test f["named_ranges"]["LOCAL_REF"][2] == 20
- @test f["named_ranges_2"]["LOCAL_REF"][1] == "local"
- @test f["named_ranges_2"]["LOCAL_REF"][2] == "reference"
- end
+ f = XLSX.opentemplate(joinpath(data_directory, "general.xlsx"))
+ @test f["SINGLE_CELL"] == "single cell A2"
+ @test f["RANGE_B4C5"] == Any["range B4:C5" "range B4:C5"; "range B4:C5" "range B4:C5"]
+ @test f["CONST_DATE"] == 43383
+ @test isapprox(f["CONST_FLOAT"], 10.2)
+ @test f["CONST_INT"] == 100
+ @test f["LOCAL_INT"] == 2000
+ @test f["named_ranges_2"]["LOCAL_INT"] == 2000
+ @test f["named_ranges"]["LOCAL_INT"] == 1000
+ @test f["named_ranges"]["LOCAL_NAME"] == "Hey You"
+ @test f["named_ranges_2"]["LOCAL_NAME"] == "out there in the cold"
+ @test f["named_ranges"]["SINGLE_CELL"] == "single cell A2"
+
+ @test_throws XLSX.XLSXError f["header_error"]["LOCAL_REF"]
+ @test f["named_ranges"]["LOCAL_REF"][1] == 10
+ @test f["named_ranges"]["LOCAL_REF"][2] == 20
+ @test f["named_ranges_2"]["LOCAL_REF"][1] == "local"
+ @test f["named_ranges_2"]["LOCAL_REF"][2] == "reference"
+
+ XLSX.addDefinedName(f["lookup"], "Life_the_Universe_and_Everything", 42)
+ XLSX.addDefinedName(f["lookup"], "FirstName", "Hello World")
+ XLSX.addDefinedName(f["lookup"], "single", "C2"; absolute=true)
+ XLSX.addDefinedName(f["lookup"], "range", "C3:C5"; absolute=true)
+ XLSX.addDefinedName(f["lookup"], "NonContig", "C3:C5,D3:D5"; absolute=true)
+ @test f["lookup"]["Life_the_Universe_and_Everything"] == 42
+ @test f["lookup"]["FirstName"] == "Hello World"
+ @test f["lookup"]["single"] == "NAME"
+ @test f["lookup"]["range"] == Any["name1"; "name2"; "name3";;] # A 2D Array, size (3, 1)
+ @test f["lookup"]["NonContig"] == Any["name1", "name2", "name3", 100, 200, 300] # NonContiguousRanges return a vector
+
+ XLSX.addDefinedName(f, "Life_the_Universe_and_Everything", 42)
+ XLSX.addDefinedName(f, "FirstName", "Hello World")
+ XLSX.addDefinedName(f, "single", "lookup!C2"; absolute=true)
+ XLSX.addDefinedName(f, "range", "lookup!C3:C5"; absolute=true)
+ XLSX.addDefinedName(f, "NonContig", "lookup!C3:C5,lookup!D3:D5"; absolute=true)
+ @test f["Life_the_Universe_and_Everything"] == 42
+ @test f["FirstName"] == "Hello World"
+ @test f["single"] == "NAME"
+ @test f["range"] == Any["name1"; "name2"; "name3";;] # A 2D Array, size (3, 1)
+ @test f["NonContig"] == Any["name1", "name2", "name3", 100, 200, 300] # NonContiguousRanges return a vector
+
+ XLSX.setFont(f["lookup"], "NonContig"; name="Arial", size=12, color="FF0000FF", bold=true, italic=true, under="single", strike=true)
+ @test XLSX.getFont(f["lookup"], "C3").font == Dict("i" => nothing, "b" => nothing, "u" => nothing, "strike" => nothing, "sz" => Dict("val" => "12"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF0000FF"))
+ @test XLSX.getFont(f["lookup"], "C4").font == Dict("i" => nothing, "b" => nothing, "u" => nothing, "strike" => nothing, "sz" => Dict("val" => "12"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF0000FF"))
+ @test XLSX.getFont(f["lookup"], "C5").font == Dict("i" => nothing, "b" => nothing, "u" => nothing, "strike" => nothing, "sz" => Dict("val" => "12"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF0000FF"))
+ @test XLSX.getFont(f["lookup"], "D3").font == Dict("i" => nothing, "b" => nothing, "u" => nothing, "strike" => nothing, "sz" => Dict("val" => "12"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF0000FF"))
+ @test XLSX.getFont(f["lookup"], "D4").font == Dict("i" => nothing, "b" => nothing, "u" => nothing, "strike" => nothing, "sz" => Dict("val" => "12"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF0000FF"))
+ @test XLSX.getFont(f["lookup"], "D5").font == Dict("i" => nothing, "b" => nothing, "u" => nothing, "strike" => nothing, "sz" => Dict("val" => "12"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF0000FF"))
+ XLSX.setFont(f, "single"; name="Arial", size=12, color="FF0000FF", bold=true, italic=true, under="double", strike=true)
+ @test XLSX.getFont(f["lookup"], "C2").font == Dict("i" => nothing, "b" => nothing, "u" => Dict("val" => "double"), "strike" => nothing, "sz" => Dict("val" => "12"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF0000FF"))
+
+ XLSX.writexlsx("mytest.xlsx", f, overwrite=true)
+
+ f = XLSX.readxlsx("mytest.xlsx")
+ @test f["Life_the_Universe_and_Everything"] == 42
+ @test f["FirstName"] == "Hello World"
+ @test f["single"] == "NAME"
+ @test f["range"] == Any["name1"; "name2"; "name3";;] # A 2D Array, size (3, 1)
+ @test f["NonContig"] == Any["name1", "name2", "name3", 100, 200, 300] # NonContiguousRanges return a vector
+ isfile("mytest.xlsx") && rm("mytest.xlsx")
@test XLSX.readdata(joinpath(data_directory, "general.xlsx"), "SINGLE_CELL") == "single cell A2"
@test XLSX.readdata(joinpath(data_directory, "general.xlsx"), "RANGE_B4C5") == Any["range B4:C5" "range B4:C5"; "range B4:C5" "range B4:C5"]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:B3"] = "Hello world"
+ XLSX.addDefinedName(f, "Life_the_Universe_and_Everything", 42)
+ XLSX.addDefinedName(f[1], "FirstName", "Hello World")
+ XLSX.addDefinedName(f, "MyCell", "Sheet1!A1")
+ XLSX.addDefinedName(f[1], "YourCells", "Sheet1!A2:B3")
+ @test_throws XLSX.XLSXError XLSX.addDefinedName(s, "yourcells", "Sheet1!A2:B3") # not unique (case insensitive)
+ @test_throws XLSX.XLSXError XLSX.addDefinedName(s, "firstname", "NewText") # not unique (case insensitive)
+ @test_throws XLSX.XLSXError s["FirstName"] = 32
+ s["MyCell"] = true
+ @test s["MyCell"] == true
+ s["YourCells"] = false
+ @test s["YourCells"] == Any[false false; false false]
+
+ XLSX.writexlsx("mytest.xlsx", f, overwrite=true)
+ f = XLSX.readxlsx("mytest.xlsx")
+ @test s["MyCell"] == true
+ @test s["YourCells"] == Any[false false; false false]
+ isfile("mytest.xlsx") && rm("mytest.xlsx")
+
+ @test_throws XLSX.XLSXError XLSX.addDefinedName(f, "A1", "Sheet1!B1")
+ @test_throws XLSX.XLSXError XLSX.addDefinedName(f, "A1:A3", "Sheet1!B2:B3")
+ @test_throws XLSX.XLSXError XLSX.addDefinedName(f, "A1,A3", 42)
+ @test_throws XLSX.XLSXError XLSX.addDefinedName(s, "Sheet1!A1", "Sheet1!B1")
+ @test_throws XLSX.XLSXError XLSX.addDefinedName(s, "Sheet1!A1:A3", "Sheet1!B2:B3")
+ @test_throws XLSX.XLSXError XLSX.addDefinedName(s, "Sheet1!A1,Sheet!A3", 42)
+
end
@testset "Book1.xlsx" begin
@@ -530,19 +794,90 @@ end
@test XLSX.column_number(emptycell) == 2
end
+@testset "No Dimension" begin
+ noDim = XLSX.readxlsx(joinpath(data_directory, "NoDim.xlsx")) # These two files are the same except the `NoDim` file has the dimension nodes removed.
+ Dim = XLSX.readxlsx(joinpath(data_directory, "customXml.xlsx"))
+ @test noDim[1].dimension == Dim[1].dimension
+ @test noDim[2].dimension == Dim[2].dimension
+
+ f = XLSX.newxlsx()
+ s=f[1]
+ for i=10:20, j=10:20
+ s[i, j] = i+j
+ end
+ XLSX.set_dimension!(s,XLSX.CellRange(XLSX.CellRef("J10"), XLSX.CellRef("T20")))
+ @test XLSX.get_dimension(s) == XLSX.CellRange(XLSX.CellRef("J10"), XLSX.CellRef("T20"))
+ s["A1"]=2
+ @test XLSX.get_dimension(s) == XLSX.CellRange(XLSX.CellRef("A1"), XLSX.CellRef("T20"))
+
+end
+
@testset "Column Range" begin
cr = XLSX.ColumnRange("B:D")
@test string(cr) == "B:D"
@test cr.start == 2
@test cr.stop == 4
@test length(cr) == 3
- @test_throws AssertionError XLSX.ColumnRange("B1:D3")
- @test_throws AssertionError XLSX.ColumnRange("D:A")
+ @test_throws XLSX.XLSXError XLSX.ColumnRange("B1:D3")
+ @test_throws XLSX.XLSXError XLSX.ColumnRange("D:A")
@test collect(cr) == ["B", "C", "D"]
@test XLSX.ColumnRange("B:D") == XLSX.ColumnRange("B:D")
@test hash(XLSX.ColumnRange("B:D")) == hash(XLSX.ColumnRange("B:D"))
end
+@testset "Row Range" begin # Issue #150
+ cr = XLSX.RowRange("2:5")
+ @test string(cr) == "2:5"
+ @test cr.start == 2
+ @test cr.stop == 5
+ @test length(cr) == 4
+ @test collect(cr) == ["2", "3", "4", "5"]
+
+ cr = XLSX.RowRange("2")
+ @test string(cr) == "2:2"
+ @test cr.start == 2
+ @test cr.stop == 2
+ @test length(cr) == 1
+ @test collect(cr) == ["2"]
+
+ @test_throws XLSX.XLSXError XLSX.RowRange("B1:D3")
+ @test_throws XLSX.XLSXError XLSX.RowRange("5:2")
+ @test XLSX.RowRange("2:5") == XLSX.RowRange("2:5")
+ @test hash(XLSX.RowRange("2:5")) == hash(XLSX.RowRange("2:5"))
+end
+
+@testset "Non-contiguous Range" begin
+ cr = XLSX.NonContiguousRange("Sheet1!D1:D3,Sheet1!B1:B3")
+ @test string(cr) == "Sheet1!D1:D3,Sheet1!B1:B3"
+ @test cr.sheet == "Sheet1"
+ @test cr.rng == [XLSX.CellRange("D1:D3"), XLSX.CellRange("B1:B3")]
+ @test length(cr) == 6
+ @test length(XLSX.NonContiguousRange("Sheet1!B1:B1,Sheet1!B1")) == 1
+ @test collect(cr.rng) == [XLSX.CellRange("D1:D3"), XLSX.CellRange("B1:B3")]
+ @test XLSX.NonContiguousRange("Sheet1!D1:D3,Sheet1!B1:B3") == XLSX.NonContiguousRange("Sheet1!D1:D3,Sheet1!B1:B3")
+ @test hash(XLSX.NonContiguousRange("Sheet1!D1:D3,Sheet1!B1:B3")) == hash(XLSX.NonContiguousRange("Sheet1!D1:D3,Sheet1!B1:B3"))
+
+ f = XLSX.newxlsx("Sheet 1")
+ s = f["Sheet 1"]
+ for cell in XLSX.CellRange("A1:D6")
+ s[cell] = ""
+ end
+ cr = XLSX.NonContiguousRange(s, "D1:D3,A2,B1:B3")
+ @test string(cr) == "'Sheet 1'!D1:D3,'Sheet 1'!A2,'Sheet 1'!B1:B3"
+ @test cr.sheet == "Sheet 1"
+ @test cr.rng == [XLSX.CellRange("D1:D3"), XLSX.CellRef("A2"), XLSX.CellRange("B1:B3")]
+ @test length(cr) == 7
+ @test length(XLSX.NonContiguousRange(s, "B1:B1,B1")) == 1
+ @test collect(cr.rng) == [XLSX.CellRange("D1:D3"), XLSX.CellRef("A2"), XLSX.CellRange("B1:B3")]
+ @test XLSX.NonContiguousRange(s, "D1:D3,A2,B1:B3") == XLSX.NonContiguousRange(s, "D1:D3,A2,B1:B3")
+ @test hash(XLSX.NonContiguousRange(s, "D1:D3,A2,B1:B3")) == hash(XLSX.NonContiguousRange(s, "D1:D3,A2,B1:B3"))
+
+ @test_throws XLSX.XLSXError XLSX.NonContiguousRange("Sheet1!D1:D3,B1:B3")
+ @test_throws XLSX.XLSXError XLSX.NonContiguousRange("Sheet1!D1:D3,Sheet2!B1:B3")
+ @test_throws XLSX.XLSXError XLSX.NonContiguousRange("B1:D3")
+ @test_throws XLSX.XLSXError XLSX.NonContiguousRange("2:5")
+end
+
@testset "CellRange iterator" begin
rng = XLSX.CellRange("A2:C4")
@test collect(rng) == [XLSX.CellRef("A2"), XLSX.CellRef("B2"), XLSX.CellRef("C2"), XLSX.CellRef("A3"), XLSX.CellRef("B3"), XLSX.CellRef("C3"), XLSX.CellRef("A4"), XLSX.CellRef("B4"), XLSX.CellRef("C4")]
@@ -618,7 +953,7 @@ end
@test XLSX.last_column_index(r, 2) == 4
@test XLSX.last_column_index(r, 3) == 4
@test XLSX.last_column_index(r, 4) == 4
- @test_throws AssertionError XLSX.last_column_index(r, 5)
+ @test_throws XLSX.XLSXError XLSX.last_column_index(r, 5)
elseif XLSX.row_number(r) == 9
@test XLSX.last_column_index(r, 2) == 3
@test XLSX.last_column_index(r, 3) == 3
@@ -731,35 +1066,35 @@ end
# queries based on ColumnRange
x = XLSX.getcellrange(s, XLSX.ColumnRange("B:D"))
- @test size(x) == (11, 3)
+ @test size(x) == (12, 3)
y = XLSX.getcellrange(s, "B:D")
- @test size(y) == (11, 3)
+ @test size(y) == (12, 3)
@test x == y
- @test_throws AssertionError XLSX.getcellrange(s, "D:B")
- @test_throws ErrorException XLSX.getcellrange(s, "A:C1")
+ @test_throws XLSX.XLSXError XLSX.getcellrange(s, "D:B")
+ @test_throws XLSX.XLSXError XLSX.getcellrange(s, "A:C1")
d = XLSX.getdata(s, "B:D")
- @test size(d) == (11, 3)
- @test_throws ErrorException XLSX.getdata(s, "A:C1")
+ @test size(d) == (12, 3)
+ @test_throws XLSX.XLSXError XLSX.getdata(s, "A:C1")
@test d[1, 1] == "Column B"
@test d[1, 2] == "Column C"
@test d[1, 3] == "Column D"
@test d[9, 1] == 8
@test d[9, 2] == "Str2"
@test d[9, 3] == Date(2018, 4, 28)
- @test d[10, 1] == "trash"
- @test ismissing(d[10, 2])
- @test d[10, 3] == "trash"
- @test ismissing(d[11, 1])
+ @test d[11, 1] == "trash"
@test ismissing(d[11, 2])
- @test ismissing(d[11, 3])
+ @test d[11, 3] == "trash"
+ @test ismissing(d[12, 1])
+ @test ismissing(d[12, 2])
+ @test ismissing(d[12, 3])
d2 = f["table!B:D"]
@test size(d) == size(d2)
@test all(d .=== d2)
- @test_throws ErrorException f["table!B1:D"]
- @test_throws AssertionError f["table!D:B"]
+ @test_throws XLSX.XLSXError f["table!B1:D"]
+ @test_throws XLSX.XLSXError f["table!D:B"]
s = f["table2"]
test_data = Vector{Any}(undef, 3)
@@ -787,7 +1122,7 @@ end
@test XLSX.get_column_label(rowdata, 2) == :HB
@test XLSX.get_column_label(rowdata, 3) == :HC
- @test_throws ErrorException XLSX.getdata(rowdata, :INVALID_COLUMN)
+ @test_throws XLSX.XLSXError XLSX.getdata(rowdata, :INVALID_COLUMN)
end
override_col_names_strs = ["ColumnA", "ColumnB", "ColumnC"]
@@ -844,7 +1179,7 @@ end
data, col_names = dtable.data, dtable.column_labels
@test col_names == [:H1, :H2, :H3]
check_test_data(data, test_data)
- @test_throws ErrorException XLSX.find_row(XLSX.eachrow(s), 20)
+ @test_throws XLSX.XLSXError XLSX.find_row(XLSX.eachrow(s), 20)
for r in XLSX.eachrow(s)
@test isempty(XLSX.getcell(r, "A"))
@@ -855,7 +1190,7 @@ end
end
@test XLSX._find_first_row_with_data(s, 5) == 5
- @test_throws ErrorException XLSX._find_first_row_with_data(s, 7)
+ @test_throws XLSX.XLSXError XLSX._find_first_row_with_data(s, 7)
s = f["table4"]
dtable = XLSX.gettable(s)
@@ -866,10 +1201,10 @@ end
@testset "empty/invalid" begin
XLSX.openxlsx(joinpath(data_directory, "general.xlsx")) do xf
empty_sheet = XLSX.getsheet(xf, "empty")
- @test_throws ErrorException XLSX.gettable(empty_sheet)
+ @test_throws XLSX.XLSXError XLSX.gettable(empty_sheet)
itr = XLSX.eachrow(empty_sheet)
- @test_throws ErrorException XLSX.find_row(itr, 1)
- @test_throws ErrorException XLSX.getsheet(xf, "invalid_sheet")
+ @test_throws XLSX.XLSXError XLSX.find_row(itr, 1)
+ @test_throws XLSX.XLSXError XLSX.getsheet(xf, "invalid_sheet")
end
end
@@ -970,6 +1305,53 @@ end
test_data = Any[Any["C3", missing], Any[missing, "D4"]]
check_test_data(data, test_data)
end
+
+ @testset "Read DataFrame" begin
+
+ df = XLSX.readto(joinpath(data_directory, "general.xlsx"), "table4", "F:G", DataFrames.DataFrame)
+ @test names(df) == ["H2", "H3"]
+ @test size(df) == (2, 2)
+ @test df[1, :H2] == "C3"
+ @test df[2, :H3] == "D4"
+ @test ismissing(df[1, 2])
+ @test ismissing(df[2, 1])
+
+ df = XLSX.readto(joinpath(data_directory, "general.xlsx"), "table4", DataFrames.DataFrame)
+ @test names(df) == ["H1", "H2", "H3"]
+ @test size(df) == (3, 3)
+ @test df[1, :H2] == "C3"
+ @test df[2, :H3] == "D4"
+ @test ismissing(df[1, :H1])
+ @test ismissing(df[2, :H2])
+
+ df = XLSX.readto(joinpath(data_directory, "general.xlsx"), DataFrames.DataFrame)
+ @test names(df) == ["text", "regular text"]
+ @test size(df) == (9, 2)
+ @test df[1, "text"] == "integer"
+ @test df[2, "regular text"] == 102.2
+ @test df[3, 2] == Dates.Date(1983, 04, 16)
+ @test df[5, 2] == Dates.DateTime(2018, 04, 16, 19, 19, 51)
+
+ @test_throws XLSX.XLSXError df = XLSX.readto(joinpath(data_directory, "general.xlsx")) # No sink
+ @test_throws XLSX.XLSXError df = XLSX.readto(joinpath(data_directory, "general.xlsx"), 3) # No sink
+ @test_throws XLSX.XLSXError df = XLSX.readto(joinpath(data_directory, "general.xlsx"), 3, "F:G") # No sink
+
+ end
+
+ @testset "normalizenames" begin # Issue #260
+
+ data = Vector{Any}()
+ push!(data, [:sym1, :sym2, :sym3])
+ push!(data, [1.0, 2.0, 3.0])
+ push!(data, ["abc", "DeF", "gHi"])
+ push!(data, [true, true, false])
+ cols = ["1 col", "col \$2", "local", "col:4"]
+
+ XLSX.writetable("mytest.xlsx", data, cols; overwrite=true)
+ df = DataFrames.DataFrame(XLSX.readtable("mytest.xlsx", "Sheet1", normalizenames=true))
+ @test DataFrames.names(df) == Any["_1_col", "col_2", "_local", "col_4"]
+
+ end
end
@testset "Write" begin
@@ -998,6 +1380,25 @@ end
isfile(filename_copy) && rm(filename_copy)
end
+@testset "Save" begin
+ f=XLSX.openxlsx("saveable.xlsx", mode="w")
+ XLSX.rename!(f["Sheet1"], "new_name")
+ s=f[1]
+ s[1:10, 1:10] = "hello world"
+ @test XLSX.savexlsx(f) == abspath("saveable.xlsx")
+ f2 = XLSX.openxlsx("saveable.xlsx", mode="rw")
+ @test XLSX.hassheet(f2, "new_name")
+ @test f2["new_name"][1, 1] == "hello world"
+ @test f2["new_name"][10, 10] == "hello world"
+ f2["new_name"][1:5, 1:5] = "goodbye world"
+ XLSX.savexlsx(f2)
+ f3 = XLSX.openxlsx("saveable.xlsx", mode="r")
+ @test f3["new_name"][1, 1] == "goodbye world"
+ @test f3["new_name"][5, 5] == "goodbye world"
+ @test f3["new_name"][10, 10] == "hello world"
+ isfile("saveable.xlsx") && rm("saveable.xlsx")
+end
+
@testset "CustomXml" begin
# issue #210
# None of the example .xlsx files in the test suite include custoimXml internal files
@@ -1011,7 +1412,7 @@ end
sheet["Q3"] = "this"
sheet["Q4"] = "template"
end
- @test XLSX.writexlsx(filename_copy, template, overwrite=true) === nothing # This is where the bug will throw if custoimXml internal files present.
+ @test XLSX.writexlsx(filename_copy, template, overwrite=true) == abspath(filename_copy) # This is where the bug will throw if customXml internal files present.
@test isfile(filename_copy)
f_copy = XLSX.readxlsx(filename_copy) # Don't really think this second part is necessary.
test_Xmlread = [["Cant", "write", "this", "template"]]
@@ -1038,15 +1439,33 @@ end
rm(new_filename)
end
-@testset "addsheet!" begin
- new_filename = "template_with_new_sheet.xlsx"
- f = XLSX.open_empty_template()
- s = XLSX.addsheet!(f, "new_sheet")
- s["A1"] = 10
+@testset "add/copy sheet!" begin
+
+ @testset "addsheet!" begin
+
+ new_filename = "template_with_new_sheet.xlsx"
+ f = XLSX.open_empty_template()
+ s = XLSX.addsheet!(f, "new_sheet")
+ s["A1"] = 10
+ @test XLSX.sheetnames(f) == ["Sheet1", "new_sheet"]
+ XLSX.writexlsx(new_filename, f, overwrite=true)
+
- @testset "check invalid sheet names" begin
+ big_sheetname = "aaaaaaaaaabbbbbbbbbbccccccccccd"
+ s2 = XLSX.addsheet!(f, big_sheetname)
+
+ XLSX.writexlsx(new_filename, f, overwrite=true)
+ fx = XLSX.opentemplate(new_filename)
+ @test XLSX.sheetnames(f) == ["Sheet1", "new_sheet", big_sheetname]
+
+ end
+
+ @testset "invalid sheet names" begin
+
+ f = XLSX.open_empty_template()
+ s = XLSX.addsheet!(f, "new_sheet")
+ s["A1"] = 10
invalid_names = [
- "new_sheet",
"aaaaaaaaaabbbbbbbbbbccccccccccd1",
"abc:def",
"abcdef/",
@@ -1057,28 +1476,122 @@ end
]
for invalid_name in invalid_names
- @test_throws AssertionError XLSX.addsheet!(f, invalid_name)
+ @test_throws XLSX.XLSXError XLSX.addsheet!(f, invalid_name)
end
+
end
- big_sheetname = "aaaaaaaaaabbbbbbbbbbccccccccccd"
- s2 = XLSX.addsheet!(f, big_sheetname)
+ @testset "copysheet!" begin
- XLSX.writexlsx(new_filename, f, overwrite=true)
+ f=XLSX.newxlsx()
+ XLSX.rename!(f["Sheet1"], "new_name")
+ XLSX.addsheet!(f)
+ for x=1:10, y=1:10
+ f["Sheet1"][x, y] = x + y
+ f["new_name"][x, y] = x * y
+ end
+ XLSX.addDefinedName(f["new_name"], "new_name_range", "A1:B10")
+ XLSX.addDefinedName(f["Sheet1"], "Sheet1_range", "C1:D10")
+ XLSX.setBorder(f["new_name"], "A1:D10"; allsides=["style"=>"thin", "color"=>"red"])
+ XLSX.setBorder(f["Sheet1"], "A1:D10"; allsides=["style"=>"thin", "color"=>"red"])
+ XLSX.setConditionalFormat(f["new_name"], "A1:D10", :colorScale)
+
+ s3 = XLSX.copysheet!(f["new_name"], "copied_sheet")
+ @test s3.name == "copied_sheet"
+ @test s3["A1"] == 1
+ @test s3[5, 5] == 25
+ @test s3[10, 10] == 100
+ @test XLSX.get_workbook(s3).worksheet_names == XLSX.get_workbook(f["new_name"]).worksheet_names
+ @test XLSX.getConditionalFormats(s3) == XLSX.getConditionalFormats(f["new_name"])
+ @test XLSX.getBorder(s3,"C5").border == XLSX.getBorder(f["new_name"],"C5").border
+
+ # Check that the original sheet is unchanged
+ s2=f["new_name"]
+ @test s2["A1"] == 1
+ @test s2[5, 5] == 25
+ @test s2[10, 10] == 100
+
+ s4 = XLSX.copysheet!(s3)
+ @test s4.name == "copied_sheet (copy)"
+ @test s4["A1"] == 1
+ @test s4[5, 5] == 25
+ @test s4[10, 10] == 100
+
+ @test XLSX.get_workbook(s4).worksheet_names == XLSX.get_workbook(f["new_name"]).worksheet_names
+ XLSX.setBorder(s4, "F1:H10"; allsides=["style"=>"thin", "color"=>"green"])
+ XLSX.setConditionalFormat(s4, "F1:H10", :colorScale; colorscale="redyellowgreen")
+
+ XLSX.writexlsx("copied_sheets.xlsx", f, overwrite=true)
+ f = XLSX.opentemplate("copied_sheets.xlsx")
+ @test XLSX.sheetnames(f) == ["new_name", "Sheet1", "copied_sheet", "copied_sheet (copy)"]
+ @test XLSX.get_workbook(f["copied_sheet"]).worksheet_names == XLSX.get_workbook(f["new_name"]).worksheet_names
+ @test XLSX.getConditionalFormats(f["copied_sheet (copy)"]) == XLSX.getConditionalFormats(s4)
+ @test XLSX.getBorder(f["copied_sheet (copy)"],"C5").border == XLSX.getBorder(f["new_name"],"C5").border
+ @test XLSX.getBorder(f["copied_sheet (copy)"],"G5").border == XLSX.getBorder(s4,"G5").border
- # @test !XLSX.isopen(f)
+ end
+ isfile("copied_sheets.xlsx") && rm("copied_sheets.xlsx")
+
+ @testset "deletesheet!" begin
+
+ new_filename = "template_with_new_sheet.xlsx"
+ big_sheetname = "aaaaaaaaaabbbbbbbbbbccccccccccd"
+ fx = XLSX.opentemplate(new_filename)
+ XLSX.deletesheet!(fx, big_sheetname)
+ @test XLSX.sheetnames(fx) == ["Sheet1", "new_sheet"]
+ XLSX.writexlsx(new_filename, fx, overwrite=true)
+ f = XLSX.readxlsx(new_filename)
+ @test XLSX.sheetnames(f) == ["Sheet1", "new_sheet"]
+
+ f = XLSX.opentemplate(joinpath(data_directory, "general.xlsx"))
+ sc = XLSX.sheetcount(f)
+ XLSX.deletesheet!(f, "empty")
+ @test XLSX.sheetcount(f) == sc - 1 # Check it's gone.
+ @test XLSX.hassheet(f, "empty") == false # Check it's gone.
+ @test_throws XLSX.XLSXError XLSX.deletesheet!(f, "empty") # Already deleted.
+ @test_throws XLSX.XLSXError XLSX.deletesheet!(f, "nosuchsheet") # Never there.
+ s2 = XLSX.addsheet!(f, "this_now")
+ @test XLSX.sheetnames(f) == ["general", "table3", "table4", "table", "table2", "table5", "table6", "table7", "lookup", "header_error", "named_ranges_2", "named_ranges", "this_now"]
+ XLSX.writexlsx(new_filename, f, overwrite=true)
+
+ f = XLSX.opentemplate(new_filename)
+ @test XLSX.sheetnames(f) == ["general", "table3", "table4", "table", "table2", "table5", "table6", "table7", "lookup", "header_error", "named_ranges_2", "named_ranges", "this_now"]
+ XLSX.deletesheet!(f, "named_ranges")
+ XLSX.deletesheet!(f["general"])
+ @test XLSX.sheetnames(f) == ["table3", "table4", "table", "table2", "table5", "table6", "table7", "lookup", "header_error", "named_ranges_2", "this_now"]
+ XLSX.writexlsx(new_filename, f, overwrite=true)
+ dtable = XLSX.readtable(new_filename, "table4", "F:G")
+ data, col_names = dtable.data, dtable.column_labels
+ @test col_names == [:H2, :H3]
+ test_data = Any[Any["C3", missing], Any[missing, "D4"]]
+ check_test_data(data, test_data)
+ @test XLSX.deletesheet!(f, 1) === f
+ @test XLSX.sheetnames(f) == ["table4", "table", "table2", "table5", "table6", "table7", "lookup", "header_error", "named_ranges_2", "this_now"]
+ XLSX.writexlsx(new_filename, f, overwrite=true)
+ dtable = XLSX.readtable(new_filename, "table4", "F:G")
+ data, col_names = dtable.data, dtable.column_labels
+ @test col_names == [:H2, :H3]
+ test_data = Any[Any["C3", missing], Any[missing, "D4"]]
+ check_test_data(data, test_data)
+
+ f = XLSX.opentemplate(joinpath(data_directory, "Book_1904.xlsx")) # Only one sheet - can't delete
+ @test_throws XLSX.XLSXError XLSX.deletesheet!(f, 1)
+ s = f[1]
+ @test_throws XLSX.XLSXError XLSX.deletesheet!(s)
+ @test_throws XLSX.XLSXError XLSX.deletesheet!(f, "Sheet1")
+
+ end
+
+ isfile("template_with_new_sheet.xlsx") && rm("template_with_new_sheet.xlsx")
- f = XLSX.readxlsx(new_filename)
- @test XLSX.sheetnames(f) == ["Sheet1", "new_sheet", big_sheetname]
- rm(new_filename)
end
@testset "Edit" begin
f = XLSX.open_xlsx_template(joinpath(data_directory, "general.xlsx"))
s = f["general"]
- @test_throws ErrorException s["A1"] = :sym
+ @test_throws XLSX.XLSXError s["A1"] = :sym
XLSX.rename!(s, "general") # no-op
- @test_throws AssertionError XLSX.rename!(s, "table") # name is taken
+ @test_throws XLSX.XLSXError XLSX.rename!(s, "table") # name is taken
XLSX.rename!(s, "renamed_sheet")
@test s.name == "renamed_sheet"
s["A1"] = "Hey You!"
@@ -1139,7 +1652,7 @@ end
dtable = XLSX.readtable("output_table.xlsx", "report")
read_data, read_column_names = dtable.data, dtable.column_labels
@test length(read_column_names) == length(col_names)
- for c in 1:length(col_names)
+ for c in axes(col_names, 1)
@test Symbol(col_names[c]) == read_column_names[c]
end
check_test_data(read_data, data)
@@ -1170,7 +1683,6 @@ end
@test labels[1] == :COLUMN_A
@test labels[2] == :COLUMN_B
check_test_data(data, report_2_data)
-
XLSX.writetable("output_tables.xlsx", [("REPORT_A", report_1_data, report_1_column_names), ("REPORT_B", report_2_data, report_2_column_names)], overwrite=true)
dtable = XLSX.readtable("output_tables.xlsx", "REPORT_A")
@@ -1187,7 +1699,6 @@ end
report_1_column_names = [:HEADER_A, :HEADER_B]
report_2_column_names = [:COLUMN_A, :COLUMN_B]
-
XLSX.writetable("output_tables.xlsx", [("REPORT_A", report_1_data, report_1_column_names), ("REPORT_B", report_2_data, report_2_column_names)], overwrite=true)
dtable = XLSX.readtable("output_tables.xlsx", "REPORT_A")
@@ -1209,7 +1720,6 @@ end
report_2_data = Vector{Any}(undef, 2)
report_2_data[1] = [Date(2017, 2, 1), Date(2018, 2, 1)]
report_2_data[2] = [10.2, 10.3]
-
XLSX.writetable("output_tables.xlsx", [("REPORT_A", report_1_data, report_1_column_names), ("REPORT_B", report_2_data, report_2_column_names)], overwrite=true)
dtable = XLSX.readtable("output_tables.xlsx", "REPORT_A")
@@ -1236,14 +1746,39 @@ end
@test dt_read.column_label_index == dt.column_label_index
end
+ @testset "extended types" begin # Issue #239
+ @enum enums begin
+ enum1
+ enum2
+ enum3
+ end
+
+ data = Vector{Any}()
+ push!(data, [:sym1, :sym2, :sym3])
+ push!(data, [1.0, 2.0, 3.0])
+ push!(data, ["abc", "DeF", "gHi"])
+ push!(data, [true, true, false])
+ push!(data, [XLSX.CellRef("A1"), XLSX.CellRef("B2"), XLSX.CellRef("CCC34000")])
+ push!(data, collect(instances(enums)))
+ cols = [string(eltype(x)) for x in data]
+
+ XLSX.writetable("mytest.xlsx", data, cols; overwrite=true)
+
+ f = XLSX.readxlsx("mytest.xlsx")
+ @test f[1]["A1"] == "Symbol"
+ @test f[1]["A1:A4"] == Any["Symbol"; "sym1"; "sym2"; "sym3";;] # A 2D Array, size (4, 1)
+ @test f[1]["A1"] == "Symbol"
+ @test f[1]["E1:E4"] == Any["XLSX.CellRef"; "A1"; "B2"; "CCC34000";;]
+ end
+
# delete files created by this testset
- delete_files = ["output_table.xlsx", "output_tables.xlsx"]
+ delete_files = ["output_table.xlsx", "output_tables.xlsx", "mytest.xlsx"]
for f in delete_files
isfile(f) && rm(f)
end
end
-@testset "Styles" verbose = true begin
+@testset "Styles" begin
@testset "Original" begin
using XLSX: CellValue, id, getcell, setdata!, CellRef
@@ -1462,6 +1997,49 @@ end
end
@testset "setFont" begin
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:B2,D1:E2"] = ""
+
+ XLSX.setFont(s, "A1:A2"; bold=true, italic=true, size=24, name="Arial")
+ XLSX.setFont(s, "B1:B2"; bold=true, italic=false, size=14, name="Aptos")
+ XLSX.setFont(s, "D1:D2"; bold=false, italic=true, size=34, name="Berlin Sans FB Demi")
+ XLSX.setFont(s, "E1:E2"; bold=false, italic=false, size=4, name="Times New Roman")
+ XLSX.setUniformFont(s, "A1:B2,D1:E2"; color="blue") # `setUniformAttribute()` on a non-contiguous range
+ @test XLSX.getFont(s, "A1").font == Dict("b" => nothing, "i" => nothing, "sz" => Dict("val" => "24"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF0000FF"))
+ @test XLSX.getFont(s, "B2").font == Dict("b" => nothing, "i" => nothing, "sz" => Dict("val" => "24"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF0000FF"))
+ @test XLSX.getFont(s, "D1").font == Dict("b" => nothing, "i" => nothing, "sz" => Dict("val" => "24"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF0000FF"))
+ @test XLSX.getFont(s, "E2").font == Dict("b" => nothing, "i" => nothing, "sz" => Dict("val" => "24"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF0000FF"))
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:Z26"] = ""
+
+ XLSX.setFont(s, "Sheet1!A1:A2"; bold=true, italic=true, size=24, name="Arial", color="blue")
+ @test XLSX.getFont(s, "A1").font == Dict("b" => nothing, "i" => nothing, "sz" => Dict("val" => "24"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF0000FF"))
+ XLSX.setFont(s, "Sheet1!Y:Z"; bold=true, italic=false, size=14, name="Aptos", color="blue")
+ @test XLSX.getFont(s, "Y20").font == Dict("b" => nothing, "sz" => Dict("val" => "14"), "name" => Dict("val" => "Aptos"), "color" => Dict("rgb" => "FF0000FF"))
+ XLSX.setFont(s, "Sheet1!2:3"; bold=false, italic=true, size=34, name="Berlin Sans FB Demi", color="blue")
+ @test XLSX.getFont(s, "M3").font == Dict("i" => nothing, "sz" => Dict("val" => "34"), "name" => Dict("val" => "Berlin Sans FB Demi"), "color" => Dict("rgb" => "FF0000FF"))
+ XLSX.setFont(f, "Sheet1!A1:A2"; bold=false, italic=false, size=14, name="Aptos", color="green")
+ @test XLSX.getFont(s, "A1").font == Dict("sz" => Dict("val" => "14"), "name" => Dict("val" => "Aptos"), "color" => Dict("rgb" => "FF008000"))
+ XLSX.setFont(f, "Sheet1!Y:Z"; bold=false, italic=true, size=24, name="Arial", color="green")
+ @test XLSX.getFont(s, "Y20").font == Dict("i" => nothing, "sz" => Dict("val" => "24"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF008000"))
+ XLSX.setFont(f, "Sheet1!2:3"; bold=true, italic=false, size=24, name="Times New Roman", color="green")
+ @test XLSX.getFont(s, "M3").font == Dict("b" => nothing, "sz" => Dict("val" => "24"), "name" => Dict("val" => "Times New Roman"), "color" => Dict("rgb" => "FF008000"))
+ XLSX.setFont(s, "E1,E2,G2:G4"; bold=false, italic=false, size=4, name="Times New Roman", color="blue")
+ @test XLSX.getFont(s, "G3").font == Dict("sz" => Dict("val" => "4"), "name" => Dict("val" => "Times New Roman"), "color" => Dict("rgb" => "FF0000FF"))
+ XLSX.setFont(s, :, 15:16; bold=true, italic=false, size=38, name="Wingdings", color="red")
+ @test XLSX.getFont(s, "P10").font == Dict("b" => nothing, "sz" => Dict("val" => "38"), "name" => Dict("val" => "Wingdings"), "color" => Dict("rgb" => "FFFF0000"))
+ XLSX.setFont(s, 15:16, :; bold=false, italic=true, size=8, name="Wingdings", color="red")
+ @test XLSX.getFont(f, "Sheet1!T16").font == Dict("i" => nothing, "sz" => Dict("val" => "8"), "name" => Dict("val" => "Wingdings"), "color" => Dict("rgb" => "FFFF0000"))
+ XLSX.setFont(s, [20, 22, 24], :; bold=false, italic=true, size=48, name="Aptos", color="red")
+ @test XLSX.getFont(f, "Sheet1!H22").font == Dict("i" => nothing, "sz" => Dict("val" => "48"), "name" => Dict("val" => "Aptos"), "color" => Dict("rgb" => "FFFF0000"))
+ XLSX.setUniformFont(s, [15, 16, 20, 22, 24], :; bold=false, italic=true, size=28, name="Aptos", color="red")
+ @test XLSX.getFont(f, "Sheet1!H15").font == Dict("i" => nothing, "sz" => Dict("val" => "28"), "name" => Dict("val" => "Aptos"), "color" => Dict("rgb" => "FFFF0000"))
+ @test XLSX.getFont(f, "Sheet1!H22").font == Dict("i" => nothing, "sz" => Dict("val" => "28"), "name" => Dict("val" => "Aptos"), "color" => Dict("rgb" => "FFFF0000"))
+
xfile = XLSX.open_empty_template()
wb = XLSX.get_workbook(xfile)
sheet = xfile["Sheet1"]
@@ -1491,6 +2069,9 @@ end
dcolorkey = collect(keys(default_font["color"]))[1]
dcolorval = collect(values(default_font["color"]))[1]
+ # Sheet mismatch
+ @test_throws XLSX.XLSXError XLSX.setFont(sheet, "S2!A1"; bold=true, size=24, name="Arial")
+
XLSX.setFont(sheet, "A1"; bold=true, size=24, name="Arial")
@test XLSX.getFont(sheet, "A1").font == Dict("b" => nothing, "sz" => Dict("val" => "24"), "name" => Dict("val" => "Arial"), "color" => Dict(dcolorkey => dcolorval))
XLSX.setFont(sheet, "A1"; size=18)
@@ -1559,12 +2140,74 @@ end
@test XLSX.getFont(xfile, "Sheet1!B4").font == Dict("sz" => Dict("val" => "12"), "name" => Dict("val" => "Times New Roman"), "color" => Dict("rgb" => "FF040404"))
isfile("output.xlsx") && rm("output.xlsx")
+
+ f = XLSX.newxlsx()
+ sheet = f[1]
+ sheet["A1:E5"] = ""
+ XLSX.setFont(sheet, :, [1, 2, 3, 4, 5]; size=18, name="Arial", color="FF040404")
+ XLSX.setFont(sheet, 1:3, [1, 3]; size=12, name="Aptos", color="FF040408")
+ XLSX.setFont(sheet, [4, 5], [2, 4]; size=6, name="Courier New", color="FF040400")
+ @test XLSX.getFont(sheet, "A4").font == Dict("sz" => Dict("val" => "18"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF040404"))
+ @test XLSX.getFont(sheet, "A3").font == Dict("sz" => Dict("val" => "12"), "name" => Dict("val" => "Aptos"), "color" => Dict("rgb" => "FF040408"))
+ @test XLSX.getFont(f, "Sheet1!D5").font == Dict("sz" => Dict("val" => "6"), "name" => Dict("val" => "Courier New"), "color" => Dict("rgb" => "FF040400"))
+ @test_throws XLSX.XLSXError XLSX.setFont(sheet, "1:10"; size=18, name="Arial", color="FF040404")
+ @test_throws XLSX.XLSXError XLSX.setFont(sheet, "A:K"; size=18, name="Arial", color="FF040404")
+ @test_throws XLSX.XLSXError XLSX.setFont(f, "Sheet1!garbage"; size=18, name="Arial", color="FF040404")
+ @test_throws XLSX.XLSXError XLSX.setFont(sheet, "Sheet1!garbage"; size=18, name="Arial", color="FF040404")
+ @test_throws XLSX.XLSXError XLSX.setFont(sheet, "garbage"; size=18, name="Arial", color="FF040404")
+ @test_throws XLSX.XLSXError XLSX.setFont(sheet, "garbage1:garbage2"; size=18, name="Arial", color="FF040404")
+
+ f = XLSX.newxlsx()
+ sheet = f[1]
+ sheet["A1:E5"] = ""
+ XLSX.setUniformFont(sheet, "Sheet1!A1:E1"; size=18, name="Arial", color="FF040404")
+ @test XLSX.getFont(sheet, "D1").font == Dict("sz" => Dict("val" => "18"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF040404"))
+ XLSX.setUniformFont(sheet, "Sheet1!2:3"; size=18, name="Arial", color="FF040408")
+ @test XLSX.getFont(sheet, "A3").font == Dict("sz" => Dict("val" => "18"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF040408"))
+ XLSX.setUniformFont(sheet, "Sheet1!D:E"; size=18, name="Arial", color="FF040400")
+ @test XLSX.getFont(sheet, "E5").font == Dict("sz" => Dict("val" => "18"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF040400"))
+ XLSX.setUniformFont(sheet, "A1:E1"; size=18, name="Arial", color="FF040304")
+ @test XLSX.getFont(sheet, "D1").font == Dict("sz" => Dict("val" => "18"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF040304"))
+ XLSX.setUniformFont(sheet, "2:3"; size=18, name="Arial", color="FF040308")
+ @test XLSX.getFont(sheet, "A3").font == Dict("sz" => Dict("val" => "18"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF040308"))
+ XLSX.setUniformFont(sheet, "D:E"; size=18, name="Arial", color="FF040300")
+ @test XLSX.getFont(sheet, "E5").font == Dict("sz" => Dict("val" => "18"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF040300"))
+
+ f = XLSX.newxlsx()
+ sheet = f[1]
+ sheet["A1:E5"] = ""
+ XLSX.setUniformFont(sheet, :, 1; size=18, name="Arial", color="FF040404")
+ @test XLSX.getFont(sheet, "A3").font == Dict("sz" => Dict("val" => "18"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF040404"))
+ XLSX.setUniformFont(sheet, :, [2, 3]; size=18, name="Arial", color="FF040400")
+ @test XLSX.getFont(sheet, "C4").font == Dict("sz" => Dict("val" => "18"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF040400"))
+ XLSX.setUniformFont(sheet, [1, 3, 4], 5; size=18, name="Arial", color="FF040300")
+ @test XLSX.getFont(sheet, "E1").font == Dict("sz" => Dict("val" => "18"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF040300"))
+ XLSX.setUniformFont(sheet, 5, [3, 4]; size=18, name="Arial", color="FF030300")
+ @test XLSX.getFont(sheet, "D5").font == Dict("sz" => Dict("val" => "18"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF030300"))
+ XLSX.setUniformFont(sheet, [2, 3, 4], [3, 4]; size=18, name="Arial", color="FF030308")
+ @test XLSX.getFont(sheet, "C3").font == Dict("sz" => Dict("val" => "18"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF030308"))
+ XLSX.setUniformFont(sheet, 4:5, 4; size=18, name="Arial", color="FF030408")
+ @test XLSX.getFont(sheet, "D4").font == Dict("sz" => Dict("val" => "18"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF030408"))
+ XLSX.setUniformFont(sheet, :; size=8, name="Arial", color="FF030408")
+ @test XLSX.getFont(sheet, "D4").font == Dict("sz" => Dict("val" => "8"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF030408"))
+ XLSX.setUniformFont(sheet, :, :; size=28, name="Arial", color="FF030408")
+ @test XLSX.getFont(sheet, "D4").font == Dict("sz" => Dict("val" => "28"), "name" => Dict("val" => "Arial"), "color" => Dict("rgb" => "FF030408"))
+ @test_throws XLSX.XLSXError XLSX.setUniformFont(sheet, :, [1, 3, 10, 15]; size=18, name="Arial", color="FF040404")
+ @test_throws XLSX.XLSXError XLSX.setUniformFont(sheet, [1, 3, 10, 15], :; size=18, name="Arial", color="FF040404")
+ @test_throws XLSX.XLSXError XLSX.setUniformFont(sheet, 1, [1, 3, 10, 15]; size=18, name="Arial", color="FF040404")
+ @test_throws XLSX.XLSXError XLSX.setUniformFont(sheet, [1, 3, 10, 15], 2:3; size=18, name="Arial", color="FF040404")
+ @test_throws XLSX.XLSXError XLSX.setUniformFont(f, "Sheet1!garbage"; size=18, name="Arial", color="FF040404")
+ @test_throws XLSX.XLSXError XLSX.setUniformFont(sheet, "Sheet1!garbage"; size=18, name="Arial", color="FF040404")
+ @test_throws XLSX.XLSXError XLSX.setUniformFont(sheet, "garbage"; size=18, name="Arial", color="FF040404")
+ @test_throws XLSX.XLSXError XLSX.setUniformFont(sheet, "garbage1:garbage2"; size=18, name="Arial", color="FF040404")
end
@testset "setBorder" begin
f = XLSX.open_xlsx_template(joinpath(data_directory, "Borders.xlsx"))
s = f["Sheet1"]
+ @test XLSX.getDefaultBorders(s).border == Dict("left" => nothing, "bottom" => nothing, "right" => nothing, "top" => nothing, "diagonal" => nothing)
+
@test isnothing(XLSX.getBorder(s, "A1"))
@test XLSX.getBorder(s, "B2").border == Dict("left" => Dict("auto" => "1", "style" => "medium"), "bottom" => Dict("auto" => "1", "style" => "medium"), "right" => Dict("auto" => "1", "style" => "medium"), "top" => Dict("auto" => "1", "style" => "medium"), "diagonal" => nothing)
@@ -1573,34 +2216,96 @@ end
@test XLSX.getBorder(f, "Sheet1!D6").border == Dict("left" => Dict("auto" => "1", "style" => "thick"), "bottom" => Dict("auto" => "1", "style" => "thick"), "right" => Dict("auto" => "1", "style" => "thick"), "top" => Dict("auto" => "1", "style" => "thick"), "diagonal" => nothing)
XLSX.setBorder(f, "Sheet1!D6"; left=["style" => "dotted", "color" => "FF000FF0"], right=["style" => "medium", "color" => "FF765000"], top=["style" => "thick", "color" => "FF230000"], bottom=["style" => "medium", "color" => "FF0000FF"], diagonal=["style" => "dotted", "color" => "FF00D4D4"])
- @test XLSX.getBorder(s, "D6").border == Dict("left" => Dict("rgb" => "FF000FF0", "style" => "dotted"), "bottom" => Dict("rgb" => "FF0000FF", "style" => "medium"), "right" => Dict("rgb" => "FF765000", "style" => "medium"), "top" => Dict("rgb" => "FF230000", "style" => "thick"), "diagonal" => Dict("rgb" => "FF00D4D4", "style" => "dotted"))
+ @test XLSX.getBorder(s, "D6").border == Dict("left" => Dict("rgb" => "FF000FF0", "style" => "dotted"), "bottom" => Dict("rgb" => "FF0000FF", "style" => "medium"), "right" => Dict("rgb" => "FF765000", "style" => "medium"), "top" => Dict("rgb" => "FF230000", "style" => "thick"), "diagonal" => Dict("rgb" => "FF00D4D4", "style" => "dotted", "direction" => "both"))
XLSX.setBorder(f, "Sheet1!B2:D4"; left=["style" => "hair"], right=["color" => "FF111111"], top=["style" => "hair"], bottom=["color" => "FF111111"], diagonal=["style" => "hair"])
- @test XLSX.getBorder(s, "B2").border == Dict("left" => Dict("auto" => "1", "style" => "hair"), "bottom" => Dict("rgb" => "FF111111", "style" => "medium"), "right" => Dict("rgb" => "FF111111", "style" => "medium"), "top" => Dict("auto" => "1", "style" => "hair"), "diagonal" => Dict("style" => "hair"))
- @test XLSX.getBorder(f, "Sheet1!D4").border == Dict("left" => Dict("theme" => "3", "style" => "hair", "tint" => "0.24994659260841701"), "bottom" => Dict("rgb" => "FF111111", "style" => "dashed"), "right" => Dict("rgb" => "FF111111", "style" => "dashed"), "top" => Dict("theme" => "3", "style" => "hair", "tint" => "0.24994659260841701"), "diagonal" => Dict("style" => "hair"))
-
- XLSX.setBorder(f, "Sheet1!A1:D11"; left=["style" => "hair", "color" => "FF111111"], right=["style" => "hair", "color" => "FF111111"], top=["style" => "hair", "color" => "FF111111"], bottom=["style" => "hair", "color" => "FF111111"], diagonal=["style" => "hair", "color" => "FF111111"])
- @test XLSX.getBorder(s, "B4").border == Dict("left" => Dict("rgb" => "FF111111", "style" => "hair"), "bottom" => Dict("rgb" => "FF111111", "style" => "hair"), "right" => Dict("rgb" => "FF111111", "style" => "hair"), "top" => Dict("rgb" => "FF111111", "style" => "hair"), "diagonal" => Dict("rgb" => "FF111111", "style" => "hair"))
- @test XLSX.getBorder(s, "B6").border == Dict("left" => Dict("rgb" => "FF111111", "style" => "hair"), "bottom" => Dict("rgb" => "FF111111", "style" => "hair"), "right" => Dict("rgb" => "FF111111", "style" => "hair"), "top" => Dict("rgb" => "FF111111", "style" => "hair"), "diagonal" => Dict("rgb" => "FF111111", "style" => "hair"))
- @test XLSX.getBorder(s, "D4").border == Dict("left" => Dict("rgb" => "FF111111", "style" => "hair"), "bottom" => Dict("rgb" => "FF111111", "style" => "hair"), "right" => Dict("rgb" => "FF111111", "style" => "hair"), "top" => Dict("rgb" => "FF111111", "style" => "hair"), "diagonal" => Dict("rgb" => "FF111111", "style" => "hair"))
- @test XLSX.getBorder(s, "D8").border == Dict("left" => Dict("rgb" => "FF111111", "style" => "hair"), "bottom" => Dict("rgb" => "FF111111", "style" => "hair"), "right" => Dict("rgb" => "FF111111", "style" => "hair"), "top" => Dict("rgb" => "FF111111", "style" => "hair"), "diagonal" => Dict("rgb" => "FF111111", "style" => "hair"))
- @test XLSX.getBorder(s, "A1").border == Dict("left" => Dict("rgb" => "FF111111", "style" => "hair"), "bottom" => Dict("rgb" => "FF111111", "style" => "hair"), "right" => Dict("rgb" => "FF111111", "style" => "hair"), "top" => Dict("rgb" => "FF111111", "style" => "hair"), "diagonal" => Dict("rgb" => "FF111111", "style" => "hair"))
- @test XLSX.getBorder(s, "D10").border == Dict("left" => Dict("rgb" => "FF111111", "style" => "hair"), "bottom" => Dict("rgb" => "FF111111", "style" => "hair"), "right" => Dict("rgb" => "FF111111", "style" => "hair"), "top" => Dict("rgb" => "FF111111", "style" => "hair"), "diagonal" => Dict("rgb" => "FF111111", "style" => "hair"))
+ @test XLSX.getBorder(s, "B2").border == Dict("left" => Dict("auto" => "1", "style" => "hair"), "bottom" => Dict("rgb" => "FF111111", "style" => "medium"), "right" => Dict("rgb" => "FF111111", "style" => "medium"), "top" => Dict("auto" => "1", "style" => "hair"), "diagonal" => Dict("style" => "hair", "direction" => "both"))
+ @test XLSX.getBorder(f, "Sheet1!D4").border == Dict("left" => Dict("theme" => "3", "style" => "hair", "tint" => "0.24994659260841701"), "bottom" => Dict("rgb" => "FF111111", "style" => "dashed"), "right" => Dict("rgb" => "FF111111", "style" => "dashed"), "top" => Dict("theme" => "3", "style" => "hair", "tint" => "0.24994659260841701"), "diagonal" => Dict("style" => "hair", "direction" => "both"))
+
+ XLSX.setBorder(f, "Sheet1!A1:D10"; left=["style" => "hair", "color" => "FF111111"], right=["style" => "hair", "color" => "FF111111"], top=["style" => "hair", "color" => "FF111111"], bottom=["style" => "hair", "color" => "FF111111"], diagonal=["style" => "hair", "color" => "FF111111"])
+ @test XLSX.getBorder(s, "B4").border == Dict("left" => Dict("rgb" => "FF111111", "style" => "hair"), "bottom" => Dict("rgb" => "FF111111", "style" => "hair"), "right" => Dict("rgb" => "FF111111", "style" => "hair"), "top" => Dict("rgb" => "FF111111", "style" => "hair"), "diagonal" => Dict("rgb" => "FF111111", "style" => "hair", "direction" => "both"))
+ @test XLSX.getBorder(s, "B6").border == Dict("left" => Dict("rgb" => "FF111111", "style" => "hair"), "bottom" => Dict("rgb" => "FF111111", "style" => "hair"), "right" => Dict("rgb" => "FF111111", "style" => "hair"), "top" => Dict("rgb" => "FF111111", "style" => "hair"), "diagonal" => Dict("rgb" => "FF111111", "style" => "hair", "direction" => "both"))
+ @test XLSX.getBorder(s, "D4").border == Dict("left" => Dict("rgb" => "FF111111", "style" => "hair"), "bottom" => Dict("rgb" => "FF111111", "style" => "hair"), "right" => Dict("rgb" => "FF111111", "style" => "hair"), "top" => Dict("rgb" => "FF111111", "style" => "hair"), "diagonal" => Dict("rgb" => "FF111111", "style" => "hair", "direction" => "both"))
+ @test XLSX.getBorder(s, "D8").border == Dict("left" => Dict("rgb" => "FF111111", "style" => "hair"), "bottom" => Dict("rgb" => "FF111111", "style" => "hair"), "right" => Dict("rgb" => "FF111111", "style" => "hair"), "top" => Dict("rgb" => "FF111111", "style" => "hair"), "diagonal" => Dict("rgb" => "FF111111", "style" => "hair", "direction" => "up"))
+ @test XLSX.getBorder(s, "A1").border == Dict("left" => Dict("rgb" => "FF111111", "style" => "hair"), "bottom" => Dict("rgb" => "FF111111", "style" => "hair"), "right" => Dict("rgb" => "FF111111", "style" => "hair"), "top" => Dict("rgb" => "FF111111", "style" => "hair"), "diagonal" => Dict("rgb" => "FF111111", "style" => "hair", "direction" => "both"))
+ @test XLSX.getBorder(s, "D10").border == Dict("left" => Dict("rgb" => "FF111111", "style" => "hair"), "bottom" => Dict("rgb" => "FF111111", "style" => "hair"), "right" => Dict("rgb" => "FF111111", "style" => "hair"), "top" => Dict("rgb" => "FF111111", "style" => "hair"), "diagonal" => Dict("rgb" => "FF111111", "style" => "hair", "direction" => "both"))
@test XLSX.getcell(s, "D11") isa XLSX.EmptyCell
- @test isnothing(XLSX.getBorder(s, "D11")) # Cannot set a border in an EmptyCell (outside sheet dimension).
+ @test_throws XLSX.XLSXError XLSX.getBorder(s, "D11") # Cannot get a border outside sheet dimension.
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s[1:6, 1:6] = ""
+ XLSX.setBorder(s, "B2:E5"; outside=["color" => "FFFF0000", "style" => "thick"])
+ @test XLSX.getBorder(s, "B2").border == Dict("left" => Dict("rgb" => "FFFF0000", "style" => "thick"), "bottom" => nothing, "right" => nothing, "top" => Dict("rgb" => "FFFF0000", "style" => "thick"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, "B3").border == Dict("left" => Dict("rgb" => "FFFF0000", "style" => "thick"), "bottom" => nothing, "right" => nothing, "top" => nothing, "diagonal" => nothing)
+ @test XLSX.getBorder(s, "B4").border == Dict("left" => Dict("rgb" => "FFFF0000", "style" => "thick"), "bottom" => nothing, "right" => nothing, "top" => nothing, "diagonal" => nothing)
+ @test XLSX.getBorder(s, "B5").border == Dict("left" => Dict("rgb" => "FFFF0000", "style" => "thick"), "bottom" => Dict("rgb" => "FFFF0000", "style" => "thick"), "right" => nothing, "top" => nothing, "diagonal" => nothing)
+ @test XLSX.getBorder(s, "C2").border == Dict("left" => nothing, "bottom" => nothing, "right" => nothing, "top" => Dict("rgb" => "FFFF0000", "style" => "thick"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, "C3") === nothing
+ @test XLSX.getBorder(s, "C4") === nothing
+ @test XLSX.getBorder(s, "C5").border == Dict("left" => nothing, "bottom" => Dict("rgb" => "FFFF0000", "style" => "thick"), "right" => nothing, "top" => nothing, "diagonal" => nothing)
+ @test XLSX.getBorder(s, "D2").border == Dict("left" => nothing, "bottom" => nothing, "right" => nothing, "top" => Dict("rgb" => "FFFF0000", "style" => "thick"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, "D3") === nothing
+ @test XLSX.getBorder(s, "D4") === nothing
+ @test XLSX.getBorder(s, "D5").border == Dict("left" => nothing, "bottom" => Dict("rgb" => "FFFF0000", "style" => "thick"), "right" => nothing, "top" => nothing, "diagonal" => nothing)
+ @test XLSX.getBorder(s, "E2").border == Dict("left" => nothing, "bottom" => nothing, "right" => Dict("rgb" => "FFFF0000", "style" => "thick"), "top" => Dict("rgb" => "FFFF0000", "style" => "thick"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, "E3").border == Dict("left" => nothing, "bottom" => nothing, "right" => Dict("rgb" => "FFFF0000", "style" => "thick"), "top" => nothing, "diagonal" => nothing)
+ @test XLSX.getBorder(s, "E4").border == Dict("left" => nothing, "bottom" => nothing, "right" => Dict("rgb" => "FFFF0000", "style" => "thick"), "top" => nothing, "diagonal" => nothing)
+ @test XLSX.getBorder(s, "E5").border == Dict("left" => nothing, "bottom" => Dict("rgb" => "FFFF0000", "style" => "thick"), "right" => Dict("rgb" => "FFFF0000", "style" => "thick"), "top" => nothing, "diagonal" => nothing)
+
+ XLSX.setBorder(s, "B2:E5"; outside=["color" => "dodgerblue4"])
+ @test XLSX.getBorder(s, "B2").border == Dict("left" => Dict("rgb" => "FF104E8B", "style" => "thick"), "bottom" => nothing, "right" => nothing, "top" => Dict("rgb" => "FF104E8B", "style" => "thick"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, "B3").border == Dict("left" => Dict("rgb" => "FF104E8B", "style" => "thick"), "bottom" => nothing, "right" => nothing, "top" => nothing, "diagonal" => nothing)
+ @test XLSX.getBorder(s, "B4").border == Dict("left" => Dict("rgb" => "FF104E8B", "style" => "thick"), "bottom" => nothing, "right" => nothing, "top" => nothing, "diagonal" => nothing)
+ @test XLSX.getBorder(s, "B5").border == Dict("left" => Dict("rgb" => "FF104E8B", "style" => "thick"), "bottom" => Dict("rgb" => "FF104E8B", "style" => "thick"), "right" => nothing, "top" => nothing, "diagonal" => nothing)
+ @test XLSX.getBorder(s, "C2").border == Dict("left" => nothing, "bottom" => nothing, "right" => nothing, "top" => Dict("rgb" => "FF104E8B", "style" => "thick"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, "C3") === nothing
+ @test XLSX.getBorder(s, "C4") === nothing
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s[1:6, 1:6] = ""
+ XLSX.setBorder(s, "Sheet1!A1"; allsides=["color" => "FFFF00FF", "style" => "thick"])
+ @test XLSX.getBorder(s, "A1").border == Dict("left" => Dict("rgb" => "FFFF00FF", "style" => "thick"), "bottom" => Dict("rgb" => "FFFF00FF", "style" => "thick"), "right" => Dict("rgb" => "FFFF00FF", "style" => "thick"), "top" => Dict("rgb" => "FFFF00FF", "style" => "thick"), "diagonal" => nothing)
+ XLSX.setBorder(s, "Sheet1!A1:E1"; allsides=["color" => "FFFF0000", "style" => "thick"])
+ @test XLSX.getBorder(s, "B1").border == Dict("left" => Dict("rgb" => "FFFF0000", "style" => "thick"), "bottom" => Dict("rgb" => "FFFF0000", "style" => "thick"), "right" => Dict("rgb" => "FFFF0000", "style" => "thick"), "top" => Dict("rgb" => "FFFF0000", "style" => "thick"), "diagonal" => nothing)
+ XLSX.setBorder(s, "Sheet1!A:E"; left=["color" => "FFFF0001", "style" => "thick"])
+ @test XLSX.getBorder(s, "B2").border == Dict("left" => Dict("rgb" => "FFFF0001", "style" => "thick"), "bottom" => nothing, "right" => nothing, "top" => nothing, "diagonal" => nothing)
+ XLSX.setBorder(s, "Sheet1!3:4"; left=["color" => "FFFF0002", "style" => "thick"])
+ @test XLSX.getBorder(s, "B4").border == Dict("left" => Dict("rgb" => "FFFF0002", "style" => "thick"), "bottom" => nothing, "right" => nothing, "top" => nothing, "diagonal" => nothing)
+ XLSX.setBorder(s, "B2,B4"; left=["color" => "FFFF0004", "style" => "thick"])
+ @test XLSX.getBorder(s, "B2").border == Dict("left" => Dict("rgb" => "FFFF0004", "style" => "thick"), "bottom" => nothing, "right" => nothing, "top" => nothing, "diagonal" => nothing)
+ @test XLSX.getBorder(s, "B4").border == Dict("left" => Dict("rgb" => "FFFF0004", "style" => "thick"), "bottom" => nothing, "right" => nothing, "top" => nothing, "diagonal" => nothing)
+ @test XLSX.getBorder(s, "B3").border == Dict("left" => Dict("rgb" => "FFFF0002", "style" => "thick"), "bottom" => nothing, "right" => nothing, "top" => nothing, "diagonal" => nothing)
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s[1:6, 1:6] = ""
+ XLSX.setBorder(s, 1, :; left=["color" => "FFFF0001", "style" => "thick"])
+ @test XLSX.getBorder(s, "B1").border == Dict("left" => Dict("rgb" => "FFFF0001", "style" => "thick"), "bottom" => nothing, "right" => nothing, "top" => nothing, "diagonal" => nothing)
+ XLSX.setBorder(s, [2, 3], :; left=["color" => "FFFF0002", "style" => "thick"])
+ @test XLSX.getBorder(s, "D3").border == Dict("left" => Dict("rgb" => "FFFF0002", "style" => "thick"), "bottom" => nothing, "right" => nothing, "top" => nothing, "diagonal" => nothing)
+ XLSX.setBorder(s, :, [2, 3]; left=["color" => "FFFF0003", "style" => "thick"])
+ @test XLSX.getBorder(s, "C4").border == Dict("left" => Dict("rgb" => "FFFF0003", "style" => "thick"), "bottom" => nothing, "right" => nothing, "top" => nothing, "diagonal" => nothing)
+ XLSX.setBorder(s, 4, [2, 3]; left=["color" => "FFFF0004", "style" => "thick"])
+ @test XLSX.getBorder(s, "B4").border == Dict("left" => Dict("rgb" => "FFFF0004", "style" => "thick"), "bottom" => nothing, "right" => nothing, "top" => nothing, "diagonal" => nothing)
+ XLSX.setBorder(s, 3:2:5, [2, 3]; left=["color" => "FFFF0005", "style" => "thick"])
+ @test XLSX.getBorder(s, "C5").border == Dict("left" => Dict("rgb" => "FFFF0005", "style" => "thick"), "bottom" => nothing, "right" => nothing, "top" => nothing, "diagonal" => nothing)
+ @test_throws XLSX.XLSXError XLSX.setFont(s, "1:10"; left=["color" => "FFFF0005", "style" => "thick"])
+ @test_throws XLSX.XLSXError XLSX.setFont(s, "A:K"; left=["color" => "FFFF0005", "style" => "thick"])
f = XLSX.open_xlsx_template(joinpath(data_directory, "Borders.xlsx"))
s = f["Sheet1"]
- XLSX.setUniformBorder(f, "Sheet1!A1:D4"; left=["style" => "dotted", "color" => "FF000FF0"],
+ XLSX.setUniformBorder(f, "Sheet1!A1:D4"; left=["style" => "dotted", "color" => "darkseagreen3"],
right=["style" => "medium", "color" => "FF765000"],
top=["style" => "thick", "color" => "FF230000"],
bottom=["style" => "medium", "color" => "FF0000FF"],
diagonal=["style" => "none"]
)
- @test XLSX.getBorder(s, "A1").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF000FF0"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
- @test XLSX.getBorder(f, "Sheet1!B2").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF000FF0"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
- @test XLSX.getBorder(f, "Sheet1!D4").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF000FF0"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, "A1").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF9BCD9B"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+ @test XLSX.getBorder(f, "Sheet1!B2").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF9BCD9B"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+ @test XLSX.getBorder(f, "Sheet1!D4").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF9BCD9B"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
@test XLSX.getcell(s, "C3") isa XLSX.EmptyCell
@test isnothing(XLSX.getBorder(s, "C3"))
@@ -1608,48 +2313,246 @@ end
f = XLSX.open_xlsx_template(joinpath(data_directory, "customXml.xlsx"))
s = f["Mock-up"]
- XLSX.setBorder(s, "ID"; left=["style" => "dotted", "color" => "FF000FF0"], bottom=["style" => "medium", "color" => "FF0000FF"], right=["style" => "medium", "color" => "FF765000"], top=["style" => "thick", "color" => "FF230000"], diagonal=nothing)
- @test XLSX.getBorder(s, "ID").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF000FF0"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+ # Sheet mismatch
+ @test_throws XLSX.XLSXError XLSX.setUniformBorder(s, "Document History!A1:D4"; left=["style" => "dotted", "color" => "darkseagreen3"],
+ right=["style" => "medium", "color" => "FF765000"],
+ top=["style" => "thick", "color" => "FF230000"],
+ bottom=["style" => "medium", "color" => "FF0000FF"],
+ diagonal=["style" => "none"]
+ )
+
+ @test XLSX.setUniformBorder(s, "Mock-up!A1:B4,Mock-up!D4:E6"; left=["style" => "dotted", "color" => "darkseagreen3"],
+ right=["style" => "medium", "color" => "FF765000"],
+ top=["style" => "thick", "color" => "FF230000"],
+ bottom=["style" => "medium", "color" => "FF0000FF"],
+ diagonal=["style" => "none"]) == 28
+
+ XLSX.setBorder(s, "ID"; left=["style" => "dotted", "color" => "grey36"], bottom=["style" => "medium", "color" => "FF0000FF"], right=["style" => "medium", "color" => "FF765000"], top=["style" => "thick", "color" => "FF230000"], diagonal=nothing)
+ @test XLSX.getBorder(s, "ID").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF5C5C5C"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
# Location is a non-contiguous range
- XLSX.setBorder(s, "Location"; left=["style" => "hair", "color" => "FF111111"], right=["style" => "hair", "color" => "FF111111"], top=["style" => "hair", "color" => "FF111111"], bottom=["style" => "hair", "color" => "FF111111"], diagonal=["style" => "hair", "color" => "FF111111"])
- @test XLSX.getBorder(s, "D18").border == Dict("left" => Dict("rgb" => "FF111111", "style" => "hair"), "bottom" => Dict("rgb" => "FF111111", "style" => "hair"), "right" => Dict("rgb" => "FF111111", "style" => "hair"), "top" => Dict("rgb" => "FF111111", "style" => "hair"), "diagonal" => Dict("rgb" => "FF111111", "style" => "hair"))
- @test XLSX.getBorder(s, "D20").border == Dict("left" => Dict("rgb" => "FF111111", "style" => "hair"), "bottom" => Dict("rgb" => "FF111111", "style" => "hair"), "right" => Dict("rgb" => "FF111111", "style" => "hair"), "top" => Dict("rgb" => "FF111111", "style" => "hair"), "diagonal" => Dict("rgb" => "FF111111", "style" => "hair"))
- @test XLSX.getBorder(s, "J18").border == Dict("left" => Dict("rgb" => "FF111111", "style" => "hair"), "bottom" => Dict("rgb" => "FF111111", "style" => "hair"), "right" => Dict("rgb" => "FF111111", "style" => "hair"), "top" => Dict("rgb" => "FF111111", "style" => "hair"), "diagonal" => Dict("rgb" => "FF111111", "style" => "hair"))
- @test XLSX.getBorder(s, "J18").border == Dict("left" => Dict("rgb" => "FF111111", "style" => "hair"), "bottom" => Dict("rgb" => "FF111111", "style" => "hair"), "right" => Dict("rgb" => "FF111111", "style" => "hair"), "top" => Dict("rgb" => "FF111111", "style" => "hair"), "diagonal" => Dict("rgb" => "FF111111", "style" => "hair"))
+ XLSX.setBorder(s, "Location"; left=["style" => "hair", "color" => "chocolate4"], right=["style" => "hair", "color" => "chocolate4"], top=["style" => "hair", "color" => "chocolate4"], bottom=["style" => "hair", "color" => "chocolate4"], diagonal=["style" => "hair", "color" => "chocolate4"])
+ @test XLSX.getBorder(s, "D18").border == Dict("left" => Dict("rgb" => "FF8B4513", "style" => "hair"), "bottom" => Dict("rgb" => "FF8B4513", "style" => "hair"), "right" => Dict("rgb" => "FF8B4513", "style" => "hair"), "top" => Dict("rgb" => "FF8B4513", "style" => "hair"), "diagonal" => Dict("rgb" => "FF8B4513", "style" => "hair", "direction" => "both"))
+ @test XLSX.getBorder(s, "D20").border == Dict("left" => Dict("rgb" => "FF8B4513", "style" => "hair"), "bottom" => Dict("rgb" => "FF8B4513", "style" => "hair"), "right" => Dict("rgb" => "FF8B4513", "style" => "hair"), "top" => Dict("rgb" => "FF8B4513", "style" => "hair"), "diagonal" => Dict("rgb" => "FF8B4513", "style" => "hair", "direction" => "both"))
+ @test XLSX.getBorder(s, "J18").border == Dict("left" => Dict("rgb" => "FF8B4513", "style" => "hair"), "bottom" => Dict("rgb" => "FF8B4513", "style" => "hair"), "right" => Dict("rgb" => "FF8B4513", "style" => "hair"), "top" => Dict("rgb" => "FF8B4513", "style" => "hair"), "diagonal" => Dict("rgb" => "FF8B4513", "style" => "hair", "direction" => "both"))
+ @test XLSX.getBorder(s, "J20").border == Dict("left" => Dict("rgb" => "FF8B4513", "style" => "hair"), "bottom" => Dict("rgb" => "FF8B4513", "style" => "hair"), "right" => Dict("rgb" => "FF8B4513", "style" => "hair"), "top" => Dict("rgb" => "FF8B4513", "style" => "hair"), "diagonal" => Dict("rgb" => "FF8B4513", "style" => "hair", "direction" => "both"))
- # Cant get attributes on a range.
- @test_throws AssertionError XLSX.getBorder(s, "Contiguous")
+ # Can't get attributes on a range.
+ @test_throws XLSX.XLSXError XLSX.getBorder(s, "Contiguous")
f = XLSX.open_empty_template()
s = f["Sheet1"]
-
- # All these cells are `EmptyCells`
- @test XLSX.setUniformFont(s, "A1:B4"; size=12, name="Times New Roman", color="FF040404") == -1
- @test XLSX.setUniformBorder(f, "Sheet1!A1:D4"; left=["style" => "dotted", "color" => "FF000FF0"],
+ s["A1"] = ""
+ s["F21"] = ""
+ # All these cells are empty.
+ @test XLSX.setUniformFont(s, "A2:B4"; size=12, name="Times New Roman", color="chocolate4") == -1
+ @test XLSX.setUniformBorder(f, "Sheet1!A2:D4"; left=["style" => "dotted", "color" => "chocolate4"],
right=["style" => "medium", "color" => "FF765000"],
top=["style" => "thick", "color" => "FF230000"],
- bottom=["style" => "medium", "color" => "FF0000FF"],
+ bottom=["style" => "medium", "color" => "chocolate4"],
diagonal=["style" => "none"]
) == -1
@test XLSX.setUniformFill(s, "B2:D4"; pattern="gray125", bgColor="FF000000") == -1
- @test XLSX.setFont(s, "A1:B2"; size=18, name="Arial") == -1
- @test XLSX.setBorder(f, "Sheet1!B2:D4"; left=["style" => "hair"], right=["color" => "FF111111"], top=["style" => "hair"], bottom=["color" => "FF111111"], diagonal=["style" => "hair"]) == -1
- @test XLSX.setFill(f, "Sheet1!A1:F20"; pattern="none", fgColor="88FF8800") == -1
+ @test XLSX.setFont(s, "A2:F20"; size=18, name="Arial") == -1
+ @test XLSX.setBorder(f, "Sheet1!B2:D4"; left=["style" => "hair"], right=["color" => "FF8B4513"], top=["style" => "hair"], bottom=["color" => "chocolate4"], diagonal=["style" => "hair"]) == -1
+ @test XLSX.setAlignment(s, "A2:F20"; horizontal="right", wrapText=true) == -1
+ @test XLSX.setUniformFill(s, [2, 4], 2:4; pattern="gray125", bgColor="FF000000") == -1
+ @test XLSX.setFont(s, [2, 4], 2:4; size=18, name="Arial") == -1
+ @test XLSX.setBorder(s, [2, 4], :; left=["style" => "hair"], right=["color" => "FF8B4513"], top=["style" => "hair"], bottom=["color" => "chocolate4"], diagonal=["style" => "hair"]) == -1
+ @test XLSX.setAlignment(s, [2, 4], 2:4; horizontal="right", wrapText=true) == -1
+ @test XLSX.setUniformFill(s, "B2,C2"; pattern="gray125", bgColor="FF000000") == -1
+ @test XLSX.setFont(s, "A2,A4"; size=18, name="Arial") == -1
+ @test XLSX.setBorder(f, "Sheet1!B2,Sheet1!C2"; left=["style" => "hair"], right=["color" => "FF8B4513"], top=["style" => "hair"], bottom=["color" => "chocolate4"], diagonal=["style" => "hair"]) == -1
+ @test XLSX.setAlignment(s, "A2,B3:C4"; horizontal="right", wrapText=true) == -1
+ @test XLSX.setUniformAlignment(s, "B2,D2"; horizontal="right", wrapText=true) == -1
+ @test XLSX.setUniformStyle(s, "B2:D2,E3") ==-1
+ @test_throws XLSX.XLSXError XLSX.setUniformFill(s, "B2,B2"; pattern="gray125", bgColor="FF000000")
+ @test_throws XLSX.XLSXError XLSX.setFont(s, "A2,A2"; size=18, name="Arial")
+ @test_throws XLSX.XLSXError XLSX.setBorder(f, "Sheet1!B2,Sheet1!B2"; left=["style" => "hair"], right=["color" => "FF8B4513"], top=["style" => "hair"], bottom=["color" => "chocolate4"], diagonal=["style" => "hair"])
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, "A2,A2:A2"; horizontal="right", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setUniformAlignment(s, "B2,B2"; horizontal="right", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setUniformStyle(s, "B2:B2,B2")
+
+ f = XLSX.open_empty_template()
+ s = f["Sheet1"]
+ # All these cells are outside the sheet dimension.
+ @test_throws XLSX.XLSXError XLSX.setUniformFont(s, "A1:B4"; size=12, name="Times New Roman", color="chocolate4")
+ @test_throws XLSX.XLSXError XLSX.setUniformBorder(f, "Sheet1!A1:D4"; left=["style" => "dotted", "color" => "chocolate4"],
+ right=["style" => "medium", "color" => "FF765000"],
+ top=["style" => "thick", "color" => "FF230000"],
+ bottom=["style" => "medium", "color" => "chocolate4"],
+ diagonal=["style" => "none"]
+ )
+ @test_throws XLSX.XLSXError XLSX.setUniformFill(s, "B2:D4"; pattern="gray125", bgColor="FF000000")
+ @test_throws XLSX.XLSXError XLSX.setFont(s, "A1:F20"; size=18, name="Arial")
+ @test_throws XLSX.XLSXError XLSX.setBorder(f, "Sheet1!B2:D4"; left=["style" => "hair"], right=["color" => "FF8B4513"], top=["style" => "hair"], bottom=["color" => "chocolate4"], diagonal=["style" => "hair"])
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, "A1:F20"; horizontal="right", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setFill(f, "Sheet1!A1"; pattern="none", fgColor="88FF8800")
+ @test_throws XLSX.XLSXError XLSX.setFont(s, "A1"; size=18, name="Arial")
+ @test_throws XLSX.XLSXError XLSX.setBorder(f, "Sheet1!B2"; left=["style" => "hair"], right=["color" => "FF8B4513"], top=["style" => "hair"], bottom=["color" => "FF8B4513"], diagonal=["style" => "hair"])
+ @test_throws XLSX.XLSXError XLSX.setFill(s, "F20"; pattern="none", fgColor="88FF8800")
f = XLSX.open_xlsx_template(joinpath(data_directory, "customXml.xlsx"))
s = f["Mock-up"]
# Can't set a uniform attribute to a single cell.
@test_throws MethodError XLSX.setUniformFill(s, "D4"; pattern="gray125", bgColor="FF000000")
+ @test_throws MethodError XLSX.setUniformFill(s, "ID"; pattern="darkTrellis", fgColor="FF222222", bgColor="FFDDDDDD")
@test_throws MethodError XLSX.setUniformFont(s, "B4"; size=12, name="Times New Roman", color="FF040404")
+ @test_throws MethodError XLSX.setUniformBorder(f[2], "B4"; left=["style" => "hair"], right=["color" => "FF8B4513"], top=["style" => "hair"], bottom=["color" => "FF8B4513"], diagonal=["style" => "hair"])
+ @test_throws MethodError XLSX.setUniformStyle(s, "ID")
@test_throws MethodError XLSX.setUniformBorder(f, "Mock-up!D4"; left=["style" => "dotted", "color" => "FF000FF0"],
right=["style" => "medium", "color" => "FF765000"],
top=["style" => "thick", "color" => "FF230000"],
bottom=["style" => "medium", "color" => "FF0000FF"],
diagonal=["style" => "none"]
)
- @test_throws MethodError XLSX.setUniformFill(s, "ID"; pattern="darkTrellis", fgColor="FF222222", bgColor="FFDDDDDD")
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s[1:6, 1:6] = ""
+ XLSX.setUniformBorder(s, "Sheet1!A:B";
+ left=["style" => "dotted", "color" => "darkseagreen3"],
+ right=["style" => "medium", "color" => "FF765000"],
+ top=["style" => "thick", "color" => "FF230000"],
+ bottom=["style" => "medium", "color" => "FF0000FF"],
+ diagonal=["style" => "none"]
+ )
+ @test XLSX.getBorder(s, "A1").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF9BCD9B"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+ XLSX.setUniformBorder(s, "Sheet1!2:4";
+ left=["style" => "dotted", "color" => "FF9BCD9C"],
+ right=["style" => "medium", "color" => "FF765000"],
+ top=["style" => "thick", "color" => "FF230000"],
+ bottom=["style" => "medium", "color" => "FF0000FF"],
+ diagonal=["style" => "none"]
+ )
+ @test XLSX.getBorder(s, "C2").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF9BCD9C"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+ XLSX.setUniformBorder(s, "A:B";
+ left=["style" => "dotted", "color" => "FF9BCD9E"],
+ right=["style" => "medium", "color" => "FF765000"],
+ top=["style" => "thick", "color" => "FF230000"],
+ bottom=["style" => "medium", "color" => "FF0000FF"],
+ diagonal=["style" => "none"]
+ )
+ @test XLSX.getBorder(s, "B3").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF9BCD9E"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+ XLSX.setUniformBorder(s, "2:4";
+ left=["style" => "dotted", "color" => "FF9BCD9D"],
+ right=["style" => "medium", "color" => "FF765000"],
+ top=["style" => "thick", "color" => "FF230000"],
+ bottom=["style" => "medium", "color" => "FF0000FF"],
+ diagonal=["style" => "none"]
+ )
+ @test XLSX.getBorder(s, "C3").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF9BCD9D"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+ XLSX.setUniformBorder(s, 5, :;
+ left=["style" => "dotted", "color" => "FF9BCD8D"],
+ right=["style" => "medium", "color" => "FF765000"],
+ top=["style" => "thick", "color" => "FF230000"],
+ bottom=["style" => "medium", "color" => "FF0000FF"],
+ diagonal=["style" => "none"]
+ )
+ @test XLSX.getBorder(s, "F5").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF9BCD8D"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+ XLSX.setUniformBorder(s, :, 5;
+ left=["style" => "dotted", "color" => "FF9BBD8D"],
+ right=["style" => "medium", "color" => "FF765000"],
+ top=["style" => "thick", "color" => "FF230000"],
+ bottom=["style" => "medium", "color" => "FF0000FF"],
+ diagonal=["style" => "none"]
+ )
+ @test XLSX.getBorder(s, "E2").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF9BBD8D"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+ XLSX.setUniformBorder(s, :, :;
+ left=["style" => "dotted", "color" => "FF9BCD7D"],
+ right=["style" => "medium", "color" => "FF765000"],
+ top=["style" => "thick", "color" => "FF230000"],
+ bottom=["style" => "medium", "color" => "FF0000FF"],
+ diagonal=["style" => "none"]
+ )
+ @test XLSX.getBorder(s, "F5").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF9BCD7D"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+ XLSX.setUniformBorder(s, :;
+ left=["style" => "dotted", "color" => "FF9BCD6D"],
+ right=["style" => "medium", "color" => "FF765000"],
+ top=["style" => "thick", "color" => "FF230000"],
+ bottom=["style" => "medium", "color" => "FF0000FF"],
+ diagonal=["style" => "none"]
+ )
+ @test XLSX.getBorder(s, "D3").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF9BCD6D"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+ XLSX.setUniformBorder(s, [2, 3], :;
+ left=["style" => "dotted", "color" => "FF9BCE6D"],
+ right=["style" => "medium", "color" => "FF765000"],
+ top=["style" => "thick", "color" => "FF230000"],
+ bottom=["style" => "medium", "color" => "FF0000FF"],
+ diagonal=["style" => "none"]
+ )
+ @test XLSX.getBorder(s, "B2").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF9BCE6D"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+ XLSX.setUniformBorder(s, :, [2, 3];
+ left=["style" => "dotted", "color" => "FF9BCB6D"],
+ right=["style" => "medium", "color" => "FF765000"],
+ top=["style" => "thick", "color" => "FF230000"],
+ bottom=["style" => "medium", "color" => "FF0000FF"],
+ diagonal=["style" => "none"]
+ )
+ @test XLSX.getBorder(s, "C6").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF9BCB6D"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+ XLSX.setUniformBorder(s, 1, [2, 3];
+ left=["style" => "dotted", "color" => "FF8BCB6D"],
+ right=["style" => "medium", "color" => "FF765000"],
+ top=["style" => "thick", "color" => "FF230000"],
+ bottom=["style" => "medium", "color" => "FF0000FF"],
+ diagonal=["style" => "none"]
+ )
+ @test XLSX.getBorder(s, "C1").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF8BCB6D"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+ XLSX.setUniformBorder(s, [1, 2], [4, 5, 6];
+ left=["style" => "dotted", "color" => "FF6BCB6D"],
+ right=["style" => "medium", "color" => "FF765000"],
+ top=["style" => "thick", "color" => "FF230000"],
+ bottom=["style" => "medium", "color" => "FF0000FF"],
+ diagonal=["style" => "none"]
+ )
+ @test XLSX.getBorder(s, "E2").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF6BCB6D"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+ XLSX.setUniformBorder(s, 4, 4;
+ left=["style" => "dotted", "color" => "FF7BCB6D"],
+ right=["style" => "medium", "color" => "FF765000"],
+ top=["style" => "thick", "color" => "FF230000"],
+ bottom=["style" => "medium", "color" => "FF0000FF"],
+ diagonal=["style" => "none"]
+ )
+ @test XLSX.getBorder(s, "D4").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF7BCB6D"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s[1:6, 1:6] = ""
+ XLSX.setOutsideBorder(s, "Sheet1!A1:A2"; outside=["style" => "dotted", "color" => "FF003FF0"])
+ @test XLSX.getBorder(s, "A1").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF003FF0"), "bottom" => nothing, "right" => Dict("style" => "dotted", "rgb" => "FF003FF0"), "top" => Dict("style" => "dotted", "rgb" => "FF003FF0"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, "A2").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF003FF0"), "bottom" => Dict("style" => "dotted", "rgb" => "FF003FF0"), "right" => Dict("style" => "dotted", "rgb" => "FF003FF0"), "top" => nothing, "diagonal" => nothing)
+ XLSX.setOutsideBorder(s, "Sheet1!C:E"; outside=["style" => "dotted", "color" => "FF000FF0"])
+ @test XLSX.getBorder(s, "C1").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF000FF0"), "bottom" => nothing, "right" => nothing, "top" => Dict("style" => "dotted", "rgb" => "FF000FF0"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, "E6").border == Dict("left" => nothing, "bottom" => Dict("style" => "dotted", "rgb" => "FF000FF0"), "right" => Dict("style" => "dotted", "rgb" => "FF000FF0"), "top" => nothing, "diagonal" => nothing)
+ XLSX.setOutsideBorder(s, "Sheet1!3:5"; outside=["style" => "dotted", "color" => "FF000FFF"])
+ @test XLSX.getBorder(s, "A3").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF000FFF"), "bottom" => nothing, "right" => nothing, "top" => Dict("style" => "dotted", "rgb" => "FF000FFF"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, "F5").border == Dict("left" => nothing, "bottom" => Dict("style" => "dotted", "rgb" => "FF000FFF"), "right" => Dict("style" => "dotted", "rgb" => "FF000FFF"), "top" => nothing, "diagonal" => nothing)
+ XLSX.setOutsideBorder(s, "C:E"; outside=["style" => "dotted", "color" => "FFFF0FF0"])
+ @test XLSX.getBorder(s, "C1").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FFFF0FF0"), "bottom" => nothing, "right" => nothing, "top" => Dict("style" => "dotted", "rgb" => "FFFF0FF0"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, "E6").border == Dict("left" => nothing, "bottom" => Dict("style" => "dotted", "rgb" => "FFFF0FF0"), "right" => Dict("style" => "dotted", "rgb" => "FFFF0FF0"), "top" => nothing, "diagonal" => nothing)
+ XLSX.setOutsideBorder(s, "3:5"; outside=["style" => "dotted", "color" => "FFF50FFF"])
+ @test XLSX.getBorder(s, "A3").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FFF50FFF"), "bottom" => nothing, "right" => nothing, "top" => Dict("style" => "dotted", "rgb" => "FFF50FFF"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, "F5").border == Dict("left" => nothing, "bottom" => Dict("style" => "dotted", "rgb" => "FFF50FFF"), "right" => Dict("style" => "dotted", "rgb" => "FFF50FFF"), "top" => nothing, "diagonal" => nothing)
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s[1:6, 1:6] = ""
+ XLSX.setOutsideBorder(s, 1, :; outside=["style" => "dotted", "color" => "FF002FF0"])
+ @test XLSX.getBorder(s, "A1").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF002FF0"), "bottom" => Dict("style" => "dotted", "rgb" => "FF002FF0"), "right" => nothing, "top" => Dict("style" => "dotted", "rgb" => "FF002FF0"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, "F1").border == Dict("left" => nothing, "bottom" => Dict("style" => "dotted", "rgb" => "FF002FF0"), "right" => Dict("style" => "dotted", "rgb" => "FF002FF0"), "top" => Dict("style" => "dotted", "rgb" => "FF002FF0"), "diagonal" => nothing)
+ XLSX.setOutsideBorder(s, :, 1; outside=["style" => "dotted", "color" => "FF003FF0"])
+ @test XLSX.getBorder(s, "A1").border == Dict("left" => Dict("rgb" => "FF003FF0", "style" => "dotted"), "bottom" => Dict("rgb" => "FF002FF0", "style" => "dotted"), "right" => Dict("rgb" => "FF003FF0", "style" => "dotted"), "top" => Dict("rgb" => "FF003FF0", "style" => "dotted"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, "A6").border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF003FF0"), "bottom" => Dict("style" => "dotted", "rgb" => "FF003FF0"), "right" => Dict("style" => "dotted", "rgb" => "FF003FF0"), "top" => nothing, "diagonal" => nothing)
+ XLSX.setOutsideBorder(s, :, :; outside=["style" => "dotted", "color" => "FF000FF0"])
+ @test XLSX.getBorder(s, "A1").border == Dict("left" => Dict("rgb" => "FF000FF0", "style" => "dotted"), "bottom" => Dict("rgb" => "FF002FF0", "style" => "dotted"), "right" => Dict("rgb" => "FF003FF0", "style" => "dotted"), "top" => Dict("rgb" => "FF000FF0", "style" => "dotted"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, "F6").border == Dict("left" => nothing, "bottom" => Dict("style" => "dotted", "rgb" => "FF000FF0"), "right" => Dict("style" => "dotted", "rgb" => "FF000FF0"), "top" => nothing, "diagonal" => nothing)
+ XLSX.setOutsideBorder(s, :; outside=["style" => "dotted", "color" => "FF000FFF"])
+ @test XLSX.getBorder(s, "A1").border == Dict("left" => Dict("rgb" => "FF000FFF", "style" => "dotted"), "bottom" => Dict("rgb" => "FF002FF0", "style" => "dotted"), "right" => Dict("rgb" => "FF003FF0", "style" => "dotted"), "top" => Dict("rgb" => "FF000FFF", "style" => "dotted"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, "F6").border == Dict("left" => nothing, "bottom" => Dict("style" => "dotted", "rgb" => "FF000FFF"), "right" => Dict("style" => "dotted", "rgb" => "FF000FFF"), "top" => nothing, "diagonal" => nothing)
+ XLSX.setOutsideBorder(s, 1:2, 1; outside=["style" => "dotted", "color" => "FFFFFFF0"])
+ @test XLSX.getBorder(s, "A1").border == Dict("left" => Dict("rgb" => "FFFFFFF0", "style" => "dotted"), "bottom" => Dict("rgb" => "FF002FF0", "style" => "dotted"), "right" => Dict("rgb" => "FFFFFFF0", "style" => "dotted"), "top" => Dict("rgb" => "FFFFFFF0", "style" => "dotted"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, "A2").border == Dict("left" => Dict("rgb" => "FFFFFFF0", "style" => "dotted"), "bottom" => Dict("rgb" => "FFFFFFF0", "style" => "dotted"), "right" => Dict("rgb" => "FFFFFFF0", "style" => "dotted"), "top" => nothing, "diagonal" => nothing)
end
@@ -1658,6 +2561,8 @@ end
f = XLSX.open_xlsx_template(joinpath(data_directory, "customXml.xlsx"))
s = f["Mock-up"]
+ @test XLSX.getDefaultFill(s).fill == Dict("patternFill" => Dict("patternType" => "none"))
+
@test XLSX.getFill(s, "D17").fill == Dict("patternFill" => Dict("bgindexed" => "64", "patternType" => "solid", "fgtint" => "-9.9978637043366805E-2", "fgtheme" => "2"))
@test XLSX.getFill(f, "Mock-up!D18").fill == Dict("patternFill" => Dict("bgindexed" => "64", "patternType" => "solid", "fgtint" => "-0.499984740745262", "fgtheme" => "2"))
@@ -1666,15 +2571,19 @@ end
XLSX.setFill(s, "ID"; pattern="darkTrellis", fgColor="FF222222", bgColor="FFDDDDDD")
@test XLSX.getFill(s, "ID").fill == Dict("patternFill" => Dict("bgrgb" => "FFDDDDDD", "patternType" => "darkTrellis", "fgrgb" => "FF222222"))
+ @test_throws XLSX.XLSXError XLSX.setFill(s, "ID"; pattern="darkTrellis", fgColor="notAcolor", bgColor="FFDDDDDD")
+ @test_throws XLSX.XLSXError XLSX.setFill(s, "ID"; pattern="notApattern", fgColor="FF222222", bgColor="FFDDDDDD")
+ @test_throws XLSX.XLSXError XLSX.setFill(s, "ID"; pattern="darkTrellis", fgColor="FF222222", bgColor="FFDDDDDDFF")
+ @test_throws XLSX.XLSXError XLSX.setFill(s, "ID"; fgColor="FF222222", bgColor="FFDDDDDDFF")
# Location is a non-contiguous range
- XLSX.setFill(s, "Location"; pattern="lightVertical")
+ XLSX.setFill(s, "Location"; pattern="lightVertical") # Default colors unchanged
@test XLSX.getFill(s, "D18").fill == Dict("patternFill" => Dict("bgindexed" => "64", "patternType" => "lightVertical", "fgtint" => "-0.499984740745262", "fgtheme" => "2"))
@test XLSX.getFill(s, "D20").fill == Dict("patternFill" => Dict("bgindexed" => "64", "patternType" => "lightVertical", "fgtint" => "-0.499984740745262", "fgtheme" => "2"))
@test XLSX.getFill(s, "J18").fill == Dict("patternFill" => Dict("bgindexed" => "64", "patternType" => "lightVertical", "fgtint" => "-0.499984740745262", "fgtheme" => "2"))
@test XLSX.getFill(s, "J18").fill == Dict("patternFill" => Dict("bgindexed" => "64", "patternType" => "lightVertical", "fgtint" => "-0.499984740745262", "fgtheme" => "2"))
- XLSX.setFill(s, "Contiguous"; pattern="lightVertical")
+ XLSX.setFill(s, "Contiguous"; pattern="lightVertical") # Default colors unchanged
@test XLSX.getFill(s, "D23").fill == Dict("patternFill" => Dict("patternType" => "lightVertical", "bgindexed" => "64", "fgtheme" => "0"))
@test XLSX.getFill(s, "D24").fill == Dict("patternFill" => Dict("patternType" => "lightVertical", "bgindexed" => "64", "fgtheme" => "0"))
@test XLSX.getFill(s, "D25").fill == Dict("patternFill" => Dict("patternType" => "lightVertical", "bgindexed" => "64", "fgtheme" => "0"))
@@ -1682,7 +2591,7 @@ end
@test XLSX.getFill(s, "D27").fill == Dict("patternFill" => Dict("patternType" => "lightVertical", "bgindexed" => "64", "fgtheme" => "0"))
# Can't get attributes on a range.
- @test_throws AssertionError XLSX.getFill(s, "Contiguous")
+ @test_throws XLSX.XLSXError XLSX.getFill(s, "Contiguous")
XLSX.setUniformFill(s, "B3:D5"; pattern="lightGrid", fgColor="FF0000FF", bgColor="FF00FF00")
@test XLSX.getFill(s, "B3").fill == Dict("patternFill" => Dict("bgrgb" => "FF00FF00", "patternType" => "lightGrid", "fgrgb" => "FF0000FF"))
@@ -1707,7 +2616,7 @@ end
@test XLSX.getFill(s, "D27").fill == Dict("patternFill" => Dict("patternType" => "lightVertical", "bgindexed" => "64", "fgtheme" => "0"))
# Can't get attributes on a range.
- @test_throws AssertionError XLSX.getFill(s, "Contiguous")
+ @test_throws XLSX.XLSXError XLSX.getFill(s, "Contiguous")
XLSX.writexlsx("output.xlsx", f, overwrite=true)
@test isfile("output.xlsx")
@@ -1723,10 +2632,112 @@ end
isfile("output.xlsx") && rm("output.xlsx")
+ f = XLSX.newxlsx()
+ s = f[1]
+ s[1:6, 1:6] = ""
+ XLSX.setFill(s, "Sheet1!A1"; pattern="darkTrellis", fgColor="FF222222", bgColor="FFDDDDDD")
+ @test XLSX.getFill(s, "A1").fill == Dict("patternFill" => Dict("bgrgb" => "FFDDDDDD", "patternType" => "darkTrellis", "fgrgb" => "FF222222"))
+ XLSX.setFill(s, "Sheet1!A2:F2"; pattern="darkTrellis", fgColor="FF222224", bgColor="FFDDDDD4")
+ @test XLSX.getFill(s, "A2").fill == Dict("patternFill" => Dict("bgrgb" => "FFDDDDD4", "patternType" => "darkTrellis", "fgrgb" => "FF222224"))
+ XLSX.setFill(s, "Sheet1!C:D"; pattern="darkTrellis", fgColor="FF222228", bgColor="FFDDDDD8")
+ @test XLSX.getFill(s, "D4").fill == Dict("patternFill" => Dict("bgrgb" => "FFDDDDD8", "patternType" => "darkTrellis", "fgrgb" => "FF222228"))
+ XLSX.setFill(s, "Sheet1!5:6"; pattern="darkTrellis", fgColor="FF222220", bgColor="FFDDDDD0")
+ @test XLSX.getFill(s, "F5").fill == Dict("patternFill" => Dict("bgrgb" => "FFDDDDD0", "patternType" => "darkTrellis", "fgrgb" => "FF222220"))
+ XLSX.setFill(s, "Sheet1!E4:E6,Sheet1!A4"; pattern="darkTrellis", fgColor="FF422220", bgColor="FF4DDDD0")
+ @test XLSX.getFill(s, "E4").fill == Dict("patternFill" => Dict("bgrgb" => "FF4DDDD0", "patternType" => "darkTrellis", "fgrgb" => "FF422220"))
+ @test XLSX.getFill(s, "E5").fill == Dict("patternFill" => Dict("bgrgb" => "FF4DDDD0", "patternType" => "darkTrellis", "fgrgb" => "FF422220"))
+ @test XLSX.getFill(s, "E6").fill == Dict("patternFill" => Dict("bgrgb" => "FF4DDDD0", "patternType" => "darkTrellis", "fgrgb" => "FF422220"))
+ @test XLSX.getFill(s, "A4").fill == Dict("patternFill" => Dict("bgrgb" => "FF4DDDD0", "patternType" => "darkTrellis", "fgrgb" => "FF422220"))
+ XLSX.setFill(s, :, 2; pattern="darkTrellis", fgColor="FF622220", bgColor="FF6DDDD0")
+ @test XLSX.getFill(s, "B4").fill == Dict("patternFill" => Dict("bgrgb" => "FF6DDDD0", "patternType" => "darkTrellis", "fgrgb" => "FF622220"))
+ XLSX.setFill(s, [2, 6], :; pattern="darkTrellis", fgColor="FF622222", bgColor="FF6DDDD2")
+ @test XLSX.getFill(s, "C2").fill == Dict("patternFill" => Dict("bgrgb" => "FF6DDDD2", "patternType" => "darkTrellis", "fgrgb" => "FF622222"))
+ @test XLSX.getFill(s, "F6").fill == Dict("patternFill" => Dict("bgrgb" => "FF6DDDD2", "patternType" => "darkTrellis", "fgrgb" => "FF622222"))
+ XLSX.setFill(s, :, [2, 5]; pattern="darkTrellis", fgColor="FF622224", bgColor="FF6DDDD4")
+ @test XLSX.getFill(s, "B2").fill == Dict("patternFill" => Dict("bgrgb" => "FF6DDDD4", "patternType" => "darkTrellis", "fgrgb" => "FF622224"))
+ @test XLSX.getFill(s, "E3").fill == Dict("patternFill" => Dict("bgrgb" => "FF6DDDD4", "patternType" => "darkTrellis", "fgrgb" => "FF622224"))
+ XLSX.setFill(s, 2, [3, 6]; pattern="darkTrellis", fgColor="FF622226", bgColor="FF6DDDD6")
+ @test XLSX.getFill(s, "C2").fill == Dict("patternFill" => Dict("bgrgb" => "FF6DDDD6", "patternType" => "darkTrellis", "fgrgb" => "FF622226"))
+ XLSX.setFill(s, 2:2:6, [4, 5]; pattern="darkTrellis", fgColor="FF622228", bgColor="FF6DDDD8")
+ @test XLSX.getFill(s, "E4").fill == Dict("patternFill" => Dict("bgrgb" => "FF6DDDD8", "patternType" => "darkTrellis", "fgrgb" => "FF622228"))
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s[1:6, 1:6] = ""
+ XLSX.setUniformFill(s, "Sheet1!A2:F2"; pattern="darkTrellis", fgColor="FF222224", bgColor="FFDDDDD4")
+ @test XLSX.getFill(s, "A2").fill == Dict("patternFill" => Dict("bgrgb" => "FFDDDDD4", "patternType" => "darkTrellis", "fgrgb" => "FF222224"))
+ XLSX.setUniformFill(s, "Sheet1!C:D"; pattern="darkTrellis", fgColor="FF222228", bgColor="FFDDDDD8")
+ @test XLSX.getFill(s, "D4").fill == Dict("patternFill" => Dict("bgrgb" => "FFDDDDD8", "patternType" => "darkTrellis", "fgrgb" => "FF222228"))
+ XLSX.setUniformFill(s, "Sheet1!5:6"; pattern="darkTrellis", fgColor="FF222220", bgColor="FFDDDDD0")
+ @test XLSX.getFill(s, "F5").fill == Dict("patternFill" => Dict("bgrgb" => "FFDDDDD0", "patternType" => "darkTrellis", "fgrgb" => "FF222220"))
+ XLSX.setUniformFill(s, "A2:F2"; pattern="darkTrellis", fgColor="FF222224", bgColor="FFDDDDD4")
+ @test XLSX.getFill(s, "A2").fill == Dict("patternFill" => Dict("bgrgb" => "FFDDDDD4", "patternType" => "darkTrellis", "fgrgb" => "FF222224"))
+ XLSX.setUniformFill(s, "C:D"; pattern="darkTrellis", fgColor="FF222228", bgColor="FFDDDDD8")
+ @test XLSX.getFill(s, "D4").fill == Dict("patternFill" => Dict("bgrgb" => "FFDDDDD8", "patternType" => "darkTrellis", "fgrgb" => "FF222228"))
+ XLSX.setUniformFill(s, "5:6"; pattern="darkTrellis", fgColor="FF222220", bgColor="FFDDDDD0")
+ @test XLSX.getFill(s, "F5").fill == Dict("patternFill" => Dict("bgrgb" => "FFDDDDD0", "patternType" => "darkTrellis", "fgrgb" => "FF222220"))
+ XLSX.setUniformFill(s, "E4:E6,A4"; pattern="darkTrellis", fgColor="FF422220", bgColor="FF4DDDD0")
+ @test XLSX.getFill(s, "E4").fill == Dict("patternFill" => Dict("bgrgb" => "FF4DDDD0", "patternType" => "darkTrellis", "fgrgb" => "FF422220"))
+ @test XLSX.getFill(s, "E5").fill == Dict("patternFill" => Dict("bgrgb" => "FF4DDDD0", "patternType" => "darkTrellis", "fgrgb" => "FF422220"))
+ @test XLSX.getFill(s, "E6").fill == Dict("patternFill" => Dict("bgrgb" => "FF4DDDD0", "patternType" => "darkTrellis", "fgrgb" => "FF422220"))
+ @test XLSX.getFill(s, "A4").fill == Dict("patternFill" => Dict("bgrgb" => "FF4DDDD0", "patternType" => "darkTrellis", "fgrgb" => "FF422220"))
+ XLSX.setUniformFill(s, :, 2; pattern="darkTrellis", fgColor="FF622220", bgColor="FF6DDDD0")
+ @test XLSX.getFill(s, "B4").fill == Dict("patternFill" => Dict("bgrgb" => "FF6DDDD0", "patternType" => "darkTrellis", "fgrgb" => "FF622220"))
+ XLSX.setUniformFill(s, [2, 6], :; pattern="darkTrellis", fgColor="FF622222", bgColor="FF6DDDD2")
+ @test XLSX.getFill(s, "C2").fill == Dict("patternFill" => Dict("bgrgb" => "FF6DDDD2", "patternType" => "darkTrellis", "fgrgb" => "FF622222"))
+ @test XLSX.getFill(s, "F6").fill == Dict("patternFill" => Dict("bgrgb" => "FF6DDDD2", "patternType" => "darkTrellis", "fgrgb" => "FF622222"))
+ XLSX.setUniformFill(s, :, [2, 5]; pattern="darkTrellis", fgColor="FF622224", bgColor="FF6DDDD4")
+ @test XLSX.getFill(s, "B2").fill == Dict("patternFill" => Dict("bgrgb" => "FF6DDDD4", "patternType" => "darkTrellis", "fgrgb" => "FF622224"))
+ @test XLSX.getFill(s, "E3").fill == Dict("patternFill" => Dict("bgrgb" => "FF6DDDD4", "patternType" => "darkTrellis", "fgrgb" => "FF622224"))
+ XLSX.setUniformFill(s, 2, [3, 6]; pattern="darkTrellis", fgColor="FF622226", bgColor="FF6DDDD6")
+ @test XLSX.getFill(s, "C2").fill == Dict("patternFill" => Dict("bgrgb" => "FF6DDDD6", "patternType" => "darkTrellis", "fgrgb" => "FF622226"))
+ XLSX.setUniformFill(s, [2, 3], 5:6; pattern="darkTrellis", fgColor="FF642226", bgColor="FF64DDD6")
+ @test XLSX.getFill(s, "F2").fill == Dict("patternFill" => Dict("bgrgb" => "FF64DDD6", "patternType" => "darkTrellis", "fgrgb" => "FF642226"))
+ XLSX.setUniformFill(s, 2:2:6, [4, 5]; pattern="darkTrellis", fgColor="FF622228", bgColor="FF6DDDD8")
+ @test XLSX.getFill(s, "E4").fill == Dict("patternFill" => Dict("bgrgb" => "FF6DDDD8", "patternType" => "darkTrellis", "fgrgb" => "FF622228"))
+ XLSX.setUniformFill(s, :, :; pattern="darkTrellis", fgColor="FF822228", bgColor="FF8DDDD8")
+ @test XLSX.getFill(s, "E4").fill == Dict("patternFill" => Dict("bgrgb" => "FF8DDDD8", "patternType" => "darkTrellis", "fgrgb" => "FF822228"))
+ XLSX.setUniformFill(s, :; pattern="darkTrellis", fgColor="FF822288", bgColor="FF8DDD88")
+ @test XLSX.getFill(s, "E4").fill == Dict("patternFill" => Dict("bgrgb" => "FF8DDD88", "patternType" => "darkTrellis", "fgrgb" => "FF822288"))
+ XLSX.setUniformFill(s, :; pattern="darkTrellis", fgColor="FF822288", bgColor="FF8DDD88")
+ @test XLSX.getFill(s, "E4").fill == Dict("patternFill" => Dict("bgrgb" => "FF8DDD88", "patternType" => "darkTrellis", "fgrgb" => "FF822288"))
+ XLSX.setUniformFill(s, 1, 1:2; pattern="darkTrellis", fgColor="FF822268", bgColor="FF8DDD68")
+ @test XLSX.getFill(s, "B1").fill == Dict("patternFill" => Dict("bgrgb" => "FF8DDD68", "patternType" => "darkTrellis", "fgrgb" => "FF822268"))
+
end
@testset "setAlignment" begin
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:Z26"] = ""
+ XLSX.setAlignment(s, "Sheet1!A1"; horizontal="right", vertical="justify", wrapText=true)
+ @test XLSX.getAlignment(s, "A1").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ XLSX.setAlignment(s, "Sheet1!A2:C4"; horizontal="right", vertical="justify", wrapText=true)
+ @test XLSX.getAlignment(s, "B3").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ XLSX.setAlignment(s, "Sheet1!D:E"; horizontal="right", vertical="justify", wrapText=true)
+ @test XLSX.getAlignment(s, "D26").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ XLSX.setAlignment(s, "Sheet1!25:26"; horizontal="left", vertical="top", wrapText=false)
+ @test XLSX.getAlignment(s, "D26").alignment == Dict("alignment" => Dict("horizontal" => "left", "vertical" => "top", "wrapText" => "0"))
+ XLSX.setAlignment(s, "G8,H10,J15:M18"; horizontal="left", vertical="bottom", wrapText=true)
+ @test XLSX.getAlignment(s, "G8").alignment == Dict("alignment" => Dict("horizontal" => "left", "vertical" => "bottom", "wrapText" => "1"))
+ @test XLSX.getAlignment(s, "H10").alignment == Dict("alignment" => Dict("horizontal" => "left", "vertical" => "bottom", "wrapText" => "1"))
+ @test XLSX.getAlignment(s, "L16").alignment == Dict("alignment" => Dict("horizontal" => "left", "vertical" => "bottom", "wrapText" => "1"))
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:Z26"] = ""
+ XLSX.setAlignment(s, :, 1:3; horizontal="right", vertical="justify", wrapText=true)
+ @test XLSX.getAlignment(s, "B25").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ XLSX.setAlignment(s, 8:2:16, :; horizontal="right", vertical="justify", wrapText=true)
+ @test XLSX.getAlignment(s, "C12").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ XLSX.setAlignment(s, :, [8, 10, 12, 14, 16]; horizontal="right", vertical="top", wrapText=true)
+ @test XLSX.getAlignment(s, "L22").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "top", "wrapText" => "1"))
+ XLSX.setAlignment(s, 18, 20:3:26; horizontal="left", vertical="top", wrapText=true)
+ @test XLSX.getAlignment(s, "W18").alignment == Dict("alignment" => Dict("horizontal" => "left", "vertical" => "top", "wrapText" => "1"))
+ XLSX.setAlignment(s, 18:2:22, 20:3:26; horizontal="left", vertical="bottom", wrapText=true)
+ @test XLSX.getAlignment(s, "Z20").alignment == Dict("alignment" => Dict("horizontal" => "left", "vertical" => "bottom", "wrapText" => "1"))
+
f = XLSX.open_xlsx_template(joinpath(data_directory, "customXml.xlsx"))
s = f["Mock-up"]
@@ -1755,6 +2766,105 @@ end
isfile("output.xlsx") && rm("output.xlsx")
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:Z26"] = ""
+ XLSX.setUniformAlignment(s, "Sheet1!E5:E6"; horizontal="right", vertical="justify", wrapText=true)
+ @test XLSX.getAlignment(s, "E5").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ XLSX.setUniformAlignment(s, "Sheet1!A:A"; horizontal="right", vertical="top", wrapText=true)
+ @test XLSX.getAlignment(s, "A23").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "top", "wrapText" => "1"))
+ XLSX.setUniformAlignment(s, "Sheet1!15:24"; horizontal="left", vertical="top", wrapText=true)
+ @test XLSX.getAlignment(s, "Q15").alignment == Dict("alignment" => Dict("horizontal" => "left", "vertical" => "top", "wrapText" => "1"))
+ @test XLSX.getAlignment(s, "A23").alignment == Dict("alignment" => Dict("horizontal" => "left", "vertical" => "top", "wrapText" => "1"))
+ XLSX.setUniformAlignment(s, "A:A"; horizontal="right", vertical="bottom", wrapText=true)
+ @test XLSX.getAlignment(s, "A15").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "bottom", "wrapText" => "1"))
+ XLSX.setUniformAlignment(s, "10:12"; horizontal="right", vertical="bottom", wrapText=true)
+ @test XLSX.getAlignment(s, "Q11").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "bottom", "wrapText" => "1"))
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:Z26"] = ""
+ XLSX.setUniformAlignment(s, 2, :; horizontal="right", vertical="justify", wrapText=true)
+ @test XLSX.getAlignment(s, "E2").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ XLSX.setUniformAlignment(s, :, 4:5; horizontal="right", vertical="top", wrapText=true)
+ @test XLSX.getAlignment(s, "D23").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "top", "wrapText" => "1"))
+ XLSX.setUniformAlignment(s, :, :; horizontal="left", vertical="top", wrapText=true)
+ @test XLSX.getAlignment(s, "Q15").alignment == Dict("alignment" => Dict("horizontal" => "left", "vertical" => "top", "wrapText" => "1"))
+ @test XLSX.getAlignment(s, "A23").alignment == Dict("alignment" => Dict("horizontal" => "left", "vertical" => "top", "wrapText" => "1"))
+ XLSX.setUniformAlignment(s, :; horizontal="right", vertical="bottom", wrapText=true)
+ @test XLSX.getAlignment(s, "A15").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "bottom", "wrapText" => "1"))
+ XLSX.setUniformAlignment(s, :, [8, 12, 14]; horizontal="right", vertical="bottom", wrapText=false)
+ @test XLSX.getAlignment(s, "L12").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "bottom", "wrapText" => "0"))
+ XLSX.setUniformAlignment(s, 8:12:20, 3; horizontal="right", vertical="top", wrapText=false)
+ @test XLSX.getAlignment(s, "C20").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "top", "wrapText" => "0"))
+ XLSX.setUniformAlignment(s, 8:12:20, [3, 4]; horizontal="justify", vertical="justify", wrapText=false)
+ @test XLSX.getAlignment(s, "D8").alignment == Dict("alignment" => Dict("horizontal" => "justify", "vertical" => "justify", "wrapText" => "0"))
+ XLSX.setUniformAlignment(s, 8:20, 8; horizontal="justify", vertical="justify", wrapText=true)
+ @test XLSX.getAlignment(s, "H15").alignment == Dict("alignment" => Dict("horizontal" => "justify", "vertical" => "justify", "wrapText" => "1"))
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:Z26"] = ""
+ XLSX.setAlignment(s, "A1"; horizontal="right", vertical="justify", wrapText=true)
+ XLSX.setUniformAlignment(f, "Sheet1!A1,Sheet1!C3,Sheet1!E5:E6")
+ @test XLSX.getAlignment(s, "A1").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ @test XLSX.getAlignment(s, "C3").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ @test XLSX.getAlignment(s, "E5").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ @test XLSX.getAlignment(s, "E6").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, 2, [1, 3, 10, 15, 28]; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, [1, 3, 10, 15, 28], 2:3; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, :, [1, 3, 10, 15, 28]; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, [1, 3, 10, 15, 28], :; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, "Z100"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, "Sheet1!Z100"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, "Sheet1!E1:F5,Sheet1!Z100"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(f, "Sheet1!garbage"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, "Sheet1!garbage"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, "garbage"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, "garbage1:garbage2"; horizontal="right", vertical="justify", wrapText=true)
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:Z26"] = ""
+ XLSX.setAlignment(s, "A1"; horizontal="right", vertical="justify", wrapText=true)
+ XLSX.setUniformAlignment(s, 1, 1:2:25)
+ @test XLSX.getAlignment(s, 1, 1).alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ @test XLSX.getAlignment(s, 1, 9).alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ @test XLSX.getAlignment(s, 1, 19).alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ @test XLSX.getAlignment(s, 1, 25).alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ @test XLSX.getAlignment(s, 1, 8) === nothing
+ @test XLSX.getAlignment(s, 1, 16) === nothing
+ @test XLSX.getAlignment(s, 1, 22) === nothing
+ @test XLSX.getAlignment(s, 1, 24) === nothing
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:Z26"] = ""
+ XLSX.setAlignment(s, "A2"; horizontal="right", vertical="justify", wrapText=true)
+ XLSX.setUniformAlignment(s, 2:2:26, :)
+ @test XLSX.getAlignment(s, "A2").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ @test XLSX.getAlignment(s, "C4").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ @test XLSX.getAlignment(s, "K6").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ @test XLSX.getAlignment(s, "Y24").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ @test XLSX.getAlignment(s, "A3") === nothing
+ @test XLSX.getAlignment(s, "C5") === nothing
+ @test XLSX.getAlignment(s, "K7") === nothing
+ @test XLSX.getAlignment(s, "Y25") === nothing
+ @test_throws XLSX.XLSXError XLSX.setUniformAlignment(s, 2, [1, 3, 10, 15, 28]; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setUniformAlignment(s, [1, 3, 10, 15, 28], 2:3; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setUniformAlignment(s, :, [1, 3, 10, 15, 28]; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setUniformAlignment(s, [1, 3, 10, 15, 28], :; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setUniformAlignment(s, "Z100:Z101"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setUniformAlignment(s, "Sheet1!Z100:Z101"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setUniformAlignment(s, "Sheet1!E1:F5,Sheet1!Z100"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setUniformAlignment(s, "Z100:Z100"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setUniformAlignment(s, "Sheet1!Z100:Z100"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setUniformAlignment(s, "Sheet1!Z100,Sheet1!Z100"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setUniformAlignment(f, "Sheet1!garbage"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setUniformAlignment(s, "Sheet1!garbage"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setUniformAlignment(s, "garbage"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setUniformAlignment(s, "garbage1:garbage2"; horizontal="right", vertical="justify", wrapText=true)
+
end
@testset "setFormat" begin
@@ -1810,6 +2920,10 @@ end
@test XLSX.setFormat(s, "F1:F5"; format="#,##0.000") == -1
@test XLSX.getFormat(s, "E2").format == Dict("numFmt" => Dict("formatCode" => "#,##0.000"))
@test XLSX.getFormat(f, "Sheet1!F2").format == Dict("numFmt" => Dict("formatCode" => "#,##0.000"))
+ @test_throws XLSX.XLSXError XLSX.setFormat(s, "Z100"; format="Currency")
+ @test_throws XLSX.XLSXError XLSX.setFormat(s, "Sheet1!Z100"; format="Currency")
+ @test_throws XLSX.XLSXError XLSX.setFormat(s, "Sheet1!E1:F5,Sheet1!Z100"; format="Currency")
+ @test_throws XLSX.XLSXError XLSX.setFormat(s, "E2"; format="ffzz345")
XLSX.writexlsx("test.xlsx", f, overwrite=true)
@@ -1824,6 +2938,174 @@ end
isfile("test.xlsx") && rm("test.xlsx")
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:Z26"] = ""
+ XLSX.setFormat(s, "Sheet1!E5"; format="Currency")
+ @test XLSX.getFormat(f, "Sheet1!E5").format == Dict("numFmt" => Dict("numFmtId" => "7", "formatCode" => "\$#,##0.00_);(\$#,##0.00)"))
+ XLSX.setFormat(s, "Sheet1!W5:X8"; format="Currency")
+ @test XLSX.getFormat(f, "Sheet1!X7").format == Dict("numFmt" => Dict("numFmtId" => "7", "formatCode" => "\$#,##0.00_);(\$#,##0.00)"))
+ XLSX.setFormat(s, "Sheet1!F:G"; format="Currency")
+ @test XLSX.getFormat(s, "F3").format == Dict("numFmt" => Dict("numFmtId" => "7", "formatCode" => "\$#,##0.00_);(\$#,##0.00)"))
+ XLSX.setFormat(s, "Sheet1!4:8"; format="Currency")
+ @test XLSX.getFormat(s, "Q7").format == Dict("numFmt" => Dict("numFmtId" => "7", "formatCode" => "\$#,##0.00_);(\$#,##0.00)"))
+ XLSX.setFormat(s, "N4,M8:M15,Z25:Z26"; format="#,##0.000")
+ @test XLSX.getFormat(s, "Z26").format == Dict("numFmt" => Dict("formatCode" => "#,##0.000"))
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:Z26"] = ""
+ XLSX.setFormat(s, :, 2:4; format="Currency")
+ @test XLSX.getFormat(f, "Sheet1!B23").format == Dict("numFmt" => Dict("numFmtId" => "7", "formatCode" => "\$#,##0.00_);(\$#,##0.00)"))
+ XLSX.setFormat(s, 4:3:10, :; format="Currency")
+ @test XLSX.getFormat(s, "Q7").format == Dict("numFmt" => Dict("numFmtId" => "7", "formatCode" => "\$#,##0.00_);(\$#,##0.00)"))
+ XLSX.setFormat(s, :, [8, 23, 4]; format="Currency")
+ @test XLSX.getFormat(s, "H1").format == Dict("numFmt" => Dict("numFmtId" => "7", "formatCode" => "\$#,##0.00_);(\$#,##0.00)"))
+ XLSX.setFormat(s, 25:26, 20:26; format="#,##0.000")
+ @test XLSX.getFormat(s, "Z26").format == Dict("numFmt" => Dict("formatCode" => "#,##0.000"))
+ XLSX.setFormat(s, 25:26, 15; format="#,##0.0000")
+ @test XLSX.getFormat(s, "O26").format == Dict("numFmt" => Dict("formatCode" => "#,##0.0000"))
+ XLSX.setFormat(s, 21:2:25, [15, 16]; format="#,##0.0")
+ @test XLSX.getFormat(s, "P25").format == Dict("numFmt" => Dict("formatCode" => "#,##0.0"))
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:Z26"] = ""
+ XLSX.setUniformFormat(s, "Sheet1!W5:X8"; format="Currency")
+ @test XLSX.getFormat(f, "Sheet1!X7").format == Dict("numFmt" => Dict("numFmtId" => "7", "formatCode" => "\$#,##0.00_);(\$#,##0.00)"))
+ XLSX.setUniformFormat(s, "Sheet1!F:G"; format="Currency")
+ @test XLSX.getFormat(s, "F3").format == Dict("numFmt" => Dict("numFmtId" => "7", "formatCode" => "\$#,##0.00_);(\$#,##0.00)"))
+ XLSX.setUniformFormat(s, "Sheet1!4:8"; format="Currency")
+ @test XLSX.getFormat(s, "Q7").format == Dict("numFmt" => Dict("numFmtId" => "7", "formatCode" => "\$#,##0.00_);(\$#,##0.00)"))
+ XLSX.setUniformFormat(s, "N4,M8:M15,Z25:Z26"; format="#,##0.000")
+ @test XLSX.getFormat(s, "Z26").format == Dict("numFmt" => Dict("formatCode" => "#,##0.000"))
+ @test_throws XLSX.XLSXError XLSX.setUniformFormat(s, "Z100:Z101"; format="Currency")
+ @test_throws XLSX.XLSXError XLSX.setUniformFormat(s, "Sheet1!Z100:Z101"; format="Currency")
+ @test_throws XLSX.XLSXError XLSX.setUniformFormat(s, "Sheet1!E1:F5,Sheet1!Z100"; format="Currency")
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:Z26"] = ""
+ XLSX.setUniformFormat(s, :, 2:4; format="Currency")
+ @test XLSX.getFormat(f, "Sheet1!B23").format == Dict("numFmt" => Dict("numFmtId" => "7", "formatCode" => "\$#,##0.00_);(\$#,##0.00)"))
+ XLSX.setUniformFormat(s, 4:3:10, :; format="Currency")
+ @test XLSX.getFormat(s, "Q7").format == Dict("numFmt" => Dict("numFmtId" => "7", "formatCode" => "\$#,##0.00_);(\$#,##0.00)"))
+ XLSX.setUniformFormat(s, :, [8, 23, 4]; format="Currency")
+ @test XLSX.getFormat(s, "H1").format == Dict("numFmt" => Dict("numFmtId" => "7", "formatCode" => "\$#,##0.00_);(\$#,##0.00)"))
+ XLSX.setUniformFormat(s, 25:26, 20:26; format="#,##0.000")
+ @test XLSX.getFormat(s, "Z26").format == Dict("numFmt" => Dict("formatCode" => "#,##0.000"))
+ XLSX.setUniformFormat(s, 25:26, 15; format="#,##0.0000")
+ @test XLSX.getFormat(s, "O26").format == Dict("numFmt" => Dict("formatCode" => "#,##0.0000"))
+ XLSX.setUniformFormat(s, 21:2:25, [15, 16]; format="#,##0.0")
+ @test XLSX.getFormat(s, "P25").format == Dict("numFmt" => Dict("formatCode" => "#,##0.0"))
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:Z26"] = ""
+ XLSX.setUniformFormat(s, :, :; format="Currency")
+ @test XLSX.getFormat(f, "Sheet1!B23").format == Dict("numFmt" => Dict("numFmtId" => "7", "formatCode" => "\$#,##0.00_);(\$#,##0.00)"))
+ XLSX.setUniformFormat(s, 4:10, :; format="#,##0.000")
+ @test XLSX.getFormat(s, "Q7").format == Dict("numFmt" => Dict("formatCode" => "#,##0.000"))
+ XLSX.setUniformFormat(s, [8, 23, 4], 8; format="#,##0.0")
+ @test XLSX.getFormat(s, "H8").format == Dict("numFmt" => Dict("formatCode" => "#,##0.0"))
+
+ end
+
+ @testset "UniformStyle" begin
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:Z26"] = ""
+
+ XLSX.setFont(s, "A1:F5"; size=18, name="Arial")
+ cell_style = parse(Int, XLSX.getcell(s, "A1").style)
+ @test XLSX.setUniformStyle(s, "A1:F5") == cell_style
+ @test parse(Int, XLSX.getcell(s, "F5").style) == cell_style
+
+ XLSX.setFont(s, "A6:F10"; size=10, name="Aptos")
+ cell_style = parse(Int, XLSX.getcell(s, "E6").style)
+ @test XLSX.setUniformStyle(s, [6, 7, 8, 9, 10], 5) == cell_style
+ @test parse(Int, XLSX.getcell(s, "E8").style) == cell_style
+
+ XLSX.setFont(s, "A11:F15"; size=10, name="Times New Roman")
+ cell_style = parse(Int, XLSX.getcell(s, "E6").style)
+ @test XLSX.setUniformStyle(s, [6, 7, 8, 9, 10], :) == cell_style
+ @test parse(Int, XLSX.getcell(s, "Z8").style) == cell_style
+
+ XLSX.setFont(s, "A16"; size=80, name="Ariel")
+ cell_style = parse(Int, XLSX.getcell(s, "A16").style)
+ @test XLSX.setUniformStyle(s, "A16,A15,D20:E25,F25") == cell_style
+ @test parse(Int, XLSX.getcell(s, "A15").style) == cell_style
+ @test parse(Int, XLSX.getcell(s, "D20").style) == cell_style
+ @test parse(Int, XLSX.getcell(s, "F25").style) == cell_style
+
+ XLSX.setFont(s, "A1"; size=8, name="Aptos")
+ cell_style = parse(Int, XLSX.getcell(s, "A1").style)
+ @test XLSX.setUniformStyle(s, :) == cell_style
+ @test parse(Int, XLSX.getcell(s, "A1").style) == cell_style
+ @test parse(Int, XLSX.getcell(s, "M13").style) == cell_style
+ @test parse(Int, XLSX.getcell(s, "Z26").style) == cell_style
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:Z26"] = ""
+ XLSX.setFont(s, "A1"; size=8, name="Aptos")
+ cell_style = parse(Int, XLSX.getcell(s, "A1").style)
+ @test XLSX.setUniformStyle(s, "Sheet1!A1:A26") == cell_style
+ @test parse(Int, XLSX.getcell(s, "A2").style) == cell_style
+ @test parse(Int, XLSX.getcell(s, "A13").style) == cell_style
+ @test parse(Int, XLSX.getcell(s, "A26").style) == cell_style
+ @test XLSX.setUniformStyle(s, "Sheet1!1:2") == cell_style
+ @test parse(Int, XLSX.getcell(s, "B1").style) == cell_style
+ @test parse(Int, XLSX.getcell(s, "M2").style) == cell_style
+ @test parse(Int, XLSX.getcell(s, "Z1").style) == cell_style
+ @test XLSX.setUniformStyle(s, "Sheet1!B:C") == cell_style
+ @test parse(Int, XLSX.getcell(s, "C3").style) == cell_style
+ @test parse(Int, XLSX.getcell(s, "B13").style) == cell_style
+ @test parse(Int, XLSX.getcell(s, "C26").style) == cell_style
+
+ XLSX.setFont(s, "A1"; size=8, name="Arial")
+ cell_style = parse(Int, XLSX.getcell(s, "A1").style)
+ @test XLSX.setUniformStyle(s, "A1:A26") == cell_style
+ @test parse(Int, XLSX.getcell(s, "A2").style) == cell_style
+ @test parse(Int, XLSX.getcell(s, "A13").style) == cell_style
+ @test parse(Int, XLSX.getcell(s, "A26").style) == cell_style
+ @test XLSX.setUniformStyle(s, "1:2") == cell_style
+ @test parse(Int, XLSX.getcell(s, "B1").style) == cell_style
+ @test parse(Int, XLSX.getcell(s, "M2").style) == cell_style
+ @test parse(Int, XLSX.getcell(s, "Z1").style) == cell_style
+ @test XLSX.setUniformStyle(s, "B:C") == cell_style
+ @test parse(Int, XLSX.getcell(s, "C3").style) == cell_style
+ @test parse(Int, XLSX.getcell(s, "B13").style) == cell_style
+ @test parse(Int, XLSX.getcell(s, "C26").style) == cell_style
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:Z26"] = ""
+ XLSX.setFont(s, "A1"; size=8, name="Aptos")
+ cell_style = parse(Int, XLSX.getcell(s, "A1").style)
+ @test XLSX.setUniformStyle(s, 1, :) == cell_style
+ @test parse(Int, XLSX.getcell(s, "B1").style) == cell_style
+ @test XLSX.setUniformStyle(s, :, 2) == cell_style
+ @test parse(Int, XLSX.getcell(s, "B13").style) == cell_style
+ @test XLSX.setUniformStyle(s, :, 5:2:15) == cell_style
+ @test parse(Int, XLSX.getcell(s, "E25").style) == cell_style
+ @test XLSX.setUniformStyle(s, 5:10, [15, 16, 17]) == cell_style
+ @test parse(Int, XLSX.getcell(s, "P10").style) == cell_style
+ @test XLSX.setUniformStyle(s, 5:10, 17:19) == cell_style
+ @test parse(Int, XLSX.getcell(s, "S10").style) == cell_style
+ @test XLSX.setUniformStyle(s, [10, 12, 26], [19, 24, 26]) == cell_style
+ @test parse(Int, XLSX.getcell(s, "Z26").style) == cell_style
+ @test XLSX.setUniformStyle(s, :, :) == cell_style
+ @test parse(Int, XLSX.getcell(s, "Y4").style) == cell_style
+ @test_throws XLSX.XLSXError XLSX.setUniformStyle(s, :, [1, 3, 10, 15, 28])
+ @test_throws XLSX.XLSXError XLSX.setUniformStyle(s, [1, 3, 10, 15, 28], :)
+ @test_throws XLSX.XLSXError XLSX.setUniformStyle(s, 1, [1, 3, 10, 15, 28])
+ @test_throws XLSX.XLSXError XLSX.setUniformStyle(s, [1, 3, 10, 15, 28], 2:3)
+ @test_throws XLSX.XLSXError XLSX.setUniformStyle(f, "Sheet1!garbage")
+ @test_throws XLSX.XLSXError XLSX.setUniformStyle(s, "Sheet1!garbage")
+ @test_throws XLSX.XLSXError XLSX.setUniformStyle(s, "garbage")
+ @test_throws XLSX.XLSXError XLSX.setUniformStyle(s, "garbage1:garbage2")
+
end
@testset "Width and height" begin
@@ -1873,44 +3155,2360 @@ end
f = XLSX.open_xlsx_template(joinpath(data_directory, "customXml.xlsx"))
s = f["Mock-up"]
- XLSX.setColumnWidth(s, "Location"; width = 60)
- XLSX.setRowHeight(s, "Location"; height = 50)
+ XLSX.setColumnWidth(s, "Location"; width=60)
+ XLSX.setRowHeight(s, "Location"; height=50)
@test XLSX.getRowHeight(s, "D18") ≈ 50.2109375
@test XLSX.getColumnWidth(s, "D18") ≈ 60.7109375
@test XLSX.getRowHeight(f, "Mock-up!J20") ≈ 50.2109375
@test XLSX.getColumnWidth(f, "Mock-up!J20") ≈ 60.7109375
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:Z26"] = ""
+ XLSX.setColumnWidth(s, "Sheet1!A1"; width=60)
+ @test XLSX.getColumnWidth(s, "A1") ≈ 60.7109375
+ XLSX.setColumnWidth(s, "Sheet1!A1:Z1"; width=60)
+ @test XLSX.getColumnWidth(s, "R1") ≈ 60.7109375
+ XLSX.setColumnWidth(s, "Sheet1!A:B"; width=60)
+ @test XLSX.getColumnWidth(s, "B26") ≈ 60.7109375
+ XLSX.setColumnWidth(s, "Sheet1!2:3"; width=60)
+ @test XLSX.getColumnWidth(s, "R26") ≈ 60.7109375
+ XLSX.setColumnWidth(s, "A:B"; width=30.5)
+ @test XLSX.getColumnWidth(s, "B26") ≈ 31.2109375
+ XLSX.setColumnWidth(s, "2:3"; width=30.5)
+ @test XLSX.getColumnWidth(s, "R3") ≈ 31.2109375
+ XLSX.setColumnWidth(s, "Sheet1!C5:C7,Sheet1!F5:F7,Sheet1!H7"; width=10.1)
+ @test XLSX.getColumnWidth(s, "F26") ≈ 10.8109375
+ XLSX.setColumnWidth(s, 5, :; width=10.0)
+ @test XLSX.getColumnWidth(s, "Q5") ≈ 10.7109375
+ XLSX.setColumnWidth(s, 5:7; width=10.2)
+ @test XLSX.getColumnWidth(s, "G22") ≈ 10.9109375
+ XLSX.setColumnWidth(s, :, 5:7; width=10.3)
+ @test XLSX.getColumnWidth(s, "G22") ≈ 11.0109375
+ XLSX.setColumnWidth(s, :, :; width=10.4)
+ @test XLSX.getColumnWidth(s, "G22") ≈ 11.1109375
+ XLSX.setColumnWidth(s, :; width=10.5)
+ @test XLSX.getColumnWidth(s, "G22") ≈ 11.2109375
+ XLSX.setColumnWidth(s, 2:3:11, :; width=10.6)
+ @test XLSX.getColumnWidth(s, "Z26") ≈ 11.3109375
+ XLSX.setColumnWidth(s, 2:3:11; width=10.7)
+ @test XLSX.getColumnWidth(s, "E26") ≈ 11.4109375
+ XLSX.setColumnWidth(s, :, [2, 3, 11]; width=10.8)
+ @test XLSX.getColumnWidth(s, "K15") ≈ 11.5109375
+ XLSX.setColumnWidth(s, 3:6, [2, 3, 11]; width=10.9)
+ @test XLSX.getColumnWidth(s, "K15") ≈ 11.6109375
+ XLSX.setColumnWidth(s, 3:3:6, [2, 3, 11]; width=11.0)
+ @test XLSX.getColumnWidth(s, "K15") ≈ 11.7109375
+ XLSX.setColumnWidth(s, 11, 7:13; width=11.1)
+ @test XLSX.getColumnWidth(s, "K15") ≈ 11.8109375
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:Z26"] = ""
+ XLSX.setRowHeight(s, "Sheet1!A1"; height=10.1)
+ @test XLSX.getRowHeight(s, "A1") ≈ 10.3109375
+ XLSX.setRowHeight(s, "Sheet1!A1:A26"; height=10.2)
+ @test XLSX.getRowHeight(s, "R20") ≈ 10.4109375
+ XLSX.setRowHeight(s, "Sheet1!A:B"; height=10.3)
+ @test XLSX.getRowHeight(s, "B26") ≈ 10.5109375
+ XLSX.setRowHeight(s, "Sheet1!2:3"; height=10.4)
+ @test XLSX.getRowHeight(s, "R3") ≈ 10.6109375
+ XLSX.setRowHeight(s, "A:B"; height=10.5)
+ @test XLSX.getRowHeight(s, "B26") ≈ 10.7109375
+ XLSX.setRowHeight(s, "2:3"; height=10.6)
+ @test XLSX.getRowHeight(s, "R3") ≈ 10.8109375
+ XLSX.setRowHeight(s, "Sheet1!C5:C7,Sheet1!F5:F7,Sheet1!H7"; height=10.7)
+ @test XLSX.getRowHeight(s, "F6") ≈ 10.9109375
+ XLSX.setRowHeight(s, 5, :; height=10.8)
+ @test XLSX.getRowHeight(s, "Q5") ≈ 11.0109375
+ XLSX.setRowHeight(s, 5:7; height=10.9)
+ @test XLSX.getRowHeight(s, "P6") ≈ 11.1109375
+ XLSX.setRowHeight(s, :, 5:7; height=11.0)
+ @test XLSX.getRowHeight(s, "G22") ≈ 11.2109375
+ XLSX.setRowHeight(s, :, :; height=11.1)
+ @test XLSX.getRowHeight(s, "G22") ≈ 11.3109375
+ XLSX.setRowHeight(s, :; height=11.2)
+ @test XLSX.getRowHeight(s, "G22") ≈ 11.4109375
+ XLSX.setRowHeight(s, 2:3:11, :; height=11.3)
+ @test XLSX.getRowHeight(s, "J8") ≈ 11.5109375
+ XLSX.setRowHeight(s, 2:3:11; height=11.4)
+ @test XLSX.getRowHeight(s, "J8") ≈ 11.6109375
+ XLSX.setRowHeight(s, :, [2, 3, 11]; height=11.5)
+ @test XLSX.getRowHeight(s, "K15") ≈ 11.7109375
+ XLSX.setRowHeight(s, 3:6, [2, 3, 11]; height=11.6)
+ @test XLSX.getRowHeight(s, "K5") ≈ 11.8109375
+ XLSX.setRowHeight(s, 3:3:6, [2, 3, 11]; height=11.7)
+ @test XLSX.getRowHeight(s, "K6") ≈ 11.9109375
+ XLSX.setRowHeight(s, 11, 7:13; height=11.8)
+ @test XLSX.getRowHeight(s, "K11") ≈ 12.0109375
+
end
@testset "No cache" begin
XLSX.openxlsx(joinpath(data_directory, "customXml.xlsx"); mode="r", enable_cache=true) do f
@test XLSX.getRowHeight(f, "Mock-up!B2") ≈ 23.25
- @test_throws AssertionError XLSX.getColumnWidth(f, "Mock-up!B2")
+ @test_throws XLSX.XLSXError XLSX.getColumnWidth(f, "Mock-up!B2") # File not writable
end
XLSX.openxlsx(joinpath(data_directory, "customXml.xlsx"); mode="r", enable_cache=false) do f
- @test_throws AssertionError XLSX.getRowHeight(f, "Mock-up!B2")
- @test_throws AssertionError XLSX.getColumnWidth(f, "Mock-up!B2")
- @test_throws AssertionError XLSX.getFont(f, "Mock-up!B2")
- @test_throws AssertionError XLSX.getFill(f, "Mock-up!B2")
- @test_throws AssertionError XLSX.getBorder(f, "Mock-up!B2")
- @test_throws AssertionError XLSX.getFormat(f, "Mock-up!B2")
- @test_throws AssertionError XLSX.getAlignment(f, "Mock-up!B2")
- @test_throws AssertionError XLSX.setRowHeight(f, "Mock-up!B2")
- @test_throws AssertionError XLSX.setColumnWidth(f, "Mock-up!B2")
- @test_throws AssertionError XLSX.setFont(f, "Mock-up!B2")
- @test_throws AssertionError XLSX.setFill(f, "Mock-up!B2")
- @test_throws AssertionError XLSX.setBorder(f, "Mock-up!B2")
- @test_throws AssertionError XLSX.setFormat(f, "Mock-up!B2")
- @test_throws AssertionError XLSX.setAlignment(f, "Mock-up!B2")
- @test_throws AssertionError XLSX.setUniformFont(f, "Mock-up!B2:C4")
- @test_throws AssertionError XLSX.setUniformFill(f, "Mock-up!B2:C4")
- @test_throws AssertionError XLSX.setUniformBorder(f, "Mock-up!B2:C4")
- @test_throws AssertionError XLSX.setUniformFormat(f, "Mock-up!B2:C4")
- @test_throws AssertionError XLSX.setUniformAlignment(f, "Mock-up!B2:C4")
- @test_throws AssertionError XLSX.setOutsideBorder(f, "Mock-up!B2:C4")
+ @test_throws XLSX.XLSXError XLSX.getRowHeight(f, "Mock-up!B2")
+ @test_throws XLSX.XLSXError XLSX.getFont(f, "Mock-up!B2")
+ @test_throws XLSX.XLSXError XLSX.getFill(f, "Mock-up!B2")
+ @test_throws XLSX.XLSXError XLSX.getBorder(f, "Mock-up!B2")
+ @test_throws XLSX.XLSXError XLSX.getFormat(f, "Mock-up!B2")
+ @test_throws XLSX.XLSXError XLSX.getAlignment(f, "Mock-up!B2")
+ @test_throws XLSX.XLSXError XLSX.setRowHeight(f, "Mock-up!B2")
+ @test_throws XLSX.XLSXError XLSX.setColumnWidth(f, "Mock-up!B2")
+ @test_throws XLSX.XLSXError XLSX.setFont(f, "Mock-up!B2")
+ @test_throws XLSX.XLSXError XLSX.setFill(f, "Mock-up!B2")
+ @test_throws XLSX.XLSXError XLSX.setBorder(f, "Mock-up!B2")
+ @test_throws XLSX.XLSXError XLSX.setFormat(f, "Mock-up!B2")
+ @test_throws XLSX.XLSXError XLSX.setAlignment(f, "Mock-up!B2")
+ @test_throws XLSX.XLSXError XLSX.setUniformFont(f, "Mock-up!B2:C4")
+ @test_throws XLSX.XLSXError XLSX.setUniformFill(f, "Mock-up!B2:C4")
+ @test_throws XLSX.XLSXError XLSX.setUniformBorder(f, "Mock-up!B2:C4")
+ @test_throws XLSX.XLSXError XLSX.setUniformFormat(f, "Mock-up!B2:C4")
+ @test_throws XLSX.XLSXError XLSX.setUniformAlignment(f, "Mock-up!B2:C4")
+ @test_throws XLSX.XLSXError XLSX.setOutsideBorder(f, "Mock-up!B2:C4")
end
end
+ @testset "indexing setAttribute" begin
+ f = XLSX.newxlsx() # Empty XLSXFile
+ s = f[1] #1×1 XLSX.Worksheet: ["Sheet1"](A1:A1)
+
+ #Can't write to single, empty cells
+ @test_throws XLSX.XLSXError XLSX.setFont(s, "A1"; color="grey42")
+ @test_throws XLSX.XLSXError XLSX.setFont(s, "A1:A1"; color="grey42")
+ @test_throws XLSX.XLSXError XLSX.setFont(s, "A:A"; color="grey42")
+ @test_throws XLSX.XLSXError XLSX.setFont(s, "1"; color="grey42")
+ @test_throws XLSX.XLSXError XLSX.setFont(s, 1, 1; color="grey42")
+ @test_throws XLSX.XLSXError XLSX.setFont(s, [1], 1; color="grey42")
+ @test_throws XLSX.XLSXError XLSX.setFont(s, 1, 1:1; color="grey42")
+ @test_throws XLSX.XLSXError XLSX.setFont(s, 1, :; color="grey42")
+ @test_throws XLSX.XLSXError XLSX.setFont(s, :; color="grey42")
+ @test_throws XLSX.XLSXError XLSX.setFont(s, :, :; color="grey42")
+
+ s[2, 1] = ""
+ s[3, 3] = ""
+ # Skip empty cells silently in ranges
+ @test XLSX.setFont(s, 2:3, 1:3; color="grey42") == -1
+
+ # Outside sheet dimension
+ @test_throws XLSX.XLSXError XLSX.getFont(s, 2, 4)
+ @test_throws XLSX.XLSXError XLSX.getFont(s, 4, 2)
+ @test_throws XLSX.XLSXError XLSX.getFont(s, 4, 4)
+
+ s[1:3, 1:3] = ""
+ default_font = XLSX.getDefaultFont(s).font
+ dname = default_font["name"]["val"]
+ dsize = default_font["sz"]["val"]
+ XLSX.setFont(s, "A1"; color="grey42")
+ @test XLSX.getFont(s, "A1").font == Dict("name" => Dict("val" => dname), "sz" => Dict("val" => dsize), "color" => Dict("rgb" => "FF6B6B6B"))
+ XLSX.setFont(s, 2, 2; color="grey43", name="Ariel")
+ @test XLSX.getFont(s, 2, 2).font == Dict("name" => Dict("val" => "Ariel"), "sz" => Dict("val" => dsize), "color" => Dict("rgb" => "FF6E6E6E"))
+ XLSX.setFont(s, [2, 3], 1:3; color="grey44", name="Courier New")
+ @test XLSX.getFont(s, 3, 1).font == Dict("name" => Dict("val" => "Courier New"), "sz" => Dict("val" => dsize), "color" => Dict("rgb" => "FF707070"))
+ @test XLSX.getFont(s, 2, 2).font == Dict("name" => Dict("val" => "Courier New"), "sz" => Dict("val" => dsize), "color" => Dict("rgb" => "FF707070"))
+ @test XLSX.getFont(s, 3, 3).font == Dict("name" => Dict("val" => "Courier New"), "sz" => Dict("val" => dsize), "color" => Dict("rgb" => "FF707070"))
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ @test_throws XLSX.XLSXError XLSX.setBorder(s, "A1"; allsides=["color" => "grey42", "style" => "thick"])
+ @test_throws XLSX.XLSXError XLSX.setBorder(s, "A1:A1"; allsides=["color" => "grey42", "style" => "thick"])
+ @test_throws XLSX.XLSXError XLSX.setBorder(s, "A"; allsides=["color" => "grey42", "style" => "thick"])
+ @test_throws XLSX.XLSXError XLSX.setBorder(s, "1"; allsides=["color" => "grey42", "style" => "thick"])
+ @test_throws XLSX.XLSXError XLSX.setBorder(s, 1, 1; allsides=["color" => "grey42", "style" => "thick"])
+ @test_throws XLSX.XLSXError XLSX.setBorder(s, [1], 1; allsides=["color" => "grey42", "style" => "thick"])
+ @test_throws XLSX.XLSXError XLSX.setBorder(s, 1, 1:1; allsides=["color" => "grey42", "style" => "thick"])
+ @test_throws XLSX.XLSXError XLSX.setBorder(s, :, 1; allsides=["color" => "grey42", "style" => "thick"])
+ @test_throws XLSX.XLSXError XLSX.setBorder(s, :; allsides=["color" => "grey42", "style" => "thick"])
+ @test_throws XLSX.XLSXError XLSX.setBorder(s, :, :; allsides=["color" => "grey42", "style" => "thick"])
+ @test_throws XLSX.XLSXError XLSX.setBorder(s, [2, 3], 1:3; allsides=["color" => "grey42", "style" => "thick"]) == -1
+ @test_throws XLSX.XLSXError XLSX.getBorder(s, 2, 1)
+ @test_throws XLSX.XLSXError XLSX.getBorder(s, 3, 2)
+ @test_throws XLSX.XLSXError XLSX.getBorder(s, 2, 3)
+ s[1:3, 1:3] = ""
+ XLSX.setBorder(s, "A1"; allsides=["color" => "grey42", "style" => "thick"])
+ @test XLSX.getBorder(s, "A1").border == Dict("left" => Dict("rgb" => "FF6B6B6B", "style" => "thick"), "bottom" => Dict("rgb" => "FF6B6B6B", "style" => "thick"), "right" => Dict("rgb" => "FF6B6B6B", "style" => "thick"), "top" => Dict("rgb" => "FF6B6B6B", "style" => "thick"), "diagonal" => nothing)
+ XLSX.setBorder(s, 2, 2; allsides=["color" => "grey43", "style" => "thin"])
+ @test XLSX.getBorder(s, 2, 2).border == Dict("left" => Dict("rgb" => "FF6E6E6E", "style" => "thin"), "bottom" => Dict("rgb" => "FF6E6E6E", "style" => "thin"), "right" => Dict("rgb" => "FF6E6E6E", "style" => "thin"), "top" => Dict("rgb" => "FF6E6E6E", "style" => "thin"), "diagonal" => nothing)
+ XLSX.setBorder(s, [2, 3], 1:3; allsides=["color" => "grey44", "style" => "hair"], diagonal=["color" => "grey44", "style" => "thin", "direction" => "down"])
+ @test XLSX.getBorder(s, 3, 1).border == Dict("left" => Dict("rgb" => "FF707070", "style" => "hair"), "bottom" => Dict("rgb" => "FF707070", "style" => "hair"), "right" => Dict("rgb" => "FF707070", "style" => "hair"), "top" => Dict("rgb" => "FF707070", "style" => "hair"), "diagonal" => Dict("rgb" => "FF707070", "style" => "thin", "direction" => "down"))
+ @test XLSX.getBorder(s, 2, 2).border == Dict("left" => Dict("rgb" => "FF707070", "style" => "hair"), "bottom" => Dict("rgb" => "FF707070", "style" => "hair"), "right" => Dict("rgb" => "FF707070", "style" => "hair"), "top" => Dict("rgb" => "FF707070", "style" => "hair"), "diagonal" => Dict("rgb" => "FF707070", "style" => "thin", "direction" => "down"))
+ @test XLSX.getBorder(s, 3, 3).border == Dict("left" => Dict("rgb" => "FF707070", "style" => "hair"), "bottom" => Dict("rgb" => "FF707070", "style" => "hair"), "right" => Dict("rgb" => "FF707070", "style" => "hair"), "top" => Dict("rgb" => "FF707070", "style" => "hair"), "diagonal" => Dict("rgb" => "FF707070", "style" => "thin", "direction" => "down"))
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ @test_throws XLSX.XLSXError XLSX.setFill(s, "A1"; pattern="lightVertical", fgColor="Red", bgColor="blue")
+ @test_throws XLSX.XLSXError XLSX.setFill(s, "A1:A1"; pattern="lightVertical", fgColor="Red", bgColor="blue")
+ @test_throws XLSX.XLSXError XLSX.setFill(s, "A"; pattern="lightVertical", fgColor="Red", bgColor="blue")
+ @test_throws XLSX.XLSXError XLSX.setFill(s, "1"; pattern="lightVertical", fgColor="Red", bgColor="blue")
+ @test_throws XLSX.XLSXError XLSX.setFill(s, 1, 1; pattern="lightVertical", fgColor="Red", bgColor="blue")
+ @test_throws XLSX.XLSXError XLSX.setFill(s, [1], 1; pattern="lightVertical", fgColor="Red", bgColor="blue")
+ @test_throws XLSX.XLSXError XLSX.setFill(s, 1, 1:1; pattern="lightVertical", fgColor="Red", bgColor="blue")
+ @test_throws XLSX.XLSXError XLSX.setFill(s, 1, :; pattern="lightVertical", fgColor="Red", bgColor="blue")
+ @test_throws XLSX.XLSXError XLSX.setFill(s, :, :; pattern="lightVertical", fgColor="Red", bgColor="blue")
+ @test_throws XLSX.XLSXError XLSX.setFill(s, :; pattern="lightVertical", fgColor="Red", bgColor="blue")
+ @test_throws XLSX.XLSXError XLSX.setFill(s, [2, 3], 1:3; pattern="lightVertical", fgColor="Red", bgColor="blue") == -1
+ @test_throws XLSX.XLSXError XLSX.getFill(s, 2, 1)
+ @test_throws XLSX.XLSXError XLSX.getFill(s, 3, 2)
+ @test_throws XLSX.XLSXError XLSX.getFill(s, 2, 3)
+ s[1:3, 1:3] = ""
+ XLSX.setFill(s, "A1"; pattern="lightVertical", fgColor="Red", bgColor="blue")
+ @test XLSX.getFill(s, "A1").fill == Dict("patternFill" => Dict("bgrgb" => "FF0000FF", "patternType" => "lightVertical", "fgrgb" => "FFFF0000"))
+ XLSX.setFill(s, 2, 2; pattern="lightGrid", fgColor="Red", bgColor="blue")
+ @test XLSX.getFill(s, 2, 2).fill == Dict("patternFill" => Dict("bgrgb" => "FF0000FF", "patternType" => "lightGrid", "fgrgb" => "FFFF0000"))
+ XLSX.setFill(s, [2, 3], 1:3; pattern="lightGrid", fgColor="Red", bgColor="blue")
+ @test XLSX.getFill(s, 3, 1).fill == Dict("patternFill" => Dict("bgrgb" => "FF0000FF", "patternType" => "lightGrid", "fgrgb" => "FFFF0000"))
+ @test XLSX.getFill(s, 2, 2).fill == Dict("patternFill" => Dict("bgrgb" => "FF0000FF", "patternType" => "lightGrid", "fgrgb" => "FFFF0000"))
+ @test XLSX.getFill(s, 3, 3).fill == Dict("patternFill" => Dict("bgrgb" => "FF0000FF", "patternType" => "lightGrid", "fgrgb" => "FFFF0000"))
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, "A1"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, "A1:A1"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, "A"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, "1"; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, 1, 1; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, [1], 1; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, 1, 1:1; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, 1, :; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, :, :; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, :; horizontal="right", vertical="justify", wrapText=true)
+ @test_throws XLSX.XLSXError XLSX.setAlignment(s, [2, 3], 1:3; horizontal="right", vertical="justify", wrapText=true) == -1
+ @test_throws XLSX.XLSXError XLSX.getAlignment(s, 2, 1)
+ @test_throws XLSX.XLSXError XLSX.getAlignment(s, 3, 2)
+ @test_throws XLSX.XLSXError XLSX.getAlignment(s, 2, 3)
+ s[1:3, 1:3] = ""
+ XLSX.setAlignment(s, "A1"; horizontal="right", vertical="justify", wrapText=true)
+ @test XLSX.getAlignment(s, "A1").alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1"))
+ XLSX.setAlignment(s, 2, 2; horizontal="right", vertical="justify", wrapText=true, rotation=90)
+ @test XLSX.getAlignment(s, 2, 2).alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1", "textRotation" => "90"))
+ XLSX.setAlignment(s, [2, 3], 1:3; horizontal="right", vertical="justify", shrink=true, rotation=90)
+ @test XLSX.getAlignment(s, 3, 1).alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "shrinkToFit" => "1", "textRotation" => "90"))
+ @test XLSX.getAlignment(s, 2, 2).alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "wrapText" => "1", "shrinkToFit" => "1", "textRotation" => "90"))
+ @test XLSX.getAlignment(s, 3, 3).alignment == Dict("alignment" => Dict("horizontal" => "right", "vertical" => "justify", "shrinkToFit" => "1", "textRotation" => "90"))
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ @test_throws XLSX.XLSXError XLSX.setFormat(s, "A1"; format="Percentage")
+ @test_throws XLSX.XLSXError XLSX.setFormat(s, "A1:A1"; format="Percentage")
+ @test_throws XLSX.XLSXError XLSX.setFormat(s, "A"; format="Percentage")
+ @test_throws XLSX.XLSXError XLSX.setFormat(s, "1"; format="Percentage")
+ @test_throws XLSX.XLSXError XLSX.setFormat(s, 1, 1; format="Percentage")
+ @test_throws XLSX.XLSXError XLSX.setFormat(s, [1], 1; format="Percentage")
+ @test_throws XLSX.XLSXError XLSX.setFormat(s, 1, 1:1; format="Percentage")
+ @test_throws XLSX.XLSXError XLSX.setFormat(s, 1, :; format="Percentage")
+ @test_throws XLSX.XLSXError XLSX.setFormat(s, :, :; format="Percentage")
+ @test_throws XLSX.XLSXError XLSX.setFormat(s, :; format="Percentage")
+ @test_throws XLSX.XLSXError XLSX.setFormat(s, [2, 3], 1:3; format="Percentage") == -1
+ @test_throws XLSX.XLSXError XLSX.getFormat(s, 2, 1)
+ @test_throws XLSX.XLSXError XLSX.getFormat(s, 3, 2)
+ @test_throws XLSX.XLSXError XLSX.getFormat(s, 2, 3)
+ s[1:3, 1:3] = ""
+ XLSX.setFormat(s, "A1"; format="#,##0.000;(#,##0.000)")
+ @test XLSX.getFormat(s, "A1").format == Dict("numFmt" => Dict("formatCode" => "#,##0.000;(#,##0.000)"))
+ XLSX.setFormat(s, 2, 2; format="Currency")
+ @test XLSX.getFormat(s, 2, 2).format == Dict("numFmt" => Dict("numFmtId" => "7", "formatCode" => "\$#,##0.00_);(\$#,##0.00)"))
+ XLSX.setFormat(s, [2, 3], 1:3; format="LongDate")
+ @test XLSX.getFormat(s, 3, 1).format == Dict("numFmt" => Dict("numFmtId" => "15", "formatCode" => "d-mmm-yy"))
+ @test XLSX.getFormat(s, 2, 2).format == Dict("numFmt" => Dict("numFmtId" => "15", "formatCode" => "d-mmm-yy"))
+ @test XLSX.getFormat(s, 3, 3).format == Dict("numFmt" => Dict("numFmtId" => "15", "formatCode" => "d-mmm-yy"))
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ @test_throws XLSX.XLSXError XLSX.getColumnWidth(s, "B2") # Cell outside sheet dimension
+ s[1:3, 1:3] = ""
+ XLSX.setColumnWidth(s, "A1"; width=30)
+ @test XLSX.getColumnWidth(s, "A1") ≈ 30.7109375
+ XLSX.setColumnWidth(s, 2, 2; width=40)
+ @test XLSX.getColumnWidth(s, 2, 2) ≈ 40.7109375
+ XLSX.setColumnWidth(s, [2, 3], 1:3; width=50)
+ @test XLSX.getColumnWidth(s, 3, 1) ≈ 50.7109375
+ @test XLSX.getColumnWidth(s, 2, 2) ≈ 50.7109375
+ @test XLSX.getColumnWidth(s, 3, 3) ≈ 50.7109375
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ @test_throws XLSX.XLSXError XLSX.getRowHeight(s, "B2") # Cell outside sheet dimension
+ s[1:3, 1:3] = ""
+ XLSX.setRowHeight(s, "A1"; height=30)
+ @test XLSX.getRowHeight(s, "A1") ≈ 30.2109375
+ XLSX.setRowHeight(s, 2, 2; height=40)
+ @test XLSX.getRowHeight(s, 2, 2) ≈ 40.2109375
+ XLSX.setRowHeight(s, [2, 3], 1:3; height=50)
+ @test XLSX.getRowHeight(s, 3, 1) ≈ 50.2109375
+ @test XLSX.getRowHeight(s, 2, 2) ≈ 50.2109375
+ @test XLSX.getRowHeight(s, 3, 3) ≈ 50.2109375
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s[1:30, 1:26] = ""
+ XLSX.setUniformFont(s, 1:4, :; size=12, name="Times New Roman", color="FF040404")
+ @test XLSX.getFont(f, "Sheet1!A1").font == Dict("sz" => Dict("val" => "12"), "name" => Dict("val" => "Times New Roman"), "color" => Dict("rgb" => "FF040404"))
+ @test XLSX.getFont(f, "Sheet1!G2").font == Dict("sz" => Dict("val" => "12"), "name" => Dict("val" => "Times New Roman"), "color" => Dict("rgb" => "FF040404"))
+ @test XLSX.getFont(f, "Sheet1!N3").font == Dict("sz" => Dict("val" => "12"), "name" => Dict("val" => "Times New Roman"), "color" => Dict("rgb" => "FF040404"))
+ @test XLSX.getFont(f, "Sheet1!Y4").font == Dict("sz" => Dict("val" => "12"), "name" => Dict("val" => "Times New Roman"), "color" => Dict("rgb" => "FF040404"))
+
+ XLSX.setUniformFill(s, :, 2:8; pattern="lightGrid", fgColor="FF0000FF", bgColor="FF00FF00")
+ @test XLSX.getFill(s, "B10").fill == Dict("patternFill" => Dict("bgrgb" => "FF00FF00", "patternType" => "lightGrid", "fgrgb" => "FF0000FF"))
+ @test XLSX.getFill(s, "D20").fill == Dict("patternFill" => Dict("bgrgb" => "FF00FF00", "patternType" => "lightGrid", "fgrgb" => "FF0000FF"))
+ @test XLSX.getFill(s, "F30").fill == Dict("patternFill" => Dict("bgrgb" => "FF00FF00", "patternType" => "lightGrid", "fgrgb" => "FF0000FF"))
+
+ XLSX.setUniformFormat(s, :; format="#,##0.000")
+ @test XLSX.getFormat(s, "A1").format == Dict("numFmt" => Dict("formatCode" => "#,##0.000"))
+ @test XLSX.getFormat(s, "G10").format == Dict("numFmt" => Dict("formatCode" => "#,##0.000"))
+ @test XLSX.getFormat(s, "M20").format == Dict("numFmt" => Dict("formatCode" => "#,##0.000"))
+ @test XLSX.getFormat(s, "X30").format == Dict("numFmt" => Dict("formatCode" => "#,##0.000"))
+
+ f = XLSX.open_xlsx_template(joinpath(data_directory, "Borders.xlsx"))
+ s = f["Sheet1"]
+ XLSX.setUniformBorder(s, [1, 2, 3, 4], 1:4; left=["style" => "dotted", "color" => "darkseagreen3"],
+ right=["style" => "medium", "color" => "FF765000"],
+ top=["style" => "thick", "color" => "FF230000"],
+ bottom=["style" => "medium", "color" => "FF0000FF"],
+ diagonal=["style" => "none"]
+ )
+ @test XLSX.getBorder(s, 1, 1).border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF9BCD9B"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, 2, 2).border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF9BCD9B"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+ @test XLSX.getBorder(s, 4, 4).border == Dict("left" => Dict("style" => "dotted", "rgb" => "FF9BCD9B"), "bottom" => Dict("style" => "medium", "rgb" => "FF0000FF"), "right" => Dict("style" => "medium", "rgb" => "FF765000"), "top" => Dict("style" => "thick", "rgb" => "FF230000"), "diagonal" => nothing)
+
+ end
+ @testset "existing formatting" begin
+ f = XLSX.opentemplate(joinpath(data_directory, "customXml.xlsx"))
+ s = f[1]
+ s["B2"] = pi
+ s["D20"] = "Hello World"
+ s["J45"] = Dates.Date(2025, 01, 24)
+ @test XLSX.getFont(s, "B2").font == Dict("name" => Dict("val" => "Calibri"), "family" => Dict("val" => "2"), "b" => nothing, "sz" => Dict("val" => "18"), "color" => Dict("theme" => "1"), "scheme" => Dict("val" => "minor"))
+ @test XLSX.getFill(s, "D20").fill == Dict("patternFill" => Dict("bgindexed" => "64", "patternType" => "solid", "fgtint" => "-0.499984740745262", "fgtheme" => "2"))
+ @test XLSX.getBorder(s, "J45").border == Dict("left" => Dict("indexed" => "64", "style" => "thin"), "bottom" => Dict("indexed" => "64", "style" => "thin"), "right" => Dict("indexed" => "64", "style" => "thin"), "top" => Dict("indexed" => "64", "style" => "thin"), "diagonal" => nothing)
+ end
+end
+
+@testset "Conditional Formats" begin
+
+ @testset "DataBar" begin
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5, j in 1:5
+ s[i, j] = i + j
+ end
+
+ @test_throws MethodError XLSX.setConditionalFormat(s, "A1,A3", :dataBar) # Non-contiguous ranges not allowed
+ @test_throws MethodError XLSX.setConditionalFormat(s, [1], 1, :dataBar) # Vectors may be non-contiguous
+ @test_throws MethodError XLSX.setConditionalFormat(s, 1, 1:3:7, :dataBar) # StepRange is non-contiguous
+ @test XLSX.setConditionalFormat(s, "1:1", :dataBar) == 0
+ @test XLSX.setConditionalFormat(s, 2, :, :dataBar; databar="greengrad") == 0
+ @test XLSX.setConditionalFormat(s, 3, 1:5, :dataBar;
+ min_type="least",
+ min_val="green", #should be ignored because type=least
+ max_type="percentile",
+ max_val="50",
+ ) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A4:E4", :dataBar;
+ min_type="automatic",
+ max_type="automatic",
+ ) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!A5:E5", :dataBar;
+ min_type="num",
+ min_val="\$A\$1",
+ max_type="formula",
+ max_val="\$A\$2"
+ ) == 0
+ @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("A5:E5") => (type="dataBar", priority=5), XLSX.CellRange("A4:E4") => (type="dataBar", priority=4), XLSX.CellRange("A3:E3") => (type="dataBar", priority=3), XLSX.CellRange("A2:E2") => (type="dataBar", priority=2), XLSX.CellRange("A1:E1") => (type="dataBar", priority=1)]
+ @test XLSX.setConditionalFormat(s, "A1", :dataBar) == 0
+ @test XLSX.setConditionalFormat(s, "A1:C3", :dataBar) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1", :dataBar) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1:A2", :dataBar) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!1:2", :dataBar) == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :dataBar) == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :dataBar) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :dataBar) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!1:2", :dataBar) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!A:C", :dataBar) == 0
+ @test XLSX.setConditionalFormat(s, :, 1:3, :dataBar) == 0
+ @test XLSX.setConditionalFormat(s, 1:3, :, :dataBar) == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :dataBar) == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :dataBar) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :dataBar) == 0
+ @test XLSX.setConditionalFormat(s, :, :dataBar) == 0
+ @test XLSX.setConditionalFormat(s, :, :, :dataBar) == 0
+ @test length(XLSX.getConditionalFormats(s)) == 22
+ @test XLSX.getConditionalFormats(s) == [
+ XLSX.CellRange("A1:E5") => (type="dataBar", priority=21),
+ XLSX.CellRange("A1:E5") => (type="dataBar", priority=22),
+ XLSX.CellRange("A1:E3") => (type="dataBar", priority=17),
+ XLSX.CellRange("A1:C5") => (type="dataBar", priority=12),
+ XLSX.CellRange("A1:C5") => (type="dataBar", priority=13),
+ XLSX.CellRange("A1:C5") => (type="dataBar", priority=15),
+ XLSX.CellRange("A1:C5") => (type="dataBar", priority=16),
+ XLSX.CellRange("A1:C5") => (type="dataBar", priority=19),
+ XLSX.CellRange("A1:C5") => (type="dataBar", priority=20),
+ XLSX.CellRange("A2:E4") => (type="dataBar", priority=11),
+ XLSX.CellRange("A2:E4") => (type="dataBar", priority=18),
+ XLSX.CellRange("A1:E2") => (type="dataBar", priority=10),
+ XLSX.CellRange("A1:E2") => (type="dataBar", priority=14),
+ XLSX.CellRange("A1:A2") => (type="dataBar", priority=9),
+ XLSX.CellRange("A1:C3") => (type="dataBar", priority=7),
+ XLSX.CellRange("A1:A1") => (type="dataBar", priority=6),
+ XLSX.CellRange("A1:A1") => (type="dataBar", priority=8),
+ XLSX.CellRange("A5:E5") => (type="dataBar", priority=5),
+ XLSX.CellRange("A4:E4") => (type="dataBar", priority=4),
+ XLSX.CellRange("A3:E3") => (type="dataBar", priority=3),
+ XLSX.CellRange("A2:E2") => (type="dataBar", priority=2),
+ XLSX.CellRange("A1:E1") => (type="dataBar", priority=1)
+ ]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5, j in 1:5
+ s[i, j] = i + j
+ end
+ @test XLSX.setConditionalFormat(s, "A1:A5", :dataBar) == 0
+ @test XLSX.setConditionalFormat(s, :, 2, :dataBar; databar="orange") == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!E:E", :dataBar; databar = "purplegrad") == 0
+ @test XLSX.setConditionalFormat(s, 1:5, 3:4, :dataBar;
+ borders = "false",
+ min_type="percentile",
+ min_val="25",
+ max_type="percentile",
+ max_val="75"
+ ) == 0
+ @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("C1:D5") => (type="dataBar", priority=4), XLSX.CellRange("E1:E5") => (type="dataBar", priority=3), XLSX.CellRange("B1:B5") => (type="dataBar", priority=2), XLSX.CellRange("A1:A5") => (type="dataBar", priority=1)]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5, j in 1:5
+ s[i, j] = i + j
+ end
+
+ @test XLSX.setConditionalFormat(s, :, 1:4, :dataBar;
+ databar="red",
+ borders="true",
+ fill_col="blue",
+ border_col="yellow",
+ neg_fill_col="magenta",
+ neg_border_col="green",
+ axis_col="cyan"
+ ) == 0
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5, j in 1:5
+ s[i, j] = i + j
+ end
+ XLSX.addDefinedName(s, "myRange", "A1:B5")
+ @test XLSX.setConditionalFormat(s, "myRange", :dataBar;
+ showVal = "false",
+ direction="leftToRight",
+ borders="true",
+ sameNegBorders="false"
+ ) == 0
+ XLSX.addDefinedName(s, "myNCRange", "C1:C5,D1:D5")
+ @test_throws MethodError XLSX.setConditionalFormat(s, "myNCRange", :dataBar; # Non-contiguous ranges not allowed
+ showVal = "false",
+ direction="leftToRight",
+ borders="true",
+ sameNegBorders="false"
+ )
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "A1:A2", :dataBar;
+ databar="rainbow"
+ )
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5, j in 1:12
+ s[i, j] = i + j
+ end
+ s[1, 13]=5
+
+ @test XLSX.setConditionalFormat(s, :, 1, :dataBar;
+ databar="orange",
+ sameNegFill="true",
+ sameNegBorders="true"
+ )==0
+ @test XLSX.setConditionalFormat(s, :, 2, :dataBar;
+ databar="orange",
+ axis_pos="none"
+ )==0
+ @test XLSX.setConditionalFormat(s, :, 3, :dataBar;
+ databar="orange",
+ axis_pos="middle"
+ )==0
+ @test XLSX.setConditionalFormat(s, :, 4, :dataBar;
+ databar="orange",
+ min_type="num",
+ min_val="Sheet1!\$M\$1"
+ )==0
+ @test XLSX.setConditionalFormat(s, :, 5, :dataBar;
+ databar="orange",
+ showVal = "false",
+ direction="rightToLeft",
+ borders="true",
+ sameNegBorders="false",
+ sameNegFill="false"
+ ) == 0
+
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, :, 4, :dataBar;
+ axis_pos="nonsense",
+ databar="orange",
+ min_type="num",
+ min_val="Sheet1!\$M\$1"
+ )
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, :, 4, :dataBar;
+ borders="nonsense",
+ databar="orange",
+ min_type="num",
+ min_val="Sheet1!\$M\$1"
+ )
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, :, 4, :dataBar;
+ fill_col="nonsense",
+ databar="orange",
+ min_type="num",
+ min_val="Sheet1!\$M\$1"
+ )
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, :, 4, :dataBar;
+ sameNegFill="nonsense",
+ databar="orange",
+ min_type="num",
+ min_val="Sheet1!\$M\$1"
+ )
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, :, 4, :dataBar;
+ databar="orange",
+ min_type="num",
+ min_val="Sheet2!\$M\$1"
+ )
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5, j in 1:12
+ s[i, j] = i + j
+ end
+ for (j, k) in enumerate(keys(XLSX.databars))
+ @test XLSX.setConditionalFormat(s, :, j, :dataBar; databar=k)==0
+ end
+ end
+
+ @testset "colorScale" begin
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5, j in 1:5
+ s[i, j] = i + j
+ end
+
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "A1,A3", :wrongOne)
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, 1, 2, :wrongOne)
+
+ @test_throws MethodError XLSX.setConditionalFormat(s, "A1,A3", :colorScale) # Non-contiguous ranges not allowed
+ @test_throws MethodError XLSX.setConditionalFormat(s, [1], 1, :colorScale) # Vectors may be non-contiguous
+ @test_throws MethodError XLSX.setConditionalFormat(s, 1, 1:3:7, :colorScale) # StepRange is non-contiguous
+ @test XLSX.setConditionalFormat(s, "1:1", :colorScale) == 0
+ @test XLSX.setConditionalFormat(s, 2, :, :colorScale; colorscale="redwhiteblue") == 0
+ @test XLSX.setConditionalFormat(s, 3, 1:5, :colorScale;
+ min_type="min",
+ min_col="green",
+ mid_type="percentile",
+ mid_val="50",
+ mid_col="red",
+ max_type="max",
+ max_col="blue"
+ ) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A4:E4", :colorScale;
+ min_type="min",
+ min_col="tomato",
+ max_type="max",
+ max_col="gold4"
+ ) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!A5:E5", :colorScale;
+ min_type="min",
+ min_col="yellow",
+ max_type="max",
+ max_col="darkgreen"
+ ) == 0
+ @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("A5:E5") => (type="colorScale", priority=5), XLSX.CellRange("A4:E4") => (type="colorScale", priority=4), XLSX.CellRange("A3:E3") => (type="colorScale", priority=3), XLSX.CellRange("A2:E2") => (type="colorScale", priority=2), XLSX.CellRange("A1:E1") => (type="colorScale", priority=1)]
+ @test XLSX.setConditionalFormat(s, "A1", :colorScale) == 0
+ @test XLSX.setConditionalFormat(s, "A1:C3", :colorScale) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1", :colorScale) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1:A2", :colorScale) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!1:2", :colorScale) == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :colorScale) == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :colorScale) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :colorScale) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!1:2", :colorScale) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!A:C", :colorScale) == 0
+ @test XLSX.setConditionalFormat(s, :, 1:3, :colorScale) == 0
+ @test XLSX.setConditionalFormat(s, 1:3, :, :colorScale) == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :colorScale) == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :colorScale) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :colorScale) == 0
+ @test XLSX.setConditionalFormat(s, :, :colorScale) == 0
+ @test XLSX.setConditionalFormat(s, :, :, :colorScale) == 0
+ @test length(XLSX.getConditionalFormats(s)) == 22
+ @test XLSX.getConditionalFormats(s) == [
+ XLSX.CellRange("A1:E5") => (type="colorScale", priority=21),
+ XLSX.CellRange("A1:E5") => (type="colorScale", priority=22),
+ XLSX.CellRange("A1:E3") => (type="colorScale", priority=17),
+ XLSX.CellRange("A1:C5") => (type="colorScale", priority=12),
+ XLSX.CellRange("A1:C5") => (type="colorScale", priority=13),
+ XLSX.CellRange("A1:C5") => (type="colorScale", priority=15),
+ XLSX.CellRange("A1:C5") => (type="colorScale", priority=16),
+ XLSX.CellRange("A1:C5") => (type="colorScale", priority=19),
+ XLSX.CellRange("A1:C5") => (type="colorScale", priority=20),
+ XLSX.CellRange("A2:E4") => (type="colorScale", priority=11),
+ XLSX.CellRange("A2:E4") => (type="colorScale", priority=18),
+ XLSX.CellRange("A1:E2") => (type="colorScale", priority=10),
+ XLSX.CellRange("A1:E2") => (type="colorScale", priority=14),
+ XLSX.CellRange("A1:A2") => (type="colorScale", priority=9),
+ XLSX.CellRange("A1:C3") => (type="colorScale", priority=7),
+ XLSX.CellRange("A1:A1") => (type="colorScale", priority=6),
+ XLSX.CellRange("A1:A1") => (type="colorScale", priority=8),
+ XLSX.CellRange("A5:E5") => (type="colorScale", priority=5),
+ XLSX.CellRange("A4:E4") => (type="colorScale", priority=4),
+ XLSX.CellRange("A3:E3") => (type="colorScale", priority=3),
+ XLSX.CellRange("A2:E2") => (type="colorScale", priority=2),
+ XLSX.CellRange("A1:E1") => (type="colorScale", priority=1)
+ ]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5, j in 1:5
+ s[i, j] = i + j
+ end
+ @test XLSX.setConditionalFormat(s, "A1:A5", :colorScale) == 0
+ @test XLSX.setConditionalFormat(s, :, 2, :colorScale; colorscale="redwhiteblue") == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!E:E", :colorScale; colorscale="greenwhitered") == 0
+ @test XLSX.setConditionalFormat(s, 1:5, 3:4, :colorScale;
+ min_type="min",
+ min_col="green",
+ mid_type="percentile",
+ mid_val="50",
+ mid_col="red",
+ max_type="max",
+ max_col="blue"
+ ) == 0
+ @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("C1:D5") => (type="colorScale", priority=4), XLSX.CellRange("E1:E5") => (type="colorScale", priority=3), XLSX.CellRange("B1:B5") => (type="colorScale", priority=2), XLSX.CellRange("A1:A5") => (type="colorScale", priority=1)]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5, j in 1:5
+ s[i, j] = i + j
+ end
+
+ @test XLSX.setConditionalFormat(s, :, 1:4, :colorScale;
+ min_type="min",
+ min_col="green",
+ mid_type="percentile",
+ mid_val="\$E\$4",
+ mid_col="red",
+ max_type="max",
+ max_col="blue"
+ ) == 0
+ @test XLSX.setConditionalFormat(s, :, 5, :colorScale;
+ min_type="min",
+ min_col="green",
+ mid_type="percentile",
+ mid_val="Sheet1!\$E\$4",
+ mid_col="red",
+ max_type="max",
+ max_col="blue"
+ ) == 0
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, :, 5, :colorScale;
+ min_type="min",
+ min_col="green",
+ mid_type="percentile",
+ mid_val="Sheet2!\$E\$4",
+ mid_col="red",
+ max_type="max",
+ max_col="blue"
+ )
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5, j in 1:5
+ s[i, j] = i + j
+ end
+ XLSX.addDefinedName(s, "myRange", "A1:B5")
+ @test XLSX.setConditionalFormat(s, "myRange", :colorScale;
+ min_type="min",
+ min_col="green",
+ mid_type="percentile",
+ mid_val="50",
+ mid_col="red",
+ max_type="max",
+ max_col="blue"
+ ) == 0
+ XLSX.addDefinedName(s, "myNCRange", "C1:C5,D1:D5")
+ @test_throws MethodError XLSX.setConditionalFormat(s, "myNCRange", :colorScale; # Non-contiguous ranges not allowed
+ min_type="min",
+ min_col="green",
+ mid_type="percentile",
+ mid_val="50",
+ mid_col="red",
+ max_type="max",
+ max_col="blue"
+ )
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "A1:A2", :colorScale;
+ colorscale="rainbow"
+ )
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5, j in 1:12
+ s[i, j] = i + j
+ end
+ for (j, k) in enumerate(keys(XLSX.colorscales))
+ @test XLSX.setConditionalFormat(s, :, j, :colorScale; colorscale=k)==0
+ end
+ end
+
+ @testset "iconSet" begin
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5, j in 1:5
+ s[i, j] = i + j
+ end
+ @test_throws MethodError XLSX.setConditionalFormat(s, "A1,A3", :iconSet) # Non-contiguous ranges not allowed
+ @test_throws MethodError XLSX.setConditionalFormat(s, [1], 1, :iconSet) # Vectors may be non-contiguous
+ @test_throws MethodError XLSX.setConditionalFormat(s, 1, 1:3:7, :iconSet) # StepRange is non-contiguous
+ @test XLSX.setConditionalFormat(s, "1:1", :iconSet) == 0
+ @test XLSX.setConditionalFormat(s, 2, :, :iconSet; iconset="3Arrows") == 0
+ @test XLSX.setConditionalFormat(s, 3, 1:5, :iconSet;
+ min_type="percent",
+ min_val="20",
+ max_type="num",
+ max_val="4"
+ ) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A4:E4", :iconSet;
+ min_type="percentile",
+ min_val="10",
+ max_type="num",
+ max_val="\$C\$4"
+ ) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!A5:E5", :iconSet;
+ min_type="percentile",
+ min_val="\$D\$5",
+ max_type="percent",
+ max_val="95"
+ ) == 0
+ @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("A5:E5") => (type = "iconSet", priority = 5),XLSX.CellRange("A4:E4") => (type = "iconSet", priority = 4), XLSX.CellRange("A3:E3") => (type = "iconSet", priority = 3), XLSX.CellRange("A2:E2") => (type = "iconSet", priority = 2), XLSX.CellRange("A1:E1") => (type = "iconSet", priority = 1)]
+ @test XLSX.setConditionalFormat(s, "A1", :iconSet) == 0
+ @test XLSX.setConditionalFormat(s, "A1:C3", :iconSet) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1", :iconSet) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1:A2", :iconSet) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!1:2", :iconSet) == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :iconSet) == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :iconSet) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :iconSet) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!1:2", :iconSet) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!A:C", :iconSet) == 0
+ @test XLSX.setConditionalFormat(s, :, 1:3, :iconSet) == 0
+ @test XLSX.setConditionalFormat(s, 1:3, :, :iconSet) == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :iconSet) == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :iconSet) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :iconSet) == 0
+ @test XLSX.setConditionalFormat(s, :, :iconSet) == 0
+ @test XLSX.setConditionalFormat(s, :, :, :iconSet) == 0
+ @test length(XLSX.getConditionalFormats(s)) == 22
+
+ @test XLSX.getConditionalFormats(s) == [
+ XLSX.CellRange("A1:E5") => (type = "iconSet", priority = 21),
+ XLSX.CellRange("A1:E5") => (type = "iconSet", priority = 22),
+ XLSX.CellRange("A1:E3") => (type = "iconSet", priority = 17),
+ XLSX.CellRange("A1:C5") => (type = "iconSet", priority = 12),
+ XLSX.CellRange("A1:C5") => (type = "iconSet", priority = 13),
+ XLSX.CellRange("A1:C5") => (type = "iconSet", priority = 15),
+ XLSX.CellRange("A1:C5") => (type = "iconSet", priority = 16),
+ XLSX.CellRange("A1:C5") => (type = "iconSet", priority = 19),
+ XLSX.CellRange("A1:C5") => (type = "iconSet", priority = 20),
+ XLSX.CellRange("A2:E4") => (type = "iconSet", priority = 11),
+ XLSX.CellRange("A2:E4") => (type = "iconSet", priority = 18),
+ XLSX.CellRange("A1:E2") => (type = "iconSet", priority = 10),
+ XLSX.CellRange("A1:E2") => (type = "iconSet", priority = 14),
+ XLSX.CellRange("A1:A2") => (type = "iconSet", priority = 9),
+ XLSX.CellRange("A1:C3") => (type = "iconSet", priority = 7),
+ XLSX.CellRange("A1:A1") => (type = "iconSet", priority = 6),
+ XLSX.CellRange("A1:A1") => (type = "iconSet", priority = 8),
+ XLSX.CellRange("A5:E5") => (type = "iconSet", priority = 5),
+ XLSX.CellRange("A4:E4") => (type = "iconSet", priority = 4),
+ XLSX.CellRange("A3:E3") => (type = "iconSet", priority = 3),
+ XLSX.CellRange("A2:E2") => (type = "iconSet", priority = 2),
+ XLSX.CellRange("A1:E1") => (type = "iconSet", priority = 1)
+ ]
+
+ f=XLSX.newxlsx()
+ s=f[1]
+
+ XLSX.writetable!(s, [collect(1:10), collect(1:10), collect(1:10), collect(1:10), collect(1:10), collect(1:10)],
+ ["normal", "showVal=\"false\"", "reverse=\"true\"", "min_gte=\"false\"", "extra1", "extra2"])
+ s["G1"]=3
+ s["G4"]="y"
+
+ @test XLSX.setConditionalFormat(s, "A2:A11", :iconSet;
+ min_type="num", max_type="formula",
+ min_val="3", max_val="if(\$G\$4=\"y\", \$G\$1+5, 10)") == 0
+
+ @test XLSX.setConditionalFormat(s, "A2:A11", :iconSet;
+ min_type="num", max_type="num",
+ min_val="3", max_val="8") == 0
+
+ @test XLSX.setConditionalFormat(s, "B2:B11", :iconSet; iconset = "4TrafficLights",
+ min_type="num", mid_type="percent", max_type="num",
+ min_val="3", mid_val="50", max_val="8",
+ showVal="false") == 0
+
+ @test XLSX.setConditionalFormat(s, "C2:C11", :iconSet; iconset = "3Symbols2",
+ min_type="num", mid_type="percentile", max_type="num",
+ min_val="3", mid_val="50", max_val="8",
+ reverse="true") == 0
+
+ @test XLSX.setConditionalFormat(s, "D2:D11", :iconSet; iconset = "5Arrows",
+ min_type="num", mid_type="percentile", mid2_type="percentile", max_type="num",
+ min_val="3", mid_val="45", mid2_val="65", max_val="8",
+ min_gte="false", max_gte="false") == 0
+
+ @test XLSX.setConditionalFormat(s, "E2:E11", :iconSet; iconset = "3Stars",
+ reverse = "true",
+ showVal = "false",
+ min_type="num", mid_type="percentile", mid2_type="percentile", max_type="num",
+ min_val="3", mid_val="45", mid2_val="65", max_val="8",
+ min_gte="false", max_gte="false") == 0
+
+ @test XLSX.setConditionalFormat(s, "F2:F11", :iconSet; iconset = "5Boxes",
+ reverse = "true",
+ showVal = "false",
+ min_type="num", mid_type="percentile", mid2_type="percentile", max_type="num",
+ min_val="3", mid_val="45", mid2_val="65", max_val="8",
+ min_gte="false", mid_gte="false", mid2_gte="false", max_gte="false") == 0
+
+ @test XLSX.getConditionalFormats(s) == [
+ XLSX.CellRange("D2:D11") => (type = "iconSet", priority = 5),
+ XLSX.CellRange("C2:C11") => (type = "iconSet", priority = 4),
+ XLSX.CellRange("B2:B11") => (type = "iconSet", priority = 3),
+ XLSX.CellRange("A2:A11") => (type = "iconSet", priority = 1),
+ XLSX.CellRange("A2:A11") => (type = "iconSet", priority = 2),
+ XLSX.CellRange("E2:E11") => (type = "iconSet", priority = 6),
+ XLSX.CellRange("F2:F11") => (type = "iconSet", priority = 7)
+ ]
+
+ f=XLSX.newxlsx()
+ s=f[1]
+ for i = 0:3
+ for j=1:13
+ s[i+1,j]=i*13+j
+ end
+ end
+ for j=1:13
+ @test XLSX.setConditionalFormat(s, 1:4, j, :iconSet; # Create a custom 4-icon set in each column.
+ iconset="Custom",
+ icon_list=[j, 13+j, 26+j, 39+j],
+ min_type="percent", mid_type="percent", max_type="percent",
+ min_val="25", mid_val="50", max_val="75"
+ ) == 0
+ end
+
+ @test XLSX.setConditionalFormat(s, 1:4, 1, :iconSet;
+ iconset="Custom",
+ icon_list = [1,2,3,4,5],
+ min_type="percent", max_type="percent",
+ min_val="25", max_val="75",
+ min_gte="false", max_gte="false"
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:4, 1, :iconSet;
+ iconset="Custom",
+ showVal = "false",
+ icon_list = [1,2,3,4,5],
+ min_type="percent", mid_type="percent", max_type="percent",
+ min_val="25", mid_val="50", max_val="75"
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:4, 1, :iconSet;
+ iconset="Custom",
+ reverse="true",
+ icon_list = [1,2,3,4,5],
+ min_type="percent", mid_type="percent", mid2_type="percentile", max_type="percent",
+ min_val="25", mid_val="50", mid2_val="60", max_val="75"
+ ) == 0
+
+ @test XLSX.setConditionalFormat(s, "A2:M2", :iconSet;
+ iconset = "Custom",
+ icon_list = [31,24,11],
+ min_type="num", max_type="formula",
+ min_val="3", max_val="if(\$G\$4=\"y\", \$G\$1+5, 10)") == 0
+
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, 1:4, 1, :iconSet;
+ iconset="Custom",
+ icon_list = [1,2,3,4,5],
+ min_type="percent", mid_type="madeUp", mid2_type="percentile", max_type="num",
+ min_val="25", mid_val="50", mid2_val="60", max_val="75"
+ )
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, 1:4, 1, :iconSet;
+ iconset="Custom",
+ icon_list = [99,2,3,4,5],
+ min_type="percent", mid_type="percent", mid2_type="percentile", max_type="num",
+ min_val="25", mid_val="50", mid2_val="60", max_val="75"
+ )
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, 1:4, 1, :iconSet;
+ iconset="Custom",
+ min_type="percent", mid_type="percent", max_type="percent",
+ min_val="25", mid_val="50", max_val="75"
+ )
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, 1:4, 1, :iconSet;
+ iconset="Custom",
+ icon_list=[],
+ min_type="percent", mid_type="percent", max_type="percent",
+ min_val="25", mid_val="50", max_val="75"
+ )
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, 1:4, 1, :iconSet;
+ iconset="Custom",
+ icon_list=[1, 13, 26],
+ min_type="percent", mid_type="percent", max_type="percent",
+ min_val="25", mid_val="50", max_val="75"
+ )
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, 1:4, 1, :iconSet;
+ iconset="Custom",
+ icon_list=[1, 13, 26, 39],
+ min_type="percent", max_type="percent",
+ min_val="25"
+ )==0
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, 1:4, 1, :iconSet;
+ iconset="Custom",
+ icon_list=[1, 13, 26, 39],
+ min_type="percent",
+ min_val="25", max_val="75"
+ )==0
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, 1:4, 1, :iconSet;
+ iconset="Custom",
+ icon_list=[1, 13, 26, 39]
+ )==0
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, 1:4, 1, :iconSet;
+ iconset="10ThousandManiacs",
+ )==0
+
+
+ @test XLSX.getConditionalFormats(s) == [
+ XLSX.CellRange("A1:A4") => (type = "iconSet", priority = 16),
+ XLSX.CellRange("A1:A4") => (type = "iconSet", priority = 15),
+ XLSX.CellRange("A1:A4") => (type = "iconSet", priority = 14),
+ XLSX.CellRange("A1:A4") => (type = "iconSet", priority = 1),
+ XLSX.CellRange("B1:B4") => (type = "iconSet", priority = 2),
+ XLSX.CellRange("C1:C4") => (type = "iconSet", priority = 3),
+ XLSX.CellRange("D1:D4") => (type = "iconSet", priority = 4),
+ XLSX.CellRange("E1:E4") => (type = "iconSet", priority = 5),
+ XLSX.CellRange("F1:F4") => (type = "iconSet", priority = 6),
+ XLSX.CellRange("G1:G4") => (type = "iconSet", priority = 7),
+ XLSX.CellRange("H1:H4") => (type = "iconSet", priority = 8),
+ XLSX.CellRange("I1:I4") => (type = "iconSet", priority = 9),
+ XLSX.CellRange("J1:J4") => (type = "iconSet", priority = 10),
+ XLSX.CellRange("K1:K4") => (type = "iconSet", priority = 11),
+ XLSX.CellRange("L1:L4") => (type = "iconSet", priority = 12),
+ XLSX.CellRange("M1:M4") => (type = "iconSet", priority = 13),
+ XLSX.CellRange("A2:M2") => (type = "iconSet", priority = 17)
+ ]
+
+ XLSX.addDefinedName(s, "myRange", "A1:B2")
+ XLSX.addDefinedName(s, "myNCRange", "C1:C5,D1:D5")
+ @test XLSX.setConditionalFormat(s, "myRange", :iconSet) == 0
+ @test_throws MethodError XLSX.setConditionalFormat(s, "myNCRange", :iconSet)
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5, j in 1:21
+ s[i, j] = i + j
+ end
+ for (j, k) in enumerate(keys(XLSX.iconsets))
+ if k=="Custom"
+ @test XLSX.setConditionalFormat(s, :, j, :iconSet;
+ iconset=k,
+ icon_list=[1,2,3,4,5],
+ min_type="num", mid_type="num", mid2_type="num", max_type="num",
+ min_val="8", mid_val="12", mid2_val="15", max_val="18",
+ )==0
+ else
+ @test XLSX.setConditionalFormat(s, :, j, :iconSet; iconset=k)==0
+ end
+ end
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:3, j in 1:21
+ s[i, j] = i + j
+ end
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1:E1", :iconSet;
+ min_type="percentile",
+ min_val="10",
+ max_type="num",
+ max_val="Sheet1!\$C\$4"
+ ) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!A2:E2", :iconSet;
+ min_type="percentile",
+ min_val="Sheet1!\$D\$5",
+ max_type="percent",
+ max_val="95"
+ ) == 0
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "Sheet1!A1:E1", :iconSet;
+ min_type="percentile",
+ min_val="10",
+ max_type="num",
+ max_val="Sheet2!\$C\$4"
+ )
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(f, "Sheet1!A5:E5", :iconSet;
+ min_type="percentile",
+ min_val="Sheet2!\$D\$5",
+ max_type="percent",
+ max_val="95"
+ )
+ @test XML.tag(XLSX.get_x14_icon("3Triangles")) == "x14:cfRule"
+ @test XML.attributes(XLSX.get_x14_icon("3Stars")) == XML.OrderedDict("type" => "iconSet", "priority" => "1", "id" => "XXXX-xxxx-XXXX")
+ @test length(XML.children(XLSX.get_x14_icon("5Boxes"))) == 1
+ @test typeof(XLSX.get_x14_icon("Custom")) == XML.Node
+ end
+
+ @testset "cellIs" begin
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5, j in 1:5
+ s[i, j] = i + j
+ end
+ @test_throws MethodError XLSX.setConditionalFormat(s, "A1,A3", :cellIs) # Non-contiguous ranges not allowed
+ @test_throws MethodError XLSX.setConditionalFormat(s, [1], 1, :cellIs) # Vectors may be non-contiguous
+ @test_throws MethodError XLSX.setConditionalFormat(s, 1, 1:3:7, :cellIs) # StepRange is non-contiguous
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "A1:A3", :cellIs; dxStyle="madeUp") # dxStyle invalid
+ @test XLSX.setConditionalFormat(s, "1:1", :cellIs) == 0
+ @test XLSX.setConditionalFormat(s, 2, :, :cellIs; dxStyle="greenfilltext") == 0
+ @test XLSX.setConditionalFormat(s, 3, 1:5, :cellIs;
+ operator="between",
+ value="2",
+ value2="3",
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "FFFFC7CE"],
+ format=["format" => "0.00%"],
+ font=["color" => "blue", "bold" => "true"]
+ ) == 0
+
+ @test XLSX.setConditionalFormat(s, "Sheet1!A4:E4", :cellIs;
+ operator="greaterThan",
+ value="4",
+ fill=["pattern" => "none", "bgColor" => "green"],
+ format=["format" => "0.0"],
+ font=["color" => "red", "italic" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!A5:E5", :cellIs;
+ operator="lessThan",
+ value="2",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "thick", "color" => "coral"]
+ ) == 0
+ @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("A5:E5") => (type="cellIs", priority=5), XLSX.CellRange("A4:E4") => (type="cellIs", priority=4), XLSX.CellRange("A3:E3") => (type="cellIs", priority=3), XLSX.CellRange("A2:E2") => (type="cellIs", priority=2), XLSX.CellRange("A1:E1") => (type="cellIs", priority=1)]
+ @test XLSX.setConditionalFormat(s, "A1", :cellIs) == 0
+ @test XLSX.setConditionalFormat(s, "A1:C3", :cellIs) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1", :cellIs) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1:A2", :cellIs) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!1:2", :cellIs) == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :cellIs) == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :cellIs) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :cellIs) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!1:2", :cellIs) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!A:C", :cellIs) == 0
+ @test XLSX.setConditionalFormat(s, :, 1:3, :cellIs) == 0
+ @test XLSX.setConditionalFormat(s, 1:3, :, :cellIs) == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :cellIs) == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :cellIs) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :cellIs) == 0
+ @test XLSX.setConditionalFormat(s, :, :cellIs) == 0
+ @test XLSX.setConditionalFormat(s, :, :, :cellIs) == 0
+ @test length(XLSX.getConditionalFormats(s)) == 22
+ @test XLSX.getConditionalFormats(s) == [
+ XLSX.CellRange("A1:E5") => (type="cellIs", priority=21),
+ XLSX.CellRange("A1:E5") => (type="cellIs", priority=22),
+ XLSX.CellRange("A1:E3") => (type="cellIs", priority=17),
+ XLSX.CellRange("A1:C5") => (type="cellIs", priority=12),
+ XLSX.CellRange("A1:C5") => (type="cellIs", priority=13),
+ XLSX.CellRange("A1:C5") => (type="cellIs", priority=15),
+ XLSX.CellRange("A1:C5") => (type="cellIs", priority=16),
+ XLSX.CellRange("A1:C5") => (type="cellIs", priority=19),
+ XLSX.CellRange("A1:C5") => (type="cellIs", priority=20),
+ XLSX.CellRange("A2:E4") => (type="cellIs", priority=11),
+ XLSX.CellRange("A2:E4") => (type="cellIs", priority=18),
+ XLSX.CellRange("A1:E2") => (type="cellIs", priority=10),
+ XLSX.CellRange("A1:E2") => (type="cellIs", priority=14),
+ XLSX.CellRange("A1:A2") => (type="cellIs", priority=9),
+ XLSX.CellRange("A1:C3") => (type="cellIs", priority=7),
+ XLSX.CellRange("A1:A1") => (type="cellIs", priority=6),
+ XLSX.CellRange("A1:A1") => (type="cellIs", priority=8),
+ XLSX.CellRange("A5:E5") => (type="cellIs", priority=5),
+ XLSX.CellRange("A4:E4") => (type="cellIs", priority=4),
+ XLSX.CellRange("A3:E3") => (type="cellIs", priority=3),
+ XLSX.CellRange("A2:E2") => (type="cellIs", priority=2),
+ XLSX.CellRange("A1:E1") => (type="cellIs", priority=1)
+ ]
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "Sheet1!A4:E4", :cellIs;
+ operator="madeUp",
+ value="4",
+ fill=["pattern" => "none", "bgColor" => "green"],
+ format=["format" => "0.0"],
+ font=["color" => "red", "italic" => "true"]
+ )
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5, j in 1:5
+ s[i, j] = i + j
+ end
+ XLSX.setConditionalFormat(s, "A1:A5", :cellIs)
+ XLSX.setConditionalFormat(s, :, 2, :cellIs; dxStyle="redborder")
+ XLSX.setConditionalFormat(s, "Sheet1!E:E", :cellIs; dxStyle="redfilltext")
+ XLSX.setConditionalFormat(s, 1:5, 3:4, :cellIs;
+ operator="between",
+ value="2",
+ value2="4",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "thick", "color" => "coral"]
+ ) == 0
+
+ @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("C1:D5") => (type="cellIs", priority=4), XLSX.CellRange("E1:E5") => (type="cellIs", priority=3), XLSX.CellRange("B1:B5") => (type="cellIs", priority=2), XLSX.CellRange("A1:A5") => (type="cellIs", priority=1)]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5, j in 1:5
+ s[i, j] = i + j
+ end
+
+ @test XLSX.setConditionalFormat(s, :, 1:4, :cellIs;
+ operator="lessThan",
+ value="\$E\$4",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green", "under" => "double"],
+ border=["style" => "thin", "color" => "coral"]
+ ) == 0
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5, j in 1:5
+ s[i, j] = i + j
+ end
+ XLSX.addDefinedName(s, "myRange", "A1:B5")
+ @test XLSX.setConditionalFormat(s, "myRange", :cellIs;
+ operator="lessThan",
+ value="2",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "hair", "color" => "cyan"]
+ ) == 0
+ XLSX.addDefinedName(s, "myNCRange", "C1:C5,D1:D5")
+ @test_throws MethodError XLSX.setConditionalFormat(s, "myNCRange", :cellIs; # Non-contiguous ranges not allowed
+ operator="lessThan",
+ value="2",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "hair", "color" => "cyan"]
+ )
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:5, j in 1:6
+ s[i, j] = i + j
+ end
+ for (j, k) in enumerate(keys(XLSX.highlights))
+ @test XLSX.setConditionalFormat(s, :, j, :cellIs; dxStyle=k)==0
+ end
+ end
+
+
+ @testset "containsText" begin
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:E1"] = "Hello World"
+ s["A2:E2"] = "Life the universe and everything"
+ s["A3:E3"] = "Once upon a time"
+ s["A4:E4"] = "In America"
+ s["A5:E5"] = "a"
+ @test_throws MethodError XLSX.setConditionalFormat(s, "A1,A3", :containsText; value="a") # Non-contiguous ranges not allowed
+ @test_throws MethodError XLSX.setConditionalFormat(s, [1], 1, :containsText; value="a") # Vectors may be non-contiguous
+ @test_throws MethodError XLSX.setConditionalFormat(s, 1, 1:3:7, :containsText; value="a") # StepRange is non-contiguous
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "1:1", :containsText) # value must be defined
+ @test XLSX.setConditionalFormat(s, "1:1", :containsText; value="a") == 0
+ @test XLSX.setConditionalFormat(s, 2, :, :containsText; value="a", dxStyle="greenfilltext") == 0
+ @test XLSX.setConditionalFormat(s, 3, 1:5, :containsText;
+ operator="notContainsText",
+ value="a",
+ fill=["pattern" => "none", "bgColor" => "FFFFC7CE"],
+ format=["format" => "0.00%"],
+ font=["color" => "blue", "bold" => "true"]
+ ) == 0
+
+ @test XLSX.setConditionalFormat(s, "Sheet1!A4:E4", :containsText;
+ operator="notContainsText",
+ value="a",
+ fill=["pattern" => "none", "bgColor" => "green"],
+ format=["format" => "0.0"],
+ font=["color" => "red", "italic" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!A5:E5", :containsText;
+ operator="beginsWith",
+ value="a",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "thick", "color" => "coral"]
+ ) == 0
+ @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("A5:E5") => (type="beginsWith", priority=5), XLSX.CellRange("A4:E4") => (type="notContainsText", priority=4), XLSX.CellRange("A3:E3") => (type="notContainsText", priority=3), XLSX.CellRange("A2:E2") => (type="containsText", priority=2), XLSX.CellRange("A1:E1") => (type="containsText", priority=1)]
+ # @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("A5:E5") => (type = "containsText", priority = 5), XLSX.CellRange("A4:E4") => (type = "containsText", priority = 4), XLSX.CellRange("A3:E3") => (type = "containsText", priority = 3), XLSX.CellRange("A2:E2") => (type = "containsText", priority = 2), XLSX.CellRange("A1:E1") => (type = "containsText", priority = 1)]
+ @test XLSX.setConditionalFormat(s, "A1", :containsText; value="a") == 0
+ @test XLSX.setConditionalFormat(s, "A1:C3", :containsText; value="a") == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1", :containsText; value="a") == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1:A2", :containsText; value="a") == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!1:2", :containsText; value="a") == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :containsText; value="a") == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :containsText; value="a") == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :containsText; value="a") == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!1:2", :containsText; value="a") == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!A:C", :containsText; value="a") == 0
+ @test XLSX.setConditionalFormat(s, :, 1:3, :containsText; value="a") == 0
+ @test XLSX.setConditionalFormat(s, 1:3, :, :containsText; value="a") == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :containsText; value="a") == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :containsText; value="a") == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :containsText; value="a") == 0
+ @test XLSX.setConditionalFormat(s, :, :containsText; value="a") == 0
+ @test XLSX.setConditionalFormat(s, :, :, :containsText; value="a") == 0
+ @test length(XLSX.getConditionalFormats(s)) == 22
+ @test XLSX.getConditionalFormats(s) == [
+ XLSX.CellRange("A1:E5") => (type="containsText", priority=21),
+ XLSX.CellRange("A1:E5") => (type="containsText", priority=22),
+ XLSX.CellRange("A1:E3") => (type="containsText", priority=17),
+ XLSX.CellRange("A1:C5") => (type="containsText", priority=12),
+ XLSX.CellRange("A1:C5") => (type="containsText", priority=13),
+ XLSX.CellRange("A1:C5") => (type="containsText", priority=15),
+ XLSX.CellRange("A1:C5") => (type="containsText", priority=16),
+ XLSX.CellRange("A1:C5") => (type="containsText", priority=19),
+ XLSX.CellRange("A1:C5") => (type="containsText", priority=20),
+ XLSX.CellRange("A2:E4") => (type="containsText", priority=11),
+ XLSX.CellRange("A2:E4") => (type="containsText", priority=18),
+ XLSX.CellRange("A1:E2") => (type="containsText", priority=10),
+ XLSX.CellRange("A1:E2") => (type="containsText", priority=14),
+ XLSX.CellRange("A1:A2") => (type="containsText", priority=9),
+ XLSX.CellRange("A1:C3") => (type="containsText", priority=7),
+ XLSX.CellRange("A1:A1") => (type="containsText", priority=6),
+ XLSX.CellRange("A1:A1") => (type="containsText", priority=8),
+ XLSX.CellRange("A5:E5") => (type="beginsWith", priority=5),
+ XLSX.CellRange("A4:E4") => (type="notContainsText", priority=4),
+ XLSX.CellRange("A3:E3") => (type="notContainsText", priority=3),
+ XLSX.CellRange("A2:E2") => (type="containsText", priority=2),
+ XLSX.CellRange("A1:E1") => (type="containsText", priority=1)
+ ]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:E1"] = "Hello World"
+ s["A2:E2"] = "Life the universe and everything"
+ s["A3:E3"] = "Once upon a time"
+ s["A4:E4"] = "In America"
+ s["A5:E5"] = "a"
+ XLSX.setConditionalFormat(s, "A1:A5", :containsText; value="a")
+ XLSX.setConditionalFormat(s, :, 2, :containsText; value="a", dxStyle="redborder")
+ XLSX.setConditionalFormat(s, "Sheet1!E:E", :containsText; value="a", dxStyle="redfilltext")
+ XLSX.setConditionalFormat(s, 1:5, 3:4, :containsText;
+ operator="endsWith",
+ value="a",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "thick", "color" => "coral"]
+ ) == 0
+
+ @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("C1:D5") => (type="endsWith", priority=4), XLSX.CellRange("E1:E5") => (type="containsText", priority=3), XLSX.CellRange("B1:B5") => (type="containsText", priority=2), XLSX.CellRange("A1:A5") => (type="containsText", priority=1)]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:E1"] = "Hello World"
+ s["A2:E2"] = "Life the universe and everything"
+ s["A3:E3"] = "Once upon a time"
+ s["A4:E4"] = "In America"
+ s["A5:E5"] = "a"
+
+ @test XLSX.setConditionalFormat(s, :, 1:4, :containsText;
+ operator="containsText",
+ value="Sheet1!\$E\$5",
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green", "under" => "double"],
+ border=["style" => "thin", "color" => "coral"]
+ ) == 0
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s["A1:E1"] = "Hello World"
+ s["A2:E2"] = "Life the universe and everything"
+ s["A3:E3"] = "Once upon a time"
+ s["A4:E4"] = "In America"
+ s["A5:E5"] = "a"
+ XLSX.addDefinedName(s, "myRange", "A1:B5")
+ @test XLSX.setConditionalFormat(s, "myRange", :containsText;
+ operator="notContainsText",
+ value="a",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "hair", "color" => "cyan"]
+ ) == 0
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "myRange", :containsText;
+ operator="madeUp",
+ value="a",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "hair", "color" => "cyan"]
+ ) == 0
+ XLSX.addDefinedName(s, "myNCRange", "C1:C5,D1:D5")
+ @test_throws MethodError XLSX.setConditionalFormat(s, "myNCRange", :containsText; # Non-contiguous ranges not allowed
+ operator="beginsWith",
+ value="a",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "hair", "color" => "cyan"]
+ )
+
+ end
+
+ @testset "top10" begin
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i = 1:10
+ for j = 1:10
+ s[i, j] = i * j
+ end
+ end
+ @test_throws MethodError XLSX.setConditionalFormat(s, "A1,A3", :top10) # Non-contiguous ranges not allowed
+ @test_throws MethodError XLSX.setConditionalFormat(s, [1], 1, :top10) # Vectors may be non-contiguous
+ @test_throws MethodError XLSX.setConditionalFormat(s, 1, 1:3:7, :top10) # StepRange is non-contiguous
+ @test XLSX.setConditionalFormat(s, "1:1", :top10) == 0
+ @test XLSX.setConditionalFormat(s, 2, :, :top10; dxStyle="greenfilltext") == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:10, :top10;
+ operator="topN",
+ value="5",
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "green"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:10, :top10;
+ operator="bottomN",
+ value="5",
+ stopIfTrue="true",
+ fill=["pattern" => "lightVertical", "fgColor" => "grey", "bgColor" => "FFFFC7CE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:10, :top10;
+ operator="topN%",
+ value="20",
+ fill=["pattern" => "none", "bgColor" => "FFFFC7CE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:10, :top10;
+ operator="bottomN%",
+ value="30",
+ fill=["pattern" => "none", "bgColor" => "pink"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "italic" => "true"]
+ ) == 0
+ @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("A1:J10") => (type="top10", priority=3), XLSX.CellRange("A1:J10") => (type="top10", priority=4), XLSX.CellRange("A1:J10") => (type="top10", priority=5), XLSX.CellRange("A1:J10") => (type="top10", priority=6), XLSX.CellRange("A2:J2") => (type="top10", priority=2), XLSX.CellRange("A1:J1") => (type="top10", priority=1)]
+
+ @test XLSX.setConditionalFormat(s, "A1", :top10) == 0
+ @test XLSX.setConditionalFormat(s, "A1:C3", :top10) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1", :top10) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1:A2", :top10) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!1:2", :top10) == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :top10) == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :top10) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :top10) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!1:2", :top10) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!A:C", :top10) == 0
+ @test XLSX.setConditionalFormat(s, :, 1:3, :top10) == 0
+ @test XLSX.setConditionalFormat(s, 1:3, :, :top10) == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :top10) == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :top10) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :top10) == 0
+ @test XLSX.setConditionalFormat(s, :, :top10) == 0
+ @test XLSX.setConditionalFormat(s, :, :, :top10) == 0
+ @test length(XLSX.getConditionalFormats(s)) == 23
+ @test XLSX.getConditionalFormats(s) == [
+ XLSX.CellRange("A1:J3") => (type="top10", priority=18),
+ XLSX.CellRange("A1:C10") => (type="top10", priority=13),
+ XLSX.CellRange("A1:C10") => (type="top10", priority=14),
+ XLSX.CellRange("A1:C10") => (type="top10", priority=16),
+ XLSX.CellRange("A1:C10") => (type="top10", priority=17),
+ XLSX.CellRange("A1:C10") => (type="top10", priority=20),
+ XLSX.CellRange("A1:C10") => (type="top10", priority=21),
+ XLSX.CellRange("A2:J4") => (type="top10", priority=12),
+ XLSX.CellRange("A2:J4") => (type="top10", priority=19),
+ XLSX.CellRange("A1:J2") => (type="top10", priority=11),
+ XLSX.CellRange("A1:J2") => (type="top10", priority=15),
+ XLSX.CellRange("A1:A2") => (type="top10", priority=10),
+ XLSX.CellRange("A1:C3") => (type="top10", priority=8),
+ XLSX.CellRange("A1:A1") => (type="top10", priority=7),
+ XLSX.CellRange("A1:A1") => (type="top10", priority=9),
+ XLSX.CellRange("A1:J10") => (type="top10", priority=3),
+ XLSX.CellRange("A1:J10") => (type="top10", priority=4),
+ XLSX.CellRange("A1:J10") => (type="top10", priority=5),
+ XLSX.CellRange("A1:J10") => (type="top10", priority=6),
+ XLSX.CellRange("A1:J10") => (type="top10", priority=22),
+ XLSX.CellRange("A1:J10") => (type="top10", priority=23),
+ XLSX.CellRange("A2:J2") => (type="top10", priority=2),
+ XLSX.CellRange("A1:J1") => (type="top10", priority=1)
+ ]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i = 1:10
+ for j = 1:10
+ s[i, j] = i * j
+ end
+ end
+ XLSX.setConditionalFormat(s, "A1:A5", :top10)
+ XLSX.setConditionalFormat(s, :, 2, :top10; dxStyle="redborder")
+ XLSX.setConditionalFormat(s, "Sheet1!E:E", :top10; dxStyle="redfilltext")
+ XLSX.setConditionalFormat(s, 1:5, 3:4, :top10;
+ operator="topN%",
+ value="20",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "thick", "color" => "coral"]
+ ) == 0
+
+ @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("C1:D5") => (type="top10", priority=4), XLSX.CellRange("E1:E5") => (type="top10", priority=3), XLSX.CellRange("B1:B5") => (type="top10", priority=2), XLSX.CellRange("A1:A5") => (type="top10", priority=1)]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i = 1:10
+ for j = 1:10
+ s[i, j] = i * j
+ end
+ end
+
+ @test XLSX.setConditionalFormat(s, :, 1:4, :top10;
+ operator="bottomN",
+ value="\$E\$4",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green", "under" => "double"],
+ border=["style" => "thin", "color" => "coral"]
+ ) == 0
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i = 1:10
+ for j = 1:10
+ s[i, j] = i * j
+ end
+ end
+ XLSX.addDefinedName(s, "myRange", "A1:E5")
+ @test XLSX.setConditionalFormat(s, "myRange", :top10;
+ operator="topN%",
+ value="2",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "medium", "color" => "cyan"]
+ ) == 0
+ XLSX.addDefinedName(s, "myNCRange", "C1:C5,D1:D5")
+ @test_throws MethodError XLSX.setConditionalFormat(s, "myNCRange", :top10; # Non-contiguous ranges not allowed
+ operator="bottomN%",
+ value="2",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "hair", "color" => "cyan"]
+ )
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "myRange", :top10;
+ operator="madeUp",
+ value="2",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "hair", "color" => "cyan"]
+ )
+
+ end
+
+ @testset "aboveAverage" begin
+ f = XLSX.newxlsx()
+ s = f[1]
+ d = Dist.Normal()
+ columns = [rand(d, 1000), rand(d, 1000), rand(d, 1000)]
+ XLSX.writetable!(s, columns, ["normal1", "normal2", "normal3"])
+ @test_throws MethodError XLSX.setConditionalFormat(s, "A2:A1001,C1:C1000", :aboveAverage) # Non-contiguous ranges not allowed
+ @test_throws MethodError XLSX.setConditionalFormat(s, [2, 3, 19], 1:3, :aboveAverage) # Vectors may be non-contiguous
+ @test_throws MethodError XLSX.setConditionalFormat(s, 2, 1:3:7, :aboveAverage) # StepRange is non-contiguous
+ @test XLSX.setConditionalFormat(s, "2:2", :aboveAverage) == 0
+ @test XLSX.setConditionalFormat(s, 2, :, :aboveAverage; dxStyle="greenfilltext") == 0
+ @test XLSX.setConditionalFormat(s, 2:10, 1:3, :aboveAverage;
+ operator="plus3StdDev",
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "FFFFC7CE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 2:1001, 1:3, :aboveAverage;
+ operator="minus3StdDev",
+ stopIfTrue="true",
+ fill=["pattern" => "lightVertical", "fgColor" => "grey", "bgColor" => "FFFFC7CE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 2:1001, 1:3, :aboveAverage;
+ operator="plus2StdDev",
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "FFFFC7CE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 2:1001, 1:3, :aboveAverage;
+ operator="minus2StdDev",
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "pink"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "italic" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 2:1001, 1:3, :aboveAverage;
+ operator="plus1StdDev",
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "FFFFCFCE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "yellow", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:1001, 1:3, :aboveAverage;
+ operator="minus1StdDev",
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "green", "bold" => "true", "italic" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:1001, 1:3, :aboveAverage;
+ operator="aboveAverage",
+ fill=["pattern" => "none", "bgColor" => "FFFFCFCE"],
+ border=["style" => "thick", "color" => "gray"],
+ font=["color" => "yellow", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:1001, 1:3, :aboveAverage;
+ operator="belowAverage",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ border=["style" => "thick", "color" => "green"],
+ font=["color" => "green", "bold" => "true", "italic" => "true"]
+ ) == 0
+ @test XLSX.getConditionalFormats(s) == [
+ XLSX.CellRange("A1:C1001") => (type="aboveAverage", priority=8),
+ XLSX.CellRange("A1:C1001") => (type="aboveAverage", priority=9),
+ XLSX.CellRange("A1:C1001") => (type="aboveAverage", priority=10),
+ XLSX.CellRange("A2:C1001") => (type="aboveAverage", priority=4),
+ XLSX.CellRange("A2:C1001") => (type="aboveAverage", priority=5),
+ XLSX.CellRange("A2:C1001") => (type="aboveAverage", priority=6),
+ XLSX.CellRange("A2:C1001") => (type="aboveAverage", priority=7),
+ XLSX.CellRange("A2:C10") => (type="aboveAverage", priority=3),
+ XLSX.CellRange("A2:C2") => (type="aboveAverage", priority=1),
+ XLSX.CellRange("A2:C2") => (type="aboveAverage", priority=2)
+ ]
+
+ @test XLSX.setConditionalFormat(s, "A1", :aboveAverage) == 0
+ @test XLSX.setConditionalFormat(s, "A1:C3", :aboveAverage) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1", :aboveAverage) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1:A2", :aboveAverage) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!1:2", :aboveAverage) == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :aboveAverage) == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :aboveAverage) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :aboveAverage) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!1:2", :aboveAverage) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!A:C", :aboveAverage) == 0
+ @test XLSX.setConditionalFormat(s, :, 1:3, :aboveAverage) == 0
+ @test XLSX.setConditionalFormat(s, 1:3, :, :aboveAverage) == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :aboveAverage) == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :aboveAverage) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :aboveAverage) == 0
+ @test XLSX.setConditionalFormat(s, :, :aboveAverage) == 0
+ @test XLSX.setConditionalFormat(s, :, :, :aboveAverage) == 0
+ @test length(XLSX.getConditionalFormats(s)) == 27
+ @test XLSX.getConditionalFormats(s) == [
+ XLSX.CellRange("A2:C4") => (type="aboveAverage", priority=16),
+ XLSX.CellRange("A2:C4") => (type="aboveAverage", priority=23),
+ XLSX.CellRange("A1:C2") => (type="aboveAverage", priority=15),
+ XLSX.CellRange("A1:C2") => (type="aboveAverage", priority=19),
+ XLSX.CellRange("A1:A2") => (type="aboveAverage", priority=14),
+ XLSX.CellRange("A1:C3") => (type="aboveAverage", priority=12),
+ XLSX.CellRange("A1:C3") => (type="aboveAverage", priority=22),
+ XLSX.CellRange("A1:A1") => (type="aboveAverage", priority=11),
+ XLSX.CellRange("A1:A1") => (type="aboveAverage", priority=13),
+ XLSX.CellRange("A1:C1001") => (type="aboveAverage", priority=8),
+ XLSX.CellRange("A1:C1001") => (type="aboveAverage", priority=9),
+ XLSX.CellRange("A1:C1001") => (type="aboveAverage", priority=10),
+ XLSX.CellRange("A1:C1001") => (type="aboveAverage", priority=17),
+ XLSX.CellRange("A1:C1001") => (type="aboveAverage", priority=18),
+ XLSX.CellRange("A1:C1001") => (type="aboveAverage", priority=20),
+ XLSX.CellRange("A1:C1001") => (type="aboveAverage", priority=21),
+ XLSX.CellRange("A1:C1001") => (type="aboveAverage", priority=24),
+ XLSX.CellRange("A1:C1001") => (type="aboveAverage", priority=25),
+ XLSX.CellRange("A1:C1001") => (type="aboveAverage", priority=26),
+ XLSX.CellRange("A1:C1001") => (type="aboveAverage", priority=27),
+ XLSX.CellRange("A2:C1001") => (type="aboveAverage", priority=4),
+ XLSX.CellRange("A2:C1001") => (type="aboveAverage", priority=5),
+ XLSX.CellRange("A2:C1001") => (type="aboveAverage", priority=6),
+ XLSX.CellRange("A2:C1001") => (type="aboveAverage", priority=7),
+ XLSX.CellRange("A2:C10") => (type="aboveAverage", priority=3),
+ XLSX.CellRange("A2:C2") => (type="aboveAverage", priority=1),
+ XLSX.CellRange("A2:C2") => (type="aboveAverage", priority=2)
+ ]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i = 1:10
+ for j = 1:10
+ s[i, j] = i * j
+ end
+ end
+ @test XLSX.setConditionalFormat(s, "A1:A5", :aboveAverage) == 0
+ @test XLSX.setConditionalFormat(s, :, 2, :aboveAverage; dxStyle="redborder") == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!E:E", :aboveAverage; dxStyle="redfilltext") == 0
+ @test XLSX.setConditionalFormat(s, 1:5, 3:4, :aboveAverage;
+ operator="aboveEqAverage",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "thick", "color" => "coral"]
+ ) == 0
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, 1:5, 3:4, :aboveAverage;
+ operator="madeup",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "thick", "color" => "coral"]
+ ) == 0
+
+ @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("C1:D5") => (type="aboveAverage", priority=4), XLSX.CellRange("E1:E5") => (type="aboveAverage", priority=3), XLSX.CellRange("B1:B5") => (type="aboveAverage", priority=2), XLSX.CellRange("A1:A5") => (type="aboveAverage", priority=1)]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i = 1:10
+ for j = 1:10
+ s[i, j] = i * j
+ end
+ end
+
+ @test XLSX.setConditionalFormat(s, :, 1:4, :aboveAverage;
+ operator="belowEqAverage",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green", "under" => "double"],
+ border=["style" => "thin", "color" => "coral"]
+ ) == 0
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i = 1:10
+ for j = 1:10
+ s[i, j] = i * j
+ end
+ end
+ XLSX.addDefinedName(s, "myRange", "A1:E5")
+ @test XLSX.setConditionalFormat(s, "myRange", :aboveAverage;
+ operator="aboveEqAverage",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "medium", "color" => "cyan"]
+ ) == 0
+ XLSX.addDefinedName(s, "myNCRange", "C1:C5,D1:D5")
+ @test_throws MethodError XLSX.setConditionalFormat(s, "myNCRange", :aboveAverage; # Non-contiguous ranges not allowed
+ operator="belowEqAverage",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "hair", "color" => "cyan"]
+ )
+
+ end
+
+ @testset "timePeriod" begin
+ f = XLSX.newxlsx()
+ s = f[1]
+ todaynow = Dates.today()
+ s[1, 1:10] = todaynow - Dates.Year(1)
+ s[2, 1:10] = todaynow - Dates.Month(1)
+ s[3, 1:10] = todaynow - Dates.Day(14)
+ s[4, 1:10] = todaynow - Dates.Day(5)
+ s[5, 1:10] = todaynow - Dates.Day(1)
+ s[6, 1:10] = todaynow
+ s[7, 1:10] = todaynow + Dates.Day(1)
+ s[8, 1:10] = todaynow + Dates.Day(14)
+ s[9, 1:10] = todaynow + Dates.Month(1)
+ s[10, 1:10] = todaynow + Dates.Year(1)
+
+ @test_throws MethodError XLSX.setConditionalFormat(s, "A1:A5,C1:C5", :timePeriod) # Non-contiguous ranges not allowed
+ @test_throws MethodError XLSX.setConditionalFormat(s, [2, 3, 8], 1:3, :timePeriod) # Vectors may be non-contiguous
+ @test_throws MethodError XLSX.setConditionalFormat(s, 2, 1:3:7, :timePeriod) # StepRange is non-contiguous
+ @test XLSX.setConditionalFormat(s, "2:2", :timePeriod) == 0
+ @test XLSX.setConditionalFormat(s, 2, :, :timePeriod; dxStyle="greenfilltext") == 0
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, 1:10, 1:3, :timePeriod;
+ operator="madeUp",
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "FFFFC7CE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "strike" => "true"]
+ )
+ @test XLSX.setConditionalFormat(s, 1:10, 1:3, :timePeriod;
+ operator="today",
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "FFFFC7CE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:3, :timePeriod;
+ operator="yesterday",
+ stopIfTrue="true",
+ fill=["pattern" => "lightVertical", "fgColor" => "grey", "bgColor" => "FFFFC7CE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:3, :timePeriod;
+ operator="tomorrow",
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "FFFFC7CE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:3, :timePeriod;
+ operator="lastMonth",
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "pink"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "italic" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:3, :timePeriod;
+ operator="thisMonth",
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "FFCC4411"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "yellow", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:3, :timePeriod;
+ operator="nextMonth",
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "FFFFCFCE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "yellow", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:3, :timePeriod;
+ operator="last7Days",
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "green", "bold" => "true", "italic" => "true"]
+ ) == 0
+ @test XLSX.getConditionalFormats(s) == [
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=3),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=4),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=5),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=6),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=7),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=8),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=9),
+ XLSX.CellRange("A2:J2") => (type="timePeriod", priority=1),
+ XLSX.CellRange("A2:J2") => (type="timePeriod", priority=2)
+ ]
+
+ @test XLSX.setConditionalFormat(s, "A1", :timePeriod) == 0
+ @test XLSX.setConditionalFormat(s, "A1:C3", :timePeriod) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1", :timePeriod) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1:A2", :timePeriod) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!1:2", :timePeriod) == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :timePeriod) == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :timePeriod) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :timePeriod) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!1:2", :timePeriod) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!A:C", :timePeriod) == 0
+ @test XLSX.setConditionalFormat(s, :, 1:3, :timePeriod) == 0
+ @test XLSX.setConditionalFormat(s, 1:3, :, :timePeriod) == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :timePeriod) == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :timePeriod) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :timePeriod) == 0
+ @test XLSX.setConditionalFormat(s, :, :timePeriod) == 0
+ @test XLSX.setConditionalFormat(s, :, :, :timePeriod) == 0
+ @test length(XLSX.getConditionalFormats(s)) == 26
+
+ @test XLSX.getConditionalFormats(s) == [
+ XLSX.CellRange("A1:J10") => (type="timePeriod", priority=25),
+ XLSX.CellRange("A1:J10") => (type="timePeriod", priority=26),
+ XLSX.CellRange("A1:J3") => (type="timePeriod", priority=21),
+ XLSX.CellRange("A2:J4") => (type="timePeriod", priority=15),
+ XLSX.CellRange("A2:J4") => (type="timePeriod", priority=22),
+ XLSX.CellRange("A1:J2") => (type="timePeriod", priority=14),
+ XLSX.CellRange("A1:J2") => (type="timePeriod", priority=18),
+ XLSX.CellRange("A1:A2") => (type="timePeriod", priority=13),
+ XLSX.CellRange("A1:C3") => (type="timePeriod", priority=11),
+ XLSX.CellRange("A1:A1") => (type="timePeriod", priority=10),
+ XLSX.CellRange("A1:A1") => (type="timePeriod", priority=12),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=3),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=4),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=5),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=6),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=7),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=8),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=9),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=16),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=17),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=19),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=20),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=23),
+ XLSX.CellRange("A1:C10") => (type="timePeriod", priority=24),
+ XLSX.CellRange("A2:J2") => (type="timePeriod", priority=1),
+ XLSX.CellRange("A2:J2") => (type="timePeriod", priority=2)
+ ]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s[1, 1:10] = todaynow - Dates.Year(1)
+ s[2, 1:10] = todaynow - Dates.Month(1)
+ s[3, 1:10] = todaynow - Dates.Day(14)
+ s[4, 1:10] = todaynow - Dates.Day(5)
+ s[5, 1:10] = todaynow - Dates.Day(1)
+ s[6, 1:10] = todaynow
+ s[7, 1:10] = todaynow + Dates.Day(1)
+ s[8, 1:10] = todaynow + Dates.Day(14)
+ s[9, 1:10] = todaynow + Dates.Month(1)
+ s[10, 1:10] = todaynow + Dates.Year(1)
+ XLSX.setConditionalFormat(s, "A1:A5", :timePeriod)
+ XLSX.setConditionalFormat(s, :, 2, :timePeriod; dxStyle="redborder")
+ XLSX.setConditionalFormat(s, "Sheet1!E:E", :timePeriod; dxStyle="redfilltext")
+ XLSX.setConditionalFormat(s, 1:5, 3:4, :timePeriod;
+ operator="lastWeek",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "thick", "color" => "coral"]
+ ) == 0
+
+ @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("C1:D5") => (type="timePeriod", priority=4), XLSX.CellRange("E1:E10") => (type="timePeriod", priority=3), XLSX.CellRange("B1:B10") => (type="timePeriod", priority=2), XLSX.CellRange("A1:A5") => (type="timePeriod", priority=1)]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s[1, 1:10] = todaynow - Dates.Year(1)
+ s[2, 1:10] = todaynow - Dates.Month(1)
+ s[3, 1:10] = todaynow - Dates.Day(14)
+ s[4, 1:10] = todaynow - Dates.Day(5)
+ s[5, 1:10] = todaynow - Dates.Day(1)
+ s[6, 1:10] = todaynow
+ s[7, 1:10] = todaynow + Dates.Day(1)
+ s[8, 1:10] = todaynow + Dates.Day(14)
+ s[9, 1:10] = todaynow + Dates.Month(1)
+ s[10, 1:10] = todaynow + Dates.Year(1)
+
+ @test XLSX.setConditionalFormat(s, :, 1:4, :timePeriod;
+ operator="thisWeek",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green", "under" => "double"],
+ border=["style" => "thin", "color" => "coral"]
+ ) == 0
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ s[1, 1:10] = todaynow - Dates.Year(1)
+ s[2, 1:10] = todaynow - Dates.Month(1)
+ s[3, 1:10] = todaynow - Dates.Day(14)
+ s[4, 1:10] = todaynow - Dates.Day(5)
+ s[5, 1:10] = todaynow - Dates.Day(1)
+ s[6, 1:10] = todaynow
+ s[7, 1:10] = todaynow + Dates.Day(1)
+ s[8, 1:10] = todaynow + Dates.Day(14)
+ s[9, 1:10] = todaynow + Dates.Month(1)
+ s[10, 1:10] = todaynow + Dates.Year(1)
+ XLSX.addDefinedName(s, "myRange", "A1:E5")
+ @test XLSX.setConditionalFormat(s, "myRange", :timePeriod;
+ operator="nextWeek",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "medium", "color" => "cyan"]
+ ) == 0
+ XLSX.addDefinedName(s, "myNCRange", "C1:C5,D1:D5")
+ @test_throws MethodError XLSX.setConditionalFormat(s, "myNCRange", :timePeriod; # Non-contiguous ranges not allowed
+ operator="lastWeek",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "hair", "color" => "cyan"]
+ )
+
+ end
+
+ @testset "expression" begin
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i = 1:10
+ for j = 1:10
+ s[i, j] = i * j
+ end
+ end
+
+ @test_throws MethodError XLSX.setConditionalFormat(s, "A1:A5,C1:C5", :expression; formula = "A1>3") # Non-contiguous ranges not allowed
+ @test_throws MethodError XLSX.setConditionalFormat(s, [2, 3, 8], 1:3, :expression; formula = "A1 > 11") # Vectors may be non-contiguous
+ @test_throws MethodError XLSX.setConditionalFormat(s, 2, 1:3:7, :expression; formula = "A1 < 7") # StepRange is non-contiguous
+ @test XLSX.setConditionalFormat(s, "2:2", :expression; formula = "A1 = 16") == 0
+ @test XLSX.setConditionalFormat(s, 2, :, :expression; formula = "A1 < 16", dxStyle="greenfilltext") == 0
+ @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, 1:10, 1:3, :expression;
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "FFFFC7CE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:3, :expression;
+ formula = "A1 > 15",
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "FFFFC7CE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:3, :expression;
+ formula = "iseven(A1)",
+ stopIfTrue="true",
+ fill=["pattern" => "lightVertical", "fgColor" => "grey", "bgColor" => "FFFFC7CE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:3, :expression;
+ formula = "A1 < 10",
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "FFFFC7CE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:3, :expression;
+ formula = "A1 < 5",
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "pink"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "italic" => "true"]
+ ) == 0
+ @test XLSX.getConditionalFormats(s) == [
+ XLSX.CellRange("A1:C10") => (type="expression", priority=3),
+ XLSX.CellRange("A1:C10") => (type="expression", priority=4),
+ XLSX.CellRange("A1:C10") => (type="expression", priority=5),
+ XLSX.CellRange("A1:C10") => (type="expression", priority=6),
+ XLSX.CellRange("A2:J2") => (type="expression", priority=1),
+ XLSX.CellRange("A2:J2") => (type="expression", priority=2),
+ ]
+
+ @test XLSX.setConditionalFormat(s, "A1", :expression; formula = "iseven(A1)") == 0
+ @test XLSX.setConditionalFormat(s, "A1:C3", :expression; formula = "iseven(A1)") == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1", :expression; formula = "iseven(A1)") == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1:A2", :expression; formula = "iseven(A1)") == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!1:2", :expression; formula = "iseven(A1)") == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :expression; formula = "iseven(A1)") == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :expression; formula = "iseven(A1)") == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :expression; formula = "iseven(A1)") == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!1:2", :expression; formula = "iseven(A1)") == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!A:C", :expression; formula = "iseven(A1)") == 0
+ @test XLSX.setConditionalFormat(s, :, 1:3, :expression; formula = "iseven(A1)") == 0
+ @test XLSX.setConditionalFormat(s, 1:3, :, :expression; formula = "iseven(A1)") == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :expression; formula = "iseven(A1)") == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :expression; formula = "iseven(A1)") == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :expression; formula = "iseven(A1)") == 0
+ @test XLSX.setConditionalFormat(s, :, :expression; formula = "iseven(A1)") == 0
+ @test XLSX.setConditionalFormat(s, :, :, :expression; formula = "iseven(A1)") == 0
+ @test length(XLSX.getConditionalFormats(s)) == 23
+ @test XLSX.getConditionalFormats(s) == [
+ XLSX.CellRange("A1:J10") => (type = "expression", priority = 22),
+ XLSX.CellRange("A1:J10") => (type = "expression", priority = 23),
+ XLSX.CellRange("A1:J3") => (type = "expression", priority = 18),
+ XLSX.CellRange("A2:J4") => (type = "expression", priority = 12),
+ XLSX.CellRange("A2:J4") => (type = "expression", priority = 19),
+ XLSX.CellRange("A1:J2") => (type = "expression", priority = 11),
+ XLSX.CellRange("A1:J2") => (type = "expression", priority = 15),
+ XLSX.CellRange("A1:A2") => (type = "expression", priority = 10),
+ XLSX.CellRange("A1:C3") => (type = "expression", priority = 8),
+ XLSX.CellRange("A1:A1") => (type = "expression", priority = 7),
+ XLSX.CellRange("A1:A1") => (type = "expression", priority = 9),
+ XLSX.CellRange("A1:C10") => (type = "expression", priority = 3),
+ XLSX.CellRange("A1:C10") => (type = "expression", priority = 4),
+ XLSX.CellRange("A1:C10") => (type = "expression", priority = 5),
+ XLSX.CellRange("A1:C10") => (type = "expression", priority = 6),
+ XLSX.CellRange("A1:C10") => (type = "expression", priority = 13),
+ XLSX.CellRange("A1:C10") => (type = "expression", priority = 14),
+ XLSX.CellRange("A1:C10") => (type = "expression", priority = 16),
+ XLSX.CellRange("A1:C10") => (type = "expression", priority = 17),
+ XLSX.CellRange("A1:C10") => (type = "expression", priority = 20),
+ XLSX.CellRange("A1:C10") => (type = "expression", priority = 21),
+ XLSX.CellRange("A2:J2") => (type = "expression", priority = 1),
+ XLSX.CellRange("A2:J2") => (type = "expression", priority = 2)
+ ]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i = 1:10
+ for j = 1:10
+ s[i, j] = i * j
+ end
+ end
+ XLSX.setConditionalFormat(s, "A1:A5", :expression; formula="A1=1")
+ XLSX.setConditionalFormat(s, :, 2, :expression; formula="A1=1", dxStyle="redborder")
+ XLSX.setConditionalFormat(s, "Sheet1!E:E", :expression; formula="A1=1", dxStyle="redfilltext")
+ XLSX.setConditionalFormat(s, 1:5, 3:4, :expression;
+ formula="A1=1",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "thick", "color" => "coral"]
+ ) == 0
+
+ @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("C1:D5") => (type="expression", priority=4), XLSX.CellRange("E1:E10") => (type="expression", priority=3), XLSX.CellRange("B1:B10") => (type="expression", priority=2), XLSX.CellRange("A1:A5") => (type="expression", priority=1)]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i = 1:10
+ for j = 1:10
+ s[i, j] = i * j
+ end
+ end
+ @test XLSX.setConditionalFormat(s, :, 1:4, :expression;
+ formula = "A1 > \$E\$3",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green", "under" => "double"],
+ border=["style" => "thin", "color" => "coral"]
+ ) == 0
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i = 1:10
+ for j = 1:10
+ s[i, j] = i * j
+ end
+ end
+ XLSX.addDefinedName(f, "myTest", "Sheet1!L11")
+ s["L11"] = 70
+ XLSX.addDefinedName(s, "myRange", "F6:J10")
+
+ @test XLSX.setConditionalFormat(s, "myRange", :expression;
+ formula="E5 > myTest",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "medium", "color" => "cyan"]
+ ) == 0
+ XLSX.addDefinedName(s, "myNCRange", "C1:C5,D1:D5")
+ @test_throws MethodError XLSX.setConditionalFormat(s, "myNCRange", :expression; # Non-contiguous ranges not allowed
+ formula = "C4 < myTest",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "hair", "color" => "cyan"]
+ )
+
+ end
+
+ @testset "containsErrors" begin
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i = 1:10
+ for j = 1:10
+ s[i, j] = i * j
+ end
+ end
+ @test_throws MethodError XLSX.setConditionalFormat(s, "A1:A5,C1:C5", :containsErrors) # Non-contiguous ranges not allowed
+ @test_throws MethodError XLSX.setConditionalFormat(s, [2, 3, 8], 1:3, :containsErrors) # Vectors may be non-contiguous
+ @test_throws MethodError XLSX.setConditionalFormat(s, 2, 1:3:7, :containsErrors) # StepRange is non-contiguous
+ @test XLSX.setConditionalFormat(s, "2:2", :containsErrors) == 0
+ @test XLSX.setConditionalFormat(s, 2, :, :containsErrors; dxStyle="greenfilltext") == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:3, :containsErrors;
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "FFFFC7CE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:3, :notContainsErrors;
+ stopIfTrue="true",
+ fill=["pattern" => "lightVertical", "fgColor" => "grey", "bgColor" => "FFFFC7CE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:3, :containsBlanks;
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "FFFFC7CE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:3, :notContainsBlanks;
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "pink"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "blue", "bold" => "true", "italic" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:3, :uniqueValues;
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "FFFFCFCE"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "yellow", "bold" => "true", "strike" => "true"]
+ ) == 0
+ @test XLSX.setConditionalFormat(s, 1:10, 1:3, :duplicateValues;
+ stopIfTrue="true",
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ border=["style" => "thick", "color" => "coral"],
+ font=["color" => "green", "bold" => "true", "italic" => "true"]
+ ) == 0
+ @test XLSX.getConditionalFormats(s) == [
+ XLSX.CellRange("A1:C10") => (type="containsErrors", priority=3),
+ XLSX.CellRange("A1:C10") => (type="notContainsErrors", priority=4),
+ XLSX.CellRange("A1:C10") => (type="containsBlanks", priority=5),
+ XLSX.CellRange("A1:C10") => (type="notContainsBlanks", priority=6),
+ XLSX.CellRange("A1:C10") => (type="uniqueValues", priority=7),
+ XLSX.CellRange("A1:C10") => (type="duplicateValues", priority=8),
+ XLSX.CellRange("A2:J2") => (type="containsErrors", priority=1),
+ XLSX.CellRange("A2:J2") => (type="containsErrors", priority=2)
+ ]
+
+ @test XLSX.setConditionalFormat(s, "A1", :containsErrors) == 0
+ @test XLSX.setConditionalFormat(s, "A1:C3", :notContainsErrors) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1", :containsBlanks) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A1:A2", :notContainsBlanks) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!1:2", :uniqueValues) == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :duplicateValues) == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :containsErrors) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :containsErrors) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!1:2", :containsErrors) == 0
+ @test XLSX.setConditionalFormat(f, "Sheet1!A:C", :containsErrors) == 0
+ @test XLSX.setConditionalFormat(s, :, 1:3, :containsErrors) == 0
+ @test XLSX.setConditionalFormat(s, 1:3, :, :notContainsErrors) == 0
+ @test XLSX.setConditionalFormat(s, "2:4", :containsBlanks) == 0
+ @test XLSX.setConditionalFormat(s, "A:C", :notContainsBlanks) == 0
+ @test XLSX.setConditionalFormat(s, "Sheet1!A:C", :containsErrors) == 0
+ @test XLSX.setConditionalFormat(s, :, :uniqueValues) == 0
+ @test XLSX.setConditionalFormat(s, :, :, :duplicateValues) == 0
+ @test length(XLSX.getConditionalFormats(s)) == 25
+ @test XLSX.getConditionalFormats(s) == [
+ XLSX.CellRange("A1:J10") => (type="uniqueValues", priority=24),
+ XLSX.CellRange("A1:J10") => (type="duplicateValues", priority=25),
+ XLSX.CellRange("A1:J3") => (type="notContainsErrors", priority=20),
+ XLSX.CellRange("A2:J4") => (type="duplicateValues", priority=14),
+ XLSX.CellRange("A2:J4") => (type="containsBlanks", priority=21),
+ XLSX.CellRange("A1:J2") => (type="uniqueValues", priority=13),
+ XLSX.CellRange("A1:J2") => (type="containsErrors", priority=17),
+ XLSX.CellRange("A1:A2") => (type="notContainsBlanks", priority=12),
+ XLSX.CellRange("A1:C3") => (type="notContainsErrors", priority=10),
+ XLSX.CellRange("A1:A1") => (type="containsErrors", priority=9),
+ XLSX.CellRange("A1:A1") => (type="containsBlanks", priority=11),
+ XLSX.CellRange("A1:C10") => (type="containsErrors", priority=3),
+ XLSX.CellRange("A1:C10") => (type="notContainsErrors", priority=4),
+ XLSX.CellRange("A1:C10") => (type="containsBlanks", priority=5),
+ XLSX.CellRange("A1:C10") => (type="notContainsBlanks", priority=6),
+ XLSX.CellRange("A1:C10") => (type="uniqueValues", priority=7),
+ XLSX.CellRange("A1:C10") => (type="duplicateValues", priority=8),
+ XLSX.CellRange("A1:C10") => (type="containsErrors", priority=15),
+ XLSX.CellRange("A1:C10") => (type="containsErrors", priority=16),
+ XLSX.CellRange("A1:C10") => (type="containsErrors", priority=18),
+ XLSX.CellRange("A1:C10") => (type="containsErrors", priority=19),
+ XLSX.CellRange("A1:C10") => (type="notContainsBlanks", priority=22),
+ XLSX.CellRange("A1:C10") => (type="containsErrors", priority=23),
+ XLSX.CellRange("A2:J2") => (type="containsErrors", priority=1),
+ XLSX.CellRange("A2:J2") => (type="containsErrors", priority=2)
+ ]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i = 1:10
+ for j = 1:10
+ s[i, j] = i * j
+ end
+ end
+ XLSX.setConditionalFormat(s, "A1:A5", :containsErrors)
+ XLSX.setConditionalFormat(s, :, 2, :notContainsErrors; dxStyle="redborder")
+ XLSX.setConditionalFormat(s, "Sheet1!E:E", :containsBlanks; dxStyle="redfilltext")
+ XLSX.setConditionalFormat(s, 1:5, 3:4, :uniqueValues;
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "thick", "color" => "coral"]
+ ) == 0
+
+ @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("C1:D5") => (type="uniqueValues", priority=4), XLSX.CellRange("E1:E10") => (type="containsBlanks", priority=3), XLSX.CellRange("B1:B10") => (type="notContainsErrors", priority=2), XLSX.CellRange("A1:A5") => (type="containsErrors", priority=1)]
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i = 1:10
+ for j = 1:10
+ s[i, j] = i * j
+ end
+ end
+
+ @test XLSX.setConditionalFormat(s, :, 1:4, :containsErrors;
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green", "under" => "double"],
+ border=["style" => "thin", "color" => "coral"]
+ ) == 0
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i = 1:10
+ for j = 1:10
+ s[i, j] = i * j
+ end
+ end
+ XLSX.addDefinedName(s, "myRange", "A1:E5")
+ @test XLSX.setConditionalFormat(s, "myRange", :containsErrors;
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "medium", "color" => "cyan"]
+ ) == 0
+ XLSX.addDefinedName(s, "myNCRange", "C1:C5,D1:D5")
+ @test_throws MethodError XLSX.setConditionalFormat(s, "myNCRange", :containsErrors; # Non-contiguous ranges not allowed
+ fill=["pattern" => "none", "bgColor" => "yellow"],
+ format=["format" => "0.0"],
+ font=["color" => "green"],
+ border=["style" => "hair", "color" => "cyan"]
+ )
+
+ end
+
+end
+
+@testset "merged cells" begin
+ XLSX.openxlsx(joinpath(data_directory, "customXml.xlsx")) do f
+ @test_throws XLSX.XLSXError XLSX.getMergedCells(f["Mock-up"]) # File isn't writeable
+ end
+ XLSX.openxlsx(joinpath(data_directory, "customXml.xlsx"); mode="rw") do f
+ mc = sort(XLSX.getMergedCells(f["Mock-up"]))
+ @test length(mc) == 25
+ @test mc == sort(XLSX.CellRange[XLSX.CellRange("D49:H49"), XLSX.CellRange("D72:J72"), XLSX.CellRange("F94:J94"), XLSX.CellRange("F96:J96"), XLSX.CellRange("F84:J84"), XLSX.CellRange("F86:J86"), XLSX.CellRange("D62:J63"), XLSX.CellRange("D51:J53"), XLSX.CellRange("D55:J60"), XLSX.CellRange("D92:J92"), XLSX.CellRange("D82:J82"), XLSX.CellRange("D74:J74"), XLSX.CellRange("D67:J68"), XLSX.CellRange("D47:H47"), XLSX.CellRange("D9:H9"), XLSX.CellRange("D11:G11"), XLSX.CellRange("D12:G12"), XLSX.CellRange("D14:E14"), XLSX.CellRange("D16:E16"), XLSX.CellRange("D32:F32"), XLSX.CellRange("D38:J38"), XLSX.CellRange("D34:J34"), XLSX.CellRange("D18:E18"), XLSX.CellRange("D20:E20"), XLSX.CellRange("D13:G13")])
+ s = f["Mock-up"]
+ @test XLSX.isMergedCell(f, "Mock-up!D47")
+ @test XLSX.isMergedCell(f, "Mock-up!D49"; mergedCells=mc)
+ @test XLSX.isMergedCell(s, "H84")
+ @test XLSX.isMergedCell(s, "G84"; mergedCells=mc)
+ @test XLSX.isMergedCell(s, "Short_Description")
+ @test !XLSX.isMergedCell(f, "Mock-up!B2")
+ @test !XLSX.isMergedCell(s, "H40"; mergedCells=mc)
+ @test !XLSX.isMergedCell(s, "ID"; mergedCells=mc)
+ @test_throws XLSX.XLSXError XLSX.isMergedCell(s, "Contiguous"; mergedCells=mc) # Can't test a range
+ @test_throws XLSX.XLSXError XLSX.getMergedBaseCell(s, "Location")
+
+ @test XLSX.getMergedBaseCell(f[1], "F72") == (baseCell=XLSX.CellRef("D72"), baseValue=Dates.Date("2025-03-24"))
+ @test XLSX.getMergedBaseCell(f, "Mock-up!G72") == (baseCell=XLSX.CellRef("D72"), baseValue=Dates.Date("2025-03-24"))
+ @test XLSX.getMergedBaseCell(s, "H53") == (baseCell=XLSX.CellRef("D51"), baseValue="Hello World")
+ @test XLSX.getMergedBaseCell(s, "G52") == (baseCell=XLSX.CellRef("D51"), baseValue="Hello World")
+ @test XLSX.getMergedBaseCell(s, 53, 8) == (baseCell=XLSX.CellRef("D51"), baseValue="Hello World")
+ @test XLSX.getMergedBaseCell(s, "Short_Description") == (baseCell=XLSX.CellRef("D51"), baseValue="Hello World")
+ @test isnothing(XLSX.getMergedBaseCell(s, "F73"))
+ @test isnothing(XLSX.getMergedBaseCell(f, "Mock-up!H73"))
+ @test_throws XLSX.XLSXError XLSX.getMergedBaseCell(s, "Location") # Can't get base cell for a range
+
+ @test isnothing(XLSX.getMergedCells(f["Document History"]))
+ s = f["Document History"]
+ @test !XLSX.isMergedCell(f, "Document History!B2")
+ @test !XLSX.isMergedCell(s, "C5"; mergedCells=XLSX.getMergedCells(f["Document History"]))
+ end
+
+ f = XLSX.opentemplate(joinpath(data_directory, "testmerge.xlsx"))
+ @test XLSX.mergeCells(f, "Sheet1!A1:B2") == 0
+ @test f[1]["A1"] == "Tables"
+ @test ismissing(f[1]["B2"])
+ @test f[1]["C3"] == 4
+ @test XLSX.mergeCells(f[1], 4:6, 4:6) == 0
+ @test f[1][4, 4] == 9
+ @test ismissing(f[1][5, 5])
+ @test f[1][7, 7] == 36
+ @test XLSX.mergeCells(f[1], "J") == 0
+ @test f[1]["J1"] == 9
+ @test ismissing(f[1]["J2"])
+ @test ismissing(f[1]["J12"])
+ @test XLSX.isMergedCell(f[1], "J8")
+ mc = XLSX.getMergedCells(f["Sheet1"])
+ @test XLSX.isMergedCell(f[1], "J9"; mergedCells=mc)
+ @test XLSX.getMergedBaseCell(f[1], "J12") == (baseCell=XLSX.CellRef("J1"), baseValue=9)
+
+ @test_throws XLSX.XLSXError XLSX.mergeCells(f[1], "Sheet1!M13:M13") # Single cell
+ @test_throws XLSX.XLSXError XLSX.mergeCells(f[1], 1, :) # Overlapping
+ @test_throws XLSX.XLSXError XLSX.mergeCells(f[1], 10, :) # Overlapping
+ @test_throws XLSX.XLSXError XLSX.mergeCells(f["Sheet1"], "M1:P15") # Outside dimension
+ @test_throws XLSX.XLSXError XLSX.mergeCells(f["Sheet1"], "Sheet2!L1:M2") # Sheets don't match
+
+ XLSX.writexlsx("outfile.xlsx", f, overwrite=true)
+
+ XLSX.openxlsx("outfile.xlsx"; mode="rw") do f
+ mc = sort(XLSX.getMergedCells(f["Sheet1"]))
+ @test length(mc) == 3
+ @test mc == sort(XLSX.CellRange[XLSX.CellRange("A1:B2"), XLSX.CellRange("D4:F6"), XLSX.CellRange("J1:J13")])
+ @test XLSX.isMergedCell(f[1], "B2")
+ @test XLSX.isMergedCell(f[1], 6, 6; mergedCells=mc)
+ @test XLSX.getMergedBaseCell(f[1], "F6") == (baseCell=XLSX.CellRef("D4"), baseValue=9)
+ @test f[1]["A1"] == "Tables"
+ @test ismissing(f[1]["B2"])
+ @test f[1]["C3"] == 4
+ @test f[1][4, 4] == 9
+ @test ismissing(f[1][5, 5])
+ @test f[1][7, 7] == 36
+ @test f[1]["J1"] == 9
+ @test ismissing(f[1]["J2"])
+ @test ismissing(f[1]["J12"])
+ @test XLSX.isMergedCell(f[1], "J8")
+ @test XLSX.isMergedCell(f[1], "J9"; mergedCells=XLSX.getMergedCells(f["Sheet1"]))
+ @test XLSX.getMergedBaseCell(f[1], "J12") == (baseCell=XLSX.CellRef("J1"), baseValue=9)
+ end
+ isfile("outfile.xlsx") && rm("outfile.xlsx")
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:3, j in 1:3
+ s[i, j] = i + j
+ end
+ XLSX.mergeCells(s, "Sheet1!A:B")
+ @test XLSX.getMergedBaseCell(f, "Sheet1!B2") == (baseCell=XLSX.CellRef("A1"), baseValue=2)
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:4, j in 1:4
+ s[i, j] = i + j
+ end
+ XLSX.mergeCells(s, "Sheet1!2:3")
+ @test XLSX.getMergedBaseCell(f, "Sheet1!C3") == (baseCell=XLSX.CellRef("A2"), baseValue=3)
+ XLSX.mergeCells(s, "Sheet1!4:4")
+ @test XLSX.getMergedBaseCell(f, "Sheet1!C4") == (baseCell=XLSX.CellRef("A4"), baseValue=5)
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:3, j in 1:3
+ s[i, j] = i + j
+ end
+ XLSX.mergeCells(s, :, 2:3)
+ @test XLSX.getMergedBaseCell(f, "Sheet1!C3") == (baseCell=XLSX.CellRef("B1"), baseValue=3)
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:3, j in 1:3
+ s[i, j] = i + j
+ end
+ XLSX.mergeCells(s, :, :)
+ @test XLSX.getMergedBaseCell(f, "Sheet1!B2") == (baseCell=XLSX.CellRef("A1"), baseValue=2)
+
+ f = XLSX.newxlsx()
+ s = f[1]
+ for i in 1:3, j in 1:3
+ s[i, j] = i + j
+ end
+ XLSX.mergeCells(s, :)
+ @test XLSX.getMergedBaseCell(f, "Sheet1!B2") == (baseCell=XLSX.CellRef("A1"), baseValue=2)
+
+
+
end
@testset "filemodes" begin
@@ -1928,11 +5526,11 @@ end
]
# can't read or edit a file that does not exist
- @test_throws AssertionError XLSX.openxlsx(filename, mode="r") do xf
+ @test_throws XLSX.XLSXError XLSX.openxlsx(filename, mode="r") do xf
error("This should fail.")
end
- @test_throws AssertionError XLSX.openxlsx(filename, mode="rw") do xf
+ @test_throws XLSX.XLSXError XLSX.openxlsx(filename, mode="rw") do xf
error("This should fail.")
end
@@ -1992,9 +5590,11 @@ end
# test writing throws error if flag not set
XLSX.openxlsx(filename) do xf
sheet = xf[1]
- @test_throws AssertionError sheet[1, 1] = "failure"
+ @test_throws XLSX.XLSXError sheet[1, 1] = "failure"
end
+ @test_throws XLSX.XLSXError f = XLSX.openxlsx(filename; mode="rw", enable_cache=false) # Cache must be enabled to open in `write` mode.
+
@testset "write column" begin
col_data = collect(1:50)
@@ -2052,7 +5652,7 @@ end
test_data = [1 2 3; 4 5 6; 7 8 9]
XLSX.openxlsx(filename, mode="w") do xf
sheet = xf[1]
- @test_throws AssertionError sheet["A7:C10"] = test_data
+ @test_throws XLSX.XLSXError sheet["A7:C10"] = test_data
end
end
@@ -2132,28 +5732,28 @@ end
# These tests are not sufficient. It may be possible using these tests (or similar) to create XLSX files
# that are not valid Excel files and will not successfully open. I do not now how to test this here but
# have successfully tested `output_table_escape_test.xlsx` and `escape.xlsx` manually.
- @test XLSX.xlsx_escape("hello&world<'") == "hello&world<'"
+ @test XML.escape("hello&world<'") == "hello&world<'"
@test XML.unescape("hello&world<'") == "hello&world<'"
esc_filename = "output_table_escape_test.xlsx"
isfile(esc_filename) && rm(esc_filename)
esc_col_names = ["&' & \" < > '", "I❤Julia", "\"<'&O-O&'>\"", "<&>"]
- esc_sheetname = XLSX.xlsx_escape("& & \" > < ")
+ esc_sheetname = "& & \" > < "
esc_data = Vector{Any}(undef, 4)
esc_data[1] = ["11&&", "12\"&", "13<&", "14>&", "15'&"]
esc_data[2] = ["21&&&&", "22&\"&&", "23&<&&", "24&>&&", "25&'&&"]
esc_data[3] = ["31&&&&&&", "32&&\"&&&", "33&&<&&&", "34&&>&&&", "35&&'&&&"]
esc_data[4] = ["41& &; &&", "42\" \"; \"\"", "43< <; <<", "44> >; >>", "45' '; ''"]
- XLSX.writetable(esc_filename, esc_data, XLSX.xlsx_escape.(esc_col_names), overwrite=true, sheetname=esc_sheetname)
+ XLSX.writetable(esc_filename, esc_data, esc_col_names, overwrite=true, sheetname=esc_sheetname)
dtable = XLSX.readtable(esc_filename, esc_sheetname)
r1_data, r1_col_names = dtable.data, dtable.column_labels
check_test_data(r1_data, esc_data)
- @test XML.unescape(string(r1_col_names[4])) == esc_col_names[4]
- @test XML.unescape(string(r1_col_names[3])) == esc_col_names[3]
- @test XML.unescape(string(r1_col_names[2])) == esc_col_names[2]
- @test XML.unescape(string(r1_col_names[1])) == esc_col_names[1]
+ @test r1_col_names[4] == Symbol(esc_col_names[4])
+ @test r1_col_names[3] == Symbol(esc_col_names[3])
+ @test r1_col_names[2] == Symbol(esc_col_names[2])
+ @test r1_col_names[1] == Symbol(esc_col_names[1])
rm(esc_filename)
# compare to the backup version: escape.xlsx
@@ -2161,10 +5761,10 @@ end
r2_data, r2_col_names = [[x isa String ? XML.unescape(x) : x for x in y] for y in dtable.data], dtable.column_labels
check_test_data(r2_data, esc_data)
check_test_data(r2_data, r1_data)
- @test XML.unescape(string(r2_col_names[4])) == esc_col_names[4]
- @test XML.unescape(string(r2_col_names[3])) == esc_col_names[3]
- @test XML.unescape(string(r2_col_names[2])) == esc_col_names[2]
- @test XML.unescape(string(r2_col_names[1])) == esc_col_names[1]
+ @test string(r2_col_names[4]) == esc_col_names[4]
+ @test string(r2_col_names[3]) == esc_col_names[3]
+ @test string(r2_col_names[2]) == esc_col_names[2]
+ @test string(r2_col_names[1]) == esc_col_names[1]
end
# issue #67
@@ -2257,6 +5857,13 @@ end
@test ismissing(sheet["J2"])
end
+# issue #299
+@testset "empty_v" begin
+ xf = XLSX.readxlsx(joinpath(data_directory, "empty_v.xlsx"))
+ sheet1 = xf["Sheet1"]
+ @test XLSX.getcell(sheet1, "A1") == XLSX.Cell(XLSX.CellRef("A1"), "str", "", "", XLSX.Formula("\"\""))
+end
+
@testset "Tables.jl integration" begin
f = XLSX.readxlsx(joinpath(data_directory, "general.xlsx"))
s = f["table"]