From 727c729bdf44199ef556c919e8aaf07eed550b83 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 3 Mar 2025 15:47:09 +0000 Subject: [PATCH 001/154] Add `newxlsx()` and `opentemplate()` and `CellRef` to docs --- docs/src/api.md | 7 ++-- src/cellformats.jl | 21 +++++++++++ src/read.jl | 3 +- src/types.jl | 1 + src/write.jl | 88 ++++++++++++++++++++++++++++------------------ 5 files changed, 82 insertions(+), 38 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index 90f7447d..2a2b32bb 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -5,15 +5,20 @@ XLSX.XLSXFile XLSX.readxlsx XLSX.openxlsx +XLSX.opentemplate +XLSX.newxlsx XLSX.writexlsx XLSX.sheetnames XLSX.sheetcount XLSX.hassheet XLSX.Worksheet +XLSX.rename! +XLSX.addsheet! XLSX.readdata XLSX.getdata XLSX.getcell XLSX.getcellrange +XLSX.CellRef XLSX.row_number XLSX.column_number XLSX.eachrow @@ -22,8 +27,6 @@ XLSX.gettable XLSX.eachtablerow XLSX.writetable XLSX.writetable! -XLSX.rename! -XLSX.addsheet! XLSX.setFormat XLSX.setUniformFormat XLSX.setFont diff --git a/src/cellformats.jl b/src/cellformats.jl index a64b11e1..ada3e6ef 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -463,6 +463,7 @@ julia> setFont(xf, "bigred"; size=48, color="FF00FF00") ``` """ +function setFont end 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, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setFont, ws, ref_or_rng; kw...) @@ -600,6 +601,7 @@ julia> setUniformFont(xf, "bigred"; size=48, color="FF00FF00") ``` """ +function setUniformFont end setUniformFont(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setUniformFont, ws, colrng; 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...) @@ -646,6 +648,7 @@ julia> getFont(xf, "Sheet1!A1") ``` """ +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) @@ -743,6 +746,7 @@ julia> getBorder(xf, "Sheet1!A1") ``` """ +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) @@ -870,6 +874,7 @@ Julia> setBorder(xf, "Sheet1!D4"; left = ["style" => "dotted", "color" => "F ``` """ +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_or_rng::AbstractString; kw...)::Int = process_ranges(setBorder, ws, ref_or_rng; kw...) @@ -1003,6 +1008,7 @@ Julia> setUniformBorder(xf, "Sheet1!A1:F20"; left = ["style" => "dotted", "c ``` """ +function setUniformBorder end setUniformBorder(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setUniformBorder, ws, colrng; 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...) @@ -1036,6 +1042,7 @@ Julia> setOutsideBorder(xf, "Sheet1!A1:F20"; style = "dotted", color = "FF000FF0 ``` """ +function setOutsideBorder end setOutsideBorder(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setOutsideBorder, ws, colrng; 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...) @@ -1149,6 +1156,7 @@ julia> getFill(xf, "Sheet1!A1") ``` """ +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) @@ -1245,6 +1253,7 @@ Julia> setFill(xf, "Sheet1!A1:F20"; pattern="none", fgColor = "88FF8800") ``` """ +function setFill end setFill(ws::Worksheet, rng::CellRange; kw...)::Int = process_cellranges(setFill, ws, rng; 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...) @@ -1355,6 +1364,7 @@ Julia> setUniformFill(xf, "Sheet1!A1:F20"; pattern="none", fgColor = "88FF8800") ``` """ +function setUniformFill end setUniformFill(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setUniformFill, ws, colrng; 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...) @@ -1412,6 +1422,7 @@ julia> getAlignment(xf, "Sheet1!A1") ``` """ +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) @@ -1485,6 +1496,7 @@ julia> setAlignment(sh, "L6"; horizontal="center", rotation="90", shrink=true, i ``` """ +function setAlignment end 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, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setAlignment, ws, ref_or_rng; kw...) @@ -1604,6 +1616,7 @@ Julia> setUniformAlignment(xf, "Sheet1!A1:F20"; horizontal="center", vertical="t ``` """ +function setUniformAlignment end setUniformAlignment(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setUniformAlignment, ws, colrng; 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...) @@ -1639,6 +1652,7 @@ julia> getFormat(xf, "Sheet1!A1") ``` """ +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) @@ -1714,6 +1728,7 @@ julia> XLSX.setFormat(sh, "A2"; format = "_-£* #,##0.00_-;-£* #,##0.00_-;_-£* ``` """ +function setFormat end 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, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setFormat, ws, ref_or_rng; kw...) @@ -1812,6 +1827,7 @@ julia> XLSX.setUniformFormat(sh, "F1:F5"; format = "Currency") ``` """ +function setUniformFormat end setUniformFormat(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setUniformFormat, ws, colrng; 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...) @@ -1849,6 +1865,7 @@ julia> XLSX.setUniformStyle(sh, "F1:F5") ``` """ +function setUniformStyle end setUniformStyle(ws::Worksheet, colrng::ColumnRange)::Int = process_columnranges(setUniformStyle, ws, colrng) 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) @@ -1919,6 +1936,7 @@ julia> XLSX.setColumnWidth(sh, "I"; width = 24.37) ``` """ +function setColumnWidth end setColumnWidth(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setColumnWidth, ws, colrng; 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...) @@ -2009,6 +2027,7 @@ julia> XLSX.getColumnWidth(sh, "F1") ``` """ +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) function getColumnWidth(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real} @@ -2080,6 +2099,7 @@ julia> XLSX.setRowHeight(sh, "I"; height = 24.56) ``` """ +function setRowHeight end setRowHeight(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(setRowHeight, ws, colrng; 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...) @@ -2129,6 +2149,7 @@ julia> XLSX.getRowHeight(sh, "F1") ``` """ +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) function getRowHeight(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real} diff --git a/src/read.jl b/src/read.jl index 4cf9acf5..5ba9bb63 100644 --- a/src/read.jl +++ b/src/read.jl @@ -45,7 +45,8 @@ Consider using [`XLSX.openxlsx`](@ref) for lazy loading of Excel file contents. """ openxlsx(f::F, source::Union{AbstractString, IO}; mode::AbstractString="r", enable_cache::Bool=true) where {F<:Function} -Open XLSX file for reading and/or writing. It returns an opened XLSXFile that will be automatically closed after applying `f` to the file. +Open an XLSX file for reading and/or writing. It returns an opened XLSXFile that will be automatically closed +after applying `f` to the file. # `Do` syntax diff --git a/src/types.jl b/src/types.jl index a6dcb473..ae9ba8c3 100644 --- a/src/types.jl +++ b/src/types.jl @@ -34,6 +34,7 @@ println( XLSX.row_number(cn) ) # will print 1 println( XLSX.column_number(cn) ) # will print 28 println( string(cn) ) # will print out AB1 ``` + """ struct CellRef name::String diff --git a/src/write.jl b/src/write.jl index e9062f43..94613f39 100644 --- a/src/write.jl +++ b/src/write.jl @@ -1,35 +1,44 @@ -#= - open_xlsx_template(source::Union{AbstractString, IO}) :: XLSXFile +""" + opentemplate(source::Union{AbstractString, IO}) :: 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`. + +# Examples +```julia +julia> xf = opentemplate("myExcelFile") +``` + +""" +opentemplate(source::Union{AbstractString, IO}) :: XLSXFile = open_or_read_xlsx(source, true, true, true) -Open an Excel file as template for editing and saving to another file with `XLSX.writexlsx`. -=# @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 -#= - open_empty_template( - sheetname::AbstractString=""; - relocatable_data_path::AbstractString=_relocatable_data_path() - ) :: XLSXFile +""" + newxlsx() :: XLSXFile -Returns an empty, writable `XLSXFile` with 1 worksheet. +Return an empty, writable `XLSXFile` with 1 worksheet (`Sheet1`) for editing and +saving to a file with `XLSX.writexlsx`. -# Arguments +# Examples +```julia +julia> xf = newxlsx() +``` -* `sheetname` is the name of the worksheet. When provided with an empty string `""`, this routine selects the first sheet of the workbook. +""" +newxlsx() = open_empty_template() -* `relocatable_data_path` is the filepath for a blank workbook template. It defaults to the template provided by the package artifact. -=# function open_empty_template( sheetname::AbstractString=""; - relocatable_data_path::AbstractString=_relocatable_data_path() + path::AbstractString=_relocatable_data_path() ) :: XLSXFile - empty_excel_template = joinpath(relocatable_data_path, "blank.xlsx") + 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) @@ -43,7 +52,7 @@ end """ writexlsx(output_source, xlsx_file; [overwrite=false]) -Writes an Excel file given by `xlsx_file::XLSXFile` to IO or filepath `output_source`. +Write an Excel file given by `xlsx_file::XLSXFile` to IO or filepath `output_source`. If `overwrite=true`, `output_source` (when a filepath) will be overwritten if it exists. """ @@ -378,33 +387,40 @@ function setdata!(ws::Worksheet, cell::Cell) nothing end - -const ESCAPE_CHARS = ('&' => "&", '<' => "<", '>' => ">", "'" => "'", '"' => """) -#const ILLEGAL_CHARS = [Char(0x02) => " ", Char(0x12) => "'", Char(0x16) => ""] -#conts ILLEGAL_CHARS [r"\x00-\x08\x0B\x0E\x0F\x10-\x19" => ""] - +# This set of characters works in my use case. I don't know: +# - if the set is sufficient, or if other charachers may be needed in other use cases +# - if all of these characters are necessary or if one or two coulld be dropped +# - What the optimum replacement character should be. +const ILLEGAL_CHARS = [ + Char(0x00) => "", + Char(0x01) => "", + Char(0x02) => "", + Char(0x03) => "", + Char(0x04) => "", + Char(0x05) => "", + Char(0x06) => "", + Char(0x07) => "", + Char(0x08) => "", + Char(0x12) => "'", + Char(0x16) => "" +] function strip_illegal_chars(x::String) -# Not implemented yet! -# result = x -# for (pat, r) in ILLEGAL_CHARS -# result = replace(result, pat => r) -# end -# return result - return x + result = x + for (pat, r) in ILLEGAL_CHARS + result = replace(result, pat => r) + end + 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) @@ -618,7 +634,8 @@ end """ rename!(ws::Worksheet, name::AbstractString) -Renames a `Worksheet`. +Rename a `Worksheet` to `name`. + """ function rename!(ws::Worksheet, name::AbstractString) @@ -658,8 +675,9 @@ addsheet!(xl::XLSXFile, name::AbstractString="") :: Worksheet = addsheet!(get_wo """ addsheet!(workbook, [name]) :: Worksheet -Create a new worksheet with named `name`. +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) From faada661dd623733968b1b53178814592f0097ec Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 3 Mar 2025 16:03:57 +0000 Subject: [PATCH 002/154] A few typos --- src/cellformats.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cellformats.jl b/src/cellformats.jl index ada3e6ef..504a3ab2 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -1104,7 +1104,7 @@ in the `patternFill` element are: - `bgColor` : Specifies the background color of the pattern. The child elements `fgColor` and `bgColor` can each have one or two attributes -of their own. These color attributes are pushed in to the `DellFill.fill` Dict +of their own. These color attributes are pushed in to the `CellFill.fill` Dict of attributes with either `fg` or `bg` prepended to their names to support later reconstruction of the xml element. @@ -1714,7 +1714,8 @@ Alternatively, `format` can be used to specify any custom format directly. Only weak checks are made of custom formats specified - they are otherwise added to the XLSXfile verbatim. -Formats may need characters that must to be escaped when specified. +Formats may need characters that must be escaped when specified (see third +example, below). # Examples: ```julia From 2ec5241b31f515a533b61891bc783589088be081 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 8 Mar 2025 17:02:49 +0000 Subject: [PATCH 003/154] Add row ranges to formatting functions. --- src/cellformats.jl | 116 +++++++++++++++++++++++++++++++++------------ src/cellref.jl | 67 ++++++++++++++++++++++++-- src/types.jl | 13 +++++ src/worksheet.jl | 2 +- 4 files changed, 162 insertions(+), 36 deletions(-) diff --git a/src/cellformats.jl b/src/cellformats.jl index 504a3ab2..524fc53f 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -223,6 +223,9 @@ function process_sheetcell(f::Function, xl::XLSXFile, sheetcell::String; kw...): elseif is_valid_sheet_column_range(sheetcell) sheetcolrng = SheetColumnRange(sheetcell) newid = f(xl[sheetcolrng.sheet], sheetcolrng.colrng; kw...) + elseif is_valid_sheet_row_range(sheetcell) + sheetrowrng = SheetRowRange(sheetcell) + newid = f(xl[sheetrowrng.sheet], sheetrowrng.rowrng; kw...) elseif is_valid_sheet_cellrange(sheetcell) sheetcellrng = SheetCellRange(sheetcell) newid = f(xl[sheetcellrng.sheet], sheetcellrng.rng; kw...) @@ -265,6 +268,9 @@ function process_ranges(f::Function, ws::Worksheet, ref_or_rng::AbstractString; elseif is_valid_column_range(ref_or_rng) colrng = ColumnRange(ref_or_rng) newid = f(ws, colrng; kw...) + elseif is_valid_row_range(ref_or_rng) + rowrng = RowRange(ref_or_rng) + newid = f(ws, rowrng; kw...) elseif is_valid_cellrange(ref_or_rng) rng = CellRange(ref_or_rng) newid = f(ws, rng; kw...) @@ -296,6 +302,27 @@ function process_columnranges(f::Function, ws::Worksheet, colrng::ColumnRange; k error("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 + error("Row range $rowrng 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 @@ -326,7 +353,7 @@ function process_get_cellname(f::Function, ws::Worksheet, ref_or_rng::AbstractSt 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.") + error("Can only assign borders 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 @@ -406,7 +433,7 @@ end setFont(xf::XLSXFile, cr::String, 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. Font attributes are specified using keyword arguments: - `bold::Bool = nothing` : set to `true` to make the font bold. @@ -457,6 +484,8 @@ julia> setFont(sh, "A:B"; italic=true, color="FF8888FF", under="single") 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 @@ -466,6 +495,7 @@ julia> setFont(xf, "bigred"; size=48, color="FF00FF00") function setFont end 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, 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; @@ -559,8 +589,8 @@ 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. +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. 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 @@ -595,6 +625,8 @@ 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 @@ -603,6 +635,7 @@ julia> setUniformFont(xf, "bigred"; size=48, color="FF00FF00") """ function setUniformFont end 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(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, rng::CellRange; kw...)::Int = process_uniform_attribute(setFont, ws, rng, ["fontId", "applyFont"]; kw...) @@ -669,7 +702,7 @@ function getFont(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellFont} 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)))." +## @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 @@ -801,7 +834,7 @@ end setBorder(xf::XLSXFile, cr::String; 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. Borders are independently defined for the keywords: - `left::Vector{Pair{String,String} = nothing` @@ -877,6 +910,7 @@ Julia> setBorder(xf, "Sheet1!D4"; left = ["style" => "dotted", "color" => "F 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, rowrng::RowRange; kw...)::Int = process_rowranges(setBorder, ws, rowrng; 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...) function setBorder(sh::Worksheet, cellref::CellRef; @@ -969,8 +1003,8 @@ 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. +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. 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 @@ -1010,6 +1044,7 @@ Julia> setUniformBorder(xf, "Sheet1!A1:F20"; left = ["style" => "dotted", "c """ function setUniformBorder end 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(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, rng::CellRange; kw...)::Int = process_uniform_attribute(setBorder, ws, rng, ["borderId", "applyBorder"]; kw...) @@ -1018,8 +1053,8 @@ setUniformBorder(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_at 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. +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. Two key words can be defined: - `style::String = nothing` : defines the style of the outside border @@ -1044,6 +1079,7 @@ Julia> setOutsideBorder(xf, "Sheet1!A1:F20"; style = "dotted", color = "FF000FF0 """ function setOutsideBorder end 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; @@ -1202,7 +1238,7 @@ end setFill(xf::XLSXFile, cr::String; 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. The following keywords are used to define a fill: - `pattern::String = nothing` : Sets the patternType for the fill. @@ -1251,10 +1287,13 @@ Julia> setFill(sh, "B2"; pattern="gray125", bgColor = "FF000000") Julia> setFill(xf, "Sheet1!A1:F20"; pattern="none", fgColor = "88FF8800") +Julia> setFill(sh, "11:24"; pattern="none", fgColor = "88FF8800") + ``` """ function setFill end 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, 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...) @@ -1330,8 +1369,8 @@ 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. +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. 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 @@ -1366,6 +1405,7 @@ Julia> setUniformFill(xf, "Sheet1!A1:F20"; pattern="none", fgColor = "88FF8800") """ function setUniformFill end 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(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, rng::CellRange; kw...)::Int = process_uniform_attribute(setFill, ws, rng, ["fillId", "applyFill"]; kw...) @@ -1451,7 +1491,7 @@ end setAlignment(xf::XLSXFile, cr::String; 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. The following keywords are used to define an alignment: - `horizontal::String = nothing` : Sets the horizontal alignment. @@ -1499,6 +1539,7 @@ julia> setAlignment(sh, "L6"; horizontal="center", rotation="90", shrink=true, i function setAlignment end 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, 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; @@ -1581,8 +1622,8 @@ 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. +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. 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 @@ -1618,6 +1659,7 @@ Julia> setUniformAlignment(xf, "Sheet1!A1:F20"; horizontal="center", vertical="t """ function setUniformAlignment end 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(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, rng::CellRange; kw...)::Int = process_uniform_attribute(setAlignment, ws, rng; kw...) @@ -1693,7 +1735,7 @@ end setFormat(xf::XLSXFile, cr::String; 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. +row range or a named cell or named range in a worksheet or XLSXfile. The function uses one keyword used to define a format: - `format::String = nothing` : Defines a built-in or custom number format @@ -1732,6 +1774,7 @@ julia> XLSX.setFormat(sh, "A2"; format = "_-£* #,##0.00_-;-£* #,##0.00_-;_-£* function setFormat end 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, 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...) function setFormat(sh::Worksheet, cellref::CellRef; @@ -1803,8 +1846,8 @@ 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. +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. 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 @@ -1830,6 +1873,7 @@ julia> XLSX.setUniformFormat(sh, "F1:F5"; format = "Currency") """ function setUniformFormat end 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(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...) @@ -1842,9 +1886,9 @@ 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`. +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`. As a result, every cell in the range will have a uniform `style`. @@ -1868,6 +1912,7 @@ julia> XLSX.setUniformStyle(sh, "F1:F5") """ function setUniformStyle end setUniformStyle(ws::Worksheet, colrng::ColumnRange)::Int = process_columnranges(setUniformStyle, ws, colrng) +setUniformStyle(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(setUniformStyle, ws, rowrng; kw...) 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} @@ -1939,6 +1984,7 @@ julia> XLSX.setColumnWidth(sh, "I"; width = 24.37) """ function setColumnWidth end 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, 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...) @@ -2086,9 +2132,13 @@ function does not attempt to replicate it, but simply adds 0.21 internal units to the value specified. The value set is unlikely to match the value seen interactivley in the resultant spreadsheet, but it will be close. +Row height cannot be set for empty rows, which will quietly be skipped. +A row must have at least one cell containing a value before its height can be set. + You can set a row height to 0. -The function returns a value of 0. +The function returns a value of 0 unless all rows are empty, in which case +it returns a value of -1. # Examples: ```julia @@ -2097,11 +2147,12 @@ julia> XLSX.setRowHeight(xf, "Sheet1!A2"; height = 50) julia> XLSX.setRowHeight(sh, "F1:F5"; heighth = 0) julia> XLSX.setRowHeight(sh, "I"; height = 24.56) -``` +``` """ function setRowHeight end 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, 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...) @@ -2117,16 +2168,21 @@ function setRowHeight(ws::Worksheet, rng::CellRange; height::Union{Nothing,Real} if isnothing(height) # No-op return 0 end + first = true for r in eachrow(ws) if r.row in top:bottom - if r.row ∈ ws.cache.rows_in_cache + if haskey(ws.cache.row_ht, r.row) ws.cache.row_ht[r.row] = padded_height + first = false end - end + end end + if first == true + return -1 + end return 0 # meaningless return value. Int required to comply with reference decoding structure. end @@ -2142,6 +2198,8 @@ The function will use the row number and ignore the column. The function returns the value of the row height or nothing if the row does not have an explicitly defined height. +If the row is not found (an empty row), returns -1. + # Examples: ```julia julia> XLSX.getRowHeight(xf, "Sheet1!A2") @@ -2162,13 +2220,11 @@ function getRowHeight(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real} for r in eachrow(ws) if r.row == cellref.row_number - if r.row ∈ ws.cache.rows_in_cache + if haskey(ws.cache.row_ht, r.row) return ws.cache.row_ht[r.row] - else - return nothing end - end + end end diff --git a/src/cellref.jl b/src/cellref.jl index 07cac399..d4c9e3f7 100644 --- a/src/cellref.jl +++ b/src/cellref.jl @@ -60,6 +60,7 @@ Base.:(==)(c1::CellRef, c2::CellRef) = c1.name == c2.name Base.hash(c::CellRef) = hash(c.name) 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,6 +76,18 @@ 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]+$" @@ -267,8 +280,13 @@ 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). +# Also works for row ranges! @inline function split_column_range(n::AbstractString) if !occursin(":", n) return n, n @@ -279,24 +297,39 @@ 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) - 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) + @assert row_number > 0 && row_number <= EXCEL_MAX_ROWS "Row number should be in the range from 1 to $EXCEL_MAX_ROWS." + return true + end + if !occursin(RGX_ROW_RANGE, r) + return false + end + start_name, stop_name = split_column_range(r) + if !is_valid_row_name(start_name) || !is_valid_row_name(stop_name) + return false + end return true end +function RowRange(r::AbstractString) + @assert is_valid_row_range(r) "Invalid row range: $r." + start_name, stop_name = split_column_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) @@ -305,9 +338,13 @@ 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) @@ -361,10 +398,12 @@ Base.hash(cr::SheetColumnRange) = hash(cr.sheet) + hash(cr.colrng) 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 !occursin(RGX_SHEET_CELLNAME, n) @@ -404,6 +443,18 @@ 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]+$" @@ -451,6 +502,12 @@ function SheetColumnRange(n::AbstractString) sheetname = parse_sheetname_from_sheetcell_name(n) return SheetColumnRange(sheetname, ColumnRange(column_range)) end +function SheetRowRange(n::AbstractString) + @assert is_valid_sheet_row_range(n) "$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_SHEET_CELLNAME = r"^.+!\$[A-Z]+\$[0-9]+$" diff --git a/src/types.jl b/src/types.jl index ae9ba8c3..3e3a5ec8 100644 --- a/src/types.jl +++ b/src/types.jl @@ -201,6 +201,15 @@ struct ColumnRange return new(a, b) end end +struct RowRange + start::Int # row number + stop::Int # row number + + function RowRange(a::Int, b::Int) + @assert a <= b "Invalid RowRange. Start row must be located before end row." + return new(a, b) + end +end struct SheetCellRef sheet::String @@ -216,6 +225,10 @@ struct SheetColumnRange sheet::String colrng::ColumnRange end +struct SheetRowRange + sheet::String + rowrng::RowRange +end abstract type MSOfficePackage end diff --git a/src/worksheet.jl b/src/worksheet.jl index 15dbe5e2..b37c231b 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -239,7 +239,7 @@ 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 single.row_number ∈ ws.cache.rows_in_cache + 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 From 7ec47107013a0597f44c3d73d665bc6ff848bc1c Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 8 Mar 2025 17:17:31 +0000 Subject: [PATCH 004/154] Update CI --- .github/workflows/ci.yml | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4705dc50..487c6f25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,21 +28,12 @@ jobs: arch: - x64 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v1 - env: - cache-name: cache-artifacts - with: - path: ~/.julia/artifacts - key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} - restore-keys: | - ${{ runner.os }}-test-${{ env.cache-name }}- - ${{ runner.os }}-test- - ${{ runner.os }}- + - uses: actions/cache@v2 - uses: julia-actions/julia-buildpkg@v1 continue-on-error: ${{ matrix.version == 'nightly' }} - uses: julia-actions/julia-runtest@v1 @@ -55,7 +46,7 @@ jobs: name: Documentation runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v2 with: version: '1' From c215a61b7011cbe96c2ff7c9b3a44d2c6a325d4e Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 14 Mar 2025 19:52:59 +0000 Subject: [PATCH 005/154] Add `normalizenames` keyword to `XLSX.readtable` (#260) --- Project.toml | 1 + src/XLSX.jl | 1 + src/cellformats.jl | 2 ++ src/read.jl | 13 ++++++---- src/table.jl | 64 +++++++++++++++++++++++++++++++--------------- 5 files changed, 56 insertions(+), 25 deletions(-) diff --git a/Project.toml b/Project.toml index 5e95882a..6688b876 100644 --- a/Project.toml +++ b/Project.toml @@ -12,6 +12,7 @@ Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" XML = "72c71f33-b9b6-44de-8c94-c961784809e2" ZipArchives = "49080126-0e18-4c2a-b176-c102e4b3760c" +Unicode = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" [compat] Tables = "1" diff --git a/src/XLSX.jl b/src/XLSX.jl index 59309249..deadb45c 100644 --- a/src/XLSX.jl +++ b/src/XLSX.jl @@ -7,6 +7,7 @@ import Printf.@printf import ZipArchives import XML import Tables +import Unicode import Base.convert const SPREADSHEET_NAMESPACE_XPATH_ARG = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" diff --git a/src/cellformats.jl b/src/cellformats.jl index 524fc53f..84de272b 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -1065,6 +1065,8 @@ 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 settings for all internal cells in the range will remain unchanged. +Top and bottom borders for column ranges are taken from the worksheet `dimension`. + The value returned is is -1. For keyword definitions see [`setBorder()`](@ref). diff --git a/src/read.jl b/src/read.jl index 5ba9bb63..0a8b42bf 100644 --- a/src/read.jl +++ b/src/read.jl @@ -526,7 +526,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`. @@ -550,6 +551,8 @@ 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`. @@ -582,16 +585,16 @@ 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) +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, normalizenames::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) + 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, normalizenames=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) +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, normalizenames::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) + 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, normalizenames=normalizenames) end return c end diff --git a/src/table.jl b/src/table.jl index e0fbe436..edf32a6d 100644 --- a/src/table.jl +++ b/src/table.jl @@ -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_symbol(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*"_$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,9 +148,10 @@ 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 + if first_row === nothing first_row = _find_first_row_with_data(sheet, convert(ColumnRange, cols).start) end @@ -144,23 +159,28 @@ function eachtablerow( column_range = convert(ColumnRange, cols) if column_labels === nothing - column_labels = Vector{Symbol}() + col_lab = Vector{String}() 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) + push_unique!(col_lab, sheet, cell) end else # generate column_labels if there's no header information anywhere for c in column_range - push!(column_labels, Symbol(c)) + push!(col_lab, string(c)) 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." + @assert length(col_lab) == length(column_range) "`column_range` (length=$(length(column_range))) and `column_labels` (length=$(length(col_lab))) must have the same length." + end + 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 @@ -179,6 +199,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,7 +245,7 @@ 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 @@ -463,7 +484,7 @@ function check_table_data_dimension(data::Vector) nothing end -function gettable(itr::TableRowIterator; infer_eltypes::Bool=false) :: DataTable +function gettable(itr::TableRowIterator; infer_eltypes::Bool=false, normalizenames::Bool=false) :: DataTable column_labels = get_column_labels(itr) columns_count = table_columns_count(itr) data = Vector{Any}(undef, columns_count) @@ -516,7 +537,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,6 +562,8 @@ 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`. @@ -571,15 +595,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) +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, normalizenames::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, normalizenames=normalizenames) return gettable(itr; infer_eltypes=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) +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, normalizenames::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, normalizenames=normalizenames) return gettable(itr; infer_eltypes=infer_eltypes) end From 98ea6c833032e408ae7f9b3d7c813c916e127281 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 16 Mar 2025 13:21:04 +0000 Subject: [PATCH 006/154] Add three functions for merged cells (no tests yet) to address #241 --- src/cellformats.jl | 134 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 1 deletion(-) diff --git a/src/cellformats.jl b/src/cellformats.jl index 84de272b..1ec5f388 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -1065,7 +1065,8 @@ 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 settings for all internal cells in the range will remain unchanged. -Top and bottom borders for column ranges are taken from the worksheet `dimension`. +Top and bottom borders for column ranges and left and right borders for +row ranges are taken from the worksheet `dimension`. The value returned is is -1. @@ -2233,3 +2234,134 @@ function getRowHeight(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real} return -1 # Row specified not found (is empty) end + +""" + 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 + + +# 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} + + @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot get merged cells because cache is not enabled." + + sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet$(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]) + @assert length(c) == parse(Int, sheetdoc[i][j]["count"]) "Unexpected number of mergeCells found: $(length(c)). Expected $(sheetdoc[i][j]["count"])." + + mergedCells = Vector{CellRange}() + for cell in c + @assert haskey(cell, "ref") "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 + +Return `true` if a cell is part of a merged cell range and `false` if not. + +Alternatively, if you have already obtained the merged cells for the worksheet, +you can avoid repeated determinations and pass them as an argument to the function: + + isMergedCell(ws::Worksheet, cr::CellRef, mergedCells::Union{Vector{CellRange}, Nothing}) -> Bool + +# Examples: +```julia +julia> XLSX.isMergedCell(xf, "Sheet1!A1") + +julia> XLSX.isMergedCell(sh, "A1") + +julia> mc = XLSX.getMergedCells(sh) +julia> XLSX.isMergedCell(sh, XLSX.CellRef("A1"), mc) + +``` +""" +function isMergedCell end +isMergedCell(xl::XLSXFile, sheetcell::String)::Bool = process_get_sheetcell(isMergedCell, xl, sheetcell) +isMergedCell(ws::Worksheet, cr::String)::Bool = process_get_cellname(isMergedCell, ws, cr) +isMergedCell(ws::Worksheet, cellref::CellRef)::Bool = isMergedCell(ws, cellref, getMergedCells(ws)) +function isMergedCell(ws::Worksheet, cellref::CellRef, mergedCells::Union{Vector{CellRange}, Nothing})::Bool + + @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot get merged cells because cache is not enabled." + + if isnothing(mergedCells) + 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}} + +Return the cell reference and cell value of the base cell of a merged cell range in a worksheet as a named tuple. +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 + +# Examples: +```julia +julia> XLSX.getMergedBaseCell(xf, "Sheet1!B2") +(baseCell = B1, baseValue = 3) + +julia> XLSX.getMergedBaseCell(sh, "B2") +(baseCell = B1, baseValue = 3) + + +``` +""" +function getMergedBaseCell end +getMergedBaseCell(xl::XLSXFile, sheetcell::String) = process_get_sheetcell(getMergedBaseCell, xl, sheetcell) +getMergedBaseCell(ws::Worksheet, cr::String) = process_get_cellname(getMergedBaseCell, ws, cr) +getMergedBaseCell(ws::Worksheet, cellref::CellRef) = getMergedBaseCell(ws, cellref, getMergedCells(ws)) +function getMergedBaseCell(ws::Worksheet, cellref::CellRef, mergedCells::Union{Vector{CellRange}, Nothing}) + + @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot get merged cells because cache is not enabled." + +# @assert isMergedCell(ws, cellref, mergedCells) "Cell $cellref is not part of a merged cell." # Just return nothing instead! + + for rng in mergedCells + if cellref ∈ rng + return (; baseCell=rng.start, baseValue = ws[rng.start]) + end + end + return nothing +end \ No newline at end of file From aeff287d002c644ef2e86fd6f54bba1f3fd2fab1 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 16 Mar 2025 17:17:47 +0000 Subject: [PATCH 007/154] Functions to add Defined Names (more work to do) --- src/types.jl | 3 +- src/workbook.jl | 80 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/types.jl b/src/types.jl index 3e3a5ec8..2fc0518b 100644 --- a/src/types.jl +++ b/src/types.jl @@ -341,7 +341,8 @@ 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. diff --git a/src/workbook.jl b/src/workbook.jl index db0375c2..7cf15470 100644 --- a/src/workbook.jl +++ b/src/workbook.jl @@ -183,3 +183,83 @@ end @inline is_defined_name_value_a_reference(v::DefinedNameValueTypes) = isa(v, SheetCellRef) || isa(v, SheetCellRange) @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 !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(wb::Workbook, name::AbstractString, value::DefinedNameValueTypes) + if is_workbook_defined_name(wb, name) + error("Workbook already has a defined name called $name.") + end + if !is_valid_defined_name(name) + error("Invalid defined name: $name.") + end + wb.workbook_names[name] = value +end +function addDefName(ws::Worksheet, name::AbstractString, value::DefinedNameValueTypes) + wb = get_workbook(ws) + if is_worksheet_defined_name(wb, ws.sheetId, name) + error("Worksheet $(ws.name) already has a defined name called $name.") + end + if !is_valid_defined_name(name) + error("Invalid defined name: $name.") + end + wb.worksheet_names[(ws.sheetId, name)] = value +end + +""" +When naming workbook name references (or named ranges) in Excel, there are specific rules and guidelines to follow to ensure they work properly. Here's a summary: + +Start with a Letter: The name must begin with a letter or an underscore (_) and cannot start with a number or special character. + +Avoid Spaces: Names cannot contain spaces. Instead, use an underscore (_) or capitalize the first letter of each word (e.g., "SalesData" or "Sales_Data"). + +Length: The name can be up to 255 characters long, though shorter, meaningful names are preferable for clarity. + +Unique within a Workbook: Each name must be unique within a workbook. You cannot reuse the same name for multiple references. + +Restricted Characters: Names cannot include special characters such as +, -, /, *, ,, or .. They can only contain letters, numbers, underscores (_), and backslashes (\\). + +No Cell References: A name cannot look like a cell reference (e.g., "A1" or "Z100") to avoid confusion. + +Reserved Words: Names cannot use reserved words like "R" or "C," as Excel uses these for row and column references in certain settings. +""" +function addDefinedName end +addDefinedName(wb::Workbook, name::AbstractString, value::Union{Int, Float64, Missing}) = addDefName(wb, name, value) +addDefinedName(ws::Worksheet, name::AbstractString, value::Union{Int, Float64, Missing}) = addDefName(ws, name, value) +function addDefinedName(wb::Workbook, name::AbstractString, value::AbstractString) + if value == "" + error("Defined name value cannot be an empty string.") + end + if is_valid_sheet_cellname(value) + return addDefName(wb, name, SheetCellRef(value)) + elseif is_valid_sheet_cellrange(value) + return addDefName(wb, name, SheetCellRange(value)) + else + return addDefName(wb, name, value) + end +end +function addDefinedName(ws::Worksheet, name::AbstractString, value::AbstractString) + if value == "" + error("Defined name value cannot be an empty string.") + end + if is_valid_cellname(value) + return addDefName(ws, name, SheetCellRef(ws.name, CellRef(value))) + elseif is_valid_cellrange(value) + return addDefName(ws, name, SheetCellRange(ws.name, CellRange(value))) + else + return addDefName(ws, name, value) + end +end From a80af317c7f281c89f3f3f67bb758071718925bc Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 16 Mar 2025 23:43:58 +0000 Subject: [PATCH 008/154] Add more support for non-contiguous ranges in defined names. --- src/cellformats.jl | 10 ++++- src/cellref.jl | 103 +++++++++++++++++++++++++++++++++++++++++++++ src/types.jl | 7 ++- src/workbook.jl | 43 +++++++++++++++---- 4 files changed, 152 insertions(+), 11 deletions(-) diff --git a/src/cellformats.jl b/src/cellformats.jl index 1ec5f388..8d2e4cb6 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -2081,6 +2081,7 @@ 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) 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." @@ -2235,6 +2236,10 @@ function getRowHeight(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real} end +# +# -- Get merged cells +# + """ getMergedCells(ws::Worksheet) -> Union{Vector{CellRange}, Nothing} @@ -2260,9 +2265,12 @@ julia> XLSX.getMergedCells(s) ``` """ function getMergedCells(ws::Worksheet)::Union{Vector{CellRange}, Nothing} + # May be better if merged cells were part of ws.cache? @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot get merged cells because cache is not enabled." + # No need to update the xml file using the worksheet cache first (like we did for column width) + # because we cannot change merged cells in XLSX.jl. sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet$(ws.sheetId).xml") # find the block in the worksheet's xml file i, j = get_idces(sheetdoc, "worksheet", "mergeCells") @@ -2356,8 +2364,6 @@ function getMergedBaseCell(ws::Worksheet, cellref::CellRef, mergedCells::Union{V @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot get merged cells because cache is not enabled." -# @assert isMergedCell(ws, cellref, mergedCells) "Cell $cellref is not part of a merged cell." # Just return nothing instead! - for rng in mergedCells if cellref ∈ rng return (; baseCell=rng.start, baseValue = ws[rng.start]) diff --git a/src/cellref.jl b/src/cellref.jl index d4c9e3f7..0eba2bbf 100644 --- a/src/cellref.jl +++ b/src/cellref.jl @@ -109,6 +109,9 @@ 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 @@ -149,6 +152,9 @@ julia> XLSX.split_cellrange("AB12:CD24") 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 @@ -395,6 +401,11 @@ 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::NonContiguousRange) = join([string(cr.sheet, "!", x) for x in cr.rng],",") +Base.show(io::IO, cr::NonContiguousRange) = print(io, string(cr)) +Base.:(==)(cr1::NonContiguousRange, cr2::SheetColumnRange) = cr1.sheet == cr2.sheet && cr2.rng == cr2.rng +Base.hash(cr::NonContiguousRange) = hash(cr.sheet) + hash(cr.rng) + 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]$" @@ -406,6 +417,11 @@ 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 @@ -419,6 +435,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 @@ -517,3 +538,85 @@ is_valid_fixed_sheet_cellname(s::AbstractString) = occursin(RGX_FIXED_SHEET_CELL 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) + 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) + return false + end + end + + return true +end + +nonContiguousRange(s::Worksheet, v::AbstractString)::NonContiguousRange = nCR(s.name, string.(split(v, ","))) +function nonContiguousRange(v::AbstractString)::NonContiguousRange + + @assert is_valid_non_contiguous_range(v) "$v is not a valid non-contiguous range." + + ranges = string.(split(v, ",")) + firstsheet = parse_sheetname_from_sheetcell_name(ranges[1]) + @assert all(parse_sheetname_from_sheetcell_name(r) == firstsheet for r in ranges) "All `CellRef`s and `CellRange`s should have the same sheet name." + + return nCR(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_CELNAME_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_cellrange(n) + push!(noncontig, CellRange(n)) + elseif is_valid_cellname(n) + push!(noncontig, CellRef(n)) + else + error("Invalid non-contiguous range: $n.") + end + end + + return NonContiguousRange(s, noncontig) +end \ No newline at end of file diff --git a/src/types.jl b/src/types.jl index 2fc0518b..83282b2b 100644 --- a/src/types.jl +++ b/src/types.jl @@ -221,6 +221,11 @@ struct SheetCellRange rng::CellRange end +struct NonContiguousRange + sheet::String + rng::Vector{Union{CellRef, CellRange}} +end + struct SheetColumnRange sheet::String colrng::ColumnRange @@ -322,7 +327,7 @@ 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} # Workbook is the result of parsing file `xl/workbook.xml`. mutable struct Workbook diff --git a/src/workbook.jl b/src/workbook.jl index 7cf15470..383c68bb 100644 --- a/src/workbook.jl +++ b/src/workbook.jl @@ -216,25 +216,46 @@ function addDefName(ws::Worksheet, name::AbstractString, value::DefinedNameValue if !is_valid_defined_name(name) error("Invalid defined name: $name.") end + if value isa NonContiguousRange + @assert replace(value.sheet, "'" => "") == ws.name "Non-contiguous range must be in the same worksheet." + end wb.worksheet_names[(ws.sheetId, name)] = value end """ -When naming workbook name references (or named ranges) in Excel, there are specific rules and guidelines to follow to ensure they work properly. Here's a summary: + addDefinedName(wb::Workbook, name::AbstractString, value::Union{Int, Float64, Missing}) + addDefinedName(wb::Workbook, name::AbstractString, value::AbstractString) + addDefinedName(ws::Worksheet, name::AbstractString, value::Union{Int, Float64, Missing}) + addDefinedName(ws::Worksheet, name::AbstractString, value::AbstractString) + +Add a defined name to the Workbook or Worksheet. + +A defined name is a text string that represents a cell, range of cells, formula, or constant value. +It can be used to refer to a specific cell or range of cells in an Excel formula, making it easier +to read and understand complex formulas. -Start with a Letter: The name must begin with a letter or an underscore (_) and cannot start with a number or special character. +A defined name should: +- Start with a letter an underscore (_) and cannot start with a number or special character. +- Not contain spaces +- Be no more than 255 characters in length +- Benique within a Workbook +- Must not include special characters (such as +, -, /, *, ,, or .) They can only contain letters, numbers, underscores (_), and backslashes (\\). +- Cannot look like a cell reference (e.g., "A1" or "Z100") +- May not use reserved words like "R" or "C" -Avoid Spaces: Names cannot contain spaces. Instead, use an underscore (_) or capitalize the first letter of each word (e.g., "SalesData" or "Sales_Data"). +# Examples +```julia +julia> XLSX.addDefinedName(sh, "ID", "C21") -Length: The name can be up to 255 characters long, though shorter, meaningful names are preferable for clarity. +julia> XLSX.addDefinedName(sh, "NEW", "'Mock-up'!A1:B2") -Unique within a Workbook: Each name must be unique within a workbook. You cannot reuse the same name for multiple references. +julia> XLSX.addDefinedName(sh, "my_name", "A1,B2,C3") -Restricted Characters: Names cannot include special characters such as +, -, /, *, ,, or .. They can only contain letters, numbers, underscores (_), and backslashes (\\). +julia> XLSX.addDefinedName(XLSX.get_workbook(sh), "Life_the_universe_and_everything", 42) -No Cell References: A name cannot look like a cell reference (e.g., "A1" or "Z100") to avoid confusion. +julia> XLSX.addDefinedName(XLSX.get_workbook(sh), "first_name", "Hello World") -Reserved Words: Names cannot use reserved words like "R" or "C," as Excel uses these for row and column references in certain settings. +``` """ function addDefinedName end addDefinedName(wb::Workbook, name::AbstractString, value::Union{Int, Float64, Missing}) = addDefName(wb, name, value) @@ -247,6 +268,8 @@ function addDefinedName(wb::Workbook, name::AbstractString, value::AbstractStrin return addDefName(wb, name, SheetCellRef(value)) elseif is_valid_sheet_cellrange(value) return addDefName(wb, name, SheetCellRange(value)) + elseif is_valid_non_contiguous_sheetcellrange(value) + return addDefName(wb, name, nonContiguousRange(value)) else return addDefName(wb, name, value) end @@ -259,6 +282,10 @@ function addDefinedName(ws::Worksheet, name::AbstractString, value::AbstractStri return addDefName(ws, name, SheetCellRef(ws.name, CellRef(value))) elseif is_valid_cellrange(value) return addDefName(ws, name, SheetCellRange(ws.name, CellRange(value))) + elseif is_valid_non_contiguous_sheetcellrange(value) + return addDefName(ws, name, nonContiguousRange(value)) + elseif is_valid_non_contiguous_cellrange(value) + return addDefName(ws, name, nonContiguousRange(ws, value)) else return addDefName(ws, name, value) end From ab8a906bb848f236d3ac08095a82c2a9c2ef1922 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 17 Mar 2025 15:30:03 +0000 Subject: [PATCH 009/154] Add fix to #239 in writetable!() function --- src/write.jl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/write.jl b/src/write.jl index 94613f39..3a7a4ad2 100644 --- a/src/write.jl +++ b/src/write.jl @@ -586,6 +586,12 @@ 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`, or `DateTime` +will be converted to strings before writing. + + See also: [`XLSX.writetable`](@ref). """ function writetable!( @@ -612,8 +618,10 @@ function writetable!( anchor_row = row_number(anchor_cell) anchor_col = column_number(anchor_cell) start_from_anchor = 1 + # write table header if write_columnnames + columnnames = map(col -> eltype(col) <: String ? col : (s -> "$s").(col), columnnames) # Address issue #239 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]))) @@ -622,6 +630,7 @@ function writetable!( end # write table data + data = map(col -> eltype(col) <: Union{Float64, Int64, String, Bool, Date, DateTime} ? col : (s -> "$s").(col), 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) From 46e608071eda4b01e58b5d499087e2fb354a973c Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 17 Mar 2025 15:32:23 +0000 Subject: [PATCH 010/154] More updates to support non-contiguous cell ramges --- src/cellref.jl | 2 +- src/workbook.jl | 55 +++++++++++++++++++++++++------------------------ 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/src/cellref.jl b/src/cellref.jl index 0eba2bbf..9d01cd29 100644 --- a/src/cellref.jl +++ b/src/cellref.jl @@ -583,7 +583,7 @@ function is_valid_non_contiguous_cellrange(v::AbstractString) :: Bool return true end -nonContiguousRange(s::Worksheet, v::AbstractString)::NonContiguousRange = nCR(s.name, string.(split(v, ","))) +nonContiguousRange(s::Worksheet, v::AbstractString)::NonContiguousRange = nCR("'$(s.name)'", string.(split(v, ","))) function nonContiguousRange(v::AbstractString)::NonContiguousRange @assert is_valid_non_contiguous_range(v) "$v is not a valid non-contiguous range." diff --git a/src/workbook.jl b/src/workbook.jl index 383c68bb..4e16f505 100644 --- a/src/workbook.jl +++ b/src/workbook.jl @@ -14,7 +14,7 @@ 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 ] @inline sheetnames(xl::XLSXFile) = sheetnames(xl.workbook) @@ -23,7 +23,7 @@ sheetnames(wb::Workbook) = [ s.name for s in wb.sheets ] 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 for s in wb.sheets @@ -39,7 +39,7 @@ 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) @@ -181,7 +181,7 @@ function get_defined_name_value(ws::Worksheet, name::AbstractString) :: DefinedN return wb.worksheet_names[(sheetId, name)] 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 @@ -199,23 +199,23 @@ function is_valid_defined_name(name::AbstractString) :: Bool return true end -function addDefName(wb::Workbook, name::AbstractString, value::DefinedNameValueTypes) - if is_workbook_defined_name(wb, name) - error("Workbook already has a defined name called $name.") - end +function addDefName(xf::XLSXFile, name::AbstractString, value::DefinedNameValueTypes) if !is_valid_defined_name(name) error("Invalid defined name: $name.") end - wb.workbook_names[name] = value + if is_workbook_defined_name(xf, name) + error("Workbook already has a defined name called $name.") + end + xf.workbook.workbook_names[name] = value end function addDefName(ws::Worksheet, name::AbstractString, value::DefinedNameValueTypes) wb = get_workbook(ws) - if is_worksheet_defined_name(wb, ws.sheetId, name) - error("Worksheet $(ws.name) already has a defined name called $name.") - end if !is_valid_defined_name(name) error("Invalid defined name: $name.") end + if is_worksheet_defined_name(ws, name) + error("Worksheet $(ws.name) already has a defined name called $name.") + end if value isa NonContiguousRange @assert replace(value.sheet, "'" => "") == ws.name "Non-contiguous range must be in the same worksheet." end @@ -223,12 +223,13 @@ function addDefName(ws::Worksheet, name::AbstractString, value::DefinedNameValue end """ - addDefinedName(wb::Workbook, name::AbstractString, value::Union{Int, Float64, Missing}) - addDefinedName(wb::Workbook, name::AbstractString, value::AbstractString) - addDefinedName(ws::Worksheet, name::AbstractString, value::Union{Int, Float64, Missing}) - addDefinedName(ws::Worksheet, name::AbstractString, value::AbstractString) + addDefinedName(xf::XLSXFile, name::AbstractString, value::Union{Int, Float64, Missing}) + addDefinedName(xf::XLSXFile, name::AbstractString, value::AbstractString) + addDefinedName(sh::Worksheet, name::AbstractString, value::Union{Int, Float64, Missing}) + addDefinedName(sh::Worksheet, name::AbstractString, value::AbstractString) -Add a defined name to the Workbook or Worksheet. +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. A defined name is a text string that represents a cell, range of cells, formula, or constant value. It can be used to refer to a specific cell or range of cells in an Excel formula, making it easier @@ -251,27 +252,27 @@ julia> XLSX.addDefinedName(sh, "NEW", "'Mock-up'!A1:B2") julia> XLSX.addDefinedName(sh, "my_name", "A1,B2,C3") -julia> XLSX.addDefinedName(XLSX.get_workbook(sh), "Life_the_universe_and_everything", 42) +julia> XLSX.addDefinedName(xf, "Life_the_universe_and_everything", 42) -julia> XLSX.addDefinedName(XLSX.get_workbook(sh), "first_name", "Hello World") +julia> XLSX.addDefinedName(xf, "first_name", "Hello World") ``` """ function addDefinedName end -addDefinedName(wb::Workbook, name::AbstractString, value::Union{Int, Float64, Missing}) = addDefName(wb, name, value) +addDefinedName(xf::XLSXFile, name::AbstractString, value::Union{Int, Float64, Missing}) = addDefName(xf, name, value) addDefinedName(ws::Worksheet, name::AbstractString, value::Union{Int, Float64, Missing}) = addDefName(ws, name, value) -function addDefinedName(wb::Workbook, name::AbstractString, value::AbstractString) +function addDefinedName(xf::XLSXFile, name::AbstractString, value::AbstractString) if value == "" error("Defined name value cannot be an empty string.") end if is_valid_sheet_cellname(value) - return addDefName(wb, name, SheetCellRef(value)) + return addDefName(xf, name, SheetCellRef(value)) elseif is_valid_sheet_cellrange(value) - return addDefName(wb, name, SheetCellRange(value)) + return addDefName(xf, name, SheetCellRange(value)) elseif is_valid_non_contiguous_sheetcellrange(value) - return addDefName(wb, name, nonContiguousRange(value)) + return addDefName(xf, name, nonContiguousRange(value)) else - return addDefName(wb, name, value) + return addDefName(xf, name, value) end end function addDefinedName(ws::Worksheet, name::AbstractString, value::AbstractString) @@ -279,9 +280,9 @@ function addDefinedName(ws::Worksheet, name::AbstractString, value::AbstractStri error("Defined name value cannot be an empty string.") end if is_valid_cellname(value) - return addDefName(ws, name, SheetCellRef(ws.name, CellRef(value))) + return addDefName(ws, name, SheetCellRef("'$(ws.name)'", CellRef(value))) elseif is_valid_cellrange(value) - return addDefName(ws, name, SheetCellRange(ws.name, CellRange(value))) + return addDefName(ws, name, SheetCellRange("'$(ws.name)'", CellRange(value))) elseif is_valid_non_contiguous_sheetcellrange(value) return addDefName(ws, name, nonContiguousRange(value)) elseif is_valid_non_contiguous_cellrange(value) From e6ef55b746726612d85f1e1cef26f987931da001 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 17 Mar 2025 17:13:27 +0000 Subject: [PATCH 011/154] Preparing to write definedNames in `xlsxwrite()` --- Project.toml | 1 + src/types.jl | 3 +++ src/write.jl | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/Project.toml b/Project.toml index 6688b876..0e31f02d 100644 --- a/Project.toml +++ b/Project.toml @@ -19,6 +19,7 @@ Tables = "1" XML = "0.3.4" ZipArchives = "2" julia = "1.7" +Unicode = 3 [extras] DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" diff --git a/src/types.jl b/src/types.jl index 83282b2b..1ef4a7d9 100644 --- a/src/types.jl +++ b/src/types.jl @@ -330,6 +330,9 @@ end const DefinedNameValueTypes = Union{SheetCellRef, SheetCellRange, NonContiguousRange, Int, Float64, String, Missing} # Workbook is the result of parsing file `xl/workbook.xml`. +# The `xl/workbook.xml` wi9ll 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. diff --git a/src/write.jl b/src/write.jl index 3a7a4ad2..9b8e64b3 100644 --- a/src/write.jl +++ b/src/write.jl @@ -65,6 +65,7 @@ function writexlsx(output_source::Union{AbstractString, IO}, xf::XLSXFile; overw end update_worksheets_xml!(xf) + update_workbook_xml!(xf) ZipArchives.ZipWriter(output_source) do xlsx # write XML files @@ -185,6 +186,22 @@ function unlink_rows(node::XML.Node) # removes all rows from a sheetData XML nod end return new_worksheet end +function unlink_definedNames(node::XML.Node) # removes each `col` from a `cols` XML node. + new_cols = XML.Element("definedNames") + a = XML.attributes(node) + if !isnothing(a) # Copy attributes across to new node (probably none) + 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) != "definedName" # Shouldn't be any. + push!(new_cols, child) + end + end + return new_cols +end + function get_idces(doc, t, b) i=1 j=1 @@ -336,6 +353,29 @@ function update_worksheets_xml!(xl::XLSXFile) nothing end +function update_workbook_xml!(xl::XLSXFile) + wb = get_workbook(xl) + + wbdoc = xmlroot(xl, "xl/workbook.xml") # find the block in the workbook's xml file + i, j = get_idces(wbdoc, "definedNames", "definedName") + println(i, " ", j) + + definedNames = unlink_definedNames(wbdoc[i][j]) # Remove old defined names + + definedNames = XML.Element("definedNames") # Create a new definedNames block + for (k, v) in wb.workbook_names + dn_node = XML.Element("definedName", name=dn[k], XML.Text(dn[v])) + push!(definedNames, dn_node) + end + for (k, v) in wb.worksheet_names + dn_node = XML.Element("definedName", name=last(dn[k]), localSheetId=first(dn[k]), XML.Text(dn[v])) + push!(definedNames, dn_node) + end + println(XML.write(definedNames)) + wbdoc[i][j] = definedNames # Add the new definedNames block to the workbook's xml file + return nothing +end + function add_cell_to_worksheet_dimension!(ws::Worksheet, cell::Cell) # update worksheet dimension ws_dimension = get_dimension(ws) From ebd6ebcc51861e1ea73378e63459b464744b1532 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 17 Mar 2025 23:51:23 +0000 Subject: [PATCH 012/154] Bug fixing new code with existing tests --- Project.toml | 6 ++--- src/cellformats.jl | 30 ++++++++++++++++++++++- src/cellref.jl | 25 +++++++++++++------ src/read.jl | 4 +-- src/table.jl | 61 ++++++++++++++++++++++++---------------------- src/workbook.jl | 26 +++++++++++--------- src/write.jl | 34 +++++++++++++++++++------- 7 files changed, 123 insertions(+), 63 deletions(-) diff --git a/Project.toml b/Project.toml index 0e31f02d..5f2ac321 100644 --- a/Project.toml +++ b/Project.toml @@ -10,20 +10,20 @@ Artifacts = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" XML = "72c71f33-b9b6-44de-8c94-c961784809e2" ZipArchives = "49080126-0e18-4c2a-b176-c102e4b3760c" -Unicode = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" [compat] Tables = "1" XML = "0.3.4" ZipArchives = "2" julia = "1.7" -Unicode = 3 + [extras] DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test", "DataFrames"] +test = ["Test", "DataFrames"] \ No newline at end of file diff --git a/src/cellformats.jl b/src/cellformats.jl index 8d2e4cb6..9de6fbdf 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -220,14 +220,21 @@ function process_sheetcell(f::Function, xl::XLSXFile, sheetcell::String; kw...): else error("Unexpected defined name value: $v.") end + elseif is_valid_non_contiguous_sheetcellrange(sheetcell) + sheetncrng = nonContiguousRange(sheetcell) + @assert hassheet(xl, sheetncrng.sheet) "Sheet $(ref.sheet) not found." + newid = f(xl[sheetncrng.sheet], sheetncrng; kw...) elseif is_valid_sheet_column_range(sheetcell) sheetcolrng = SheetColumnRange(sheetcell) + @assert hassheet(xl, sheetcolrng.sheet) "Sheet $(ref.sheet) not found." newid = f(xl[sheetcolrng.sheet], sheetcolrng.colrng; kw...) elseif is_valid_sheet_row_range(sheetcell) sheetrowrng = SheetRowRange(sheetcell) + @assert hassheet(xl, sheetrowrng.sheet) "Sheet $(ref.sheet) not found." newid = f(xl[sheetrowrng.sheet], sheetrowrng.rowrng; kw...) elseif is_valid_sheet_cellrange(sheetcell) sheetcellrng = SheetCellRange(sheetcell) + @assert hassheet(xl, sheetcellrng.sheet) "Sheet $(ref.sheet) not found." newid = f(xl[sheetcellrng.sheet], sheetcellrng.rng; kw...) elseif is_valid_sheet_cellname(sheetcell) ref = SheetCellRef(sheetcell) @@ -256,7 +263,7 @@ function process_ranges(f::Function, ws::Worksheet, ref_or_rng::AbstractString; 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) + if is_valid_non_contiguous_range(string(v)) _ = f.(Ref(get_xlsxfile(wb)), replace.(split(string(v), ","), "'" => "", "\$" => ""); kw...) newid = -1 else @@ -323,6 +330,12 @@ function process_rowranges(f::Function, ws::Worksheet, rowrng::RowRange; kw...): error("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 + for r in ncrng.rng + _ = f(ws, r; kw...) + end + return -1 +end function process_cellranges(f::Function, ws::Worksheet, rng::CellRange; kw...)::Int for cellref in rng if getcell(ws, cellref) isa EmptyCell @@ -496,6 +509,7 @@ function setFont end 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; @@ -636,6 +650,7 @@ julia> setUniformFont(xf, "bigred"; size=48, color="FF00FF00") function setUniformFont end 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_ncranges(setUniformFont, ws, ncrng; 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, rng::CellRange; kw...)::Int = process_uniform_attribute(setFont, ws, rng, ["fontId", "applyFont"]; kw...) @@ -911,6 +926,7 @@ 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, rowrng::RowRange; kw...)::Int = process_rowranges(setBorder, ws, 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...) function setBorder(sh::Worksheet, cellref::CellRef; @@ -1045,6 +1061,7 @@ Julia> setUniformBorder(xf, "Sheet1!A1:F20"; left = ["style" => "dotted", "c function setUniformBorder end 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_ncranges(setUniformBorder, ws, ncrng; 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, rng::CellRange; kw...)::Int = process_uniform_attribute(setBorder, ws, rng, ["borderId", "applyBorder"]; kw...) @@ -1068,6 +1085,8 @@ settings for all internal cells in the range will remain unchanged. Top and bottom borders for column ranges and left and right borders for row ranges are taken from the worksheet `dimension`. +An outside border cannot be set for a non-contiguous range. + The value returned is is -1. For keyword definitions see [`setBorder()`](@ref). @@ -1297,6 +1316,7 @@ Julia> setFill(sh, "11:24"; pattern="none", fgColor = "88FF8800") function setFill end 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...) @@ -1409,6 +1429,7 @@ Julia> setUniformFill(xf, "Sheet1!A1:F20"; pattern="none", fgColor = "88FF8800") function setUniformFill end 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_ncranges(setUniformFill, ws, ncrng; 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, rng::CellRange; kw...)::Int = process_uniform_attribute(setFill, ws, rng, ["fillId", "applyFill"]; kw...) @@ -1543,6 +1564,7 @@ function setAlignment end 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; @@ -1663,6 +1685,7 @@ Julia> setUniformAlignment(xf, "Sheet1!A1:F20"; horizontal="center", vertical="t function setUniformAlignment end 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_ncranges(setUniformAlignment, 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, rng::CellRange; kw...)::Int = process_uniform_attribute(setAlignment, ws, rng; kw...) @@ -1777,6 +1800,7 @@ julia> XLSX.setFormat(sh, "A2"; format = "_-£* #,##0.00_-;-£* #,##0.00_-;_-£* function setFormat end 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...) @@ -1877,6 +1901,7 @@ julia> XLSX.setUniformFormat(sh, "F1:F5"; format = "Currency") function setUniformFormat end 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_ncranges(setUniformFormat, ws, ncrng; 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...) @@ -1916,6 +1941,7 @@ julia> XLSX.setUniformStyle(sh, "F1:F5") function setUniformStyle end setUniformStyle(ws::Worksheet, colrng::ColumnRange)::Int = process_columnranges(setUniformStyle, ws, colrng) setUniformStyle(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(setUniformStyle, ws, rowrng; kw...) +setUniformStyle(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setUniformStyle, ws, ncrng; kw...) 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} @@ -1988,6 +2014,7 @@ julia> XLSX.setColumnWidth(sh, "I"; width = 24.37) function setColumnWidth end 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...) @@ -2157,6 +2184,7 @@ julia> XLSX.setRowHeight(sh, "I"; height = 24.56) function setRowHeight end 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...) diff --git a/src/cellref.jl b/src/cellref.jl index 9d01cd29..997896c7 100644 --- a/src/cellref.jl +++ b/src/cellref.jl @@ -266,7 +266,7 @@ 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))" @@ -275,6 +275,12 @@ Base.:(==)(cr1::ColumnRange, cr2::ColumnRange) = cr1.start == cr2.start && cr2.s 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) = "$(cr.start):$(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." return column_number - rng.start + 1 @@ -292,7 +298,7 @@ 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). -# Also works for row ranges! +# Also works for row ranges (row_name_start, row_name_stop)! @inline function split_column_range(n::AbstractString) if !occursin(":", n) return n, n @@ -324,7 +330,7 @@ function is_valid_row_range(r::AbstractString) :: Bool if !occursin(RGX_ROW_RANGE, r) return false end - start_name, stop_name = split_column_range(r) + start_name, stop_name = split_column_range(r) # Function works for row ranges too. if !is_valid_row_name(start_name) || !is_valid_row_name(stop_name) return false end @@ -333,7 +339,7 @@ end function RowRange(r::AbstractString) @assert is_valid_row_range(r) "Invalid row range: $r." - start_name, stop_name = split_column_range(r) + start_name, stop_name = split_column_range(r) # Function works for row ranges too. return RowRange(parse(Int, start_name), parse(Int, stop_name)) end function ColumnRange(r::AbstractString) @@ -383,7 +389,7 @@ function Base.length(rng::CellRange) end # -# SheetCellRef, SheetCellRange, SheetColumnRange +# SheetCellRef, SheetCellRange, SheetColumnRange, SheetRowRange, NonContiguousRange # Base.string(cr::SheetCellRef) = string(cr.sheet, "!", cr.cellref) @@ -401,6 +407,11 @@ 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(cr.sheet, "!", cr.colrng) +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(cr.sheet, "!", x) for x in cr.rng],",") Base.show(io::IO, cr::NonContiguousRange) = print(io, string(cr)) Base.:(==)(cr1::NonContiguousRange, cr2::SheetColumnRange) = cr1.sheet == cr2.sheet && cr2.rng == cr2.rng @@ -537,7 +548,7 @@ const RGX_FIXED_SHEET_CELLRANGE = r"^.+!\$[A-Z]+\$[0-9]+:\$[A-Z]+\$[0-9]+$" 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_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) @@ -583,7 +594,7 @@ function is_valid_non_contiguous_cellrange(v::AbstractString) :: Bool return true end -nonContiguousRange(s::Worksheet, v::AbstractString)::NonContiguousRange = nCR("'$(s.name)'", string.(split(v, ","))) +nonContiguousRange(s::Worksheet, v::AbstractString)::NonContiguousRange = nCR(quoteit(s.name), string.(split(v, ","))) function nonContiguousRange(v::AbstractString)::NonContiguousRange @assert is_valid_non_contiguous_range(v) "$v is not a valid non-contiguous range." diff --git a/src/read.jl b/src/read.jl index 0a8b42bf..7322dcb3 100644 --- a/src/read.jl +++ b/src/read.jl @@ -396,10 +396,10 @@ function parse_workbook!(xf::XLSXFile) else # Couldn't parse definedName. Will silently ignore it, since this is not a critical feature. - continue + # continue # debug - #error("Could not parse value $(defined_value_string) for definedName $name.") + error("Could not parse value $(defined_value_string) for definedName $name.") end a = XML.attributes(defined_name_node) if haskey(a,"localSheetId") diff --git a/src/table.jl b/src/table.jl index edf32a6d..0dc99e87 100644 --- a/src/table.jl +++ b/src/table.jl @@ -59,7 +59,7 @@ function _colname_prefix_string(sheet::Worksheet, cell::Cell) return string(d) end end -_colname_prefix_symbol(sheet::Worksheet, ::EmptyCell) = "#Empty" +_colname_prefix_string(sheet::Worksheet, ::EmptyCell) = "#Empty" # helper function to manage problematic column labels # Empty cell -> "#Empty" @@ -151,40 +151,43 @@ function eachtablerow( normalizenames::Bool=false ) :: TableRowIterator - if first_row === nothing - first_row = _find_first_row_with_data(sheet, convert(ColumnRange, cols).start) - end + #let col_lab - itr = eachrow(sheet) - column_range = convert(ColumnRange, cols) + if first_row === nothing + first_row = _find_first_row_with_data(sheet, convert(ColumnRange, cols).start) + end - if column_labels === nothing + itr = eachrow(sheet) + column_range = convert(ColumnRange, cols) col_lab = Vector{String}() - 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) + + 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!(col_lab, string(c)) - end + # 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 - else - # check consistency for column_range and column_labels - @assert length(col_lab) == length(column_range) "`column_range` (length=$(length(column_range))) and `column_labels` (length=$(length(col_lab))) must have the same length." - end - 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) + 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) diff --git a/src/workbook.jl b/src/workbook.jl index 4e16f505..92314e38 100644 --- a/src/workbook.jl +++ b/src/workbook.jl @@ -120,13 +120,7 @@ function getdata(xl::XLSXFile, rng::SheetColumnRange) 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 @@ -135,9 +129,15 @@ function getdata(xl::XLSXFile, s::AbstractString) else error("Unexpected 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)) end - error("$s is not a valid sheetname or cell/range reference.") + error("$s is not a valid definedName or cell/range reference.") end function getcell(xl::XLSXFile, ref::SheetCellRef) @@ -222,6 +222,8 @@ function addDefName(ws::Worksheet, name::AbstractString, value::DefinedNameValue wb.worksheet_names[(ws.sheetId, name)] = value end +quoteit(x::AbstractString) = occursin(r"^[0-9]|[\s,:!&#@*]", x) ? "'$x'" : x + """ addDefinedName(xf::XLSXFile, name::AbstractString, value::Union{Int, Float64, Missing}) addDefinedName(xf::XLSXFile, name::AbstractString, value::AbstractString) @@ -272,7 +274,7 @@ function addDefinedName(xf::XLSXFile, name::AbstractString, value::AbstractStrin elseif is_valid_non_contiguous_sheetcellrange(value) return addDefName(xf, name, nonContiguousRange(value)) else - return addDefName(xf, name, value) + return addDefName(xf, name, value isa String ? "\"$value\"" : value) end end function addDefinedName(ws::Worksheet, name::AbstractString, value::AbstractString) @@ -280,14 +282,14 @@ function addDefinedName(ws::Worksheet, name::AbstractString, value::AbstractStri error("Defined name value cannot be an empty string.") end if is_valid_cellname(value) - return addDefName(ws, name, SheetCellRef("'$(ws.name)'", CellRef(value))) + return addDefName(ws, name, SheetCellRef(quoteit(ws.name), CellRef(value))) elseif is_valid_cellrange(value) - return addDefName(ws, name, SheetCellRange("'$(ws.name)'", CellRange(value))) + return addDefName(ws, name, SheetCellRange(quoteit(ws.name), CellRange(value))) elseif is_valid_non_contiguous_sheetcellrange(value) return addDefName(ws, name, nonContiguousRange(value)) elseif is_valid_non_contiguous_cellrange(value) return addDefName(ws, name, nonContiguousRange(ws, value)) else - return addDefName(ws, name, value) + return addDefName(ws, name, value isa String ? "\"$value\"" : value) end end diff --git a/src/write.jl b/src/write.jl index 9b8e64b3..32f616be 100644 --- a/src/write.jl +++ b/src/write.jl @@ -357,22 +357,37 @@ function update_workbook_xml!(xl::XLSXFile) wb = get_workbook(xl) wbdoc = xmlroot(xl, "xl/workbook.xml") # find the block in the workbook's xml file - i, j = get_idces(wbdoc, "definedNames", "definedName") - println(i, " ", j) + i, j = get_idces(wbdoc, "workbook", "definedNames") + + definedNames = isnothing(j) ? XML.Element("definedNames") : unlink_definedNames(wbdoc[i][j]) # Remove old defined names + + 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 - definedNames = unlink_definedNames(wbdoc[i][j]) # Remove old defined names + else + definedNames = unlink_definedNames(wbdoc[i][j]) # Remove old defined names + end - definedNames = XML.Element("definedNames") # Create a new definedNames block for (k, v) in wb.workbook_names - dn_node = XML.Element("definedName", name=dn[k], XML.Text(dn[v])) + dn_node = XML.Element("definedName", name=k, XML.Text(v)) push!(definedNames, dn_node) end for (k, v) in wb.worksheet_names - dn_node = XML.Element("definedName", name=last(dn[k]), localSheetId=first(dn[k]), XML.Text(dn[v])) + dn_node = XML.Element("definedName", name=last(k), localSheetId=first(k)-1, XML.Text(v)) push!(definedNames, dn_node) end - println(XML.write(definedNames)) + wbdoc[i][j] = definedNames # Add the new definedNames block to the workbook's xml file + return nothing end @@ -661,7 +676,6 @@ function writetable!( # write table header if write_columnnames - columnnames = map(col -> eltype(col) <: String ? col : (s -> "$s").(col), columnnames) # Address issue #239 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]))) @@ -670,11 +684,13 @@ function writetable!( end # write table data - data = map(col -> eltype(col) <: Union{Float64, Int64, String, Bool, Date, DateTime} ? col : (s -> "$s").(col), 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] + if !(typeof(v) <: Union{Number, String, Bool, Dates.Date, Dates.Time, Dates.DateTime, Missing, Nothing}) + v = "$v" + end sheet[target_cell_ref] = v isa String ? strip_illegal_chars(xlsx_escape(v)) : v end end From 893e23a355c688789e415b40a0ff362f0993c305 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 18 Mar 2025 18:44:09 +0000 Subject: [PATCH 013/154] Support `getdata()` for row ranges --- src/cellformats.jl | 16 ++++++-- src/cellref.jl | 59 ++++++++++++++++++++++++++--- src/read.jl | 10 +++-- src/stream.jl | 6 ++- src/table.jl | 3 +- src/workbook.jl | 10 +++++ src/worksheet.jl | 92 +++++++++++++++++++++++++++++++++++++++++----- src/write.jl | 23 +++++++++--- 8 files changed, 190 insertions(+), 29 deletions(-) diff --git a/src/cellformats.jl b/src/cellformats.jl index 9de6fbdf..8fdf8d5a 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -331,10 +331,20 @@ function process_rowranges(f::Function, ws::Worksheet, rowrng::RowRange; kw...): end end function process_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int - for r in ncrng.rng - _ = f(ws, r; kw...) + bounds = nc_bounds(ncrng) + 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 + _ = f(ws, r; kw...) + end + return -1 + else + error("Non-contiguous range $ncrng is out of bounds. Worksheet `$(ws.name)` only has dimension `$dim`.") end - return -1 end function process_cellranges(f::Function, ws::Worksheet, rng::CellRange; kw...)::Int for cellref in rng diff --git a/src/cellref.jl b/src/cellref.jl index 997896c7..6989c225 100644 --- a/src/cellref.jl +++ b/src/cellref.jl @@ -417,6 +417,56 @@ Base.show(io::IO, cr::NonContiguousRange) = print(io, string(cr)) Base.:(==)(cr1::NonContiguousRange, cr2::SheetColumnRange) = 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 # Assumes the same sheet name for both `CellRef` and `NonContiguousRange`. + if ref.sheet != ncrng.sheet + return false + end + for r in ncrng.rng + if r isa CellRef + if ref == r + return true + end + else + if ref 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 `rng`. + s = 0 + for rng in r.rng + if rng isa CellRef + s += 1 + else + s += length(rng) + end + end + return s +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]$" @@ -490,7 +540,7 @@ 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." @@ -517,7 +567,7 @@ 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." @@ -548,8 +598,6 @@ const RGX_FIXED_SHEET_CELLRANGE = r"^.+!\$[A-Z]+\$[0-9]+:\$[A-Z]+\$[0-9]+$" 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 @@ -572,7 +620,6 @@ function is_valid_non_contiguous_sheetcellrange(v::AbstractString) :: Bool return false end - return true end @@ -616,7 +663,7 @@ function nCR(s::AbstractString, ranges::Vector{String}) :: NonContiguousRange 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_CELNAME_RIGHT_FIXED, n).match + 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)) diff --git a/src/read.jl b/src/read.jl index 7322dcb3..fedb38d0 100644 --- a/src/read.jl +++ b/src/read.jl @@ -396,12 +396,14 @@ function parse_workbook!(xf::XLSXFile) 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) + #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. + # error("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 diff --git a/src/stream.jl b/src/stream.jl index fbf52dcb..525d1cd5 100644 --- a/src/stream.jl +++ b/src/stream.jl @@ -20,7 +20,7 @@ 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. @@ -306,6 +306,10 @@ 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. """ function eachrow(ws::Worksheet) :: SheetRowIterator if is_cache_enabled(ws) diff --git a/src/table.jl b/src/table.jl index 0dc99e87..90d9d9c8 100644 --- a/src/table.jl +++ b/src/table.jl @@ -487,7 +487,8 @@ function check_table_data_dimension(data::Vector) nothing end -function gettable(itr::TableRowIterator; infer_eltypes::Bool=false, normalizenames::Bool=false) :: DataTable +#function gettable(itr::TableRowIterator; infer_eltypes::Bool=false, normalizenames::Bool=false) :: DataTable +function gettable(itr::TableRowIterator; infer_eltypes::Bool=false) :: DataTable column_labels = get_column_labels(itr) columns_count = table_columns_count(itr) data = Vector{Any}(undef, columns_count) diff --git a/src/workbook.jl b/src/workbook.jl index 92314e38..68a2f099 100644 --- a/src/workbook.jl +++ b/src/workbook.jl @@ -119,6 +119,11 @@ function getdata(xl::XLSXFile, rng::SheetColumnRange) return getdata(getsheet(xl, rng.sheet), rng.colrng) end +function getdata(xl::XLSXFile, rng::SheetRowRange) + @assert hassheet(xl, rng.sheet) "Sheet $(rng.sheet) not found." + return getdata(getsheet(xl, rng.sheet), rng.rowrng) +end + function getdata(xl::XLSXFile, s::AbstractString) if is_workbook_defined_name(xl, s) v = get_defined_name_value(xl.workbook, s) @@ -157,6 +162,11 @@ function getcellrange(xl::XLSXFile, rng::SheetColumnRange) return getcellrange(getsheet(xl, rng.sheet), rng.colrng) end +function getcellrange(xl::XLSXFile, rng::SheetRowRange) + @assert hassheet(xl, rng.sheet) "Sheet $(rng.sheet) not found." + return getcellrange(getsheet(xl, rng.sheet), rng.rowrng) +end + function getcellrange(xl::XLSXFile, rng_str::AbstractString) if is_valid_sheet_cellrange(rng_str) return getcellrange(xl, SheetCellRange(rng_str)) diff --git a/src/worksheet.jl b/src/worksheet.jl index b37c231b..d570fdfb 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -82,6 +82,10 @@ julia> sheet = f["mysheet"] julia> matrix = sheet["A1:B4"] +julia> matrix = sheet["A:B"] + +julia> matrix = sheet["1:4"] + julia> single_value = sheet[2, 2] # B2 ``` @@ -162,15 +166,40 @@ function getdata(ws::Worksheet, rng::ColumnRange) :: Array{Any,2} return hcat(columns...) end +function getdata(ws::Worksheet, rng::RowRange) :: Array{Any,2} + dim = get_dimension(ws) + + rows = Vector{Vector{Any}}() + + let + top, bottom = row_bounds(rng) + left = dim.start.column_number + right = dim.stop.column_number + + for (i, sheetrow) in enumerate(eachrow(ws)) + push!(rows, Vector{Any}()) + if top <= sheetrow.row && sheetrow.row <= bottom + for column in left:right + cell = getcell(sheetrow, column) + push!(rows[i], getdata(ws, cell)) + end + end + if sheetrow.row > bottom + break + end + end + end + + cols = length(rows[1]) + for r in rows + @assert length(r) == cols "Inconsistent state: Each row should have the same number of columns." + end + + return permutedims(hcat(rows...)) +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) + if is_worksheet_defined_name(ws, ref) v = get_defined_name_value(ws, ref) if is_defined_name_value_a_constant(v) return v @@ -189,6 +218,14 @@ function getdata(ws::Worksheet, ref::AbstractString) :: Union{Array{Any,2}, Any} else error("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)) else error("$ref is not a valid cell or range reference.") end @@ -271,8 +308,11 @@ getcell(ws::Worksheet, row::Integer, col::Integer) = getcell(ws, CellRef(row, co """ 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"`. +For row and column ranges, the extent of the range in the other +dimension is determined by the worksheet's dimension. """ function getcellrange(ws::Worksheet, rng::CellRange) :: Array{AbstractCell,2} result = Array{AbstractCell, 2}(undef, size(rng)) @@ -333,11 +373,45 @@ function getcellrange(ws::Worksheet, rng::ColumnRange) :: Array{AbstractCell,2} return hcat(columns...) end +function getcellrange(ws::Worksheet, rng::RowRange) :: Array{AbstractCell,2} + dim = get_dimension(ws) + + rows = Vector{Vector{AbstractCell}}() + + let + top, bottom = row_bounds(rng) + left = dim.start.column_number + right = dim.stop.column_number + + for (i, sheetrow) in enumerate(eachrow(ws)) + push!(rows, Vector{AbstractCell}()) + if top <= sheetrow.row && sheetrow.row <= bottom + for column in left:right + cell = getcell(sheetrow, column) + push!(rows[i], cell) + end + end + if sheetrow.row > bottom + break + end + end + end + + cols = length(rows[1]) + for r in rows + @assert length(r) == cols "Inconsistent state: Each row should have the same number of columns." + end + + return permutedims(hcat(rows...)) +end + function getcellrange(ws::Worksheet, rng::AbstractString) if is_valid_cellrange(rng) return getcellrange(ws, CellRange(rng)) elseif is_valid_column_range(rng) return getcellrange(ws, ColumnRange(rng)) + elseif is_valid_row_range(rng) + return getcellrange(ws, RowRange(rng)) else error("$rng is not a valid cell range.") end diff --git a/src/write.jl b/src/write.jl index 32f616be..ba822988 100644 --- a/src/write.jl +++ b/src/write.jl @@ -626,6 +626,20 @@ function target_cell_ref_from_offset(anchor_cell::CellRef, offset::Integer, dim: 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 -> "$x", col) + else + # Case 3: Mixed types, process each element + return [typeof(x) <: ALLOWED_TYPES ? x : "$x" for x in col] + end +end + """ writetable!( sheet::Worksheet, @@ -643,8 +657,9 @@ starting at `anchor_cell`. 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`, or `DateTime` -will be converted to strings before writing. +type `String`, `Float64`, `Int64`, `Bool`, `Date`, `Time`, +`DateTime`, `Missing`, or `Nothing` will be converted to strings +before writing. See also: [`XLSX.writetable`](@ref). @@ -684,13 +699,11 @@ function writetable!( 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] - if !(typeof(v) <: Union{Number, String, Bool, Dates.Date, Dates.Time, Dates.DateTime, Missing, Nothing}) - v = "$v" - end sheet[target_cell_ref] = v isa String ? strip_illegal_chars(xlsx_escape(v)) : v end end From e15f5b58a42897aec562a60d7a8ffeb2b551c25c Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Wed, 19 Mar 2025 00:16:15 +0000 Subject: [PATCH 014/154] Handle apostrophes in sheet names in `definedNames` --- src/cellref.jl | 5 ++--- src/read.jl | 4 +++- src/workbook.jl | 23 +++++++++++++++++++++-- src/worksheet.jl | 31 +++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/cellref.jl b/src/cellref.jl index 6989c225..58f77314 100644 --- a/src/cellref.jl +++ b/src/cellref.jl @@ -99,7 +99,6 @@ const RGX_CELLNAME_RIGHT = r"[0-9]+$" 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 @@ -109,6 +108,7 @@ 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 @@ -603,13 +603,12 @@ is_valid_non_contiguous_range(v::AbstractString) :: Bool = is_valid_non_contiguo 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) + 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 diff --git a/src/read.jl b/src/read.jl index fedb38d0..c0157349 100644 --- a/src/read.jl +++ b/src/read.jl @@ -378,7 +378,9 @@ function parse_workbook!(xf::XLSXFile) local defined_value::DefinedNameValueTypes - if is_valid_fixed_sheet_cellname(defined_value_string) || is_valid_sheet_cellname(defined_value_string) + if is_valid_non_contiguous_range(defined_value_string) + defined_value = nonContiguousRange(defined_value_string) + elseif 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) diff --git a/src/workbook.jl b/src/workbook.jl index 68a2f099..845248fc 100644 --- a/src/workbook.jl +++ b/src/workbook.jl @@ -27,7 +27,7 @@ Return `true` if `wb` contains a sheet named `sheetname`. """ 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 @@ -50,7 +50,7 @@ Count the number of sheets in the Workbook. 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 @@ -124,6 +124,11 @@ function getdata(xl::XLSXFile, rng::SheetRowRange) return getdata(getsheet(xl, rng.sheet), rng.rowrng) end +function getdata(xl::XLSXFile, rng::NonContiguousRange) + @assert hassheet(xl, rng.sheet) "Sheet $(rng.sheet) not found." + return getdata(getsheet(xl, rng.sheet), rng) +end + function getdata(xl::XLSXFile, s::AbstractString) if is_workbook_defined_name(xl, s) v = get_defined_name_value(xl.workbook, s) @@ -140,6 +145,10 @@ function getdata(xl::XLSXFile, s::AbstractString) 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_range(s) + return getdata(xl, nonContiguousRange(s)) end error("$s is not a valid definedName or cell/range reference.") @@ -167,11 +176,20 @@ function getcellrange(xl::XLSXFile, rng::SheetRowRange) return getcellrange(getsheet(xl, rng.sheet), rng.rowrng) end +function getcellrange(xl::XLSXFile, rng::NonContiguousRange) + @assert hassheet(xl, rng.sheet) "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) 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.") @@ -233,6 +251,7 @@ function addDefName(ws::Worksheet, name::AbstractString, value::DefinedNameValue end quoteit(x::AbstractString) = occursin(r"^[0-9]|[\s,:!&#@*]", x) ? "'$x'" : x +unquoteit(x::AbstractString) = replace(x, "'" => "") """ addDefinedName(xf::XLSXFile, name::AbstractString, value::Union{Int, Float64, Missing}) diff --git a/src/worksheet.jl b/src/worksheet.jl index d570fdfb..fa579dce 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -198,6 +198,20 @@ function getdata(ws::Worksheet, rng::RowRange) :: Array{Any,2} return permutedims(hcat(rows...)) end +function getdata(ws::Worksheet, rng::NonContiguousRange) :: Vector{Any} + 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 + return results +end + function getdata(ws::Worksheet, ref::AbstractString) :: Union{Array{Any,2}, Any} if is_worksheet_defined_name(ws, ref) v = get_defined_name_value(ws, ref) @@ -226,6 +240,8 @@ function getdata(ws::Worksheet, ref::AbstractString) :: Union{Array{Any,2}, Any} return getdata(ws, ColumnRange(ref)) elseif is_valid_row_range(ref) return getdata(ws, RowRange(ref)) + elseif is_valid_non_contiguous_range(ref) + return getdata(ws, nonContiguousRange(ws, ref)) else error("$ref is not a valid cell or range reference.") end @@ -404,6 +420,19 @@ function getcellrange(ws::Worksheet, rng::RowRange) :: Array{AbstractCell,2} return permutedims(hcat(rows...)) 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 + return results +end function getcellrange(ws::Worksheet, rng::AbstractString) if is_valid_cellrange(rng) @@ -412,6 +441,8 @@ function getcellrange(ws::Worksheet, rng::AbstractString) return getcellrange(ws, ColumnRange(rng)) elseif is_valid_row_range(rng) return getcellrange(ws, RowRange(rng)) + elseif is_valid_non_contiguous_range(rng) + return getcellrange(ws, nonContiguousRange(ws, rng)) else error("$rng is not a valid cell range.") end From 6448f27d88bfd1f1e45aa6d2557b71c59cbf2ea5 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Thu, 20 Mar 2025 19:24:43 +0000 Subject: [PATCH 015/154] Write new `definedNames` out to a new XLSXFile --- src/cellref.jl | 18 ++++++++++---- src/read.jl | 31 +++++++++++++++++------ src/types.jl | 10 ++++++-- src/workbook.jl | 66 +++++++++++++++++++++++++------------------------ src/write.jl | 30 ++++++++++++++++++++++ 5 files changed, 109 insertions(+), 46 deletions(-) diff --git a/src/cellref.jl b/src/cellref.jl index 58f77314..c68e6690 100644 --- a/src/cellref.jl +++ b/src/cellref.jl @@ -592,9 +592,13 @@ function SheetRowRange(n::AbstractString) 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) @@ -632,7 +636,7 @@ function is_valid_non_contiguous_cellrange(v::AbstractString) :: Bool ranges = split(v, ",") for r in ranges - if !is_valid_cellname(r) && !is_valid_cellrange(r) + if !is_valid_cellname(r) && !is_valid_cellrange(r) &&!is_valid_fixed_cellname(r) && !is_valid_fixed_cellrange(r) return false end end @@ -640,7 +644,7 @@ function is_valid_non_contiguous_cellrange(v::AbstractString) :: Bool return true end -nonContiguousRange(s::Worksheet, v::AbstractString)::NonContiguousRange = nCR(quoteit(s.name), string.(split(v, ","))) +nonContiguousRange(s::Worksheet, v::AbstractString)::NonContiguousRange = nCR(s.name, string.(split(v, ","))) function nonContiguousRange(v::AbstractString)::NonContiguousRange @assert is_valid_non_contiguous_range(v) "$v is not a valid non-contiguous range." @@ -649,7 +653,7 @@ function nonContiguousRange(v::AbstractString)::NonContiguousRange firstsheet = parse_sheetname_from_sheetcell_name(ranges[1]) @assert all(parse_sheetname_from_sheetcell_name(r) == firstsheet for r in ranges) "All `CellRef`s and `CellRange`s should have the same sheet name." - return nCR(firstsheet, ranges) + return nCR(unquoteit(firstsheet), ranges) end function nCR(s::AbstractString, ranges::Vector{String}) :: NonContiguousRange @@ -666,10 +670,14 @@ function nCR(s::AbstractString, ranges::Vector{String}) :: NonContiguousRange 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_cellrange(n) - push!(noncontig, CellRange(n)) + 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 error("Invalid non-contiguous range: $n.") end diff --git a/src/read.jl b/src/read.jl index c0157349..d17e19a1 100644 --- a/src/read.jl +++ b/src/read.jl @@ -377,29 +377,46 @@ function parse_workbook!(xf::XLSXFile) name = XML.attributes(defined_name_node)["name"] local defined_value::DefinedNameValueTypes - if is_valid_non_contiguous_range(defined_value_string) - defined_value = nonContiguousRange(defined_value_string) - elseif is_valid_fixed_sheet_cellname(defined_value_string) || is_valid_sheet_cellname(defined_value_string) + defined_value = nonContiguousRange(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 + @assert length(isabs)==length(defined_value.rng) "Error parsing absolute references in non-contiguous range." + elseif is_valid_fixed_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) + isabs=true + elseif is_valid_sheet_cellname(defined_value_string) + defined_value = SheetCellRef(defined_value_string) + isabs=false + elseif is_valid_fixed_sheet_cellrange(defined_value_string) + defined_value = SheetCellRange(defined_value_string) + isabs=true + elseif is_valid_sheet_cellrange(defined_value_string) defined_value = SheetCellRange(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. # Actually is just interpreted as a string anyway and added to the defined names (is this true?). - defined_value = string(defined_value_string) + defined_value = string(defined_value_string) + isabs=false #continue # debug - Now more important since we are writing updated defined names to back to output file. @@ -414,10 +431,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 diff --git a/src/types.jl b/src/types.jl index 1ef4a7d9..bc35312e 100644 --- a/src/types.jl +++ b/src/types.jl @@ -328,6 +328,12 @@ mutable struct SharedStringTable end 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` wi9ll need to be updated using the Workbook_names and @@ -341,8 +347,8 @@ 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 diff --git a/src/workbook.jl b/src/workbook.jl index 845248fc..aaff8aa5 100644 --- a/src/workbook.jl +++ b/src/workbook.jl @@ -201,12 +201,13 @@ end @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) -@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 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) || isa(v, NonContiguousRange) @@ -227,16 +228,16 @@ function is_valid_defined_name(name::AbstractString) :: Bool return true end -function addDefName(xf::XLSXFile, name::AbstractString, value::DefinedNameValueTypes) +function addDefName(xf::XLSXFile, name::AbstractString, value::DefinedNameValueTypes; absolute=true) if !is_valid_defined_name(name) error("Invalid defined name: $name.") end if is_workbook_defined_name(xf, name) error("Workbook already has a defined name called $name.") end - xf.workbook.workbook_names[name] = value + xf.workbook.workbook_names[name] = DefinedNameValue(value, absolute) end -function addDefName(ws::Worksheet, name::AbstractString, value::DefinedNameValueTypes) +function addDefName(ws::Worksheet, name::AbstractString, value::DefinedNameValueTypes; absolute=true) wb = get_workbook(ws) if !is_valid_defined_name(name) error("Invalid defined name: $name.") @@ -244,36 +245,37 @@ function addDefName(ws::Worksheet, name::AbstractString, value::DefinedNameValue if is_worksheet_defined_name(ws, name) error("Worksheet $(ws.name) already has a defined name called $name.") end +# local abs::Union{Bool, Vector{Bool}} if value isa NonContiguousRange @assert replace(value.sheet, "'" => "") == ws.name "Non-contiguous range must be in the same worksheet." + abs = absolute ? fill(true, length(value.rng)) : fill(false, length(value.rng)) + else + abs = absolute ? true : false end - wb.worksheet_names[(ws.sheetId, name)] = value + wb.worksheet_names[(ws.sheetId, name)] = DefinedNameValue(value, abs) end quoteit(x::AbstractString) = occursin(r"^[0-9]|[\s,:!&#@*]", x) ? "'$x'" : x unquoteit(x::AbstractString) = replace(x, "'" => "") """ - addDefinedName(xf::XLSXFile, name::AbstractString, value::Union{Int, Float64, Missing}) - addDefinedName(xf::XLSXFile, name::AbstractString, value::AbstractString) - addDefinedName(sh::Worksheet, name::AbstractString, value::Union{Int, Float64, Missing}) - addDefinedName(sh::Worksheet, name::AbstractString, value::AbstractString) + 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. -A defined name is a text string that represents a cell, range of cells, formula, or constant value. -It can be used to refer to a specific cell or range of cells in an Excel formula, making it easier -to read and understand complex formulas. +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). The `absolute` argument is ignored if the `definedName` is +not a cell reference or range. -A defined name should: -- Start with a letter an underscore (_) and cannot start with a number or special character. -- Not contain spaces -- Be no more than 255 characters in length -- Benique within a Workbook -- Must not include special characters (such as +, -, /, *, ,, or .) They can only contain letters, numbers, underscores (_), and backslashes (\\). -- Cannot look like a cell reference (e.g., "A1" or "Z100") -- May not use reserved words like "R" or "C" +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 @@ -290,34 +292,34 @@ julia> XLSX.addDefinedName(xf, "first_name", "Hello World") ``` """ function addDefinedName end -addDefinedName(xf::XLSXFile, name::AbstractString, value::Union{Int, Float64, Missing}) = addDefName(xf, name, value) -addDefinedName(ws::Worksheet, name::AbstractString, value::Union{Int, Float64, Missing}) = addDefName(ws, name, value) -function addDefinedName(xf::XLSXFile, name::AbstractString, value::AbstractString) +addDefinedName(xf::XLSXFile, name::AbstractString, value::Union{Int, Float64}; absolute=true) = addDefName(xf, name, value) +addDefinedName(ws::Worksheet, name::AbstractString, value::Union{Int, Float64}; absolute=true) = addDefName(ws, name, value) +function addDefinedName(xf::XLSXFile, name::AbstractString, value::AbstractString; absolute=true) if value == "" error("Defined name value cannot be an empty string.") end if is_valid_sheet_cellname(value) - return addDefName(xf, name, SheetCellRef(value)) + return addDefName(xf, name, SheetCellRef(value); absolute) elseif is_valid_sheet_cellrange(value) - return addDefName(xf, name, SheetCellRange(value)) + return addDefName(xf, name, SheetCellRange(value); absolute) elseif is_valid_non_contiguous_sheetcellrange(value) - return addDefName(xf, name, nonContiguousRange(value)) + return addDefName(xf, name, nonContiguousRange(value); absolute) else return addDefName(xf, name, value isa String ? "\"$value\"" : value) end end -function addDefinedName(ws::Worksheet, name::AbstractString, value::AbstractString) +function addDefinedName(ws::Worksheet, name::AbstractString, value::AbstractString; absolute=true) if value == "" error("Defined name value cannot be an empty string.") end if is_valid_cellname(value) - return addDefName(ws, name, SheetCellRef(quoteit(ws.name), CellRef(value))) + return addDefName(ws, name, SheetCellRef(ws.name, CellRef(value)); absolute) elseif is_valid_cellrange(value) - return addDefName(ws, name, SheetCellRange(quoteit(ws.name), CellRange(value))) + return addDefName(ws, name, SheetCellRange(ws.name, CellRange(value)); absolute) elseif is_valid_non_contiguous_sheetcellrange(value) - return addDefName(ws, name, nonContiguousRange(value)) + return addDefName(ws, name, nonContiguousRange(value); absolute) elseif is_valid_non_contiguous_cellrange(value) - return addDefName(ws, name, nonContiguousRange(ws, value)) + return addDefName(ws, name, nonContiguousRange(ws, value); absolute) else return addDefName(ws, name, value isa String ? "\"$value\"" : value) end diff --git a/src/write.jl b/src/write.jl index ba822988..06cd67d2 100644 --- a/src/write.jl +++ b/src/write.jl @@ -353,6 +353,30 @@ function update_worksheets_xml!(xl::XLSXFile) nothing end +function abscell(c::CellRef) + col, row = split_cellname(c.name) + return "\$$col\$$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 *= cr.sheet * "!" * mkabs(cr) * "," + else + v *= string(cr) * "," + end + end + return v[1:end-1] + else + return dn.isabs ? dn.value.sheet * "!" * mkabs(dn.value) : string(dn.value) + end +end + function update_workbook_xml!(xl::XLSXFile) wb = get_workbook(xl) @@ -378,10 +402,16 @@ function update_workbook_xml!(xl::XLSXFile) end for (k, v) in wb.workbook_names + if typeof(v.value) <: DefinedNameRangeTypes + v=make_absolute(v) + 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) + end dn_node = XML.Element("definedName", name=last(k), localSheetId=first(k)-1, XML.Text(v)) push!(definedNames, dn_node) end From c644f72bdcb3f6b9ace42ec71f455a27d21fd30d Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Thu, 20 Mar 2025 23:34:23 +0000 Subject: [PATCH 016/154] Handle apostrophes correctly in sheetname part of `definedNames` --- src/cellref.jl | 10 ++++---- src/read.jl | 63 ++++++++++++++++++++++++++++++++++++------------ src/table.jl | 10 ++++---- src/workbook.jl | 4 +-- src/worksheet.jl | 4 ++- src/write.jl | 4 +-- 6 files changed, 64 insertions(+), 31 deletions(-) diff --git a/src/cellref.jl b/src/cellref.jl index c68e6690..5df750f8 100644 --- a/src/cellref.jl +++ b/src/cellref.jl @@ -392,27 +392,27 @@ end # 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(cr.sheet, "!", cr.colrng) +Base.string(cr::SheetRowRange) = string(quoteit(cr.sheet), "!", cr.colrng) 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(cr.sheet, "!", x) for x in cr.rng],",") +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::SheetColumnRange) = cr1.sheet == cr2.sheet && cr2.rng == cr2.rng Base.hash(cr::NonContiguousRange) = hash(cr.sheet) + hash(cr.rng) diff --git a/src/read.jl b/src/read.jl index d17e19a1..9647e119 100644 --- a/src/read.jl +++ b/src/read.jl @@ -378,23 +378,23 @@ function parse_workbook!(xf::XLSXFile) local defined_value::DefinedNameValueTypes if is_valid_non_contiguous_range(defined_value_string) - defined_value = nonContiguousRange(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 @assert length(isabs)==length(defined_value.rng) "Error parsing absolute references in non-contiguous range." elseif is_valid_fixed_sheet_cellname(defined_value_string) - defined_value = SheetCellRef(defined_value_string) + defined_value = SheetCellRef(unquoteit(defined_value_string)) isabs=true elseif is_valid_sheet_cellname(defined_value_string) - defined_value = SheetCellRef(defined_value_string) + defined_value = SheetCellRef(unquoteit(defined_value_string)) isabs=false elseif is_valid_fixed_sheet_cellrange(defined_value_string) - defined_value = SheetCellRange(defined_value_string) + defined_value = SheetCellRange(unquoteit(defined_value_string)) isabs=true elseif is_valid_sheet_cellrange(defined_value_string) - defined_value = SheetCellRange(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 @@ -494,7 +494,7 @@ end readdata(source, sheet, ref) readdata(source, sheetref) -Returns a scalar or matrix with values from a spreadsheet. +Returns a scalar or matrix with values from a spreadsheet file. See also [`XLSX.getdata`](@ref). @@ -559,10 +559,14 @@ 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. +Alternatively, use `columns` to specify a row range, like `"2:4"`. +This will select rows `2`, `3` and `4`. + Use `first_row` to indicate the first row from the table. `first_row=5` will look for a table starting at sheet row `5`. If `first_row` is not given, the algorithm will look for the first -non-empty row in the spreadsheet. +non-empty row in the spreadsheet (if a column range is specified) +or range (if a row range is specified). `header` is a `Bool` indicating if the first row is a header. If `header=true` and `column_labels` is not specified, the column labels @@ -577,11 +581,13 @@ 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`. -`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`: @@ -592,9 +598,14 @@ function stop_function(r) 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 @@ -608,14 +619,34 @@ 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, normalizenames::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, normalizenames=normalizenames) + 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, normalizenames::Bool=false) +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=false, 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=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}, rows::RowRange; 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, normalizenames::Bool=false) + first_row = isnothing(first_row) ? rows.start : first_row + stop_in_row_function = isnothing(stop_in_row_function) ? r -> r.row == rows.stop : stop_in_row_function +# return readtable(source, sheet; first_row, column_labels, header, infer_eltypes, stop_in_empty_row, stop_in_row_function=stop_function, enable_cache, keep_empty_rows, normalizenames) +# end 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, normalizenames=normalizenames) + 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}, range::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, normalizenames::Bool=false) + if is_valid_row_range(range) + range = RowRange(range) + elseif is_valid_column_range(range) + range = ColumnRange(range) + else + error("The columns argument must be a valid column range or row 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 diff --git a/src/table.jl b/src/table.jl index 90d9d9c8..7716d65e 100644 --- a/src/table.jl +++ b/src/table.jl @@ -418,7 +418,7 @@ function Base.iterate(itr::TableRowIterator, state::TableRowIteratorState) @assert !is_empty_table_row(sheet_row) || itr.keep_empty_rows 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 @@ -603,11 +603,11 @@ julia> df = XLSX.openxlsx("myfile.xlsx") do xf 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, normalizenames::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, normalizenames=normalizenames) - return gettable(itr; infer_eltypes=infer_eltypes) + 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, normalizenames::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, normalizenames=normalizenames) - return gettable(itr; infer_eltypes=infer_eltypes) + 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/workbook.jl b/src/workbook.jl index aaff8aa5..b75b5230 100644 --- a/src/workbook.jl +++ b/src/workbook.jl @@ -247,7 +247,7 @@ function addDefName(ws::Worksheet, name::AbstractString, value::DefinedNameValue end # local abs::Union{Bool, Vector{Bool}} if value isa NonContiguousRange - @assert replace(value.sheet, "'" => "") == ws.name "Non-contiguous range must be in the same worksheet." + @assert value.sheet == ws.name "Non-contiguous range must be in the same worksheet." abs = absolute ? fill(true, length(value.rng)) : fill(false, length(value.rng)) else abs = absolute ? true : false @@ -255,7 +255,7 @@ function addDefName(ws::Worksheet, name::AbstractString, value::DefinedNameValue wb.worksheet_names[(ws.sheetId, name)] = DefinedNameValue(value, abs) end -quoteit(x::AbstractString) = occursin(r"^[0-9]|[\s,:!&#@*]", x) ? "'$x'" : x +quoteit(x::AbstractString) = occursin(r"[^\w]|\s", x) ? "'$x'" : x unquoteit(x::AbstractString) = replace(x, "'" => "") """ diff --git a/src/worksheet.jl b/src/worksheet.jl index fa579dce..dfce4d1d 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -68,7 +68,7 @@ end getdata(sheet, ref) getdata(sheet, row, column) -Returns a scalar or a matrix with values from a spreadsheet. +Returns a scalar, vector or a matrix with values from a spreadsheet. `ref` can be a cell reference or a range. Indexing in a `Worksheet` will dispatch to `getdata` method. @@ -86,6 +86,8 @@ julia> matrix = sheet["A:B"] julia> matrix = sheet["1:4"] +julia> vector = sheet["A1:A4,C1:C4,G5"] # non-contiguous range + julia> single_value = sheet[2, 2] # B2 ``` diff --git a/src/write.jl b/src/write.jl index 06cd67d2..fdc63979 100644 --- a/src/write.jl +++ b/src/write.jl @@ -366,14 +366,14 @@ function make_absolute(dn::DefinedNameValue) 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 *= cr.sheet * "!" * mkabs(cr) * "," + v *= quoteit(cr.sheet) * "!" * mkabs(cr) * "," else v *= string(cr) * "," end end return v[1:end-1] else - return dn.isabs ? dn.value.sheet * "!" * mkabs(dn.value) : string(dn.value) + return dn.isabs ? quoteit(dn.value.sheet) * "!" * mkabs(dn.value) : string(dn.value) end end From de4aaad668b2f996e55184199bbb3e8ae32c4a8e Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 21 Mar 2025 12:01:48 +0000 Subject: [PATCH 017/154] Minor row range fixes --- data/general.xlsx | Bin 20482 -> 20544 bytes src/read.jl | 15 ++++++++++++--- src/table.jl | 4 ++-- src/worksheet.jl | 22 ++++++++++++++-------- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/data/general.xlsx b/data/general.xlsx index a5c92d67a5b1a09512818b59ef56e3f68b03c972..9ec8612e058351c817dd3ea1cc2d22ef8715cf1f 100644 GIT binary patch delta 4235 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 r.row == rows.stop : stop_in_row_function -# return readtable(source, sheet; first_row, column_labels, header, infer_eltypes, stop_in_empty_row, stop_in_row_function=stop_function, enable_cache, keep_empty_rows, normalizenames) -# end + stop_in_row_function = isnothing(stop_in_row_function) ? r -> r.row >= rows.stop-first_row+1 : stop_in_row_function c = openxlsx(source, enable_cache=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 diff --git a/src/table.jl b/src/table.jl index 7716d65e..2d5b11ab 100644 --- a/src/table.jl +++ b/src/table.jl @@ -348,7 +348,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 @@ -393,7 +393,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 diff --git a/src/worksheet.jl b/src/worksheet.jl index dfce4d1d..0acf9140 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -169,26 +169,32 @@ function getdata(ws::Worksheet, rng::ColumnRange) :: Array{Any,2} return hcat(columns...) end function getdata(ws::Worksheet, rng::RowRange) :: Array{Any,2} + rows_count = length(rng) dim = get_dimension(ws) - rows = Vector{Vector{Any}}() + rows = Vector{Vector{Any}}(undef, rows_count) + for i in 1:rows_count + rows[i] = Vector{Any}() + end let top, bottom = row_bounds(rng) left = dim.start.column_number right = dim.stop.column_number - for (i, sheetrow) in enumerate(eachrow(ws)) - push!(rows, Vector{Any}()) - if top <= sheetrow.row && sheetrow.row <= bottom + for sheetrow in eachrow(ws) + if sheetrow.row > bottom + break + end + if top > sheetrow.row + continue + else + row_index=sheetrow.row-top+1 for column in left:right cell = getcell(sheetrow, column) - push!(rows[i], getdata(ws, cell)) + push!(rows[row_index], getdata(ws, cell)) end end - if sheetrow.row > bottom - break - end end end From 1c429f44544e566105b0a5e7afdbe8a9f4c55caf Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 21 Mar 2025 18:07:11 +0000 Subject: [PATCH 018/154] Begin adding some tests --- src/cellformats.jl | 2 +- src/cellref.jl | 15 ++++- src/read.jl | 2 +- src/workbook.jl | 27 ++++---- src/worksheet.jl | 12 +++- src/write.jl | 6 +- test/runtests.jl | 156 +++++++++++++++++++++++++++++++++++++++------ 7 files changed, 179 insertions(+), 41 deletions(-) diff --git a/src/cellformats.jl b/src/cellformats.jl index 8fdf8d5a..a5fb78e3 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -221,7 +221,7 @@ function process_sheetcell(f::Function, xl::XLSXFile, sheetcell::String; kw...): error("Unexpected defined name value: $v.") end elseif is_valid_non_contiguous_sheetcellrange(sheetcell) - sheetncrng = nonContiguousRange(sheetcell) + sheetncrng = NonContiguousRange(sheetcell) @assert hassheet(xl, sheetncrng.sheet) "Sheet $(ref.sheet) not found." newid = f(xl[sheetncrng.sheet], sheetncrng; kw...) elseif is_valid_sheet_column_range(sheetcell) diff --git a/src/cellref.jl b/src/cellref.jl index 5df750f8..ff0d1733 100644 --- a/src/cellref.jl +++ b/src/cellref.jl @@ -367,6 +367,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)) @@ -414,7 +423,7 @@ 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::SheetColumnRange) = cr1.sheet == cr2.sheet && cr2.rng == cr2.rng +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 # Assumes the same sheet name for both `CellRef` and `NonContiguousRange`. @@ -644,8 +653,8 @@ function is_valid_non_contiguous_cellrange(v::AbstractString) :: Bool return true end -nonContiguousRange(s::Worksheet, v::AbstractString)::NonContiguousRange = nCR(s.name, string.(split(v, ","))) -function nonContiguousRange(v::AbstractString)::NonContiguousRange +NonContiguousRange(s::Worksheet, v::AbstractString)::NonContiguousRange = nCR(s.name, string.(split(v, ","))) +function NonContiguousRange(v::AbstractString)::NonContiguousRange @assert is_valid_non_contiguous_range(v) "$v is not a valid non-contiguous range." diff --git a/src/read.jl b/src/read.jl index b73bcca5..923e7310 100644 --- a/src/read.jl +++ b/src/read.jl @@ -378,7 +378,7 @@ function parse_workbook!(xf::XLSXFile) local defined_value::DefinedNameValueTypes if is_valid_non_contiguous_range(defined_value_string) - defined_value = nonContiguousRange(unquoteit(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) diff --git a/src/workbook.jl b/src/workbook.jl index b75b5230..402a6053 100644 --- a/src/workbook.jl +++ b/src/workbook.jl @@ -132,10 +132,10 @@ end function getdata(xl::XLSXFile, s::AbstractString) if is_workbook_defined_name(xl, s) v = get_defined_name_value(xl.workbook, s) - if is_defined_name_value_a_constant(v) + if is_defined_name_value_a_constant(v) return v elseif is_defined_name_value_a_reference(v) - return getdata(xl, v) + return getdata(xl, v) else error("Unexpected defined name value: $v.") end @@ -148,7 +148,7 @@ function getdata(xl::XLSXFile, s::AbstractString) elseif is_valid_sheet_row_range(s) return getdata(xl, SheetRowRange(s)) elseif is_valid_non_contiguous_range(s) - return getdata(xl, nonContiguousRange(s)) + return getdata(xl, NonContiguousRange(s)) end error("$s is not a valid definedName or cell/range reference.") @@ -189,7 +189,7 @@ function getcellrange(xl::XLSXFile, rng_str::AbstractString) 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)) + return getcellrange(xl, NonContiguousRange(rng_str)) end error("$rng_str is not a valid range reference.") @@ -235,7 +235,12 @@ function addDefName(xf::XLSXFile, name::AbstractString, value::DefinedNameValueT if is_workbook_defined_name(xf, name) error("Workbook already has a defined name called $name.") end - xf.workbook.workbook_names[name] = DefinedNameValue(value, absolute) + 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) @@ -245,7 +250,7 @@ function addDefName(ws::Worksheet, name::AbstractString, value::DefinedNameValue if is_worksheet_defined_name(ws, name) error("Worksheet $(ws.name) already has a defined name called $name.") end -# local abs::Union{Bool, Vector{Bool}} + if value isa NonContiguousRange @assert value.sheet == ws.name "Non-contiguous range must be in the same worksheet." abs = absolute ? fill(true, length(value.rng)) : fill(false, length(value.rng)) @@ -303,9 +308,9 @@ function addDefinedName(xf::XLSXFile, name::AbstractString, value::AbstractStrin 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) + return addDefName(xf, name, NonContiguousRange(value); absolute) else - return addDefName(xf, name, value isa String ? "\"$value\"" : value) + return addDefName(xf, name, value) end end function addDefinedName(ws::Worksheet, name::AbstractString, value::AbstractString; absolute=true) @@ -317,10 +322,10 @@ function addDefinedName(ws::Worksheet, name::AbstractString, value::AbstractStri elseif is_valid_cellrange(value) return addDefName(ws, name, SheetCellRange(ws.name, CellRange(value)); absolute) elseif is_valid_non_contiguous_sheetcellrange(value) - return addDefName(ws, name, nonContiguousRange(value); absolute) + return addDefName(ws, name, NonContiguousRange(value); absolute) elseif is_valid_non_contiguous_cellrange(value) - return addDefName(ws, name, nonContiguousRange(ws, value); absolute) + return addDefName(ws, name, NonContiguousRange(ws, value); absolute) else - return addDefName(ws, name, value isa String ? "\"$value\"" : value) + return addDefName(ws, name, value) end end diff --git a/src/worksheet.jl b/src/worksheet.jl index 0acf9140..f9222609 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -206,6 +206,12 @@ function getdata(ws::Worksheet, rng::RowRange) :: Array{Any,2} return permutedims(hcat(rows...)) end +# Needed for definedName references +getdata(ws::Worksheet, s::SheetCellRef) = getdata(ws, s.cellref) +getdata(ws::Worksheet, s::SheetCellRange) = getdata(ws, s.rng) +getdata(ws::Worksheet, s::SheetColumnRange) = getdata(ws, s.colrng) +getdata(ws::Worksheet, s::SheetRowRange) = getdata(ws, s.rowrng) + function getdata(ws::Worksheet, rng::NonContiguousRange) :: Vector{Any} results=Vector{Any}() for r in rng.rng @@ -249,13 +255,13 @@ function getdata(ws::Worksheet, ref::AbstractString) :: Union{Array{Any,2}, Any} elseif is_valid_row_range(ref) return getdata(ws, RowRange(ref)) elseif is_valid_non_contiguous_range(ref) - return getdata(ws, nonContiguousRange(ws, ref)) + return getdata(ws, NonContiguousRange(ws, ref)) else error("$ref is not a valid cell or range reference.") end end -getdata(ws::Worksheet, rng::SheetCellRange) = getdata(get_xlsxfile(ws), rng) +#getdata(ws::Worksheet, rng::SheetCellRange) = getdata(get_xlsxfile(ws), rng) function getdata(ws::Worksheet) if ws.dimension !== nothing @@ -450,7 +456,7 @@ function getcellrange(ws::Worksheet, rng::AbstractString) elseif is_valid_row_range(rng) return getcellrange(ws, RowRange(rng)) elseif is_valid_non_contiguous_range(rng) - return getcellrange(ws, nonContiguousRange(ws, rng)) + return getcellrange(ws, NonContiguousRange(ws, rng)) else error("$rng is not a valid cell range.") end diff --git a/src/write.jl b/src/write.jl index fdc63979..85c02762 100644 --- a/src/write.jl +++ b/src/write.jl @@ -31,7 +31,7 @@ julia> xf = newxlsx() ``` """ -newxlsx() = open_empty_template() +newxlsx(sheetname::AbstractString=""; path::AbstractString=_relocatable_data_path()) :: XLSXFile = open_empty_template(sheetname; path) function open_empty_template( sheetname::AbstractString=""; @@ -404,6 +404,8 @@ function update_workbook_xml!(xl::XLSXFile) 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) @@ -411,6 +413,8 @@ function update_workbook_xml!(xl::XLSXFile) 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) diff --git a/test/runtests.jl b/test/runtests.jl index 11b81d40..902e010c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -416,25 +416,56 @@ 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 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" + + 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.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"] @@ -543,6 +574,49 @@ end @test hash(XLSX.ColumnRange("B:D")) == hash(XLSX.ColumnRange("B:D")) end +@testset "Row Range" begin + cr = XLSX.RowRange("2:5") + @test string(cr) == "2:5" + @test cr.start == 2 + @test cr.stop == 5 + @test length(cr) == 4 + @test_throws AssertionError XLSX.RowRange("B1:D3") + @test_throws AssertionError XLSX.RowRange("5:2") + @test collect(cr) == ["2", "3", "4", "5"] + @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 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 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 AssertionError XLSX.NonContiguousRange("Sheet1!D1:D3,B1:B3") + @test_throws AssertionError XLSX.NonContiguousRange("Sheet1!D1:D3,Sheet2!B1:B3") + @test_throws AssertionError XLSX.NonContiguousRange("B1:D3") + @test_throws AssertionError 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")] @@ -970,6 +1044,21 @@ end test_data = Any[Any["C3", missing], Any[missing, "D4"]] check_test_data(data, test_data) end + + @testset "normalizenames" begin + + 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 @@ -1236,14 +1325,39 @@ end @test dt_read.column_label_index == dt.column_label_index end + @testset "extended types" begin + @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 From 290c9465b96dd99c3a2eea5115cef8fa1ac2b76b Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 22 Mar 2025 08:43:06 +0000 Subject: [PATCH 019/154] Add new functions to API docs --- docs/src/api.md | 4 ++++ src/worksheet.jl | 17 ++++++++--------- test/runtests.jl | 22 +++++++++++++++------- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index 2a2b32bb..34cb938f 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -41,4 +41,8 @@ XLSX.setUniformAlignment XLSX.setUniformStyle XLSX.setColumnWidth XLSX.setRowHeight +XLSX.getMergedCells +XLSX.isMergedCell +XLSX.getMergedBaseCell +XLSX.addDefinedName ``` diff --git a/src/worksheet.jl b/src/worksheet.jl index f9222609..ddf3ecc2 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -206,12 +206,6 @@ function getdata(ws::Worksheet, rng::RowRange) :: Array{Any,2} return permutedims(hcat(rows...)) end -# Needed for definedName references -getdata(ws::Worksheet, s::SheetCellRef) = getdata(ws, s.cellref) -getdata(ws::Worksheet, s::SheetCellRange) = getdata(ws, s.rng) -getdata(ws::Worksheet, s::SheetColumnRange) = getdata(ws, s.colrng) -getdata(ws::Worksheet, s::SheetRowRange) = getdata(ws, s.rowrng) - function getdata(ws::Worksheet, rng::NonContiguousRange) :: Vector{Any} results=Vector{Any}() for r in rng.rng @@ -226,6 +220,12 @@ function getdata(ws::Worksheet, rng::NonContiguousRange) :: Vector{Any} return results end +# Needed for definedName references +getdata(ws::Worksheet, s::SheetCellRef) = getdata(ws, s.cellref) +getdata(ws::Worksheet, s::SheetCellRange) = getdata(ws, s.rng) +getdata(ws::Worksheet, s::SheetColumnRange) = getdata(ws, s.colrng) +getdata(ws::Worksheet, s::SheetRowRange) = getdata(ws, s.rowrng) + function getdata(ws::Worksheet, ref::AbstractString) :: Union{Array{Any,2}, Any} if is_worksheet_defined_name(ws, ref) v = get_defined_name_value(ws, ref) @@ -261,8 +261,6 @@ function getdata(ws::Worksheet, ref::AbstractString) :: Union{Array{Any,2}, Any} 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)) @@ -340,9 +338,10 @@ getcell(ws::Worksheet, row::Integer, col::Integer) = getcell(ws, CellRef(row, co 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"`. +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. """ function getcellrange(ws::Worksheet, rng::CellRange) :: Array{AbstractCell,2} result = Array{AbstractCell, 2}(undef, size(rng)) diff --git a/test/runtests.jl b/test/runtests.jl index 902e010c..009b01e1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -408,7 +408,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) @@ -574,15 +574,23 @@ end @test hash(XLSX.ColumnRange("B:D")) == hash(XLSX.ColumnRange("B:D")) end -@testset "Row Range" begin +@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 AssertionError XLSX.RowRange("B1:D3") @test_throws AssertionError XLSX.RowRange("5:2") - @test collect(cr) == ["2", "3", "4", "5"] @test XLSX.RowRange("2:5") == XLSX.RowRange("2:5") @test hash(XLSX.RowRange("2:5")) == hash(XLSX.RowRange("2:5")) end @@ -1045,7 +1053,7 @@ end check_test_data(data, test_data) end - @testset "normalizenames" begin + @testset "normalizenames" begin # Issue #260 data = Vector{Any}() push!(data, [:sym1, :sym2, :sym3]) @@ -1325,7 +1333,7 @@ end @test dt_read.column_label_index == dt.column_label_index end - @testset "extended types" begin + @testset "extended types" begin # Issue #239 @enum enums begin enum1 enum2 @@ -1730,9 +1738,9 @@ end @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")) + @test XLSX.getBorder(s, "J20").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")) - # Cant get attributes on a range. + # Can't get attributes on a range. @test_throws AssertionError XLSX.getBorder(s, "Contiguous") f = XLSX.open_empty_template() From 62605f3d5afe2bf342022107834a7f219e77b0d3 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 22 Mar 2025 12:44:16 +0000 Subject: [PATCH 020/154] Add more tests --- data/customXml.xlsx | Bin 26620 -> 26400 bytes src/cellformats.jl | 57 +++++++++++++++++++++----------- src/cellref.jl | 8 +++-- src/read.jl | 2 ++ test/runtests.jl | 78 +++++++++++++++++++++++++++++++------------- 5 files changed, 100 insertions(+), 45 deletions(-) diff --git a/data/customXml.xlsx b/data/customXml.xlsx index 2dc7f9fa4a2262449c42c26983af6a8624ab0658..55c580acd172374a82b224a1f32a1c4bd041a67c 100644 GIT binary patch delta 11563 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 Op99#}jOdTQ-~Jzi{h^Zp delta 11728 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 "")) + new_att = f(get_xlsxfile(wb), replace(string(v), "'" => ""); kw...) else error("Unexpected defined name value: $v.") end elseif is_valid_cellname(ref_or_rng) - new_att = f(ws, CellRef(ref_or_rng)) + new_att = f(ws, CellRef(ref_or_rng); kw...) else error("Invalid cell reference or range: $ref_or_rng") end @@ -2335,9 +2335,11 @@ end Return `true` if a cell is part of a merged cell range and `false` if not. Alternatively, if you have already obtained the merged cells for the worksheet, -you can avoid repeated determinations and pass them as an argument to the function: +you can avoid repeated determinations and pass them as a keyword argument to +the function: - isMergedCell(ws::Worksheet, cr::CellRef, mergedCells::Union{Vector{CellRange}, Nothing}) -> Bool + 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 # Examples: ```julia @@ -2351,14 +2353,17 @@ julia> XLSX.isMergedCell(sh, XLSX.CellRef("A1"), mc) ``` """ function isMergedCell end -isMergedCell(xl::XLSXFile, sheetcell::String)::Bool = process_get_sheetcell(isMergedCell, xl, sheetcell) -isMergedCell(ws::Worksheet, cr::String)::Bool = process_get_cellname(isMergedCell, ws, cr) -isMergedCell(ws::Worksheet, cellref::CellRef)::Bool = isMergedCell(ws, cellref, getMergedCells(ws)) -function isMergedCell(ws::Worksheet, cellref::CellRef, mergedCells::Union{Vector{CellRange}, Nothing})::Bool - +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, cellref::CellRef)::Bool = isMergedCell(ws, cellref, getMergedCells(ws)) +function isMergedCell(ws::Worksheet, cellref::CellRef; mergedCells::Union{Vector{CellRange}, Nothing, Missing} = missing)::Bool + @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot get merged cells because cache is not enabled." - if isnothing(mergedCells) + 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 @@ -2383,6 +2388,13 @@ 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}} + # Examples: ```julia julia> XLSX.getMergedBaseCell(xf, "Sheet1!B2") @@ -2395,13 +2407,20 @@ julia> XLSX.getMergedBaseCell(sh, "B2") ``` """ function getMergedBaseCell end -getMergedBaseCell(xl::XLSXFile, sheetcell::String) = process_get_sheetcell(getMergedBaseCell, xl, sheetcell) -getMergedBaseCell(ws::Worksheet, cr::String) = process_get_cellname(getMergedBaseCell, ws, cr) -getMergedBaseCell(ws::Worksheet, cellref::CellRef) = getMergedBaseCell(ws, cellref, getMergedCells(ws)) -function getMergedBaseCell(ws::Worksheet, cellref::CellRef, mergedCells::Union{Vector{CellRange}, Nothing}) +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, cellref::CellRef) = getMergedBaseCell(ws, cellref, getMergedCells(ws)) +function getMergedBaseCell(ws::Worksheet, cellref::CellRef; mergedCells::Union{Vector{CellRange}, Nothing, Missing}=missing) @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot get merged cells because cache is not enabled." + 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]) diff --git a/src/cellref.jl b/src/cellref.jl index ff0d1733..2bb0f29b 100644 --- a/src/cellref.jl +++ b/src/cellref.jl @@ -58,6 +58,7 @@ 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]*$" @@ -189,6 +190,7 @@ 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) @@ -367,7 +369,7 @@ 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. +# 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 @@ -426,7 +428,7 @@ 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 # Assumes the same sheet name for both `CellRef` and `NonContiguousRange`. +function Base.in(ref::SheetCellRef, ncrng::NonContiguousRange) :: Bool if ref.sheet != ncrng.sheet return false end @@ -464,7 +466,7 @@ function nc_bounds(r::NonContiguousRange)::CellRange # Smallest rectangualar `Ce end return CellRange(CellRef(top, left), CellRef(bottom, right)) end -function Base.length(r::NonContiguousRange)::Int # Number of cells in `rng`. +function Base.length(r::NonContiguousRange)::Int # Number of cells in `r`. s = 0 for rng in r.rng if rng isa CellRef diff --git a/src/read.jl b/src/read.jl index 923e7310..83d8c5c6 100644 --- a/src/read.jl +++ b/src/read.jl @@ -638,6 +638,7 @@ end # It probably needs a dedicated RowRange implementation. # It is best not to use this function with row ranges. Use `readdata` or `getdata` instead, both of which work # on row ranges, or index the sheet directly to get the rows you want (e.g. sh["3"] or sh["3:5"]). +#= function readtable(source::Union{AbstractString, IO}, sheet::Union{AbstractString, Int}, rows::RowRange; 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, normalizenames::Bool=false) if rows.start == rows.stop && header==true error("Only 1 row specified in `RowRange` with `header=true`.\nThe header row is the same as the data row. Specify at least two rows to read header data with `header=true`.") @@ -649,6 +650,7 @@ function readtable(source::Union{AbstractString, IO}, sheet::Union{AbstractStrin 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=false, 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_row_range(range) range = RowRange(range) diff --git a/test/runtests.jl b/test/runtests.jl index 009b01e1..c693efb9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -581,14 +581,14 @@ end @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 AssertionError XLSX.RowRange("B1:D3") @test_throws AssertionError XLSX.RowRange("5:2") @test XLSX.RowRange("2:5") == XLSX.RowRange("2:5") @@ -596,28 +596,28 @@ end end @testset "Non-contiguous Range" begin - cr = XLSX.NonContiguousRange("Sheet1!D1:D3,Sheet1!B1:B3") + 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 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")) + @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"] + f = XLSX.newxlsx("Sheet 1") + s = f["Sheet 1"] for cell in XLSX.CellRange("A1:D6") - s[cell]="" + s[cell] = "" end - cr = XLSX.NonContiguousRange(s, "D1:D3,A2,B1:B3") + 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 cr.rng == [XLSX.CellRange("D1:D3"), XLSX.CellRef("A2"), XLSX.CellRange("B1:B3")] @test length(cr) == 7 @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 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 AssertionError XLSX.NonContiguousRange("Sheet1!D1:D3,B1:B3") @test_throws AssertionError XLSX.NonContiguousRange("Sheet1!D1:D3,Sheet2!B1:B3") @@ -1061,9 +1061,9 @@ end 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)) + df = DataFrames.DataFrame(XLSX.readtable("mytest.xlsx", "Sheet1", normalizenames=true)) @test DataFrames.names(df) == Any["_1_col", "col_2", "_local", "col_4"] end @@ -1339,7 +1339,7 @@ end enum2 enum3 end - + data = Vector{Any}() push!(data, [:sym1, :sym2, :sym3]) push!(data, [1.0, 2.0, 3.0]) @@ -1348,10 +1348,10 @@ end 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") + 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" @@ -1764,6 +1764,7 @@ end # 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, "Mock-up!D4"; left=["style" => "dotted", "color" => "FF000FF0"], right=["style" => "medium", "color" => "FF765000"], @@ -1771,7 +1772,6 @@ end bottom=["style" => "medium", "color" => "FF0000FF"], diagonal=["style" => "none"] ) - @test_throws MethodError XLSX.setUniformFill(s, "ID"; pattern="darkTrellis", fgColor="FF222222", bgColor="FFDDDDDD") end @@ -1790,13 +1790,13 @@ end @test XLSX.getFill(s, "ID").fill == Dict("patternFill" => Dict("bgrgb" => "FFDDDDDD", "patternType" => "darkTrellis", "fgrgb" => "FF222222")) # 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")) @@ -1995,8 +1995,8 @@ 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 @@ -2007,7 +2007,7 @@ 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 AssertionError XLSX.getColumnWidth(f, "Mock-up!B2") end XLSX.openxlsx(joinpath(data_directory, "customXml.xlsx"); mode="r", enable_cache=false) do f @test_throws AssertionError XLSX.getRowHeight(f, "Mock-up!B2") @@ -2034,7 +2034,39 @@ end end end +@testset "merged cells" begin + XLSX.openxlsx(joinpath(data_directory, "customXml.xlsx")) 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 AssertionError XLSX.isMergedCell(s, "Contiguous"; mergedCells=mc) # Can't test a range + @test_throws AssertionError XLSX.getMergedBaseCell(s, "Location") + + @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"])) + + @test XLSX.getMergedBaseCell(f[1], "F72") == (baseCell = CellRef("D72"), baseValue = Dates.Date("2025-03-24")) + @test XLSX.getMergedBaseCell(f, "Mock-up!G72") == (baseCell = CellRef("D72"), baseValue = Dates.Date("2025-03-24")) + @test XLSX.getMergedBaseCell(s, "H53") == (baseCell = CellRef("D51"), baseValue = "Hello World") + @test XLSX.getMergedBaseCell(s, "G52") == (baseCell = CellRef("D51"), baseValue = "Hello World") + @test XLSX.getMergedBaseCell(s, "Short_Description") == (baseCell = CellRef("D51"), baseValue = "Hello World") + @test isnothing(XLSX.getMergedBaseCell(s, "F73")) + @test isnothing(XLSX.getMergedBaseCell(f, "Mock-up!H73")) + @test_throws AssertionError XLSX.getMergedBaseCell(s, "Location") # Can't get base cell for a range + end +end @testset "filemodes" begin sheetname = "New Sheet" From 3977bef560f3f87bbd553c869191fbedc94556b5 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 22 Mar 2025 12:58:16 +0000 Subject: [PATCH 021/154] Minor changes to docstrings... --- src/read.jl | 14 +++++++++++++- src/worksheet.jl | 18 +++++++++++------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/read.jl b/src/read.jl index 83d8c5c6..1c075995 100644 --- a/src/read.jl +++ b/src/read.jl @@ -494,7 +494,10 @@ end readdata(source, sheet, ref) readdata(source, sheetref) -Returns a scalar or matrix with values from a spreadsheet file. +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). @@ -520,6 +523,15 @@ julia> XLSX.readdata("myfile.xlsx", "mysheet!A2:B4") 1 "first" 2 "second" 3 "third" + +Non-contiguous ranges return vectors. + +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) diff --git a/src/worksheet.jl b/src/worksheet.jl index ddf3ecc2..2c4214cf 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -69,7 +69,7 @@ end getdata(sheet, row, column) Returns a scalar, vector or a matrix with values from a spreadsheet. -`ref` can be a cell reference or a range. +`ref` can be a cell reference or a range or a valid defined name. Indexing in a `Worksheet` will dispatch to `getdata` method. @@ -78,17 +78,21 @@ 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"] +julia> matrix = sheet["A1:B4"] # CellRange -julia> matrix = sheet["A:B"] +julia> matrix = sheet["A:B"] # Column range -julia> matrix = sheet["1:4"] +julia> matrix = sheet["1:4"] # Row range -julia> vector = sheet["A1:A4,C1:C4,G5"] # non-contiguous range +julia> matrix = sheet["Contiguous"] # Named range -julia> single_value = sheet[2, 2] # B2 +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). From 0328924c9abeb1c79740085b028af3149c76cb78 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 22 Mar 2025 13:23:29 +0000 Subject: [PATCH 022/154] Missing line in CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 487c6f25..affcc9d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,7 @@ jobs: with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} + show-versioninfo: true - uses: actions/cache@v2 - uses: julia-actions/julia-buildpkg@v1 continue-on-error: ${{ matrix.version == 'nightly' }} From e3f0c510da5675c11fc9004a31ef925d59f2700b Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 22 Mar 2025 21:01:46 +0000 Subject: [PATCH 023/154] Try again. :-( --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index affcc9d1..72561c2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: version: ${{ matrix.version }} arch: ${{ matrix.arch }} show-versioninfo: true - - uses: actions/cache@v2 + - uses: julia-actions/cache@v2 - uses: julia-actions/julia-buildpkg@v1 continue-on-error: ${{ matrix.version == 'nightly' }} - uses: julia-actions/julia-runtest@v1 From 19ae933afae765e7001b32899f2b4318bc21a869 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 22 Mar 2025 21:32:01 +0000 Subject: [PATCH 024/154] Bump to julia 1.8 and above --- .github/workflows/ci.yml | 1 - Project.toml | 2 +- src/cellformats.jl | 1 - test/runtests.jl | 11 ++++++----- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72561c2e..2e2f243f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,6 @@ jobs: fail-fast: false matrix: version: - - '1.7' - '1.8' - '1.9' - '1' # automatically expands to the latest stable 1.x release of Julia diff --git a/Project.toml b/Project.toml index 5f2ac321..58859c93 100644 --- a/Project.toml +++ b/Project.toml @@ -18,7 +18,7 @@ ZipArchives = "49080126-0e18-4c2a-b176-c102e4b3760c" Tables = "1" XML = "0.3.4" ZipArchives = "2" -julia = "1.7" +julia = "1.8" [extras] diff --git a/src/cellformats.jl b/src/cellformats.jl index 38ea136c..fea97f04 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -2420,7 +2420,6 @@ function getMergedBaseCell(ws::Worksheet, cellref::CellRef; mergedCells::Union{V 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]) diff --git a/test/runtests.jl b/test/runtests.jl index c693efb9..06981ecb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2051,11 +2051,6 @@ end @test_throws AssertionError XLSX.isMergedCell(s, "Contiguous"; mergedCells=mc) # Can't test a range @test_throws AssertionError XLSX.getMergedBaseCell(s, "Location") - @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"])) - @test XLSX.getMergedBaseCell(f[1], "F72") == (baseCell = CellRef("D72"), baseValue = Dates.Date("2025-03-24")) @test XLSX.getMergedBaseCell(f, "Mock-up!G72") == (baseCell = CellRef("D72"), baseValue = Dates.Date("2025-03-24")) @test XLSX.getMergedBaseCell(s, "H53") == (baseCell = CellRef("D51"), baseValue = "Hello World") @@ -2065,6 +2060,12 @@ end @test isnothing(XLSX.getMergedBaseCell(f, "Mock-up!H73")) @test_throws AssertionError 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 end @testset "filemodes" begin From 60354dca513f17fa3183903e55478d8d11e15ed0 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 23 Mar 2025 11:52:33 +0000 Subject: [PATCH 025/154] New branch for bug fixing post PR #289 --- .github/workflows/ci.yml | 3 +++ src/write.jl | 8 +++++--- test/runtests.jl | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e2f243f..f35a96d8 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 @@ -17,6 +18,8 @@ jobs: version: - '1.8' - '1.9' + - '1.10' + - '1.11' - '1' # automatically expands to the latest stable 1.x release of Julia - 'pre' - 'nightly' diff --git a/src/write.jl b/src/write.jl index 85c02762..60008c21 100644 --- a/src/write.jl +++ b/src/write.jl @@ -377,14 +377,16 @@ function make_absolute(dn::DefinedNameValue) end end -function update_workbook_xml!(xl::XLSXFile) +function update_workbook_xml!(xl::XLSXFile) # Only the block will need updating. wb = get_workbook(xl) + if length(wb.workbook_names)==0 && length(wb.workbook_names)==0 # No-op if no defined names present + return nothing + end + wbdoc = xmlroot(xl, "xl/workbook.xml") # find the block in the workbook's xml file i, j = get_idces(wbdoc, "workbook", "definedNames") - definedNames = isnothing(j) ? XML.Element("definedNames") : unlink_definedNames(wbdoc[i][j]) # Remove old defined names - 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. diff --git a/test/runtests.jl b/test/runtests.jl index 06981ecb..11d00ce0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -469,6 +469,7 @@ end @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"] + end @testset "Book1.xlsx" begin From 931c83b275c0f3e9db5bf362a73e1191f7a72701 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 23 Mar 2025 15:36:30 +0000 Subject: [PATCH 026/154] Add 'lts' to CI per @Eben60 review --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f35a96d8..0c2712c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,7 @@ jobs: - '1.9' - '1.10' - '1.11' + - 'lts' - '1' # automatically expands to the latest stable 1.x release of Julia - 'pre' - 'nightly' From 2419e55933d0bc07e4080853531bb592ccb4077a Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 23 Mar 2025 22:53:41 +0000 Subject: [PATCH 027/154] Fix to bug highlighted when `Node.depth` bug in XML was itself fixed (as yet untested). --- src/worksheet.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/worksheet.jl b/src/worksheet.jl index 2c4214cf..0c2696f4 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -31,6 +31,7 @@ function read_worksheet_dimension(xf::XLSXFile, relationship_id, name) :: Union{ 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) + println("worksheet34 : ", name) reader = iterate(doc) # Now let's look for a row element, if it exists From f320d9642122573ea2cc2562e7b0c4103b5a22b1 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 23 Mar 2025 22:56:32 +0000 Subject: [PATCH 028/154] Previous fix but now with actual change!! --- src/worksheet.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/worksheet.jl b/src/worksheet.jl index 0c2696f4..5b80cd8f 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -31,14 +31,13 @@ function read_worksheet_dimension(xf::XLSXFile, relationship_id, name) :: Union{ 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) - println("worksheet34 : ", name) 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) == 1 "Malformed Worksheet \"$(ws.name)\": unexpected node depth for dimension node: $(XML.depth(sheet_row))." + @assert XML.depth(sheet_row) == 1 "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)") From 31d9ccf3b8d655e49d27746683f684f52d11e883 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 23 Mar 2025 23:10:41 +0000 Subject: [PATCH 029/154] Update CI to include `aarch64` for MacOS-latest --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c2712c7..00dd72b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,9 @@ jobs: - macOS-latest arch: - x64 + include: + - os: macOS-latest + arch: aarch64 steps: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v2 From c5a54b5ed0c1b89ff5146a3cd960490c5b856a65 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 23 Mar 2025 23:15:25 +0000 Subject: [PATCH 030/154] Include versions in CL for MacOS aarch64 --- .github/workflows/ci.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00dd72b9..b44e36ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,16 @@ jobs: include: - os: macOS-latest arch: aarch64 + version: + - '1.8' + - '1.9' + - '1.10' + - '1.11' + - 'lts' + - '1' # automatically expands to the latest stable 1.x release of Julia + - 'pre' + - 'nightly' + steps: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v2 From 46aac0188265fcca0448769c1645fe2d2831f7bd Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 23 Mar 2025 23:24:09 +0000 Subject: [PATCH 031/154] Now with all julia versions specified for aarch64, too. (is this right???) --- .github/workflows/ci.yml | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b44e36ff..0b52f431 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,16 +33,29 @@ jobs: include: - os: macOS-latest arch: aarch64 - version: - - '1.8' - - '1.9' - - '1.10' - - '1.11' - - 'lts' - - '1' # automatically expands to the latest stable 1.x release of Julia - - 'pre' - - 'nightly' - + version: '1.8' + - os: macOS-latest + arch: aarch64 + version: '1.9' + - os: macOS-latest + arch: aarch64 + version: '1.10' + - os: macOS-latest + arch: aarch64 + version: '1.11' + - os: macOS-latest + arch: aarch64 + version: 'lts' + - os: macOS-latest + arch: aarch64 + version: '1' + - os: macOS-latest + arch: aarch64 + version: 'pre' + - os: macOS-latest + arch: aarch64 + version: 'nightly' + steps: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v2 From da56f09c557752e8bbc3f345e491e07625339644 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 24 Mar 2025 17:18:01 +0000 Subject: [PATCH 032/154] Add test for `setOutsideBorder` --- src/cellformats.jl | 4 ++-- test/runtests.jl | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/cellformats.jl b/src/cellformats.jl index fea97f04..ded85f6b 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -1123,8 +1123,8 @@ function setOutsideBorder(ws::Worksheet, rng::CellRange; 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) + 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]) diff --git a/test/runtests.jl b/test/runtests.jl index 11d00ce0..8ba10a9a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1712,6 +1712,31 @@ end @test XLSX.getcell(s, "D11") isa XLSX.EmptyCell @test isnothing(XLSX.getBorder(s, "D11")) # Cannot set a border in an EmptyCell (outside sheet dimension). + f = XLSX.newxlsx() + s=f[1] + for i = 1:6 + for j = 1:6 + s[i, j]="" + end + end + XLSX.setOutsideBorder(s, "B2:E5"; 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) + f = XLSX.open_xlsx_template(joinpath(data_directory, "Borders.xlsx")) s = f["Sheet1"] @@ -1756,9 +1781,13 @@ end 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.setFont(s, "A1:F20"; 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.setAlignment(s, "A1:F20"; horizontal="right", wrapText=true) == -1 + @test_throws AssertionError XLSX.setFill(f, "Sheet1!A1"; pattern="none", fgColor="88FF8800") + @test_throws AssertionError XLSX.setFont(s, "A1"; size=18, name="Arial") + @test_throws AssertionError XLSX.setBorder(f, "Sheet1!B2"; left=["style" => "hair"], right=["color" => "FF111111"], top=["style" => "hair"], bottom=["color" => "FF111111"], diagonal=["style" => "hair"]) + @test_throws AssertionError XLSX.setFill(s, "F20"; pattern="none", fgColor="88FF8800") f = XLSX.open_xlsx_template(joinpath(data_directory, "customXml.xlsx")) s = f["Mock-up"] From 97b0b0b42a81aa69b1c39286c5f9347850b39886 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 24 Mar 2025 18:13:50 +0000 Subject: [PATCH 033/154] Fix the two node depth tests to `XML.depth==2` (XML uses 1-based counting whereas EzXML used 0-based) --- src/stream.jl | 2 +- src/worksheet.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stream.jl b/src/stream.jl index 525d1cd5..9b5bd08a 100644 --- a/src/stream.jl +++ b/src/stream.jl @@ -93,7 +93,7 @@ function Base.iterate(itr::SheetRowStreamIterator, state::Union{Nothing, SheetRo if nrows == 0 return nothing end - @assert XML.depth(lznode) == 1 "Malformed Worksheet \"$(ws.name)\": unexpected node depth for sheetData node: $(XML.depth(lznode))." + @assert XML.depth(lznode) == 2 "Malformed Worksheet \"$(ws.name)\": unexpected node depth for sheetData node: $(XML.depth(lznode))." break end diff --git a/src/worksheet.jl b/src/worksheet.jl index 5b80cd8f..ec9c4f03 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -37,7 +37,7 @@ function read_worksheet_dimension(xf::XLSXFile, relationship_id, name) :: Union{ 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) == 1 "Malformed Worksheet \"$name\": unexpected node depth for dimension node: $(XML.depth(sheet_row))." + @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)") From 094f5c5014ad721ba22ff38be48bfd16f045f3f2 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 24 Mar 2025 19:31:47 +0000 Subject: [PATCH 034/154] Begin to introduce support for a few named colors --- data/Borders.xlsx | Bin 9490 -> 9869 bytes src/cellformats.jl | 44 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/data/Borders.xlsx b/data/Borders.xlsx index 345a63e5ef7683d6f5078d8839988f5d47a54b59..ac409a1ddf4debcce113c211918ab5101477bd97 100644 GIT binary patch delta 3978 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 "FF000000", # color name => RGB + "white" => "FFFFFFFF", + "red" => "FFFF0000", + "lime" => "FF00FF00", + "blue" => "FF0000FF", + "yellow" => "FFFFFF00", + "cyan" => "FF00FFFF", + "mMagenta" => "FFFF00FF", + "silver" => "FFC0C0C0", + "gray" => "FF808080", + "maroon" => "FF800000", + "olive" => "FF808000", + "green" => "FF008000", + "purple" => "FF800080", + "teal" => "FF008080", + "navy" => "FF000080" + ) + + +function get_color(s::String)::String + if occursin(r"^[0-9A-F]{8}$", s) # is a valid 6 digit hexadecimal color + return s + end + @assert haskey(valid_colors, s) "Invalid color value: $s. Must be an 8-digit hexadecimal RGB value or one of Black, White, Red, Lime, Blue, Yellow, Cyan, Magenta, Silver, Gray, Maroon, Olive, Green, Purple, Teal, Navy." + return allowed_colors[s] +end # ========================================================================================== # @@ -478,7 +504,7 @@ As an expedient to get fonts to work, the `scheme` attribute is simply dropped f 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 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. @@ -575,11 +601,11 @@ 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." + #@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." @@ -1010,8 +1036,8 @@ function setBorder(sh::Worksheet, cellref::CellRef; 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 + #@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"] = get_color(v) end end end @@ -1371,8 +1397,8 @@ 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 + #@assert occursin(r"^[0-9A-F]{8}$", fgColor) "Invalid color value: $fgColor. Must be an 8-digit hexadecimal RGB value." + patternFill["fgrgb"] = get_color(fgColor) end elseif a == "bg" if isnothing(bgColor) @@ -1382,8 +1408,8 @@ 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 + #@assert occursin(r"^[0-9A-F]{8}$", bgColor) "Invalid color value: $bgColor. Must be an 8-digit hexadecimal RGB value." + patternFill["bgrgb"] = get_color(bgColor) end end end From 20d87d8854d2315da56a447db177057202520d22 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 25 Mar 2025 17:12:14 +0000 Subject: [PATCH 035/154] Add support for named colors from Colors.jl --- Project.toml | 5 +++-- src/XLSX.jl | 1 + src/cellformats.jl | 53 +++++++++++++++++++++++----------------------- test/runtests.jl | 9 ++++++++ 4 files changed, 39 insertions(+), 29 deletions(-) diff --git a/Project.toml b/Project.toml index 58859c93..c486e0c0 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,7 @@ version = "0.10.5-dev" [deps] Artifacts = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" @@ -15,15 +16,15 @@ XML = "72c71f33-b9b6-44de-8c94-c961784809e2" ZipArchives = "49080126-0e18-4c2a-b176-c102e4b3760c" [compat] +Colors = "0.13.0" Tables = "1" XML = "0.3.4" ZipArchives = "2" julia = "1.8" - [extras] DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test", "DataFrames"] \ No newline at end of file +test = ["Test", "DataFrames"] diff --git a/src/XLSX.jl b/src/XLSX.jl index deadb45c..6e1c4c8a 100644 --- a/src/XLSX.jl +++ b/src/XLSX.jl @@ -8,6 +8,7 @@ import ZipArchives import XML import Tables import Unicode +import Colors import Base.convert const SPREADSHEET_NAMESPACE_XPATH_ARG = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" diff --git a/src/cellformats.jl b/src/cellformats.jl index df8a6a2c..9a1be741 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -445,31 +445,27 @@ function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange; k return newid end end -const valid_colors = Dict("black" => "FF000000", # color name => RGB - "white" => "FFFFFFFF", - "red" => "FFFF0000", - "lime" => "FF00FF00", - "blue" => "FF0000FF", - "yellow" => "FFFFFF00", - "cyan" => "FF00FFFF", - "mMagenta" => "FFFF00FF", - "silver" => "FFC0C0C0", - "gray" => "FF808080", - "maroon" => "FF800000", - "olive" => "FF808000", - "green" => "FF008000", - "purple" => "FF800080", - "teal" => "FF008080", - "navy" => "FF000080" - ) +# Check if a string is a valid colorant in Colors.jlfunction is_valid_colorant(color_string::String) +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 6 digit hexadecimal color return s end - @assert haskey(valid_colors, s) "Invalid color value: $s. Must be an 8-digit hexadecimal RGB value or one of Black, White, Red, Lime, Blue, Yellow, Cyan, Magenta, Silver, Gray, Maroon, Olive, Green, Purple, Teal, Navy." - return allowed_colors[s] + c = get_colorant(s) + if isnothing(c) + error("Invalid colorant name or rgb color specified: $s. Either give an valid colors.jl colorant name or an 8-digit rgb color in the form AARRGGBB") + end + return c end # ========================================================================================== @@ -490,7 +486,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 @@ -510,6 +506,9 @@ The `color` attribute can only be defined as rgb values. - 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 +(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. @@ -820,8 +819,6 @@ 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") @@ -922,8 +919,9 @@ 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 set by specifying an 8-digit hexadecimal value. +Alternatively, you can use the name of any named color from Colors.jl +(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 @@ -1167,7 +1165,6 @@ function setOutsideBorder(ws::Worksheet, rng::CellRange; setBorder(ws, CellRange(topRight, bottomRight); right= ["color" => color]) setBorder(ws, CellRange(bottomLeft, bottomRight); bottom= ["color" => color]) end - return -1 @@ -1324,8 +1321,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 +(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. diff --git a/test/runtests.jl b/test/runtests.jl index 8ba10a9a..113a189b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1737,6 +1737,15 @@ end @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.setOutsideBorder(s, "B2:E5"; color="dodgerblue4", style="thick") + @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.open_xlsx_template(joinpath(data_directory, "Borders.xlsx")) s = f["Sheet1"] From 6a7dcc374b5250a3f7029c36ae028591ebef0b92 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 25 Mar 2025 18:35:36 +0000 Subject: [PATCH 036/154] Revert to XML.escape with XML.jl 0.3.5 --- Project.toml | 2 +- src/cellformats.jl | 9 ++++----- src/styles.jl | 2 +- src/write.jl | 22 +++++++++++----------- test/runtests.jl | 6 +++--- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/Project.toml b/Project.toml index c486e0c0..47e32798 100644 --- a/Project.toml +++ b/Project.toml @@ -18,7 +18,7 @@ ZipArchives = "49080126-0e18-4c2a-b176-c102e4b3760c" [compat] Colors = "0.13.0" Tables = "1" -XML = "0.3.4" +XML = "0.3.5" ZipArchives = "2" julia = "1.8" diff --git a/src/cellformats.jl b/src/cellformats.jl index 9a1be741..ebf20062 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -446,7 +446,7 @@ function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange; k end end -# Check if a string is a valid colorant in Colors.jlfunction is_valid_colorant(color_string::String) +# 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) @@ -456,14 +456,13 @@ function get_colorant(color_string::String) return nothing end end - function get_color(s::String)::String - if occursin(r"^[0-9A-F]{8}$", s) # is a valid 6 digit hexadecimal color + 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) - error("Invalid colorant name or rgb color specified: $s. Either give an valid colors.jl colorant name or an 8-digit rgb color in the form AARRGGBB") + error("Invalid color specified: $s. Either give a valid color name (from Colors.jl) or an 8-digit rgb color in the form AARRGGBB") end return c end @@ -1884,7 +1883,7 @@ function setFormat(sh::Worksheet, cellref::CellRef; format_node = XML.Element("numFmt"; numFmtId = string(existing_elements_count + PREDEFINED_NUMFMT_COUNT), - formatCode = xlsx_escape(format) + formatCode = XML.escape(format) ) new_formatid = styles_add_cell_attribute(wb, format_node, "numFmts") + PREDEFINED_NUMFMT_COUNT diff --git a/src/styles.jl b/src/styles.jl index 89b42c72..8063ba19 100644 --- a/src/styles.jl +++ b/src/styles.jl @@ -118,7 +118,7 @@ function styles_add_numFmt(wb::Workbook, format_code::AbstractString) :: Integer fmt_code = existing_numFmt_elements_count + PREDEFINED_NUMFMT_COUNT new_fmt = XML.Element("numFmt"; numFmtId = fmt_code, - formatCode = xlsx_escape(format_code) + formatCode = XML.escape(format_code) ) push!(numfmts, new_fmt) return fmt_code diff --git a/src/write.jl b/src/write.jl index 60008c21..e86a530d 100644 --- a/src/write.jl +++ b/src/write.jl @@ -503,14 +503,14 @@ 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 +#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) @@ -518,7 +518,7 @@ function xlsx_encode(ws::Worksheet, val::AbstractString) 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(XML.escape(val))) return ("s", string(sst_ind)) end @@ -729,7 +729,7 @@ function writetable!( 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 @@ -740,7 +740,7 @@ function writetable!( 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 diff --git a/test/runtests.jl b/test/runtests.jl index 113a189b..17f0ba73 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2326,20 +2326,20 @@ 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 = XML.escape("& & \" > < ") 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, XML.escape.(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 From b9b54f0b50bb980034dfc9fcd62e6aefff3f15ac Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 25 Mar 2025 23:17:50 +0000 Subject: [PATCH 037/154] Integrate `setOutsideBorder` into `setBorder` --- docs/src/api.md | 1 - src/cellformats.jl | 159 +++++++++++++++++++++++++++++---------------- src/write.jl | 36 ++++------ test/runtests.jl | 4 +- 4 files changed, 117 insertions(+), 83 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index 34cb938f..9e7584a9 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -33,7 +33,6 @@ XLSX.setFont XLSX.setUniformFont XLSX.setBorder XLSX.setUniformBorder -XLSX.setOutsideBorder XLSX.setFill XLSX.setUniformFill XLSX.setAlignment diff --git a/src/cellformats.jl b/src/cellformats.jl index ebf20062..925df639 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -144,21 +144,7 @@ function buildNode(tag::String, attributes::Dict{String,Union{Nothing,Dict{Strin 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) @@ -890,16 +876,23 @@ Borders are independently defined for the keywords: - `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. +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 a non-contiguous range and `outside` 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: @@ -918,7 +911,9 @@ Allowed values for `style` are: - `mediumDashDotDot` - `slantDashDot` -The `color` attribute can set by specifying an 8-digit hexadecimal value. +The `color` attribute can set by specifying an 8-digit hexadecimal value +in the format "AARRGGBB". The transparency ("AA") is ignored by Excel but +is required. Alternatively, you can use the name of any named color from Colors.jl (https://juliagraphics.github.io/Colors.jl/stable/namedcolors/) @@ -950,19 +945,80 @@ Julia> setBorder(xf, "Sheet1!D4"; left = ["style" => "dotted", "color" => "F right = ["style" => "medium", "color" => "FF765000"], top = ["style" => "thick", "color" => "FF230000"], bottom = ["style" => "medium", "color" => "FF0000FF"], - diagonal = ["style" => "dotted", "color" => "FF00D4D4"] + 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, rowrng::RowRange; kw...)::Int = process_rowranges(setBorder, ws, rowrng; kw...) -setBorder(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setBorder, ws, ncrng; kw...) +setBorder(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setBorder, ws, ncrng; outside) 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...) +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 + @assert all(isnothing, [left, right, top, bottom, diagonal, allsides]) "Keyword `outside` is incompatible with any other keywords except `diagonal`." + 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 + @assert all(isnothing, [left, right, top, bottom, diagonal, allsides]) "Keyword `outside` is incompatible with any other keywords except `diagonal`." + 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 + @assert all(isnothing, [left, right, top, bottom, diagonal, allsides]) "Keyword `outside` is incompatible with any other keywords except `diagonal`." + 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 + @assert all(isnothing, [left, right, top, bottom, diagonal, allsides]) "Keyword `outside` is incompatible with any other keywords except `diagonal`." + return process_sheetcell(setOutsideBorder, xl, sheetcell; outside) + end +end function setBorder(sh::Worksheet, cellref::CellRef; + 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, @@ -981,6 +1037,8 @@ 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) + @assert isnothing(outside) "Cannot set an outside border on a single cell." + 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) @@ -1106,13 +1164,14 @@ setUniformBorder(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_at 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. -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. Top and bottom borders for column ranges and left and right borders for @@ -1122,15 +1181,13 @@ An outside border cannot be set for a non-contiguous range. The value returned is is -1. -For keyword definitions see [`setBorder()`](@ref). - # 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, colrng::ColumnRange; kw...)::Int = process_columnranges(setOutsideBorder, ws, colrng; kw...) @@ -1138,32 +1195,24 @@ setOutsideBorder(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowrange 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 + outside::Union{Nothing,Vector{Pair{String,String}}}=nothing, +)::Int @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot set borders because cache is not enabled." + 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) - 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]) - end + + 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 @@ -2104,7 +2153,7 @@ function setColumnWidth(ws::Worksheet, rng::CellRange; width::Union{Nothing,Real 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 diff --git a/src/write.jl b/src/write.jl index e86a530d..1c2f1d89 100644 --- a/src/write.jl +++ b/src/write.jl @@ -171,37 +171,23 @@ function get_node_paths!(xpaths::Vector{xpath}, node::XML.Node, default_ns, path 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 givenn by att[2] from a parent XML node with a tag given by att[1]. +function unlink(node::XML.Node, att::Vector{String}) + new_node = XML.Element(att[1]) 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) != att[2] + push!(new_node, child) end end - return new_worksheet + return new_node end -function unlink_definedNames(node::XML.Node) # removes each `col` from a `cols` XML node. - new_cols = XML.Element("definedNames") - a = XML.attributes(node) - if !isnothing(a) # Copy attributes across to new node (probably none) - 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) != "definedName" # Shouldn't be any. - push!(new_cols, child) - end - end - return new_cols -end - function get_idces(doc, t, b) i=1 j=1 @@ -268,7 +254,7 @@ function update_worksheets_xml!(xl::XLSXFile) end end - doc[i][j] = unlink_rows(parent) + doc[i][j] = unlink(parent, ["sheetData", "row"]) end # updates sheetData @@ -400,7 +386,7 @@ function update_workbook_xml!(xl::XLSXFile) # Only the block will j=n+1 else - definedNames = unlink_definedNames(wbdoc[i][j]) # Remove old defined names + definedNames = unlink(wbdoc[i][j], ["definedNames", "definedName"]) # Remove old defined names end for (k, v) in wb.workbook_names diff --git a/test/runtests.jl b/test/runtests.jl index 17f0ba73..99cd0a93 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1719,7 +1719,7 @@ end s[i, j]="" end end - XLSX.setOutsideBorder(s, "B2:E5"; color="FFFF0000", style="thick") + 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) @@ -1737,7 +1737,7 @@ end @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.setOutsideBorder(s, "B2:E5"; color="dodgerblue4", style="thick") + 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) From 21d7e3a1217d31ae4812e4c99aeaf2cc5422309e Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Wed, 26 Mar 2025 14:18:56 +0000 Subject: [PATCH 038/154] Minor tweaks! --- src/cellformats.jl | 2 +- src/write.jl | 10 +++++----- test/runtests.jl | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/cellformats.jl b/src/cellformats.jl index 925df639..0ab026c2 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -2153,7 +2153,7 @@ function setColumnWidth(ws::Worksheet, rng::CellRange; width::Union{Nothing,Real end end - new_cols = unlink(sheetdoc[i][j], ["cols", "col"]) # 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 diff --git a/src/write.jl b/src/write.jl index 1c2f1d89..ce5ac7ee 100644 --- a/src/write.jl +++ b/src/write.jl @@ -173,8 +173,8 @@ function get_node_paths!(xpaths::Vector{xpath}, node::XML.Node, default_ns, path end # Remove all children with tag givenn by att[2] from a parent XML node with a tag given by att[1]. -function unlink(node::XML.Node, att::Vector{String}) - new_node = XML.Element(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) @@ -182,7 +182,7 @@ function unlink(node::XML.Node, att::Vector{String}) end end 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) != att[2] + if XML.tag(child) != last(att) push!(new_node, child) end end @@ -254,7 +254,7 @@ function update_worksheets_xml!(xl::XLSXFile) end end - doc[i][j] = unlink(parent, ["sheetData", "row"]) + doc[i][j] = unlink(parent, ("sheetData", "row")) end # updates sheetData @@ -386,7 +386,7 @@ function update_workbook_xml!(xl::XLSXFile) # Only the block will j=n+1 else - definedNames = unlink(wbdoc[i][j], ["definedNames", "definedName"]) # Remove old defined names + definedNames = unlink(wbdoc[i][j], ("definedNames", "definedName")) # Remove old defined names end for (k, v) in wb.workbook_names diff --git a/test/runtests.jl b/test/runtests.jl index 99cd0a93..be8576ed 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -215,7 +215,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 @@ -1237,7 +1237,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) From bb524273cd569649f6be292dde534477dc537f93 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Thu, 27 Mar 2025 00:12:54 +0000 Subject: [PATCH 039/154] Fix `getBorder()` for diagonal borders --- .github/workflows/ci.yml | 4 ++-- src/cellformats.jl | 11 +++++------ src/styles.jl | 15 ++++++++------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b52f431..685059f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ name: CI on: - workflow_dispatch: # <-- Add this to allow manual execution +# workflow_dispatch: # <-- Add this to allow manual execution pull_request: branches: - master @@ -30,7 +30,7 @@ jobs: - macOS-latest arch: - x64 - include: + include: # must be a better way! - os: macOS-latest arch: aarch64 version: '1.8' diff --git a/src/cellformats.jl b/src/cellformats.jl index 0ab026c2..2a25089d 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -837,7 +837,7 @@ function getBorder(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellBorder @assert length(XML.attributes(side)) == 1 "Too many border attributes found for $(XML.tag(side)) Expected 1, found $(length(XML.attributes(side)))." 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") @@ -1018,7 +1018,6 @@ function setBorder(xl::XLSXFile, sheetcell::String; end end function setBorder(sh::Worksheet, cellref::CellRef; - 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, @@ -1037,8 +1036,6 @@ 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) - @assert isnothing(outside) "Cannot set an outside border on a single cell." - 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) @@ -1136,6 +1133,8 @@ 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"]) @@ -1195,8 +1194,8 @@ setOutsideBorder(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowrange 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; - outside::Union{Nothing,Vector{Pair{String,String}}}=nothing, -)::Int + outside::Union{Nothing,Vector{Pair{String,String}}}=nothing, + )::Int @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot set borders because cache is not enabled." diff --git a/src/styles.jl b/src/styles.jl index 8063ba19..15395e13 100644 --- a/src/styles.jl +++ b/src/styles.jl @@ -103,13 +103,14 @@ function styles_add_numFmt(wb::Workbook, format_code::AbstractString) :: Integer # 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 +# nchildren = length(XML.children(stylesheet)) + numfmts = XML.Element("numFmts", count="1") + XML.pushfirst!(stylesheet, numfmts) +# push!(stylesheet, stylesheet[end]) +# for i in nchildren-1:-1:1 +# stylesheet[i+1]=stylesheet[i] +# end +# stylesheet[1]=numfmts else numfmts = numfmts[1] end From c49e74f80430365d00737ddc71fb0268cfd00863 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Thu, 27 Mar 2025 00:23:33 +0000 Subject: [PATCH 040/154] Add examples of named colors in docstrings --- src/cellformats.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cellformats.jl b/src/cellformats.jl index 2a25089d..15d8edf5 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -942,9 +942,9 @@ For cell ranges, column ranges and named ranges, the value returned is -1. Julia> setBorder(sh, "D6"; allsides = ["style" => "thick"], diagonal = ["style" => "hair", "direction" => "up"]) 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"], + bottom = ["style" => "medium", "color" => "goldenrod3"], diagonal = ["style" => "dotted", "color" => "FF00D4D4", "direction" => "both"] ) @@ -1118,7 +1118,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. From 35de99f72caa34bceb43d6e2ae3a19707841b6dc Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Thu, 27 Mar 2025 10:32:21 +0000 Subject: [PATCH 041/154] Update tests for changes to getBorder Fix behaviour of `setUniformFormat()` --- src/cellformats.jl | 17 +++++++++-------- test/runtests.jl | 26 +++++++++++++------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/cellformats.jl b/src/cellformats.jl index 15d8edf5..9a81329a 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -402,6 +402,7 @@ function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange, a 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." @@ -507,21 +508,21 @@ 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="FF8888FF", under="single") # Column range +julia> setFont(xf, "Sheet1!A:B"; italic=true, color="red", 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 ``` """ @@ -1986,7 +1987,7 @@ setUniformFormat(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowrange setUniformFormat(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setUniformFormat, ws, ncrng; 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, rng::CellRange; kw...)::Int = process_uniform_attribute(setFormat, ws, rng, ["numFmtId", "applyNumberFormat"]; kw...) # # -- Set uniform styles @@ -2038,16 +2039,16 @@ function setUniformStyle(ws::Worksheet, rng::CellRange)::Union{Nothing, Int} continue end if first # Get the style of the first cell in the range. - newid = cell.style + newid = parse(Int, cell.style) first = false else # Apply the same style to the rest of the cells in the range. - cell.style = newid + cell.style = string(newid) end end if first newid = -1 end - return isnothing(newID) ? nothing : newid + return isnothing(newid) ? nothing : newid end end diff --git a/test/runtests.jl b/test/runtests.jl index be8576ed..057e9b62 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1696,19 +1696,19 @@ 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")) + @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: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, "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). @@ -1770,10 +1770,10 @@ end # 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, "J20").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, "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", "direction" => "both")) + @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", "direction" => "both")) + @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", "direction" => "both")) + @test XLSX.getBorder(s, "J20").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")) # Can't get attributes on a range. @test_throws AssertionError XLSX.getBorder(s, "Contiguous") From b3feab59282be414eabf91123fface19ea42d65d Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 28 Mar 2025 16:04:53 +0000 Subject: [PATCH 042/154] Replace `@assert` with `throw()` (#190) --- .github/workflows/ci.yml | 50 +++---- src/cell.jl | 23 +++- src/cellformats.jl | 275 +++++++++++++++++++++++++++------------ src/cellref.jl | 42 +++--- src/read.jl | 28 ++-- src/relationship.jl | 20 +-- src/sst.jl | 18 ++- src/stream.jl | 20 +-- src/styles.jl | 18 ++- src/table.jl | 20 ++- src/types.jl | 31 ++++- src/workbook.jl | 22 ++-- src/worksheet.jl | 13 +- src/write.jl | 76 +++++------ test/runtests.jl | 102 +++++++-------- 15 files changed, 464 insertions(+), 294 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 685059f3..8c713a1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,31 +30,31 @@ jobs: - macOS-latest arch: - x64 - include: # must be a better way! - - os: macOS-latest - arch: aarch64 - version: '1.8' - - os: macOS-latest - arch: aarch64 - version: '1.9' - - os: macOS-latest - arch: aarch64 - version: '1.10' - - os: macOS-latest - arch: aarch64 - version: '1.11' - - os: macOS-latest - arch: aarch64 - version: 'lts' - - os: macOS-latest - arch: aarch64 - version: '1' - - os: macOS-latest - arch: aarch64 - version: 'pre' - - os: macOS-latest - arch: aarch64 - version: 'nightly' +# include: # must be a better way! +# - os: macOS-latest +# arch: aarch64 +# version: '1.8' +# - os: macOS-latest +# arch: aarch64 +# version: '1.9' +# - os: macOS-latest +# arch: aarch64 +# version: '1.10' +# - os: macOS-latest +# arch: aarch64 +# version: '1.11' +# - os: macOS-latest +# arch: aarch64 +# version: 'lts' +# - os: macOS-latest +# arch: aarch64 +# version: '1' +# - os: macOS-latest +# arch: aarch64 +# version: 'pre' +# - os: macOS-latest +# arch: aarch64 +# version: 'nightly' steps: - uses: actions/checkout@v4 diff --git a/src/cell.jl b/src/cell.jl index 113b6907..c3f70b4a 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 @@ -255,11 +257,15 @@ 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 $XML")) + end local dt::Dates.Date local hr::Dates.Time diff --git a/src/cellformats.jl b/src/cellformats.jl index 9a81329a..6d904edc 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -133,7 +133,9 @@ function buildNode(tag::String, attributes::Dict{String,Union{Nothing,Dict{Strin bgcolor[k[3:end]] = v end end - @assert haskey(patternfill, "patternType") "No `patternType` attribute found." + 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 @@ -148,7 +150,9 @@ 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." + 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 @@ -176,7 +180,9 @@ function styles_add_cell_attribute(wb::Workbook, new_att::XML.Node, att::String) 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"]))." + 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])) @@ -208,26 +214,26 @@ function process_sheetcell(f::Function, xl::XLSXFile, sheetcell::String; kw...): end elseif is_valid_non_contiguous_sheetcellrange(sheetcell) sheetncrng = NonContiguousRange(sheetcell) - @assert hassheet(xl, sheetncrng.sheet) "Sheet $(ref.sheet) not found." + !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) - @assert hassheet(xl, sheetcolrng.sheet) "Sheet $(ref.sheet) not found." + !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) - @assert hassheet(xl, sheetrowrng.sheet) "Sheet $(ref.sheet) not found." + !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) - @assert hassheet(xl, sheetcellrng.sheet) "Sheet $(ref.sheet) not found." + !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) - @assert hassheet(xl, ref.sheet) "Sheet $(ref.sheet) not found." + !hassheet(xl, ref.sheet) && throw(XLSXError("Sheet $(ref.sheet) not found.")) newid = f(getsheet(xl, ref.sheet), ref.cellref; kw...) else - error("Invalid sheet cell reference: $sheetcell") + throw(XLSXError("Invalid sheet cell reference: $sheetcell")) end return newid end @@ -343,7 +349,7 @@ function process_cellranges(f::Function, ws::Worksheet, rng::CellRange; kw...):: end function process_get_sheetcell(f::Function, xl::XLSXFile, sheetcell::String; kw...) ref = SheetCellRef(sheetcell) - @assert hassheet(xl, ref.sheet) "Sheet $(ref.sheet) not found." + !hassheet(xl, ref.sheet) && throw(XLSXError("Sheet $(ref.sheet) not found.")) return f(getsheet(xl, ref.sheet), ref.cellref; kw...) end function process_get_cellref(f::Function, ws::Worksheet, cellref::CellRef; kw...) @@ -377,7 +383,9 @@ function process_get_cellname(f::Function, ws::Worksheet, ref_or_rng::AbstractSt 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." + if !get_xlsxfile(ws).use_cache_for_sheet_data + throw(XLSXError( "Cannot set uniform attributes because cache is not enabled.")) + end let newid first = true @@ -405,7 +413,9 @@ 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." + if !get_xlsxfile(ws).use_cache_for_sheet_data + throw(XLSXError( "Cannot set uniform attributes because cache is not enabled.")) + end let newid, alignment_node first = true @@ -543,12 +553,16 @@ function setFont(sh::Worksheet, cellref::CellRef; name::Union{Nothing,String}=nothing )::Int - @assert get_xlsxfile(sh).use_cache_for_sheet_data "Cannot set font because cache is not enabled." + 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 if cell.style == "" cell.style = string(get_num_style_index(sh, 0).id) @@ -571,7 +585,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) @@ -586,14 +602,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" => 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) @@ -724,21 +739,24 @@ getFont(ws::Worksheet, cellref::CellRef)::Union{Nothing,CellFont} = process_get_ 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"]))" + 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 @@ -820,14 +838,18 @@ getBorder(ws::Worksheet, cr::String) = process_get_cellname(getBorder, ws, cr) 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"]))" + 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}}() @@ -835,7 +857,9 @@ 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 XML.tag(side) == "diagonal" && !isnothing(diag_atts) @@ -846,7 +870,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 @@ -966,7 +990,9 @@ function setBorder(ws::Worksheet, rng::CellRange; if isnothing(outside) return process_cellranges(setBorder, ws, rng; allsides, left, right, top, bottom, diagonal) else - @assert all(isnothing, [left, right, top, bottom, diagonal, allsides]) "Keyword `outside` is incompatible with any other keywords except `diagonal`." + 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 @@ -982,7 +1008,9 @@ function setBorder(ws::Worksheet, colrng::ColumnRange; if isnothing(outside) return process_columnranges(setBorder, ws, colrng; allsides, left, right, top, bottom, diagonal) else - @assert all(isnothing, [left, right, top, bottom, diagonal, allsides]) "Keyword `outside` is incompatible with any other keywords except `diagonal`." + 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 @@ -998,7 +1026,9 @@ function setBorder(ws::Worksheet, rowrng::RowRange; if isnothing(outside) return process_rowranges(setBorder, ws, rowrng; allsides, left, right, top, bottom, diagonal) else - @assert all(isnothing, [left, right, top, bottom, diagonal, allsides]) "Keyword `outside` is incompatible with any other keywords except `diagonal`." + 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 @@ -1014,7 +1044,9 @@ function setBorder(xl::XLSXFile, sheetcell::String; if isnothing(outside) return process_sheetcell(setBorder, xl, sheetcell; allsides, left, right, top, bottom, diagonal) else - @assert all(isnothing, [left, right, top, bottom, diagonal, allsides]) "Keyword `outside` is incompatible with any other keywords except `diagonal`." + 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 @@ -1027,7 +1059,9 @@ function setBorder(sh::Worksheet, cellref::CellRef; diagonal::Union{Nothing,Vector{Pair{String,String}}}=nothing )::Int - @assert get_xlsxfile(sh).use_cache_for_sheet_data "Cannot set borders because cache is not enabled." + if !get_xlsxfile(sh).use_cache_for_sheet_data + throw(XLSXError("Cannot set borders because cache is not enabled.")) + end kwdict = Dict{String,Union{Dict{String,String},Nothing}}() kwdict["allsides"] = isnothing(allsides) ? nothing : Dict{String,String}(p for p in allsides) @@ -1038,14 +1072,18 @@ function setBorder(sh::Worksheet, cellref::CellRef; 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`." + 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 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 if cell.style == "" cell.style = string(get_num_style_index(sh, 0).id) @@ -1066,7 +1104,9 @@ function setBorder(sh::Worksheet, cellref::CellRef; 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`." + 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 new_border_atts[a]["style"] = kwdict[a]["style"] end if a == "diagonal" @@ -1077,7 +1117,9 @@ function setBorder(sh::Worksheet, cellref::CellRef; new_border_atts[a]["direction"] = "both" # default if direction not specified or inherited end elseif haskey(kwdict[a], "direction") - @assert kwdict[a]["direction"] ∈ ["up", "down", "both"] "Invalid direction: $v. Must be one of: `up`, `down`, `both`." + 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 @@ -1089,7 +1131,6 @@ function setBorder(sh::Worksheet, cellref::CellRef; 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"] = get_color(v) end end @@ -1198,7 +1239,9 @@ function setOutsideBorder(ws::Worksheet, rng::CellRange; outside::Union{Nothing,Vector{Pair{String,String}}}=nothing, )::Int - @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot set borders because cache is not enabled." + if !get_xlsxfile(ws).use_cache_for_sheet_data + throw(XLSXError("Cannot set borders because cache is not enabled.")) + end kwdict = Dict{String,Union{Dict{String,String},Nothing}}() kwdict["outside"] = Dict{String,String}(p for p in outside) @@ -1302,26 +1345,36 @@ getFill(ws::Worksheet, cr::String) = process_get_cellname(getFill, ws, cr) 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"]))" + 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 @@ -1409,12 +1462,16 @@ function setFill(sh::Worksheet, cellref::CellRef; 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 if cell.style == "" cell.style = string(get_num_style_index(sh, 0).id) @@ -1444,7 +1501,6 @@ 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"] = get_color(fgColor) end elseif a == "bg" @@ -1455,7 +1511,6 @@ 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"] = get_color(bgColor) end end @@ -1576,13 +1631,17 @@ getAlignment(ws::Worksheet, cr::String) = process_get_cellname(getAlignment, ws, #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 @@ -1659,12 +1718,16 @@ function setAlignment(sh::Worksheet, cellref::CellRef; rotation::Union{Nothing,Int}=nothing )::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 fill for an `EmptyCell`: $(cellref.name). Set the value first.")) + end if cell.style == "" cell.style = string(get_num_style_index(sh, 0).id) @@ -1680,12 +1743,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"] @@ -1810,7 +1873,9 @@ getFormat(ws::Worksheet, cr::String) = process_get_cellname(getFormat, ws, cr) #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"] @@ -1819,16 +1884,22 @@ function getFormat(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellFormat 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"]))" + 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 = 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)))." + 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 @@ -1891,12 +1962,16 @@ function setFormat(sh::Worksheet, cellref::CellRef; 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 if cell.style == "" cell.style = string(get_num_style_index(sh, 0).id) @@ -1920,7 +1995,9 @@ function setFormat(sh::Worksheet, cellref::CellRef; 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" + if !occursin(floatformats, code) && !any(map(x->occursin(x, code), DATETIME_CODES)) + throw(XLSXError("Specified format is not a valid numFmt: $format")) + end xroot = styles_xmlroot(wb) i, j = get_idces(xroot, "styleSheet", "numFmts") @@ -1928,7 +2005,9 @@ function setFormat(sh::Worksheet, cellref::CellRef; 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"]))." + 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), @@ -2029,7 +2108,9 @@ setUniformStyle(xl::XLSXFile, sheetcell::AbstractString)::Int = process_sheetcel setUniformStyle(ws::Worksheet, ref_or_rng::AbstractString)::Int = process_ranges(setUniformStyle, ws, ref_or_rng) function setUniformStyle(ws::Worksheet, rng::CellRange)::Union{Nothing, Int} - @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot set styles because cache is not enabled." + if !get_xlsxfile(ws).use_cache_for_sheet_data + throw(XLSXError("Cannot set styles because cache is not enabled.")) + end let newid::Union{Nothing, Int}, first = true @@ -2103,7 +2184,9 @@ setColumnWidth(xl::XLSXFile, sheetcell::String; kw...)::Int = process_sheetcell( setColumnWidth(ws::Worksheet, cr::CellRef; kw...)::Int = setColumnWidth(ws::Worksheet, CellRange(cr, cr); 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." + if !get_xlsxfile(ws).is_writable + throw(XLSXError("Cannot set column widths: `XLSXFile` is not writable.")) + end # 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)) @@ -2111,7 +2194,9 @@ function setColumnWidth(ws::Worksheet, rng::CellRange; width::Union{Nothing,Real 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 @@ -2123,7 +2208,7 @@ function setColumnWidth(ws::Worksheet, rng::CellRange; width::Union{Nothing,Real 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 @@ -2193,10 +2278,14 @@ getColumnWidth(ws::Worksheet, cr::String) = process_get_cellname(getColumnWidth, 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\"" + if cellref.row_number < d.start.row_number || cellref.row_number > d.stop.row_number + throw(XLSXError("Cell specified is outside sheet dimension \"$d\"")) + end # 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)) @@ -2273,12 +2362,16 @@ setRowHeight(xl::XLSXFile, sheetcell::String; kw...)::Int = process_sheetcell(se setRowHeight(ws::Worksheet, cr::CellRef; kw...)::Int = setRowHeight(ws::Worksheet, CellRange(cr, cr); 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 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 @@ -2287,10 +2380,10 @@ function setRowHeight(ws::Worksheet, rng::CellRange; height::Union{Nothing,Real} 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 + if haskey(ws.cache.row_ht, r.row) + ws.cache.row_ht[r.row] = padded_height + first = false + end end end @@ -2328,17 +2421,21 @@ getRowHeight(xl::XLSXFile, sheetcell::String)::Union{Nothing,Real} = process_get getRowHeight(ws::Worksheet, cr::String) = process_get_cellname(getRowHeight, ws, cr) 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.row_number < d.start.row_number && cellref.row_number > d.stop.row_number + throw(XLSXError("Cell specified is outside sheet dimension \"$d\"")) + end 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 + if haskey(ws.cache.row_ht, r.row) + return ws.cache.row_ht[r.row] + end end end @@ -2378,7 +2475,9 @@ julia> XLSX.getMergedCells(s) function getMergedCells(ws::Worksheet)::Union{Vector{CellRange}, Nothing} # May be better if merged cells were part of ws.cache? - @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot get merged cells because cache is not enabled." + if !get_xlsxfile(ws).use_cache_for_sheet_data + throw(XLSXError("Cannot get merged cells because cache is not enabled.")) + end # No need to update the xml file using the worksheet cache first (like we did for column width) # because we cannot change merged cells in XLSX.jl. @@ -2390,11 +2489,13 @@ function getMergedCells(ws::Worksheet)::Union{Vector{CellRange}, Nothing} end c = XML.children(sheetdoc[i][j]) - @assert length(c) == parse(Int, sheetdoc[i][j]["count"]) "Unexpected number of mergeCells found: $(length(c)). Expected $(sheetdoc[i][j]["count"])." + 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 - @assert haskey(cell, "ref") "No `ref` attribute found in `mergeCell` element." + !haskey(cell, "ref") && throw(XLSXError("No `ref` attribute found in `mergeCell` element.")) push!(mergedCells, CellRange(cell["ref"])) end @@ -2431,7 +2532,9 @@ isMergedCell(ws::Worksheet, cr::String; kw...)::Bool = process_get_cellname(isMe #isMergedCell(ws::Worksheet, cellref::CellRef)::Bool = isMergedCell(ws, cellref, getMergedCells(ws)) function isMergedCell(ws::Worksheet, cellref::CellRef; mergedCells::Union{Vector{CellRange}, Nothing, Missing} = missing)::Bool - @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot get merged cells because cache is not enabled." + if !get_xlsxfile(ws).use_cache_for_sheet_data + throw(XLSXError("Cannot get merged cells because cache is not enabled.")) + end if ismissing(mergedCells) # Get mergedCells if missing mergedCells=getMergedCells(ws) @@ -2485,7 +2588,9 @@ getMergedBaseCell(ws::Worksheet, cr::String; kw...) = process_get_cellname(getMe #getMergedBaseCell(ws::Worksheet, cellref::CellRef) = getMergedBaseCell(ws, cellref, getMergedCells(ws)) function getMergedBaseCell(ws::Worksheet, cellref::CellRef; mergedCells::Union{Vector{CellRange}, Nothing, Missing}=missing) - @assert get_xlsxfile(ws).use_cache_for_sheet_data "Cannot get merged cells because cache is not enabled." + if !get_xlsxfile(ws).use_cache_for_sheet_data + throw(XLSXError("Cannot get merged cells because cache is not enabled.")) + end if ismissing(mergedCells) # Get mergedCells if missing mergedCells=getMergedCells(ws) diff --git a/src/cellref.jl b/src/cellref.jl index 2bb0f29b..d5270ee4 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) @@ -95,7 +97,7 @@ 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) @@ -148,7 +150,7 @@ 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 @@ -179,7 +181,7 @@ 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 @@ -257,7 +259,7 @@ 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) @@ -284,7 +286,7 @@ 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 @@ -326,7 +328,9 @@ end function is_valid_row_range(r::AbstractString) :: Bool if occursin(RGX_SINGLE_ROW, r) row_number = parse(Int, r) - @assert row_number > 0 && row_number <= EXCEL_MAX_ROWS "Row number should be in the range from 1 to $EXCEL_MAX_ROWS." + 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) @@ -340,12 +344,12 @@ function is_valid_row_range(r::AbstractString) :: Bool end function RowRange(r::AbstractString) - @assert is_valid_row_range(r) "Invalid row range: $r." + !is_valid_row_range(r) && throw(XLSXError("Invalid row range: $r.")) start_name, stop_name = split_column_range(r) # Function works for row ranges too. 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." + !is_valid_column_range(r) && throw(XLSXError("Invalid column range: $r.")) start_name, stop_name = split_column_range(r) return ColumnRange(decode_column_number(start_name), decode_column_number(stop_name)) end @@ -554,7 +558,7 @@ const RGX_CELLNAME_RIGHT_FIXED = r"\$[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 @@ -567,7 +571,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) @@ -581,7 +585,7 @@ function SheetCellRange(n::AbstractString) 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 @@ -590,13 +594,13 @@ 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) - @assert is_valid_sheet_row_range(n) "$n is not a valid SheetRowRange." + !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)) @@ -658,11 +662,13 @@ end NonContiguousRange(s::Worksheet, v::AbstractString)::NonContiguousRange = nCR(s.name, string.(split(v, ","))) function NonContiguousRange(v::AbstractString)::NonContiguousRange - @assert is_valid_non_contiguous_range(v) "$v is not a valid non-contiguous range." + !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]) - @assert all(parse_sheetname_from_sheetcell_name(r) == firstsheet for r in ranges) "All `CellRef`s and `CellRange`s should have the same sheet name." + 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 diff --git a/src/read.jl b/src/read.jl index 1c075995..5f7a7e21 100644 --- a/src/read.jl +++ b/src/read.jl @@ -24,7 +24,7 @@ function check_for_xlsx_file_format(source::IO, label::AbstractString="input") 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) @@ -132,7 +132,9 @@ function openxlsx(f::F, source::Union{AbstractString, IO}; _read, _write = parse_file_mode(mode) if _read - @assert source isa IO || isfile(source) "File $source not found." + if !(source isa IO || isfile(source)) + throw(XLSXError("File $source not found.")) + end xf = open_or_read_xlsx(source, _write, enable_cache, _write) # Why _write, _write here??? else xf = open_empty_template() @@ -165,7 +167,9 @@ function openxlsx(source::Union{AbstractString, IO}; _read, _write = parse_file_mode(mode) if _read - @assert source isa IO || isfile(source) "File $source not found." + if !(source isa IO || isfile(source)) + throw(XLSXError("File $source not found.")) + end return open_or_read_xlsx(source, _write, enable_cache, _write) # Why _write, _write here??? else return open_empty_template() @@ -187,7 +191,7 @@ end function open_or_read_xlsx(source::Union{IO, AbstractString}, read_files::Bool, enable_cache::Bool, read_as_template::Bool) :: XLSXFile # sanity check if read_as_template - @assert read_files && enable_cache + !(read_files && enable_cache) && throw(XLSXError("Something wrong here!")) end xf = XLSXFile(source, enable_cache, read_as_template) @@ -285,7 +289,7 @@ 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 nothing @@ -300,7 +304,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 +312,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 +321,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) @@ -356,7 +360,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 @@ -383,7 +387,7 @@ function parse_workbook!(xf::XLSXFile) for (i, d) in enumerate(split(defined_value_string, ",")) isabs[i]=is_valid_fixed_sheet_cellname(d) || is_valid_fixed_sheet_cellrange(d) end - @assert length(isabs)==length(defined_value.rng) "Error parsing absolute references in non-contiguous range." + 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 @@ -456,13 +460,13 @@ 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 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) diff --git a/src/relationship.jl b/src/relationship.jl index b65285d8..245accda 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\"." + XML.tag(e) != "Relationship" && throw(XLSXError("Unexpected XMLElement: $(XML.tag(e)). Expected: \"Relationship\".")) a = XML.attributes(e) return Relationship( a["Id"], @@ -10,10 +10,10 @@ function Relationship(e::XML.Node) :: Relationship end function parse_relationship_target(prefix::String, target::String) :: String - @assert !isempty(prefix) && !isempty(target) + 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 @@ -49,22 +49,26 @@ end 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 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 xf = get_xlsxfile(wb) - @assert is_writable(xf) "XLSXFile instance is not writable." + !is_writable(xf) && throws(XLSXError("XLSXFile instance is not writable.")) local rId :: String let diff --git a/src/sst.jl b/src/sst.jl index 87ae973c..f3d76106 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" @@ -78,11 +80,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." + if XML.tag(el) != "si" + throw(XLSXError("Unsupported node $(XML.tag(el)) in sst table.")) + end push!(sst.unformatted_strings, unformatted_text(el)) push!(sst.formatted_strings, XML.write(el)) diff --git a/src/stream.jl b/src/stream.jl index 9b5bd08a..01270e62 100644 --- a/src/stream.jl +++ b/src/stream.jl @@ -48,8 +48,10 @@ 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." + !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.filepath) was not found.")) + end if filename in ZipArchives.zip_names(xf.io) return XML.parse(XML.LazyNode, ZipArchives.zip_readentry(xf.io, filename, String)) @@ -78,8 +80,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]) @@ -93,7 +95,7 @@ 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 @@ -122,7 +124,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 @@ -146,7 +148,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 @@ -281,7 +285,7 @@ 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 diff --git a/src/styles.jl b/src/styles.jl index 15395e13..ebf55e2b 100644 --- a/src/styles.jl +++ b/src/styles.jl @@ -42,7 +42,7 @@ 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) @@ -63,8 +63,10 @@ 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.") @@ -132,7 +134,7 @@ function styles_numFmt_formatCode(wb::Workbook, numFmtId::AbstractString) :: Str 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." + length(elements_found) != 1 && throw(XLSXError("numFmtId $numFmtId not found.")) return XML.attributes(elements_found[1])["formatCode"] end @@ -172,7 +174,7 @@ 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 @@ -207,7 +209,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 @@ -261,7 +263,9 @@ 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])) diff --git a/src/table.jl b/src/table.jl index 2d5b11ab..5d2bff51 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 @@ -177,7 +177,9 @@ function eachtablerow( 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." + 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 if normalizenames column_labels = normalizename.(column_labels===nothing ? col_lab : column_labels) @@ -415,7 +417,9 @@ 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 + if is_empty_table_row(sheet_row) && !itr.keep_empty_rows + throw(XLSXError("Something wrong here!")) + end table_row = TableRow(table_row_index, itr.index, sheet_row) # user asked to stop (or end of row range) @@ -471,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 @@ -481,7 +487,9 @@ 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 diff --git a/src/types.jl b/src/types.jl index bc35312e..6b18f4c9 100644 --- a/src/types.jl +++ b/src/types.jl @@ -186,7 +186,9 @@ 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 @@ -197,7 +199,9 @@ struct ColumnRange 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 @@ -206,7 +210,9 @@ struct RowRange stop::Int # row number function RowRange(a::Int, b::Int) - @assert a <= b "Invalid RowRange. Start row must be located before end row." + if a > b + throw(XLSXError("Invalid RowRange. Start row must be located before end row.")) + end return new(a, b) end end @@ -407,7 +413,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) @@ -453,11 +461,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 @@ -472,4 +484,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 402a6053..f8f0c37e 100644 --- a/src/workbook.jl +++ b/src/workbook.jl @@ -105,27 +105,27 @@ 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) - @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.rowrng) end function getdata(xl::XLSXFile, rng::NonContiguousRange) - @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) end @@ -155,29 +155,29 @@ function getdata(xl::XLSXFile, s::AbstractString) 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 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) - @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.rowrng) end function getcellrange(xl::XLSXFile, rng::NonContiguousRange) - @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) end @@ -252,7 +252,7 @@ function addDefName(ws::Worksheet, name::AbstractString, value::DefinedNameValue end if value isa NonContiguousRange - @assert value.sheet == ws.name "Non-contiguous range must be in the same worksheet." + value.sheet != ws.name && throw(XLSXError("Non-contiguous range must be in the same worksheet.")) abs = absolute ? fill(true, length(value.rng)) : fill(false, length(value.rng)) else abs = absolute ? true : false diff --git a/src/worksheet.jl b/src/worksheet.jl index ec9c4f03..0fd99233 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -1,6 +1,6 @@ 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"] @@ -37,7 +37,7 @@ function read_worksheet_dimension(xf::XLSXFile, relationship_id, name) :: Union{ 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))." + 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)") @@ -167,7 +167,7 @@ function getdata(ws::Worksheet, rng::ColumnRange) :: Array{Any,2} 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." + length(columns[i]) != rows && throw(XLSXError("Inconsistent state: Each column should have the same number of rows.")) end return hcat(columns...) @@ -204,7 +204,7 @@ function getdata(ws::Worksheet, rng::RowRange) :: Array{Any,2} cols = length(rows[1]) for r in rows - @assert length(r) == cols "Inconsistent state: Each row should have the same number of columns." + length(r) != cols && throw(XLSXError("Inconsistent state: Each row should have the same number of columns.")) end return permutedims(hcat(rows...)) @@ -400,7 +400,7 @@ function getcellrange(ws::Worksheet, rng::ColumnRange) :: Array{AbstractCell,2} 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." + length(columns[i]) != rows && throw(XLSXError("Inconsistent state: Each column should have the same number of rows.")) end return hcat(columns...) @@ -432,11 +432,12 @@ function getcellrange(ws::Worksheet, rng::RowRange) :: Array{AbstractCell,2} cols = length(rows[1]) for r in rows - @assert length(r) == cols "Inconsistent state: Each row should have the same number of columns." + length(r) != cols && throw(XLSXError("Inconsistent state: Each row should have the same number of columns.")) end return permutedims(hcat(rows...)) end + function getcellrange(ws::Worksheet, rng::NonContiguousRange) :: Vector{AbstractCell} results=Vector{AbstractCell}() for r in rng.rng diff --git a/src/write.jl b/src/write.jl index ce5ac7ee..78ef6ee1 100644 --- a/src/write.jl +++ b/src/write.jl @@ -39,7 +39,7 @@ function open_empty_template( ) :: XLSXFile empty_excel_template = joinpath(path, "blank.xlsx") - @assert isfile(empty_excel_template) "Couldn't find template file $empty_excel_template." + !isfile(empty_excel_template) && throw(XLSXError("Couldn't find template file $empty_excel_template.")) xf = open_xlsx_template(empty_excel_template) if sheetname != "" @@ -58,10 +58,12 @@ If `overwrite=true`, `output_source` (when a filepath) will be overwritten if it """ 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) @@ -97,16 +99,16 @@ get_worksheet_internal_file(ws::Worksheet) = get_relationship_target_by_id("xl", 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." + !sst.is_loaded && throw(XLSXError("Can't generate XML string from a Shared String Table that is not loaded.")) buff = IOBuffer() # TODO: subtree. @@ -445,8 +447,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) @@ -590,7 +592,7 @@ setdata!(sheet::Worksheet, ::Colon, col::Integer, data::AbstractVector) = setdat 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 @@ -598,7 +600,7 @@ 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 @@ -626,7 +628,7 @@ function setdata!(sheet::Worksheet, ref::CellRef, matrix::Array{T, 2}) where {T} 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))) " + 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 @@ -696,14 +698,14 @@ function writetable!( # 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 @@ -745,8 +747,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] @@ -781,10 +783,10 @@ 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." + !is_writable(xf) && throw(XLSXError("XLSXFile instance is not writable.")) 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.")) if name == "" # name was not provided. Will find a unique name. @@ -801,20 +803,20 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path: else end - @assert name != "" + 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." + name ∈ sheetnames(wb) && throw(XLSXError("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) @@ -868,7 +870,7 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path: # 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" @@ -916,7 +918,7 @@ 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")) if filename isa AbstractString && !overwrite - @assert !isfile(filename) "$filename already exists." + isfile(filename) && throw(XLSXError("$filename already exists.")) end xf = open_empty_template(sheetname) @@ -957,7 +959,7 @@ julia> XLSX.writetable("report.xlsx", "REPORT_A" => df1, "REPORT_B" => df2) 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() @@ -985,7 +987,7 @@ 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}} 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 057e9b62..7875343c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -276,8 +276,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 +305,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) @@ -568,8 +568,8 @@ end @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")) @@ -590,8 +590,8 @@ end @test length(cr) == 1 @test collect(cr) == ["2"] - @test_throws AssertionError XLSX.RowRange("B1:D3") - @test_throws AssertionError XLSX.RowRange("5: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 @@ -620,10 +620,10 @@ end @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 AssertionError XLSX.NonContiguousRange("Sheet1!D1:D3,B1:B3") - @test_throws AssertionError XLSX.NonContiguousRange("Sheet1!D1:D3,Sheet2!B1:B3") - @test_throws AssertionError XLSX.NonContiguousRange("B1:D3") - @test_throws AssertionError XLSX.NonContiguousRange("2:5") + @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 @@ -701,7 +701,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 @@ -818,7 +818,7 @@ end y = XLSX.getcellrange(s, "B:D") @test size(y) == (11, 3) @test x == y - @test_throws AssertionError XLSX.getcellrange(s, "D:B") + @test_throws XLSX.XLSXError XLSX.getcellrange(s, "D:B") @test_throws ErrorException XLSX.getcellrange(s, "A:C1") d = XLSX.getdata(s, "B:D") @@ -842,7 +842,7 @@ end @test all(d .=== d2) @test_throws ErrorException f["table!B1:D"] - @test_throws AssertionError f["table!D:B"] + @test_throws XLSX.XLSXError f["table!D:B"] s = f["table2"] test_data = Vector{Any}(undef, 3) @@ -1155,7 +1155,7 @@ 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 @@ -1176,7 +1176,7 @@ end s = f["general"] @test_throws ErrorException 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!" @@ -1776,7 +1776,7 @@ end @test XLSX.getBorder(s, "J20").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")) # Can't get attributes on a range. - @test_throws AssertionError XLSX.getBorder(s, "Contiguous") + @test_throws XLSX.XLSXError XLSX.getBorder(s, "Contiguous") f = XLSX.open_empty_template() s = f["Sheet1"] @@ -1793,10 +1793,10 @@ end @test XLSX.setFont(s, "A1:F20"; 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.setAlignment(s, "A1:F20"; horizontal="right", wrapText=true) == -1 - @test_throws AssertionError XLSX.setFill(f, "Sheet1!A1"; pattern="none", fgColor="88FF8800") - @test_throws AssertionError XLSX.setFont(s, "A1"; size=18, name="Arial") - @test_throws AssertionError XLSX.setBorder(f, "Sheet1!B2"; left=["style" => "hair"], right=["color" => "FF111111"], top=["style" => "hair"], bottom=["color" => "FF111111"], diagonal=["style" => "hair"]) - @test_throws AssertionError XLSX.setFill(s, "F20"; pattern="none", fgColor="88FF8800") + @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" => "FF111111"], top=["style" => "hair"], bottom=["color" => "FF111111"], 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"] @@ -1843,7 +1843,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")) @@ -1868,7 +1868,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") @@ -2046,29 +2046,29 @@ 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") 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.getColumnWidth(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 @@ -2087,8 +2087,8 @@ end @test !XLSX.isMergedCell(f, "Mock-up!B2") @test !XLSX.isMergedCell(s, "H40"; mergedCells=mc) @test !XLSX.isMergedCell(s, "ID"; mergedCells=mc) - @test_throws AssertionError XLSX.isMergedCell(s, "Contiguous"; mergedCells=mc) # Can't test a range - @test_throws AssertionError XLSX.getMergedBaseCell(s, "Location") + @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 = CellRef("D72"), baseValue = Dates.Date("2025-03-24")) @test XLSX.getMergedBaseCell(f, "Mock-up!G72") == (baseCell = CellRef("D72"), baseValue = Dates.Date("2025-03-24")) @@ -2097,7 +2097,7 @@ end @test XLSX.getMergedBaseCell(s, "Short_Description") == (baseCell = CellRef("D51"), baseValue = "Hello World") @test isnothing(XLSX.getMergedBaseCell(s, "F73")) @test isnothing(XLSX.getMergedBaseCell(f, "Mock-up!H73")) - @test_throws AssertionError XLSX.getMergedBaseCell(s, "Location") # Can't get base cell for a range + @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"] @@ -2122,11 +2122,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 @@ -2186,7 +2186,7 @@ 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 @testset "write column" begin @@ -2246,7 +2246,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 From 2b5ed9abee3ec91f9b13beb67244cd3c67664476 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 28 Mar 2025 16:26:08 +0000 Subject: [PATCH 043/154] Row and/or column indexing with vectors of Ints (#276) --- src/worksheet.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/worksheet.jl b/src/worksheet.jl index 0fd99233..98b5cf96 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -88,6 +88,10 @@ julia> matrix = sheet["1:4"] # Row range 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 @@ -99,6 +103,9 @@ 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::Int, col::Vector{Int}) = [getdata(ws, a, b) for a in [row], b in col] +getdata(ws::Worksheet, row::Vector{Int}, col::Int) = [getdata(ws, a, b) for a in row, b in [col]] +getdata(ws::Worksheet, row::Vector{Int}, col::Vector{Int}) = [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)))) function getdata(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) dim = get_dimension(ws) From a8d20cd3b7fc7482f0e1ee7fcebf5da600020f62 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 28 Mar 2025 17:18:04 +0000 Subject: [PATCH 044/154] Address #258 as proposed... --- src/worksheet.jl | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/worksheet.jl b/src/worksheet.jl index 98b5cf96..9fa8d4f6 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -155,6 +155,20 @@ function getdata(ws::Worksheet, rng::CellRange) :: Array{Any,2} return result end +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) + 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) + getdata(ws, CellRange(start, stop)) +end + +#= function getdata(ws::Worksheet, rng::ColumnRange) :: Array{Any,2} columns_count = length(rng) columns = Vector{Vector{Any}}(undef, columns_count) @@ -179,6 +193,7 @@ function getdata(ws::Worksheet, rng::ColumnRange) :: Array{Any,2} return hcat(columns...) end + function getdata(ws::Worksheet, rng::RowRange) :: Array{Any,2} rows_count = length(rng) dim = get_dimension(ws) @@ -216,6 +231,7 @@ function getdata(ws::Worksheet, rng::RowRange) :: Array{Any,2} return permutedims(hcat(rows...)) end +=# function getdata(ws::Worksheet, rng::NonContiguousRange) :: Vector{Any} results=Vector{Any}() From e73e22ba50835a245a6215b9d1530ef9f5370516 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 28 Mar 2025 19:19:54 +0000 Subject: [PATCH 045/154] Address issue #147 Also remove dependency of `getcellrange()` functions on `eachrow()` --- src/stream.jl | 8 ++++++-- src/worksheet.jl | 24 +++++++++++++++++++----- test/runtests.jl | 12 ++++++------ 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/stream.jl b/src/stream.jl index 01270e62..90588947 100644 --- a/src/stream.jl +++ b/src/stream.jl @@ -313,9 +313,11 @@ 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. +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 +function Base.eachrow(ws::Worksheet) :: SheetRowIterator if is_cache_enabled(ws) if ws.cache === nothing ws.cache = WorksheetCache(ws) @@ -329,3 +331,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/worksheet.jl b/src/worksheet.jl index 9fa8d4f6..552910b4 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -374,9 +374,10 @@ 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) - end - + 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) @@ -398,10 +399,23 @@ function getcellrange(ws::Worksheet, rng::CellRange) :: Array{AbstractCell,2} break end end - +=# return result end +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) + 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) + getcellrange(ws, CellRange(start, stop)) +end +#= function getcellrange(ws::Worksheet, rng::ColumnRange) :: Array{AbstractCell,2} columns_count = length(rng) columns = Vector{Vector{AbstractCell}}(undef, columns_count) @@ -460,7 +474,7 @@ function getcellrange(ws::Worksheet, rng::RowRange) :: Array{AbstractCell,2} return permutedims(hcat(rows...)) end - +=# function getcellrange(ws::Worksheet, rng::NonContiguousRange) :: Vector{AbstractCell} results=Vector{AbstractCell}() for r in rng.rng diff --git a/test/runtests.jl b/test/runtests.jl index 7875343c..587437ea 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -822,7 +822,7 @@ end @test_throws ErrorException XLSX.getcellrange(s, "A:C1") d = XLSX.getdata(s, "B:D") - @test size(d) == (11, 3) + @test size(d) == (12, 3) @test_throws ErrorException XLSX.getdata(s, "A:C1") @test d[1, 1] == "Column B" @test d[1, 2] == "Column C" @@ -830,12 +830,12 @@ end @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) From c128d36228d03008ec26335a826578fff7022019 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 28 Mar 2025 19:24:51 +0000 Subject: [PATCH 046/154] Update tests for changes to `getcellranges` --- test/runtests.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 587437ea..71aa7161 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -814,9 +814,9 @@ 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 XLSX.XLSXError XLSX.getcellrange(s, "D:B") @test_throws ErrorException XLSX.getcellrange(s, "A:C1") From 6ada379e72cdbc233e63220aab7623d3c5d3f7eb Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 29 Mar 2025 13:33:34 +0000 Subject: [PATCH 047/154] Change `error()` to `throw(XLSXError())` --- src/cell.jl | 16 +-- src/cellformats.jl | 121 +++++++++++---------- src/cellref.jl | 4 +- src/read.jl | 19 ++-- src/relationship.jl | 4 +- src/sst.jl | 2 +- src/stream.jl | 4 +- src/styles.jl | 2 +- src/table.jl | 6 +- src/tables_interface.jl | 4 +- src/workbook.jl | 75 ++++++++----- src/worksheet.jl | 233 +++++++++++++++++++++++++++------------- src/write.jl | 6 +- test/runtests.jl | 50 ++++----- 14 files changed, 337 insertions(+), 209 deletions(-) diff --git a/src/cell.jl b/src/cell.jl index c3f70b4a..7a3ba466 100644 --- a/src/cell.jl +++ b/src/cell.jl @@ -77,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 @@ -87,7 +87,7 @@ 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 @@ -98,7 +98,7 @@ function Cell(c::XML.LazyNode) # 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 @@ -113,7 +113,7 @@ 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) @@ -133,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, @@ -143,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"]), @@ -240,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 @@ -251,7 +251,7 @@ 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} diff --git a/src/cellformats.jl b/src/cellformats.jl index 6d904edc..d908b729 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -70,7 +70,7 @@ function buildNode(tag::String, attributes::Dict{String,Union{Nothing,Dict{Strin elseif tag == "fill" attribute_tags = fill_tags else - error("Unknown tag: $tag") + 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 @@ -106,7 +106,7 @@ function buildNode(tag::String, attributes::Dict{String,Union{Nothing,Dict{Strin else#if k == "rgb" color[k] = v #else - #error("Incorect border attribute found: $k") # shouldn't happen! + #throw(XLSXError("Incorect border attribute found: $k")) # shouldn't happen! end end if length(XML.attributes(color)) > 0 # Don't push an empty color. @@ -206,11 +206,11 @@ function process_sheetcell(f::Function, xl::XLSXFile, sheetcell::String; kw...): 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.") + 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_ranges(f, xl, string(v); kw...) else - error("Unexpected defined name value: $v.") + throw(XLSXError("Unexpected defined name value: $v.")) end elseif is_valid_non_contiguous_sheetcellrange(sheetcell) sheetncrng = NonContiguousRange(sheetcell) @@ -242,18 +242,18 @@ function process_ranges(f::Function, ws::Worksheet, ref_or_rng::AbstractString; 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.") + 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 - error("Unexpected defined name value: $v.") + 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) - error("Can only assign attributes to cells but `$(ref_or_rng)` is a constant: $(ref_or_rng)=$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...) @@ -262,7 +262,7 @@ function process_ranges(f::Function, ws::Worksheet, ref_or_rng::AbstractString; newid = f(get_xlsxfile(wb), replace(string(v), "'" => "", "\$" => ""); kw...) end else - error("Unexpected defined name value: $v.") + throw(XLSXError("Unexpected defined name value: $v.")) end elseif is_valid_column_range(ref_or_rng) colrng = ColumnRange(ref_or_rng) @@ -276,66 +276,79 @@ function process_ranges(f::Function, ws::Worksheet, ref_or_rng::AbstractString; 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") + 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...) + return if dim === nothing + @warn "No worksheet dimension found" + [] else - error("Column range $colrng is out of bounds. Worksheet `$(ws.name)` only has dimension `$dim`.") + 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 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...) + return if dim === nothing + @warn "No worksheet dimension found" + [] else - error("Row range $rowrng is out of bounds. Worksheet `$(ws.name)` only has dimension `$dim`.") + 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 end function process_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int bounds = nc_bounds(ncrng) 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 - _ = f(ws, r; kw...) - end - return -1 + return if dim === nothing + @warn "No worksheet dimension found" + [] else - error("Non-contiguous range $ncrng is out of bounds. Worksheet `$(ws.name)` only has dimension `$dim`.") + 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 + _ = 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 end function process_cellranges(f::Function, ws::Worksheet, rng::CellRange; kw...)::Int @@ -368,16 +381,16 @@ function process_get_cellname(f::Function, ws::Worksheet, ref_or_rng::AbstractSt 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 borders to cells but `$(ref_or_rng)` is a constant: $(ref_or_rng)=$v.") + throw(XLSXError("Can only assign borders 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 - error("Unexpected defined name value: $v.") + 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 - error("Invalid cell reference or range: $ref_or_rng") + throw(XLSXError("Invalid cell reference or range: $ref_or_rng")) end return new_att end @@ -459,7 +472,7 @@ function get_color(s::String)::String end c = get_colorant(s) if isnothing(c) - error("Invalid color specified: $s. Either give a valid color name (from Colors.jl) or an 8-digit rgb color in the form AARRGGBB") + throw(XLSXError("Invalid color specified: $s. Either give a valid color name (from Colors.jl) or an 8-digit rgb color in the form AARRGGBB")) end return c end @@ -1991,7 +2004,7 @@ function setFormat(sh::Worksheet, cellref::CellRef; end if haskey(builtinFormatNames, uppercasefirst(format)) # User specified a format by name - new_formatid = builtinFormatNames[uppercasefirst(format)] + new_formatid = builtinFormatNames[format] else # user specified a format code code = lowercase(format) code = remove_formatting(code) diff --git a/src/cellref.jl b/src/cellref.jl index d5270ee4..c16fd746 100644 --- a/src/cellref.jl +++ b/src/cellref.jl @@ -106,7 +106,7 @@ const RGX_CELLNAME_RIGHT = r"[0-9]+$" 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. @@ -696,7 +696,7 @@ function nCR(s::AbstractString, ranges::Vector{String}) :: NonContiguousRange elseif is_valid_cellrange(n) push!(noncontig, CellRange(n)) else - error("Invalid non-contiguous range: $n.") + throw(XLSXError("Invalid non-contiguous range: $n.")) end end diff --git a/src/read.jl b/src/read.jl index 5f7a7e21..80018925 100644 --- a/src/read.jl +++ b/src/read.jl @@ -17,9 +17,9 @@ 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 @@ -184,7 +184,7 @@ 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 @@ -277,7 +277,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 @@ -344,7 +344,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 @@ -424,7 +424,7 @@ function parse_workbook!(xf::XLSXFile) #continue # debug - Now more important since we are writing updated defined names to back to output file. - # error("Could not parse value $(defined_value_string) for definedName $name.") + # throw(XLSXError("Could not parse value $(defined_value_string) for definedName $name.")) end a = XML.attributes(defined_name_node) if haskey(a,"localSheetId") @@ -474,8 +474,7 @@ function internal_xml_file_read(xf::XLSXFile, filename::String) :: XML.Node xf.data[filename] = XML.parse(XML.Node, (ZipArchives.zip_readentry(xf.io, filename, String))) 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 @@ -657,7 +656,7 @@ end #= function readtable(source::Union{AbstractString, IO}, sheet::Union{AbstractString, Int}, rows::RowRange; 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, normalizenames::Bool=false) if rows.start == rows.stop && header==true - error("Only 1 row specified in `RowRange` with `header=true`.\nThe header row is the same as the data row. Specify at least two rows to read header data with `header=true`.") + throw(XLSXError("Only 1 row specified in `RowRange` with `header=true`.\nThe header row is the same as the data row. Specify at least two rows to read header data with `header=true`.")) end first_row = isnothing(first_row) ? rows.start : first_row stop_in_row_function = isnothing(stop_in_row_function) ? r -> r.row >= rows.stop-first_row+1 : stop_in_row_function @@ -673,7 +672,7 @@ function readtable(source::Union{AbstractString, IO}, sheet::Union{AbstractStrin elseif is_valid_column_range(range) range = ColumnRange(range) else - error("The columns argument must be a valid column range or row range.") + throw(XLSXError("The columns argument must be a valid column range or row 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 diff --git a/src/relationship.jl b/src/relationship.jl index 245accda..f6231eec 100644 --- a/src/relationship.jl +++ b/src/relationship.jl @@ -26,7 +26,7 @@ function get_relationship_target_by_id(prefix::String, wb::Workbook, Id::String) 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 @@ -35,7 +35,7 @@ function get_relationship_target_by_type(prefix::String, wb::Workbook, _type_::S 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 diff --git a/src/sst.jl b/src/sst.jl index f3d76106..f2fbdcb0 100644 --- a/src/sst.jl +++ b/src/sst.jl @@ -97,7 +97,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 diff --git a/src/stream.jl b/src/stream.jl index 90588947..36384f73 100644 --- a/src/stream.jl +++ b/src/stream.jl @@ -57,7 +57,7 @@ Base.show(io::IO, state::SheetRowStreamIteratorState) = print(io, "SheetRowStrea return XML.parse(XML.LazyNode, ZipArchives.zip_readentry(xf.io, filename, String)) end - error("Couldn't find $filename in $(xf.source).") + throw(XLSXError("Couldn't find $filename in $(xf.source).")) end # Creates a reader for row elements in the Worksheet's XML. @@ -260,7 +260,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 diff --git a/src/styles.jl b/src/styles.jl index ebf55e2b..45ed85eb 100644 --- a/src/styles.jl +++ b/src/styles.jl @@ -69,7 +69,7 @@ function styles_xmlroot(workbook::Workbook) 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 diff --git a/src/table.jl b/src/table.jl index 5d2bff51..1dc66bae 100644 --- a/src/table.jl +++ b/src/table.jl @@ -255,7 +255,7 @@ function eachtablerow( 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) @@ -265,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) @@ -332,7 +332,7 @@ function getdata(r::TableRow, column_label::Symbol) 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 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/workbook.jl b/src/workbook.jl index f8f0c37e..674b520b 100644 --- a/src/workbook.jl +++ b/src/workbook.jl @@ -54,7 +54,7 @@ function getsheet(wb::Workbook, sheetname::String) :: Worksheet 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] @@ -105,27 +105,27 @@ function Base.getindex(xl::XLSXFile, s::AbstractString) end function getdata(xl::XLSXFile, ref::SheetCellRef) - !hassheet(xl, ref.sheet) && throw(XLSXError("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) - !hassheet(xl, rng.sheet) && throw(XLSXError("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) - !hassheet(xl, rng.sheet) && throw(XLSXError("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.")) + !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.")) + !hassheet(xl, rng.sheet) && throw(XLSXError("Sheet `$(rng.sheet)` not found.")) return getdata(getsheet(xl, rng.sheet), rng) end @@ -137,7 +137,7 @@ function getdata(xl::XLSXFile, s::AbstractString) 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)) @@ -151,38 +151,66 @@ function getdata(xl::XLSXFile, s::AbstractString) return getdata(xl, NonContiguousRange(s)) end - error("$s is not a valid definedName or cell/range reference.") + throw(XLSXError("`$s` is not a valid definedName or cell/range reference.")) end function getcell(xl::XLSXFile, ref::SheetCellRef) - !hassheet(xl, ref.sheet) && throw(XLSXError("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) - !hassheet(xl, rng.sheet) && throw(XLSXError("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) - !hassheet(xl, rng.sheet) && throw(XLSXError("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.")) + !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.")) + !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)) @@ -191,8 +219,7 @@ function getcellrange(xl::XLSXFile, rng_str::AbstractString) 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) @@ -201,7 +228,7 @@ end @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) -@inline get_defined_name_value(wb::Workbook, name::AbstractString) :: DefinedNameValueTypes = wb.workbook_names[name].value + @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 wb = get_workbook(ws) @@ -230,10 +257,10 @@ end function addDefName(xf::XLSXFile, name::AbstractString, value::DefinedNameValueTypes; absolute=true) if !is_valid_defined_name(name) - error("Invalid defined name: $name.") + throw(XLSXError("Invalid defined name: `$name`.")) end if is_workbook_defined_name(xf, name) - error("Workbook already has a defined name called $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)) @@ -245,10 +272,10 @@ end function addDefName(ws::Worksheet, name::AbstractString, value::DefinedNameValueTypes; absolute=true) wb = get_workbook(ws) if !is_valid_defined_name(name) - error("Invalid defined name: $name.") + throw(XLSXError("Invalid defined name: `$name`.")) end if is_worksheet_defined_name(ws, name) - error("Worksheet $(ws.name) already has a defined name called $name.") + throw(XLSXError("Worksheet `$(ws.name)` already has a defined name called `$name`.")) end if value isa NonContiguousRange @@ -301,7 +328,7 @@ addDefinedName(xf::XLSXFile, name::AbstractString, value::Union{Int, Float64}; a addDefinedName(ws::Worksheet, name::AbstractString, value::Union{Int, Float64}; absolute=true) = addDefName(ws, name, value) function addDefinedName(xf::XLSXFile, name::AbstractString, value::AbstractString; absolute=true) if value == "" - error("Defined name value cannot be an empty string.") + throw(XLSXError("Defined name value cannot be an empty string.")) end if is_valid_sheet_cellname(value) return addDefName(xf, name, SheetCellRef(value); absolute) @@ -315,7 +342,7 @@ function addDefinedName(xf::XLSXFile, name::AbstractString, value::AbstractStrin end function addDefinedName(ws::Worksheet, name::AbstractString, value::AbstractString; absolute=true) if value == "" - error("Defined name value cannot be an empty string.") + 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) diff --git a/src/worksheet.jl b/src/worksheet.jl index 552910b4..04505cf9 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -25,8 +25,8 @@ 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} + local result::Union{Nothing,CellRange} = nothing wb = get_workbook(xf) target_file = get_relationship_target_by_id("xl", wb, relationship_id) @@ -57,7 +57,7 @@ end # 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 +@inline get_dimension(ws::Worksheet)::Union{Nothing,CellRange} = ws.dimension function set_dimension!(ws::Worksheet, rng::CellRange) ws.dimension = rng @@ -109,25 +109,23 @@ getdata(ws::Worksheet, row::Vector{Int}, col::Vector{Int}) = [getdata(ws, a, b) 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)))) function getdata(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) dim = get_dimension(ws) - return if dim === nothing - @warn "No worksheet dimension found" - [] + if dim === nothing + throw(XLSXError("No worksheet dimension found")) else getdata(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number))) end end function getdata(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}) dim = get_dimension(ws) - return if dim === nothing - @warn "No worksheet dimension found" - [] + if dim === nothing + throw(XLSXError("No worksheet dimension found")) else getdata(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col)))) end 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) @@ -155,17 +153,25 @@ function getdata(ws::Worksheet, rng::CellRange) :: Array{Any,2} return result end -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) - getdata(ws, CellRange(start, stop)) +function getdata(ws::Worksheet, rng::ColumnRange)::Array{Any,2} + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + start = CellRef(dim.start.row_number, rng.start) + stop = CellRef(dim.stop.row_number, rng.stop) + return getdata(ws, CellRange(start, stop)) + end 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) - getdata(ws, CellRange(start, stop)) +function getdata(ws::Worksheet, rng::RowRange)::Array{Any,2} + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + start = CellRef(rng.start, dim.start.column_number,) + stop = CellRef(rng.stop, dim.stop.column_number) + return getdata(ws, CellRange(start, stop)) + end end #= @@ -197,7 +203,7 @@ end function getdata(ws::Worksheet, rng::RowRange) :: Array{Any,2} rows_count = length(rng) dim = get_dimension(ws) - + rows = Vector{Vector{Any}}(undef, rows_count) for i in 1:rows_count rows[i] = Vector{Any}() @@ -207,7 +213,7 @@ function getdata(ws::Worksheet, rng::RowRange) :: Array{Any,2} top, bottom = row_bounds(rng) left = dim.start.column_number right = dim.stop.column_number - + for sheetrow in eachrow(ws) if sheetrow.row > bottom break @@ -233,8 +239,8 @@ function getdata(ws::Worksheet, rng::RowRange) :: Array{Any,2} end =# -function getdata(ws::Worksheet, rng::NonContiguousRange) :: Vector{Any} - results=Vector{Any}() +function getdata(ws::Worksheet, rng::NonContiguousRange)::Vector{Any} + results = Vector{Any}() for r in rng.rng if r isa CellRef push!(results, getdata(ws, r)) @@ -253,7 +259,7 @@ getdata(ws::Worksheet, s::SheetCellRange) = getdata(ws, s.rng) getdata(ws::Worksheet, s::SheetColumnRange) = getdata(ws, s.colrng) getdata(ws::Worksheet, s::SheetRowRange) = getdata(ws, s.rowrng) -function getdata(ws::Worksheet, ref::AbstractString) :: Union{Array{Any,2}, Any} +function getdata(ws::Worksheet, ref::AbstractString)::Union{Array{Any,2},Any} if is_worksheet_defined_name(ws, ref) v = get_defined_name_value(ws, ref) if is_defined_name_value_a_constant(v) @@ -261,7 +267,7 @@ function getdata(ws::Worksheet, ref::AbstractString) :: Union{Array{Any,2}, Any} 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) @@ -271,7 +277,7 @@ 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)) @@ -281,10 +287,18 @@ function getdata(ws::Worksheet, ref::AbstractString) :: Union{Array{Any,2}, Any} 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_range(ref) return getdata(ws, NonContiguousRange(ws, ref)) else - error("$ref is not a valid cell or range reference.") + throw(XLSXError("`$ref` is not a valid cell or range reference.")) end end @@ -292,7 +306,7 @@ function getdata(ws::Worksheet) if ws.dimension !== nothing return getdata(ws, get_dimension(ws)) else - error("Worksheet dimension is unknown.") + throw(XLSXError("Worksheet dimension is unknown.")) end end @@ -316,6 +330,8 @@ end Returns an `AbstractCell` that represents a cell in the spreadsheet. +If `ref` is a range, `getcell` dispatches to `getcellrange`. + Example: ```julia @@ -326,7 +342,7 @@ julia> sheet = xf["mysheet"] julia> cell = XLSX.getcell(sheet, "A1") ``` """ -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 @@ -349,13 +365,49 @@ function getcell(ws::Worksheet, single::CellRef) :: AbstractCell return EmptyCell(single) end +getcell(ws::Worksheet, s::SheetCellRef) = getcell(ws, s.cellref) +getcell(ws::Worksheet, s::SheetCellRange) = getcell(ws, s.rng) +getcell(ws::Worksheet, s::SheetColumnRange) = getcell(ws, s.colrng) +getcell(ws::Worksheet, s::SheetRowRange) = getcell(ws, s.rowrng) 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)) @@ -370,49 +422,63 @@ 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. """ -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) 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) + 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 + 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 - end - # don't need to read new rows - if sheetrow.row > bottom - break + # don't need to read new rows + if sheetrow.row > bottom + break + end end - end -=# + =# return result end -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) - getcellrange(ws, CellRange(start, stop)) + +getcellrange(ws::Worksheet, s::SheetCellRef) = getcellrange(ws, s.cellref) +getcellrange(ws::Worksheet, s::SheetCellRange) = getcellrange(ws, s.rng) +getcellrange(ws::Worksheet, s::SheetColumnRange) = getcellrange(ws, s.colrng) +getcellrange(ws::Worksheet, s::SheetRowRange) = getcellrange(ws, s.rowrng) + +function getcellrange(ws::Worksheet, rng::ColumnRange)::Array{AbstractCell,2} + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + start = CellRef(dim.start.row_number, rng.start) + stop = CellRef(dim.stop.row_number, rng.stop) + return getcellrange(ws, CellRange(start, stop)) + end 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) - getcellrange(ws, CellRange(start, stop)) +function getcellrange(ws::Worksheet, rng::RowRange)::Array{AbstractCell,2} + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + start = CellRef(rng.start, dim.start.column_number,) + stop = CellRef(rng.stop, dim.stop.column_number) + return getcellrange(ws, CellRange(start, stop)) + end end #= @@ -445,14 +511,14 @@ end function getcellrange(ws::Worksheet, rng::RowRange) :: Array{AbstractCell,2} dim = get_dimension(ws) - + rows = Vector{Vector{AbstractCell}}() let top, bottom = row_bounds(rng) left = dim.start.column_number right = dim.stop.column_number - + for (i, sheetrow) in enumerate(eachrow(ws)) push!(rows, Vector{AbstractCell}()) if top <= sheetrow.row && sheetrow.row <= bottom @@ -475,8 +541,8 @@ function getcellrange(ws::Worksheet, rng::RowRange) :: Array{AbstractCell,2} return permutedims(hcat(rows...)) end =# -function getcellrange(ws::Worksheet, rng::NonContiguousRange) :: Vector{AbstractCell} - results=Vector{AbstractCell}() +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)) @@ -490,7 +556,23 @@ function getcellrange(ws::Worksheet, rng::NonContiguousRange) :: Vector{Abstract 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)) @@ -498,7 +580,14 @@ function getcellrange(ws::Worksheet, rng::AbstractString) return getcellrange(ws, RowRange(rng)) elseif is_valid_non_contiguous_range(rng) return getcellrange(ws, NonContiguousRange(ws, rng)) - else - error("$rng is not a valid cell range.") + 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 78ef6ee1..bbf89447 100644 --- a/src/write.jl +++ b/src/write.jl @@ -576,7 +576,7 @@ end setdata!(ws::Worksheet, ref_str::AbstractString, value) = setdata!(ws, CellRef(ref_str), 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) @@ -613,7 +613,7 @@ function setdata!(sheet::Worksheet, ref_or_rng::AbstractString, matrix::Array{T, 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 @@ -642,7 +642,7 @@ function target_cell_ref_from_offset(anchor_row::Integer, anchor_col::Integer, o elseif dim == 2 return CellRef(anchor_row, anchor_col + offset) else - error("Invalid dimension: $dim.") + throw(XLSXError("Invalid dimension: $dim.")) end end diff --git a/test/runtests.jl b/test/runtests.jl index 71aa7161..411469bd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -123,7 +123,7 @@ 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 @@ -133,7 +133,7 @@ data_directory = joinpath(dirname(pathof(XLSX)), "..", "data") 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 @@ -355,7 +355,7 @@ 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") + @test_throws XLSX.XLSXError XLSX.getcellrange(f, "B2:C3") # a cell can be put in a dict c = XLSX.getcell(sheet1, "B2") @@ -429,7 +429,7 @@ end @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_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" @@ -819,11 +819,11 @@ end @test size(y) == (12, 3) @test x == y @test_throws XLSX.XLSXError XLSX.getcellrange(s, "D:B") - @test_throws ErrorException XLSX.getcellrange(s, "A:C1") + @test_throws XLSX.XLSXError XLSX.getcellrange(s, "A:C1") d = XLSX.getdata(s, "B:D") @test size(d) == (12, 3) - @test_throws ErrorException XLSX.getdata(s, "A:C1") + @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" @@ -841,7 +841,7 @@ end @test size(d) == size(d2) @test all(d .=== d2) - @test_throws ErrorException f["table!B1:D"] + @test_throws XLSX.XLSXError f["table!B1:D"] @test_throws XLSX.XLSXError f["table!D:B"] s = f["table2"] @@ -870,7 +870,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"] @@ -927,7 +927,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")) @@ -938,7 +938,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) @@ -949,10 +949,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 @@ -1174,7 +1174,7 @@ 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 XLSX.XLSXError XLSX.rename!(s, "table") # name is taken XLSX.rename!(s, "renamed_sheet") @@ -1713,13 +1713,13 @@ end @test isnothing(XLSX.getBorder(s, "D11")) # Cannot set a border in an EmptyCell (outside sheet dimension). f = XLSX.newxlsx() - s=f[1] + s = f[1] for i = 1:6 for j = 1:6 - s[i, j]="" + s[i, j] = "" end end - XLSX.setBorder(s, "B2:E5"; outside = ["color"=>"FFFF0000", "style"=>"thick"]) + 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) @@ -1737,7 +1737,7 @@ end @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"]) + 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) @@ -2078,7 +2078,7 @@ end 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"] + 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") @@ -2090,17 +2090,17 @@ end @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 = CellRef("D72"), baseValue = Dates.Date("2025-03-24")) - @test XLSX.getMergedBaseCell(f, "Mock-up!G72") == (baseCell = CellRef("D72"), baseValue = Dates.Date("2025-03-24")) - @test XLSX.getMergedBaseCell(s, "H53") == (baseCell = CellRef("D51"), baseValue = "Hello World") - @test XLSX.getMergedBaseCell(s, "G52") == (baseCell = CellRef("D51"), baseValue = "Hello World") - @test XLSX.getMergedBaseCell(s, "Short_Description") == (baseCell = CellRef("D51"), baseValue = "Hello World") + @test XLSX.getMergedBaseCell(f[1], "F72") == (baseCell=CellRef("D72"), baseValue=Dates.Date("2025-03-24")) + @test XLSX.getMergedBaseCell(f, "Mock-up!G72") == (baseCell=CellRef("D72"), baseValue=Dates.Date("2025-03-24")) + @test XLSX.getMergedBaseCell(s, "H53") == (baseCell=CellRef("D51"), baseValue="Hello World") + @test XLSX.getMergedBaseCell(s, "G52") == (baseCell=CellRef("D51"), baseValue="Hello World") + @test XLSX.getMergedBaseCell(s, "Short_Description") == (baseCell=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"] + s = f["Document History"] @test !XLSX.isMergedCell(f, "Document History!B2") @test !XLSX.isMergedCell(s, "C5"; mergedCells=XLSX.getMergedCells(f["Document History"])) From ab89728067ec45d1323178ebc139a42e66926b23 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 30 Mar 2025 15:31:59 +0100 Subject: [PATCH 048/154] Extend indexing options for `setdata!()` --- src/cellref.jl | 10 ++-- src/types.jl | 18 +++--- src/worksheet.jl | 40 +++++++++++-- src/write.jl | 153 ++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 197 insertions(+), 24 deletions(-) diff --git a/src/cellref.jl b/src/cellref.jl index c16fd746..51754db3 100644 --- a/src/cellref.jl +++ b/src/cellref.jl @@ -303,7 +303,7 @@ const RGX_SINGLE_ROW = r"^[1-9][0-9]*$" # Returns tuple (column_name_start, column_name_stop). # Also works for row ranges (row_name_start, row_name_stop)! -@inline function split_column_range(n::AbstractString) +@inline function split_sheet_range(n::AbstractString) if !occursin(":", n) return n, n else @@ -319,7 +319,7 @@ function is_valid_column_range(r::AbstractString) :: Bool 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 @@ -336,7 +336,7 @@ function is_valid_row_range(r::AbstractString) :: Bool if !occursin(RGX_ROW_RANGE, r) return false end - start_name, stop_name = split_column_range(r) # Function works for row ranges too. + 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 @@ -345,12 +345,12 @@ end function RowRange(r::AbstractString) !is_valid_row_range(r) && throw(XLSXError("Invalid row range: $r.")) - start_name, stop_name = split_column_range(r) # Function works for row ranges too. + start_name, stop_name = split_sheet_range(r) return RowRange(parse(Int, start_name), parse(Int, stop_name)) end function ColumnRange(r::AbstractString) !is_valid_column_range(r) && throw(XLSXError("Invalid column range: $r.")) - start_name, stop_name = split_column_range(r) + start_name, stop_name = split_sheet_range(r) return ColumnRange(decode_column_number(start_name), decode_column_number(stop_name)) end diff --git a/src/types.jl b/src/types.jl index 6b18f4c9..94e095bc 100644 --- a/src/types.jl +++ b/src/types.jl @@ -175,7 +175,11 @@ As a convenience, `@range_str` macro is provided. cr = XLSX.range"A1:C4" ``` =# -struct CellRange + +abstract type AbstractCellRange end +abstract type ContiguousCellRange end + +struct CellRange <: ContiguousCellRange start::CellRef stop::CellRef @@ -194,7 +198,7 @@ struct CellRange end end -struct ColumnRange +struct ColumnRange <: ContiguousCellRange start::Int # column number stop::Int # column number @@ -205,7 +209,7 @@ struct ColumnRange return new(a, b) end end -struct RowRange +struct RowRange <: ContiguousCellRange start::Int # row number stop::Int # row number @@ -222,21 +226,21 @@ struct SheetCellRef cellref::CellRef end -struct SheetCellRange +struct SheetCellRange <: ContiguousCellRange sheet::String rng::CellRange end -struct NonContiguousRange +struct NonContiguousRange <: AbstractCellRange sheet::String rng::Vector{Union{CellRef, CellRange}} end -struct SheetColumnRange +struct SheetColumnRange <: ContiguousCellRange sheet::String colrng::ColumnRange end -struct SheetRowRange +struct SheetRowRange <: ContiguousCellRange sheet::String rowrng::RowRange end diff --git a/src/worksheet.jl b/src/worksheet.jl index 04505cf9..85a71297 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -366,9 +366,34 @@ function getcell(ws::Worksheet, single::CellRef)::AbstractCell return EmptyCell(single) end getcell(ws::Worksheet, s::SheetCellRef) = getcell(ws, s.cellref) -getcell(ws::Worksheet, s::SheetCellRange) = getcell(ws, s.rng) -getcell(ws::Worksheet, s::SheetColumnRange) = getcell(ws, s.colrng) -getcell(ws::Worksheet, s::SheetRowRange) = getcell(ws, s.rowrng) +getcell(ws::Worksheet, s::SheetCellRange) = getcellrange(ws, s.rng) +getcell(ws::Worksheet, s::SheetColumnRange) = getcellrange(ws, s.colrng) +getcell(ws::Worksheet, s::SheetRowRange) = getcellrange(ws, s.rowrng) +getcell(ws::Worksheet, s::CellRange) = getcellrange(ws, s.rng) +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::Vector{Int}) = [getcell(ws, a, b) for a in collect(row), b in col] +getcell(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}) = [getcell(ws, a, b) for a in row, b in collect(col)] +getcell(ws::Worksheet, row::Vector{Int}, col::Vector{Int}) = [getcell(ws, a, b) for a in row, b in 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) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + getcellrange(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number))) + end +end +function getcell(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + getcellrange(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col)))) + end +end function getcell(ws::Worksheet, ref::AbstractString) if is_worksheet_defined_name(ws, ref) @@ -410,8 +435,6 @@ function getcell(ws::Worksheet, ref::AbstractString) 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) @@ -460,6 +483,13 @@ getcellrange(ws::Worksheet, s::SheetCellRange) = getcellrange(ws, s.rng) getcellrange(ws::Worksheet, s::SheetColumnRange) = getcellrange(ws, s.colrng) getcellrange(ws::Worksheet, s::SheetRowRange) = getcellrange(ws, s.rowrng) +getcellrange(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}) = [getcell(ws, a, b) for a in collect(row), b in col] +getcellrange(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}) = [getcell(ws, a, b) for a in row, b in collect(col)] +getcellrange(ws::Worksheet, row::Vector{Int}, col::Vector{Int}) = [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) + function getcellrange(ws::Worksheet, rng::ColumnRange)::Array{AbstractCell,2} dim = get_dimension(ws) if dim === nothing diff --git a/src/write.jl b/src/write.jl index bbf89447..3169b05a 100644 --- a/src/write.jl +++ b/src/write.jl @@ -518,6 +518,31 @@ xlsx_encode(ws::Worksheet, val::Dates.Date) = ("", string(date_to_excel_value(va 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, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) = setdata!(ws, row, :, v) +#Base.setindex!(ws::Worksheet, v, ::Colon, col::Union{Integer,UnitRange{<:Integer}}) = setdata!(ws, :, 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::Vector{Int}) + for a in collect(row), b in col + setdata!(ws, CellRef(a, b), v) + end +end +function Base.setindex!(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}) + for a in row, b in collect(col) + setdata!(ws, CellRef(a, b), v) + end +end +function Base.setindex!(ws::Worksheet, v, row::Vector{Int}, col::Vector{Int}) + 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("")) @@ -534,11 +559,6 @@ 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) function setdata!(ws::Worksheet, ref::CellRef, val::CellValueType) # use existing cell format if it exists @@ -572,8 +592,127 @@ function setdata!(ws::Worksheet, ref::CellRef, val::CellValueType) # use existin 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 gsetdata!(ws, SheetRowRange(ref), value) + elseif is_valid_non_contiguous_range(ref) + return setdata!(ws, NonContiguousRange(ref), 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) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + start = CellRef(rng.start, dim.start.column_number,) + stop = CellRef(rng.stop, dim.stop.column_number) + setdata!(ws, CellRange(start, stop), value) + end +end +function setdata!(ws::Worksheet, rng::ColumnRange, value) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + start = CellRef(dim.start.row_number, rng.start) + stop = CellRef(dim.stop.row_number, rng.stop) + setdata!(ws, CellRange(start, stop), value) + end +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 + psetdata!(ws, cell, value) + end + end + end +end +function setdata!(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon, v) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + setdata!(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)), v) + end +end +function setdata!(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}, v) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + setdata!(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))), v) + end +end +function setdata!(ws::Worksheet, row::Vector{Int}, ::Colon, v) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + for a in row + for b in dim.start.column_number:dim.stop.column_number + setdata!(ws, CellRef(a, b), v) + end + end + end +end +function setdata!(ws::Worksheet, ::Colon, col::Vector{Int}, v) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + for b in column + for a in dim.start.row_number:dim.stop.row_number + setdata!(ws, CellRef(a, b), v) + end + 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) = throw(XLSXError("Unsupported datatype $(typeof(value)) for writing data to Excel file. Supported data types are $(CellValueType) or $(CellValue).")) From 324cf68f3db66eb49427f2cfadbc36c05e9bdab0 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 30 Mar 2025 17:27:36 +0100 Subject: [PATCH 049/154] Extend indexing for `setAttribute` family of cell formatting functions. --- src/cellformats.jl | 126 +++++++++++++++++++++++++++++++++++++++++++++ src/write.jl | 3 +- 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/src/cellformats.jl b/src/cellformats.jl index d908b729..98cb5688 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -360,6 +360,62 @@ function process_cellranges(f::Function, ws::Worksheet, rng::CellRange; kw...):: end return -1 # Each cell may have a different attribute Id so we can't return a single value. end +function process_intcolon(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + f(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)); kw...) + end +end +function process_colonint(f::Function, ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + f(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))); kw...) + end +end +function process_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + for a in row + for b in dim.start.column_number:dim.stop.column_number + f(ws, CellRef(a, b); kw...) + end + end + end +end +function process_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + for b in col + for a in dim.start.row_number:dim.stop.row_number + f(ws, CellRef(a, b); kw...) + end + end + end +end +function process_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) + for a in collect(row), b in col + f(ws, CellRef(a, b); kw...) + end +end +function process_vecint(f::Function, ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) + for a in row, b in collect(col) + f(ws, CellRef(a, b), kw...) + end +end +function process_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) + for a in row, b in col + f(ws, CellRef(a, b); kw...) + end +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.")) @@ -556,6 +612,15 @@ setFont(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(setFont 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...) +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_intcolon(setFont, ws, row, : ; kw...) +setFont(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFont, ws, :, col; kw...) +setFont(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFont, ws, row, : ; kw...) +setFont(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_veccolon(setFont, ws, :, col; kw...) +setFont(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setFont, ws, row, col; kw...) +setFont(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFont, ws, row, col; kw...) +setFont(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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, @@ -749,6 +814,7 @@ 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; kw...) = getFont(ws, CellRef(row, col); kw...) 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} @@ -848,6 +914,7 @@ 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; kw...) = getBorder(ws, CellRef(row, col); kw...) 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} @@ -991,6 +1058,15 @@ Julia> setBorder(xf, "Sheet1!D4"; left = ["style" => "dotted", "color" => "F function setBorder end setBorder(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setBorder, ws, ncrng; outside) setBorder(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setBorder, ws, ref_or_rng; 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_intcolon(setBorder, ws, row, : ; kw...) +setBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setBorder, ws, :, col; kw...) +setBorder(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setBorder, ws, row, : ; kw...) +setBorder(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_veccolon(setBorder, ws, :, col; kw...) +setBorder(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setBorder, ws, row, col; kw...) +setBorder(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setBorder, ws, row, col; kw...) +setBorder(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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, @@ -1355,6 +1431,7 @@ 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; kw...) = getFill(ws, CellRef(row, col); kw...) 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} @@ -1469,6 +1546,15 @@ setFill(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges 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_intcolon(setFill, ws, row, : ; kw...) +setFill(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFill, ws, :, col; kw...) +setFill(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFill, ws, row, : ; kw...) +setFill(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_veccolon(setFill, ws, :, col; kw...) +setFill(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setFill, ws, row, col; kw...) +setFill(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFill, ws, row, col; kw...) +setFill(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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, @@ -1641,6 +1727,7 @@ 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; kw...) = getAlignment(ws, CellRef(row, col); kw...) #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} @@ -1722,6 +1809,15 @@ setAlignment(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(se 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...) +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_intcolon(setAlignment, ws, row, : ; kw...) +setAlignment(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setAlignment, ws, :, col; kw...) +setAlignment(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setAlignment, ws, row, : ; kw...) +setAlignment(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_veccolon(setAlignment, ws, :, col; kw...) +setAlignment(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setAlignment, ws, row, col; kw...) +setAlignment(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setAlignment, ws, row, col; kw...) +setAlignment(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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, @@ -1883,6 +1979,7 @@ 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; kw...) = getFormat(ws, CellRef(row, col); kw...) #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} @@ -1971,6 +2068,15 @@ setFormat(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncrang 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_intcolon(setFormat, ws, row, : ; kw...) +setFormat(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFormat, ws, :, col; kw...) +setFormat(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFormat, ws, row, : ; kw...) +setFormat(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_veccolon(setFormat, ws, :, col; kw...) +setFormat(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setFormat, ws, row, col; kw...) +setFormat(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFormat, ws, row, col; kw...) +setFormat(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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 @@ -2195,6 +2301,15 @@ setColumnWidth(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_n 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_intcolon(setColumnWidth, ws, row, : ; kw...) +setColumnWidth(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setColumnWidth, ws, :, col; kw...) +setColumnWidth(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setColumnWidth, ws, row, : ; kw...) +setColumnWidth(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_veccolon(setColumnWidth, ws, :, col; kw...) +setColumnWidth(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setColumnWidth, ws, row, col; kw...) +setColumnWidth(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setColumnWidth, ws, row, col; kw...) +setColumnWidth(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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 if !get_xlsxfile(ws).is_writable @@ -2288,6 +2403,7 @@ julia> XLSX.getColumnWidth(sh, "F1") 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; kw...) = getColumnWidth(ws, CellRef(row, col); kw...) function getColumnWidth(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real} # May be better if column width were part of ws.cache? @@ -2373,6 +2489,15 @@ setRowHeight(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncr 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}}, ::Colon; kw...) = process_intcolon(setRowHeight, ws, row, : ; kw...) +setRowHeight(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setRowHeight, ws, :, col; kw...) +setRowHeight(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setRowHeight, ws, row, : ; kw...) +setRowHeight(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_veccolon(setRowHeight, ws, :, col; kw...) +setRowHeight(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setRowHeight, ws, row, col; kw...) +setRowHeight(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setRowHeight, ws, row, col; kw...) +setRowHeight(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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 if !get_xlsxfile(ws).use_cache_for_sheet_data @@ -2432,6 +2557,7 @@ julia> XLSX.getRowHeight(sh, "F1") 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; kw...) = getRowHeight(ws, CellRef(row, col); kw...) function getRowHeight(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real} if !get_xlsxfile(ws).use_cache_for_sheet_data diff --git a/src/write.jl b/src/write.jl index 3169b05a..1247b4c6 100644 --- a/src/write.jl +++ b/src/write.jl @@ -559,6 +559,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) +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 @@ -702,7 +703,7 @@ function setdata!(ws::Worksheet, ::Colon, col::Vector{Int}, v) if dim === nothing throw(XLSXError("No worksheet dimension found")) else - for b in column + for b in col for a in dim.start.row_number:dim.stop.row_number setdata!(ws, CellRef(a, b), v) end From 76a08ecc25c16f4ae776afc2acb5dc62871d6eb2 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 30 Mar 2025 18:13:45 +0100 Subject: [PATCH 050/154] Add to docstrings. --- src/cellformats.jl | 89 +++++++++++++++++++++++++++++++++++++++++++--- src/worksheet.jl | 13 ++++++- 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/src/cellformats.jl b/src/cellformats.jl index 98cb5688..ab9c94b1 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -542,8 +542,13 @@ 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 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. @@ -602,7 +607,16 @@ julia> setFont(xf, "Sheet1!6:12"; italic=false, color="FF8888FF", under="none") julia> setFont(sh, "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="magenta") # all rows + + ``` """ function setFont end @@ -773,7 +787,9 @@ setUniformFont(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attr """ getFont(sh::Worksheet, cr::String) -> ::Union{Nothing, CellFont} getFont(xf::XLSXFile, cr::String) -> ::Union{Nothing, CellFont} - + + getFont(sh::Worksheet, row::Int, col::Int) -> ::Union{Nothing, CellFont} + Get the font used by a single cell at reference `cr` in a worksheet `sh` or XLSXfile `xf`. Return a `CellFont` object containing: @@ -854,6 +870,8 @@ 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. @@ -906,6 +924,8 @@ For example: . ```julia julia> getBorder(sh, "A1") +julia> getBorder(sh, 3, 6) + julia> getBorder(xf, "Sheet1!A1") ``` @@ -970,9 +990,14 @@ 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 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` @@ -1357,6 +1382,8 @@ 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. @@ -1423,6 +1450,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") ``` @@ -1482,9 +1511,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 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. @@ -1678,6 +1711,8 @@ 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. @@ -1719,6 +1754,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") ``` @@ -1755,9 +1792,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 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. @@ -1952,6 +1994,8 @@ 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. @@ -1972,6 +2016,8 @@ the format for built-in formats, too. julia> getFormat(sh, "A1") julia> getFormat(xf, "Sheet1!A1") + +julia> getFormat(sh, 1, 1) ``` """ @@ -2023,9 +2069,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 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 @@ -2260,11 +2310,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 @@ -2384,6 +2439,8 @@ 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. A standard cell reference or defined name may be used to define the column. @@ -2397,6 +2454,8 @@ does not have an explicitly defined width. julia> XLSX.getColumnWidth(xf, "Sheet1!A2") julia> XLSX.getColumnWidth(sh, "F1") + +julia> XLSX.getColumnWidth(sh, 1, 6) ``` """ @@ -2447,11 +2506,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. @@ -2536,6 +2599,8 @@ 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. A standard cell reference or defined name must be used to define the row. @@ -2551,6 +2616,8 @@ 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) ``` """ @@ -2645,6 +2712,8 @@ 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. Alternatively, if you have already obtained the merged cells for the worksheet, @@ -2654,13 +2723,18 @@ 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 + # 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) ``` @@ -2668,6 +2742,7 @@ 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...) #isMergedCell(ws::Worksheet, cellref::CellRef)::Bool = isMergedCell(ws, cellref, getMergedCells(ws)) function isMergedCell(ws::Worksheet, cellref::CellRef; mergedCells::Union{Vector{CellRange}, Nothing, Missing} = missing)::Bool @@ -2694,6 +2769,8 @@ 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. If the specified cell is not part of a merged cell range, return `nothing`. @@ -2710,6 +2787,8 @@ 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}} + # Examples: ```julia julia> XLSX.getMergedBaseCell(xf, "Sheet1!B2") @@ -2718,13 +2797,15 @@ julia> XLSX.getMergedBaseCell(xf, "Sheet1!B2") 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, cellref::CellRef) = getMergedBaseCell(ws, cellref, getMergedCells(ws)) +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).use_cache_for_sheet_data diff --git a/src/worksheet.jl b/src/worksheet.jl index 85a71297..0c8682c6 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -327,8 +327,11 @@ end """ getcell(sheet, ref) + getcell(sheet, row, col) -Returns an `AbstractCell` that represents a cell in the spreadsheet. +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. If `ref` is a range, `getcell` dispatches to `getcellrange`. @@ -340,7 +343,12 @@ 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 @@ -444,6 +452,9 @@ 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)) From deedc766854e9e7c9846349cf19ae0f02c3aa582 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 31 Mar 2025 07:49:00 +0100 Subject: [PATCH 051/154] Fix indexing issues --- src/cellformats.jl | 709 ++++++++++++++++++++++++++++++--------------- 1 file changed, 481 insertions(+), 228 deletions(-) diff --git a/src/cellformats.jl b/src/cellformats.jl index ab9c94b1..0ba724f3 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -3,61 +3,61 @@ const font_tags = ["b", "i", "u", "strike", "outline", "shadow", "condense", "ex 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 - ) + "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 +"""ix # # -- A bunch of helper functions first... # -function copynode(o::XML.Node) +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 @@ -105,7 +105,7 @@ function buildNode(tag::String, attributes::Dict{String,Union{Nothing,Dict{Strin end else#if k == "rgb" color[k] = v - #else + #else #throw(XLSXError("Incorect border attribute found: $k")) # shouldn't happen! end end @@ -161,7 +161,7 @@ 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 + if length(XML.children(new_cell_xf)) == 0 push!(new_cell_xf, alignment) else new_cell_xf[1] = alignment @@ -218,21 +218,21 @@ function process_sheetcell(f::Function, xl::XLSXFile, sheetcell::String; kw...): 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.")) + !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.")) + !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.")) + !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.")) + !hassheet(xl, ref.sheet) && throw(XLSXError("Sheet $(ref.sheet) not found.")) newid = f(getsheet(xl, ref.sheet), ref.cellref; kw...) - else + else throw(XLSXError("Invalid sheet cell reference: $sheetcell")) end return newid @@ -337,7 +337,7 @@ function process_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguousRange; @warn "No worksheet dimension found" [] else - OK = dim.start.column_number <= bounds.start.column_number + 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 @@ -450,28 +450,124 @@ function process_get_cellname(f::Function, ws::Worksheet, ref_or_rng::AbstractSt end return new_att end + +function process_uniform_core(f::Function, ws::Worksheet, 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, 0).id) + end + cell.style = string(update_template_xf(ws, CellDataFormat(parse(Int, cell.style)), atts, ["$newid", "1"]).id) + end + return newid, first +end +function process_uniform_core(f::Function, ws::Worksheet, cellref::CellRef, newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing}; kw...) # Alignment 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, 0).id) + end + cell.style = string(update_template_xf(ws, 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, atts::Vector{String}; kw...) if !get_xlsxfile(ws).use_cache_for_sheet_data - throw(XLSXError( "Cannot set uniform attributes because cache is not enabled.")) + throw(XLSXError("Cannot set uniform attributes because cache is not enabled.")) end - let newid + let newid::Union{Int,Nothing}, first::Bool + newid = nothing first = true for cellref in rng - cell = getcell(ws, cellref) - if cell isa EmptyCell # Can't add a attribute to an empty cell. - continue + newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) + end + if first + newid = -1 + end + return newid + end +end +function process_uniform_intcolon(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + f(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)); kw...) + end +end +function process_uniform_colonint(f::Function, ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + f(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))); kw...) + end +end +function process_uniform_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon, atts::Vector{String}; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + let newid::Union{Int,Nothing}, first::Bool + newid = nothing + first = true + for a in row + for b in dim.start.column_number:dim.stop.column_number + cellref = CellRef(a, b) + newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) + end 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) + if first + newid = -1 + end + return newid + end + end +end +function process_uniform_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}, atts::Vector{String}; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + let newid::Union{Int,Nothing}, first::Bool + newid = nothing + first = true + for b in col + for a in dim.start.row_number:dim.stop.row_number + cellref = CellRef(a, b) + newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) end - cell.style = string(update_template_xf(ws, CellDataFormat(parse(Int, cell.style)), atts, ["$newid", "1"]).id) end + if first + newid = -1 + end + return newid + end + end +end +function process_uniform_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}, atts::Vector{String}; kw...) + let newid::Union{Int,Nothing}, first::Bool + newid = nothing + first = true + for a in collect(row), b in col + cellref = CellRef(a, b).name + newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) end if first newid = -1 @@ -479,31 +575,136 @@ function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange, a return newid end end +function process_uniform_vecint(f::Function, ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}, atts::Vector{String}; kw...) + let newid::Union{Int,Nothing}, first::Bool + newid = nothing + first = true + for a in row, b in collect(col) + cellref = CellRef(a, b) + newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) + end + if first + newid = -1 + end + return newid + end +end +function process_uniform_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, col::Vector{Int}, atts::Vector{String}; kw...) + let newid::Union{Int,Nothing}, first::Bool + newid = nothing + first = true + for a in row, b in col + cellref = CellRef(a, b) + newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) + end + if first + newid = -1 + end + return newid + end +end + -function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange; kw...) +function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange; kw...) # Alignment is different if !get_xlsxfile(ws).use_cache_for_sheet_data - throw(XLSXError( "Cannot set uniform attributes because cache is not enabled.")) + throw(XLSXError("Cannot set uniform attributes because cache is not enabled.")) end - let newid, alignment_node + let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} + newid = nothing first = true + alignment_node = nothing for cellref in rng - cell = getcell(ws, cellref) - if cell isa EmptyCell # Can't add a attribute to an empty cell. - continue + newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) + end + if first + newid = -1 + end + return newid + end +end +function process_uniform_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + 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) + newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) + end 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) + if first + newid = -1 + end + return newid + end + end +end +function process_uniform_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} + newid = nothing + first = true + alignment_node = nothing + for b in col + for a in dim.start.row_number:dim.stop.row_number + cellref = CellRef(a, b) + newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) end - cell.style = string(update_template_xf(ws, CellDataFormat(parse(Int, cell.style)), alignment_node).id) end + if first + newid = -1 + end + return newid + end + end +end +function process_uniform_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) + let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} + newid = nothing + first = true + alignment_node = nothing + for a in collect(row), b in col + cellref = CellRef(a, b) + newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) + end + if first + newid = -1 + end + return newid + end +end +function process_uniform_vecint(f::Function, ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) + let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} + newid = nothing + first = true + alignment_node = nothing + for a in row, b in collect(col) + cellref = CellRef(a, b) + newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) + end + if first + newid = -1 + end + return newid + end +end +function process_uniform_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) + let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} + newid = nothing + first = true + for a in row, b in col + cellref = CellRef(a, b) + newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) end if first newid = -1 @@ -517,7 +718,7 @@ function get_colorant(color_string::String) try c = Colors.parse(Colors.Colorant, color_string) rgb = Colors.hex(c, :RRGGBB) - return "FF"*rgb + return "FF" * rgb catch return nothing end @@ -627,34 +828,34 @@ setFont(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges 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...) 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_intcolon(setFont, ws, row, : ; kw...) +setFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_intcolon(setFont, ws, row, :; kw...) setFont(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFont, ws, :, col; kw...) -setFont(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFont, ws, row, : ; kw...) -setFont(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_veccolon(setFont, ws, :, col; kw...) +setFont(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFont, ws, row, :; kw...) +setFont(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setFont, ws, :, col; kw...) setFont(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setFont, ws, row, col; kw...) -setFont(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFont, ws, row, col; kw...) -setFont(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(setFont, ws, row, col; kw...) +setFont(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFont, ws, row, col; kw...) +setFont(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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 +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.")) + throw(XLSXError("Cannot set font because cache is not enabled.")) end wb = get_workbook(sh) cell = getcell(sh, cellref) - if cell isa EmptyCell + if cell isa EmptyCell throw(XLSXError("Cannot set attribute for an `EmptyCell`: $(cellref.name). Set the value first.")) - end + end if cell.style == "" cell.style = string(get_num_style_index(sh, 0).id) @@ -781,6 +982,14 @@ setUniformFont(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges( setUniformFont(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setUniformFont, ws, ncrng; 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_uniform_intcolon(setFont, ws, row, :; kw...) +setUniformFont(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setFont, ws, :, col; kw...) +setUniformFont(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setFont, ws, row, :, ["fontId", "applyFont"]; kw...) +setUniformFont(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setFont, ws, :, col, ["fontId", "applyFont"]; kw...) +setUniformFont(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setFont, ws, row, col, ["fontId", "applyFont"]; kw...) +setUniformFont(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFont, ws, row, col, ["fontId", "applyFont"]; kw...) +setUniformFont(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setFont, ws, row, col, ["fontId", "applyFont"]; kw...) +setUniformFont(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...) setUniformFont(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attribute(setFont, ws, rng, ["fontId", "applyFont"]; kw...) @@ -1084,94 +1293,94 @@ function setBorder end setBorder(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setBorder, ws, ncrng; outside) setBorder(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setBorder, ws, ref_or_rng; 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_intcolon(setBorder, ws, row, : ; kw...) +setBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_intcolon(setBorder, ws, row, :; kw...) setBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setBorder, ws, :, col; kw...) -setBorder(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setBorder, ws, row, : ; kw...) -setBorder(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_veccolon(setBorder, ws, :, col; kw...) +setBorder(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setBorder, ws, row, :; kw...) +setBorder(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setBorder, ws, :, col; kw...) setBorder(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setBorder, ws, row, col; kw...) -setBorder(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setBorder, ws, row, col; kw...) -setBorder(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(setBorder, ws, row, col; kw...) +setBorder(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setBorder, ws, row, col; kw...) +setBorder(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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 +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]) + 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 +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]) + 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 +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]) + 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 +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]) + 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.")) @@ -1220,7 +1429,7 @@ function setBorder(sh::Worksheet, cellref::CellRef; 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 + end new_border_atts[a]["style"] = kwdict[a]["style"] end if a == "diagonal" @@ -1310,6 +1519,14 @@ setUniformBorder(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowrange setUniformBorder(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setUniformBorder, ws, ncrng; 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_uniform_intcolon(setBorder, ws, row, :; kw...) +setUniformBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setBorder, ws, :, col; kw...) +setUniformBorder(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setBorder, ws, row, :, ["borderId", "applyBorder"]; kw...) +setUniformBorder(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setBorder, ws, :, col, ["borderId", "applyBorder"]; kw...) +setUniformBorder(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...) +setUniformBorder(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...) +setUniformBorder(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...) +setUniformBorder(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...) setUniformBorder(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attribute(setBorder, ws, rng, ["borderId", "applyBorder"]; kw...) """ @@ -1349,9 +1566,12 @@ setOutsideBorder(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_colum 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; - outside::Union{Nothing,Vector{Pair{String,String}}}=nothing, - )::Int +setOutsideBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_uniform_intcolon(setOutsideBorder, ws, row, :; kw...) +setOutsideBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setOutsideBorder, ws, :, col; kw...) +setOutsideBorder(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))), ["borderId", "applyBorder"]; 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.")) @@ -1361,15 +1581,15 @@ function setOutsideBorder(ws::Worksheet, rng::CellRange; 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) + 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) + 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 @@ -1580,19 +1800,19 @@ setFill(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(s 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_intcolon(setFill, ws, row, : ; kw...) +setFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_intcolon(setFill, ws, row, :; kw...) setFill(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFill, ws, :, col; kw...) -setFill(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFill, ws, row, : ; kw...) -setFill(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_veccolon(setFill, ws, :, col; kw...) +setFill(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFill, ws, row, :; kw...) +setFill(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setFill, ws, :, col; kw...) setFill(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setFill, ws, row, col; kw...) -setFill(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFill, ws, row, col; kw...) -setFill(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(setFill, ws, row, col; kw...) +setFill(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFill, ws, row, col; kw...) +setFill(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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 if !get_xlsxfile(sh).use_cache_for_sheet_data throw(XLSXError("Cannot set fill because cache is not enabled.")) @@ -1610,7 +1830,7 @@ function setFill(sh::Worksheet, cellref::CellRef; end cell_style = styles_cell_xf(wb, parse(Int, cell.style)) - + new_fill_atts = Dict{String,Union{Dict{String,String},Nothing}}() patternFill = Dict{String,String}() @@ -1702,6 +1922,14 @@ setUniformFill(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges( setUniformFill(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setUniformFill, ws, ncrng; 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_uniform_intcolon(setFill, ws, row, :; kw...) +setUniformFill(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setFill, ws, :, col; kw...) +setUniformFill(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setFill, ws, row, :, ["fillId", "applyFill"]; kw...) +setUniformFill(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setFill, ws, :, col, ["fillId", "applyFill"]; kw...) +setUniformFill(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setFill, ws, row, col, ["fillId", "applyFill"]; kw...) +setUniformFill(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFill, ws, row, col, ["fillId", "applyFill"]; kw...) +setUniformFill(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setFill, ws, row, col, ["fillId", "applyFill"]; kw...) +setUniformFill(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...) setUniformFill(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attribute(setFill, ws, rng, ["fillId", "applyFill"]; kw...) # @@ -1781,7 +2009,7 @@ function getAlignment(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellAli 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 @@ -1852,22 +2080,22 @@ setAlignment(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncr 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...) 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_intcolon(setAlignment, ws, row, : ; kw...) +setAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_intcolon(setAlignment, ws, row, :; kw...) setAlignment(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setAlignment, ws, :, col; kw...) -setAlignment(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setAlignment, ws, row, : ; kw...) -setAlignment(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_veccolon(setAlignment, ws, :, col; kw...) +setAlignment(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setAlignment, ws, row, :; kw...) +setAlignment(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setAlignment, ws, :, col; kw...) setAlignment(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setAlignment, ws, row, col; kw...) -setAlignment(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setAlignment, ws, row, col; kw...) -setAlignment(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(setAlignment, ws, row, col; kw...) +setAlignment(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setAlignment, ws, row, col; kw...) +setAlignment(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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; +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 if !get_xlsxfile(sh).use_cache_for_sheet_data throw(XLSXError("Cannot set alignment because cache is not enabled.")) @@ -1985,6 +2213,14 @@ setUniformAlignment(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowra setUniformAlignment(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setUniformAlignment, 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_uniform_intcolon(setAlignment, ws, row, :; kw...) +setUniformAlignment(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setAlignment, ws, :, col; kw...) +setUniformAlignment(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setAlignment, ws, row, :; kw...) +setUniformAlignment(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setAlignment, ws, :, col; kw...) +setUniformAlignment(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setAlignment, ws, row, col; kw...) +setUniformAlignment(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setAlignment, ws, row, col; kw...) +setUniformAlignment(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setAlignment, ws, row, col; kw...) +setUniformAlignment(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...) setUniformAlignment(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attribute(setAlignment, ws, rng; kw...) # @@ -2051,7 +2287,7 @@ function getFormat(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellFormat format_atts[XML.tag(current_format)] = Dict(k => XML.unescape(v)) end else -# any(num in r for r in ranges) + # any(num in r for r in ranges) ranges = [0:22, 37:40, 45:49] 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.")) @@ -2119,17 +2355,17 @@ setFormat(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(setFo 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_intcolon(setFormat, ws, row, : ; kw...) +setFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_intcolon(setFormat, ws, row, :; kw...) setFormat(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFormat, ws, :, col; kw...) -setFormat(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFormat, ws, row, : ; kw...) -setFormat(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_veccolon(setFormat, ws, :, col; kw...) +setFormat(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFormat, ws, row, :; kw...) +setFormat(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setFormat, ws, :, col; kw...) setFormat(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setFormat, ws, row, col; kw...) -setFormat(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFormat, ws, row, col; kw...) -setFormat(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(setFormat, ws, row, col; kw...) +setFormat(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFormat, ws, row, col; kw...) +setFormat(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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 if !get_xlsxfile(sh).use_cache_for_sheet_data throw(XLSXError("Cannot set number formats because cache is not enabled.")) @@ -2147,8 +2383,8 @@ function setFormat(sh::Worksheet, cellref::CellRef; end cell_style = styles_cell_xf(wb, parse(Int, cell.style)) - -# new_format_atts = Dict{String,Union{Dict{String,String},Nothing}}() + + # new_format_atts = Dict{String,Union{Dict{String,String},Nothing}}() new_format = XML.OrderedDict{String,String}() cell_format = getFormat(wb, cell_style) @@ -2164,10 +2400,10 @@ function setFormat(sh::Worksheet, cellref::CellRef; 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)) + if !occursin(floatformats, code) && !any(map(x -> occursin(x, code), DATETIME_CODES)) 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 @@ -2179,8 +2415,8 @@ function setFormat(sh::Worksheet, cellref::CellRef; end format_node = XML.Element("numFmt"; - numFmtId = string(existing_elements_count + PREDEFINED_NUMFMT_COUNT), - formatCode = XML.escape(format) + numFmtId=string(existing_elements_count + PREDEFINED_NUMFMT_COUNT), + formatCode=XML.escape(format) ) new_formatid = styles_add_cell_attribute(wb, format_node, "numFmts") + PREDEFINED_NUMFMT_COUNT @@ -2196,7 +2432,7 @@ function setFormat(sh::Worksheet, cellref::CellRef; end newstyle = string(update_template_xf(sh, CellDataFormat(parse(Int, cell.style)), atts, vals).id) cell.style = newstyle - + return new_formatid end @@ -2235,6 +2471,14 @@ setUniformFormat(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowrange setUniformFormat(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setUniformFormat, ws, ncrng; 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, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_uniform_intcolon(setFormat, ws, row, :; kw...) +setUniformFormat(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setFormat, ws, :, col; kw...) +setUniformFormat(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setFormat, ws, row, :, ["numFmtId", "applyNumberFormat"]; kw...) +setUniformFormat(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setFormat, ws, :, col, ["numFmtId", "applyNumberFormat"]; kw...) +setUniformFormat(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...) +setUniformFormat(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...) +setUniformFormat(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...) +setUniformFormat(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...) setUniformFormat(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attribute(setFormat, ws, rng, ["numFmtId", "applyNumberFormat"]; kw...) # @@ -2271,18 +2515,27 @@ julia> XLSX.setUniformStyle(sh, "F1:F5") """ function setUniformStyle end setUniformStyle(ws::Worksheet, colrng::ColumnRange)::Int = process_columnranges(setUniformStyle, ws, colrng) -setUniformStyle(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(setUniformStyle, ws, rowrng; kw...) -setUniformStyle(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setUniformStyle, ws, ncrng; kw...) +setUniformStyle(ws::Worksheet, rowrng::RowRange)::Int = process_rowranges(setUniformStyle, ws, rowrng) +setUniformStyle(ws::Worksheet, ncrng::NonContiguousRange)::Int = process_ncranges(setUniformStyle, 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_uniform_intcolon(setUniformStyle, ws, row, :) +setUniformStyle(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setUniformStyle, ws, :, col) +setUniformStyle(ws::Worksheet, row::Vector{Int}, ::Colon) = process_uniform_veccolon(setUniformStyle, ws, row, :) +setUniformStyle(ws::Worksheet, ::Colon, col::Vector{Int}) = process_uniform_colonvec(setUniformStyle, ws, :, col) +setUniformStyle(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}) = process_uniform_intvec(setUniformtyle, ws, row, col) +setUniformStyle(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}) = process_uniform_vecint(setUniformStyle, ws, row, col) +setUniformStyle(ws::Worksheet, row::Vector{Int}, col::Vector{Int}) = process_uniform_vecvec(setUniformStyle, 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 - let newid::Union{Nothing, Int}, + 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. @@ -2300,7 +2553,7 @@ function setUniformStyle(ws::Worksheet, rng::CellRange)::Union{Nothing, Int} end return isnothing(newid) ? nothing : newid end -end +end # # -- Get and set column width @@ -2357,24 +2610,24 @@ setColumnWidth(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ 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_intcolon(setColumnWidth, ws, row, : ; kw...) +setColumnWidth(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_intcolon(setColumnWidth, ws, row, :; kw...) setColumnWidth(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setColumnWidth, ws, :, col; kw...) -setColumnWidth(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setColumnWidth, ws, row, : ; kw...) -setColumnWidth(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_veccolon(setColumnWidth, ws, :, col; kw...) +setColumnWidth(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setColumnWidth, ws, row, :; kw...) +setColumnWidth(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setColumnWidth, ws, :, col; kw...) setColumnWidth(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setColumnWidth, ws, row, col; kw...) -setColumnWidth(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setColumnWidth, ws, row, col; kw...) -setColumnWidth(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(setColumnWidth, ws, row, col; kw...) +setColumnWidth(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setColumnWidth, ws, row, col; kw...) +setColumnWidth(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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 - + if !get_xlsxfile(ws).is_writable throw(XLSXError("Cannot set column widths: `XLSXFile` is not writable.")) end # 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)) + update_worksheets_xml!(get_xlsxfile(ws)) - 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 if !isnothing(width) && width < 0 @@ -2399,7 +2652,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}}() @@ -2415,7 +2668,7 @@ 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 @@ -2476,7 +2729,7 @@ function getColumnWidth(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real} end # 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)) + update_worksheets_xml!(get_xlsxfile(ws)) sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet$(ws.sheetId).xml") # find the block in the worksheet's xml file i, j = get_idces(sheetdoc, "worksheet", "cols") @@ -2553,13 +2806,13 @@ setRowHeight(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ra 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}}, ::Colon; kw...) = process_intcolon(setRowHeight, ws, row, : ; kw...) +setRowHeight(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_intcolon(setRowHeight, ws, row, :; kw...) setRowHeight(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setRowHeight, ws, :, col; kw...) -setRowHeight(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setRowHeight, ws, row, : ; kw...) -setRowHeight(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_veccolon(setRowHeight, ws, :, col; kw...) +setRowHeight(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setRowHeight, ws, row, :; kw...) +setRowHeight(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setRowHeight, ws, :, col; kw...) setRowHeight(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setRowHeight, ws, row, col; kw...) -setRowHeight(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setRowHeight, ws, row, col; kw...) -setRowHeight(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(setRowHeight, ws, row, col; kw...) +setRowHeight(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setRowHeight, ws, row, col; kw...) +setRowHeight(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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 @@ -2567,7 +2820,7 @@ function setRowHeight(ws::Worksheet, rng::CellRange; height::Union{Nothing,Real} 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 if !isnothing(height) && height < 0 @@ -2580,12 +2833,12 @@ function setRowHeight(ws::Worksheet, rng::CellRange; height::Union{Nothing,Real} 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 - + end end @@ -2638,11 +2891,11 @@ function getRowHeight(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real} 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 - + end end @@ -2678,7 +2931,7 @@ julia> XLSX.getMergedCells(s) ``` """ -function getMergedCells(ws::Worksheet)::Union{Vector{CellRange}, Nothing} +function getMergedCells(ws::Worksheet)::Union{Vector{CellRange},Nothing} # May be better if merged cells were part of ws.cache? if !get_xlsxfile(ws).use_cache_for_sheet_data @@ -2744,14 +2997,14 @@ isMergedCell(xl::XLSXFile, sheetcell::String; kw...)::Bool = process_get_sheetce 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...) #isMergedCell(ws::Worksheet, cellref::CellRef)::Bool = isMergedCell(ws, cellref, getMergedCells(ws)) -function isMergedCell(ws::Worksheet, cellref::CellRef; mergedCells::Union{Vector{CellRange}, Nothing, Missing} = missing)::Bool - +function isMergedCell(ws::Worksheet, cellref::CellRef; mergedCells::Union{Vector{CellRange},Nothing,Missing}=missing)::Bool + if !get_xlsxfile(ws).use_cache_for_sheet_data throw(XLSXError("Cannot get merged cells because cache is not enabled.")) end if ismissing(mergedCells) # Get mergedCells if missing - mergedCells=getMergedCells(ws) + mergedCells = getMergedCells(ws) end if isnothing(mergedCells) # No merged cells in sheet return false @@ -2806,21 +3059,21 @@ 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) +function getMergedBaseCell(ws::Worksheet, cellref::CellRef; mergedCells::Union{Vector{CellRange},Nothing,Missing}=missing) if !get_xlsxfile(ws).use_cache_for_sheet_data throw(XLSXError("Cannot get merged cells because cache is not enabled.")) end if ismissing(mergedCells) # Get mergedCells if missing - mergedCells=getMergedCells(ws) + 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]) + return (; baseCell=rng.start, baseValue=ws[rng.start]) end end return nothing From 0f32cf9d592e8ea04ad1f0c6907f81e39c65af68 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 31 Mar 2025 15:58:38 +0100 Subject: [PATCH 052/154] Split cellformat-helpers.jl into separate file --- src/XLSX.jl | 1 + src/cellformat-helpers.jl | 841 ++++++++++++++++++++++++++++++++++++++ src/cellformats.jl | 747 +-------------------------------- 3 files changed, 844 insertions(+), 745 deletions(-) create mode 100644 src/cellformat-helpers.jl diff --git a/src/XLSX.jl b/src/XLSX.jl index 6e1c4c8a..990f9f70 100644 --- a/src/XLSX.jl +++ b/src/XLSX.jl @@ -29,6 +29,7 @@ include("worksheet.jl") include("cell.jl") include("styles.jl") include("cellformats.jl") +include("cellformat-helpers.jl") include("write.jl") end # module XLSX diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl new file mode 100644 index 00000000..3db28943 --- /dev/null +++ b/src/cellformat-helpers.jl @@ -0,0 +1,841 @@ + +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.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 + 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#if k == "rgb" + color[k] = v + #else + #throw(XLSXError("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 + 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 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, 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])) + 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 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) + 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_ranges(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) + colrng = ColumnRange(ref_or_rng) + newid = f(ws, colrng; kw...) + elseif is_valid_row_range(ref_or_rng) + rowrng = RowRange(ref_or_rng) + newid = f(ws, rowrng; 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 + 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)) + return if dim === nothing + @warn "No worksheet dimension found" + [] + else + 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 +end +function process_rowranges(f::Function, ws::Worksheet, rowrng::RowRange; kw...)::Int + bounds = row_bounds(rowrng) + dim = (get_dimension(ws)) + return if dim === nothing + @warn "No worksheet dimension found" + [] + else + 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 +end +function process_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int + bounds = nc_bounds(ncrng) + dim = (get_dimension(ws)) + return if dim === nothing + @warn "No worksheet dimension found" + [] + else + 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 + _ = 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 +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; kw...) + ref = SheetCellRef(sheetcell) + !hassheet(xl, ref.sheet) && throw(XLSXError("Sheet $(ref.sheet) not found.")) + return f(getsheet(xl, ref.sheet), ref.cellref; kw...) +end +function process_get_cellref(f::Function, ws::Worksheet, cellref::CellRef; kw...) + 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; 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) # Can these have fonts? + throw(XLSXError("Can only assign borders 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 or range: $ref_or_rng")) + end + return new_att +end + +# +# - Used for indexing `setAttribute` family of functions +# +function process_intcolon(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + f(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)); kw...) + end +end +function process_colonint(f::Function, ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + f(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))); kw...) + end +end +function process_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + for a in row + for b in dim.start.column_number:dim.stop.column_number + f(ws, CellRef(a, b); kw...) + end + end + end +end +function process_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + for b in col + for a in dim.start.row_number:dim.stop.row_number + f(ws, CellRef(a, b); kw...) + end + end + end +end +function process_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) + for a in collect(row), b in col + f(ws, CellRef(a, b); kw...) + end +end +function process_vecint(f::Function, ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) + for a in row, b in collect(col) + f(ws, CellRef(a, b), kw...) + end +end +function process_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) + for a in row, b in col + f(ws, CellRef(a, b); kw...) + end +end + +# +# - Used for indexing `setUniformAttribute` family of functions +# +function process_uniform_core(f::Function, ws::Worksheet, cellref::CellRef, atts::Vector{String}, newid::Union{Int,Nothing}, first::Bool; kw...) # Most functions in set + 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, 0).id) + end + cell.style = string(update_template_xf(ws, CellDataFormat(parse(Int, cell.style)), atts, ["$newid", "1"]).id) + end + return newid, first +end +function process_uniform_core(f::Function, ws::Worksheet, 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, 0).id) + end + cell.style = string(update_template_xf(ws, CellDataFormat(parse(Int, cell.style)), alignment_node).id) + end + return newid, first, alignment_node +end +function process_uniform_core(ws::Worksheet, cellref::CellRef, newid::Union{Int,Nothing}, first::Bool) # setUniformStyle is different, too + 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_attribute(f::Function, ws::Worksheet, rng::CellRange, atts::Vector{String}; kw...) # Most set functions + + if !get_xlsxfile(ws).use_cache_for_sheet_data + throw(XLSXError("Cannot set uniform attributes because cache is not enabled.")) + end + + let newid::Union{Int,Nothing}, first::Bool + newid = nothing + first = true + for cellref in rng + newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) + end + if first + newid = -1 + end + return newid + end +end +function process_uniform_intcolon(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + f(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)); kw...) + end +end +function process_uniform_colonint(f::Function, ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + f(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))); kw...) + end +end +function process_uniform_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon, atts::Vector{String}; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + let newid::Union{Int,Nothing}, first::Bool + newid = nothing + first = true + for a in row + for b in dim.start.column_number:dim.stop.column_number + cellref = CellRef(a, b) + newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) + end + end + if first + newid = -1 + end + return newid + end + end +end +function process_uniform_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}, atts::Vector{String}; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + let newid::Union{Int,Nothing}, first::Bool + newid = nothing + first = true + for b in col + for a in dim.start.row_number:dim.stop.row_number + cellref = CellRef(a, b) + newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) + end + end + if first + newid = -1 + end + return newid + end + end +end +function process_uniform_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}, atts::Vector{String}; kw...) + let newid::Union{Int,Nothing}, first::Bool + newid = nothing + first = true + for a in collect(row), b in col + cellref = CellRef(a, b).name + newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) + end + if first + newid = -1 + end + return newid + end +end +function process_uniform_vecint(f::Function, ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}, atts::Vector{String}; kw...) + let newid::Union{Int,Nothing}, first::Bool + newid = nothing + first = true + for a in row, b in collect(col) + cellref = CellRef(a, b) + newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) + end + if first + newid = -1 + end + return newid + end +end +function process_uniform_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, col::Vector{Int}, atts::Vector{String}; kw...) + let newid::Union{Int,Nothing}, first::Bool + newid = nothing + first = true + for a in row, b in col + cellref = CellRef(a, b) + newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) + end + if first + newid = -1 + end + return newid + end +end + +# UniformStyles +function process_uniform_veccolon(ws::Worksheet, row::Vector{Int}, ::Colon, atts::Vector{String}; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + let newid::Union{Int,Nothing}, first::Bool + newid = nothing + first = true + for a in row + for b in dim.start.column_number:dim.stop.column_number + cellref = CellRef(a, b) + newid, first = process_uniform_core(ws, cellref, atts, newid, first; kw...) + end + end + if first + newid = -1 + end + return newid + end + end +end +function process_uniform_colonvec(ws::Worksheet, ::Colon, col::Vector{Int}, atts::Vector{String}; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + let newid::Union{Int,Nothing}, first::Bool + newid = nothing + first = true + for b in col + for a in dim.start.row_number:dim.stop.row_number + cellref = CellRef(a, b) + newid, first = process_uniform_core(ws, cellref, atts, newid, first; kw...) + end + end + if first + newid = -1 + end + return newid + end + end +end +function process_uniform_intvec(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}, atts::Vector{String}; kw...) + let newid::Union{Int,Nothing}, first::Bool + newid = nothing + first = true + for a in collect(row), b in col + cellref = CellRef(a, b).name + newid, first = process_uniform_core(ws, cellref, atts, newid, first; kw...) + end + if first + newid = -1 + end + return newid + end +end +function process_uniform_vecint(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}, atts::Vector{String}; kw...) + let newid::Union{Int,Nothing}, first::Bool + newid = nothing + first = true + for a in row, b in collect(col) + cellref = CellRef(a, b) + newid, first = process_uniform_core(ws, cellref, atts, newid, first; kw...) + end + if first + newid = -1 + end + return newid + end +end +function process_uniform_vecvec(ws::Worksheet, row::Vector{Int}, col::Vector{Int}, atts::Vector{String}; kw...) + let newid::Union{Int,Nothing}, first::Bool + newid = nothing + first = true + for a in row, b in col + cellref = CellRef(a, b) + newid, first = process_uniform_core(ws, cellref, atts, newid, first; kw...) + end + if first + newid = -1 + end + return newid + end +end + + +function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange; kw...) # Alignment is different + + if !get_xlsxfile(ws).use_cache_for_sheet_data + throw(XLSXError("Cannot set uniform attributes because cache is not enabled.")) + end + + let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} + newid = nothing + first = true + alignment_node = nothing + for cellref in rng + newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) + end + if first + newid = -1 + end + return newid + end +end +function process_uniform_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + 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) + newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) + end + end + if first + newid = -1 + end + return newid + end + end +end +function process_uniform_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} + newid = nothing + first = true + alignment_node = nothing + for b in col + for a in dim.start.row_number:dim.stop.row_number + cellref = CellRef(a, b) + newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) + end + end + if first + newid = -1 + end + return newid + end + end +end +function process_uniform_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) + let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} + newid = nothing + first = true + alignment_node = nothing + for a in collect(row), b in col + cellref = CellRef(a, b) + newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) + end + if first + newid = -1 + end + return newid + end +end +function process_uniform_vecint(f::Function, ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) + let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} + newid = nothing + first = true + alignment_node = nothing + for a in row, b in collect(col) + cellref = CellRef(a, b) + newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) + end + if first + newid = -1 + end + return newid + end +end +function process_uniform_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) + let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} + newid = nothing + first = true + for a in row, b in col + cellref = CellRef(a, b) + newid, first, alignment_node = process_uniform_core(f, ws, 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 AARRGGBB")) + end + return c +end diff --git a/src/cellformats.jl b/src/cellformats.jl index 0ba724f3..0c1dab53 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -1,739 +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 - 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#if k == "rgb" - color[k] = v - #else - #throw(XLSXError("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 - 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 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, 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])) - 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 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) - 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_ranges(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) - colrng = ColumnRange(ref_or_rng) - newid = f(ws, colrng; kw...) - elseif is_valid_row_range(ref_or_rng) - rowrng = RowRange(ref_or_rng) - newid = f(ws, rowrng; 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 - 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)) - return if dim === nothing - @warn "No worksheet dimension found" - [] - else - 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 -end -function process_rowranges(f::Function, ws::Worksheet, rowrng::RowRange; kw...)::Int - bounds = row_bounds(rowrng) - dim = (get_dimension(ws)) - return if dim === nothing - @warn "No worksheet dimension found" - [] - else - 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 -end -function process_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int - bounds = nc_bounds(ncrng) - dim = (get_dimension(ws)) - return if dim === nothing - @warn "No worksheet dimension found" - [] - else - 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 - _ = 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 -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_intcolon(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - f(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)); kw...) - end -end -function process_colonint(f::Function, ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - f(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))); kw...) - end -end -function process_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - for a in row - for b in dim.start.column_number:dim.stop.column_number - f(ws, CellRef(a, b); kw...) - end - end - end -end -function process_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - for b in col - for a in dim.start.row_number:dim.stop.row_number - f(ws, CellRef(a, b); kw...) - end - end - end -end -function process_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) - for a in collect(row), b in col - f(ws, CellRef(a, b); kw...) - end -end -function process_vecint(f::Function, ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) - for a in row, b in collect(col) - f(ws, CellRef(a, b), kw...) - end -end -function process_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) - for a in row, b in col - f(ws, CellRef(a, b); kw...) - end -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.")) - return f(getsheet(xl, ref.sheet), ref.cellref; kw...) -end -function process_get_cellref(f::Function, ws::Worksheet, cellref::CellRef; kw...) - 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; 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) # Can these have fonts? - throw(XLSXError("Can only assign borders 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 or range: $ref_or_rng")) - end - return new_att -end - -function process_uniform_core(f::Function, ws::Worksheet, 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, 0).id) - end - cell.style = string(update_template_xf(ws, CellDataFormat(parse(Int, cell.style)), atts, ["$newid", "1"]).id) - end - return newid, first -end -function process_uniform_core(f::Function, ws::Worksheet, cellref::CellRef, newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing}; kw...) # Alignment 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, 0).id) - end - cell.style = string(update_template_xf(ws, 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, 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 - - let newid::Union{Int,Nothing}, first::Bool - newid = nothing - first = true - for cellref in rng - newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) - end - if first - newid = -1 - end - return newid - end -end -function process_uniform_intcolon(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - f(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)); kw...) - end -end -function process_uniform_colonint(f::Function, ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - f(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))); kw...) - end -end -function process_uniform_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon, atts::Vector{String}; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - let newid::Union{Int,Nothing}, first::Bool - newid = nothing - first = true - for a in row - for b in dim.start.column_number:dim.stop.column_number - cellref = CellRef(a, b) - newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) - end - end - if first - newid = -1 - end - return newid - end - end -end -function process_uniform_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}, atts::Vector{String}; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - let newid::Union{Int,Nothing}, first::Bool - newid = nothing - first = true - for b in col - for a in dim.start.row_number:dim.stop.row_number - cellref = CellRef(a, b) - newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) - end - end - if first - newid = -1 - end - return newid - end - end -end -function process_uniform_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}, atts::Vector{String}; kw...) - let newid::Union{Int,Nothing}, first::Bool - newid = nothing - first = true - for a in collect(row), b in col - cellref = CellRef(a, b).name - newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) - end - if first - newid = -1 - end - return newid - end -end -function process_uniform_vecint(f::Function, ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}, atts::Vector{String}; kw...) - let newid::Union{Int,Nothing}, first::Bool - newid = nothing - first = true - for a in row, b in collect(col) - cellref = CellRef(a, b) - newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) - end - if first - newid = -1 - end - return newid - end -end -function process_uniform_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, col::Vector{Int}, atts::Vector{String}; kw...) - let newid::Union{Int,Nothing}, first::Bool - newid = nothing - first = true - for a in row, b in col - cellref = CellRef(a, b) - newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) - end - if first - newid = -1 - end - return newid - end -end - - -function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange; kw...) # Alignment is different - - if !get_xlsxfile(ws).use_cache_for_sheet_data - throw(XLSXError("Cannot set uniform attributes because cache is not enabled.")) - end - - let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} - newid = nothing - first = true - alignment_node = nothing - for cellref in rng - newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) - end - if first - newid = -1 - end - return newid - end -end -function process_uniform_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - 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) - newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) - end - end - if first - newid = -1 - end - return newid - end - end -end -function process_uniform_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} - newid = nothing - first = true - alignment_node = nothing - for b in col - for a in dim.start.row_number:dim.stop.row_number - cellref = CellRef(a, b) - newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) - end - end - if first - newid = -1 - end - return newid - end - end -end -function process_uniform_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) - let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} - newid = nothing - first = true - alignment_node = nothing - for a in collect(row), b in col - cellref = CellRef(a, b) - newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) - end - if first - newid = -1 - end - return newid - end -end -function process_uniform_vecint(f::Function, ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) - let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} - newid = nothing - first = true - alignment_node = nothing - for a in row, b in collect(col) - cellref = CellRef(a, b) - newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) - end - if first - newid = -1 - end - return newid - end -end -function process_uniform_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) - let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} - newid = nothing - first = true - for a in row, b in col - cellref = CellRef(a, b) - newid, first, alignment_node = process_uniform_core(f, ws, 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 AARRGGBB")) - end - return c -end - # ========================================================================================== # # -- Get and set font attributes @@ -2534,19 +1799,11 @@ function setUniformStyle(ws::Worksheet, rng::CellRange)::Union{Nothing,Int} end let newid::Union{Nothing,Int}, + newid = nothing 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 = 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 + newid, first = process_uniform_core(ws, cellref, newid, first) end if first newid = -1 From e94a830918b89ffc34ca95710ddbcfebb130104f Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 31 Mar 2025 16:42:17 +0100 Subject: [PATCH 053/154] Tweak docstrings... --- src/cellformats.jl | 80 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 18 deletions(-) diff --git a/src/cellformats.jl b/src/cellformats.jl index 0c1dab53..1605eb26 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -14,7 +14,7 @@ Set the font 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 :. +Integer, UnitRange, Vector{Integer} or `:`. Font attributes are specified using keyword arguments: - `bold::Bool = nothing` : set to `true` to make the font bold. @@ -80,8 +80,7 @@ julia> setFont(sh, 1:3, 2; size=48, color="magenta") 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="magenta") # all rows - +julia> setFont(sh, :, 2:6; size=48, color="lightskyblue2") # all rows, columns 2 to 6 ``` """ @@ -197,8 +196,12 @@ end setUniformFont(sh::Worksheet, cr::String; kw...) -> ::Int setUniformFont(xf::XLSXFile, cr::String, kw...) -> ::Int + 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 @@ -237,7 +240,7 @@ julia> setUniformFont(sh, "33"; italic=true, color="FF8888FF", under="single") 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 ``` """ @@ -264,7 +267,7 @@ setUniformFont(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attr getFont(sh::Worksheet, row::Int, col::Int) -> ::Union{Nothing, CellFont} -Get the font used by a single cell at reference `cr` in a worksheet `sh` or XLSXfile `xf`. +Get the font used by a single cell reference in a worksheet `sh` or XLSXfile `xf`. Return a `CellFont` object containing: - `fontId` : a 0-based index of the font in the workbook @@ -297,6 +300,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) ``` """ @@ -347,7 +354,7 @@ end 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. Return a `CellBorder` object containing: - `borderId` : a 0-based index of the border in the workbook @@ -466,12 +473,11 @@ end 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 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 :. +Integer, UnitRange, Vector{Integer} or `:`. Borders are independently defined for the keywords: - `left::Vector{Pair{String,String} = nothing` @@ -493,8 +499,8 @@ keywords or with `outside` but it can be used together with `diagonal`. 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 a non-contiguous range and `outside` cannot be used in conjunction with -any other keywords. +set for any non-contiguous/non-rectangular range and `outside` 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. @@ -545,6 +551,8 @@ 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" => "firebrick2"], top = ["style" => "thick", "color" => "FF230000"], @@ -737,8 +745,12 @@ end setUniformBorder(sh::Worksheet, cr::String; kw...) -> ::Int setUniformBorder(xf::XLSXFile, cr::String, kw...) -> ::Int + 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 @@ -769,6 +781,8 @@ Note: `setUniformBorder` cannot be used with the `outside` keyword. ```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"], @@ -798,8 +812,11 @@ setUniformBorder(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_at setOutsideBorder(sh::Worksheet, cr::String; kw...) -> ::Int setOutsideBorder(xf::XLSXFile, cr::String, kw...) -> ::Int + 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 `:`. There is one key word: - `outside::Vector{Pair{String,String} = nothing` @@ -825,6 +842,7 @@ Julia> setOutsideBorder(sh, "B2:D6"; outside = ["style" => "thick") 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, colrng::ColumnRange; kw...)::Int = process_columnranges(setOutsideBorder, ws, colrng; kw...) @@ -1002,7 +1020,7 @@ end Set the fill 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 :. +Integer, UnitRange, Vector{Integer} or `:`. The following keywords are used to define a fill: - `pattern::String = nothing` : Sets the patternType for the fill. @@ -1051,9 +1069,9 @@ 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 = "88FF8800") +Julia> setFill(sh, "11:24"; pattern="none", fgColor = "yellow2") ``` """ @@ -1147,8 +1165,12 @@ end setUniformFill(sh::Worksheet, cr::String; kw...) -> ::Int setUniformFill(xf::XLSXFile, cr::String, kw...) -> ::Int + 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 @@ -1177,7 +1199,7 @@ 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") ``` """ @@ -1292,7 +1314,7 @@ end Set the alignment 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 :. +Integer, UnitRange, Vector{Integer} or `:`. The following keywords are used to define an alignment: - `horizontal::String = nothing` : Sets the horizontal alignment. @@ -1334,6 +1356,8 @@ 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") ``` """ @@ -1437,8 +1461,12 @@ end setUniformAlignment(sh::Worksheet, cr::String; kw...) -> ::Int setUniformAlignment(xf::XLSXFile, cr::String, kw...) -> ::Int + 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 @@ -1469,6 +1497,8 @@ 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") ``` """ @@ -1576,7 +1606,7 @@ end Set the 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 :. +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 @@ -1608,6 +1638,8 @@ 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_-;_-£* \\\"-\\\"??_-;_-@_-") ``` @@ -1705,8 +1737,12 @@ end setUniformFormat(sh::Worksheet, cr::String; kw...) -> ::Int setUniformFormat(xf::XLSXFile, cr::String, kw...) -> ::Int + 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 @@ -1754,9 +1790,13 @@ setUniformFormat(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_at setUniformStyle(sh::Worksheet, cr::String) -> ::Int setUniformStyle(xf::XLSXFile, cr::String) -> ::Int + 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`. @@ -1775,6 +1815,10 @@ 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, :) ``` """ @@ -1828,7 +1872,7 @@ 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. +Integer, UnitRange, Vector{Integer} or `:`, but only the columns will be used. The function uses one keyword used to define a column width: @@ -2024,7 +2068,7 @@ 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. +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. From 158583e23915dccb42a17ac5524665f6bfed1a4f Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 31 Mar 2025 21:18:17 +0100 Subject: [PATCH 054/154] Add some tests for indexing `setAttribute` functions --- src/cellformat-helpers.jl | 102 ++++++++++++++++++++++++++++++++------ src/cellformats.jl | 54 ++++++++++---------- src/worksheet.jl | 9 ++++ src/write.jl | 11 +++- test/runtests.jl | 91 ++++++++++++++++++++++++++++------ 5 files changed, 206 insertions(+), 61 deletions(-) diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl index 3db28943..40ae5632 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -283,9 +283,8 @@ end function process_columnranges(f::Function, ws::Worksheet, colrng::ColumnRange; kw...)::Int bounds = column_bounds(colrng) dim = (get_dimension(ws)) - return if dim === nothing - @warn "No worksheet dimension found" - [] + if dim === nothing + throw(XLSXError("No worksheet dimension found")) else left = bounds[begin] right = bounds[end] @@ -308,9 +307,8 @@ end function process_rowranges(f::Function, ws::Worksheet, rowrng::RowRange; kw...)::Int bounds = row_bounds(rowrng) dim = (get_dimension(ws)) - return if dim === nothing - @warn "No worksheet dimension found" - [] + if dim === nothing + throw(XLSXError("No worksheet dimension found")) else top = bounds[begin] bottom = bounds[end] @@ -333,9 +331,8 @@ end function process_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int bounds = nc_bounds(ncrng) dim = (get_dimension(ws)) - return if dim === nothing - @warn "No worksheet dimension found" - [] + if dim === nothing + throw(XLSXError("No worksheet dimension found")) else OK = dim.start.column_number <= bounds.start.column_number OK &= dim.stop.column_number >= bounds.stop.column_number @@ -403,7 +400,7 @@ function process_intcolon(f::Function, ws::Worksheet, row::Union{Integer,UnitRan if dim === nothing throw(XLSXError("No worksheet dimension found")) else - f(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)); kw...) + return f(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)); kw...) end end function process_colonint(f::Function, ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) @@ -411,7 +408,7 @@ function process_colonint(f::Function, ws::Worksheet, ::Colon, col::Union{Intege if dim === nothing throw(XLSXError("No worksheet dimension found")) else - f(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))); kw...) + return f(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))); kw...) end end function process_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon; kw...) @@ -421,10 +418,15 @@ function process_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon; else for a in row for b in dim.start.column_number:dim.stop.column_number - f(ws, CellRef(a, b); kw...) + cellref = CellRef(a, b) + if getcell(ws, cellref) isa EmptyCell + continue + end + f(ws, cellref; kw...) end end end + return -1 end function process_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}; kw...) dim = get_dimension(ws) @@ -433,25 +435,45 @@ function process_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}; else for b in col for a in dim.start.row_number:dim.stop.row_number - f(ws, CellRef(a, b); kw...) + cellref = CellRef(a, b) + if getcell(ws, cellref) isa EmptyCell + continue + end + f(ws, cellref; kw...) end end end + return -1 end function process_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) for a in collect(row), b in col - f(ws, CellRef(a, b); kw...) + cellref = CellRef(a, b) + if getcell(ws, cellref) isa EmptyCell + continue + end + f(ws, cellref; kw...) end + return -1 end function process_vecint(f::Function, ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) for a in row, b in collect(col) - f(ws, CellRef(a, b), kw...) + cellref = CellRef(a, b) + if getcell(ws, cellref) isa EmptyCell + continue + end + f(ws, cellref; kw...) end + return -1 end function process_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) for a in row, b in col - f(ws, CellRef(a, b); kw...) + cellref = CellRef(a, b) + if getcell(ws, cellref) isa EmptyCell + continue + end + f(ws, cellref; kw...) end + return -1 end # @@ -550,6 +572,9 @@ function process_uniform_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, 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 = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) end end @@ -571,6 +596,9 @@ function process_uniform_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vect for b in col for a in dim.start.row_number:dim.stop.row_number cellref = CellRef(a, b) + if getcell(ws, cellref) isa EmptyCell + continue + end newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) end end @@ -587,6 +615,9 @@ function process_uniform_intvec(f::Function, ws::Worksheet, row::Union{Integer,U first = true for a in collect(row), b in col cellref = CellRef(a, b).name + if getcell(ws, cellref) isa EmptyCell + continue + end newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) end if first @@ -601,6 +632,9 @@ function process_uniform_vecint(f::Function, ws::Worksheet, row::Vector{Int}, co first = true for a in row, b in collect(col) cellref = CellRef(a, b) + if getcell(ws, cellref) isa EmptyCell + continue + end newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) end if first @@ -615,6 +649,9 @@ function process_uniform_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, co first = true 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, cellref, atts, newid, first; kw...) end if first @@ -636,6 +673,9 @@ function process_uniform_veccolon(ws::Worksheet, row::Vector{Int}, ::Colon, atts 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 = process_uniform_core(ws, cellref, atts, newid, first; kw...) end end @@ -657,6 +697,9 @@ function process_uniform_colonvec(ws::Worksheet, ::Colon, col::Vector{Int}, atts for b in col for a in dim.start.row_number:dim.stop.row_number cellref = CellRef(a, b) + if getcell(ws, cellref) isa EmptyCell + continue + end newid, first = process_uniform_core(ws, cellref, atts, newid, first; kw...) end end @@ -673,6 +716,9 @@ function process_uniform_intvec(ws::Worksheet, row::Union{Integer,UnitRange{<:In first = true for a in collect(row), b in col cellref = CellRef(a, b).name + if getcell(ws, cellref) isa EmptyCell + continue + end newid, first = process_uniform_core(ws, cellref, atts, newid, first; kw...) end if first @@ -687,6 +733,9 @@ function process_uniform_vecint(ws::Worksheet, row::Vector{Int}, col::Union{Inte first = true for a in row, b in collect(col) cellref = CellRef(a, b) + if getcell(ws, cellref) isa EmptyCell + continue + end newid, first = process_uniform_core(ws, cellref, atts, newid, first; kw...) end if first @@ -701,6 +750,9 @@ function process_uniform_vecvec(ws::Worksheet, row::Vector{Int}, col::Vector{Int first = true 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, atts, newid, first; kw...) end if first @@ -722,6 +774,9 @@ function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange; k first = true alignment_node = nothing for cellref in rng + if getcell(ws, cellref) isa EmptyCell + continue + end newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) end if first @@ -742,6 +797,9 @@ function process_uniform_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, 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, cellref, newid, first, alignment_node; kw...) end end @@ -764,6 +822,9 @@ function process_uniform_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vect for b in col for a in dim.start.row_number:dim.stop.row_number cellref = CellRef(a, b) + if getcell(ws, cellref) isa EmptyCell + continue + end newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) end end @@ -781,6 +842,9 @@ function process_uniform_intvec(f::Function, ws::Worksheet, row::Union{Integer,U alignment_node = nothing for a in collect(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, cellref, newid, first, alignment_node; kw...) end if first @@ -796,6 +860,9 @@ function process_uniform_vecint(f::Function, ws::Worksheet, row::Vector{Int}, co alignment_node = nothing for a in row, b in collect(col) cellref = CellRef(a, b) + if getcell(ws, cellref) isa EmptyCell + continue + end newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) end if first @@ -810,6 +877,9 @@ function process_uniform_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, co first = true 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, cellref, newid, first, alignment_node; kw...) end if first diff --git a/src/cellformats.jl b/src/cellformats.jl index 1605eb26..17c3323b 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -96,8 +96,8 @@ setFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) setFont(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFont, ws, :, col; kw...) setFont(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFont, ws, row, :; kw...) setFont(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setFont, ws, :, col; kw...) -setFont(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setFont, ws, row, col; kw...) -setFont(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFont, ws, row, col; kw...) +setFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setFont, ws, row, col; kw...) +setFont(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFont, ws, row, col; kw...) setFont(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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; @@ -254,10 +254,10 @@ setUniformFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; setUniformFont(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setFont, ws, :, col; kw...) setUniformFont(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setFont, ws, row, :, ["fontId", "applyFont"]; kw...) setUniformFont(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setFont, ws, :, col, ["fontId", "applyFont"]; kw...) -setUniformFont(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setFont, ws, row, col, ["fontId", "applyFont"]; kw...) -setUniformFont(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFont, ws, row, col, ["fontId", "applyFont"]; kw...) +setUniformFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setFont, ws, row, col, ["fontId", "applyFont"]; kw...) +setUniformFont(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFont, ws, row, col, ["fontId", "applyFont"]; kw...) setUniformFont(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setFont, ws, row, col, ["fontId", "applyFont"]; kw...) -setUniformFont(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...) +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...) @@ -570,8 +570,8 @@ setBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw.. setBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setBorder, ws, :, col; kw...) setBorder(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setBorder, ws, row, :; kw...) setBorder(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setBorder, ws, :, col; kw...) -setBorder(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setBorder, ws, row, col; kw...) -setBorder(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setBorder, ws, row, col; kw...) +setBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setBorder, ws, row, col; kw...) +setBorder(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setBorder, ws, row, col; kw...) setBorder(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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; @@ -802,8 +802,8 @@ setUniformBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colo setUniformBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setBorder, ws, :, col; kw...) setUniformBorder(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setBorder, ws, row, :, ["borderId", "applyBorder"]; kw...) setUniformBorder(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setBorder, ws, :, col, ["borderId", "applyBorder"]; kw...) -setUniformBorder(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...) -setUniformBorder(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...) +setUniformBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...) +setUniformBorder(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...) setUniformBorder(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...) setUniformBorder(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...) setUniformBorder(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attribute(setBorder, ws, rng, ["borderId", "applyBorder"]; kw...) @@ -1087,8 +1087,8 @@ setFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) setFill(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFill, ws, :, col; kw...) setFill(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFill, ws, row, :; kw...) setFill(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setFill, ws, :, col; kw...) -setFill(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setFill, ws, row, col; kw...) -setFill(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFill, ws, row, col; kw...) +setFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setFill, ws, row, col; kw...) +setFill(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFill, ws, row, col; kw...) setFill(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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; @@ -1213,8 +1213,8 @@ setUniformFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; setUniformFill(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setFill, ws, :, col; kw...) setUniformFill(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setFill, ws, row, :, ["fillId", "applyFill"]; kw...) setUniformFill(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setFill, ws, :, col, ["fillId", "applyFill"]; kw...) -setUniformFill(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setFill, ws, row, col, ["fillId", "applyFill"]; kw...) -setUniformFill(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFill, ws, row, col, ["fillId", "applyFill"]; kw...) +setUniformFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setFill, ws, row, col, ["fillId", "applyFill"]; kw...) +setUniformFill(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFill, ws, row, col, ["fillId", "applyFill"]; kw...) setUniformFill(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setFill, ws, row, col, ["fillId", "applyFill"]; kw...) setUniformFill(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...) setUniformFill(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attribute(setFill, ws, rng, ["fillId", "applyFill"]; kw...) @@ -1373,8 +1373,8 @@ setAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; k setAlignment(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setAlignment, ws, :, col; kw...) setAlignment(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setAlignment, ws, row, :; kw...) setAlignment(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setAlignment, ws, :, col; kw...) -setAlignment(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setAlignment, ws, row, col; kw...) -setAlignment(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setAlignment, ws, row, col; kw...) +setAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setAlignment, ws, row, col; kw...) +setAlignment(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setAlignment, ws, row, col; kw...) setAlignment(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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; @@ -1512,8 +1512,8 @@ setUniformAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::C setUniformAlignment(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setAlignment, ws, :, col; kw...) setUniformAlignment(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setAlignment, ws, row, :; kw...) setUniformAlignment(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setAlignment, ws, :, col; kw...) -setUniformAlignment(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setAlignment, ws, row, col; kw...) -setUniformAlignment(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setAlignment, ws, row, col; kw...) +setUniformAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setAlignment, ws, row, col; kw...) +setUniformAlignment(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setAlignment, ws, row, col; kw...) setUniformAlignment(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setAlignment, ws, row, col; kw...) setUniformAlignment(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...) setUniformAlignment(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attribute(setAlignment, ws, rng; kw...) @@ -1656,8 +1656,8 @@ setFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw.. setFormat(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFormat, ws, :, col; kw...) setFormat(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFormat, ws, row, :; kw...) setFormat(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setFormat, ws, :, col; kw...) -setFormat(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setFormat, ws, row, col; kw...) -setFormat(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFormat, ws, row, col; kw...) +setFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setFormat, ws, row, col; kw...) +setFormat(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFormat, ws, row, col; kw...) setFormat(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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; @@ -1776,8 +1776,8 @@ setUniformFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colo setUniformFormat(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setFormat, ws, :, col; kw...) setUniformFormat(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setFormat, ws, row, :, ["numFmtId", "applyNumberFormat"]; kw...) setUniformFormat(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setFormat, ws, :, col, ["numFmtId", "applyNumberFormat"]; kw...) -setUniformFormat(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...) -setUniformFormat(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...) +setUniformFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...) +setUniformFormat(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...) setUniformFormat(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...) setUniformFormat(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...) setUniformFormat(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attribute(setFormat, ws, rng, ["numFmtId", "applyNumberFormat"]; kw...) @@ -1832,8 +1832,8 @@ setUniformStyle(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon setUniformStyle(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setUniformStyle, ws, :, col) setUniformStyle(ws::Worksheet, row::Vector{Int}, ::Colon) = process_uniform_veccolon(setUniformStyle, ws, row, :) setUniformStyle(ws::Worksheet, ::Colon, col::Vector{Int}) = process_uniform_colonvec(setUniformStyle, ws, :, col) -setUniformStyle(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}) = process_uniform_intvec(setUniformtyle, ws, row, col) -setUniformStyle(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}) = process_uniform_vecint(setUniformStyle, ws, row, col) +setUniformStyle(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}) = process_uniform_intvec(setUniformtyle, ws, row, col) +setUniformStyle(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}) = process_uniform_vecint(setUniformStyle, ws, row, col) setUniformStyle(ws::Worksheet, row::Vector{Int}, col::Vector{Int}) = process_uniform_vecvec(setUniformStyle, 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} @@ -1915,8 +1915,8 @@ setColumnWidth(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; setColumnWidth(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setColumnWidth, ws, :, col; kw...) setColumnWidth(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setColumnWidth, ws, row, :; kw...) setColumnWidth(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setColumnWidth, ws, :, col; kw...) -setColumnWidth(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setColumnWidth, ws, row, col; kw...) -setColumnWidth(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setColumnWidth, ws, row, col; kw...) +setColumnWidth(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setColumnWidth, ws, row, col; kw...) +setColumnWidth(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setColumnWidth, ws, row, col; kw...) setColumnWidth(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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 @@ -2111,8 +2111,8 @@ setRowHeight(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; k setRowHeight(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setRowHeight, ws, :, col; kw...) setRowHeight(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setRowHeight, ws, row, :; kw...) setRowHeight(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setRowHeight, ws, :, col; kw...) -setRowHeight(ws::Worksheet, v, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setRowHeight, ws, row, col; kw...) -setRowHeight(ws::Worksheet, v, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setRowHeight, ws, row, col; kw...) +setRowHeight(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setRowHeight, ws, row, col; kw...) +setRowHeight(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setRowHeight, ws, row, col; kw...) setRowHeight(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(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 diff --git a/src/worksheet.jl b/src/worksheet.jl index 0c8682c6..a80a2969 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -107,6 +107,15 @@ getdata(ws::Worksheet, row::Int, col::Vector{Int}) = [getdata(ws, a, b) for a in getdata(ws::Worksheet, row::Vector{Int}, col::Int) = [getdata(ws, a, b) for a in row, b in [col]] getdata(ws::Worksheet, row::Vector{Int}, col::Vector{Int}) = [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) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + getdata(ws, dim) + end +end function getdata(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) dim = get_dimension(ws) if dim === nothing diff --git a/src/write.jl b/src/write.jl index 1247b4c6..78e626cf 100644 --- a/src/write.jl +++ b/src/write.jl @@ -519,8 +519,6 @@ xlsx_encode(ws::Worksheet, val::Dates.DateTime) = ("", string(datetime_to_excel_ 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, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) = setdata!(ws, row, :, v) -#Base.setindex!(ws::Worksheet, v, ::Colon, col::Union{Integer,UnitRange{<:Integer}}) = setdata!(ws, :, 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) @@ -670,6 +668,15 @@ function setdata!(ws::Worksheet, rng::NonContiguousRange, value) end end end +setdata!(ws::Worksheet, ::Colon, ::Colon, v) = setdata!(ws::Worksheet, :, v) +function setdata!(ws::Worksheet, ::Colon, v) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + setdata!(ws, dim, v) + end +end function setdata!(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon, v) dim = get_dimension(ws) if dim === nothing diff --git a/test/runtests.jl b/test/runtests.jl index 411469bd..a026ba54 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -374,6 +374,21 @@ end @test sheet2[:] == XLSX.getdata(sheet2) 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 XLSX.getdata(s, 1:3, 1:3) == Any["Hello world" true true; 42 42 42; "Hello world" true true] + s[2, :] = 44 + @test XLSX.getdata(s, 1:3, 1:3) == Any["Hello world" true true; 44 44 44; "Hello world" true true] + s[:, :] = 0 + @test XLSX.getdata(s, 1:3, 1:3) == Any[0 0 0; 0 0 0; 0 0 0] + s[:] = 1 + @test XLSX.getdata(s, 1:3, 1:3) == Any[1 1 1; 1 1 1; 1 1 1] +end + @testset "Time and DateTime" begin @test XLSX.excel_value_to_time(0.82291666666666663) == Dates.Time(Dates.Hour(19), Dates.Minute(45)) @test XLSX.time_to_excel_value(XLSX.excel_value_to_time(0.2)) == 0.2 @@ -1749,15 +1764,15 @@ end 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")) @@ -1765,15 +1780,15 @@ 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) + 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", "direction" => "both")) - @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", "direction" => "both")) - @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", "direction" => "both")) - @test XLSX.getBorder(s, "J20").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")) + 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")) # Can't get attributes on a range. @test_throws XLSX.XLSXError XLSX.getBorder(s, "Contiguous") @@ -1782,20 +1797,20 @@ end 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"], + @test XLSX.setUniformFont(s, "A1:B4"; size=12, name="Times New Roman", color="chocolate4") == -1 + @test 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" => "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:F20"; 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.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, "A1:F20"; horizontal="right", wrapText=true) == -1 @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" => "FF111111"], top=["style" => "hair"], bottom=["color" => "FF111111"], diagonal=["style" => "hair"]) + @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")) @@ -1805,6 +1820,8 @@ end @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"], @@ -2072,7 +2089,49 @@ end end end + @testset "indexing setAttribute" begin + f=XLSX.newxlsx() + s=f[1] + @test_throws XLSX.XLSXError XLSX.setFont(s, "B2"; color="grey42") + @test_throws XLSX.XLSXError XLSX.setFont(s, 3, 3; color="grey42") + @test XLSX.setFont(s, 2:3, 1:3; color="grey42") == -1 + @test XLSX.getFont(s, 2, 1) === nothing + @test XLSX.getFont(s, 3, 2) === nothing + @test XLSX.getFont(s, 2, 3) === nothing + 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, "B2"; allsides=["color" => "grey42", "style" => "thick"]) + @test_throws XLSX.XLSXError XLSX.setBorder(s, 2, 5; allsides=["color" => "grey42", "style" => "thick"]) + @test XLSX.setBorder(s, [2,3], 1:3; allsides=["color" => "grey42", "style" => "thick"]) == -1 + @test XLSX.getBorder(s, 2, 1) === nothing + @test XLSX.getBorder(s, 3, 2) === nothing + @test XLSX.getBorder(s, 2, 3) === nothing + 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")) + end end + @testset "merged cells" begin XLSX.openxlsx(joinpath(data_directory, "customXml.xlsx")) do f mc = sort(XLSX.getMergedCells(f["Mock-up"])) From 12134e1154744aa046cbf5ae97d8e8d2009292ac Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 31 Mar 2025 21:29:16 +0100 Subject: [PATCH 055/154] Fix `setUniformAttribute` call error --- src/cellformats.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cellformats.jl b/src/cellformats.jl index 17c3323b..6873fa18 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -805,7 +805,7 @@ setUniformBorder(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_unif setUniformBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...) setUniformBorder(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...) setUniformBorder(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...) -setUniformBorder(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...) +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...) """ @@ -851,7 +851,7 @@ setOutsideBorder(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_ setOutsideBorder(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setOutsideBorder, ws, ref_or_rng; kw...) setOutsideBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_uniform_intcolon(setOutsideBorder, ws, row, :; kw...) setOutsideBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setOutsideBorder, ws, :, col; kw...) -setOutsideBorder(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))), ["borderId", "applyBorder"]; 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 @@ -1216,7 +1216,7 @@ setUniformFill(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_unifor setUniformFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setFill, ws, row, col, ["fillId", "applyFill"]; kw...) setUniformFill(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFill, ws, row, col, ["fillId", "applyFill"]; kw...) setUniformFill(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setFill, ws, row, col, ["fillId", "applyFill"]; kw...) -setUniformFill(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...) +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...) # @@ -1515,7 +1515,7 @@ setUniformAlignment(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_u setUniformAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setAlignment, ws, row, col; kw...) setUniformAlignment(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setAlignment, ws, row, col; kw...) setUniformAlignment(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setAlignment, ws, row, col; kw...) -setUniformAlignment(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...) +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...) # @@ -1779,7 +1779,7 @@ setUniformFormat(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_unif setUniformFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...) setUniformFormat(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...) setUniformFormat(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...) -setUniformFormat(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...) +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...) # From 09d482e8cda5ba60e4f5c9ba75a1264a3e143468 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 1 Apr 2025 00:12:06 +0100 Subject: [PATCH 056/154] A few more tests... --- src/cellformat-helpers.jl | 94 ++++++++++++++++++++++++++++++++++----- src/cellformats.jl | 5 +++ test/runtests.jl | 58 +++++++++++++++++++++--- 3 files changed, 138 insertions(+), 19 deletions(-) diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl index 40ae5632..f3707e72 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -349,8 +349,14 @@ function process_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguousRange; end end function process_cellranges(f::Function, ws::Worksheet, rng::CellRange; kw...)::Int + if length(rng)==1 + single=true + else + single=false + end 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...) @@ -395,12 +401,26 @@ end # # - Used for indexing `setAttribute` family of functions # +function process_colon(f::Function, ws::Worksheet, ::Colon; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + elseif length(dim)==1 + throw(XLSXError("Cannot set format for an `EmptyCell`: $(dim.start.name). Set the value first.")) + else + return f(ws, dim; kw...) + end +end function process_intcolon(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) else - return f(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)); kw...) + rng=CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)) + if length(rng)==1 + throw(XLSXError("Cannot set format for an `EmptyCell`: $(rng.start.name). Set the value first.")) + end + return f(ws, rng; kw...) end end function process_colonint(f::Function, ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) @@ -408,7 +428,11 @@ function process_colonint(f::Function, ws::Worksheet, ::Colon, col::Union{Intege if dim === nothing throw(XLSXError("No worksheet dimension found")) else - return f(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))); kw...) + rng=CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))) + if length(rng)==1 + throw(XLSXError("Cannot set format for an `EmptyCell`: $(rng.start.name). Set the value first.")) + end + return f(ws, rng; kw...) end end function process_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon; kw...) @@ -416,10 +440,16 @@ function process_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon; if dim === nothing throw(XLSXError("No worksheet dimension found")) else + if length(row)==1 && dim.start.column_number == dim.stop.column_number + single=true + else + single=false + end 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 + single && throw(XLSXError("Cannot set format for an `EmptyCell`: $(cellref.name). Set the value first.")) continue end f(ws, cellref; kw...) @@ -433,10 +463,16 @@ function process_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}; if dim === nothing throw(XLSXError("No worksheet dimension found")) else + if length(col)==1 && dim.start.row_number == dim.stop.row_number + single=true + else + single=false + end for b in col for a in dim.start.row_number:dim.stop.row_number 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...) @@ -446,9 +482,15 @@ function process_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}; return -1 end function process_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) + if length(col)==1 && length(row)==1 + single=true + else + single=false + end for a in collect(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...) @@ -456,9 +498,15 @@ function process_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange return -1 end function process_vecint(f::Function, ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) + if length(col)==1 && length(row)==1 + single=true + else + single=false + end for a in row, b in collect(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...) @@ -466,10 +514,16 @@ function process_vecint(f::Function, ws::Worksheet, row::Vector{Int}, col::Union return -1 end function process_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) + if length(col)==1 && length(row)==1 + single=true + else + single=false + end for a in row, b in col cellref = CellRef(a, b) if getcell(ws, cellref) isa EmptyCell continue + single && throw(XLSXError("Cannot set format for an `EmptyCell`: $(cellref.name). Set the value first.")) end f(ws, cellref; kw...) end @@ -545,6 +599,14 @@ function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange, a return newid end end +function process_uniform_colon(f::Function, ws::Worksheet, ::Colon; kw...) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + f(ws, dim; kw...) + end +end function process_uniform_intcolon(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) dim = get_dimension(ws) if dim === nothing @@ -662,7 +724,15 @@ function process_uniform_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, co end # UniformStyles -function process_uniform_veccolon(ws::Worksheet, row::Vector{Int}, ::Colon, atts::Vector{String}; kw...) +function process_uniform_colon(ws::Worksheet, ::Colon, atts::Vector{String}) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + setUniformStyle(ws, dim) + end +end +function process_uniform_veccolon(ws::Worksheet, row::Vector{Int}, ::Colon, atts::Vector{String}) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) @@ -676,7 +746,7 @@ function process_uniform_veccolon(ws::Worksheet, row::Vector{Int}, ::Colon, atts if getcell(ws, cellref) isa EmptyCell continue end - newid, first = process_uniform_core(ws, cellref, atts, newid, first; kw...) + newid, first = process_uniform_core(ws, cellref, newid, first) end end if first @@ -686,7 +756,7 @@ function process_uniform_veccolon(ws::Worksheet, row::Vector{Int}, ::Colon, atts end end end -function process_uniform_colonvec(ws::Worksheet, ::Colon, col::Vector{Int}, atts::Vector{String}; kw...) +function process_uniform_colonvec(ws::Worksheet, ::Colon, col::Vector{Int}, atts::Vector{String}) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) @@ -700,7 +770,7 @@ function process_uniform_colonvec(ws::Worksheet, ::Colon, col::Vector{Int}, atts if getcell(ws, cellref) isa EmptyCell continue end - newid, first = process_uniform_core(ws, cellref, atts, newid, first; kw...) + newid, first = process_uniform_core(ws, cellref, newid, first) end end if first @@ -710,7 +780,7 @@ function process_uniform_colonvec(ws::Worksheet, ::Colon, col::Vector{Int}, atts end end end -function process_uniform_intvec(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}, atts::Vector{String}; kw...) +function process_uniform_intvec(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}, atts::Vector{String}) let newid::Union{Int,Nothing}, first::Bool newid = nothing first = true @@ -719,7 +789,7 @@ function process_uniform_intvec(ws::Worksheet, row::Union{Integer,UnitRange{<:In if getcell(ws, cellref) isa EmptyCell continue end - newid, first = process_uniform_core(ws, cellref, atts, newid, first; kw...) + newid, first = process_uniform_core(ws, cellref, newid, first) end if first newid = -1 @@ -727,7 +797,7 @@ function process_uniform_intvec(ws::Worksheet, row::Union{Integer,UnitRange{<:In return newid end end -function process_uniform_vecint(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}, atts::Vector{String}; kw...) +function process_uniform_vecint(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}, atts::Vector{String}) let newid::Union{Int,Nothing}, first::Bool newid = nothing first = true @@ -736,7 +806,7 @@ function process_uniform_vecint(ws::Worksheet, row::Vector{Int}, col::Union{Inte if getcell(ws, cellref) isa EmptyCell continue end - newid, first = process_uniform_core(ws, cellref, atts, newid, first; kw...) + newid, first = process_uniform_core(ws, cellref, newid, first) end if first newid = -1 @@ -744,7 +814,7 @@ function process_uniform_vecint(ws::Worksheet, row::Vector{Int}, col::Union{Inte return newid end end -function process_uniform_vecvec(ws::Worksheet, row::Vector{Int}, col::Vector{Int}, atts::Vector{String}; kw...) +function process_uniform_vecvec(ws::Worksheet, row::Vector{Int}, col::Vector{Int}, atts::Vector{String}) let newid::Union{Int,Nothing}, first::Bool newid = nothing first = true @@ -753,7 +823,7 @@ function process_uniform_vecvec(ws::Worksheet, row::Vector{Int}, col::Vector{Int if getcell(ws, cellref) isa EmptyCell continue end - newid, first = process_uniform_core(ws, cellref, atts, newid, first; kw...) + newid, first = process_uniform_core(ws, cellref, newid, first) end if first newid = -1 diff --git a/src/cellformats.jl b/src/cellformats.jl index 6873fa18..f99073fd 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -96,6 +96,7 @@ setFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) setFont(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFont, ws, :, col; kw...) setFont(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFont, ws, row, :; kw...) setFont(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setFont, ws, :, col; kw...) +setFont(ws::Worksheet, ::Colon; kw...) = process_colon(setFont, ws, :; kw...) setFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setFont, ws, row, col; kw...) setFont(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFont, ws, row, col; kw...) setFont(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(setFont, ws, row, col; kw...) @@ -568,6 +569,7 @@ setBorder(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_range 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_intcolon(setBorder, ws, row, :; kw...) setBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setBorder, ws, :, col; kw...) +setBorder(ws::Worksheet, ::Colon; kw...) = process_colon(setBorder, ws, :; kw...) setBorder(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setBorder, ws, row, :; kw...) setBorder(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setBorder, ws, :, col; kw...) setBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setBorder, ws, row, col; kw...) @@ -1084,6 +1086,7 @@ setFill(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges( 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_intcolon(setFill, ws, row, :; kw...) +setFill(ws::Worksheet, ::Colon; kw...) = process_colon(setFill, ws, :; kw...) setFill(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFill, ws, :, col; kw...) setFill(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFill, ws, row, :; kw...) setFill(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setFill, ws, :, col; kw...) @@ -1370,6 +1373,7 @@ setAlignment(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ra setAlignment(xl::XLSXFile, sheetcell::String; kw...)::Int = process_sheetcell(setAlignment, xl, sheetcell; kw...) 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_intcolon(setAlignment, ws, row, :; kw...) +setAlignment(ws::Worksheet, ::Colon; kw...) = process_colon(setAlignment, ws, :; kw...) setAlignment(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setAlignment, ws, :, col; kw...) setAlignment(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setAlignment, ws, row, :; kw...) setAlignment(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setAlignment, ws, :, col; kw...) @@ -1654,6 +1658,7 @@ setFormat(xl::XLSXFile, sheetcell::String; kw...)::Int = process_sheetcell(setFo 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_intcolon(setFormat, ws, row, :; kw...) setFormat(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFormat, ws, :, col; kw...) +setFormat(ws::Worksheet, ::Colon; kw...) = process_colon(setFormat, ws, :; kw...) setFormat(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFormat, ws, row, :; kw...) setFormat(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setFormat, ws, :, col; kw...) setFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setFormat, ws, row, col; kw...) diff --git a/test/runtests.jl b/test/runtests.jl index a026ba54..23aafc2e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2090,14 +2090,26 @@ end end @testset "indexing setAttribute" begin - f=XLSX.newxlsx() - s=f[1] - @test_throws XLSX.XLSXError XLSX.setFont(s, "B2"; color="grey42") - @test_throws XLSX.XLSXError XLSX.setFont(s, 3, 3; color="grey42") + 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") + + # Skip empty cells silently in ranges @test XLSX.setFont(s, 2:3, 1:3; color="grey42") == -1 @test XLSX.getFont(s, 2, 1) === nothing @test XLSX.getFont(s, 3, 2) === nothing @test XLSX.getFont(s, 2, 3) === nothing + s[1:3,1:3] = " " default_font = XLSX.getDefaultFont(s).font dname = default_font["name"]["val"] @@ -2111,11 +2123,17 @@ end @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, "B2"; allsides=["color" => "grey42", "style" => "thick"]) - @test_throws XLSX.XLSXError XLSX.setBorder(s, 2, 5; allsides=["color" => "grey42", "style" => "thick"]) + @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 XLSX.setBorder(s, [2,3], 1:3; allsides=["color" => "grey42", "style" => "thick"]) == -1 @test XLSX.getBorder(s, 2, 1) === nothing @test XLSX.getBorder(s, 3, 2) === nothing @@ -2129,6 +2147,32 @@ end @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 XLSX.setFill(s, [2,3], 1:3; pattern="lightVertical", fgColor="Red", bgColor="blue") == -1 + @test XLSX.getFill(s, 2, 1) === nothing + @test XLSX.getFill(s, 3, 2) === nothing + @test XLSX.getFill(s, 2, 3) === nothing + 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")) + end end From 11f9fa37b8b95f75de7dee7c07ecb3ad615dfd5a Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 1 Apr 2025 19:17:30 +0100 Subject: [PATCH 057/154] Add another set of tests. --- src/cellformat-helpers.jl | 125 +++++++++++++++++++++++--------------- src/cellformats.jl | 80 ++++++++++++++++++------ src/read.jl | 2 +- src/worksheet.jl | 16 ++++- test/runtests.jl | 125 ++++++++++++++++++++++++++++++-------- 5 files changed, 252 insertions(+), 96 deletions(-) diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl index f3707e72..b1d2d55f 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -103,11 +103,9 @@ function buildNode(tag::String, attributes::Dict{String,Union{Nothing,Dict{Strin if v in ["down", "both"] new_node["diagonalDown"] = "1" end - else#if k == "rgb" + else color[k] = v - #else - #throw(XLSXError("Incorect border attribute found: $k")) # shouldn't happen! - end + end end if length(XML.attributes(color)) > 0 # Don't push an empty color. push!(cnode, color) @@ -367,12 +365,20 @@ 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.")) - return f(getsheet(xl, ref.sheet), ref.cellref; kw...) + 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 @@ -533,7 +539,11 @@ end # # - Used for indexing `setUniformAttribute` family of functions # -function process_uniform_core(f::Function, ws::Worksheet, cellref::CellRef, atts::Vector{String}, newid::Union{Int,Nothing}, first::Bool; kw...) # Most functions in set + +# +# Most set functions +# +function process_uniform_core(f::Function, ws::Worksheet, 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 @@ -549,39 +559,7 @@ function process_uniform_core(f::Function, ws::Worksheet, cellref::CellRef, atts end return newid, first end -function process_uniform_core(f::Function, ws::Worksheet, 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, 0).id) - end - cell.style = string(update_template_xf(ws, CellDataFormat(parse(Int, cell.style)), alignment_node).id) - end - return newid, first, alignment_node -end -function process_uniform_core(ws::Worksheet, cellref::CellRef, newid::Union{Int,Nothing}, first::Bool) # setUniformStyle is different, too - 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_attribute(f::Function, ws::Worksheet, rng::CellRange, atts::Vector{String}; kw...) # Most set functions +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.")) @@ -723,8 +701,39 @@ function process_uniform_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, co end end +# # UniformStyles -function process_uniform_colon(ws::Worksheet, ::Colon, atts::Vector{String}) +# +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_intcolon(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + setUniformStyle(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number))) + end +end +function process_uniform_colonint(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + setUniformStyle(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col)))) + end +end +function process_uniform_colon(ws::Worksheet, ::Colon) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) @@ -732,7 +741,7 @@ function process_uniform_colon(ws::Worksheet, ::Colon, atts::Vector{String}) setUniformStyle(ws, dim) end end -function process_uniform_veccolon(ws::Worksheet, row::Vector{Int}, ::Colon, atts::Vector{String}) +function process_uniform_veccolon(ws::Worksheet, row::Vector{Int}, ::Colon) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) @@ -756,7 +765,7 @@ function process_uniform_veccolon(ws::Worksheet, row::Vector{Int}, ::Colon, atts end end end -function process_uniform_colonvec(ws::Worksheet, ::Colon, col::Vector{Int}, atts::Vector{String}) +function process_uniform_colonvec(ws::Worksheet, ::Colon, col::Vector{Int}) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) @@ -780,7 +789,7 @@ function process_uniform_colonvec(ws::Worksheet, ::Colon, col::Vector{Int}, atts end end end -function process_uniform_intvec(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}, atts::Vector{String}) +function process_uniform_intvec(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}) let newid::Union{Int,Nothing}, first::Bool newid = nothing first = true @@ -797,7 +806,7 @@ function process_uniform_intvec(ws::Worksheet, row::Union{Integer,UnitRange{<:In return newid end end -function process_uniform_vecint(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}, atts::Vector{String}) +function process_uniform_vecint(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}) let newid::Union{Int,Nothing}, first::Bool newid = nothing first = true @@ -814,7 +823,7 @@ function process_uniform_vecint(ws::Worksheet, row::Vector{Int}, col::Union{Inte return newid end end -function process_uniform_vecvec(ws::Worksheet, row::Vector{Int}, col::Vector{Int}, atts::Vector{String}) +function process_uniform_vecvec(ws::Worksheet, row::Vector{Int}, col::Vector{Int}) let newid::Union{Int,Nothing}, first::Bool newid = nothing first = true @@ -832,8 +841,28 @@ function process_uniform_vecvec(ws::Worksheet, row::Vector{Int}, col::Vector{Int end end - -function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange; kw...) # Alignment is different +# +# Alignment is different +# +function process_uniform_core(f::Function, ws::Worksheet, 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, 0).id) + end + cell.style = string(update_template_xf(ws, 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.")) diff --git a/src/cellformats.jl b/src/cellformats.jl index f99073fd..d74dfaf1 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -96,6 +96,7 @@ setFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) setFont(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFont, ws, :, col; kw...) setFont(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFont, ws, row, :; kw...) setFont(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setFont, ws, :, col; kw...) +setFont(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setFont, ws, :; kw...) setFont(ws::Worksheet, ::Colon; kw...) = process_colon(setFont, ws, :; kw...) setFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setFont, ws, row, col; kw...) setFont(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFont, ws, row, col; kw...) @@ -255,6 +256,8 @@ setUniformFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; setUniformFont(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setFont, ws, :, col; kw...) setUniformFont(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setFont, ws, row, :, ["fontId", "applyFont"]; kw...) setUniformFont(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setFont, ws, :, col, ["fontId", "applyFont"]; kw...) +setUniformFont(ws::Worksheet, ::Colon, ::Colon; kw...) = process_uniform_colon(setUniformFont, ws, :; kw...) +setUniformFont(ws::Worksheet, ::Colon; kw...) = process_uniform_colon(setUniformFont, ws, :; kw...) setUniformFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setFont, ws, row, col, ["fontId", "applyFont"]; kw...) setUniformFont(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFont, ws, row, col, ["fontId", "applyFont"]; kw...) setUniformFont(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setFont, ws, row, col, ["fontId", "applyFont"]; kw...) @@ -269,6 +272,7 @@ setUniformFont(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attr 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 @@ -312,7 +316,7 @@ 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; kw...) = getFont(ws, CellRef(row, col); kw...) +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} @@ -356,6 +360,7 @@ end getBorder(sh::Worksheet, row::Int, col::Int) -> ::Union{Nothing, CellBorder} 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 @@ -416,7 +421,7 @@ 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; kw...) = getBorder(ws, CellRef(row, col); kw...) +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} @@ -569,6 +574,7 @@ setBorder(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_range 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_intcolon(setBorder, ws, row, :; kw...) setBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setBorder, ws, :, col; kw...) +setBorder(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setBorder, ws, :; kw...) setBorder(ws::Worksheet, ::Colon; kw...) = process_colon(setBorder, ws, :; kw...) setBorder(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setBorder, ws, row, :; kw...) setBorder(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setBorder, ws, :, col; kw...) @@ -804,6 +810,8 @@ setUniformBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colo setUniformBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setBorder, ws, :, col; kw...) setUniformBorder(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setBorder, ws, row, :, ["borderId", "applyBorder"]; kw...) setUniformBorder(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setBorder, ws, :, col, ["borderId", "applyBorder"]; kw...) +setUniformBorder(ws::Worksheet, ::Colon, ::Colon; kw...) = process_uniform_colon(setUniformBorder, ws, :; kw...) +setUniformBorder(ws::Worksheet, ::Colon; kw...) = process_uniform_colon(setUniformBorder, ws, :; kw...) setUniformBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...) setUniformBorder(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...) setUniformBorder(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...) @@ -853,6 +861,8 @@ setOutsideBorder(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_ setOutsideBorder(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setOutsideBorder, ws, ref_or_rng; kw...) setOutsideBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_uniform_intcolon(setOutsideBorder, ws, row, :; kw...) setOutsideBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setOutsideBorder, ws, :, col; kw...) +setOutsideBorder(ws::Worksheet, ::Colon, ::Colon; kw...) = process_uniform_colon(setOutsideBorder, ws, :; kw...) +setOutsideBorder(ws::Worksheet, ::Colon; kw...) = process_uniform_colon(setOutsideBorder, ws, :; 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, @@ -891,6 +901,7 @@ end 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 @@ -965,7 +976,7 @@ 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; kw...) = getFill(ws, CellRef(row, col); kw...) +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} @@ -1086,6 +1097,7 @@ setFill(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges( 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_intcolon(setFill, ws, row, :; kw...) +setFill(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setFill, ws, :; kw...) setFill(ws::Worksheet, ::Colon; kw...) = process_colon(setFill, ws, :; kw...) setFill(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFill, ws, :, col; kw...) setFill(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFill, ws, row, :; kw...) @@ -1216,6 +1228,8 @@ setUniformFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; setUniformFill(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setFill, ws, :, col; kw...) setUniformFill(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setFill, ws, row, :, ["fillId", "applyFill"]; kw...) setUniformFill(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setFill, ws, :, col, ["fillId", "applyFill"]; kw...) +setUniformFill(ws::Worksheet, ::Colon, ::Colon; kw...) = process_uniform_colon(setUniformFill, ws, :; kw...) +setUniformFill(ws::Worksheet, ::Colon; kw...) = process_uniform_colon(setUniformFill, ws, :; kw...) setUniformFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setFill, ws, row, col, ["fillId", "applyFill"]; kw...) setUniformFill(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFill, ws, row, col, ["fillId", "applyFill"]; kw...) setUniformFill(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setFill, ws, row, col, ["fillId", "applyFill"]; kw...) @@ -1233,6 +1247,7 @@ setUniformFill(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform_attr 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) @@ -1282,7 +1297,7 @@ 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; kw...) = getAlignment(ws, CellRef(row, col); kw...) +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} @@ -1373,6 +1388,7 @@ setAlignment(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ra setAlignment(xl::XLSXFile, sheetcell::String; kw...)::Int = process_sheetcell(setAlignment, xl, sheetcell; kw...) 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_intcolon(setAlignment, ws, row, :; kw...) +setAlignment(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setAlignment, ws, :; kw...) setAlignment(ws::Worksheet, ::Colon; kw...) = process_colon(setAlignment, ws, :; kw...) setAlignment(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setAlignment, ws, :, col; kw...) setAlignment(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setAlignment, ws, row, :; kw...) @@ -1516,6 +1532,8 @@ setUniformAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::C setUniformAlignment(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setAlignment, ws, :, col; kw...) setUniformAlignment(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setAlignment, ws, row, :; kw...) setUniformAlignment(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setAlignment, ws, :, col; kw...) +setUniformAlignment(ws::Worksheet, ::Colon, ::Colon; kw...) = process_uniform_colon(setUniformAlignment, ws, :; kw...) +setUniformAlignment(ws::Worksheet, ::Colon; kw...) = process_uniform_colon(setUniformAlignment, ws, :; kw...) setUniformAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setAlignment, ws, row, col; kw...) setUniformAlignment(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setAlignment, ws, row, col; kw...) setUniformAlignment(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setAlignment, ws, row, col; kw...) @@ -1533,6 +1551,7 @@ setUniformAlignment(ws::Worksheet, rng::CellRange; kw...)::Int = process_uniform 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 @@ -1560,7 +1579,7 @@ 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; kw...) = getFormat(ws, CellRef(row, col); kw...) +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} @@ -1586,7 +1605,6 @@ function getFormat(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellFormat 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] 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.")) @@ -1658,6 +1676,7 @@ setFormat(xl::XLSXFile, sheetcell::String; kw...)::Int = process_sheetcell(setFo 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_intcolon(setFormat, ws, row, :; kw...) setFormat(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFormat, ws, :, col; kw...) +setFormat(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setFormat, ws, :; kw...) setFormat(ws::Worksheet, ::Colon; kw...) = process_colon(setFormat, ws, :; kw...) setFormat(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFormat, ws, row, :; kw...) setFormat(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setFormat, ws, :, col; kw...) @@ -1781,6 +1800,8 @@ setUniformFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colo setUniformFormat(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setFormat, ws, :, col; kw...) setUniformFormat(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setFormat, ws, row, :, ["numFmtId", "applyNumberFormat"]; kw...) setUniformFormat(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setFormat, ws, :, col, ["numFmtId", "applyNumberFormat"]; kw...) +setUniformFormat(ws::Worksheet, ::Colon, ::Colon; kw...) = process_uniform_colon(setUniformFormat, ws, :; kw...) +setUniformFormat(ws::Worksheet, ::Colon; kw...) = process_uniform_colon(setUniformFormat, ws, :; kw...) setUniformFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...) setUniformFormat(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...) setUniformFormat(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...) @@ -1833,13 +1854,15 @@ setUniformStyle(ws::Worksheet, rowrng::RowRange)::Int = process_rowranges(setUni setUniformStyle(ws::Worksheet, ncrng::NonContiguousRange)::Int = process_ncranges(setUniformStyle, 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) -setUniformStyle(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) = process_uniform_intcolon(setUniformStyle, ws, row, :) -setUniformStyle(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setUniformStyle, ws, :, col) -setUniformStyle(ws::Worksheet, row::Vector{Int}, ::Colon) = process_uniform_veccolon(setUniformStyle, ws, row, :) -setUniformStyle(ws::Worksheet, ::Colon, col::Vector{Int}) = process_uniform_colonvec(setUniformStyle, ws, :, col) -setUniformStyle(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}) = process_uniform_intvec(setUniformtyle, ws, row, col) -setUniformStyle(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}) = process_uniform_vecint(setUniformStyle, ws, row, col) -setUniformStyle(ws::Worksheet, row::Vector{Int}, col::Vector{Int}) = process_uniform_vecvec(setUniformStyle, ws, row, col) +setUniformStyle(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) = process_uniform_intcolon(ws, row, :) +setUniformStyle(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}) = process_uniform_colonint(ws, :, col) +setUniformStyle(ws::Worksheet, row::Vector{Int}, ::Colon) = process_uniform_veccolon(ws, row, :) +setUniformStyle(ws::Worksheet, ::Colon, col::Vector{Int}) = process_uniform_colonvec(ws, :, col) +setUniformStyle(ws::Worksheet, ::Colon, ::Colon) = process_uniform_colon(ws, :) +setUniformStyle(ws::Worksheet, ::Colon) = process_uniform_colon(ws, :) +setUniformStyle(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}) = process_uniform_intvec(ws, row, col) +setUniformStyle(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}) = process_uniform_vecint(ws, row, col) +setUniformStyle(ws::Worksheet, row::Vector{Int}, col::Vector{Int}) = process_uniform_vecvec(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} @@ -1896,7 +1919,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 @@ -1920,6 +1944,8 @@ setColumnWidth(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; setColumnWidth(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setColumnWidth, ws, :, col; kw...) setColumnWidth(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setColumnWidth, ws, row, :; kw...) setColumnWidth(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setColumnWidth, ws, :, col; kw...) +setColumnWidth(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setColumnWidth, ws, :; kw...) +setColumnWidth(ws::Worksheet, ::Colon; kw...) = process_colon(setColumnWidth, ws, :; kw...) setColumnWidth(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setColumnWidth, ws, row, col; kw...) setColumnWidth(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setColumnWidth, ws, row, col; kw...) setColumnWidth(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(setColumnWidth, ws, row, col; kw...) @@ -2001,6 +2027,7 @@ end 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. @@ -2021,7 +2048,7 @@ 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; kw...) = getColumnWidth(ws, CellRef(row, col); kw...) +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? @@ -2030,7 +2057,7 @@ function getColumnWidth(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real} end d = get_dimension(ws) - if cellref.row_number < d.start.row_number || cellref.row_number > d.stop.row_number + if cellref ∉ d throw(XLSXError("Cell specified is outside sheet dimension \"$d\"")) end @@ -2116,6 +2143,8 @@ setRowHeight(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; k setRowHeight(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setRowHeight, ws, :, col; kw...) setRowHeight(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setRowHeight, ws, row, :; kw...) setRowHeight(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setRowHeight, ws, :, col; kw...) +setRowHeight(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setRowHeight, ws, :; kw...) +setRowHeight(ws::Worksheet, ::Colon; kw...) = process_colon(setRowHeight, ws, :; kw...) setRowHeight(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setRowHeight, ws, row, col; kw...) setRowHeight(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setRowHeight, ws, row, col; kw...) setRowHeight(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(setRowHeight, ws, row, col; kw...) @@ -2161,6 +2190,7 @@ end 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. @@ -2183,7 +2213,7 @@ 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; kw...) = getRowHeight(ws, CellRef(row, col); kw...) +getRowHeight(ws::Worksheet, row::Integer, col::Integer) = getRowHeight(ws, CellRef(row, col)) function getRowHeight(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real} if !get_xlsxfile(ws).use_cache_for_sheet_data @@ -2191,7 +2221,7 @@ function getRowHeight(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real} end d = get_dimension(ws) - if cellref.row_number < d.start.row_number && cellref.row_number > d.stop.row_number + if cellref ∉ d throw(XLSXError("Cell specified is outside sheet dimension \"$d\"")) end @@ -2219,7 +2249,6 @@ end Return a vector of the `CellRange` of all merged cells in the specified worksheet. Return nothing if the worksheet contains no merged cells - # Examples: ```julia julia> f = XLSX.readxlsx("test.xlsx") @@ -2274,6 +2303,7 @@ end 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 @@ -2302,13 +2332,17 @@ 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...) -#isMergedCell(ws::Worksheet, cellref::CellRef)::Bool = isMergedCell(ws, cellref, getMergedCells(ws)) function isMergedCell(ws::Worksheet, cellref::CellRef; mergedCells::Union{Vector{CellRange},Nothing,Missing}=missing)::Bool 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 @@ -2331,6 +2365,7 @@ end 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. @@ -2371,6 +2406,11 @@ function getMergedBaseCell(ws::Worksheet, cellref::CellRef; mergedCells::Union{V 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 diff --git a/src/read.jl b/src/read.jl index 80018925..c701ef90 100644 --- a/src/read.jl +++ b/src/read.jl @@ -191,7 +191,7 @@ end function open_or_read_xlsx(source::Union{IO, AbstractString}, read_files::Bool, enable_cache::Bool, read_as_template::Bool) :: XLSXFile # sanity check if read_as_template - !(read_files && enable_cache) && throw(XLSXError("Something wrong here!")) + !(read_files && enable_cache) && throw(XLSXError("Cache must be enabled for files in `write` mode.")) end xf = XLSXFile(source, enable_cache, read_as_template) diff --git a/src/worksheet.jl b/src/worksheet.jl index a80a2969..431365fa 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -56,8 +56,20 @@ 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 cache is not being used (or is empty), return `nothing`. +function get_dimension(ws::Worksheet)::Union{Nothing,CellRange} + !isnothing(ws.dimension) && return ws.dimension + (isnothing(ws.cache) || length(ws.cache.cells) < 1) && return nothing +# @warn "Dimension for worksheet $(ws.name) not found. Calculating from cells in cache." + 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)]) + return CellRange(CellRef(row_min, col_min), CellRef(row_max, col_max)) +end function set_dimension!(ws::Worksheet, rng::CellRange) ws.dimension = rng diff --git a/test/runtests.jl b/test/runtests.jl index 23aafc2e..892fe79d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1725,15 +1725,11 @@ end @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] - for i = 1:6 - for j = 1:6 - s[i, j] = "" - end - end + 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) @@ -1799,11 +1795,11 @@ end # All these cells are `EmptyCells` @test XLSX.setUniformFont(s, "A1:B4"; size=12, name="Times New Roman", color="chocolate4") == -1 @test 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"] - ) == -1 + right=["style" => "medium", "color" => "FF765000"], + top=["style" => "thick", "color" => "FF230000"], + 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: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 @@ -2063,11 +2059,10 @@ 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 XLSX.XLSXError 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 XLSX.XLSXError XLSX.getRowHeight(f, "Mock-up!B2") - @test_throws XLSX.XLSXError XLSX.getColumnWidth(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") @@ -2106,11 +2101,13 @@ end # Skip empty cells silently in ranges @test XLSX.setFont(s, 2:3, 1:3; color="grey42") == -1 - @test XLSX.getFont(s, 2, 1) === nothing - @test XLSX.getFont(s, 3, 2) === nothing - @test XLSX.getFont(s, 2, 3) === nothing - s[1:3,1:3] = " " + # Outside sheet dimension + @test_throws XLSX.XLSXError XLSX.getFont(s, 2, 1) + @test_throws XLSX.XLSXError XLSX.getFont(s, 3, 2) + @test_throws XLSX.XLSXError XLSX.getFont(s, 2, 3) + + s[1:3,1:3] = "" default_font = XLSX.getDefaultFont(s).font dname = default_font["name"]["val"] dsize = default_font["sz"]["val"] @@ -2135,10 +2132,10 @@ end @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 XLSX.setBorder(s, [2,3], 1:3; allsides=["color" => "grey42", "style" => "thick"]) == -1 - @test XLSX.getBorder(s, 2, 1) === nothing - @test XLSX.getBorder(s, 3, 2) === nothing - @test XLSX.getBorder(s, 2, 3) === nothing - s[1:3,1:3] = " " + @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"]) @@ -2160,10 +2157,10 @@ end @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 XLSX.setFill(s, [2,3], 1:3; pattern="lightVertical", fgColor="Red", bgColor="blue") == -1 - @test XLSX.getFill(s, 2, 1) === nothing - @test XLSX.getFill(s, 3, 2) === nothing - @test XLSX.getFill(s, 2, 3) === nothing - s[1:3,1:3] = " " + @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") @@ -2173,6 +2170,82 @@ end @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 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 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 + end end @@ -2292,6 +2365,8 @@ end @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) From 5304da98d1bedc9bf1e17ea90bf40688cce5a40a Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 1 Apr 2025 22:28:23 +0100 Subject: [PATCH 058/154] More tests --- test/runtests.jl | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 892fe79d..353dbda9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2211,8 +2211,8 @@ end @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, "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") @@ -2246,6 +2246,38 @@ end @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 end From 0fd3cee6b065296af7a50a02342945543fb95140 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 1 Apr 2025 22:47:26 +0100 Subject: [PATCH 059/154] Test valid range names --- test/runtests.jl | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 353dbda9..3252befd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -158,6 +158,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 +183,25 @@ 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") cn = XLSX.CellRef("A1") @test string(cn) == "A1" From b5040b96afcb38d862e884ac6864f609f61d98a9 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 1 Apr 2025 23:16:38 +0100 Subject: [PATCH 060/154] Fix logic error. --- src/sst.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sst.jl b/src/sst.jl index f2fbdcb0..c23d119d 100644 --- a/src/sst.jl +++ b/src/sst.jl @@ -41,7 +41,7 @@ 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 !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.")) + (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 From f13917857b64c196861bda1a6f997881dd1999c7 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Wed, 2 Apr 2025 19:38:38 +0100 Subject: [PATCH 061/154] Include changes proposed in #287 Thought I'd done this a while ago (my bad!) --- src/read.jl | 2 +- src/stream.jl | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/read.jl b/src/read.jl index c701ef90..6eeecd2b 100644 --- a/src/read.jl +++ b/src/read.jl @@ -471,7 +471,7 @@ function internal_xml_file_read(xf::XLSXFile, filename::String) :: XML.Node if !internal_xml_file_isread(xf, filename) try - xf.data[filename] = XML.parse(XML.Node, (ZipArchives.zip_readentry(xf.io, filename, String))) + xf.data[filename] = XML.Node(XML.Raw(ZipArchives.zip_readentry(xf.io, filename))) xf.files[filename] = true # set file as read catch err throw(XLSXError("Failed to parse internal XML file `$filename`")) diff --git a/src/stream.jl b/src/stream.jl index 36384f73..c0dbbf0c 100644 --- a/src/stream.jl +++ b/src/stream.jl @@ -52,12 +52,7 @@ Base.show(io::IO, state::SheetRowStreamIteratorState) = print(io, "SheetRowStrea if !(xf.source isa IO || isfile(xf.source)) throw(XLSXError("Can't open internal file $filename for streaming because the XLSX file $(xf.filepath) was not found.")) end - - if filename in ZipArchives.zip_names(xf.io) - return XML.parse(XML.LazyNode, ZipArchives.zip_readentry(xf.io, filename, String)) - end - - throw(XLSXError("Couldn't find $filename in $(xf.source).")) + XML.LazyNode(XML.Raw(ZipArchives.zip_readentry(xf.io, filename))) end # Creates a reader for row elements in the Worksheet's XML. From 247d32e5676e9b3e880b5694ec9e1a8e10d16a92 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Thu, 3 Apr 2025 17:38:00 +0100 Subject: [PATCH 062/154] Single cell range of one empty cell --- src/cellformat-helpers.jl | 8 -------- src/cellformats.jl | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl index b1d2d55f..cac4fa75 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -411,8 +411,6 @@ function process_colon(f::Function, ws::Worksheet, ::Colon; kw...) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) - elseif length(dim)==1 - throw(XLSXError("Cannot set format for an `EmptyCell`: $(dim.start.name). Set the value first.")) else return f(ws, dim; kw...) end @@ -423,9 +421,6 @@ function process_intcolon(f::Function, ws::Worksheet, row::Union{Integer,UnitRan throw(XLSXError("No worksheet dimension found")) else rng=CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)) - if length(rng)==1 - throw(XLSXError("Cannot set format for an `EmptyCell`: $(rng.start.name). Set the value first.")) - end return f(ws, rng; kw...) end end @@ -435,9 +430,6 @@ function process_colonint(f::Function, ws::Worksheet, ::Colon, col::Union{Intege throw(XLSXError("No worksheet dimension found")) else rng=CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))) - if length(rng)==1 - throw(XLSXError("Cannot set format for an `EmptyCell`: $(rng.start.name). Set the value first.")) - end return f(ws, rng; kw...) end end diff --git a/src/cellformats.jl b/src/cellformats.jl index d74dfaf1..ebf81c10 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -2125,7 +2125,7 @@ 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) From e5427243e4be3a1e5965b501c30800eb1a1b3e08 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Thu, 3 Apr 2025 19:34:10 +0100 Subject: [PATCH 063/154] Remove most string interpolation outside `throw()` --- src/cellformat-helpers.jl | 2 +- src/cellformats.jl | 86 ++++++++++++++++++++------------------- src/cellref.jl | 6 +-- src/styles.jl | 10 ++--- src/table.jl | 2 +- src/types.jl | 2 +- src/worksheet.jl | 2 +- src/write.jl | 14 +++---- 8 files changed, 64 insertions(+), 60 deletions(-) diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl index cac4fa75..0b6992ca 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -547,7 +547,7 @@ function process_uniform_core(f::Function, ws::Worksheet, cellref::CellRef, atts 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) + cell.style = string(update_template_xf(ws, CellDataFormat(parse(Int, cell.style)), atts, [string(newid), "1"]).id) end return newid, first end diff --git a/src/cellformats.jl b/src/cellformats.jl index ebf81c10..b9199d32 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -189,7 +189,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, CellDataFormat(parse(Int, cell.style)), ["fontId", "applyFont"], [string(new_fontid), "1"]).id) cell.style = newstyle return new_fontid end @@ -328,7 +328,7 @@ function getFont(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellFont} 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] + 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 @@ -433,7 +433,7 @@ function getBorder(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellBorder 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] + 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 @@ -702,41 +702,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") - 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 - 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) + 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") - if kwdict[a]["direction"] ∉ ["up", "down", "both"] - throw(XLSXError("Invalid direction: $v. Must be one of: `up`, `down`, `both`.")) + 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 + 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 - 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 + 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"] - new_border_atts[a]["rgb"] = get_color(v) end + else + new_border_atts = kwdict end end @@ -744,7 +748,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, CellDataFormat(parse(Int, cell.style)), ["borderId", "applyBorder"], [string(new_borderid), "1"]).id) cell.style = newstyle return new_borderid end @@ -988,7 +992,7 @@ function getFill(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellFill} 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] + 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 @@ -1171,7 +1175,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, CellDataFormat(parse(Int, cell.style)), ["fillId", "applyFill"], [string(new_fillid), "1"]).id) cell.style = newstyle return new_fillid end @@ -1593,7 +1597,7 @@ 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] + 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 @@ -1746,10 +1750,10 @@ function setFormat(sh::Worksheet, cellref::CellRef; 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) cell.style = newstyle @@ -1970,7 +1974,7 @@ function setColumnWidth(ws::Worksheet, rng::CellRange; width::Union{Nothing,Real 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. @@ -2064,7 +2068,7 @@ function getColumnWidth(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real} # 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)) - 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. @@ -2275,7 +2279,7 @@ function getMergedCells(ws::Worksheet)::Union{Vector{CellRange},Nothing} # No need to update the xml file using the worksheet cache first (like we did for column width) # because we cannot change merged cells in XLSX.jl. - 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", "mergeCells") if isnothing(j) # There are no existing merged cells. diff --git a/src/cellref.jl b/src/cellref.jl index 51754db3..77934272 100644 --- a/src/cellref.jl +++ b/src/cellref.jl @@ -188,7 +188,7 @@ 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) @@ -273,13 +273,13 @@ end # 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) = "$(cr.start):$(cr.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) diff --git a/src/styles.jl b/src/styles.jl index 45ed85eb..d6ca1eb1 100644 --- a/src/styles.jl +++ b/src/styles.jl @@ -81,7 +81,7 @@ end # `index` is 0-based. 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 @@ -99,9 +99,9 @@ end 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 @@ -132,7 +132,7 @@ const FontAttribute = Union{String, Pair{String, Pair{String, String}}} # Queries numFmt formatCode field by numFmtId. function styles_numFmt_formatCode(wb::Workbook, numFmtId::AbstractString) :: String 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) + 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"] @@ -232,7 +232,7 @@ Returns -1 if not found. =# 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) + 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) return EmptyCellDataFormat() diff --git a/src/table.jl b/src/table.jl index 1dc66bae..b29b4b49 100644 --- a/src/table.jl +++ b/src/table.jl @@ -68,7 +68,7 @@ function push_unique!(vect::Vector{String}, sheet::Worksheet, cell::AbstractCell name = _colname_prefix_string(sheet, cell) if iter > 1 - name = name*"_$iter" + name = name*"_"*string(iter) end if name in vect diff --git a/src/types.jl b/src/types.jl index 94e095bc..ed0632de 100644 --- a/src/types.jl +++ b/src/types.jl @@ -493,4 +493,4 @@ end struct XLSXError <: Exception msg::String end -Base.showerror(io::IO, e::XLSXError) = print(io, "XLSXError: $(e.msg)") \ No newline at end of file +Base.showerror(io::IO, e::XLSXError) = print(io, "XLSXError: ",e.msg) \ No newline at end of file diff --git a/src/worksheet.jl b/src/worksheet.jl index 431365fa..f615be7f 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -375,7 +375,7 @@ 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(get_xlsxfile(ws).files, "xl/worksheets/sheet"*string(ws.sheetId)*".xml") && get_xlsxfile(ws).files["xl/worksheets/sheet"*string(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] diff --git a/src/write.jl b/src/write.jl index 78e626cf..0b2ab407 100644 --- a/src/write.jl +++ b/src/write.jl @@ -234,7 +234,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] @@ -343,7 +343,7 @@ end function abscell(c::CellRef) col, row = split_cellname(c.name) - return "\$$col\$$row" + return "\$"*col*"\$"*string(row) end mkabs(c::SheetCellRef) = abscell(c.cellref) @@ -804,10 +804,10 @@ function process_vector(col) # Convert any disallowed types to strings. #239. return col elseif eltype(col) <: Any && all(x -> !(typeof(x) <: ALLOWED_TYPES), col) # Case 2: All elements are of disallowed types - return map(x -> "$x", col) + return map(x -> string(x), col) else # Case 3: Mixed types, process each element - return [typeof(x) <: ALLOWED_TYPES ? x : "$x" for x in col] + return [typeof(x) <: ALLOWED_TYPES ? x : string(x) for x in col] end end @@ -940,7 +940,7 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path: i = 1 current_sheet_names = sheetnames(wb) while true - name = "Sheet$i" + name = "Sheet"*string(i) if !in(name, current_sheet_names) # found a unique name break @@ -978,7 +978,7 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path: local xml_filename::String i = 1 while true - xml_filename = "xl/worksheets/sheet$i.xml" + xml_filename = "xl/worksheets/sheet"*string(i)*".xml" if !in(xml_filename, keys(xf.files)) break end @@ -1020,7 +1020,7 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path: 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" ) push!(ctype_root, override_node) From 1733c2978abbcacea4e7f79bd09fc00a4d84484c Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 4 Apr 2025 20:33:31 +0100 Subject: [PATCH 064/154] Clarify use of `definedNames` --- src/cellformats.jl | 2 +- src/workbook.jl | 33 ++++++++++++++++++++++----------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/cellformats.jl b/src/cellformats.jl index b9199d32..82deadbb 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -702,7 +702,7 @@ function setBorder(sh::Worksheet, cellref::CellRef; for a in ["left", "right", "top", "bottom", "diagonal"] new_border_atts[a] = Dict{String,String}() - if !isnothing(old_border_atts) + 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]) diff --git a/src/workbook.jl b/src/workbook.jl index 674b520b..cbf5fd8d 100644 --- a/src/workbook.jl +++ b/src/workbook.jl @@ -299,24 +299,29 @@ unquoteit(x::AbstractString) = replace(x, "'" => "") 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). The `absolute` argument is ignored if the `definedName` is -not a cell reference or range. +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. +written out again, the status of the `definedNames` is preserved. # Examples ```julia julia> XLSX.addDefinedName(sh, "ID", "C21") -julia> XLSX.addDefinedName(sh, "NEW", "'Mock-up'!A1:B2") +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") @@ -324,20 +329,22 @@ 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) -addDefinedName(ws::Worksheet, name::AbstractString, value::Union{Int, Float64}; absolute=true) = addDefName(ws, name, value) +addDefinedName(xf::XLSXFile, name::AbstractString, value::Union{Int, Float64}; absolute=true) = addDefName(xf, name, value; kw...) +addDefinedName(ws::Worksheet, name::AbstractString, value::Union{Int, Float64}; absolute=true) = addDefName(ws, name, value; kw...) 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_sheet_cellname(value) + 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) + return addDefName(xf, name, value; absolute) end end function addDefinedName(ws::Worksheet, name::AbstractString, value::AbstractString; absolute=true) @@ -346,13 +353,17 @@ function addDefinedName(ws::Worksheet, name::AbstractString, value::AbstractStri 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_non_contiguous_sheetcellrange(value) - return addDefName(ws, name, NonContiguousRange(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) + return addDefName(ws, name, value; absolute) end end From 31a41f95b8e62dde1058f0577324c6b9007991e2 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 4 Apr 2025 20:37:10 +0100 Subject: [PATCH 065/154] Typo! --- src/workbook.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/workbook.jl b/src/workbook.jl index cbf5fd8d..ee7cdef3 100644 --- a/src/workbook.jl +++ b/src/workbook.jl @@ -329,8 +329,8 @@ 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; kw...) -addDefinedName(ws::Worksheet, name::AbstractString, value::Union{Int, Float64}; absolute=true) = addDefName(ws, name, value; kw...) +addDefinedName(xf::XLSXFile, name::AbstractString, value::Union{Int, Float64}; absolute=true) = addDefName(xf, name, value; absolute=true) +addDefinedName(ws::Worksheet, name::AbstractString, value::Union{Int, Float64}; absolute=true) = addDefName(ws, name, value; absolute=true) function addDefinedName(xf::XLSXFile, name::AbstractString, value::AbstractString; absolute=true) if value == "" throw(XLSXError("Defined name value cannot be an empty string.")) From 3027c77b0fc6025a7b312aaeebe7242df03e586c Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Tue, 8 Apr 2025 15:20:34 +0100 Subject: [PATCH 066/154] Address issue #88 Allow indexing a `sheetrow` or `tablerow` with either a `UnitRange` or a vector of integers. --- src/stream.jl | 1 + src/table.jl | 1 + 2 files changed, 2 insertions(+) diff --git a/src/stream.jl b/src/stream.jl index c0dbbf0c..f30e0a72 100644 --- a/src/stream.jl +++ b/src/stream.jl @@ -284,6 +284,7 @@ function getcell(r::SheetRow, column_name::AbstractString) 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) diff --git a/src/table.jl b/src/table.jl index b29b4b49..11c25cc2 100644 --- a/src/table.jl +++ b/src/table.jl @@ -326,6 +326,7 @@ 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 From 6a297d1ed3228bda19f1bcf6b5a81bc123064d6b Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Thu, 10 Apr 2025 15:05:54 +0100 Subject: [PATCH 067/154] Updated to fix issue in #292 --- src/write.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/write.jl b/src/write.jl index 0b2ab407..c3e2b50d 100644 --- a/src/write.jl +++ b/src/write.jl @@ -569,23 +569,23 @@ 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) + 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)))) From 9fcd3fb8ad2c5fd254e1f60f70a33c97a6520694 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Thu, 10 Apr 2025 15:41:00 +0100 Subject: [PATCH 068/154] Updated with tests for #292 --- test/runtests.jl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 3252befd..1db147ac 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1142,7 +1142,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) === nothing # 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"]] @@ -2297,6 +2297,16 @@ end @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 "merged cells" begin From a0f1aa7b73cc354a3915b00d42011b74bbae8694 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Sat, 12 Apr 2025 16:26:44 +0100 Subject: [PATCH 069/154] Support merging of cells (#241 & #184) --- data/NoDim.xlsx | Bin 0 -> 22424 bytes data/customXml.xlsx | Bin 26400 -> 22934 bytes data/testmerge.xlsx | Bin 0 -> 9960 bytes docs/src/api.md | 1 + src/cellformat-helpers.jl | 17 ++-- src/cellformats.jl | 189 +++++++++++++++++++++++++++++++++++++- src/read.jl | 2 +- src/relationship.jl | 1 - src/worksheet.jl | 3 +- src/write.jl | 12 ++- test/runtests.jl | 68 +++++++++++++- 11 files changed, 270 insertions(+), 23 deletions(-) create mode 100644 data/NoDim.xlsx create mode 100644 data/testmerge.xlsx diff --git a/data/NoDim.xlsx b/data/NoDim.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..bcd046a9a5777f8ab1f86eafa7f38197b1666293 GIT binary patch literal 22424 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 literal 0 HcmV?d00001 diff --git a/data/customXml.xlsx b/data/customXml.xlsx index 55c580acd172374a82b224a1f32a1c4bd041a67c..8f521397576651042d9efd7edf47aa3f59e34a24 100644 GIT binary patch literal 22934 zcmag`19T?Y_CF5Cw$ZU|t7ALq7#-WTZ5tiiwv&!++Z{Xkr)TcW@6KJ{dEcz{JgG{Z z+O^O2{+v^hlLQ7q0RVu20000W0I1-OlLQ0-0LXv_0Kfo10dTRPGqC&aY-(j}|JMNn zt&62aYAV9#_<#Onep|I#Wk>LwR6+1iAzlvC!;TGCZS(_ZTm7-ji(W6ZEJUs#IaAXi z{NWkg${sqve2f*+JNj!3=5Wl$iWi46Dyw)#Dhwnz8Bj!NuU1zte#_e1cmssUGK3r= z*^l&5jOCrVtC<7EtZ`8?aI1ZaDTUOFu7i&hJKz?)jXNiwx24P1( zzyd`8$}`aP4k?PmCc;gLK$D&^nh^dkXD8zI$h3*&S;?&fn%7T{gO|Zk=aYsU@G?kF zPPF-ZUbsQusr1R+J6+t0yNZ)XJHWkpJmo`BW6XNk2gPEb;!b2nv?9^2eR>f^G`^wC zkrH{q0w5TGNqkHlc;syga19{665-Mfh6{7wiR?1p{4im;FYIwRDUgpDv`v!UIlVmu z=m><&QyYCj?LJ;fdwxUczHGCSy2ojK6M5niF8a}L1{-Y#0axi}W zh*MH@_fa};M-8r^_iJn`oeT&1htq_x)`VJ>7s&dge^=;Y^onc@SnS)w4cqH^68QFU zu^G{&QNaFBDwgw5f>2I4ApeCIK10AU0xqJ-3mDorxze9KIL+`5V)O}*d2evsr@e_y zj4hJG&+J0M{tYt+Q9q%U`TbDGBf~rQAS;J_m+|nnpi&toCS_wV(M_^-)4OR|Q2kG$ z*~^f)O?!_WoS>s8B~U791TpZe)FfLlmUbpaRQ7^zHKf1A4~L6fK@E3lx;3d`x!R&> z>(C61GHV6VUb8(dp@F!=U!2+KC>Z~uo8(G8JFp!UKiA9Wpf<4mqB)9%=36?YnKJo{ zq9q_?KVu%_9JK_QdW)nlWE0wDPGoT!!z|mBblZU0w^NV>IcKL8FXn(ms=CKSg{pBG zOM?{WZ0exr3kS}{TonCs0HFskNwU8}Uj?Nx1C_}Ws=-}XqX(DxKAQ;~j(>v_>J)oT z1sdwcIuP4bx2jFcRWi$_mbdZ>`MucrSuUn8#xx2p!3k^2bS{bUfX1FF$(&K9T?k4| zWmha+hkI8*oeI3NI=+3mskmMBsjn5W@aDzlSK>NNZ@CpSmA+BSn|aKz8@JQ!fiCy` z$)i=Kx3hEb0|C4i;-Cx%$3q<_Q!W3aY|j3}Ci}PlQ`YDL8?jFQY|XK)6o3mMF9zal@M+#H={Ja@Lb;mgwH2g&}ZvT$E*B8 zbaQ3gsm1HjX1^m_Gjb((;MwTu*Hhm--@A6FWR=M3`s=olSC^LwpXQrIlZyA#UGnMW z>3H(F*6M|bmQV0`a9P{>g)JwpHM;Y;S*K^0mjxcDUE4wvWZ|CdRwq<#5&92Jr>Eo~N zO=YdTlcgzmBNSQa_d69y;p1*?o$DPsfc|hH^%tZondE`EjlsYmK;8A+CIQ1DY-ALkMEST=wxD_Q$3V1 z=+4p(R_!+HvIL%h^@WR6-xaHMb*mpyEV3)dZ%?le`sP0h)!1LyA^n_Pu^umb*+(`! zUum-2Z{qr8jZNjBLqEJ#y51SFwD9!aI+s7%UXE?wTi){4-*&Ga#*ds@US6IbF3{I| z99Vc?uO63mbT+BmqS`jEO=dT|nxDthS+mf~y0~ArKd4tPHBg^!!>6s@kE8E9wa!-3 zJ@=-q-X3+%&#e|VHl5{L{T;l&dZ|}+oqe=kyc^j$M$|;xkvDH1w7*O?-K!o|dClOg zd~ho1e_YnA96Z)f8#nMNpQsjGPbs#ocbHlq>RdW|Yj~rdU*D`(mTAltIBY%?vhm$G zwH?9DRx)0`iJkB2c~(7Cz8sn~c(9*Mn!WG-+PoZ1UtbTV`taP5uEjt0qv^DAKAq=_ z?s|}!=SIC({{B%?{jdQ|X4^D!F?VP09V3M7obh!4QY!Hg*;W9RjC3q`5U>SZ!I*{p0opa2Dfk;47B6PXw}8oqQ0lp7x+q#&h)bC z^t*@Y0~<{Xyu+8Wa<=s$7CfDzUC$c2o7K*#I?9txZ>6s2iJpkBGCBBiNW~BkKrDdb zIUGPA#{mn5SBAWAbsiU!8eY|txn?=DBWH;%_wea3ZY>vHn~6)f=2+Phj<2g#HQqe$ zPR}n_JLN1<1ReQ{SRhb9B)(xj_;ScGAZbJpG!=*#>O@|Au38!nfzHmO1If{+ec2fe zk~T{&RpB1o?;DctLq0%ibf^EkF@#19o^HhL0|H_j0>W{VZ_+{RE&VHh4zU~}0*Dz< z+*f4I7iWWE2j`yf;6hG6CDBP4*7vPpgksE57J5Z>W5lz*>apmjOhn=NLh#Kn*g!|8z59b{v1+#Naj$c z?cIvm9 zFWRQ^y_q8O6^OHL>n^|Fhl)wNwV0sP7=5f!=_Gr`iB2{dZdyfC7E`?N8f^}pA(x;q z7~ew?MUI8-)ttm&A%m!;Cne z7C2ud2ZNJrzu5fBQ;*)8C89_t7(oqnV;csIL&{VBi==atp_@PREYajA$=mNnmvGqf?uqv?j{EAd%@drIjTfy*#+aM z^f;|T5-=(R2xI2N@=1es2Wgt#@muc1tsZ<{61BROdeaW0>&SY<8D|sQ|6|rdGmlm*_uwTp(YvLrEO2MRfsDHwAxc)T zSZY5Y#nu&OyKQ%CY_K=lPJ6MfR8!tZe6#&ik4lBz zj3h}V)e+r{4YKgC#7U+eO(`$_{cB_lD%}aI2=gXRJV_S|aJ2{+k48(3-;k3}E=b2524#l`?KX6qv&VtHzACk6^v=Dn zqlK_gTlSL*0iu}MC$q@Sk<9ectesmMWcVtuHX#_|l_AzCLeYZ#fUxV|{$rX`hwGJC zFn04knk?O}17g?RtmR)+MWMocsOw;+j2!o0kiE}XYN7+bOA{~TL#}mV!(6ZXc(8TH z>JfANJte?Ma7kk6t^0M|DKXoxL<7E-#4Odkt63#Eg*TpIhwg+VaZrEwo>*Ep&rj_3 znp3d7R=`%;I8DUaCtNLb@xVVksv|GxCUn)Yk3X-S;K?FHF(q~{*V1C%wiDjeqG2xI zyK^z|GW)WE<*K5N-mE;dKaDwe)kQG=s&ksDS#DhDqUnd-z&jKB-;AL!aQh4X5sfx! znLG5{M@|C`1_(HIVE$ICFw8~Zr#J zOCL4v*utY54?1g`9Kqw>rcnLb{ z6bop$&)qePAZls7_Y{O!clrWXyBl-HF2p7BhXoRhYgEQS5VIhK=0mOA1=B!`{H6GAyD0hZ(^Pk_yG?(;Mj{r%jufm!Q2)g3hS$f5b2@v*C(Wi#~q3 zgP^^t-1GUhZeBN}oza#s3~ln6ewz=OPdi09?#faMW=|_q{65^dYLmwwgdMvrV$v*T z_m64sHO`t$Nwy+Jwum$)JDeAtL2OQYA~MTNSY1}gL!+`>;Gq1a+_c-pfzY+S#_62z zt05pP0{p^pRB^XtN9HrxInjT5Mn37=@k6YJTps`BkqB`vB3wJ+2&%YS(TqU+22s&p z!j^t6IvcJ?nZ_T85Mv-xlKNYrpJDIQx7-GhWc*Q>dK1$u;*1>Y)dGS_#4N}&q z|I9xu3>h)&?-;PxLm{v7{AxdJPPyaL1`;;GC9={VtERLD(FwIIXVc}X~ipJmc_ZBRJr}4QWhT(|5tP6bx z=AV{9EhK+Bo&vI<)%pcGyj@qy8lA|)@37r0KAZ0yAN02j((9Mby{_;d!bjfpJcdyI zHpf20FoFev5?@z<1 zEU>bV08iDIIw35)!zEYR?OEN*`C97X{*|He0;}r*w62z`CjK}o;i|Zi! zhEAS0&^5wbNpf zNskKt$F6toXAD07zE==BE_t3j1Ja-r%kG9r8xc3fnOrAkGp+ldTE=nSU8R)NR>Pce zSRJ``0R)^lc25k7_%E9mOY66+7|8(~30PyUp@p>YKIQ8n@kRDPM)|K~V7vNxw>EO^ zBFH(ac%_)Pm4fddQCig>D^zHP*y=_KEY(=xAa!XY1?F593)xMdu}QR{iy%J_>%#e0 zq@3^%ZRvmx(gm(6tOr>=>9jr6>!;zx084;7Tpz}^E?E%l%DlfNM^xUjCB z4s*Q{(9YWPRQoBXR-_t7=^4V@M7K}viUzv1)meAmr!A@2&aECs^5YUC`xRYn|k(eI0MM)|pS8>M2?k%|txDC?CY5(~0q zQXnb3v!T}USN?T$w|yR@A}%l(qrhT{(*$R_b!9fvD*VWR9sM0)h%LDvXnYnH z`3o%cx32Z;bCzw#)v^scP3lkj6|W87()FM3jlWWwE#VzXf8Ia}Bwmq+S(~kQzfPOp zSu@BP0dc3e#g`GT@ zebyjz_q;gvyIpHpceR8{Q`KFX07xa`-o7bg6}8LwiQ{61sPt=D z==iW5Q@tj!tCR1(k;&)%wEt-mQdjNj*c#U&sM!QZh2xA(T2u?irtE4WzAAhkf71FS zZV|I@WbpfzIPSLYPb$B;F#oK6|9nyl4V%0E-{e2JGW@dxnV9-L^z-2x1~CV?;h0^y zT>R6933yX-^YgZ_m7~Qo>0k8PILrB+F-MS3pf4fS(oZy9H*ILP^lfJKy3;Ve0UISS zQDP)UP>Ki;^`&)35a)>yt=4vL>_sku@`5qrDK!5=%FB@NDK=1IK*GYjki`@ii4?it z1zdoL3T6W=x%S;6?1}Yc*A8Z3MUqK1QerH{fQ*cY3{V&^o*v#16RGOl+Z7&)Irt{f z@L!M8$3}>Yz=}Mx)KxfwUiG1V{wgCX;ow9PeICE+sM`0ljBfY7)6_D1X~1?A`DF8p zxj_o}S#hN2&t22_w`DEObP*zV$!znN0dx{ZH|dBWEY1#*AZ$}w<=>ANVIgu|(N~0S z9$!ySnS1Nik37>t*2m82!b5n_dO6&7)ZC2?z-!<+7x-8X%VS+o0Xvx`l@>+vdj3Y_ z&h-w?=j9G#)ww)4xykqw6E#9Y1eGW^_R0vm%Ww2^PF{6ygYe){fa60bytummPkOsN zAiN~Hb0!8nxb18oP#gEf*kI#JTJQzH&Qv&a3rG#kN!yB{=Kay; z4i(jkhc471)Ce(`6Grk%_Erh*cV6i*jh zqb=_KhRm*?th^p==dE~~>h9mYxYWzLE|5J>j$|$BO|9YaBvQkQ;=jU^y({t396Cge zQfFKg_2EYqy`%yM%vL)5EcpBiE4XPcQ~!b=)m{=o&7StNC{f&v{nXF6dDC@eJHdA5 zT-A>&X=p@=w?b;cI?jMHYXbePR2sA?`B1t|W@PD-esLALC~)ZReh1FpALP9rhTSDb z#<1HR$AJTydR3;)(%#gYk;cPHhVJNI!!hXeYWB+Z@#)IyZR#Q^e3LD2^{%h7b2GJnUz{C_ZEGaW^4*s^vTZ;<7kly)wRCe;laB9r{Ln!^8LU-dbh7 za*@kV7WE@<-P7}nCHl?F{bOSJozZe;*kPO12d^eiC0pCLN6V=hpO%y9(i~eUoz|ws z!DcdicD9e%(doT+t9H~ zyVhDm+0*^obB*WG*120<+GQ&qTlObpc`&*-M;DL2FCiRN0h7Zk)3pmk{8>*XdzRvY>71F6(Jht>>JHa$US5zBzf*;P09^vaWPmzfcov?~t zF|nD$DGs7!tJHZk0){QgR9W@#o0Rf7isrFi#S$E{@mZ8FW~odo^4xmj@_1>5 z^A$3z2;^Sg(}~|R*$!>DK~b)@n073=fITbU{|VZ||M+fo{yDjCKmY*HpA*!vGqkX$ zqy7E;e=Ye{#Er}J(Zl#$dWH=@%&*2E2uM5gi?tCb;dT>WM6V3WBjvce_X6^hfN|MG z=zG6gv~W`2F`Z)kid|*Kk7|PAL7UhV;s*PABdp^U+&?8X1z01(^|?}-`MFXG z|EDSZUf$5Nwl@E_1=5Odt1`P{2%d*j?PxRY<@!y#ZAc=WX1d=41kjeq=9X|~!p|q_ zR}q+ZHk;0tS`b8_&cs4@PwX-|wH!+`rxy)lC1nFt5)$<>VVTtk4SzDK^G57<6@~5$ zE0EZvZf7JKPhyt~;kyBWP+Wz~p6?u?pIPX?;Gl&TllD{Pr{qveB4MqB|w(?9vE z)3Eobv5*O&w3M2s6`-XgT|@Qk2J2QYM-&q4nQFx-lz(L(sR6j3&!8>Etq_CvrK0yP zN0;~+2KsevHX|$*PbYxq<2i??W`bBDMMN)Yxp)PE^A0UTo z#{ES!Eo@2#GU8qePv+$;t%)Cd2Kq zv$bE3cAi_OpaS2Tby!s$*6vvdj0Dl_3s>RyqgcGwTP=g% zEyGLxG?iHk!L6cGu3W^U${o_ALX1{4@+j052^5&|?_Y$z#XVMK%3$$pDPC(jvK$F7 zl#7pIs~erUQd5Zq1G+A=WNVxT8R@|ZII`$>+(%9YbI^-6E_0Lrdduek$z@HOreDDB z{mNV~g}f{V9(5(hptmjuq%)g16?~mD#T?`|hpgEP-CS%(wwq)-45!BSXK)YgC)k2zC_h8wsrCQ{Z8L&Zo(&S>& zkBUN0h8HPO$%lpL)s>>{!FwXsf&7j;eNH~b-#!p6gwf)83?Hrvjr&)laCjsO_<-4a< zy$T&{bID^LV1U>-$)J7IB4vBK3#hRt5={_+oY4j%f%1<^%_w}KYcdp?A=*7dK&&3^ zTQFRRRhv;{2>cu}r{W5_+nTfWky;h|V=9^Z?(!3j0$b~$nxx`pkg@^X2tq?-Co#1G zA9-)TVpis{hn?;xVC=gkFe}R6`JI(jladHR_CQCt%XV_c=S#N3kOD>$M^iHx;-n=Q z?JGQAdJURU3nnQHY{X#BCjWfa7B?ZsCKMkeOCcH>BbweCkd^Y<7S|kN2+fv`r$&BD z($?%ZYe=wAI6@Q>2xWxiU!$$+wgQ+rwAKVEBG^0eVDmA_T=;Cfew0X;0n|oqE@)A5 z#0m13tox@?8hVISB@~V#1=je&WY`a3E-%>RWA)`4E|eak%sE%wQoXG*JuUaE*7 z!Y?R^oDGIBl<`hzasHMjuU|(8N|05}Xd3UO_d3XEWD6w9V_s)`<9bxdvd--%AcT-H zvU<2B~?}WNdB}jAE06G)8rL zjooQ~S8onwYL45xgg-35wBoTzR-DZs;p@4}!uh$63>Bjr#3OSNZtoLQa)CjtabbW0 zT7uCNro$mf8##78_kPoo*vRvD>RICuS02~PP;55p-ZEiQ(~(i-a47Q!37m%Ed}-NA zvHiTGU7nY>uXdhT@1nQ(cmug!bD=v8nx3q$tlYbZZ)?7rR&eV@v&9$G1cSo_^5qZ! zeCxCPtRyR9R^L0JyJ2Yk3*TORmAD>$0d1E-mduQ1E7Uthl)lNkmR|(oB`f2;e5EZ) zp_PTMp@HUzb!c1qX8GDxH^Y%K*OOHir*H??)05B1n$9Q5A)vmVOH}J!Xb%M0bbQO* z4Gh6E$o+9YiZ1+(*I8UBvMOYY8WM4ZSkcZ@M4aQ75)~P$%9;;0MSypf@%S`_T&fxEgUotZM)wXmK*kc?ZIwtAH zy@hR}fV-nPf_m1IBqoUix-dH)6E^fg@e_7mTv6OSMyjT?xP4J+GWM!oTai*d6Nf8p zFx_0+(TPt2Ve;+`9*r^OkE)_k1ei|5t}HR7_}RrPaqU=1zkOI#isG6XcEm*1@s#z3 zCz1I%f9a(Kw5z6mHpMDgD7`cTaoZ2j#@&MTTgS^@?m+xf)sB-ZvDi9ucQN+MWxvDs zp7`-LOLe1~-eO=12YeC%TRVMamu$#oo;LizWCfgDI~e`U_VR9N$R@Dj)|o%o@9{#y zo|-OtpqH%m{U_kL_%F;3H7po+Bw>{9^Nv(!tSXw}nS2R%zdq#k0p_zGtI@VtaD;B) z&Bxm`!(X)aTQ*bjG*)7)D2Oiw<%lK#P8HI*2(YG?2^RZCKv^u)#Dj%`2CfZQ`=z-M#lBM-El%HuURh3Hx#3*lMi}T*pQS8I#2e zM%<%A<&XxaY3|BOO8iIU=D~vYSAbWs()>e9j+>HfJ&k3gWH9Ou$i6EJ&q~aspre2R z@WX^%b6*vdBda+zDTxyvS4xt6ApDwN{`hCY+8@KysRH~=R-d~Jkv|ibhM={TgQ1m! zj*_d5p}p3>rL6&FN$XX5n9f6$&%&p1=5xPX$ZX#VZWF+{kCt+1Xy6fOtazq3-->wM zchxiuxax32|5scTVRNTVHx&zlyvSU%rgcCY?BlC7@_J=n{%V2*zw#uH*ndK_jeWTi`UIV*HmcdMxEb9^KxPmF6HCb!$vak=(p z0gD2ic$|KnL9Rizsi)mF*j&kbfVmRvRncC%eLx89$@|l*rK2{P=);D}`C}65DyHY1 z!%NUD?Iul(3*6;AZz^6Jf1?=YAk~hoS%ojk+a|k7?_MzYl5Q8<2A`9zf}%}03NqR{ zImDehcYdq%B0D`#GQwZ#{;cy9R^gVfBR;A7p?5c&CyrKR34_nA&yxPO`hlU(=IsId zhhQ>#(-!&9vpn;l3q5+>cLKinH$&d+SD=6DJT0{mP5o2n<)5u2;-}6Hto8rXCtKSY z{+q@<18(OAg1 zU@Nosoz;*C;fo+7RDB(LDAMZ?C;iV2O%qZc1##!%ZPFDuE;nmK@K3zOB$>5%GJ9)s zU{vE~PVPis`HLJcgkR`Z0=8}?qbq^(FPJja1SdcD2hlM$HO5o4&YK31E1Ty0s-cbo z6`}^<*!YzE@7!?63d9sRQ6{X~PGvL>Kj-Rgpa2MeUH&LSTNx33epDKpR!a>uQ0jMP z#+hN)z^&6c(_x?I*!h^Dj+l*k%m|1>Br%m;fqH%DRr#R8`(Nb%6L0Usek#ZHvxEH) zknv}$|KC0Azqb7ybJCLi^ax;E?%u+~>xC7qVa)5{5bpD+?LiLDJ*?s*W2J8oL1C*j z!EkjuLr`v4oO{v9~9gTo4&p!wuUM z?7FC`I9B0#Udc9;mCLrLkMjT}yVeNFGXfEcJz$BSRK3_{*#m5^$abxygLnx!ZqR7C z<#$|8!jifTpmQvwr-E<5EdWe_GS$fk4M*a&m+F|S#|yw4L0Ywt10t{GI%DAJP|4=X zJGNTX<^=`84`XWH5%=`&zoSf}|I_MuLe%fBJ}sUI>R)#~GX9(KHza9Tr_-YhZN5-Y zZvZcS1ws^0lq|dZKEr%|eTIQt4q!CcqBPt20+&2xluKUPN{+~S@WFTZ>fsacP(&>Z zUMWFvlje@v{N zwlGP#Ho&-^*|5i6HMHPoaZt&Mx?wJRq?vq@dVk~4HxXegW;jL1@Zbpjo=5ds&q4e3 z6!I(u*yH3-@}G4YgH|=8#VhCGi1BKGXxOn(Z<^^?H;zxfsHzE$GK+Vc2L4(AHVE_Y9jdX;ZmyZ=t_=1YrmPoZ zl$vI(=+k45JWgleNab(SAmAIrT8JhLT&fYcxPDuEj%&tKQEc06OTSJO6lso*$zrcA6%sLoRYaLzb zIKp74SLDufEC5!FhDf&$wI+9xqgSQ{%C#6IX^1l#TCv+#%LkE;yH`t4_@k=V|-bzbO?2_oB1hD_P}HBcix@4 zmgA(}VDJ8Gic@8JOI1-?LxUt*Y(6mov?2@M1!@o#V~9a#=Gf$}7bs8$WR=FyQ@km% zyjI%>Or{S9{vzHAF$vy&CtxABZA~xv=e~KtNAwJiLS;~bb~aXQ9~U-q zu`fQfWRXSu1)2&}eluM2KUC2MDst?>=AZ{fR2vzPO!O;#Eu#TH{2!$CSt>icu%`-i ztPo#vb>iJ7Cr6cpX-FY6pl{~6CMB~b@!#jvCrS(pGseV^=qZ#YC{Tu518yTIJUJu! zX=_JY_o^18T{gy!OOWrP_xi+s&AoJknFmlf&%~)Q2$wQeZI%x zU}9)#_~-Y(BGl*V($=f27@ZF)2(4{N1|xXF>%B?mg_biQk*m3)>V-6<2_`WPNO|P+ z^+j4>Fh@aJU}uee^yS@9>`w zLPD|&MCfD9!=AR90-!wAN9TQCcbPj!DFxdC67du>2{;)9vE99|FlXC_Ma)3}gp7WK zRb>Mr5Mc`wz8L*JTs4arwzuX}ivZ<15;VZ;KNQDGEWFA*I)Jb<41fqja(bcb;|XFh zOX=(Ps=u0pJ&WB`2}r+MCcRfB_REDD^A2+L0huDH24l8iRKOq5)0{y;?j7eL~qP1ZO zeX1Fgt#gUCHCkh5*}mLM$y0SsNXo{;Vie4&3=uN%vKdM>SLX1Ugrr&KwMVk(!uqef z)J4Y*?SX<5J_pPfc34en=8bj*w&*55M@=b>^7fUkO@30O`D`88aD*NwNoo&vLkR(a zC0HD*nN`k~EK!Dsg^3;zu*3fZjO+*f!59FZ;7BYI8}QYa2%nUQ*{5^w7k&>s9}51B7#i)9{4fUnEy;yj>ltV?zn= zTxxf?znz>v=zQGWNa9rX=+Z@=?jYFy0>JSPW=;^9vJ7=Qi<`0nr~wM}4LCxuL?jOZ zM;5F1AytswVu*iI3vXuPmAhSrUsBb@+3`Z448f5Qk7w+-c+h8SRZAWA9pxr<^&8bMxW8}17<#z})8fw*8}E<}CU znQThcmoMlusX-xfQx`-N4O?2Q@=C3AibqhBD+KX!f%Lxc4KbEtrdTsSg0%tqVzueM zfSOKed65%=`teZSUdyut?V7t*)2pA$M>8NLOUXnVRE|3jx)_bs*Mt?P36dHB#5%Qs zK_{xu>Z)Fbj$g?^HdqEga0ht?bmz6H3X^*01_X9mb_L@cAGEIb}Yf3 zE2tb3Q|U0JeJ*9|IFbHX>?m0Ymcd(}z#YxvD}klE&T;aATt-LsYiwEBuuENg#^G@1 z<;0O~KAemf^?g;5FHZ?i947D|!Piyt@D;Ac>yAyBWMt%mX(1f-K#3qv)@sO=p+To? zvs!70?!#HhrmdsCOUF+{3cCrcjnrwC+W-wYuC*g^R@RG6^}*~2tI?BmbVPo7=kjzk zD}GK|m~M*eI=OyUKOj?VZj9U~^QTTE5wF9b`Cd9E2U?lngf zOGno-=ZbZcxaSuk07aZ)D9gmp`Pe$dM-UhG$;&x ztqUdd&9K|np8x?EB$#oYhF#~{j6Wf-&`@RUBQT5Q=7wkTSUD;8I4&Viq^K%#6xv2SM9dkm;4I#pVU|^D0K-7%%dxwo1`0b1t@B9HVamV zthutIl5*l7XoXqv{lL(dS%ZN&&iJ$1(6C!65Zc_>!eoB@*7qW77QO0fBJ>{+Eeu{Qu0`ry$P_T79|5|J`JrYvQFX)0D|>V{P_ zC-vM->ZjaH-SV|drU~3dYn5m}lMYccYB9}~Do`n-EZH(7@>ZO%CXIp<5@L}Vm>$Ny z+0Sma-qZavvtq*Rc>enAoKQdk01*DWt^ICe^KXTHg_6AW3O$0?p^A&wF*5skYo!2z zGH5zy$@-L7+t5#Y+J3?snwPKDI(%FNzGy%5&8#~w+g?20&+I+wDkW-xPV>JJx$%aO z1Clt9&U*;q%~rjcOMJ0Qyi%aTn1*F{i@)6|nFphmu<55FXZr#A1C#MU2oTj7Se9yX zZ{^z;Pm@Eb!xj|!X+1Sx0VhOZ6_^DGO8;_ePW}~gIF(23X{qq3Yf3V{YKd!gRWVa{uzlikb*b*DW)DtjF<8F zjAS^yMqnUMaBfwu{h3w(7n8s~REW7G_NO5(5AhM}3;cTCA5>N(Dq`ttmQMZ+a==N0 zxzb*yZ8o3)ttpPW8b9I8HxuHMM})Pu&@W4@o!%#?hKFS<4}RfcJ>O0SVdb;dIi+_r za>rugZn@c z+#@|h@6z!`N_IcP@`d+>zCmI8rUKN9()*i9_$0DTuY|qSYxgy9@NP{dj0*NQ@COB& zwLI~=0HS)PYozj_;w?1BOIlAm4!kaq*Xv~v(DzdS2G09h#zj1 z8s+G9>&{8H@BagYbOh)Tg?`qxtDm3W;FrFmy@R!-s-*>;se_^AzjlL`6FhLepLOhZ zSCD=vp_=9cAqN@s!Hr^9Ly#7b70EUk8gLWHJ{2n1-as^S(1~4n%%^MFh~DXX12+i zl=u}IQaUdnBZ!Zs&wFl|Urd^+oWiFi=|SO!rkq<%|7Wt&!p|Xx;j-49QXu+V#Jibvo2@s!(Ot(Z5o>6o))@W;qm^{&UAIO*cwI-7D z1GlkN#Im99F^cxh@lC%7N0`WWU%wHV7q!>x8v`PQK-??#foV&u1Z2vEYH@%Xg+&n0 z{*YI{j0K8_?FFYGX<%PP1fIp$j?!8#qb=D~v)>xnND)-j%*v!A$1IbH%D}oCbF=xW zUlqZ}F-1jJB?yeuBx#65gFShDQ#L!gp(D!x&ofF#TZge)ywY@b0##I@G`dhH?DR}) zpp9&3#!>CDXCGrheHq_Obpyy5u08zRO$3N#Al@2~9(@96`1yVHSeauxX52oXq zwsnz$|y+__>C}2zMM?BVxeuOI{?+p zRMp1#8!9Y@EEa8bgL?~zc&C>D&lS{;Ba{kb80W~2U|H|on$U*76+LOb7G`6j)wj22 zwR}KjR8!p{KrW1ie0PRjH(fS$@m=eybON2MEcsbM?(=9o-K9IeYWK6l^y~A;xs`&K zU+zTe)w^*Slj5vou31A@Kt`&XZu`~TdqU{t79UT66$^C!M8EExPt^O}N zzR`!!{bgXIXQOXpR1FXZ1_K3y1%vxl9yCXcR}c;X9dMZ!CKP4>rsQiQdJHvS*dwD4 z01-6PDozj!)Wiq&5+)`Frnc?AeLjGxR}cg#DLzh5KaA2o@PZ=Hf-Hxw(2rCfRkYzp zc#xwLP3|{=L=48}s9)^}ye<#3z#c|HdCT}Ptm%*vdIDyEA}F{{_D>~7tK{9_7?4!H z{xGy)LAsj=bO;(g1b*QnGFYH_x+oET^7K$5J>(dmL@@0DPD0#*z}#~_6|-R+*4A0~ zdQb_vAHVqW48x6r@^{aKjbxxgceH#*$@W3*Yd16<)Z>Rs{;t<8^M{DHYbednDLajjx>^A5(8n zbnnit7Oia~+|v0kCUTkGY2i3z3eE;r-i`-GA$Au!g)W=L@J>%zp%!}L!iXXdd3KA` zojdtSzsQNt3k#&;9>}s7{j-}6X_4G*<{o>DXDK`xY$mAjt16o zezWj=`q)xwfvaGnvLh5K5<2{_Rdpo$Fkbj5aMrehj`OD8rf!gaJbEZ9SUVRGO)j&N zP&s$&oD&h8YLtvQG&ufN;2oc(^m3k=|7LiYK{a|_^~?QZW2Wz=b=b?g%VN32eDh(^ zP`qOG*19cZX*Qmxi0!3KYT2%lkV4rxPx-o5bo8Jzc8$+jCt7;%=JE}Fljp739(8qW z@q9gUr%L6m>b2Iwk^lKTGB!Dn)o3vVLg1_JFWf;SXRGE!h1iTYBS@tg5uRG}%MmjyNhSHHU0Y5M?z85gI zVXd!Q*#;W%+7P)K!_Ez>I@EZ4TxdQsjQ~INa5lr`7~Ap>T)8Rc;nh=*dT1Fmt7}>Q zYF|_EX!%Z|UDa4WHam}6urK@Bos;T$MBWqJ8V9)4fRB6XxzpR$HOSl`6j_N{tWpAE zQ-Q&f#zf43A_^ufKC}vsGGqrI$xc0Q7U`S$E6OU>BTcxi1_`NpqPPH-Kw+y!JrltStzL67pc_+f$~bJBI0`h0-rftN>Z?Yo(36S)s_(56#f`Ne zgx%>s!bqUKGXmEo@vX3d2mVF=2Qgj?G}s!adjImp_?~h5y8RW2(Nn)W+%Iga-Qi2{ zMQLUV2nWTHtbB2acVc><*>{8f8)=2B681hWJ!}r7;XE>NTyM-#oqsBRc?itn^s{`C z{Z#(HDE?pT7{i!xvp#x+fXl#k|LJvYvdRy?b6q*;mLxPc)N4*ORdu2fg52A6s#(R^ zSw^lik3QC-^sGE8^`hZ=i|l-A0U}emgPGdshs9>S*qdG$JyL%frhb|AixSK=TLf)( zS+q%9M-(*5F(&I}73F)=#6!o7d7kKOd+kA%s?lb;6APULx^_DFO=>6;WIfGM(#vEb zpLk@mZ6x(65PRaXK=>JLpFw%?Jz|x}!9D_M=os_vQ(2xmtJ53zx1hWiswT5CtnlUgX<40)@30>WWNqO3dUiaA0|5z`S1G%z(oN2772xrn_-48`lp--TMb5 zAg|7l%YF#4(ue0;S1gZXP^%=+Cyk16s8Z->In-WQkj?2hb}T$~0j|fF!1?=nU|`9; z@oaOkM&>y;6FEa->di=MFF-)l}UewPRbR*F0!)Y2W)er&O{F0jY-u&V|FMkt={{za(VE*Y5b zm&R%9G-D@WsXW5KWu;(&)AElKtl6`ZWyq2d!w6+bwuZ*`+CmYcWLHSGgwSG7;`8>s z%gnr*`DdO#uHSJ#_Z-J_JokB?*LC8xx&AHD97{Hl%82vAp{_mGTaoqo(G^z!-Ou#7 z){0a8!f)s_l`q|Xy$^JJD#Gd;gMfu=KN#EyI?P;Em4uu27 zDp`80)HX6|7E7fWx#PMmyKgkeo}z}hsEyPEBfL=du=BugAD}B%t{(5|*nZmN?rUX) zrHkC=6XLoHUcOtPplB3X)3{Je3pL>z4uTde%d-EN4Bi-lWpb$(mLcRRQ(!a7Pt=}l zt)}>D>{f57Poq0t0i!mw>r*(WfS#~-Q_fAWI-$&j9F4H2yC-8QizvfG5*Dc6BlzjR zuGVhYgSLxz8v7R{Ru_XRfL=3gS(|SfSAsxaP_$t$AFb%8r48%$_w?M(?;0s<-tn!f zxNadaEa!37efe=(jmwY0bUB_zmR=1H`*Cc;b07#%GQI%V8+aEgi?+E3H#U(&#P z_V2h-Y?{Y3H8+&_2l$*M1UJd>x{T2%=U)|r4ZTQ- zFNEHz8vvrj#$((rUj<%x!=UdtZewC5ZGcVWr%tuvZ?E9dWT zyXj)Cqbn{{_XO7nw^sB<33Pz(ff54N-hRKK$}&mHEdrIZrc zU&GDZKzA-eVPZqgF}NDR&5VQc1^o!hw4Xn&9R+xW78GMj_6N|R_`b6Tc+ggQqt5#6 zeH=S(3YcRtqBbY67`!=^V;v4ucBh#D;d$kX*qqBG(!@uU%v9usu#3n^jEZ`~)rZM~ znABpNYc#xRdrN4ze;}6~&L#SmzJqTxmq8yu9~kLhn2qu6z4k&mSFN=DVsQHhCw@TQ z{*8;bv_i4qoI#+8#Zy)O${nE~`-$%_HRy+HFB{VZ;wzRKqDpqGrQKLe9i9)JlvU7k z8(Zs)(ApwsG~TX|Wq$~laLLSkqLXhVyUbxO$KtNd)~_gUDC-;Oyo6Sh-{e1SEL({Z zKY6OOnEMnPuS=Kxj&3yXx``LhV1=P%TQ0@HL>IJ@;($V@ITTN&jOB>GWiA1EP8e1M zL(f#`${mSbi14Y*SsZueOf`w2=T61*B*@fRF4w-4UU0D05KF+|N~(?Yf~#(phnnzn zOvMUU+3zHOYT@n>tvy;}*`ZVsl%WgeQ(xE;G6_C&(Y5+y5d!BFtz-T52jcYP@^AxV z@uhPsfr&CjIxm)$s)X5hai>MQPbFhltsi`id_r>>?AHX+i<%ZZuCQ5CV?2?QF7%@XUHjG7k+0RkS$%{U_azRm0#1NP>P0Zhg$OZdEd znDowk0X8`*O3rA*1(CfwCLKOp0li<6AZ~+0?kxIH$PkzbZ}nM12H)QC>XYViY7C-6By_YklF#}b?svcQ|5+&g(_VI+? z@AWSisZ+~K^$t9buUJi!Z27J!Yh$}z#W(5MW!v?bm&xtx?f0IVqfyM!JZY-+>xP#; zrb(lPTKN2>Ue?A^ZR4`^pVKr)!IQTYZ=?HIID;bm?Z8;wN{O2YO6Z;Wk0o%5<8O6z zzV@I?HE){1VS<+BPhrSM4Vp|k4Vo;hZX%;*E$-igCi$RV?SV35ltm4T8@jMck?%J) zm-!38ExGsZ`0yc5cNJ`N`c966?_T|RrFW5PpQ>g+W|VIl!uDe&p5F*tXccF|A1?BkF96Yvb?{w_g1bErT*I*u)*aFVeV0WcEXx`RF3} ziELljR7k<>bnSQ#&;fK~Fmv|A5Yw*vsxP(0Gf%a|rKX|LxSO5gHl-S?Or^P!mZdkS zJ)=O!JQ7X!%TMp?X&IacDf71hy{j^nbILXsFz{_J>T!8fuAu6>FNV@x1PnV9uI*fB z%E1x5lT5I-#Sn)~cb+0S78KxyGWDa^PhHLscESZNZM|CGt`77q6*Xl+F$E~o;a^j_ z`aIoX^$gokQ|L2Y=e=l%yY%4Hwk_&uHVgdKg-esymP#0a-Y#xwy7!^_W|BGT8$XQ` zF#BXWr>%{e6x)G!&v_7iVg0|l;^SoJjdC>gJrG&0{WhGAK4^;>*Zi|BMmJ0pAfCrA zqQ#ONnahU0@O-v|Z+v{YeS!fLJ~^5JgJuFTo7s2ojVm|KuAMELV!MYMfydAGBPgB3 ztqcZ@;)~jMzq(Rt95H1;0gd8;J2N3Fn;M$(s~*o8xD7K{izbieUg$86mx|uQgF}b& z*uxxyT52&~C4o(HyRUAH* z9A_|6z6+WL5JS&x@j-0J$<#JoJGNXcJTCvT)+`>N4UtX*YF1_=^Y>1i$8g7$#`wM( z35?4vgB1I#ti~g=%C>Ie@$}UZLgrc{ax~2kd@jFwSR?U=4a6V16n)j9n++V$!okhq z)rEOxZgRTM(twiJ44-VBqUvmJIZ3!;@GfGj1~qJ`w7Gw;Y+>ONqqTWGAEAeNcQIWt za|Z+|;7;>vJ|X`H@F)%ddEg2D6ghBRjPm4?#@m;{JZX{TBGM}S+9=J&gee9C4|xRb z0MjQY1)Z0;KaPDhdYmXwCtNFNN~I1RyX8A~K@@59B|fov6V_uK?;jUY9c#sH<2pZp z*sM8W2!rQyeJVC-R~1~%(tOn}2@1XTX3Aka-&nh3a4n9;jauaG3fjo3W-gci8SRR1 z*1Lh=sJR&_PlvQh`6~8;yatoRV-1918k@NnEceSym(y(@{KyFuX<8}VKR7Z_Qb|(( zZZPM;sq(K+LByXwemAC*oI*O2?{5YLML~qYfuwUlIdnoFIfb+fl9Mn0Zz~OXDQS$ExYXh_ z#bKdlasX*2mKb0lPVwJ_EO|L;YLmEpLE_iaf0N-P=a7aJi8)%5|Lh;}pvGTEb8-r4 zUW=F#B129&G`&R*Bn>SQ1792*l@Ez9kpoD*Sz>^oGST+_<q&I4y7ErQ>4-uF$E4KrW_`ek*|nUS|Vmy>K@MeS7;(BEvX2-UZj?=Mi+h)hM)v;~c?4V=YwtdpQ)?WMF-}|0(o&Wor*Y(sh zlUYyAsxj|zk9*drl9d1kK>+{*fB*mhAONrn*t_Wk1OQkC2LM0@fB@3?W@GJWWbLS< z=w@r=phfFyWr?2)0z{q-0Q6b^{~rH~EzqAhYP~{_AoKwG3=h;4C`cBbOHOSvRX@tg zC!ehmgrgDg1Y~l$=)HmX#wWLEr-*qdOlN=I!ZaH*(-!G6hsC&zp>8)LDG16hV$he1 z%A3_Wmn9gO?@qfU1sHprI(6 zXr8gk7TI&W!39q`N8d7vwqP9Ok-iy?;V%}8rnXXDS^I}bRD7h+Lo|nqMMtRew_WY= zN!{qjkId1z9qYVq7#l{w+?R4SbZ%qxuSL9;Z>f1FO-k>gyLpz5$^72RSoCCzwdvz^ z8=t8C_y7iw{U5I1phQn}`*}t3)3c#JU0=uE$kKt1_ILSzUH*Tupa09y%j2YE`siVT zE<|60hacwFVh{u*T=+%X2o$}1#8%-OBJ+r`*Sg4X5EQZefknJKyk19E*0>^%hY9Yt znajhGP&kPiT`Gc-pY5E$DM{^pxZk&Iu@-RRmQxv1K2}Po#M-Bo!XP@`i9oo=&Hr z4m;?ZELD08TN2zoVJRt?a+p=?XWD-ybki}k?7k69>q36>ppi}=mM3IJdSsjs86wHP z^H#5Cz8cMN>t}=PD;vK2=@*Vyy#5&l|J_N5QqXDiK>z^Gpa1~iKP}^GN#|l?Z=r8v zWAQsSmMd$??69MF>X?0eV=ZN-OB+TlY?{pegU zgQkf{qOFrZ>A%0bYFRkYKG?jet*m#aR3A(WjS3cBFpa|17mm5uTdkV$*-)E1`lA|AE*-X_5t-+W$$osVQyRDL(cnq`=EJIzzwsCy z8+s)Tkz`{~jTdJH^?hO%lcx^p99*qsLiHRPLzp)*h+n-!83d@9|pC2jvrI5-qH*f?C@T#yj2An0xS@SgE%lq`G7N?)>0edP#*Z#W zXo5%%5~I6T+ppj(@tT3d0T>mpsvTWX=*>iak@^$)lbdhk9KH&iWDmAMwDT;NB@?9Y@6gM#!_;jq>>~^p#(o96PnhO|t>`|-Cbu@I(aMloP!yHG3EoebN8HcRJo8z+- zYSzZ|P+yhX5nQjo(KQl{vb~N81zF_`U#_}Z5>E|d7RYTIt37J9l!|YH=E0wZ2Pmp& zXxA2Q!XvMBqd|PwHN8yAXx9oPW%J`L>DFSs2rOAaMqDk*oe;OAlx~AD@Hbxf$P1D^ zE1Z6X`(gmyTtL((O|udrV2Ei4q1RJm5M7>6W@^oFYC%VTmZd&(FiJ6T5n&Ceq>XBf zY6Z~=F-y8#+&wa6brYFXPgkC($z@&PANJV(>)5Dz37#>GrCt^is?FwZ9!C+YW+lp{ z!t2pY>Tt%!-%vLaXf6tVtU`!afL&a>qCUfAA^ozwi%q+tfBm8jXVUq@wVKnLt9r7H zQ;V*w@JlA-{O}0^Ex%Hny?llZVU#m@05A^BJBIFQC$xvQJwX-n^1dIPx&i2n+~BjdJY5 zomSb7yBLiJ8bRPd%=(-)@+Rq|X1m3^iaJm|A9gzTcl3)Me&k6ci1wtSUf_7N3SXl| zp`G$osvtd@ehNpS=OD_~Go@p)-p3*eRa&u&vDy!^hboY{6O^rHd0#eOk9o0>)cT88 zhPiIQE2dS5WPu-)`8o-oCamhF7B^R^e57r0xav%CN7jXN!|qfS)y;5Q$xJWZ88uef zxnhxQz29#+V!g9SRIzIO;DtFn?p9hEon0N?y~O(@n}4UH>{PuW^iQHP_@pE(00_WO zI{GVY|0^l|R|p6ERPCR2|92nl2{ID>^aukNej5a1YlRi9w6#atf{SI9n=JJ+yq56^ zW97>>HpPn#)w2>yJM9D)Z@Q#)OI|6;9@#~MYM^2${(Na|xiyJ?O>Z~jSRgfxfz}y9 zpxj87bth%x(2c2mM5Sp_g0?mJ*~aHz3i8{9^C;|bmnSJQ*iye8Q7s+4W>Y1Y;Z)^+ zoB%HME1W`sda11XEk0<8zHh7~8pZF#k4A&hWQE4yG}B|4>kv?3jNmbZ0+A2jz`#o| z?uzJ}tbbb`MijDFH0m>*=16rEOO-i*S0uU)HBJzP%TW8V+e=^Ppsw{@x{molVp_I> z8?EZJZzzAJob-BidX)yM&lCH+ySa;bw(H8?9;?>oKf($$WE@r*0{{R!3IO2q>%XF$ zgQ<~`qXXTaFNWXYERe*=oA9VXZRK0Z4g=y-o^H z>g9sd8Q(}CRi1eBNB_Zx^1mcUc8jN#AY>a+WzD|;EPEyfxVcf|*S+qrHt*QWM` zGEkL?a)D}KiFScU%Pq$u56yr%G%7!Ay zgZ>$VG7%d+p}Njq@-9Ckn(}WXoqH#VXD=x=Ejn_xM{7lcX{fyXVQB**)qhANXIcwp z#%}D0*}Egdf^`hIB0R*&eLY&!q|5yxHRI~R`R<5bbcM&5^#tcNd80P2dmwS~ULgNZ*{M^PaZ0BV(>tw!y_Tc-hE9K^|XsV$2 zZ#}F9ZdQ{xNoNt}uEQOjeM{j)$ycS~UVfUe8?{q^qp>SRdA`+W%S#6$X zvM>4L{qceB=;OU!!uzpn?P{lp_wC?AhsXQr_US^$`|0v>=lynTA&Xbb|6>QsljbJA z+Xkozo5Np|k2D6e-`6z;0Es_I@_qsOm16DYZ17<%@+0%y$-8;|l#RI=?RBp&$2_Lz z{HhPWB#ch1%=`Vh`Qxd`yAbb{IOA8CTWeygk0d@*2=Em?Ah92H3Qo?5Ubc8^jm*c* z-U*)fP1eV?j!H^gzK-|DXi?Yuy~ukXz6o4YLd*Q)XmBcimr=EQB?Zy7`~YAIpD_aX zHb0Okh#I3?0Q=Ad%E6taYOC@Jc|y;75075kIpiniaWs#JYtAG|J^ovjxTiw%Ucjx zFu|J9^vE>lG?Jmw&pj-)fO2X+X(yK2$z;kAa#ZNr+SR(kw)HfxZ%L=ymW_tf!}i~g zpJVP+p;?v)fz`{RNk&HE^1zm++oo-XfnHih^T(4qEok_a zlQb)TCPheo8KX3{Ui{PRe`y=d^;d&mrEkHI;jADtG6$gy%4a8)1KhQ5zLKs@k9J#C zqB_D-W5sDw!lsA+Y%N|i$RPg#RSL-r%~HaTJW|Yu0$p3W%9MKq4&Ilv6Fo${GZ;If zuCzke2bPIy7)&Zqbd39HEl}K8GAO|se5y%q@6^Hlv}rInr;2Qev>l0!>3w~vb;H{q z%dKA%uUC`O6rT*{tkKLFL$#e9m5M5Jph=329Z;50NIoncq@#(#f?%4q zwcckNo=;p#IP9;1Xx-S}s21{BiK=*h4-*xtmT(gtT?kfi(YUIrjB+hkj5HELKp8|( zs?^*Dk4}(7_{p#IGzXsw=ve);F}<`7>EzXL{1Zzi{^2m6VlgEaLi`bi5p+W#S^nR3 z6m9W)8{O8pNu7mKB`4b}P<<6z3bMyRJ{xj-*C+q{O)J{x-41bYxK|$DPK;kHb;o;C zMC~;j)?!9kBupayvQ6N$Rk99@V5}+PHq!FD=j)Za)QS0FH0o?K*6$&P;>4nhOBtdD zEA&RF2mBBUUdOZGKBH(5tCGG$f4(W{Nbq-OEoZ*{aaq0<$uq!0<^1t9uaP2VA() zaFMmdqdUJ=G!1;sakvF!2~AjdnRwP?7AFlz?bm zGp43buv5_{`6|jZ(J9ARxmwd-?E(c0qG22#PcKF&)iQ_Q_>;)c4UOcqP~C+8Ld&Pi zctz895n5fW+N@CW8zYCs;Hn}01t)?V8|2Z31xK|2uv*KyK9NPeSW>p?gRS1;P*YNS z!Ax?50sDeaK78?w?s$3|Ohcw`u>f5Yj{nv)>qg6O+$cVLu0=I`>PT!as6A}?9eNM_ zg#0U10cPnoDmuabpjq0PR^Q44iK#oPp*v>of3+!;D`fiVe&ao4k_TbTXTlqn2^jHM z0?{S7rS37U_6m@tePe$|80gZLW(ZivW!4>cKL^wrSjZ`%!=IKp6vf6*lDi>Xa`<7O zDOM^1z=+=uo+HsCbX8=kGfZ;BClzqlq(JAkPXDQ7{O$9WtPZKHp?6hkJKY&Fu6dLYg|75ERNw^Yp{{Aq&jS~Av2#!)jz~MwcR%8TJjL%3&xO$UO zWYy^aYXghsf=yoRhGVZtV|3*#0GBRy;jcq0`0iL&8}e z?YEeEkQCHQ_47s^CE6Tk1TR&0UgVKwA`6`WFafKZtmw-u!bsV4NTYuu_HV!XwBbkl zFFwb!pH?E~8L5kR?v{#UQBnb^#;htvV1y2#j0VVmMfb*(jswvd7Rze_+k6YqpP5U> z%KVKfiQ&u!E~u8NYC>~G8w?|#M5`KOWqiRgJ@l{yR;@haMHe8g#4p8E7FpJqvd0R5 z=J>aB{BBNBVWFAi+x+GI3PC!&QVc5p;hk|KNQxCe!)iqeO|tHK561jDTOG+THG9py zPza6W$gY$?&v+$jeNyJ305jshP`lvbzH!#3u}~`4p~e!D!C1NMF>sX*0B2@q)SCT6 zeh9NDRVd@%LZ!uLx%ZQRV;kL8m;=t92@zKa{!N^%kV?+jwxjc(&(acJJWJhE8gm*F zv@whQEq4A8s!>{5$2r^)&s;!8xd|xCcm=7t1s*rWh^1M|Cpnqi`TpiNV8lj_c`|n8 z*g{l}dxLEx!?ajU1&zv$bM=uW?L+0tdjZS>rFg&B%hJ{y!A+%s&*$es!5mZh&|-tl z$kX!?#%u+5po41E#9X`iw^QzkMnu7Lc8g;0BkKZUIO}wf<#v0^_N_ z>I>L;sqjb2dQe2jG5zH=e~*0Igk3+@8CtoETs&K627>)ZdZVEr14Qwnz1+%=31 zwZ7$8%Ax#7O7tXT%2=w}NMj)#mkgl{B^7aM_n-)eV_Me5MA9g#Bf6 z=y8_(2IN^0T*1TFkYw=ql?JHc67!9uC&Ie5wm zPK?y-&;@Lg$H0NDT+l)Cp16abM9*q+KK{;QbJJ(xn#M%m^@#-zCfo8CFU11;qrKRb z*NB+AzVbEXK>5iLLOL6x7B;-sSDtg1io(S3n;t0h-E9y0yg!Q3FUBlVzeS$v7x%+U z?f6^eRwM-BeGfq^gcdJ|;|gT!Me>^YB#lW(`sCesn&Mp60%mXJz%@ysbvdM9O?Ik^ z1ph+0RTR7HlCjsqrE-y_MMbP!3_p6V*f196KZ?fbGY$ z`rhp2>a;Vt30kR|Z{}8wxSd38*hR;}3K$|A{pm{aQs`3dwSOl#aKg(pA*?q~2Fo~FP%tx&v@{_Q?p}5trRfmDL;0hCQ7`VjE zdfUpzCFGc>#K~Y+orXcuI{}x3->wP4ZjHPFV&9k0hCN&PlGYCZNck=A1`?4 zqfV?M+Ng%S8T7hhD&s_Hp6tL$aHo8l@`%*9O9a3+JezE~V$A&o3Y?5PY$(p~7dME3 znldw%sD^m`fD!^*D^N$e$koIssz^}OkRk~KuG~W5&}kpwAK&wj7x3aA3o)p zc17v!_TnEargyUwQ^|gf$Qaf6MwR2KSAY`ggtTUm3z_vG66$^=G)&lL9JjkRFXkV> z9#7K6Qe0ne_%)HB?I^Cg<5YFUsI3Uhkl?99L6iDNEQ#lbV=mUo(KMgV3Gz!p1xKw`6`wJyfBaBPoJ zWa8US`z!GbTN77vB29i0FYCs6FSG;^@96IGB^e58evay=tIKZnT{UM0#=y@G)`$N8tLVgw(wf7nf)QFo~qr)zN3&qN%gBWQwF|umh(RsEmH=?yfW;6yLf(D_eby>UIaO%kSGSSED?bP^%M6sugu^{Yh9& zjHQYMOC4HB;KL$e_z8QGDdrm;lL4>i>z9KG9Xh-&uh%=BKND(yihjjEYdT(0{T_sg z?}+svO?SA=vrQ*)z8jm06B33}3Xw^7by9KE_$PpUKZ#sjni}INR;tpK(A^wYz1DW2*V72GFt}x_C((V(4Q?kPje@xQo(FA&~9_23O0z zOH-g99~6tiPTaqB$?~PtL<>M3Bo;^WfMDT-8a#tAKYn*|itCVX6^v7=q&bJVKN}_{ zn<+Jf3d>QLiO=sXBBeRXUnd;8Ce(kff6|w|T){^=rZ1#fzXx*?<5+DUE#tmUJ+cC~ zp6&Qo&NAHlNn<~x@Vn70$(Pdt+|2s42;6rqVM*X5#OV>{_F?Ze1nQ~r1B_a@!SqZQ6f7&8GT#0Ke1Bl2+Wu$>jW z;GM0V3tnuwVn5+V#GxX}nZ936e={QaQgI`V1wS9?3)WnGOc<-P9L9UVepQPvNhgl5YMIjpzIF6Y6cl79EaKJx;;pG(&2_U~Cdsp=fZZvNz< zbuQxpn6S0Mzc$#R4IcF$AhhSF*vi+H7wi|F+;6aJA%yJ@&KizdclLf2yA}8lgU_i4 ze}BpM#@e9mQj-PAcW$gweyKnnaaL*%TaU7-xp2~oHE`{u38|YvEw}|3OKv)B^MIgx zn&R(YiI+t}+oq71FzW!qYmiRJTct+96%Xn=$-Y=Uf+^b3$N7bazL@|GeGUI}VxC*N zV!rbk_&=q%;_fDWlRvLiLjS+!A{hVVBH~w~(&!O_E~xHssW-rZA;tIz%5dg7%1Xaf z;xd6yu@*Ku$j`Z4@WHjo=ToXf{_xf7OsVmf(Uw+^1y&`K%9tUCpzu}yAuzv^{{Gl` z9!X)Bxd5tC7?hsbGnIUFAQPWg!>I!3>KCxQSAX646T=}FXk1GdtC&F^tE^>6rOl|a zraPQgj0!DyE5?l0;yZ!iJ!TMv1ck0yzxKB?It1u2_tOofT06E>w24jTrZ&$RWMQ_D zMNyKs7JvC%q5IZ~$UW>phpb#c^j*WNu&eWp5yuTYN;8UbyTV(*xTS)Fp(}+XM%wfI zp{J}~L15!8{1sS_Zv%K16yoRQkFfI}_?E804WmYN9n|*ECX4VYcC9!M!g8s%VBr`n zXoYp_=81(TPt>>*xlcM@GfbMar8;WFTW4#P> z5)fpU#aA~%h}s8$mZdDZ0Th3B0T)8|J(c#j*f>0q?$M#4$li6|>q^-{k69Vnu@kec zRK`%V)AmCy7Ny4N7n490CIHG39KAQq$$c!T6H$AF>flF+jwi?5{t!IPV0&I*u^;a; z+?IAsF|A73A;u8WTgH3iMO5jK5PZ=To4F!mv*%)2>ZBR=dp{w zHg5HorAFAER}pT9$}G)ZOzdqb(d1T^z-?P;xbSuoQ1|cendpeieX)ytPTwt&&6F8o zCZc+k5GJn7bQql3OfT}CjI$yWt0c*xS%MYK)kebi_J(kFhZubA%HVf1He8!lz{7d9 zMn6RLKE1Yptw+-nM2`0rimqu>0T6Qsok3Rl5|qSIzg8(_k!yKjS;P|LOnh>QSF4<< z$}ms5CtSf+QEX5|sqWwl&V$6Fyj^&9~#mx~zOv>|PlCpGwb`DpT4wDbijnGZ<3zHUqCz}5~UtJ>v zd#d@lv=oTo_wv#|iA6_KBP%1iKjojC;;EWMG&UPjC-|^8{xj{#T*Xj#ZB7x{eT0>( zA)WoMXhP~2X9fHcOlV)RDAMVj$xQG zVq~g_n5*qqS0W2nnPFlA2*XA8@GgF;B?HnkIf7^O<=X3 zi7yo%O+01tm1Z~rq5nw8iu{R;iXT+qNB5O~;1y&Kdtua-4qe|oxSWKIk+(&YmY%yL zg=65=mB|c4STC<_$_7BvYA0x1&tP%!kO_b*rMr|#Lo;Jsop-DTjr`?sirWckhm6Si`}kPW1JtUb(s?DmvL1*fhx*g)!Gs+Rr4Z*Vq6=w|a=4kV3vPG-Ds~ z@GXz=t*Wde-vI;h7EqC9uWf-crqc1H%16+Cd?@WY(PG>@tDlm+h3HDquHa`clZg!r{iamp&`8-u>)VnbcGHIMmf`js* zzy&~;Tg{~4p;5>4EKO>wRp1C2PlpG)V-VM7O@K4`tPw9s9HtAVbV@EV3OP~lAsL9P z@^b}o+^#Rvp__3ND9;JGUkbI8BaUj~MFS^!r0{8}vV0NDhuDInWuuukO2Kz$g?byi z#|&P7&<{m6UCM9u7uiQ38*{z_lr5U# zgqCxWN0;WHOI$aIN5*zd76^rS+IAY@hWuK-`{jK4JMZAHrGl&iULBR*&<&w`jMLE} zn^rphp!OOVgqJ{1ropk4Y|$SKK|p;XP59dZT?kVLrHC5e_l(aAX?g*K;uJ23nLVwp z94b#k-+7IqyL?&#{D!I?wZt>D5^B9GyO#oAfRk8&+a)iM*~J<;PD)E#O3nRvJ(cP# z=yc{7RT{>WXm;FRzSA{G%60M?DL8saevq}uzL*GOa>`Q)!{r|Vh`F|2d z&tPfxUozNmaWuDd3xh#cz6&UMpzMIwhn5!Ba3r@Jw+KSh=C~UMNu4C7&scqz9o>zQ zse?0ZC~eC~;UrNWa3xue_%xmld(M#C@+BbT zH51X;C|O@<#Xv}+OefeJcOa6gyIx9h^A5X5!ksLnx4aG}l7oD%=&Q6^yc@0-Zn{L| zEv*S7Cvf(vwh15NQDdXi!S(HNtzsKrJ}yzYqeEo%G<{I=HW%la@~hEct5(1)gG`%# z2R0N_YQ+Phg%tTgvu^>bLN!W;lbATUpUsHWCs2Ll!OR*Vq}w}8-~CVu)!EBr7ZXX@bk^AlOw^#I3R1Vj49*8 zf=hZ$!u3UUBD#+eRP7sBS54p+Vx0lDcmp#`j$=vNfI%qXsY++v6p6N>vg7R>1-9QB z32F&He&I@ToGq&;k_Ok>jiE98l0iuuV`?UtisaZg+W|hI&|zYCTnQX<@a*oqDS3I$ zQ+NMGIKDmRs4A3|>c!~k9o%lDWEb*P*=W{ky~?P^>5OH8Kli&#=t6ocIy(aOOxRDH znfieh=5h{BJSVBPe!uQ(L4)88R+E4QWE=Nu|JA4RIq?40LFQ1K%EhxZ8?RT4VFLh{ zP>I5PNRm|}Xc!?KXQq@XFw0mn$|}SyWOb-zNS-pmwQ8O4=K^smtfb!WsuT^m)*Z8v zx!;{Il&Ji*&nR%HDWyy0oI7~^PGA~G=$^(O=Y_m$J8N24x5QG?!e46{RyA)`EYRBy zMNl9P>x8XlE1TRS<=fVA6MOlql*}KnJEpUzY~Y0by=Kj$Gr0Thl6 zAD+<}AUFcUMwY|%iR$`sI~S|b(Ncq1jhR#(IrP20ys-_~N=Y#nAke2(q}LD0YBuB& za+WX9pW2l<)`dKdSL)-{0B3*`ox6(=MS`z4n)KQNr%EIYiKA+`E-y_t(&Nz>GawVU z&!|~G*D2&wNbt+GrjW{9=l0TKGBOm=M9H`Qx<0%#rwPdR2HYJEXS?>idc&U%!ZK96 z5N9pWvQ4RaoYTrD!L9YMbz+C6dSBo<^$i+YHO+Ccfwb+RO7FjX0L) z!^;efcAJ>`2+5{IdOh~Ycnfz*kjJkS?i?oH8n@BsA`(h(g5{w2g97M%-Cp@3e%{=q z#RbpMVrAyOxJJ_V6*F5ne*HjCmOvNRc3cdvcUU#+g-Q}Kx`vR zI0a&W8FKj+~zhYe@&|ikvwcwYLSetw{IiF zHc5a45-z6nCS{L?Zz~80%ZP+zyb!{jFcbAC78dX~T#Ul?g^~=2fEe`!f1!7zt5B1N zZ1ubSkd;D!V&|U11O8^85%ST(X9O5Z%(N)2`Uu}DgevWyA9mHGnqYP?dwq`W-S?E2 zR>>IB_aHCC35v4ZQoE0+rCHP#Uo>!12s3i#-%y|fX!1#`;|UOQe=j4U`aMe{B4lH8Dkrb zn)qjLCV_0)zdDKVu&Hnjp`cW7s|=&0h*E{1pO96Wn&;}4b`rr`BUYF@tdbhAu;lrf zC?gbW*eZh%{iTwyR5sqMAu! znx&nI1VcfW^?^b+?D64p$1N8PD};h+GKgw?=J3i3#+;Efo=>+EFLEr0AR5i$B#u}h zuITUu?_F>nzx2>zouUpd>WO#--9Lj`9Xa+VHjdhxP#6I$J|V#P9Hd7OexK5Dz+!(q z88Fg@a~bn%Y~~3?XcbH zQ>SZnHJ{o3^5p>3AMKh77~N&)87-h=P9q?yV1`TV!G*auE42^n%QOy)mGQnK#7a}P zou&Ea7fqq12H7FHY$VaG(&@{ZMu{VHEfc8g3s)Dlu-m=YUH5|!S+qJUV24j@Z9%YD z^a$@~<>y4Z$boJwG3P@R-sm#oO|a8iW%4DsiO|(Ez3X&WQPo zkO@4~VJPDPJelPBI4JZr5huVQg`R#x7hN06cDtBqQqvMXxajC&+Ti*e<$L*eUTAU? z-=zD&NS%8f>D|7AuEUAYY##B8=b#P0A8|j1gC}%`=?bT-1mAI+Fv>8u2} zVn00Fu~p%tnL5}MFM_yt&{DWZ+uaXS2H)2D`o`p)?%sL#VQDX1y`ep~U~pvTBII^^ zHMDyZmQY@A^ehRP&>=m(=L722Ez)#Ud9kuIu(c^QWKJ~&bGvE;FQuiVzE859e;H5t zsmrF(c_5_3Oygz%185Vae@cM5f5NSY@`^Pw1A?c>HlFayy0r!|6uB5GNq{mk3xApJ_(=Q~D`=Ft*aR=l5^zp;(BIQfJUlJCB14mb4fTolh5;IZZ!F$_> zT7a1WC4seILj!1@b_{NB=DXa02Uxtc^MY%^wYPIv4VOcW-}*8KztV(ww>I8pe3N@& zAv9CU%t&viX^1KG83FsYv;Ercqq`gJQ@4J3@5zAa#(_Dt3rDpi#tfozBPSLwhj~_V z5kC3jyG;dIP}~wP2ZL%w6CFtjFogp@IF|zMi!^q#j)n-mhz`dRq6|B1b4}q})(2J- z_KA7cKFt|v8B2vl@OqyaR=Z1jL6nH5TMALCS_QWUGYibtSx1w!I+e~~R8XMoy@?!{fJ3ISvY(D%i1P9VL zckU0ht3THp1QiDX}6Lp?uFO2ci^Hm{D; zvKki@@GyQTntN;pr!-ct*v(sb;0I5}@7K|<+RV>-PO3Mb0XWd9!6%HpEStbwwdfpC zFw5s)Zn|Iaavth{h*&sg7wE;n=@_OrM-#a$l9xkZ^?xTRH8D+}>zv-5ib^l6+mAa2oazI83 zxCRMscmTH%xb)F>9DP`o1oR!v=)r@?6;uO-7A|GzxHbfnDQ6GdjXPK%)9av2)sl!Q z`y&YhVzKNvEh694k8qz(f`5*U5oRmqh7A;K_$Ze!JmQC?LL=;O>Uf_IsJAP)VEMN% zriwo}kQ{imo~pBH@^_wQrI}GKvRrcg!-n4xNIZ{dr^B4cg{#iMJWMriyO(ae))WW1Am5|B=Ql%~vv9o&x5cs9MhH4T)o-sEXv0|AWu z(o37^a9HDlv1&M=sZ(j)fy$3$rE#ccX{%;Eg_x{>i6PK-MS#<#K<1zc`>B9u{pqcmy9) zQ5@0OYNrB#cw7j0ceU+`X9x+%Y2YF-7q)nMC%A3RLt zGHDiJ?S3Xcx@U;E%}>k?gm95&jLBVRcR(VHmu!dg0JIvhs`A`@|NP}UD`HwW|Tq*>-^yUZphi>`GEq+hRMlU@TkzCl;(D_QElsbf|)E3YW@@@ zta6$oTEb7mRx#@l!W2VpFV?DT8XU)6&o@nzCGW~Q$6_YOORDE!9e^$h+RWqR;t=^= zkGv|)i>l4MS#%7rCysRt55>W0_Y5d#k6P~c5{qE~PWH1tPjeVW>a=fkF$&UpgXwml z0C;5bcVp(eerH#rCgp8Att6tmvzH@oqW2(`cjr-*A%%_<`M2Hju?EsLgRj2I-JLKUrgj@4hzEgfkD1JnnzzRzn~7 z`USdxj`Q}Yw+$iuu)&i0$3<=Rr&j@Di@bJyn{5d7giDq*= zB^ly5O-UhDrS&Oeb#C^FLQc_7`-Y3qD|hI+_idq7`KU2FsWm}es9e?f&}#uRA4`2$ zL7wT?yT1~B$%#tvx}r69NFXI;7D|(gPNJ<;&G2py@%?Od#fE;CmuchjIy0kz!ylq$%mf z4n?Q@QTjMkW+-p1b{DeD>oy9e@OV%A0s5a_YCSVq;G;ixF!z7%h{FCW*#1KT&Q_L! ze-Q!$?dN&KU&+pP^g~{Ru^x1)q-$G5VWde?(no7r3bsg$U!yTNpi0al-Nbb(cl>8Pz%680C8IiXYvp*XgFWYNJ=hlsU?19ZLA34}{o*guU2nAhZ$#X;E)c)GbLBYM5Ft ze!?y9!yx}CD=tLS$&Cy=gZ~9yD^!^DFn|iZs(lgrW*J3gw@5-eK|_nCC5(!mlVuNV5H zObTNpj#}&^j-keAFtxD}51$FKE5Q}WRKsp80BmEf229`?&9kT%^@JCg%#`<$yXhoZ z=Y91>W|vj%rW<*2t~(ps7|{ZE#1B%O?Q#*0NT?tp`km!EzOT?)xntrIIf)@fm{>6} zwQ^|(lidM`=0>WvCd$b)7&7X#vdfX0zQh|m2HZ8EHNf8_26LkXlJ_|(B-m_`-E>NN zSP0IRt1}$yo*%005oMiPAWqq{{fbF&o)jfhqff=D%wAd6F|Y650WDf1&u))^BYu*~ zN1cM*d8l-@#i_TsR#lhBcCh6FrHImAtf$)VVB^)1kI4vDm|2Ly>SpAu#2yv16O z5pBAWxUtsK*{-NjR4KFqxW%Sg-_Lfc;(4_m|7X#dP`k7U{2Au%KCw*n7mk1L>HB*N z)?b_a{(B?VpDljnaXr$1?aprhw##Y9i6CHlM2kFa4KWBDFl$A$Oe{X8Ya>Ip*KQ`*|8-YHwf z!7H8tD`tqEjN&-9g~*T+1(uNyzgs)~$Zq<}$dbYICP$-JNN&}D>(0k~mx8syuerpA z-|~Tg?Bn7rZ4gV{{e~lEClq28_U-F&6_=*-Tn&jbKFNGnhhuIs;|6HDvHI9gM~E!2 zuOS6v7VUxxzr>Jpog}Xkf7~QZha5}x)wL#E%xn3zry)<l zAOFV*E;@vAb%akr5b~LABKymRwx5dI(a2uG$kFkSF8DwGW}wwKv%WTf(7iOY)weaU zHLeDT1A~Er!Gih1CkL7>%JU5l0UdCO2PPC|0H%bm5j}<)Fzk`h2Y?8gX$3ck1?txa z_97-G2Bx;%onO6`5DJec~Pd|*}9`L*Z&^-LSFk%nH9Bgiik1E>mBRt4qP^;^6 zASHvLE$)^Zfyd=}5%?oDtVpb(Ap%$$EMo>FRsJeE089*~#08ml?nIs#uL z^s8@T&pFW zy&totYK54-P21fQGLZ!eUR8GQ#b5hzsM*kdt@yd)>1XylABCQ)M5FXMv5jkOBD(VR z{N3r7;nnVuKzxGNhmE@>ht0l03I*>3u2yDIjH?4vT@SD{e8t+R;$R(VNO%3ew*T0~rg!G$sDrd;tyGSJ_glWI;C2P^~n4E-1T6pltJSWAuhi9ZY?d|od;Zikr zzItDlX$H@yp>iPRD&*dLu$Q;Se=wbVOmosTf&S!8vQ1hh^|*bRkw16N$s1K-D=2g3 z)-lf`FkK-TcBpkMo9+^mDSf+_o_?=&_?5VKukzU8ZF98csb$#Jq|x6Qky?- z_1vT-czX0Rb0+OgtI(1|BQ~M5ZHm%uDbL7ZL-ahWt!jw$`sv|4{36q9wI$4I|K$E$ z_-di-UByeSu@(RAesFkH4!h1m5`+Ms-W7f)ueeqy&&`+_MEINT|Bu2o=+k5V!>sX) z;2~Vb=atIO57y`BbIY-TqrIi9y^XEIKWBe*Hq0-6I6ry>zNwB-7gJ+8Ar*ZPB$ieu zU+`ihbBkrnX)~BrZQ9EX-Uo-U<6t!)1?ZPjus}mMm|L?gmQ@!KS$UX9Qbs*!XsFvY zX9wr>nZy#qsEP7xio!@6_Y!?Oyt`=CjBd|V688oeDLp6;Zn)|cTJzK=Y1 z^?ByEXYJo!Yp?HK)7{D4lMQwox4gd|-y&U~y3kh1B?$`Qydk<;iodx5jLkm61 z06STm58`7&Qh$M6M)iF^91gvvi$V^H`3+L6pDiyj^cnk;T<3NU!ij> z@LWe8NAe>;c(X>SfF!qWEX>0rXc`OVP9MaYNgFi>H%$MLsCd{%U@S#ECm@~}VCZ5y zkdn~$cHj`VYx1M;>p%QMVQay~OxN>RNEk@=C>G=j$~e~+78X_vZJOsL=LA0=d^ruO zbgXFFm;Pbo7`vf126W(%43(#OMW>TeN#37jAZOpoAf7V8ZD#JwJz%XDR8eohpZaOH z`cBffZtu|}J74OljQ8)Vo|@}@)il0?o?9YI~Ry*`bcYOnJGa_Xo9SZw8` z-D!=FanbBke26D&=#B!dK~46YlJ8upNl$}`-NHSAf%?^lsu(kLEVa&O>2WiP_-BUJ zF+&mExngTQMzw|$aS_arQLj$^vRg_sDYNYcYmUl18u@*0KG10`zqvQAlVUO2X^bGe zh*QB3<6!ZYCqT_`nIrI0mQne~Gm-XZ&25}`I%;+Jj}LL0`gh$Lb&Pl0?mG6N`v{2O zCznGM9m#RFH$&jzGlppN^Ac{4Zd;FPOR2uI9ZJ>g|BQRoIls=frvMW3I7m+Abe|XL zs6GM9Y#V5*dhe8!C&l36^W#0bpRqx+p~~&qW+eFOZmLXIh$YjaRf-xQMxQ)J1^G{c zBQt5f$!&+)Q7L@KQliiZ&pvwpt5f%_z2|AYA=_f^l$d;UqJ=npd7j>qD&|+jnB05C zSOK4|OEL-F0CK*15z;s!S6JRLyA!6msw_w1E~)+%)O8 z1{CDSmaRB|>@&UxPV}&_@xp`#B3y;1`Qbb`1R8*BgjS+grE~f6n`4Cakb*H>pmSxZ zfCNNPj;9|l7PCPZz_|P5TQ~324$gcby3p+;?2xY>6Rmy866UWN$@{(yD=B92+OVZa zcnv->bkr1SHrr*=1w0<)$~cy0abr1IPUFn6({{8{emQoXdJvP=yVP4(n@+6e8ul5( zNE=?I-x0MkKEJes*g&E{_aOC+sZ7f8=n84OS8&-M&jxaMi@H7<<&{AAEx}9_T$!ut zN^-X@+^PF@JFUZG#6XLSsLP8@i!oRB=6S}w)ANqDE zxng**LCGC=me|w@D8BxuH3z!Y}XaITkm}r6NGp`~Oy5P%ce+Yw1$o8xh zG!IUkqk>$iFdC{E$&-#HlwPW@S4rkg|F~G}7B2B@^y#+i zJb&o=GJwoTq(wdqzljV8*t84I+0;DkRCF)HAAjw8i)x?#qk;45L#{?so9R*%`MxwN8Y}2@mhm%`@_?5j^>0KsT2U`ZlEX8mN>qjZo(c0jj~xi zxl)Zlfk31?{2nyVI@Uy4kE14RF3WTHm@2I3y2|K0wx1@yF)kKNYcVs8n;Yg&j9){c zMO=cxJl3TR^6QVppo)(rZmKK{-qz?_j-E7C5jj;9YIn~QYD!rAXw3O4kog0-1kVrHVq&}V zCg4lt&3w7{Z;{RB?WKf(uN9U?xE5kRLCguE(Y!7us;<|!>7IXOM{vs4sQXrexm-*gZvzuJ zp4)TPslI6uH}?^9_M&LvLLf2-2i82<^Eld!dkTSL2W1e2%(d*w| z>8tT(a+ZVG6MV#MvDgi7{=jG`IGXAZQ}bhsuS5Tc+P`vINs}l>;Zr&F?Zug|=x>f? z*V}|wABJV`!@V@AuTn68X5efxWwar@i)Pr)zMJa#3lDCojf0X!sT{^Kd5CfOnm|MSj z2VPxqu**OYmzDoK)1lX1d2|B}pPhyjoU~YzyO?ce-RAC{=)OhOya+gE7GJ^PJ%qQz;wh%i522?5-hq>MiKn{XTbi zDgoEuMbDiM?nCaC>0F2E=gD}MxZX~OMCJB7ndZq<7xpzYnr59eTUw}%NHh(^HgW>g?jPnM@sYgRs=Nhd>n|$Vq{cxJB2`GGbHaPMh^xQ|+mYQh@GGZ(+ zeFfqLu##eF6{2gX9JR)4Ay|{0z351An5q1dJ^);e7&4NY)0?5YFCV`+zKe|JevJp8 ze6ew;MrPlx$Xz7LU*f^@SoePl3X7u@%qEcH)fpgy;K630fnex6#Vj6abRV*oVNo5! zWdWk1n>Uz0w~W3_S#t<}&pf7%FnHe#TblkNf3NN!7x5Jb`XS#09-|oLPNaIf=y`9} z1m1gvdGxyxKZZrTf+Z+|$}GDxgd>Hl!1-RQ0D%Aw+1h<4snf z3gj4JZOU8lRh1~MPAxw2h%kPh8f@9i?mZVw$l>s&LVg`^i`HS-gjRpK^pispaY3p6 zhX#xYB>_f#@cLfTMxTD-QY`}A?$nw;wA67WAV{@Q!?G&uS{@j8!-p5+o`s}0FJyem zjS3J>p+uvl)r*Ol9Q-I20v*~wKTx9eYl6T?m1ys{2Tx8_BcKxdx0_%rj~(G<5spqR zT%kr>KrL=&YnASz#8TVGMMq=L!it~UGFfT25%(-sx8SM=Xs~lN#1k?+J*x|S0a);* zs`bH+ClsD<_kKy_6oqFfAo#t23X;?2vf+tV7U&P1I4jvEDh0CNYh_L;SOT0qSWv#z zG`&Z1U6JrxRy3@=KloVoDIn{lJ@gAC$4Mn|4)*aKHLM-1W|I#-;UX7CzW={^Mvb@r zjWDFoOdx-L%@od+=>OkL!8}wDzmby=^Mhx9B-^*%c^;2z3JX_Spn)0rBiroY8CsyI zN0f8&Gg^n6)%Mgwk~A{gNev`MAfb%0+}N(5MJB&ziVs}?){%@XY?!ecPJ_dDJmmNg zpCGq|9KM&Blr*So_fxKUDeK&URS6Ca@@o}!8A?Nr>1ZuQM;O7L6lYqkI3fpv7Sq3- z0%NUM=G(}>$Rmg0KTLuD>1qwax=fom3B5Ocs=dal*cjiRd9LfiaVlcSoyoQZ$kiX; zXotMronjeK8Xw>iTzk>Pm-RY1jpcDxTXBKBqJOUxD5|3P`_?H)jgX(ClkOSUs3!z! zUzPRDs#Vl!PDn$@Wl%vqo3p3d=R@w=!0nz@Xr2qL@KSZK*^^jnox!nMCLJdo+FHmp zV-kHWoX8)a!|yOM4mL6rzrkLVQpEwM*KmR)8NwsActJh*CXYEQu&Pf7x$QJlW>)V~ zPo+)fzEeF(GiG4p2f5NqbKk`d!pR|Y>x#Si1-*5K!ax%bm~S=;Zp#EG_n!QAJUbo| z0-qwsG7-6?_+{SEKKt1>&g&f$NsSs{7dcpmI+-1xw!Z0)isu{2TBrM6WNiG zs!4jArBC!EtzIA#lZ6G3+YTdW@o+(+#jJw`okeL#HxYx%DWWuLguB@GeI})5Eqe1> zn_w};-Dri*L;0d+-Y=HsIC0_%fn?BIaZk(9eD?zUozrI-4NfdN$myWI#H7YX) zu_GP|`#mCgiTu%at>cy}tI0)}o1Y>Px(n^)GnbNvBPu_9RIjpA!EI(tnO(^;7z{NsUVIlo)kX^h{e4ykpKuQ%zXDIYe)ky?wj zGAGIDa8E8_MSkdK-#l`^dQ3jf298?#y8mANP4ZoA>Br(~Jl6+|=l>Ai?kdI4KAJxx zVbqKA{&e?dRC?4+qfcmIWf^sKS;t7t{jayQAzzC~bfk%?sKof2)G?>TVA}z8d-Kn=0U$RRGIfiRkoxW* z{HPmtuNJ=orc7^Kum0YE_(}H{PC&bI_X0aMxJbT;Kuvo1Y5IU?)lLX*%)O; z109eGdd(!VKzuNWsW9I=aJ<4yc7!=9hpBRKjb4Q`u|eiz<&}Uw73vvrE0Hk%uiP*~ zr1mAX;f$e+Z0BfAY1otz$`CJho9l@vDDqoWnBY51Bhsnm0bqAhHKa-wj~A-ff%wRrR6HH=WWh@xq4BPj7T1DWjYZvdRnCG@RF)xp z(^Mse#%A`QI^lTb$(|Y4SoHzh%6wOgRX~p;*0-Bhnzy}rSPD9=*2^}URU9Vfs1Z$R z8M!>s@62%fV6iTUZ0?fOtF2vv91+i@@xFG)=yCg9(WuJcZj5A=eYtcpeSir$M`)4r<6?90-({G;w+R1fwD^Bh z1^L^%ry=ml9bBy0s#(FP$)s*4!tiA$4f5)M^4StQF79R%r39vho|XN z!j1VGabrq6y~?~y!J*DzNp#_*%^`Bqppcp*=2Q5r2Dd$dvYKYo=`ofeBS^q?7MU85FxmE0iDFE;R>F@u!aV#na zwKwPm2@C)47x7D{P*e))soo1p#I4^_&ZibC1@$=L1x4rfIR*7_Au0g%2*d?o4Ot07 z4$8%W2vk#4Rp*7NGXK9#Q8k{Z1XR7+1)*B#obb1B<*x_lTMnocRKd#yr9tE}1x5aX z3PM#JTz~@ZpM(CJ{s0w-T7JI(c8Q+@QHt*zD9_J}@u*PL^5X@R9(j!AKNllWIhQMb z7Zd>C2aCcqMY5>45sVeGKT+SeVHs96w$^5T;0@eC*eD$+6srJv8@VjUWm34WQ{+R_r gwz;@?TQ67_Yc^aK6YEb4a^&|i^5zfG`}5hq00fjJ)Bpeg diff --git a/data/testmerge.xlsx b/data/testmerge.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7dcf33b7ba65ea0e3e0414e074bccb4ef6bfe55f GIT binary patch literal 9960 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 literal 0 HcmV?d00001 diff --git a/docs/src/api.md b/docs/src/api.md index 9e7584a9..8f189147 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -43,5 +43,6 @@ XLSX.setRowHeight XLSX.getMergedCells XLSX.isMergedCell XLSX.getMergedBaseCell +XLSX.mergeCells XLSX.addDefinedName ``` diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl index 0b6992ca..95a6091f 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -263,16 +263,21 @@ function process_ranges(f::Function, ws::Worksheet, ref_or_rng::AbstractString; throw(XLSXError("Unexpected defined name value: $v.")) end elseif is_valid_column_range(ref_or_rng) - colrng = ColumnRange(ref_or_rng) - newid = f(ws, colrng; kw...) + newid = f(ws, ColumnRange(ref_or_rng); kw...) elseif is_valid_row_range(ref_or_rng) - rowrng = RowRange(ref_or_rng) - newid = f(ws, rowrng; kw...) + newid = f(ws, RowRange(ref_or_rng); kw...) elseif is_valid_cellrange(ref_or_rng) - rng = CellRange(ref_or_rng) - newid = f(ws, rng; kw...) + 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...) else throw(XLSXError("Invalid cell reference or range: $ref_or_rng")) end diff --git a/src/cellformats.jl b/src/cellformats.jl index 82deadbb..32601156 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -85,6 +85,10 @@ julia> setFont(sh, :, 2:6; size=48, color="lightskyblue2") ``` """ function setFont end +setFont(ws::Worksheet, ref::SheetCellRef; kw...) = setFont(ws, ref.cellref; kw...) +setFont(ws::Worksheet, rng::SheetCellRange; kw...) = setFont(ws, rng.rng; kw...) +setFont(ws::Worksheet, rng::SheetColumnRange; kw...) = setFont(ws, rng.colrng; kw...) +setFont(ws::Worksheet, rng::SheetRowRange; kw...) = 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...) @@ -247,6 +251,9 @@ julia> setUniformFont(sh, 1, [2, 4, 6]; size=48, color="lightskyblue2") ``` """ function setUniformFont end +setUniformFont(ws::Worksheet, rng::SheetCellRange; kw...) = setUniformFont(ws, rng.rng; kw...) +setUniformFont(ws::Worksheet, rng::SheetColumnRange; kw...) = setUniformFont(ws, rng.colrng; kw...) +setUniformFont(ws::Worksheet, rng::SheetRowRange; kw...) = 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_ncranges(setUniformFont, ws, ncrng; kw...) @@ -569,6 +576,10 @@ Julia> setBorder(xf, "Sheet1!D4"; left = ["style" => "dotted", "color" => "F ``` """ function setBorder end +setBorder(ws::Worksheet, ref::SheetCellRef; kw...) = setBorder(ws, ref.cellref; kw...) +setBorder(ws::Worksheet, rng::SheetCellRange; kw...) = setBorder(ws, rng.rng; kw...) +setBorder(ws::Worksheet, rng::SheetColumnRange; kw...) = setBorder(ws, rng.colrng; kw...) +setBorder(ws::Worksheet, rng::SheetRowRange; kw...) = setBorder(ws, rng.rowrng; kw...) setBorder(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges(setBorder, ws, ncrng; outside) setBorder(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setBorder, ws, ref_or_rng; kw...) setBorder(ws::Worksheet, row::Integer, col::Integer; kw...) = setBorder(ws, CellRef(row, col); kw...) @@ -805,6 +816,9 @@ Julia> setUniformBorder(xf, "Sheet1!A1:F20"; left = ["style" => "dotted", "c ``` """ function setUniformBorder end +setUniformBorder(ws::Worksheet, rng::SheetCellRange; kw...) = setUniformBorder(ws, rng.rng; kw...) +setUniformBorder(ws::Worksheet, rng::SheetColumnRange; kw...) = setUniformBorder(ws, rng.colrng; kw...) +setUniformBorder(ws::Worksheet, rng::SheetRowRange; kw...) = 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_ncranges(setUniformBorder, ws, ncrng; kw...) @@ -859,6 +873,9 @@ This function is equivalent to `setBorder()` called with the same arguments and """ function setOutsideBorder end +setOutsideBorder(ws::Worksheet, rng::SheetCellRange; kw...) = setOutsideBorder(ws, rng.rng; kw...) +setOutsideBorder(ws::Worksheet, rng::SheetColumnRange; kw...) = setOutsideBorder(ws, rng.colrng; kw...) +setOutsideBorder(ws::Worksheet, rng::SheetRowRange; kw...) = 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...) @@ -1093,6 +1110,10 @@ Julia> setFill(sh, "11:24"; pattern="none", fgColor = "yellow2") ``` """ function setFill end +setFill(ws::Worksheet, ref::SheetCellRef; kw...) = setFill(ws, ref.cellref; kw...) +setFill(ws::Worksheet, rng::SheetCellRange; kw...) = setFill(ws, rng.rng; kw...) +setFill(ws::Worksheet, rng::SheetColumnRange; kw...) = setFill(ws, rng.colrng; kw...) +setFill(ws::Worksheet, rng::SheetRowRange; kw...) = 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...) @@ -1223,6 +1244,9 @@ Julia> setUniformFill(xf, "Sheet1!A1:F20"; pattern="none", fgColor = "darkseagre ``` """ function setUniformFill end +setUniformFill(ws::Worksheet, rng::SheetCellRange; kw...) = setUniformFill(ws, rng.rng; kw...) +setUniformFill(ws::Worksheet, rng::SheetColumnRange; kw...) = setUniformFill(ws, rng.colrng; kw...) +setUniformFill(ws::Worksheet, rng::SheetRowRange; kw...) = 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_ncranges(setUniformFill, ws, ncrng; kw...) @@ -1384,6 +1408,10 @@ julia> setAlignment(sh, 1:3, 3:6; horizontal="center", rotation="90", shrink=tru ``` """ function setAlignment end +setAlignment(ws::Worksheet, ref::SheetCellRef; kw...) = setAlignment(ws, ref.cellref; kw...) +setAlignment(ws::Worksheet, rng::SheetCellRange; kw...) = setAlignment(ws, rng.rng; kw...) +setAlignment(ws::Worksheet, rng::SheetColumnRange; kw...) = setAlignment(ws, rng.colrng; kw...) +setAlignment(ws::Worksheet, rng::SheetRowRange; kw...) = 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...) @@ -1527,6 +1555,9 @@ Julia> setUniformAlignment(sh, :, 1:24; horizontal="center", vertical="top") ``` """ function setUniformAlignment end +setUniformAlignment(ws::Worksheet, rng::SheetCellRange; kw...) = setUniformAlignment(ws, rng.rng; kw...) +setUniformAlignment(ws::Worksheet, rng::SheetColumnRange; kw...) = setUniformAlignment(ws, rng.colrng; kw...) +setUniformAlignment(ws::Worksheet, rng::SheetRowRange; kw...) = 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_ncranges(setUniformAlignment, ws, ncrng; kw...) @@ -1671,6 +1702,10 @@ julia> XLSX.setFormat(sh, "A2"; format = "_-£* #,##0.00_-;-£* #,##0.00_-;_-£* ``` """ function setFormat end +setFormat(ws::Worksheet, ref::SheetCellRef; kw...) = setFormat(ws, ref.cellref; kw...) +setFormat(ws::Worksheet, rng::SheetCellRange; kw...) = setFormat(ws, rng.rng; kw...) +setFormat(ws::Worksheet, rng::SheetColumnRange; kw...) = setFormat(ws, rng.colrng; kw...) +setFormat(ws::Worksheet, rng::SheetRowRange; kw...) = 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...) @@ -1795,6 +1830,9 @@ julia> XLSX.setUniformFormat(sh, "F1:F5"; format = "Currency") ``` """ function setUniformFormat end +setUniformFormat(ws::Worksheet, rng::SheetCellRange; kw...) = setUniformFormat(ws, rng.rng; kw...) +setUniformFormat(ws::Worksheet, rng::SheetColumnRange; kw...) = setUniformFormat(ws, rng.colrng; kw...) +setUniformFormat(ws::Worksheet, rng::SheetRowRange; kw...) = 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_ncranges(setUniformFormat, ws, ncrng; kw...) @@ -1937,6 +1975,10 @@ julia> XLSX.setColumnWidth(sh, "I"; width = 24.37) ``` """ function setColumnWidth end +setColumnWidth(ws::Worksheet, ref::SheetCellRef; kw...) = setColumnWidth(ws, ref.cellref; kw...) +setColumnWidth(ws::Worksheet, rng::SheetCellRange; kw...) = setColumnWidth(ws, rng.rng; kw...) +setColumnWidth(ws::Worksheet, rng::SheetColumnRange; kw...) = setColumnWidth(ws, rng.colrng; kw...) +setColumnWidth(ws::Worksheet, rng::SheetRowRange; kw...) = 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...) @@ -2136,6 +2178,10 @@ julia> XLSX.setRowHeight(sh, "I"; height = 24.56) ``` """ function setRowHeight end +setRowHeight(ws::Worksheet, ref::SheetCellRef; kw...) = setRowHeight(ws, ref.cellref; kw...) +setRowHeight(ws::Worksheet, rng::SheetCellRange; kw...) = setRowHeight(ws, rng.rng; kw...) +setRowHeight(ws::Worksheet, rng::SheetColumnRange; kw...) = setRowHeight(ws, rng.colrng; kw...) +setRowHeight(ws::Worksheet, rng::SheetRowRange; kw...) = 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...) @@ -2251,7 +2297,9 @@ end 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 +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 @@ -2273,12 +2321,17 @@ julia> XLSX.getMergedCells(s) 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 - # No need to update the xml file using the worksheet cache first (like we did for column width) - # because we cannot change merged cells in XLSX.jl. + # 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)) + 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") @@ -2318,6 +2371,8 @@ the function: 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") @@ -2338,6 +2393,10 @@ isMergedCell(ws::Worksheet, cr::String; kw...)::Bool = process_get_cellname(isMe 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 @@ -2387,6 +2446,8 @@ the function: 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") @@ -2406,6 +2467,10 @@ getMergedBaseCell(ws::Worksheet, cr::String; kw...) = process_get_cellname(getMe 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 @@ -2427,4 +2492,120 @@ function getMergedBaseCell(ws::Worksheet, cellref::CellRef; mergedCells::Union{V end end return nothing -end \ No newline at end of file +end + +""" + mergeCells(ws::Worksheet, cr::String) -> 0 + mergeCells(xf::XLSXFile, cr::String) -> 0 + + mergemCells(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! + +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) = mergeCells(ws, rng.rng) +mergeCells(ws::Worksheet, rng::SheetColumnRange) = mergeCells(ws, rng.colrng) +mergeCells(ws::Worksheet, rng::SheetRowRange) = 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(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_intcolon(mergeCells, ws, row, :) +mergeCells(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}) = process_colonint(mergeCells, ws, :, col) +mergeCells(ws::Worksheet, ::Colon, ::Colon) = process_colon(mergeCells, ws, :) +mergeCells(ws::Worksheet, ::Colon) = process_colon(mergeCells, ws, :) +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 + + # 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)) + + 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 + for cell in cr + if cell in CellRange(child["ref"]) + throw(XLSXError("Merged range (\"$cr\") cannot overlap with existing merged range (\""*child["ref"]*"\").")) + end + end + end + end + + 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 + + return 0 # meaningless return value. Int required to comply with reference decoding structure. +end diff --git a/src/read.jl b/src/read.jl index 6eeecd2b..f7ebb40f 100644 --- a/src/read.jl +++ b/src/read.jl @@ -237,9 +237,9 @@ function open_or_read_xlsx(source::Union{IO, AbstractString}, read_files::Bool, for r 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) diff --git a/src/relationship.jl b/src/relationship.jl index f6231eec..9f23c00c 100644 --- a/src/relationship.jl +++ b/src/relationship.jl @@ -11,7 +11,6 @@ end function parse_relationship_target(prefix::String, target::String) :: String isempty(prefix) || isempty(target) && throw(XLSXError("Something wrong here!")) - if target[1] == '/' sizeof(target) <= 1 && throw(XLSXError("Incomplete target path $target.")) return target[2:end] diff --git a/src/worksheet.jl b/src/worksheet.jl index f615be7f..fa53be93 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -68,7 +68,8 @@ function get_dimension(ws::Worksheet)::Union{Nothing,CellRange} 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)]) - return CellRange(CellRef(row_min, col_min), CellRef(row_max, col_max)) + set_dimension!(ws, CellRange(CellRef(row_min, col_min), CellRef(row_max, col_max))) + return ws.dimension end function set_dimension!(ws::Worksheet, rng::CellRange) diff --git a/src/write.jl b/src/write.jl index c3e2b50d..d64a1c29 100644 --- a/src/write.jl +++ b/src/write.jl @@ -330,9 +330,11 @@ function update_worksheets_xml!(xl::XLSXFile) # updates worksheet dimension if get_dimension(sheet) !== nothing i, j = get_idces(doc, "worksheet", "dimension") - dimension_node = doc[i][j] - dimension_node["ref"] = string(get_dimension(sheet)) - doc[i][j] = dimension_node + if !isnothing(j) + dimension_node = doc[i][j] + dimension_node["ref"] = string(get_dimension(sheet)) + doc[i][j] = dimension_node + end end set_worksheet_xml_document!(sheet, doc) @@ -584,7 +586,7 @@ function setdata!(ws::Worksheet, ref::CellRef, val::CellValueType) # use existin 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. + # 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 @@ -624,7 +626,7 @@ function setdata!(ws::Worksheet, ref::AbstractString, value) elseif is_valid_sheet_column_range(ref) return setdata!(ws, SheetColumnRange(ref), value) elseif is_valid_sheet_row_range(ref) - return gsetdata!(ws, SheetRowRange(ref), value) + return setdata!(ws, SheetRowRange(ref), value) elseif is_valid_non_contiguous_range(ref) return setdata!(ws, NonContiguousRange(ref), value) end diff --git a/test/runtests.jl b/test/runtests.jl index 1db147ac..03a1773e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -595,6 +595,13 @@ 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 +end + @testset "Column Range" begin cr = XLSX.ColumnRange("B:D") @test string(cr) == "B:D" @@ -2311,6 +2318,9 @@ 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")]) @@ -2326,11 +2336,11 @@ end @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=CellRef("D72"), baseValue=Dates.Date("2025-03-24")) - @test XLSX.getMergedBaseCell(f, "Mock-up!G72") == (baseCell=CellRef("D72"), baseValue=Dates.Date("2025-03-24")) - @test XLSX.getMergedBaseCell(s, "H53") == (baseCell=CellRef("D51"), baseValue="Hello World") - @test XLSX.getMergedBaseCell(s, "G52") == (baseCell=CellRef("D51"), baseValue="Hello World") - @test XLSX.getMergedBaseCell(s, "Short_Description") == (baseCell=CellRef("D51"), baseValue="Hello World") + @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, "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 @@ -2340,9 +2350,57 @@ end @test !XLSX.isMergedCell(f, "Document History!B2") @test !XLSX.isMergedCell(s, "C5"; mergedCells=XLSX.getMergedCells(f["Document History"])) + 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 + + 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") + end end + @testset "filemodes" begin sheetname = "New Sheet" From 8a59fe3a39175d5ef1866858b10b5b163fe9a4af Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Sat, 12 Apr 2025 16:35:25 +0100 Subject: [PATCH 070/154] Typo! --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 03a1773e..0e8906e2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -597,7 +597,7 @@ 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")) + Dim = XLSX.readxlsx(joinpath(data_directory, "customXml.xlsx")) @test noDim[1].dimension == Dim[1].dimension @test noDim[2].dimension == Dim[2].dimension end From 989defe8a52d68e0ef62df662551ebbdde20e4ee Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Sun, 13 Apr 2025 11:09:34 +0100 Subject: [PATCH 071/154] Improved handling of non-contiguous ranges --- src/cellformat-helpers.jl | 12 ++++ src/cellformats.jl | 115 +++++++++++++++++++------------------- src/types.jl | 12 ++-- src/worksheet.jl | 14 +++-- src/write.jl | 9 ++- test/runtests.jl | 28 ++++++++-- 6 files changed, 117 insertions(+), 73 deletions(-) diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl index 95a6091f..814de2fa 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -62,6 +62,13 @@ function copynode(o::XML.Node) 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 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 @@ -278,6 +285,8 @@ function process_ranges(f::Function, ws::Worksheet, ref_or_rng::AbstractString; 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_range(ref_or_rng) + newid = f(ws, NonContiguousRange(ref_or_rng); kw...) else throw(XLSXError("Invalid cell reference or range: $ref_or_rng")) end @@ -332,6 +341,9 @@ function process_rowranges(f::Function, ws::Worksheet, rowrng::RowRange; kw...): end end function process_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int + if occursin("Uniform", string(nameof(f))) + throw(XLSXError("Cannot apply `setUnifoirmAttribute()` functions to a non-contiguous range.\nUse the equivalent `setAttribute()` function instead.")) + end bounds = nc_bounds(ncrng) dim = (get_dimension(ws)) if dim === nothing diff --git a/src/cellformats.jl b/src/cellformats.jl index 32601156..c15a2852 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -85,10 +85,10 @@ julia> setFont(sh, :, 2:6; size=48, color="lightskyblue2") ``` """ function setFont end -setFont(ws::Worksheet, ref::SheetCellRef; kw...) = setFont(ws, ref.cellref; kw...) -setFont(ws::Worksheet, rng::SheetCellRange; kw...) = setFont(ws, rng.rng; kw...) -setFont(ws::Worksheet, rng::SheetColumnRange; kw...) = setFont(ws, rng.colrng; kw...) -setFont(ws::Worksheet, rng::SheetRowRange; kw...) = setFont(ws, rng.rowrng; kw...) +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...) @@ -251,9 +251,9 @@ julia> setUniformFont(sh, 1, [2, 4, 6]; size=48, color="lightskyblue2") ``` """ function setUniformFont end -setUniformFont(ws::Worksheet, rng::SheetCellRange; kw...) = setUniformFont(ws, rng.rng; kw...) -setUniformFont(ws::Worksheet, rng::SheetColumnRange; kw...) = setUniformFont(ws, rng.colrng; kw...) -setUniformFont(ws::Worksheet, rng::SheetRowRange; kw...) = setUniformFont(ws, rng.rowrng; kw...) +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_ncranges(setUniformFont, ws, ncrng; kw...) @@ -576,10 +576,10 @@ Julia> setBorder(xf, "Sheet1!D4"; left = ["style" => "dotted", "color" => "F ``` """ function setBorder end -setBorder(ws::Worksheet, ref::SheetCellRef; kw...) = setBorder(ws, ref.cellref; kw...) -setBorder(ws::Worksheet, rng::SheetCellRange; kw...) = setBorder(ws, rng.rng; kw...) -setBorder(ws::Worksheet, rng::SheetColumnRange; kw...) = setBorder(ws, rng.colrng; kw...) -setBorder(ws::Worksheet, rng::SheetRowRange; kw...) = setBorder(ws, rng.rowrng; 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; outside) setBorder(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setBorder, ws, ref_or_rng; kw...) setBorder(ws::Worksheet, row::Integer, col::Integer; kw...) = setBorder(ws, CellRef(row, col); kw...) @@ -816,9 +816,9 @@ Julia> setUniformBorder(xf, "Sheet1!A1:F20"; left = ["style" => "dotted", "c ``` """ function setUniformBorder end -setUniformBorder(ws::Worksheet, rng::SheetCellRange; kw...) = setUniformBorder(ws, rng.rng; kw...) -setUniformBorder(ws::Worksheet, rng::SheetColumnRange; kw...) = setUniformBorder(ws, rng.colrng; kw...) -setUniformBorder(ws::Worksheet, rng::SheetRowRange; kw...) = setUniformBorder(ws, rng.rowrng; kw...) +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_ncranges(setUniformBorder, ws, ncrng; kw...) @@ -873,9 +873,9 @@ This function is equivalent to `setBorder()` called with the same arguments and """ function setOutsideBorder end -setOutsideBorder(ws::Worksheet, rng::SheetCellRange; kw...) = setOutsideBorder(ws, rng.rng; kw...) -setOutsideBorder(ws::Worksheet, rng::SheetColumnRange; kw...) = setOutsideBorder(ws, rng.colrng; kw...) -setOutsideBorder(ws::Worksheet, rng::SheetRowRange; kw...) = setOutsideBorder(ws, rng.rowrng; kw...) +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...) @@ -1110,10 +1110,10 @@ Julia> setFill(sh, "11:24"; pattern="none", fgColor = "yellow2") ``` """ function setFill end -setFill(ws::Worksheet, ref::SheetCellRef; kw...) = setFill(ws, ref.cellref; kw...) -setFill(ws::Worksheet, rng::SheetCellRange; kw...) = setFill(ws, rng.rng; kw...) -setFill(ws::Worksheet, rng::SheetColumnRange; kw...) = setFill(ws, rng.colrng; kw...) -setFill(ws::Worksheet, rng::SheetRowRange; kw...) = setFill(ws, rng.rowrng; kw...) +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...) @@ -1244,9 +1244,9 @@ Julia> setUniformFill(xf, "Sheet1!A1:F20"; pattern="none", fgColor = "darkseagre ``` """ function setUniformFill end -setUniformFill(ws::Worksheet, rng::SheetCellRange; kw...) = setUniformFill(ws, rng.rng; kw...) -setUniformFill(ws::Worksheet, rng::SheetColumnRange; kw...) = setUniformFill(ws, rng.colrng; kw...) -setUniformFill(ws::Worksheet, rng::SheetRowRange; kw...) = setUniformFill(ws, rng.rowrng; kw...) +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_ncranges(setUniformFill, ws, ncrng; kw...) @@ -1408,10 +1408,10 @@ julia> setAlignment(sh, 1:3, 3:6; horizontal="center", rotation="90", shrink=tru ``` """ function setAlignment end -setAlignment(ws::Worksheet, ref::SheetCellRef; kw...) = setAlignment(ws, ref.cellref; kw...) -setAlignment(ws::Worksheet, rng::SheetCellRange; kw...) = setAlignment(ws, rng.rng; kw...) -setAlignment(ws::Worksheet, rng::SheetColumnRange; kw...) = setAlignment(ws, rng.colrng; kw...) -setAlignment(ws::Worksheet, rng::SheetRowRange; kw...) = setAlignment(ws, rng.rowrng; kw...) +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...) @@ -1555,9 +1555,9 @@ Julia> setUniformAlignment(sh, :, 1:24; horizontal="center", vertical="top") ``` """ function setUniformAlignment end -setUniformAlignment(ws::Worksheet, rng::SheetCellRange; kw...) = setUniformAlignment(ws, rng.rng; kw...) -setUniformAlignment(ws::Worksheet, rng::SheetColumnRange; kw...) = setUniformAlignment(ws, rng.colrng; kw...) -setUniformAlignment(ws::Worksheet, rng::SheetRowRange; kw...) = setUniformAlignment(ws, rng.rowrng; kw...) +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_ncranges(setUniformAlignment, ws, ncrng; kw...) @@ -1702,10 +1702,10 @@ julia> XLSX.setFormat(sh, "A2"; format = "_-£* #,##0.00_-;-£* #,##0.00_-;_-£* ``` """ function setFormat end -setFormat(ws::Worksheet, ref::SheetCellRef; kw...) = setFormat(ws, ref.cellref; kw...) -setFormat(ws::Worksheet, rng::SheetCellRange; kw...) = setFormat(ws, rng.rng; kw...) -setFormat(ws::Worksheet, rng::SheetColumnRange; kw...) = setFormat(ws, rng.colrng; kw...) -setFormat(ws::Worksheet, rng::SheetRowRange; kw...) = setFormat(ws, rng.rowrng; kw...) +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...) @@ -1830,9 +1830,9 @@ julia> XLSX.setUniformFormat(sh, "F1:F5"; format = "Currency") ``` """ function setUniformFormat end -setUniformFormat(ws::Worksheet, rng::SheetCellRange; kw...) = setUniformFormat(ws, rng.rng; kw...) -setUniformFormat(ws::Worksheet, rng::SheetColumnRange; kw...) = setUniformFormat(ws, rng.colrng; kw...) -setUniformFormat(ws::Worksheet, rng::SheetRowRange; kw...) = setUniformFormat(ws, rng.rowrng; kw...) +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_ncranges(setUniformFormat, ws, ncrng; kw...) @@ -1891,6 +1891,9 @@ julia> XLSX.setUniformStyle(sh, 2, :) ``` """ function setUniformStyle end +setUniformStyle(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setUniformStyle(ws, rng.rng; kw...) +setUniformStyle(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setUniformStyle(ws, rng.colrng; kw...) +setUniformStyle(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setUniformStyle(ws, rng.rowrng; kw...) 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_ncranges(setUniformStyle, ws, ncrng) @@ -1975,10 +1978,10 @@ julia> XLSX.setColumnWidth(sh, "I"; width = 24.37) ``` """ function setColumnWidth end -setColumnWidth(ws::Worksheet, ref::SheetCellRef; kw...) = setColumnWidth(ws, ref.cellref; kw...) -setColumnWidth(ws::Worksheet, rng::SheetCellRange; kw...) = setColumnWidth(ws, rng.rng; kw...) -setColumnWidth(ws::Worksheet, rng::SheetColumnRange; kw...) = setColumnWidth(ws, rng.colrng; kw...) -setColumnWidth(ws::Worksheet, rng::SheetRowRange; kw...) = setColumnWidth(ws, rng.rowrng; kw...) +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...) @@ -2104,7 +2107,7 @@ function getColumnWidth(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real} d = get_dimension(ws) if cellref ∉ d - throw(XLSXError("Cell specified is outside sheet dimension \"$d\"")) + throw(XLSXError("Cell specified is outside sheet dimension `$d`")) end # Because we are working on worksheet data directly, we need to update the xml file using the worksheet cache first. @@ -2178,10 +2181,10 @@ julia> XLSX.setRowHeight(sh, "I"; height = 24.56) ``` """ function setRowHeight end -setRowHeight(ws::Worksheet, ref::SheetCellRef; kw...) = setRowHeight(ws, ref.cellref; kw...) -setRowHeight(ws::Worksheet, rng::SheetCellRange; kw...) = setRowHeight(ws, rng.rng; kw...) -setRowHeight(ws::Worksheet, rng::SheetColumnRange; kw...) = setRowHeight(ws, rng.colrng; kw...) -setRowHeight(ws::Worksheet, rng::SheetRowRange; kw...) = setRowHeight(ws, rng.rowrng; kw...) +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...) @@ -2272,7 +2275,7 @@ function getRowHeight(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real} d = get_dimension(ws) if cellref ∉ d - throw(XLSXError("Cell specified is outside sheet dimension \"$d\"")) + throw(XLSXError("Cell specified is outside sheet dimension `$d`")) end for r in eachrow(ws) @@ -2403,7 +2406,7 @@ function isMergedCell(ws::Worksheet, cellref::CellRef; mergedCells::Union{Vector d = get_dimension(ws) if cellref ∉ d - throw(XLSXError("Cell specified is outside sheet dimension \"$d\"")) + throw(XLSXError("Cell specified is outside sheet dimension `$d`")) end if ismissing(mergedCells) # Get mergedCells if missing @@ -2477,7 +2480,7 @@ function getMergedBaseCell(ws::Worksheet, cellref::CellRef; mergedCells::Union{V d = get_dimension(ws) if cellref ∉ d - throw(XLSXError("Cell specified is outside sheet dimension \"$d\"")) + throw(XLSXError("Cell specified is outside sheet dimension `$d`")) end if ismissing(mergedCells) # Get mergedCells if missing @@ -2524,9 +2527,9 @@ julia> XLSX.mergeCells(sh, "A:D") # Merge columns to the extent of the ``` """ function mergeCells end -mergeCells(ws::Worksheet, rng::SheetCellRange) = mergeCells(ws, rng.rng) -mergeCells(ws::Worksheet, rng::SheetColumnRange) = mergeCells(ws, rng.colrng) -mergeCells(ws::Worksheet, rng::SheetRowRange) = mergeCells(ws, rng.rowrng) +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(xl::XLSXFile, sheetcell::AbstractString)::Int = process_sheetcell(mergeCells, xl, sheetcell) @@ -2541,9 +2544,9 @@ function mergeCells(ws::Worksheet, cr::CellRange) # !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.")) + !issubset(cr, get_dimension(ws)) && throw(XLSXError("Range `$cr` goes outside worksheet dimension.")) - length(cr) == 1 && throw(XLSXError("Cannot merge a single cell: \"$cr\"")) + 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.")) @@ -2585,7 +2588,7 @@ function mergeCells(ws::Worksheet, cr::CellRange) for child in c for cell in cr if cell in CellRange(child["ref"]) - throw(XLSXError("Merged range (\"$cr\") cannot overlap with existing merged range (\""*child["ref"]*"\").")) + throw(XLSXError("Merged range (`$cr`) cannot overlap with existing merged range (`"*child["ref"]*"`).")) end end end diff --git a/src/types.jl b/src/types.jl index ed0632de..5689b0e8 100644 --- a/src/types.jl +++ b/src/types.jl @@ -177,7 +177,9 @@ cr = XLSX.range"A1:C4" =# abstract type AbstractCellRange end -abstract type ContiguousCellRange end +abstract type ContiguousCellRange <: AbstractCellRange end +abstract type AbstractSheetCellRange <: AbstractCellRange end +abstract type ContiguousSheetCellRange <: AbstractSheetCellRange end struct CellRange <: ContiguousCellRange start::CellRef @@ -226,21 +228,21 @@ struct SheetCellRef cellref::CellRef end -struct SheetCellRange <: ContiguousCellRange +struct SheetCellRange <: ContiguousSheetCellRange sheet::String rng::CellRange end -struct NonContiguousRange <: AbstractCellRange +struct NonContiguousRange <: AbstractSheetCellRange sheet::String rng::Vector{Union{CellRef, CellRange}} end -struct SheetColumnRange <: ContiguousCellRange +struct SheetColumnRange <: ContiguousSheetCellRange sheet::String colrng::ColumnRange end -struct SheetRowRange <: ContiguousCellRange +struct SheetRowRange <: ContiguousSheetCellRange sheet::String rowrng::RowRange end diff --git a/src/worksheet.jl b/src/worksheet.jl index fa53be93..f23a1e72 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -262,6 +262,7 @@ end =# 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 @@ -276,10 +277,10 @@ function getdata(ws::Worksheet, rng::NonContiguousRange)::Vector{Any} end # Needed for definedName references -getdata(ws::Worksheet, s::SheetCellRef) = getdata(ws, s.cellref) -getdata(ws::Worksheet, s::SheetCellRange) = getdata(ws, s.rng) -getdata(ws::Worksheet, s::SheetColumnRange) = getdata(ws, s.colrng) -getdata(ws::Worksheet, s::SheetRowRange) = getdata(ws, s.rowrng) +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)::Union{Array{Any,2},Any} if is_worksheet_defined_name(ws, ref) @@ -317,8 +318,11 @@ function getdata(ws::Worksheet, ref::AbstractString)::Union{Array{Any,2},Any} return getdata(ws, SheetColumnRange(ref)) elseif is_valid_sheet_row_range(ref) return getdata(ws, SheetRowRange(ref)) - elseif is_valid_non_contiguous_range(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 throw(XLSXError("`$ref` is not a valid cell or range reference.")) end diff --git a/src/write.jl b/src/write.jl index d64a1c29..fa5fc4f8 100644 --- a/src/write.jl +++ b/src/write.jl @@ -627,8 +627,11 @@ function setdata!(ws::Worksheet, ref::AbstractString, value) return setdata!(ws, SheetColumnRange(ref), value) elseif is_valid_sheet_row_range(ref) return setdata!(ws, SheetRowRange(ref), value) - elseif is_valid_non_contiguous_range(ref) - return setdata!(ws, NonContiguousRange(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 @@ -665,7 +668,7 @@ function setdata!(ws::Worksheet, rng::NonContiguousRange, value) setdata!(ws, r, value) else for cell in r - psetdata!(ws, cell, value) + setdata!(ws, cell, value) end end end diff --git a/test/runtests.jl b/test/runtests.jl index 0e8906e2..c020b597 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1654,6 +1654,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) @@ -1801,6 +1804,22 @@ end f = XLSX.open_xlsx_template(joinpath(data_directory, "customXml.xlsx")) s = f["Mock-up"] + # 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"] + ) + + # Uniform functions can't take non-contiguous ranges + @test_throws XLSX.XLSXError 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"] + ) + 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) @@ -2368,10 +2387,11 @@ end @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[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) From 5f21073c879705d7d20a17b676e429daaa97e63a Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Sun, 13 Apr 2025 11:44:56 +0100 Subject: [PATCH 072/154] Couple of overlooked changes! --- src/cellformat-helpers.jl | 7 +++++-- src/cellformats.jl | 9 +++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl index 814de2fa..d23e2f6d 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -285,8 +285,11 @@ function process_ranges(f::Function, ws::Worksheet, ref_or_rng::AbstractString; 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_range(ref_or_rng) - newid = f(ws, NonContiguousRange(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 diff --git a/src/cellformats.jl b/src/cellformats.jl index c15a2852..03efbadc 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -580,7 +580,7 @@ setBorder(ws::Worksheet, ref::SheetCellRef; kw...) = do_sheet_names_match(ws, re 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; outside) +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(ws::Worksheet, row::Integer, col::Integer; kw...) = setBorder(ws, CellRef(row, col); kw...) setBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_intcolon(setBorder, ws, row, :; kw...) @@ -1891,9 +1891,9 @@ julia> XLSX.setUniformStyle(sh, 2, :) ``` """ function setUniformStyle end -setUniformStyle(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setUniformStyle(ws, rng.rng; kw...) -setUniformStyle(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setUniformStyle(ws, rng.colrng; kw...) -setUniformStyle(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setUniformStyle(ws, rng.rowrng; kw...) +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_ncranges(setUniformStyle, ws, ncrng) @@ -2532,6 +2532,7 @@ mergeCells(ws::Worksheet, rng::SheetColumnRange) = do_sheet_names_match(ws, rng) 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_intcolon(mergeCells, ws, row, :) From e054a8bfd222240d71f44ac751ebcddabc4f5697 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Sun, 13 Apr 2025 16:45:06 +0100 Subject: [PATCH 073/154] Handle non-contiguous ranges for `setUniformAttribute()` functions --- src/cellformat-helpers.jl | 164 +++++++++++++++++++++++++++++++------- src/cellformats.jl | 16 ++-- test/runtests.jl | 21 ++++- 3 files changed, 162 insertions(+), 39 deletions(-) diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl index d23e2f6d..9f4caee0 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -62,7 +62,7 @@ function copynode(o::XML.Node) 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 do_sheet_names_match(ws::Worksheet, rng::T) where {T <: Union{SheetCellRef, AbstractSheetCellRange}} +function do_sheet_names_match(ws::Worksheet, rng::T) where {T<:Union{SheetCellRef,AbstractSheetCellRange}} if ws.name == rng.sheet return true else @@ -112,7 +112,7 @@ function buildNode(tag::String, attributes::Dict{String,Union{Nothing,Dict{Strin end else color[k] = v - end + end end if length(XML.attributes(color)) > 0 # Don't push an empty color. push!(cnode, color) @@ -278,17 +278,17 @@ function process_ranges(f::Function, ws::Worksheet, ref_or_rng::AbstractString; 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...) + newid = f(ws, SheetCellRef(ref_or_rng); kw...) elseif is_valid_sheet_cellrange(ref_or_rng) - newid = f(ws, SheetCellRange(ref_or_rng); kw...) + 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...) + 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...) + 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) + 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")) @@ -344,7 +344,7 @@ function process_rowranges(f::Function, ws::Worksheet, rowrng::RowRange; kw...): end end function process_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int - if occursin("Uniform", string(nameof(f))) + if occursin("Uniform", string(nameof(f))) # Shouldn't happen! throw(XLSXError("Cannot apply `setUnifoirmAttribute()` functions to a non-contiguous range.\nUse the equivalent `setAttribute()` function instead.")) end bounds = nc_bounds(ncrng) @@ -367,10 +367,10 @@ function process_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguousRange; end end function process_cellranges(f::Function, ws::Worksheet, rng::CellRange; kw...)::Int - if length(rng)==1 - single=true + if length(rng) == 1 + single = true else - single=false + single = false end for cellref in rng if getcell(ws, cellref) isa EmptyCell @@ -440,7 +440,7 @@ function process_intcolon(f::Function, ws::Worksheet, row::Union{Integer,UnitRan if dim === nothing throw(XLSXError("No worksheet dimension found")) else - rng=CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)) + rng = CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)) return f(ws, rng; kw...) end end @@ -449,7 +449,7 @@ function process_colonint(f::Function, ws::Worksheet, ::Colon, col::Union{Intege if dim === nothing throw(XLSXError("No worksheet dimension found")) else - rng=CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))) + rng = CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))) return f(ws, rng; kw...) end end @@ -458,10 +458,10 @@ function process_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon; if dim === nothing throw(XLSXError("No worksheet dimension found")) else - if length(row)==1 && dim.start.column_number == dim.stop.column_number - single=true + if length(row) == 1 && dim.start.column_number == dim.stop.column_number + single = true else - single=false + single = false end for a in row for b in dim.start.column_number:dim.stop.column_number @@ -481,10 +481,10 @@ function process_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}; if dim === nothing throw(XLSXError("No worksheet dimension found")) else - if length(col)==1 && dim.start.row_number == dim.stop.row_number - single=true + if length(col) == 1 && dim.start.row_number == dim.stop.row_number + single = true else - single=false + single = false end for b in col for a in dim.start.row_number:dim.stop.row_number @@ -500,10 +500,10 @@ function process_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}; return -1 end function process_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) - if length(col)==1 && length(row)==1 - single=true + if length(col) == 1 && length(row) == 1 + single = true else - single=false + single = false end for a in collect(row), b in col cellref = CellRef(a, b) @@ -516,10 +516,10 @@ function process_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange return -1 end function process_vecint(f::Function, ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) - if length(col)==1 && length(row)==1 - single=true + if length(col) == 1 && length(row) == 1 + single = true else - single=false + single = false end for a in row, b in collect(col) cellref = CellRef(a, b) @@ -532,10 +532,10 @@ function process_vecint(f::Function, ws::Worksheet, row::Vector{Int}, col::Union return -1 end function process_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) - if length(col)==1 && length(row)==1 - single=true + if length(col) == 1 && length(row) == 1 + single = true else - single=false + single = false end for a in row, b in col cellref = CellRef(a, b) @@ -589,6 +589,41 @@ function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange, a return newid end end +function process_uniform_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguousRange, atts::Vector{String}; kw...)::Int + bounds = nc_bounds(ncrng) + dim = (get_dimension(ws)) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + 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 + if r isa CellRef + newid, first = process_uniform_core(f, ws, r, atts, newid, first; kw...) + elseif r isa CellRange + for c in r + newid, first = process_uniform_core(f, ws, 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 +end function process_uniform_colon(f::Function, ws::Worksheet, ::Colon; kw...) dim = get_dimension(ws) if dim === nothing @@ -729,6 +764,41 @@ function process_uniform_core(ws::Worksheet, cellref::CellRef, newid::Union{Int, end return newid, first end +function process_uniform_ncranges(ws::Worksheet, ncrng::NonContiguousRange)::Int + bounds = nc_bounds(ncrng) + dim = (get_dimension(ws)) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + 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 + if r isa CellRef + newid, first = process_uniform_core(ws, r, newid, first) + elseif r isa CellRange + 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 +end function process_uniform_intcolon(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) dim = get_dimension(ws) if dim === nothing @@ -896,6 +966,42 @@ function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange; k return newid end end +function process_uniform_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int + bounds = nc_bounds(ncrng) + dim = (get_dimension(ws)) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + 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 + if r isa CellRef + newid, first, alignment_node = process_uniform_core(f, ws, r, newid, first, alignment_node; kw...) + elseif r isa CellRange + for c in r + newid, first, alignment_node = process_uniform_core(f, ws, 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 +end function process_uniform_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon; kw...) dim = get_dimension(ws) if dim === nothing @@ -1016,7 +1122,7 @@ function get_color(s::String)::String 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 AARRGGBB")) + 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 03efbadc..77f60b73 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -256,7 +256,7 @@ setUniformFont(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_mat 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_ncranges(setUniformFont, ws, ncrng; 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_uniform_intcolon(setFont, ws, row, :; kw...) @@ -821,7 +821,7 @@ setUniformBorder(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_m 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_ncranges(setUniformBorder, ws, ncrng; 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_uniform_intcolon(setBorder, ws, row, :; kw...) @@ -1249,7 +1249,7 @@ setUniformFill(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_mat 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_ncranges(setUniformFill, ws, ncrng; 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_uniform_intcolon(setFill, ws, row, :; kw...) @@ -1560,7 +1560,7 @@ setUniformAlignment(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_name 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_ncranges(setUniformAlignment, ws, ncrng; 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_uniform_intcolon(setAlignment, ws, row, :; kw...) @@ -1835,7 +1835,7 @@ setUniformFormat(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_m 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_ncranges(setUniformFormat, ws, ncrng; 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, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_uniform_intcolon(setFormat, ws, row, :; kw...) @@ -1896,7 +1896,7 @@ setUniformStyle(ws::Worksheet, rng::SheetColumnRange) = do_sheet_names_match(ws, 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_ncranges(setUniformStyle, ws, ncrng) +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) setUniformStyle(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) = process_uniform_intcolon(ws, row, :) @@ -1990,8 +1990,10 @@ setColumnWidth(xl::XLSXFile, sheetcell::String; kw...)::Int = process_sheetcell( 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_intcolon(setColumnWidth, ws, row, :; kw...) +setColumnWidth(ws::Worksheet, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setColumnWidth, ws, :, col; kw...) setColumnWidth(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setColumnWidth, ws, :, col; kw...) setColumnWidth(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setColumnWidth, ws, row, :; kw...) +setColumnWidth(ws::Worksheet, col::Vector{Int}; kw...) = process_colonvec(setColumnWidth, ws, :, col; kw...) setColumnWidth(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setColumnWidth, ws, :, col; kw...) setColumnWidth(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setColumnWidth, ws, :; kw...) setColumnWidth(ws::Worksheet, ::Colon; kw...) = process_colon(setColumnWidth, ws, :; kw...) @@ -2192,8 +2194,10 @@ setRowHeight(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ra 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_intcolon(setRowHeight, ws, row, :; kw...) setRowHeight(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_intcolon(setRowHeight, ws, row, :; kw...) setRowHeight(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setRowHeight, ws, :, col; kw...) +setRowHeight(ws::Worksheet, row::Vector{Int}; kw...) = process_veccolon(setRowHeight, ws, row, :; kw...) setRowHeight(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setRowHeight, ws, row, :; kw...) setRowHeight(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setRowHeight, ws, :, col; kw...) setRowHeight(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setRowHeight, ws, :; kw...) diff --git a/test/runtests.jl b/test/runtests.jl index c020b597..85155132 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1625,6 +1625,21 @@ 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")) + xfile = XLSX.open_empty_template() wb = XLSX.get_workbook(xfile) sheet = xfile["Sheet1"] @@ -1812,13 +1827,11 @@ end diagonal=["style" => "none"] ) - # Uniform functions can't take non-contiguous ranges - @test_throws XLSX.XLSXError XLSX.setUniformBorder(s, "Mock-up!A1:B4,Mock-up!D4:E6"; left=["style" => "dotted", "color" => "darkseagreen3"], + @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"] - ) + 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) From 29ab864de5b49043d54f6aaffbae83b11b7c6d7c Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Mon, 14 Apr 2025 09:01:37 +0100 Subject: [PATCH 074/154] Small change to docs. --- src/cellformats.jl | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/cellformats.jl b/src/cellformats.jl index 77f60b73..5a2ccb0e 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -227,6 +227,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. @@ -247,6 +250,8 @@ julia> setUniformFont(sh, "33"; italic=true, color="FF8888FF", under="single") julia> setUniformFont(sh, "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 ``` """ @@ -793,6 +798,9 @@ 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. @@ -812,6 +820,9 @@ Julia> setUniformBorder(xf, "Sheet1!A1:F20"; left = ["style" => "dotted", "c bottom = ["style" => "medium", "color" => "FF0000FF"], diagonal = ["style" => "none"] ) + +julia> setUniformBorder(sh, "B2,A5:D22") # Copy `Border` from B2 to cells in A5:D22 + ``` """ @@ -1230,6 +1241,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. @@ -1240,6 +1254,8 @@ For keyword definitions see [`setFill()`](@ref). Julia> setUniformFill(sh, "B2:D4"; pattern="gray125", bgColor = "FF000000") 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 ``` """ @@ -1538,6 +1554,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. @@ -1551,6 +1570,8 @@ 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 ``` """ @@ -1816,6 +1837,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. @@ -1826,6 +1850,8 @@ 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 ``` """ From bb90ff89f84e0b5126d8ffecd8c820aa9e47d0ce Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Mon, 14 Apr 2025 09:43:25 +0100 Subject: [PATCH 075/154] Fix behavior of `getFormat` when no keywords given. --- src/cellformats.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cellformats.jl b/src/cellformats.jl index 5a2ccb0e..2d42a6fd 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -1653,7 +1653,7 @@ function getFormat(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellFormat 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 = XML.children(format_elements)[parse(Int, numfmtid)+1-PREDEFINED_NUMFMT_COUNT] # Zero based! + 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 @@ -1773,7 +1773,7 @@ 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 From fae2925acca0c3c75c6a6bcdebcab738da0dc2d1 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Mon, 14 Apr 2025 11:15:40 +0100 Subject: [PATCH 076/154] Add `StepRange` to indexing options. --- src/cellformat-helpers.jl | 56 ++++++++-------- src/cellformats.jl | 134 +++++++++++++++++++------------------- src/worksheet.jl | 18 ++--- src/write.jl | 14 ++-- 4 files changed, 111 insertions(+), 111 deletions(-) diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl index 9f4caee0..16bd6ab6 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -453,7 +453,7 @@ function process_colonint(f::Function, ws::Worksheet, ::Colon, col::Union{Intege return f(ws, rng; kw...) end end -function process_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon; kw...) +function process_veccolon(f::Function, ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) @@ -476,7 +476,7 @@ function process_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon; end return -1 end -function process_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}; kw...) +function process_colonvec(f::Function, ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) @@ -499,13 +499,13 @@ function process_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}; end return -1 end -function process_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) +function process_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) if length(col) == 1 && length(row) == 1 single = true else single = false end - for a in collect(row), b in 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.")) @@ -515,13 +515,13 @@ function process_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange end return -1 end -function process_vecint(f::Function, ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) +function process_vecint(f::Function, ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) if length(col) == 1 && length(row) == 1 single = true else single = false end - for a in row, b in collect(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.")) @@ -531,7 +531,7 @@ function process_vecint(f::Function, ws::Worksheet, row::Vector{Int}, col::Union end return -1 end -function process_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) +function process_vecvec(f::Function, ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) if length(col) == 1 && length(row) == 1 single = true else @@ -648,7 +648,7 @@ function process_uniform_colonint(f::Function, ws::Worksheet, ::Colon, col::Unio f(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))); kw...) end end -function process_uniform_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon, atts::Vector{String}; kw...) +function process_uniform_veccolon(f::Function, ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon, atts::Vector{String}; kw...) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) @@ -672,7 +672,7 @@ function process_uniform_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, end end end -function process_uniform_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}, atts::Vector{String}; kw...) +function process_uniform_colonvec(f::Function, ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}, atts::Vector{String}; kw...) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) @@ -696,11 +696,11 @@ function process_uniform_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vect end end end -function process_uniform_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}, atts::Vector{String}; kw...) +function process_uniform_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}, atts::Vector{String}; kw...) let newid::Union{Int,Nothing}, first::Bool newid = nothing first = true - for a in collect(row), b in col + for a in row, b in col cellref = CellRef(a, b).name if getcell(ws, cellref) isa EmptyCell continue @@ -713,11 +713,11 @@ function process_uniform_intvec(f::Function, ws::Worksheet, row::Union{Integer,U return newid end end -function process_uniform_vecint(f::Function, ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}, atts::Vector{String}; kw...) +function process_uniform_vecint(f::Function, ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}, atts::Vector{String}; kw...) let newid::Union{Int,Nothing}, first::Bool newid = nothing first = true - for a in row, b in collect(col) + for a in row, b in col cellref = CellRef(a, b) if getcell(ws, cellref) isa EmptyCell continue @@ -730,7 +730,7 @@ function process_uniform_vecint(f::Function, ws::Worksheet, row::Vector{Int}, co return newid end end -function process_uniform_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, col::Vector{Int}, atts::Vector{String}; kw...) +function process_uniform_vecvec(f::Function, ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}, atts::Vector{String}; kw...) let newid::Union{Int,Nothing}, first::Bool newid = nothing first = true @@ -823,7 +823,7 @@ function process_uniform_colon(ws::Worksheet, ::Colon) setUniformStyle(ws, dim) end end -function process_uniform_veccolon(ws::Worksheet, row::Vector{Int}, ::Colon) +function process_uniform_veccolon(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) @@ -847,7 +847,7 @@ function process_uniform_veccolon(ws::Worksheet, row::Vector{Int}, ::Colon) end end end -function process_uniform_colonvec(ws::Worksheet, ::Colon, col::Vector{Int}) +function process_uniform_colonvec(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) @@ -871,11 +871,11 @@ function process_uniform_colonvec(ws::Worksheet, ::Colon, col::Vector{Int}) end end end -function process_uniform_intvec(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}) +function process_uniform_intvec(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}) let newid::Union{Int,Nothing}, first::Bool newid = nothing first = true - for a in collect(row), b in col + for a in row, b in col cellref = CellRef(a, b).name if getcell(ws, cellref) isa EmptyCell continue @@ -888,11 +888,11 @@ function process_uniform_intvec(ws::Worksheet, row::Union{Integer,UnitRange{<:In return newid end end -function process_uniform_vecint(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}) +function process_uniform_vecint(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}) let newid::Union{Int,Nothing}, first::Bool newid = nothing first = true - for a in row, b in collect(col) + for a in row, b in col cellref = CellRef(a, b) if getcell(ws, cellref) isa EmptyCell continue @@ -905,7 +905,7 @@ function process_uniform_vecint(ws::Worksheet, row::Vector{Int}, col::Union{Inte return newid end end -function process_uniform_vecvec(ws::Worksheet, row::Vector{Int}, col::Vector{Int}) +function process_uniform_vecvec(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}) let newid::Union{Int,Nothing}, first::Bool newid = nothing first = true @@ -1002,7 +1002,7 @@ function process_uniform_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguo end end end -function process_uniform_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, ::Colon; kw...) +function process_uniform_veccolon(f::Function, ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) @@ -1027,7 +1027,7 @@ function process_uniform_veccolon(f::Function, ws::Worksheet, row::Vector{Int}, end end end -function process_uniform_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vector{Int}; kw...) +function process_uniform_colonvec(f::Function, ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) @@ -1052,12 +1052,12 @@ function process_uniform_colonvec(f::Function, ws::Worksheet, ::Colon, col::Vect end end end -function process_uniform_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) +function process_uniform_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} newid = nothing first = true alignment_node = nothing - for a in collect(row), b in col + for a in row, b in col cellref = CellRef(a, b) if getcell(ws, cellref) isa EmptyCell continue @@ -1070,12 +1070,12 @@ function process_uniform_intvec(f::Function, ws::Worksheet, row::Union{Integer,U return newid end end -function process_uniform_vecint(f::Function, ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) +function process_uniform_vecint(f::Function, ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} newid = nothing first = true alignment_node = nothing - for a in row, b in collect(col) + for a in row, b in col cellref = CellRef(a, b) if getcell(ws, cellref) isa EmptyCell continue @@ -1088,7 +1088,7 @@ function process_uniform_vecint(f::Function, ws::Worksheet, row::Vector{Int}, co return newid end end -function process_uniform_vecvec(f::Function, ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) +function process_uniform_vecvec(f::Function, ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} newid = nothing first = true diff --git a/src/cellformats.jl b/src/cellformats.jl index 2d42a6fd..9e9af20f 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -98,13 +98,13 @@ setFont(xl::XLSXFile, sheetcell::String; kw...)::Int = process_sheetcell(setFont 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_intcolon(setFont, ws, row, :; kw...) setFont(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFont, ws, :, col; kw...) -setFont(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFont, ws, row, :; kw...) -setFont(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setFont, ws, :, col; kw...) +setFont(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setFont, ws, row, :; kw...) +setFont(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_colonvec(setFont, ws, :, col; kw...) setFont(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setFont, ws, :; kw...) setFont(ws::Worksheet, ::Colon; kw...) = process_colon(setFont, ws, :; kw...) -setFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setFont, ws, row, col; kw...) -setFont(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFont, ws, row, col; kw...) -setFont(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(setFont, ws, row, col; kw...) +setFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_intvec(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_vecvec(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, @@ -266,13 +266,13 @@ setUniformFont(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sh 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_uniform_intcolon(setFont, ws, row, :; kw...) setUniformFont(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setFont, ws, :, col; kw...) -setUniformFont(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setFont, ws, row, :, ["fontId", "applyFont"]; kw...) -setUniformFont(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setFont, ws, :, col, ["fontId", "applyFont"]; kw...) +setUniformFont(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_uniform_veccolon(setFont, ws, row, :, ["fontId", "applyFont"]; kw...) +setUniformFont(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_colonvec(setFont, ws, :, col, ["fontId", "applyFont"]; kw...) setUniformFont(ws::Worksheet, ::Colon, ::Colon; kw...) = process_uniform_colon(setUniformFont, ws, :; kw...) setUniformFont(ws::Worksheet, ::Colon; kw...) = process_uniform_colon(setUniformFont, ws, :; kw...) -setUniformFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setFont, ws, row, col, ["fontId", "applyFont"]; kw...) -setUniformFont(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFont, ws, row, col, ["fontId", "applyFont"]; kw...) -setUniformFont(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setFont, ws, row, col, ["fontId", "applyFont"]; kw...) +setUniformFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_intvec(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_vecvec(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...) @@ -592,11 +592,11 @@ setBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw.. setBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setBorder, ws, :, col; kw...) setBorder(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setBorder, ws, :; kw...) setBorder(ws::Worksheet, ::Colon; kw...) = process_colon(setBorder, ws, :; kw...) -setBorder(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setBorder, ws, row, :; kw...) -setBorder(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setBorder, ws, :, col; kw...) -setBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setBorder, ws, row, col; kw...) -setBorder(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setBorder, ws, row, col; kw...) -setBorder(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(setBorder, ws, row, col; kw...) +setBorder(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setBorder, ws, row, :; kw...) +setBorder(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_colonvec(setBorder, ws, :, col; kw...) +setBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_intvec(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_vecvec(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, @@ -837,13 +837,13 @@ setUniformBorder(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_ 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_uniform_intcolon(setBorder, ws, row, :; kw...) setUniformBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setBorder, ws, :, col; kw...) -setUniformBorder(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setBorder, ws, row, :, ["borderId", "applyBorder"]; kw...) -setUniformBorder(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setBorder, ws, :, col, ["borderId", "applyBorder"]; kw...) +setUniformBorder(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_uniform_veccolon(setBorder, ws, row, :, ["borderId", "applyBorder"]; kw...) +setUniformBorder(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_colonvec(setBorder, ws, :, col, ["borderId", "applyBorder"]; kw...) setUniformBorder(ws::Worksheet, ::Colon, ::Colon; kw...) = process_uniform_colon(setUniformBorder, ws, :; kw...) setUniformBorder(ws::Worksheet, ::Colon; kw...) = process_uniform_colon(setUniformBorder, ws, :; kw...) -setUniformBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...) -setUniformBorder(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...) -setUniformBorder(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setBorder, ws, row, col, ["borderId", "applyBorder"]; kw...) +setUniformBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_intvec(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_vecvec(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...) @@ -1136,11 +1136,11 @@ setFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) setFill(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setFill, ws, :; kw...) setFill(ws::Worksheet, ::Colon; kw...) = process_colon(setFill, ws, :; kw...) setFill(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFill, ws, :, col; kw...) -setFill(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFill, ws, row, :; kw...) -setFill(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setFill, ws, :, col; kw...) -setFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setFill, ws, row, col; kw...) -setFill(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFill, ws, row, col; kw...) -setFill(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(setFill, ws, row, col; kw...) +setFill(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setFill, ws, row, :; kw...) +setFill(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_colonvec(setFill, ws, :, col; kw...) +setFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_intvec(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_vecvec(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, @@ -1270,13 +1270,13 @@ setUniformFill(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sh 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_uniform_intcolon(setFill, ws, row, :; kw...) setUniformFill(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setFill, ws, :, col; kw...) -setUniformFill(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setFill, ws, row, :, ["fillId", "applyFill"]; kw...) -setUniformFill(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setFill, ws, :, col, ["fillId", "applyFill"]; kw...) +setUniformFill(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_uniform_veccolon(setFill, ws, row, :, ["fillId", "applyFill"]; kw...) +setUniformFill(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_colonvec(setFill, ws, :, col, ["fillId", "applyFill"]; kw...) setUniformFill(ws::Worksheet, ::Colon, ::Colon; kw...) = process_uniform_colon(setUniformFill, ws, :; kw...) setUniformFill(ws::Worksheet, ::Colon; kw...) = process_uniform_colon(setUniformFill, ws, :; kw...) -setUniformFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setFill, ws, row, col, ["fillId", "applyFill"]; kw...) -setUniformFill(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFill, ws, row, col, ["fillId", "applyFill"]; kw...) -setUniformFill(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setFill, ws, row, col, ["fillId", "applyFill"]; kw...) +setUniformFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_intvec(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_vecvec(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...) @@ -1439,11 +1439,11 @@ setAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; k setAlignment(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setAlignment, ws, :; kw...) setAlignment(ws::Worksheet, ::Colon; kw...) = process_colon(setAlignment, ws, :; kw...) setAlignment(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setAlignment, ws, :, col; kw...) -setAlignment(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setAlignment, ws, row, :; kw...) -setAlignment(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setAlignment, ws, :, col; kw...) -setAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setAlignment, ws, row, col; kw...) -setAlignment(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setAlignment, ws, row, col; kw...) -setAlignment(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(setAlignment, ws, row, col; kw...) +setAlignment(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setAlignment, ws, row, :; kw...) +setAlignment(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_colonvec(setAlignment, ws, :, col; kw...) +setAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_intvec(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_vecvec(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, @@ -1586,13 +1586,13 @@ setUniformAlignment(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = proce 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_uniform_intcolon(setAlignment, ws, row, :; kw...) setUniformAlignment(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setAlignment, ws, :, col; kw...) -setUniformAlignment(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setAlignment, ws, row, :; kw...) -setUniformAlignment(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setAlignment, ws, :, col; kw...) +setUniformAlignment(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_uniform_veccolon(setAlignment, ws, row, :; kw...) +setUniformAlignment(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_colonvec(setAlignment, ws, :, col; kw...) setUniformAlignment(ws::Worksheet, ::Colon, ::Colon; kw...) = process_uniform_colon(setUniformAlignment, ws, :; kw...) setUniformAlignment(ws::Worksheet, ::Colon; kw...) = process_uniform_colon(setUniformAlignment, ws, :; kw...) -setUniformAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setAlignment, ws, row, col; kw...) -setUniformAlignment(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setAlignment, ws, row, col; kw...) -setUniformAlignment(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setAlignment, ws, row, col; kw...) +setUniformAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_intvec(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_vecvec(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...) @@ -1738,11 +1738,11 @@ setFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw.. setFormat(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFormat, ws, :, col; kw...) setFormat(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setFormat, ws, :; kw...) setFormat(ws::Worksheet, ::Colon; kw...) = process_colon(setFormat, ws, :; kw...) -setFormat(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setFormat, ws, row, :; kw...) -setFormat(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setFormat, ws, :, col; kw...) -setFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setFormat, ws, row, col; kw...) -setFormat(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setFormat, ws, row, col; kw...) -setFormat(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(setFormat, ws, row, col; kw...) +setFormat(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setFormat, ws, row, :; kw...) +setFormat(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_colonvec(setFormat, ws, :, col; kw...) +setFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_intvec(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_vecvec(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, @@ -1866,13 +1866,13 @@ setUniformFormat(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_ setUniformFormat(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setUniformFormat, ws, ref_or_rng; kw...) setUniformFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_uniform_intcolon(setFormat, ws, row, :; kw...) setUniformFormat(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setFormat, ws, :, col; kw...) -setUniformFormat(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_uniform_veccolon(setFormat, ws, row, :, ["numFmtId", "applyNumberFormat"]; kw...) -setUniformFormat(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_uniform_colonvec(setFormat, ws, :, col, ["numFmtId", "applyNumberFormat"]; kw...) +setUniformFormat(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_uniform_veccolon(setFormat, ws, row, :, ["numFmtId", "applyNumberFormat"]; kw...) +setUniformFormat(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_colonvec(setFormat, ws, :, col, ["numFmtId", "applyNumberFormat"]; kw...) setUniformFormat(ws::Worksheet, ::Colon, ::Colon; kw...) = process_uniform_colon(setUniformFormat, ws, :; kw...) setUniformFormat(ws::Worksheet, ::Colon; kw...) = process_uniform_colon(setUniformFormat, ws, :; kw...) -setUniformFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_uniform_intvec(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...) -setUniformFormat(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_vecint(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...) -setUniformFormat(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_uniform_vecvec(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; kw...) +setUniformFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_intvec(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_vecvec(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...) @@ -1927,13 +1927,13 @@ setUniformStyle(xl::XLSXFile, sheetcell::AbstractString)::Int = process_sheetcel setUniformStyle(ws::Worksheet, ref_or_rng::AbstractString)::Int = process_ranges(setUniformStyle, ws, ref_or_rng) setUniformStyle(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) = process_uniform_intcolon(ws, row, :) setUniformStyle(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}) = process_uniform_colonint(ws, :, col) -setUniformStyle(ws::Worksheet, row::Vector{Int}, ::Colon) = process_uniform_veccolon(ws, row, :) -setUniformStyle(ws::Worksheet, ::Colon, col::Vector{Int}) = process_uniform_colonvec(ws, :, col) +setUniformStyle(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon) = process_uniform_veccolon(ws, row, :) +setUniformStyle(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}) = process_uniform_colonvec(ws, :, col) setUniformStyle(ws::Worksheet, ::Colon, ::Colon) = process_uniform_colon(ws, :) setUniformStyle(ws::Worksheet, ::Colon) = process_uniform_colon(ws, :) -setUniformStyle(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}) = process_uniform_intvec(ws, row, col) -setUniformStyle(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}) = process_uniform_vecint(ws, row, col) -setUniformStyle(ws::Worksheet, row::Vector{Int}, col::Vector{Int}) = process_uniform_vecvec(ws, row, col) +setUniformStyle(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}) = process_uniform_intvec(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_vecvec(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} @@ -2018,14 +2018,14 @@ setColumnWidth(ws::Worksheet, row::Integer, col::Integer; kw...) = setColumnWidt setColumnWidth(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_intcolon(setColumnWidth, ws, row, :; kw...) setColumnWidth(ws::Worksheet, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setColumnWidth, ws, :, col; kw...) setColumnWidth(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setColumnWidth, ws, :, col; kw...) -setColumnWidth(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setColumnWidth, ws, row, :; kw...) -setColumnWidth(ws::Worksheet, col::Vector{Int}; kw...) = process_colonvec(setColumnWidth, ws, :, col; kw...) -setColumnWidth(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setColumnWidth, ws, :, col; kw...) +setColumnWidth(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setColumnWidth, ws, row, :; kw...) +setColumnWidth(ws::Worksheet, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_colonvec(setColumnWidth, ws, :, col; kw...) +setColumnWidth(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_colonvec(setColumnWidth, ws, :, col; kw...) setColumnWidth(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setColumnWidth, ws, :; kw...) setColumnWidth(ws::Worksheet, ::Colon; kw...) = process_colon(setColumnWidth, ws, :; kw...) -setColumnWidth(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setColumnWidth, ws, row, col; kw...) -setColumnWidth(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setColumnWidth, ws, row, col; kw...) -setColumnWidth(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(setColumnWidth, ws, row, col; kw...) +setColumnWidth(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_intvec(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_vecvec(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 @@ -2223,14 +2223,14 @@ setRowHeight(ws::Worksheet, row::Integer, col::Integer; kw...) = setRowHeight(ws setRowHeight(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}; kw...) = process_intcolon(setRowHeight, ws, row, :; kw...) setRowHeight(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_intcolon(setRowHeight, ws, row, :; kw...) setRowHeight(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setRowHeight, ws, :, col; kw...) -setRowHeight(ws::Worksheet, row::Vector{Int}; kw...) = process_veccolon(setRowHeight, ws, row, :; kw...) -setRowHeight(ws::Worksheet, row::Vector{Int}, ::Colon; kw...) = process_veccolon(setRowHeight, ws, row, :; kw...) -setRowHeight(ws::Worksheet, ::Colon, col::Vector{Int}; kw...) = process_colonvec(setRowHeight, ws, :, col; kw...) +setRowHeight(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_veccolon(setRowHeight, ws, row, :; kw...) +setRowHeight(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setRowHeight, ws, row, :; kw...) +setRowHeight(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_colonvec(setRowHeight, ws, :, col; kw...) setRowHeight(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setRowHeight, ws, :; kw...) setRowHeight(ws::Worksheet, ::Colon; kw...) = process_colon(setRowHeight, ws, :; kw...) -setRowHeight(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}; kw...) = process_intvec(setRowHeight, ws, row, col; kw...) -setRowHeight(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_vecint(setRowHeight, ws, row, col; kw...) -setRowHeight(ws::Worksheet, row::Vector{Int}, col::Vector{Int}; kw...) = process_vecvec(setRowHeight, ws, row, col; kw...) +setRowHeight(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_intvec(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_vecvec(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 diff --git a/src/worksheet.jl b/src/worksheet.jl index f23a1e72..eecab0ef 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -116,9 +116,9 @@ 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::Int, col::Vector{Int}) = [getdata(ws, a, b) for a in [row], b in col] -getdata(ws::Worksheet, row::Vector{Int}, col::Int) = [getdata(ws, a, b) for a in row, b in [col]] -getdata(ws::Worksheet, row::Vector{Int}, col::Vector{Int}) = [getdata(ws, a, b) for a in row, b in 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) @@ -408,9 +408,9 @@ 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::Vector{Int}) = [getcell(ws, a, b) for a in collect(row), b in col] -getcell(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}) = [getcell(ws, a, b) for a in row, b in collect(col)] -getcell(ws::Worksheet, row::Vector{Int}, col::Vector{Int}) = [getcell(ws, a, b) for a in row, b in col] +getcell(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}) = [getcell(ws, a, b) for a in row, b in col] +getcell(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}) = [getcell(ws, a, b) for a in row, b in col] +getcell(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] 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) @@ -520,9 +520,9 @@ getcellrange(ws::Worksheet, s::SheetCellRange) = getcellrange(ws, s.rng) getcellrange(ws::Worksheet, s::SheetColumnRange) = getcellrange(ws, s.colrng) getcellrange(ws::Worksheet, s::SheetRowRange) = getcellrange(ws, s.rowrng) -getcellrange(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Vector{Int}) = [getcell(ws, a, b) for a in collect(row), b in col] -getcellrange(ws::Worksheet, row::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}) = [getcell(ws, a, b) for a in row, b in collect(col)] -getcellrange(ws::Worksheet, row::Vector{Int}, col::Vector{Int}) = [getcell(ws, a, b) for a in row, b in col] +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) diff --git a/src/write.jl b/src/write.jl index fa5fc4f8..5ea1c1ac 100644 --- a/src/write.jl +++ b/src/write.jl @@ -527,17 +527,17 @@ Base.setindex!(ws::Worksheet, v::AbstractVector, ref; dim::Integer=2) = setdata! 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::Vector{Int}) - for a in collect(row), b in col +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::Vector{Int}, col::Union{Integer,UnitRange{<:Integer}}) - for a in row, b in collect(col) +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::Vector{Int}, col::Vector{Int}) +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 @@ -698,7 +698,7 @@ function setdata!(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer setdata!(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))), v) end end -function setdata!(ws::Worksheet, row::Vector{Int}, ::Colon, v) +function setdata!(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon, v) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) @@ -710,7 +710,7 @@ function setdata!(ws::Worksheet, row::Vector{Int}, ::Colon, v) end end end -function setdata!(ws::Worksheet, ::Colon, col::Vector{Int}, v) +function setdata!(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}, v) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) From 94e5037e032463b945d5b0bed237c329ddaaaf58 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Tue, 15 Apr 2025 18:31:44 +0100 Subject: [PATCH 077/154] Detect Excel `.xltx` template files and throw (#293). Also rationalise `cellformat-helpers` --- src/cellformat-helpers.jl | 368 +++++++------------------------------- src/cellformats.jl | 232 ++++++++++++------------ src/cellref.jl | 1 + src/read.jl | 35 ++-- src/workbook.jl | 4 +- src/worksheet.jl | 173 ++---------------- src/write.jl | 18 +- 7 files changed, 214 insertions(+), 617 deletions(-) diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl index 16bd6ab6..fa056924 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -381,7 +381,6 @@ function process_cellranges(f::Function, ws::Worksheet, rng::CellRange; 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.")) @@ -427,70 +426,45 @@ end # # - Used for indexing `setAttribute` family of functions # -function process_colon(f::Function, ws::Worksheet, ::Colon; kw...) +function process_colon(f::Function, ws::Worksheet, row, col; kw...) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) - else - return f(ws, dim; kw...) end -end -function process_intcolon(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else + 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)) - return f(ws, rng; kw...) - end -end -function process_colonint(f::Function, ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else + elseif isnothing(row) rng = CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))) - return f(ws, rng; kw...) + else + throw(XLSXError("Something wrong here!")) end + + return f(ws, rng; kw...) end -function process_veccolon(f::Function, ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) +function process_veccolon(f::Function, ws::Worksheet, row, col; kw...) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) else - if length(row) == 1 && dim.start.column_number == dim.stop.column_number - single = true + if isnothing(col) + col = dim.start.column_number:dim.stop.column_number + elseif isnothing(row) + row = dim.start.row_number:dim.stop.row_number else - single = false + throw(XLSXError("Something wrong here!")) end - 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 - single && throw(XLSXError("Cannot set format for an `EmptyCell`: $(cellref.name). Set the value first.")) - continue - end - f(ws, cellref; kw...) - end - end - end - return -1 -end -function process_colonvec(f::Function, ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - if length(col) == 1 && dim.start.row_number == dim.stop.row_number + if length(row) == 1 && length(col) == 1 single = true else single = false end - for b in col - for a in dim.start.row_number:dim.stop.row_number + for a in row + for 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.")) + single && throw(XLSXError("Cannot set attribute for an `EmptyCell`: $(cellref.name). Set the value first.")) continue end f(ws, cellref; kw...) @@ -499,23 +473,7 @@ function process_colonvec(f::Function, ws::Worksheet, ::Colon, col::Union{Vector end return -1 end -function process_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) - if length(col) == 1 && length(row) == 1 - single = true - else - single = false - end - 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 -function process_vecint(f::Function, ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) +function process_vecint(f::Function, ws::Worksheet, row, col; kw...) if length(col) == 1 && length(row) == 1 single = true else @@ -531,29 +489,13 @@ function process_vecint(f::Function, ws::Worksheet, row::Union{Vector{Int},StepR end return -1 end -function process_vecvec(f::Function, ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) - if length(col) == 1 && length(row) == 1 - single = true - else - single = false - end - for a in row, b in col - cellref = CellRef(a, b) - if getcell(ws, cellref) isa EmptyCell - continue - single && throw(XLSXError("Cannot set format for an `EmptyCell`: $(cellref.name). Set the value first.")) - end - f(ws, cellref; kw...) - end - return -1 -end # # - Used for indexing `setUniformAttribute` family of functions # # -# Most set functions +# Most set functions (but not Style or Alignment) # function process_uniform_core(f::Function, ws::Worksheet, cellref::CellRef, atts::Vector{String}, newid::Union{Int,Nothing}, first::Bool; kw...) cell = getcell(ws, cellref) @@ -572,11 +514,9 @@ function process_uniform_core(f::Function, ws::Worksheet, cellref::CellRef, atts 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 - let newid::Union{Int,Nothing}, first::Bool newid = nothing first = true @@ -624,64 +564,23 @@ function process_uniform_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguo end end end -function process_uniform_colon(f::Function, ws::Worksheet, ::Colon; kw...) +function process_uniform_veccolon(f::Function, ws::Worksheet, row, col, atts::Vector{String}; kw...) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) else - f(ws, dim; kw...) - end -end -function process_uniform_intcolon(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - f(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)); kw...) - end -end -function process_uniform_colonint(f::Function, ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - f(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))); kw...) - end -end -function process_uniform_veccolon(f::Function, ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon, atts::Vector{String}; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - let newid::Union{Int,Nothing}, first::Bool - newid = nothing - first = true - 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 = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) - end - end - if first - newid = -1 - end - return newid + if isnothing(col) + col = dim.start.column_number:dim.stop.column_number + elseif isnothing(row) + row = dim.start.row_number:dim.stop.row_number + else + throw(XLSXError("Something wrong here!")) end - end -end -function process_uniform_colonvec(f::Function, ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}, atts::Vector{String}; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else let newid::Union{Int,Nothing}, first::Bool newid = nothing first = true - for b in col - for a in dim.start.row_number:dim.stop.row_number + for a in row + for b in col cellref = CellRef(a, b) if getcell(ws, cellref) isa EmptyCell continue @@ -696,41 +595,7 @@ function process_uniform_colonvec(f::Function, ws::Worksheet, ::Colon, col::Unio end end end -function process_uniform_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}, atts::Vector{String}; kw...) - let newid::Union{Int,Nothing}, first::Bool - newid = nothing - first = true - for a in row, b in col - cellref = CellRef(a, b).name - if getcell(ws, cellref) isa EmptyCell - continue - end - newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) - end - if first - newid = -1 - end - return newid - end -end -function process_uniform_vecint(f::Function, ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}, atts::Vector{String}; kw...) - let newid::Union{Int,Nothing}, first::Bool - newid = nothing - first = true - 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, cellref, atts, newid, first; kw...) - end - if first - newid = -1 - end - return newid - end -end -function process_uniform_vecvec(f::Function, ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}, atts::Vector{String}; kw...) +function process_uniform_vecint(f::Function, ws::Worksheet, row, col, atts::Vector{String}; kw...) let newid::Union{Int,Nothing}, first::Bool newid = nothing first = true @@ -799,64 +664,40 @@ function process_uniform_ncranges(ws::Worksheet, ncrng::NonContiguousRange)::Int end end end -function process_uniform_intcolon(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) +function process_colon(ws::Worksheet, row, col) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) - else - setUniformStyle(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number))) end -end -function process_uniform_colonint(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - setUniformStyle(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col)))) - end -end -function process_uniform_colon(ws::Worksheet, ::Colon) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) + if isnothing(row) && isnothing(col) + return setUniformStyle(ws, dim; kw...) + elseif isnothing(col) + rng = CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)) + elseif isnothing(row) + rng = CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))) else - setUniformStyle(ws, dim) + throw(XLSXError("Something wrong here!")) end + + return setUniformStyle(ws, rng; kw...) end -function process_uniform_veccolon(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon) +function process_uniform_veccolon(ws::Worksheet, row, col) dim = get_dimension(ws) if dim === nothing throw(XLSXError("No worksheet dimension found")) else - let newid::Union{Int,Nothing}, first::Bool - newid = nothing - first = true - 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 = process_uniform_core(ws, cellref, newid, first) - end - end - if first - newid = -1 - end - return newid + if isnothing(col) + col = dim.start.column_number:dim.stop.column_number + elseif isnothing(row) + row = dim.start.row_number:dim.stop.row_number + else + throw(XLSXError("Something wrong here!")) end - end -end -function process_uniform_colonvec(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else let newid::Union{Int,Nothing}, first::Bool newid = nothing first = true - for b in col - for a in dim.start.row_number:dim.stop.row_number + for a in row + for b in col cellref = CellRef(a, b) if getcell(ws, cellref) isa EmptyCell continue @@ -871,41 +712,7 @@ function process_uniform_colonvec(ws::Worksheet, ::Colon, col::Union{Vector{Int} end end end -function process_uniform_intvec(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}) - let newid::Union{Int,Nothing}, first::Bool - newid = nothing - first = true - for a in row, b in col - cellref = CellRef(a, b).name - 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 -function process_uniform_vecint(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}) - let newid::Union{Int,Nothing}, first::Bool - newid = nothing - first = true - 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 -function process_uniform_vecvec(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}) +function process_uniform_vecint(ws::Worksheet, row, col) let newid::Union{Int,Nothing}, first::Bool newid = nothing first = true @@ -945,11 +752,9 @@ function process_uniform_core(f::Function, ws::Worksheet, cellref::CellRef, newi 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 - let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} newid = nothing first = true @@ -1002,11 +807,18 @@ function process_uniform_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguo end end end -function process_uniform_veccolon(f::Function, ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) +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 + if isnothing(col) + col = dim.start.column_number:dim.stop.column_number + elseif isnothing(row) + row = dim.start.row_number:dim.stop.row_number + else + throw(XLSXError("Something wrong here!")) + end let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} newid = nothing first = true @@ -1027,50 +839,7 @@ function process_uniform_veccolon(f::Function, ws::Worksheet, row::Union{Vector{ end end end -function process_uniform_colonvec(f::Function, ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} - newid = nothing - first = true - alignment_node = nothing - for b in col - for a in dim.start.row_number:dim.stop.row_number - cellref = CellRef(a, b) - if getcell(ws, cellref) isa EmptyCell - continue - end - newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) - end - end - if first - newid = -1 - end - return newid - end - end -end -function process_uniform_intvec(f::Function, ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) - let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} - newid = nothing - first = true - alignment_node = nothing - 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, cellref, newid, first, alignment_node; kw...) - end - if first - newid = -1 - end - return newid - end -end -function process_uniform_vecint(f::Function, ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) +function process_uniform_vecint(f::Function, ws::Worksheet, row, col; kw...) let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} newid = nothing first = true @@ -1088,23 +857,6 @@ function process_uniform_vecint(f::Function, ws::Worksheet, row::Union{Vector{In return newid end end -function process_uniform_vecvec(f::Function, ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) - let newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing} - newid = nothing - first = true - 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, 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) diff --git a/src/cellformats.jl b/src/cellformats.jl index 9e9af20f..3c344abd 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -96,15 +96,15 @@ setFont(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncranges 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...) 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_intcolon(setFont, ws, row, :; kw...) -setFont(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFont, ws, :, col; kw...) -setFont(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setFont, ws, row, :; kw...) -setFont(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_colonvec(setFont, ws, :, col; kw...) -setFont(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setFont, ws, :; kw...) -setFont(ws::Worksheet, ::Colon; kw...) = process_colon(setFont, ws, :; kw...) -setFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_intvec(setFont, ws, 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_vecvec(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, @@ -264,15 +264,15 @@ setUniformFont(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges( 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_uniform_intcolon(setFont, ws, row, :; kw...) -setUniformFont(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setFont, ws, :, col; kw...) -setUniformFont(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_uniform_veccolon(setFont, ws, row, :, ["fontId", "applyFont"]; kw...) -setUniformFont(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_colonvec(setFont, ws, :, col, ["fontId", "applyFont"]; kw...) -setUniformFont(ws::Worksheet, ::Colon, ::Colon; kw...) = process_uniform_colon(setUniformFont, ws, :; kw...) -setUniformFont(ws::Worksheet, ::Colon; kw...) = process_uniform_colon(setUniformFont, ws, :; kw...) -setUniformFont(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_intvec(setFont, ws, row, col, ["fontId", "applyFont"]; 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_vecvec(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...) @@ -588,15 +588,15 @@ setBorder(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, r 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(ws::Worksheet, row::Integer, col::Integer; kw...) = setBorder(ws, CellRef(row, col); kw...) -setBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_intcolon(setBorder, ws, row, :; kw...) -setBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setBorder, ws, :, col; kw...) -setBorder(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setBorder, ws, :; kw...) -setBorder(ws::Worksheet, ::Colon; kw...) = process_colon(setBorder, ws, :; kw...) -setBorder(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setBorder, ws, row, :; kw...) -setBorder(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_colonvec(setBorder, ws, :, col; kw...) -setBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_intvec(setBorder, ws, 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_vecvec(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, @@ -835,15 +835,15 @@ setUniformBorder(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowrange 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_uniform_intcolon(setBorder, ws, row, :; kw...) -setUniformBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setBorder, ws, :, col; kw...) -setUniformBorder(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_uniform_veccolon(setBorder, ws, row, :, ["borderId", "applyBorder"]; kw...) -setUniformBorder(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_colonvec(setBorder, ws, :, col, ["borderId", "applyBorder"]; kw...) -setUniformBorder(ws::Worksheet, ::Colon, ::Colon; kw...) = process_uniform_colon(setUniformBorder, ws, :; kw...) -setUniformBorder(ws::Worksheet, ::Colon; kw...) = process_uniform_colon(setUniformBorder, ws, :; kw...) -setUniformBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_intvec(setBorder, ws, row, col, ["borderId", "applyBorder"]; 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_vecvec(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...) @@ -891,10 +891,10 @@ setOutsideBorder(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_colum 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...) -setOutsideBorder(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_uniform_intcolon(setOutsideBorder, ws, row, :; kw...) -setOutsideBorder(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setOutsideBorder, ws, :, col; kw...) -setOutsideBorder(ws::Worksheet, ::Colon, ::Colon; kw...) = process_uniform_colon(setOutsideBorder, ws, :; kw...) -setOutsideBorder(ws::Worksheet, ::Colon; kw...) = process_uniform_colon(setOutsideBorder, ws, :; kw...) +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, @@ -1132,15 +1132,15 @@ setFill(ws::Worksheet, colrng::ColumnRange; kw...)::Int = process_columnranges(s 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_intcolon(setFill, ws, row, :; kw...) -setFill(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setFill, ws, :; kw...) -setFill(ws::Worksheet, ::Colon; kw...) = process_colon(setFill, ws, :; kw...) -setFill(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFill, ws, :, col; kw...) -setFill(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setFill, ws, row, :; kw...) -setFill(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_colonvec(setFill, ws, :, col; kw...) -setFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_intvec(setFill, ws, 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_vecvec(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, @@ -1268,15 +1268,15 @@ setUniformFill(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges( 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_uniform_intcolon(setFill, ws, row, :; kw...) -setUniformFill(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setFill, ws, :, col; kw...) -setUniformFill(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_uniform_veccolon(setFill, ws, row, :, ["fillId", "applyFill"]; kw...) -setUniformFill(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_colonvec(setFill, ws, :, col, ["fillId", "applyFill"]; kw...) -setUniformFill(ws::Worksheet, ::Colon, ::Colon; kw...) = process_uniform_colon(setUniformFill, ws, :; kw...) -setUniformFill(ws::Worksheet, ::Colon; kw...) = process_uniform_colon(setUniformFill, ws, :; kw...) -setUniformFill(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_intvec(setFill, ws, row, col, ["fillId", "applyFill"]; 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_vecvec(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...) @@ -1435,15 +1435,15 @@ setAlignment(ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int = process_ncr 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...) 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_intcolon(setAlignment, ws, row, :; kw...) -setAlignment(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setAlignment, ws, :; kw...) -setAlignment(ws::Worksheet, ::Colon; kw...) = process_colon(setAlignment, ws, :; kw...) -setAlignment(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setAlignment, ws, :, col; kw...) -setAlignment(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setAlignment, ws, row, :; kw...) -setAlignment(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_colonvec(setAlignment, ws, :, col; kw...) -setAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_intvec(setAlignment, ws, 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_vecvec(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, @@ -1584,15 +1584,15 @@ setUniformAlignment(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowra 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_uniform_intcolon(setAlignment, ws, row, :; kw...) -setUniformAlignment(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setAlignment, ws, :, col; kw...) -setUniformAlignment(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_uniform_veccolon(setAlignment, ws, row, :; kw...) -setUniformAlignment(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_colonvec(setAlignment, ws, :, col; kw...) -setUniformAlignment(ws::Worksheet, ::Colon, ::Colon; kw...) = process_uniform_colon(setUniformAlignment, ws, :; kw...) -setUniformAlignment(ws::Worksheet, ::Colon; kw...) = process_uniform_colon(setUniformAlignment, ws, :; kw...) -setUniformAlignment(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_intvec(setAlignment, ws, row, col; 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_vecvec(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...) @@ -1734,15 +1734,15 @@ setFormat(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowranges(setFo 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_intcolon(setFormat, ws, row, :; kw...) -setFormat(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setFormat, ws, :, col; kw...) -setFormat(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setFormat, ws, :; kw...) -setFormat(ws::Worksheet, ::Colon; kw...) = process_colon(setFormat, ws, :; kw...) -setFormat(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setFormat, ws, row, :; kw...) -setFormat(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_colonvec(setFormat, ws, :, col; kw...) -setFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_intvec(setFormat, ws, 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_vecvec(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, @@ -1864,15 +1864,15 @@ setUniformFormat(ws::Worksheet, rowrng::RowRange; kw...)::Int = process_rowrange 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, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_uniform_intcolon(setFormat, ws, row, :; kw...) -setUniformFormat(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_uniform_colonint(setFormat, ws, :, col; kw...) -setUniformFormat(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_uniform_veccolon(setFormat, ws, row, :, ["numFmtId", "applyNumberFormat"]; kw...) -setUniformFormat(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_colonvec(setFormat, ws, :, col, ["numFmtId", "applyNumberFormat"]; kw...) -setUniformFormat(ws::Worksheet, ::Colon, ::Colon; kw...) = process_uniform_colon(setUniformFormat, ws, :; kw...) -setUniformFormat(ws::Worksheet, ::Colon; kw...) = process_uniform_colon(setUniformFormat, ws, :; kw...) -setUniformFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_uniform_intvec(setFormat, ws, row, col, ["numFmtId", "applyNumberFormat"]; 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_vecvec(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...) @@ -1925,15 +1925,15 @@ setUniformStyle(ws::Worksheet, rowrng::RowRange)::Int = process_rowranges(setUni 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) -setUniformStyle(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) = process_uniform_intcolon(ws, row, :) -setUniformStyle(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}) = process_uniform_colonint(ws, :, col) -setUniformStyle(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon) = process_uniform_veccolon(ws, row, :) -setUniformStyle(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}) = process_uniform_colonvec(ws, :, col) -setUniformStyle(ws::Worksheet, ::Colon, ::Colon) = process_uniform_colon(ws, :) -setUniformStyle(ws::Worksheet, ::Colon) = process_uniform_colon(ws, :) -setUniformStyle(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}) = process_uniform_intvec(ws, row, col) +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_vecvec(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} @@ -2015,17 +2015,17 @@ setColumnWidth(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ 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_intcolon(setColumnWidth, ws, row, :; kw...) -setColumnWidth(ws::Worksheet, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setColumnWidth, ws, :, col; kw...) -setColumnWidth(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setColumnWidth, ws, :, col; kw...) -setColumnWidth(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setColumnWidth, ws, row, :; kw...) -setColumnWidth(ws::Worksheet, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_colonvec(setColumnWidth, ws, :, col; kw...) -setColumnWidth(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_colonvec(setColumnWidth, ws, :, col; kw...) -setColumnWidth(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setColumnWidth, ws, :; kw...) -setColumnWidth(ws::Worksheet, ::Colon; kw...) = process_colon(setColumnWidth, ws, :; kw...) -setColumnWidth(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_intvec(setColumnWidth, ws, 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_vecvec(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 @@ -2220,17 +2220,17 @@ setRowHeight(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ra 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_intcolon(setRowHeight, ws, row, :; kw...) -setRowHeight(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_intcolon(setRowHeight, ws, row, :; kw...) -setRowHeight(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colonint(setRowHeight, ws, :, col; kw...) -setRowHeight(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_veccolon(setRowHeight, ws, row, :; kw...) -setRowHeight(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon; kw...) = process_veccolon(setRowHeight, ws, row, :; kw...) -setRowHeight(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_colonvec(setRowHeight, ws, :, col; kw...) -setRowHeight(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setRowHeight, ws, :; kw...) -setRowHeight(ws::Worksheet, ::Colon; kw...) = process_colon(setRowHeight, ws, :; kw...) -setRowHeight(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Vector{Int},StepRange{<:Integer}}; kw...) = process_intvec(setRowHeight, ws, 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_vecvec(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 @@ -2565,10 +2565,10 @@ mergeCells(ws::Worksheet, rowrng::RowRange)::Int = process_rowranges(mergeCells, 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_intcolon(mergeCells, ws, row, :) -mergeCells(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}) = process_colonint(mergeCells, ws, :, col) -mergeCells(ws::Worksheet, ::Colon, ::Colon) = process_colon(mergeCells, ws, :) -mergeCells(ws::Worksheet, ::Colon) = process_colon(mergeCells, ws, :) +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? diff --git a/src/cellref.jl b/src/cellref.jl index 77934272..32a94077 100644 --- a/src/cellref.jl +++ b/src/cellref.jl @@ -258,6 +258,7 @@ For example, for a range "B2:D4", we have: * "D4" relative position is (3, 3) =# + function relative_cell_position(ref::CellRef, rng::CellRange) ref ∉ rng && throw(XLSXError("$ref is outside range $rng.")) diff --git a/src/read.jl b/src/read.jl index f7ebb40f..ec301db3 100644 --- a/src/read.jl +++ b/src/read.jl @@ -292,6 +292,21 @@ function check_minimum_requirements(xf::XLSXFile) !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 end @@ -646,26 +661,6 @@ function readtable(source::Union{AbstractString, IO}, sheet::Union{AbstractStrin return c end -# `readtable` on a row range only partially works. -# Each row in the table is truncated when there is an empty column even if there are more columns in the row. -# It also evaluates the rows on the basis of the table row count, not the sheet row count, giving wrong results. -# These limitations arise because I am trying to implement this functionality without changing the existing code. -# It probably needs a dedicated RowRange implementation. -# It is best not to use this function with row ranges. Use `readdata` or `getdata` instead, both of which work -# on row ranges, or index the sheet directly to get the rows you want (e.g. sh["3"] or sh["3:5"]). -#= -function readtable(source::Union{AbstractString, IO}, sheet::Union{AbstractString, Int}, rows::RowRange; 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, normalizenames::Bool=false) - if rows.start == rows.stop && header==true - throw(XLSXError("Only 1 row specified in `RowRange` with `header=true`.\nThe header row is the same as the data row. Specify at least two rows to read header data with `header=true`.")) - end - first_row = isnothing(first_row) ? rows.start : first_row - stop_in_row_function = isnothing(stop_in_row_function) ? r -> r.row >= rows.stop-first_row+1 : stop_in_row_function - c = openxlsx(source, enable_cache=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}, range::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, normalizenames::Bool=false) if is_valid_row_range(range) range = RowRange(range) diff --git a/src/workbook.jl b/src/workbook.jl index ee7cdef3..dff1677d 100644 --- a/src/workbook.jl +++ b/src/workbook.jl @@ -147,11 +147,11 @@ function getdata(xl::XLSXFile, s::AbstractString) return getdata(xl, SheetColumnRange(s)) elseif is_valid_sheet_row_range(s) return getdata(xl, SheetRowRange(s)) - elseif is_valid_non_contiguous_range(s) + elseif is_valid_non_contiguous_sheetcellrange(s) return getdata(xl, NonContiguousRange(s)) end - throw(XLSXError("`$s` is not a valid definedName or cell/range reference.")) + throw(XLSXError("`$s` is not a valid sheetname, definedName or cell/range reference.")) end function getcell(xl::XLSXFile, ref::SheetCellRef) diff --git a/src/worksheet.jl b/src/worksheet.jl index eecab0ef..018ae9c0 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -61,11 +61,11 @@ end function get_dimension(ws::Worksheet)::Union{Nothing,CellRange} !isnothing(ws.dimension) && return ws.dimension (isnothing(ws.cache) || length(ws.cache.cells) < 1) && return nothing -# @warn "Dimension for worksheet $(ws.name) not found. Calculating from cells in cache." + # @warn "Dimension for worksheet $(ws.name) not found. Calculating from cells in cache." 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_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))) @@ -196,71 +196,6 @@ function getdata(ws::Worksheet, rng::RowRange)::Array{Any,2} end 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) - - 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)) - end - end - - rows = length(columns[1]) - for i in 1:columns_count - length(columns[i]) != rows && throw(XLSXError("Inconsistent state: Each column should have the same number of rows.")) - end - - return hcat(columns...) -end - -function getdata(ws::Worksheet, rng::RowRange) :: Array{Any,2} - rows_count = length(rng) - dim = get_dimension(ws) - - rows = Vector{Vector{Any}}(undef, rows_count) - for i in 1:rows_count - rows[i] = Vector{Any}() - end - - let - top, bottom = row_bounds(rng) - left = dim.start.column_number - right = dim.stop.column_number - - for sheetrow in eachrow(ws) - if sheetrow.row > bottom - break - end - if top > sheetrow.row - continue - else - row_index=sheetrow.row-top+1 - for column in left:right - cell = getcell(sheetrow, column) - push!(rows[row_index], getdata(ws, cell)) - end - end - end - end - - cols = length(rows[1]) - for r in rows - length(r) != cols && throw(XLSXError("Inconsistent state: Each row should have the same number of columns.")) - end - - return permutedims(hcat(rows...)) -end -=# - function getdata(ws::Worksheet, rng::NonContiguousRange)::Vector{Any} do_sheet_names_match(ws, rng) results = Vector{Any}() @@ -321,7 +256,7 @@ function getdata(ws::Worksheet, ref::AbstractString)::Union{Array{Any,2},Any} elseif is_valid_non_contiguous_cellrange(ref) return getdata(ws, NonContiguousRange(ws, ref)) elseif is_valid_non_contiguous_sheetcellrange(ref) - nc=NonContiguousRange(ref) + nc = NonContiguousRange(ref) return do_sheet_names_match(ws, nc) && getdata!(ws, nc) else throw(XLSXError("`$ref` is not a valid cell or range reference.")) @@ -380,7 +315,7 @@ 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"*string(ws.sheetId)*".xml") && get_xlsxfile(ws).files["xl/worksheets/sheet"*string(ws.sheetId)*".xml"] == true + if haskey(get_xlsxfile(ws).files, "xl/worksheets/sheet" * string(ws.sheetId) * ".xml") && get_xlsxfile(ws).files["xl/worksheets/sheet"*string(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] @@ -399,10 +334,11 @@ function getcell(ws::Worksheet, single::CellRef)::AbstractCell return EmptyCell(single) end -getcell(ws::Worksheet, s::SheetCellRef) = getcell(ws, s.cellref) -getcell(ws::Worksheet, s::SheetCellRange) = getcellrange(ws, s.rng) -getcell(ws::Worksheet, s::SheetColumnRange) = getcellrange(ws, s.colrng) -getcell(ws::Worksheet, s::SheetRowRange) = getcellrange(ws, s.rowrng) + +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.rng) getcell(ws::Worksheet, s::ColumnRange) = getcellrange(ws, s.colrng) getcell(ws::Worksheet, s::RowRange) = getcellrange(ws, s.rowrng) @@ -489,36 +425,13 @@ function getcellrange(ws::Worksheet, rng::CellRange)::Array{AbstractCell,2} 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 -getcellrange(ws::Worksheet, s::SheetCellRef) = getcellrange(ws, s.cellref) -getcellrange(ws::Worksheet, s::SheetCellRange) = getcellrange(ws, s.rng) -getcellrange(ws::Worksheet, s::SheetColumnRange) = getcellrange(ws, s.colrng) -getcellrange(ws::Worksheet, s::SheetRowRange) = getcellrange(ws, s.rowrng) +getcellrange(ws::Worksheet, s::SheetCellRef) = do_sheet_names_match(ws, s) && getcellrange(ws, s.cellref) +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) 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] @@ -548,66 +461,6 @@ function getcellrange(ws::Worksheet, rng::RowRange)::Array{AbstractCell,2} end 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 - - let - left, right = column_bounds(rng) - - 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) - end - end - end - - rows = length(columns[1]) - for i in 1:columns_count - length(columns[i]) != rows && throw(XLSXError("Inconsistent state: Each column should have the same number of rows.")) - end - - return hcat(columns...) -end - -function getcellrange(ws::Worksheet, rng::RowRange) :: Array{AbstractCell,2} - dim = get_dimension(ws) - - rows = Vector{Vector{AbstractCell}}() - - let - top, bottom = row_bounds(rng) - left = dim.start.column_number - right = dim.stop.column_number - - for (i, sheetrow) in enumerate(eachrow(ws)) - push!(rows, Vector{AbstractCell}()) - if top <= sheetrow.row && sheetrow.row <= bottom - for column in left:right - cell = getcell(sheetrow, column) - push!(rows[i], cell) - end - end - if sheetrow.row > bottom - break - end - end - end - - cols = length(rows[1]) - for r in rows - length(r) != cols && throw(XLSXError("Inconsistent state: Each row should have the same number of columns.")) - end - - return permutedims(hcat(rows...)) -end -=# function getcellrange(ws::Worksheet, rng::NonContiguousRange)::Vector{AbstractCell} results = Vector{AbstractCell}() for r in rng.rng diff --git a/src/write.jl b/src/write.jl index 5ea1c1ac..2eeabaa9 100644 --- a/src/write.jl +++ b/src/write.jl @@ -2,12 +2,17 @@ """ opentemplate(source::Union{AbstractString, IO}) :: XLSXFile -Read an existing Excel file as a template and return as a writable `XLSXFile` for editing +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`. +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") +julia> xf = opentemplate("myExcelFile.xlsx") ``` """ @@ -493,15 +498,6 @@ 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) From 86f6cb3e55d47673d69ca31647c22e5510f64bcf Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Wed, 16 Apr 2025 14:47:23 +0100 Subject: [PATCH 078/154] Address #155 and #52 --- docs/make.jl | 3 +- docs/src/Formatting.md | 310 ++++++++++++++++++++++++++++++++++++++ docs/src/api.md | 1 + docs/src/tutorial.md | 31 ++-- src/cellformat-helpers.jl | 2 +- src/cellformats.jl | 19 +-- src/read.jl | 93 +++++++++--- src/workbook.jl | 2 +- 8 files changed, 418 insertions(+), 43 deletions(-) create mode 100644 docs/src/Formatting.md diff --git a/docs/make.jl b/docs/make.jl index 5dcb4525..c5a2d834 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -7,9 +7,10 @@ makedocs( pages = [ "Home" => "index.md", "Tutorial" => "tutorial.md", + "Formatting Guide" => "formatting.md", "API Reference" => "api.md", "Migration Guides" => "migration.md", - ], + ], checkdocs=:none, ) diff --git a/docs/src/Formatting.md b/docs/src/Formatting.md new file mode 100644 index 00000000..0da6c361 --- /dev/null +++ b/docs/src/Formatting.md @@ -0,0 +1,310 @@ + +# Formatting Guide + +## 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 format attributes Excel uses. +These are applied to cells, and the functions deal with the relationships between the +individual attributes, 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 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 + +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 and add a double underline and make the font +italic. However, the color, font name and size will all remain unchanged. This +combination of attributes is unique, so a new `fontId` has been created. + +The other set attribute functions behave in similar ways. See [`XLSX.setBorder`](@ref), +[`XLSX.setFill`](@ref), [`XLSX.setFormat`](@ref) and [`XLSX.setAlignment`](@ref). + +## Indexing multiple cells at once + +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:\Users\Tim Gebbels\.julia\artifacts\c0b84c4a80d13f58b3409f4a77d4a11455b5609e\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] = "" # Can't set format attributes on `EmptyCell`s. This simply sets them to `missing` instead. +"" + +julia> XLSX.setFont(s, "A1:CV100"; name="Arial", size=24, color="blue", bold=true) +-1 # Returns -1 on a range because a single `fontId` is unlikely to be possible. + +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. +-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 + +Sometime 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.setBorder(s, "A1:CV100"; allsides = ["style" => "thin", "color" => "black"]) # set every cell individually +-1 + +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 updates the border color in cell A1 to be green and adds red diagonal lines across the cell. +It then applies all the `font` 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). + +It is possible to use each of these 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 +``` + +## 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 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. + +## 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 dtermine 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 a merged cell using `XLSX.jl`. + + ```julia + + julia> XLSX.isMergedCell(f[1], "J8") + true + + julia> f[1]["J8"] = "This cell is merged" + "This cell is merged" + + julia> XLSX.isMergedCell(f[1], "J8") + true + + julia> XLSX.getMergedBaseCell(f[1], "J8") + (baseCell = F5, baseValue = 3.141592653589793) + + julia> f[1]["J8"] + "This cell is merged" + + ``` + + The cell remains merged, and this is how Excel will see it. The assigned cell value won't be + visible in Excel, but it can be referenced in a formula, etc. This is prevented in Excel + itself by the UI (unless some clever VBA indirection is used). There is currently no check + to prevent this in `XLSX.jl`. See [#241](https://github.com/felipenoris/XLSX.jl/issues/241) \ No newline at end of file diff --git a/docs/src/api.md b/docs/src/api.md index 8f189147..4c6a4881 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -23,6 +23,7 @@ XLSX.row_number XLSX.column_number XLSX.eachrow XLSX.readtable +XLSX.readdf XLSX.gettable XLSX.eachtablerow XLSX.writetable diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index f50c135e..38d27cfb 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.readdf`](@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.readdf("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 @@ -258,7 +271,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/cellformat-helpers.jl b/src/cellformat-helpers.jl index fa056924..6b85edf6 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -418,7 +418,7 @@ function process_get_cellname(f::Function, ws::Worksheet, ref_or_rng::AbstractSt elseif is_valid_cellname(ref_or_rng) new_att = f(ws, CellRef(ref_or_rng); kw...) else - throw(XLSXError("Invalid cell reference or range: $ref_or_rng")) + throw(XLSXError("Invalid cell reference: $ref_or_rng")) end return new_att end diff --git a/src/cellformats.jl b/src/cellformats.jl index 3c344abd..ac48b547 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -2033,9 +2033,6 @@ function setColumnWidth(ws::Worksheet, rng::CellRange; width::Union{Nothing,Real throw(XLSXError("Cannot set column widths: `XLSXFile` is not writable.")) end - # 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)) - 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 @@ -2094,6 +2091,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 @@ -2138,9 +2138,6 @@ function getColumnWidth(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real} throw(XLSXError("Cell specified is outside sheet dimension `$d`")) end - # 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)) - 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") @@ -2263,7 +2260,7 @@ function setRowHeight(ws::Worksheet, rng::CellRange; height::Union{Nothing,Real} if first == true return -1 end - return 0 # meaningless return value. Int required to comply with reference decoding structure. + return 0 end """ @@ -2362,9 +2359,6 @@ function getMergedCells(ws::Worksheet)::Union{Vector{CellRange},Nothing} throw(XLSXError("Cannot get merged cells because cache is not enabled.")) end - # 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)) - 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") @@ -2587,9 +2581,6 @@ function mergeCells(ws::Worksheet, cr::CellRange) throw(XLSXError("Cannot get merged cells because cache is not enabled.")) end - # 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)) - 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") @@ -2641,5 +2632,7 @@ function mergeCells(ws::Worksheet, cr::CellRange) 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/read.jl b/src/read.jl index ec301db3..bb52f729 100644 --- a/src/read.jl +++ b/src/read.jl @@ -234,7 +234,7 @@ function open_or_read_xlsx(source::Union{IO, AbstractString}, read_files::Bool, 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) + 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. @@ -569,8 +569,8 @@ end """ readtable( source, - sheet, - [columns]; + [sheet, + [columns]]; [first_row], [column_labels], [header], @@ -584,19 +584,18 @@ end 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. - -Alternatively, use `columns` to specify a row range, like `"2:4"`. -This will select rows `2`, `3` and `4`. +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`. If `first_row` is not given, the algorithm will look for the first -non-empty row in the spreadsheet (if a column range is specified) -or range (if a row range is specified). +non-empty row in the spreadsheet. `header` is a `Bool` indicating if the first row is a header. If `header=true` and `column_labels` is not specified, the column labels @@ -609,7 +608,7 @@ 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 @@ -631,7 +630,7 @@ 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` +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 @@ -647,27 +646,85 @@ 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, normalizenames::Bool=false) +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=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=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::ColumnRange; 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, normalizenames::Bool=false) +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=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=false, 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_row_range(range) - range = RowRange(range) - elseif is_valid_column_range(range) +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 or row range.")) + 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 + +""" + readdf( + source, + [sheet, + [columns]], + sink; + [first_row], + [column_labels], + [header], + [infer_eltypes], + [stop_in_empty_row], + [stop_in_row_function], + [keep_empty_rows], + [normalizenames] + ) -> DataFrame + +Read and parse an Excel worksheet, materializing directly using +the `sink` function (e.g. `DataFrame`). + +Takes the same keyword arguments as [`XLSX.readtable`](@ref) + +# Example + +```julia +julia> using DataFrames, XLSX + +julia> df = XLSX.readdf("myfile.xlsx", DataFrame)) + +julia> df = XLSX.readdf("myfile.xlsx", "mysheet", DataFrame)) + +julia> df = XLSX.readdf("myfile.xlsx", "mysheet", "A:C", DataFrame)) +``` + +See also: [`XLSX.gettable`](@ref). +""" +function readdf(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 readtable(source, sheet, range; kw...) |> sink +end +function readdf(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 readtable(source, sheet; kw...) |> sink +end +function readdf(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 readtable(source; kw...) |> sink +end diff --git a/src/workbook.jl b/src/workbook.jl index dff1677d..18349b34 100644 --- a/src/workbook.jl +++ b/src/workbook.jl @@ -54,7 +54,7 @@ function getsheet(wb::Workbook, sheetname::String) :: Worksheet return ws end end - throw(XLSXError("$(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] From d91fcde113ceb3ab6f704154e0c81ff37ccaa9b1 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Wed, 16 Apr 2025 14:53:45 +0100 Subject: [PATCH 079/154] Filename typo! --- docs/src/Formatting.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/src/Formatting.md b/docs/src/Formatting.md index 0da6c361..dd088db4 100644 --- a/docs/src/Formatting.md +++ b/docs/src/Formatting.md @@ -305,6 +305,7 @@ It is not allowed to create new merged cells that overlap at all with any existi ``` The cell remains merged, and this is how Excel will see it. The assigned cell value won't be - visible in Excel, but it can be referenced in a formula, etc. This is prevented in Excel - itself by the UI (unless some clever VBA indirection is used). There is currently no check - to prevent this in `XLSX.jl`. See [#241](https://github.com/felipenoris/XLSX.jl/issues/241) \ No newline at end of file + visible in Excel, but it can be referenced in a formula, etc. + + This is prevented in Excel itself by the UI (unless some clever VBA indirection is used). + There is currently no check to prevent this in `XLSX.jl`. See [#241](https://github.com/felipenoris/XLSX.jl/issues/241) \ No newline at end of file From 104c8a5a28f27cfd891a00d8ab5b573917dcfb93 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Wed, 16 Apr 2025 15:09:11 +0100 Subject: [PATCH 080/154] Try again! --- docs/src/Formatting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/Formatting.md b/docs/src/Formatting.md index dd088db4..30e3986c 100644 --- a/docs/src/Formatting.md +++ b/docs/src/Formatting.md @@ -8,7 +8,7 @@ refer to the same `style` and therefore have a uniform appearance. A `style` def 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. +definition, etc, and therefore share these formatting characteristics. This hierarchy can be shown like this: `Cell` From ac994e79239b36941949721e57a64c8fe44186cb Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Wed, 16 Apr 2025 15:11:22 +0100 Subject: [PATCH 081/154] Take out recalcitrant file. --- docs/src/Formatting.md | 311 ----------------------------------------- 1 file changed, 311 deletions(-) delete mode 100644 docs/src/Formatting.md diff --git a/docs/src/Formatting.md b/docs/src/Formatting.md deleted file mode 100644 index 30e3986c..00000000 --- a/docs/src/Formatting.md +++ /dev/null @@ -1,311 +0,0 @@ - -# Formatting Guide - -## 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 format attributes Excel uses. -These are applied to cells, and the functions deal with the relationships between the -individual attributes, 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 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 - -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 and add a double underline and make the font -italic. However, the color, font name and size will all remain unchanged. This -combination of attributes is unique, so a new `fontId` has been created. - -The other set attribute functions behave in similar ways. See [`XLSX.setBorder`](@ref), -[`XLSX.setFill`](@ref), [`XLSX.setFormat`](@ref) and [`XLSX.setAlignment`](@ref). - -## Indexing multiple cells at once - -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:\Users\Tim Gebbels\.julia\artifacts\c0b84c4a80d13f58b3409f4a77d4a11455b5609e\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] = "" # Can't set format attributes on `EmptyCell`s. This simply sets them to `missing` instead. -"" - -julia> XLSX.setFont(s, "A1:CV100"; name="Arial", size=24, color="blue", bold=true) --1 # Returns -1 on a range because a single `fontId` is unlikely to be possible. - -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. --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 - -Sometime 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.setBorder(s, "A1:CV100"; allsides = ["style" => "thin", "color" => "black"]) # set every cell individually --1 - -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 updates the border color in cell A1 to be green and adds red diagonal lines across the cell. -It then applies all the `font` 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). - -It is possible to use each of these 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 -``` - -## 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 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. - -## 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 dtermine 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 a merged cell using `XLSX.jl`. - - ```julia - - julia> XLSX.isMergedCell(f[1], "J8") - true - - julia> f[1]["J8"] = "This cell is merged" - "This cell is merged" - - julia> XLSX.isMergedCell(f[1], "J8") - true - - julia> XLSX.getMergedBaseCell(f[1], "J8") - (baseCell = F5, baseValue = 3.141592653589793) - - julia> f[1]["J8"] - "This cell is merged" - - ``` - - The cell remains merged, and this is how Excel will see it. The assigned cell value won't be - visible in Excel, but it can be referenced in a formula, etc. - - This is prevented in Excel itself by the UI (unless some clever VBA indirection is used). - There is currently no check to prevent this in `XLSX.jl`. See [#241](https://github.com/felipenoris/XLSX.jl/issues/241) \ No newline at end of file From f2413b8320a79931235da1ed3e68aae8b193ff16 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Wed, 16 Apr 2025 15:12:28 +0100 Subject: [PATCH 082/154] ... and replace it! --- docs/src/formatting.md | 311 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 docs/src/formatting.md diff --git a/docs/src/formatting.md b/docs/src/formatting.md new file mode 100644 index 00000000..52db0f47 --- /dev/null +++ b/docs/src/formatting.md @@ -0,0 +1,311 @@ + +# Formatting Guide + +## 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 format attributes Excel uses. +These are applied to cells, and the functions deal with the relationships between the +individual attributes, 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 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 + +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 and add a double underline and make the font +italic. However, the color, font name and size will all remain unchanged. This +combination of attributes is unique, so a new `fontId` has been created. + +The other set attribute functions behave in similar ways. See [`XLSX.setBorder`](@ref), +[`XLSX.setFill`](@ref), [`XLSX.setFormat`](@ref) and [`XLSX.setAlignment`](@ref). + +## Indexing multiple cells at once + +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:\Users\Tim Gebbels\.julia\artifacts\c0b84c4a80d13f58b3409f4a77d4a11455b5609e\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] = "" # Can't set format attributes on `EmptyCell`s. This simply sets them to `missing` instead. +"" + +julia> XLSX.setFont(s, "A1:CV100"; name="Arial", size=24, color="blue", bold=true) +-1 # Returns -1 on a range because a single `fontId` is unlikely to be possible. + +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. +-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 + +Sometime 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.setBorder(s, "A1:CV100"; allsides = ["style" => "thin", "color" => "black"]) # set every cell individually +-1 + +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 updates the border color in cell A1 to be green and adds red diagonal lines across the cell. +It then applies all the `font` 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). + +It is possible to use each of these 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 +``` + +## 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 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. + +## 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 dtermine 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 a merged cell using `XLSX.jl`. + + ```julia + + julia> XLSX.isMergedCell(f[1], "J8") + true + + julia> f[1]["J8"] = "This cell is merged" + "This cell is merged" + + julia> XLSX.isMergedCell(f[1], "J8") + true + + julia> XLSX.getMergedBaseCell(f[1], "J8") + (baseCell = F5, baseValue = 3.141592653589793) + + julia> f[1]["J8"] + "This cell is merged" + + ``` + + The cell remains merged, and this is how Excel will see it. The assigned cell value won't be + visible in Excel, but it can be referenced in a formula, etc. + + This is prevented in Excel itself by the UI (unless some clever VBA indirection is used). + There is currently no check to prevent this in `XLSX.jl`. See [#241](https://github.com/felipenoris/XLSX.jl/issues/241) \ No newline at end of file From 0d3c1a697dfd31575474818dcdce42105dcc10fa Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Wed, 16 Apr 2025 15:33:36 +0100 Subject: [PATCH 083/154] Minor typos --- docs/src/formatting.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/src/formatting.md b/docs/src/formatting.md index 52db0f47..d1dfff4b 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -156,7 +156,7 @@ All the format setter functions have `setUniformAttribute` versions, too. See [` It is possible to use each of these 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 +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: @@ -203,7 +203,7 @@ widths in Excel itself. ## 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 dtermine the value of a merged cell range from any +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 @@ -283,6 +283,7 @@ julia> XLSX.getMergedBaseCell(f[1], "J8") 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 a merged cell using `XLSX.jl`. ```julia From 0975511c3f56106655d9675c1cdc4cb48c727361 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Fri, 18 Apr 2025 13:49:43 +0100 Subject: [PATCH 084/154] Add ability to delete a worksheet (#80) --- src/relationship.jl | 42 +++-- src/write.jl | 396 ++++++++++++++++++++++++++++++-------------- 2 files changed, 298 insertions(+), 140 deletions(-) diff --git a/src/relationship.jl b/src/relationship.jl index 9f23c00c..e56e7e50 100644 --- a/src/relationship.jl +++ b/src/relationship.jl @@ -1,5 +1,5 @@ -function Relationship(e::XML.Node) :: 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( @@ -9,7 +9,7 @@ function Relationship(e::XML.Node) :: Relationship ) end -function parse_relationship_target(prefix::String, target::String) :: String +function parse_relationship_target(prefix::String, target::String)::String isempty(prefix) || isempty(target) && throw(XLSXError("Something wrong here!")) if target[1] == '/' sizeof(target) <= 1 && throw(XLSXError("Incomplete target path $target.")) @@ -19,7 +19,7 @@ function parse_relationship_target(prefix::String, target::String) :: String 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) @@ -28,7 +28,7 @@ function get_relationship_target_by_id(prefix::String, wb::Workbook, Id::String) 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) @@ -37,7 +37,7 @@ function get_relationship_target_by_type(prefix::String, wb::Workbook, _type_::S 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 @@ -46,29 +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] 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) + 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] 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) + 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) !is_writable(xf) && throws(XLSXError("XLSXFile instance is not writable.")) - local rId :: String + local rId::String let got_unique_id = false @@ -92,9 +92,25 @@ function add_relationship!(wb::Workbook, target::String, _type::String) :: Strin push!(wb.relationships, new_relationship) # 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, Target=target, Type=_type) push!(xroot, el) return rId end + +# Renews relationships based on Worksheet data +update_relationships(xf::XLSXFile) = update_relationships(get_workbook(xf)) +function update_relationships(wb::Workbook) + xroot = get_workbook_relationship_root(get_xlsxfile(wb)) + filter!(x -> !occursin("worksheet", x["Type"]), XML.children(xroot)) + filter!(x -> !occursin("worksheet", x.Type), wb.relationships) + _type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" + for s in wb.sheets + target = "worksheets/sheet" * s.relationship_id[4:end] * ".xml" + + el = XML.Element("Relationship"; Id=s.relationship_id, Type=_type, Target=target) + push!(xroot, el) + push!(wb.relationships, Relationship(s.relationship_id, _type, target)) + end + return nothing +end \ No newline at end of file diff --git a/src/write.jl b/src/write.jl index 2eeabaa9..fc2eb5e8 100644 --- a/src/write.jl +++ b/src/write.jl @@ -16,9 +16,9 @@ julia> xf = opentemplate("myExcelFile.xlsx") ``` """ -opentemplate(source::Union{AbstractString, IO}) :: XLSXFile = open_or_read_xlsx(source, true, true, true) +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) +@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 @@ -36,12 +36,12 @@ julia> xf = newxlsx() ``` """ -newxlsx(sheetname::AbstractString=""; path::AbstractString=_relocatable_data_path()) :: XLSXFile = open_empty_template(sheetname; path) +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 + 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.")) @@ -61,18 +61,19 @@ Write an Excel file given by `xlsx_file::XLSXFile` to IO or filepath `output_sou 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) !is_writable(xf) && throw(XLSXError("XLSXFile instance is not writable.")) - if !all(values(xf.files)) + 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 isfile(output_source) && throw(XLSXError("Output file $output_source already exists.")) end - update_worksheets_xml!(xf) - update_workbook_xml!(xf) + update_worksheets_xml!(xf) + update_workbook_xml!(xf) + update_relationships(xf) ZipArchives.ZipWriter(output_source) do xlsx # write XML files @@ -101,7 +102,7 @@ function writexlsx(output_source::Union{AbstractString, IO}, xf::XLSXFile; overw 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) XML.nodetype(xdoc) != XML.Document && throw(XLSXError("Expected an XML Document node, got $(XML.nodetype(xdoc)).")) @@ -109,22 +110,25 @@ function set_worksheet_xml_document!(ws::Worksheet, xdoc::XML.Node) filename = get_worksheet_internal_file(ws) !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 +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 @@ -135,12 +139,12 @@ 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 @@ -166,21 +170,21 @@ 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 -# Remove all children with tag givenn by att[2] from a parent XML node with a tag given by att[1]. -function unlink(node::XML.Node, att::Tuple{String, String}) +# 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 @@ -196,17 +200,17 @@ function unlink(node::XML.Node, att::Tuple{String, String}) 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 @@ -229,7 +233,7 @@ function update_worksheets_xml!(xl::XLSXFile) # 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}([ @@ -239,7 +243,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] @@ -251,14 +255,14 @@ 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(parent, ("sheetData", "row")) @@ -289,7 +293,7 @@ function update_worksheets_xml!(xl::XLSXFile) 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 @@ -307,7 +311,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 @@ -330,7 +334,7 @@ function update_worksheets_xml!(xl::XLSXFile) push!(sheetData_node, row_node) end - doc[i][j]=sheetData_node + doc[i][j] = sheetData_node # updates worksheet dimension if get_dimension(sheet) !== nothing @@ -350,74 +354,82 @@ end function abscell(c::CellRef) col, row = split_cellname(c.name) - return "\$"*col*"\$"*string(row) + 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="" + 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 + 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) + else + return dn.isabs ? quoteit(dn.value.sheet) * "!" * mkabs(dn.value) : string(dn.value) end end -function update_workbook_xml!(xl::XLSXFile) # Only the block will need updating. +function update_workbook_xml!(xl::XLSXFile) # Need to update and . wb = get_workbook(xl) - if length(wb.workbook_names)==0 && length(wb.workbook_names)==0 # No-op if no defined names present - return nothing - end - - 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) + #update defined names + if length(wb.workbook_names) > 0 || length(wb.workbook_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 - v= string(v.value) + definedNames = unlink(wbdoc[i][j], ("definedNames", "definedName")) # Remove old defined names 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) + 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 - dn_node = XML.Element("definedName", name=last(k), localSheetId=first(k)-1, XML.Text(v)) - push!(definedNames, dn_node) + wbdoc[i][j] = definedNames # Add the new definedNames block to the workbook's xml file end - wbdoc[i][j] = definedNames # Add the new definedNames block to the workbook's xml file + #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=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 @@ -463,7 +475,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 @@ -511,14 +523,14 @@ 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, 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) @@ -582,14 +594,14 @@ function setdata!(ws::Worksheet, ref::CellRef, val::CellValueType) # use existin 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 retain other formatting. + # 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 -function setdata!(ws::Worksheet, ref::AbstractString, 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) @@ -626,7 +638,7 @@ function setdata!(ws::Worksheet, ref::AbstractString, 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) + 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.")) @@ -729,13 +741,13 @@ setdata!(ws::Worksheet, row::Integer, col::Integer, data::AbstractVector, dim::I 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) @@ -755,7 +767,7 @@ function setdata!(sheet::Worksheet, rows::UnitRange{T}, col::Integer, data::Abst 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) @@ -765,7 +777,7 @@ function setdata!(sheet::Worksheet, ref_or_rng::AbstractString, matrix::Array{T, 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) @@ -775,7 +787,7 @@ 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} +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 @@ -784,7 +796,7 @@ 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 @@ -794,11 +806,11 @@ function target_cell_ref_from_offset(anchor_row::Integer, anchor_col::Integer, o 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} +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 @@ -837,12 +849,12 @@ 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) @@ -850,7 +862,7 @@ function writetable!( 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]) - 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")) + 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 length(data[c]) != row_count && throw(XLSXError("Row count mismatch between column 1 ($row_count rows) and column $c ($(length(data[c])) rows).")) @@ -920,8 +932,6 @@ function rename!(ws::Worksheet, name::AbstractString) nothing end -addsheet!(xl::XLSXFile, name::AbstractString="") :: Worksheet = addsheet!(get_workbook(xl), name) - """ addsheet!(workbook, [name]) :: Worksheet @@ -929,7 +939,8 @@ 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 +addsheet!(xl::XLSXFile, name::AbstractString="")::Worksheet = addsheet!(get_workbook(xl), name) +function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path::String=_relocatable_data_path())::Worksheet xf = get_xlsxfile(wb) !is_writable(xf) && throw(XLSXError("XLSXFile instance is not writable.")) @@ -941,7 +952,7 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path: i = 1 current_sheet_names = sheetnames(wb) while true - name = "Sheet"*string(i) + name = "Sheet" * string(i) if !in(name, current_sheet_names) # found a unique name break @@ -970,7 +981,7 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path: check_valid_sheetname(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) @@ -979,7 +990,7 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path: local xml_filename::String i = 1 while true - xml_filename = "xl/worksheets/sheet"*string(i)*".xml" + xml_filename = "xl/worksheets/sheet" * string(i) * ".xml" if !in(xml_filename, keys(xf.files)) break end @@ -989,7 +1000,7 @@ 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 # @@ -1002,16 +1013,16 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path: # 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) + state = SheetRowStreamIteratorState(reader, nothing, 0, nothing) ws.cache = XLSX.WorksheetCache( - Dict{Int64, Dict{Int64, XLSX.Cell}}(), + Dict{Int64,Dict{Int64,XLSX.Cell}}(), Int64[], - Dict{Int, Union{Float64, Nothing}}(), - Dict{Int64, Int64}(), + Dict{Int,Union{Float64,Nothing}}(), + Dict{Int64,Int64}(), SheetRowStreamIterator(ws), state, false - ) + ) # adds the new sheet to the list of sheets in the workbook push!(wb.sheets, ws) @@ -1020,24 +1031,155 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path: ctype_root = xmlroot(get_xlsxfile(wb), "[Content_Types].xml")[end] 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"*string(sheetId)*".xml" + ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml", + PartName="/xl/worksheets/sheet" * string(sheetId) * ".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 + # 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["sheetId"] = string(sheetId) + # sheet_element["r:id"] = rId + # push!(node, sheet_element) + # break + # end + # end + update_workbook_xml!(xf) + + return ws +end +function renumber_files!(files, rId) + holdem=Vector{Pair{String, Any}}() + for (f, v) in files + if occursin("worksheets/sheet", f) && occursin(r"[0-9]", f) + fnum=parse(Int, f[20:findfirst(c -> c=='.', f)-1]) + if fnum > parse(Int, rId[4:end]) + push!(holdem, "xl/worksheets/sheet" * string(fnum-1) * ".xml" => v) + delete!(files, f) + end end end + for f in holdem + push!(files, f) + end +end - return ws +""" + deletesheet!(wb::Workbook, name::AbstractString) + deletesheet!(xf::XLSXFile, name::AbstractString) + deletesheet!(xf::XLSXFile, idx::Integer) + +Delete the worksheet named `name`. The workbook can be saved back to file using, +for example, `XLSX.writexlsx("myfile.xlsx", xf)`. + +!!! warning "Experimental" + `deletesheet!` is an experimental function. + It removes the most obvious manifestations of a sheet but + there may be archaeological remains with unknown effects! + + Please report any issues. + +""" +deletesheet!(xl::XLSXFile, idx::Integer) = deletesheet!(get_workbook(xl), xl[idx].name) +deletesheet!(xl::XLSXFile, name::AbstractString) = deletesheet!(get_workbook(xl), name) +function deletesheet!(wb::Workbook, name::AbstractString) + !hassheet(wb, name) && throw(XLSXError("Worksheet `$name` not found in workbook.")) + + # 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) + for s in wb.sheets[sId:end] + s.sheetId -= 1 + s.relationship_id = "rId" * string(s.sheetId) + end + deleteat!(wb.relationships, r) + update_relationships(wb) + for s in wb.sheets + println(s.name, " => ", s.relationship_id) + end + for r in wb.relationships + println(r) + end + + # 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 + + xf = get_xlsxfile(wb) + update_workbook_xml!(xf) + + # Files + xml_filename = "xl/worksheets/sheet" * rId[4:end] * ".xml" + if in(xml_filename, keys(xf.files)) + delete!(xf.files, xml_filename) + renumber_files!(xf.files, rId) + end + if in(xml_filename, keys(xf.data)) + delete!(xf.data, xml_filename) + renumber_files!(xf.data, rId) + end + if in(xml_filename, keys(xf.binary_data)) + delete!(xf.binary_data, xml_filename) + renumber_files!(xf.binary_data, rId) + 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 nothing end # @@ -1063,7 +1205,7 @@ 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 isfile(filename) && throw(XLSXError("$filename already exists.")) @@ -1104,7 +1246,7 @@ 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 isfile(filename) && throw(XLSXError("$filename already exists.")) @@ -1132,7 +1274,7 @@ function writetable(filename::Union{AbstractString, IO}; overwrite::Bool=false, 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 isfile(filename) && throw(XLSXError("$filename already exists.")) From 6ec30533f93fc4a686f27860e88f35f2a99f7164 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Fri, 18 Apr 2025 19:08:41 +0100 Subject: [PATCH 085/154] Add tests for `deletesheet()` --- docs/src/api.md | 1 + src/cellformats.jl | 2 +- src/relationship.jl | 18 +------------- src/write.jl | 57 +++++++-------------------------------------- test/runtests.jl | 51 +++++++++++++++++++++++++++++++++++++--- 5 files changed, 60 insertions(+), 69 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index 4c6a4881..870645c4 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -14,6 +14,7 @@ XLSX.hassheet XLSX.Worksheet XLSX.rename! XLSX.addsheet! +XLSX.deletesheet! XLSX.readdata XLSX.getdata XLSX.getcell diff --git a/src/cellformats.jl b/src/cellformats.jl index ac48b547..6d192dc1 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -2525,7 +2525,7 @@ end mergeCells(ws::Worksheet, cr::String) -> 0 mergeCells(xf::XLSXFile, cr::String) -> 0 - mergemCells(ws::Worksheet, row::Int, col::Int) -> 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 diff --git a/src/relationship.jl b/src/relationship.jl index e56e7e50..f65cd360 100644 --- a/src/relationship.jl +++ b/src/relationship.jl @@ -92,25 +92,9 @@ function add_relationship!(wb::Workbook, target::String, _type::String)::String push!(wb.relationships, new_relationship) # adds to XML tree + xroot = get_workbook_relationship_root(xf) el = XML.Element("Relationship"; Id=rId, Target=target, Type=_type) push!(xroot, el) return rId end - -# Renews relationships based on Worksheet data -update_relationships(xf::XLSXFile) = update_relationships(get_workbook(xf)) -function update_relationships(wb::Workbook) - xroot = get_workbook_relationship_root(get_xlsxfile(wb)) - filter!(x -> !occursin("worksheet", x["Type"]), XML.children(xroot)) - filter!(x -> !occursin("worksheet", x.Type), wb.relationships) - _type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" - for s in wb.sheets - target = "worksheets/sheet" * s.relationship_id[4:end] * ".xml" - - el = XML.Element("Relationship"; Id=s.relationship_id, Type=_type, Target=target) - push!(xroot, el) - push!(wb.relationships, Relationship(s.relationship_id, _type, target)) - end - return nothing -end \ No newline at end of file diff --git a/src/write.jl b/src/write.jl index fc2eb5e8..df0b0a8d 100644 --- a/src/write.jl +++ b/src/write.jl @@ -73,7 +73,7 @@ function writexlsx(output_source::Union{AbstractString,IO}, xf::XLSXFile; overwr update_worksheets_xml!(xf) update_workbook_xml!(xf) - update_relationships(xf) +# update_relationships(xf) ZipArchives.ZipWriter(output_source) do xlsx # write XML files @@ -1036,43 +1036,18 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path: ) 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["sheetId"] = string(sheetId) - # sheet_element["r:id"] = rId - # push!(node, sheet_element) - # break - # end - # end update_workbook_xml!(xf) return ws end -function renumber_files!(files, rId) - holdem=Vector{Pair{String, Any}}() - for (f, v) in files - if occursin("worksheets/sheet", f) && occursin(r"[0-9]", f) - fnum=parse(Int, f[20:findfirst(c -> c=='.', f)-1]) - if fnum > parse(Int, rId[4:end]) - push!(holdem, "xl/worksheets/sheet" * string(fnum-1) * ".xml" => v) - delete!(files, f) - end - end - end - for f in holdem - push!(files, f) - end -end """ - deletesheet!(wb::Workbook, name::AbstractString) - deletesheet!(xf::XLSXFile, name::AbstractString) - deletesheet!(xf::XLSXFile, idx::Integer) + deletesheet!(ws::Worksheet) -> ::Nothing + deletesheet!(wb::Workbook, name::AbstractString) -> ::Nothing + deletesheet!(xf::XLSXFile, name::AbstractString) -> ::Nothing + deletesheet!(xf::XLSXFile, idx::Integer) -> ::Nothing -Delete the worksheet named `name`. The workbook can be saved back to file using, +Delete the given worksheet. The workbook can be saved back to file using, for example, `XLSX.writexlsx("myfile.xlsx", xf)`. !!! warning "Experimental" @@ -1083,10 +1058,12 @@ for example, `XLSX.writexlsx("myfile.xlsx", xf)`. Please report any issues. """ +deletesheet!(ws::Worksheet) = deletesheet!(get_workbook(ws), ws.name) deletesheet!(xl::XLSXFile, idx::Integer) = deletesheet!(get_workbook(xl), xl[idx].name) deletesheet!(xl::XLSXFile, name::AbstractString) = deletesheet!(get_workbook(xl), name) function deletesheet!(wb::Workbook, name::AbstractString) - !hassheet(wb, name) && throw(XLSXError("Worksheet `$name` not found in workbook.")) + 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)) @@ -1094,18 +1071,6 @@ function deletesheet!(wb::Workbook, name::AbstractString) rId = wb.sheets[s].relationship_id r = findfirst(y -> occursin("worksheet", y.Type) && y.Id == rId, wb.relationships) deleteat!(wb.sheets, s) - for s in wb.sheets[sId:end] - s.sheetId -= 1 - s.relationship_id = "rId" * string(s.sheetId) - end - deleteat!(wb.relationships, r) - update_relationships(wb) - for s in wb.sheets - println(s.name, " => ", s.relationship_id) - end - for r in wb.relationships - println(r) - end # Defined Names found_wbnames = Vector{String}() @@ -1144,21 +1109,17 @@ function deletesheet!(wb::Workbook, name::AbstractString) end xf = get_xlsxfile(wb) - update_workbook_xml!(xf) # Files xml_filename = "xl/worksheets/sheet" * rId[4:end] * ".xml" if in(xml_filename, keys(xf.files)) delete!(xf.files, xml_filename) - renumber_files!(xf.files, rId) end if in(xml_filename, keys(xf.data)) delete!(xf.data, xml_filename) - renumber_files!(xf.data, rId) end if in(xml_filename, keys(xf.binary_data)) delete!(xf.binary_data, xml_filename) - renumber_files!(xf.binary_data, rId) end # update [Content_Types].xml diff --git a/test/runtests.jl b/test/runtests.jl index 85155132..173974ce 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1203,12 +1203,57 @@ end s2 = XLSX.addsheet!(f, big_sheetname) XLSX.writexlsx(new_filename, f, overwrite=true) + f = XLSX.opentemplate(new_filename) + @test XLSX.sheetnames(f) == ["Sheet1", "new_sheet", big_sheetname] + + @testset "deletesheet!" begin + + XLSX.deletesheet!(f, big_sheetname) + @test XLSX.sheetnames(f) == ["Sheet1", "new_sheet"] + XLSX.writexlsx(new_filename, f, 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) == nothing + @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) - # @test !XLSX.isopen(f) + 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 - f = XLSX.readxlsx(new_filename) - @test XLSX.sheetnames(f) == ["Sheet1", "new_sheet", big_sheetname] rm(new_filename) + end @testset "Edit" begin From c70b5bb9ea7c37f9d5080f4d8e34bee766caf602 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 20 Apr 2025 08:14:20 +0100 Subject: [PATCH 086/154] Revisions to docs --- docs/make.jl | 3 +- docs/src/api.md | 3 + docs/src/formatting.md | 387 +++++++++++++++++++++++++- docs/src/images/formatted-table.png | Bin 0 -> 25434 bytes docs/src/images/unformatted-table.png | Bin 0 -> 17113 bytes docs/src/tutorial.md | 2 +- src/cellformats.jl | 19 +- src/read.jl | 4 +- src/stream.jl | 4 +- src/table.jl | 10 +- src/workbook.jl | 4 +- 11 files changed, 400 insertions(+), 36 deletions(-) create mode 100644 docs/src/images/formatted-table.png create mode 100644 docs/src/images/unformatted-table.png diff --git a/docs/make.jl b/docs/make.jl index c5a2d834..dbe4c6c4 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -14,7 +14,8 @@ makedocs( checkdocs=:none, ) -deploydocs( +#=deploydocs( repo = "github.com/felipenoris/XLSX.jl.git", target = "build", ) +=# \ No newline at end of file diff --git a/docs/src/api.md b/docs/src/api.md index 870645c4..8199ce3c 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -1,6 +1,9 @@ # API Reference +```@index +``` + ```@docs XLSX.XLSXFile XLSX.readxlsx diff --git a/docs/src/formatting.md b/docs/src/formatting.md index d1dfff4b..ce41ad0f 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -19,14 +19,14 @@ This hierarchy can be shown like this: │ │ │ │ `font` `fill` `border` `format` ``` -A family of setter functions is provided to set each of the format attributes Excel uses. -These are applied to cells, and the functions deal with the relationships between the -individual attributes, the overarching `style` and the cell(s) themselves. +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 values, use: +`A5` in the `general` sheet of a workbook to specific `font` values, use: ```julia @@ -56,7 +56,7 @@ julia> s=f["general"] julia> XLSX.setFont(s, "A1"; name="Arial", size=24, color="blue", bold=true) 2 -XLSX.setFont(s, "A5"; name="Arial", size=24, color="blue", bold=true) +julia> XLSX.setFont(s, "A5"; name="Arial", size=24, color="blue", bold=true) 2 ``` @@ -71,9 +71,10 @@ julia> XLSX.setFont(s, "A5"; italic=true, under="double", bold=false) 3 ``` -will over-ride the `bold` setting and add a double underline and make the font -italic. However, the color, font name and size will all remain unchanged. This -combination of attributes is unique, so a new `fontId` has been created. +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. The other set attribute functions behave in similar ways. See [`XLSX.setBorder`](@ref), [`XLSX.setFill`](@ref), [`XLSX.setFormat`](@ref) and [`XLSX.setAlignment`](@ref). @@ -92,7 +93,7 @@ Thus, for example: julia> using XLSX julia> f=XLSX.newxlsx() -XLSXFile("C:\Users\Tim Gebbels\.julia\artifacts\c0b84c4a80d13f58b3409f4a77d4a11455b5609e\blank.xlsx") containing 1 Worksheet +XLSXFile("C:\...\blank.xlsx") containing 1 Worksheet sheetname size range ------------------------------------------------- Sheet1 1x1 A1:A1 @@ -100,19 +101,19 @@ XLSXFile("C:\Users\Tim Gebbels\.julia\artifacts\c0b84c4a80d13f58b3409f4a77d4a114 julia> s=f[1] 1×1 XLSX.Worksheet: ["Sheet1"](A1:A1) -julia> s[1:100, 1:100] = "" # Can't set format attributes on `EmptyCell`s. This simply sets them to `missing` instead. +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 because a single `fontId` is unlikely to be possible. +-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. +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. +julia> XLSX.setAlignment(s, 1:100, 2:2:100; rotation=90) # Rotate text 90° every second column in the first 100 rows. -1 ``` @@ -200,6 +201,68 @@ to do something similar but it is not an exact match to what Excel does. User sp column widths will therefore differ by a small amount from the values you would see setting the same widths in Excel itself. +## 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 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 are static. They apply formatting based on the + current cell values, 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 conditional formats + +As an example, a 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 + +Not implemented yet! + ## Working with Merged Cells Worksheets may contain merged cells. XLSX.jl provides functions to identify the merged cells in a worksheet, @@ -309,4 +372,300 @@ It is not allowed to create new merged cells that overlap at all with any existi visible in Excel, but it can be referenced in a formula, etc. This is prevented in Excel itself by the UI (unless some clever VBA indirection is used). - There is currently no check to prevent this in `XLSX.jl`. See [#241](https://github.com/felipenoris/XLSX.jl/issues/241) \ No newline at end of file + 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: + +![image|320x500](./images/unformatted-table.png) + +We can apply some formatting choices to change the table's appearance: + +![image|320x500](./images/formatted-table.png) + +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/images/formatted-table.png b/docs/src/images/formatted-table.png new file mode 100644 index 0000000000000000000000000000000000000000..fbd0edf346c97a00fc596b13f22d0e0136e3d89a GIT binary patch literal 25434 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 LZX<6#c>aF?)VRiZ literal 0 HcmV?d00001 diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index 38d27cfb..a9351ea5 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -207,7 +207,7 @@ where `myfile.xlsx` is a spreadsheet that doesn't fit into memory. ```julia julia> XLSX.openxlsx("myfile.xlsx", enable_cache=false) do f sheet = f["mysheet"] - for r in XLSX.eachrow(sheet) + for r in eachrow(sheet) # r is a `SheetRow`, values are read using column references rn = XLSX.row_number(r) # `SheetRow` row number v1 = r[1] # will read value at column 1 diff --git a/src/cellformats.jl b/src/cellformats.jl index 6d192dc1..90e0cbfb 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -10,7 +10,6 @@ setFont(sh::Worksheet, row, col; kw...) -> ::Int - Set the font 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 @@ -30,12 +29,12 @@ 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 `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. @@ -43,7 +42,7 @@ The `color` attribute can only be defined as rgb values. So, FF000000 means a fully opaque black color. Alternatively, you can use the name of any named color from Colors.jl -(https://juliagraphics.github.io/Colors.jl/stable/namedcolors/) +([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 @@ -517,8 +516,8 @@ keywords or with `outside` but it can be used together with `diagonal`. 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 and `outside` cannot be -indexed with vectors and cannot be used in conjunction with any other keywords. +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. @@ -539,11 +538,11 @@ Allowed values for `style` are: - `mediumDashDotDot` - `slantDashDot` -The `color` attribute can set by specifying an 8-digit hexadecimal value +The `color` attribute can be set by specifying an 8-digit hexadecimal value in the format "AARRGGBB". The transparency ("AA") is ignored by Excel but is required. Alternatively, you can use the name of any named color from Colors.jl -(https://juliagraphics.github.io/Colors.jl/stable/namedcolors/) +([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 @@ -1096,7 +1095,7 @@ Here is a list of the available `pattern` values (thanks to Copilot!): 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 -(https://juliagraphics.github.io/Colors.jl/stable/namedcolors/) +([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. @@ -1483,7 +1482,7 @@ function setAlignment(sh::Worksheet, cellref::CellRef; !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(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") diff --git a/src/read.jl b/src/read.jl index bb52f729..7dd7ae1e 100644 --- a/src/read.jl +++ b/src/read.jl @@ -100,7 +100,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 @@ -541,9 +541,11 @@ julia> XLSX.readdata("myfile.xlsx", "mysheet!A2:B4") 1 "first" 2 "second" 3 "third" +``` Non-contiguous ranges return vectors. +``` julia> XLSX.readdata("customXml.xlsx", "Mock-up", "Location") # `Location` is a `definedName` for a non-contiguous range 4-element Vector{Any}: "Here" diff --git a/src/stream.jl b/src/stream.jl index f30e0a72..e6348bdd 100644 --- a/src/stream.jl +++ b/src/stream.jl @@ -24,7 +24,7 @@ end * `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. =# @@ -298,7 +298,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) diff --git a/src/table.jl b/src/table.jl index 11c25cc2..17292921 100644 --- a/src/table.jl +++ b/src/table.jl @@ -496,8 +496,8 @@ function check_table_data_dimension(data::Vector) nothing end -#function gettable(itr::TableRowIterator; infer_eltypes::Bool=false, normalizenames::Bool=false) :: DataTable -function gettable(itr::TableRowIterator; infer_eltypes::Bool=false) :: DataTable +#function gettable(itr::TableRowIterator; infer_eltypes::Bool=true, normalizenames::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) @@ -578,7 +578,7 @@ Use `column_labels` as a vector of symbols to specify names for the header of th 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. @@ -611,12 +611,12 @@ julia> df = XLSX.openxlsx("myfile.xlsx") do xf 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, normalizenames::Bool=false) +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, normalizenames::Bool=false) +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/workbook.jl b/src/workbook.jl index 18349b34..851395a1 100644 --- a/src/workbook.jl +++ b/src/workbook.jl @@ -303,8 +303,8 @@ When adding defined name referring to a cell or range to a workbook, `value` mus 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 +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 From d692711af04581da67e84fbe7a64335e0ad666dc Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 20 Apr 2025 22:29:36 +0100 Subject: [PATCH 087/154] Begin to add some dynamic conditional formatting --- src/XLSX.jl | 1 + src/cellformats.jl | 7 +- src/cellref.jl | 37 +++++++ src/conditional-formats.jl | 217 +++++++++++++++++++++++++++++++++++++ 4 files changed, 259 insertions(+), 3 deletions(-) create mode 100644 src/conditional-formats.jl diff --git a/src/XLSX.jl b/src/XLSX.jl index 990f9f70..d29ae627 100644 --- a/src/XLSX.jl +++ b/src/XLSX.jl @@ -30,6 +30,7 @@ include("cell.jl") include("styles.jl") include("cellformats.jl") include("cellformat-helpers.jl") +include("conditional-formats.jl") include("write.jl") end # module XLSX diff --git a/src/cellformats.jl b/src/cellformats.jl index 90e0cbfb..7e425d06 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -2607,11 +2607,12 @@ function mergeCells(ws::Worksheet, cr::CellRange) throw(XLSXError("Unexpected number of mergeCells found: $(length(c)). Expected $(sheetdoc[i][j]["count"]).")) end for child in c - for cell in cr - if cell in CellRange(child["ref"]) +# for cell in cr +# if cell in CellRange(child["ref"]) + if intersects(cr, CellRange(child["ref"])) throw(XLSXError("Merged range (`$cr`) cannot overlap with existing merged range (`"*child["ref"]*"`).")) end - end +# end end end diff --git a/src/cellref.jl b/src/cellref.jl index 32a94077..9ff59184 100644 --- a/src/cellref.jl +++ b/src/cellref.jl @@ -219,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) diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl new file mode 100644 index 00000000..1f35b317 --- /dev/null +++ b/src/conditional-formats.jl @@ -0,0 +1,217 @@ +const colorscales = Dict( # Defines the 12 standard, built-in Excel color scales for conditional formatting. + "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="FFF8696B"), + XML.h.color(rgb="FFFFEB84"), + XML.h.color(rgb="FF63BE7B") + ) + ), + "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="FF63BE7B"), + XML.h.color(rgb="FFFFEB84"), + XML.h.color(rgb="FFF8696B") + ) + ), + "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="FFF8696B"), + XML.h.color(rgb="FFFCFCFF"), + XML.h.color(rgb="FF63BE7B") + ) + ), + "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="FF63BE7B"), + XML.h.color(rgb="FFFCFCFF"), + XML.h.color(rgb="FFF8696B") + ) + ), + "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="FFF8696B"), + XML.h.color(rgb="FFFCFCFF"), + XML.h.color(rgb="FF5A8AC6") + ) + ), + "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="FF5A8AC6"), + XML.h.color(rgb="FFFCFCFF"), + XML.h.color(rgb="FFF8696B") + ) + ), + "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="FFF8696B"), + XML.h.color(rgb="FFFCFCFF") + ) + ), + "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="FFFCFCFF"), + XML.h.color(rgb="FFF8696B") + ) + ), + "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="FFFCFCFF"), + XML.h.color(rgb="FF63BE7B") + ) + ), + "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="FF63BE7B"), + XML.h.color(rgb="FFFCFCFF") + ) + ), + "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="FFFFEF9C"), + XML.h.color(rgb="FF63BE7B") + ) + ), + "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="FF63BE7B"), + XML.h.color(rgb="FFFFEF9C") + ) + ) +) +""" +Get the conditional formats for a worksheet. + +# Arguments +- `ws::Worksheet`: The worksheet to get the conditional formats for. + +Return a vector of pairs: CellRange => Vector{String}, where String is the +type of the conditional format applies. + + +""" +function getConditionalFormats(ws::Worksheet)::Vector{Pair{CellRange,Vector{String}}} + sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet" * string(ws.sheetId) * ".xml") # find all the blocks in the worksheet's xml file + allcfnodes = find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":worksheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":conditionalFormatting", sheetdoc) + allcfs = Vector{Pair{CellRange,Vector{String}}}() + for (i, cf) in enumerate(allcfnodes) + cf_types = Vector{String}() + for child in XML.children(cf) + if XML.tag(child) == "cfRule" + push!(cf_types, child["type"]) + # if any(XML.tag(c) == "extLst" for c in XML.children(child)) + # println(" extras: ", true) + # end + end + end + push!(allcfs, CellRange(cf["sqref"]) => cf_types) + end + return allcfs +end + +""" + addConditionalFormat!(ws::Worksheet, rng::CellRange; kw...) -> nothing + +Add a new conditional format to a worksheet. + +Keyword argumenst `colorScale`, `dataBar`, `iconSet`, and `formula` are mutually exclusive. + +Valid values for `colorScale` are: + +- `"redyellowgreen"`: Red, Yellow, Green color scale. +- `"greenyellowred"`: Green, Yellow, Red color scale. +- `"redwhitegreen"` : Red, White, Green color scale. +- `"greenwhitered"` : Green, White, Red color scale. +- `"redwhiteblue"` : Red, White, Blue color scale. +- `"bluewhitered"` : Blue, White, Red color scale. +- `"redwhite"` : Red, White color scale. +- `"whitered"` : White, Red color scale. +- `"whitegreen"` : White, Green color scale. +- `"greenwhite"` : Green, White color scale. +- `"yellowgreen"` : Yellow, Green color scale. +- `"greenyellow"` : Green, Yellow color scale. + +These are the 12 built-in color scales in Excel. + +""" +function addConditionalFormat!(ws::Worksheet, rng::CellRange; + colorScale::Union{Nothing,AbstractString}=nothing, + dataBar::Union{Nothing,AbstractString}=nothing, + iconSet::Union{Nothing,AbstractString}=nothing, + formula::Union{Nothing,AbstractString}=nothing, + )::Nothing + + if !isnothing(colorScale) && !isnothing(dataBar) && !isnothing(iconSet) && !isnothing(formula) + throw(XLSXError("Only one of colorScale, dataBar, iconSet, or formula can be specified.")) + end + + if isnothing(colorScale) && isnothing(dataBar) && isnothing(iconSet) && isnothing(formula) + throw(XLSXError("At least one of colorScale, dataBar, iconSet, or formula must be specified.")) + end + + !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) + + old_cf = getConditionalFormats(ws) + for cf in old_cf + if intersects(cf.first, rng) + throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)`.")) + end + end + + if !isnothing(colorScale) + if !haskey(colorscales, colorScale) + throw(XLSXError("Invalid color scale: $colorScale. Valid options are: $(keys(colorscales)).")) + end + new_cf = XML.Element("conditionalFormatting"; sqref=rng) + push!(new_cf, colorscales[colorScale]) + end + + # Insert the new conditional formatting into 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 + + update_worksheets_xml!(get_xlsxfile(ws)) + + return nothing +end \ No newline at end of file From 082a8286674ef679b92eb037d6b5b7e939eb3d4f Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 21 Apr 2025 18:35:02 +0100 Subject: [PATCH 088/154] Add some docs for `colorScale` conditional formats --- docs/src/formatting.md | 85 +++++++++++++++++-- docs/src/images/Written-to-merged-cell.png | Bin 0 -> 9575 bytes docs/src/images/colorScales.png | Bin 0 -> 20191 bytes docs/src/images/custom-colorscale.png | Bin 0 -> 5172 bytes src/cellformats.jl | 2 +- src/cellref.jl | 14 ++-- src/conditional-formats.jl | 93 +++++++++++++++------ 7 files changed, 153 insertions(+), 41 deletions(-) create mode 100644 docs/src/images/Written-to-merged-cell.png create mode 100644 docs/src/images/colorScales.png create mode 100644 docs/src/images/custom-colorscale.png diff --git a/docs/src/formatting.md b/docs/src/formatting.md index ce41ad0f..4c119c05 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -76,6 +76,9 @@ 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). @@ -212,10 +215,13 @@ but not otherwise. Such conditional formatting is generally straightforward to a 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 are static. They apply formatting based on the + The examples of conditional formatting given here are mainly static. They apply formatting based on the current cell values, 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. + unchanged. + + Some dynamic conditional formatting is possible, using Excel native functions, but the range of + functions is currently more limited than Excel itself can provide. ### Static conditional formats @@ -261,7 +267,68 @@ blankmissing(sheet, XLSX.CellRange("B3:L6")) ### Dynamic conditional formats -Not implemented yet! +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 updated. + +`XLSX.addConditionalFormat(sheet, CellRange, "formatting_type"; kwargs...)` + +Each of the available `formatting_type`s is described in the following sections. + +#### 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. + +![image|320x500](./images/colorScales.png) + +In XLSX.jl, the twelve built-in scales are named by their start/mid/end colors as follows +(layout follows image) + +| | | | | +|:----------------:|:----------------:|:---------------:|:---------------:| +| greenyellowred | redyellowgreen | greenwhitered | redwhitegreen | +| bluewhitered | redwhiteblue | whitered | redwhite | +| greenwhite | whitegreen | greenyellow | yellowgreen | + +The default colorscale is `greenyellowred`. To use a different built-in color scale, +specify the name using the keyword `colorScale`, thus: + +```julia +julia> XLSX.addConditionalFormat(f["Sheet1"], "A1:F12", "colorScale") # Defaults to the `greenyellowred` built-in scale. +0 + +julia> XLSX.addConditionalFormat(f["Sheet1"], "A13:C18", "colorScale"; colorScale="whitered") +0 + +julia> XLSX.addConditionalFormat(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/). + +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. +Thus, you can apply a custom 3-color scale using, for example: + +```julia +julia> XLSX.addConditionalFormat(f["Sheet1"], "A13:F18", "colorScale"; + min_type="num", + min_val="2", + min_col="tomato", + mid_type="num", + mid_val="4", + mid_col="lawngreen", + max_type="num", + max_val="6", + max_col="cadetblue" + ) +0 +``` +![image|320x500](./images/custom-colorscale.png) ## Working with Merged Cells @@ -368,11 +435,15 @@ It is not allowed to create new merged cells that overlap at all with any existi ``` - The cell remains merged, and this is how Excel will see it. The assigned cell value won't be - visible in Excel, but it can be referenced in a formula, etc. + The cell remains merged, and this is how Excel will display it. The assigned cell value + won't be visible in Excel, but it can be referenced in a formula as shown here, where + cell L8 references cell J8 in its formula ("=J8"): + + ![image|320x500](./images/Written-to-merged-cell.png) - This is prevented in Excel itself by the UI (unless some clever VBA indirection is used). - There is currently no check to prevent this in `XLSX.jl`. See [#241](https://github.com/felipenoris/XLSX.jl/issues/241) + 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 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 0000000000000000000000000000000000000000..0d58103f239914762538469656a3ce4505e111b8 GIT binary patch literal 9575 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 z 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 Ug~3+fP&&x3_Re-pj9=1!0dTu|9{>OV literal 0 HcmV?d00001 diff --git a/docs/src/images/custom-colorscale.png b/docs/src/images/custom-colorscale.png new file mode 100644 index 0000000000000000000000000000000000000000..144671810fd324e77b4a82eec2ebde475d1a1072 GIT binary patch literal 5172 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 row_number(rng2.start) && column_number(rng1.stop) <= column_number(rng2.stop) && column_number(rng1.stop) >= column_number(rng2.start) - println("offset 1") +# 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") +# 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") +# 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") +# println("cruciform 2") return true end if in(rng1.start, rng2) || in(rng1.stop, rng2) - println("inside corner 1") +# println("inside corner 1") return true end if in(rng2.start, rng1) || in(rng2.stop, rng1) - println("inside corner 2") +# println("inside corner 2") return true end if issubset(rng1, rng2) || issubset(rng2, rng1) - println("subset") +# println("subset") return true end return false diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index 1f35b317..de9ff41e 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -1,5 +1,5 @@ const colorscales = Dict( # Defines the 12 standard, built-in Excel color scales for conditional formatting. - "redyellowgreen" => XML.h.cfRule(type="colorScale", priority="1", + "greenyellowred" => XML.h.cfRule(type="colorScale", priority="1", XML.h.colorScale( XML.h.cfvo(type="min"), XML.h.cfvo(type="percentile", val="50"), @@ -9,7 +9,7 @@ const colorscales = Dict( # Defines the 12 standard, built-in Excel color sca XML.h.color(rgb="FF63BE7B") ) ), - "greenyellowred" => XML.h.cfRule(type="colorScale", priority="1", + "redyellowgreengreenyellowred" => XML.h.cfRule(type="colorScale", priority="1", XML.h.colorScale( XML.h.cfvo(type="min"), XML.h.cfvo(type="percentile", val="50"), @@ -19,7 +19,7 @@ const colorscales = Dict( # Defines the 12 standard, built-in Excel color sca XML.h.color(rgb="FFF8696B") ) ), - "redwhitegreen" => XML.h.cfRule(type="colorScale", priority="1", + "greenwhitered" => XML.h.cfRule(type="colorScale", priority="1", XML.h.colorScale( XML.h.cfvo(type="min"), XML.h.cfvo(type="percentile", val="50"), @@ -29,7 +29,7 @@ const colorscales = Dict( # Defines the 12 standard, built-in Excel color sca XML.h.color(rgb="FF63BE7B") ) ), - "greenwhitered" => XML.h.cfRule(type="colorScale", priority="1", + "redwhitegreen" => XML.h.cfRule(type="colorScale", priority="1", XML.h.colorScale( XML.h.cfvo(type="min"), XML.h.cfvo(type="percentile", val="50"), @@ -39,7 +39,7 @@ const colorscales = Dict( # Defines the 12 standard, built-in Excel color sca XML.h.color(rgb="FFF8696B") ) ), - "redwhiteblue" => XML.h.cfRule(type="colorScale", priority="1", + "bluewhitered" => XML.h.cfRule(type="colorScale", priority="1", XML.h.colorScale( XML.h.cfvo(type="min"), XML.h.cfvo(type="percentile", val="50"), @@ -49,7 +49,7 @@ const colorscales = Dict( # Defines the 12 standard, built-in Excel color sca XML.h.color(rgb="FF5A8AC6") ) ), - "bluewhitered" => XML.h.cfRule(type="colorScale", priority="1", + "redwhiteblue" => XML.h.cfRule(type="colorScale", priority="1", XML.h.colorScale( XML.h.cfvo(type="min"), XML.h.cfvo(type="percentile", val="50"), @@ -59,7 +59,7 @@ const colorscales = Dict( # Defines the 12 standard, built-in Excel color sca XML.h.color(rgb="FFF8696B") ) ), - "redwhite" => XML.h.cfRule(type="colorScale", priority="1", + "whitered" => XML.h.cfRule(type="colorScale", priority="1", XML.h.colorScale( XML.h.cfvo(type="min"), XML.h.cfvo(type="max"), @@ -67,7 +67,7 @@ const colorscales = Dict( # Defines the 12 standard, built-in Excel color sca XML.h.color(rgb="FFFCFCFF") ) ), - "whitered" => XML.h.cfRule(type="colorScale", priority="1", + "redwhite" => XML.h.cfRule(type="colorScale", priority="1", XML.h.colorScale( XML.h.cfvo(type="min"), XML.h.cfvo(type="max"), @@ -75,7 +75,7 @@ const colorscales = Dict( # Defines the 12 standard, built-in Excel color sca XML.h.color(rgb="FFF8696B") ) ), - "whitegreen" => XML.h.cfRule(type="colorScale", priority="1", + "greenwhite" => XML.h.cfRule(type="colorScale", priority="1", XML.h.colorScale( XML.h.cfvo(type="min"), XML.h.cfvo(type="max"), @@ -83,7 +83,7 @@ const colorscales = Dict( # Defines the 12 standard, built-in Excel color sca XML.h.color(rgb="FF63BE7B") ) ), - "greenwhite" => XML.h.cfRule(type="colorScale", priority="1", + "whitegreen" => XML.h.cfRule(type="colorScale", priority="1", XML.h.colorScale( XML.h.cfvo(type="min"), XML.h.cfvo(type="max"), @@ -91,7 +91,7 @@ const colorscales = Dict( # Defines the 12 standard, built-in Excel color sca XML.h.color(rgb="FFFCFCFF") ) ), - "yellowgreen" => XML.h.cfRule(type="colorScale", priority="1", + "greenyellow" => XML.h.cfRule(type="colorScale", priority="1", XML.h.colorScale( XML.h.cfvo(type="min"), XML.h.cfvo(type="max"), @@ -99,7 +99,7 @@ const colorscales = Dict( # Defines the 12 standard, built-in Excel color sca XML.h.color(rgb="FF63BE7B") ) ), - "greenyellow" => XML.h.cfRule(type="colorScale", priority="1", + "yellowgreen" => XML.h.cfRule(type="colorScale", priority="1", XML.h.colorScale( XML.h.cfvo(type="min"), XML.h.cfvo(type="max"), @@ -163,20 +163,48 @@ Valid values for `colorScale` are: These are the 12 built-in color scales in Excel. """ -function addConditionalFormat!(ws::Worksheet, rng::CellRange; - colorScale::Union{Nothing,AbstractString}=nothing, - dataBar::Union{Nothing,AbstractString}=nothing, - iconSet::Union{Nothing,AbstractString}=nothing, - formula::Union{Nothing,AbstractString}=nothing, - )::Nothing - - if !isnothing(colorScale) && !isnothing(dataBar) && !isnothing(iconSet) && !isnothing(formula) - throw(XLSXError("Only one of colorScale, dataBar, iconSet, or formula can be specified.")) +function addConditionalFormat(ws::Worksheet, ref_or_rng::AbstractString, type::AbstractString; kw...) + if type=="colorScale" + process_ranges(addCfColorScale, ws, ref_or_rng; kw...)::Int + elseif type=="dataBar" + throw(XLSXError("Data bars are not yet implemented.")) + elseif type=="iconSet" + throw(XLSXError("Icon sets are not yet implemented.")) + elseif type=="formula" + throw(XLSXError("Formulas are not yet implemented.")) + else + throw(XLSXError("Invalid conditional format type: $type. Valid options are: colorScale, dataBar, iconSet, formula.")) end +end +addCfColorScale(ws::Worksheet, ref::SheetCellRef; kw...) = do_sheet_names_match(ws, ref) && addCfColorScale(ws, ref.cellref; kw...) +addCfColorScale(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && addCfColorScale(ws, rng.rng; kw...) +addCfColorScale(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && addCfColorScale(ws, rng.colrng; kw...) +addCfColorScale(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && addCfColorScale(ws, rng.rowrng; kw...) +addCfColorScale(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(addCfColorScale, ws, rng; kw...) +addCfColorScale(ws::Worksheet, rng::ColumnRange; kw...) = process_colranges(addCfColorScale, ws, rng; kw...) +addCfColorScale(xl::XLSXFile, sheetcell::AbstractString)::Int = process_sheetcell(addCfColorScale, xl, sheetcell) +addCfColorScale(ws::Worksheet, ref_or_rng::AbstractString)::Int = process_ranges(addCfColorScale, ws, ref_or_rng) +addCfColorScale(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) = process_colon(addCfColorScale, ws, row, nothing) +addCfColorScale(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}) = process_colon(addCfColorScale, ws, nothing, col) +addCfColorScale(ws::Worksheet, ::Colon, ::Colon) = process_colon(addCfColorScale, ws, nothing, nothing) +addCfColorScale(ws::Worksheet, ::Colon) = process_colon(addCfColorScale, ws, nothing, nothing) +addCfColorScale(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}) = addCfColorScale(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col)))) +function addCfColorScale(ws::Worksheet, rng::CellRange; + 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}="percentile", + mid_val::Union{Nothing,String}="50", + mid_col::Union{Nothing,String}="FFFFEB84", + max_type::Union{Nothing,String}="max", + max_val::Union{Nothing,String}=nothing, + max_col::Union{Nothing,String}="FF63BE7B", + )::Int - if isnothing(colorScale) && isnothing(dataBar) && isnothing(iconSet) && isnothing(formula) - throw(XLSXError("At least one of colorScale, dataBar, iconSet, or formula must be specified.")) - end +# if isnothing(colorScale) && isnothing(dataBar) && isnothing(iconSet) && isnothing(formula) +# throw(XLSXError("At least one of colorScale, dataBar, iconSet, or formula must be specified.")) +# end !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) @@ -187,7 +215,20 @@ function addConditionalFormat!(ws::Worksheet, rng::CellRange; end end - if !isnothing(colorScale) + new_cf = XML.Element("conditionalFormatting"; sqref=rng) + if isnothing(colorScale) + push!(new_cf, XML.h.cfRule(type="colorScale", priority="1", + XML.h.colorScale( + isnothing(min_val) ? XML.h.cfvo(type=min_type) : XML.h.cfvo(type=min_type, val=min_val), + isnothing(mid_type) ? nothing : 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) ? nothing : XML.h.color(rgb=get_color(mid_col)), + XML.h.color(rgb=get_color(max_col)) + ) + ) + ) + else if !haskey(colorscales, colorScale) throw(XLSXError("Invalid color scale: $colorScale. Valid options are: $(keys(colorscales)).")) end @@ -213,5 +254,5 @@ function addConditionalFormat!(ws::Worksheet, rng::CellRange; update_worksheets_xml!(get_xlsxfile(ws)) - return nothing + return 0 end \ No newline at end of file From 57344d95ac0f25997799182d13ee2d5f4fb122f5 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 22 Apr 2025 23:04:33 +0100 Subject: [PATCH 089/154] Adding tests to improve code coverage --- data/general.xlsx | Bin 20544 -> 20869 bytes docs/src/formatting.md | 4 +- docs/src/images/custom-colorscale.png | Bin 5172 -> 7702 bytes src/cellformat-helpers.jl | 6 +- src/cellref.jl | 4 +- src/conditional-formats.jl | 2 +- src/read.jl | 6 +- test/runtests.jl | 146 ++++++++++++++++++++++++++ 8 files changed, 157 insertions(+), 11 deletions(-) diff --git a/data/general.xlsx b/data/general.xlsx index 9ec8612e058351c817dd3ea1cc2d22ef8715cf1f..96b6a300107f1148dfbadc7cceac66de6572379a 100644 GIT binary patch delta 4588 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 L4gm1z|D*IjN?NoF diff --git a/docs/src/formatting.md b/docs/src/formatting.md index 4c119c05..1f87ad4b 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -320,10 +320,10 @@ julia> XLSX.addConditionalFormat(f["Sheet1"], "A13:F18", "colorScale"; min_val="2", min_col="tomato", mid_type="num", - mid_val="4", + mid_val="6", mid_col="lawngreen", max_type="num", - max_val="6", + max_val="10", max_col="cadetblue" ) 0 diff --git a/docs/src/images/custom-colorscale.png b/docs/src/images/custom-colorscale.png index 144671810fd324e77b4a82eec2ebde475d1a1072..e8fb20429e588ee34f550d54a9a31b2193ef9d49 100644 GIT binary patch literal 7702 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 using DataFrames, XLSX -julia> df = XLSX.readdf("myfile.xlsx", DataFrame)) +julia> df = XLSX.readdf("myfile.xlsx", DataFrame) -julia> df = XLSX.readdf("myfile.xlsx", "mysheet", DataFrame)) +julia> df = XLSX.readdf("myfile.xlsx", "mysheet", DataFrame) -julia> df = XLSX.readdf("myfile.xlsx", "mysheet", "A:C", DataFrame)) +julia> df = XLSX.readdf("myfile.xlsx", "mysheet", "A:C", DataFrame) ``` See also: [`XLSX.gettable`](@ref). diff --git a/test/runtests.jl b/test/runtests.jl index 173974ce..c3c7ab31 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -203,6 +203,10 @@ end @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" @test XLSX.column_name(cn) == "A" @@ -490,6 +494,16 @@ end @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") @@ -1094,6 +1108,33 @@ end check_test_data(data, test_data) end + @testset "Read DataFrame" begin + + df = XLSX.readdf(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.readdf(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.readdf(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) + end + @testset "normalizenames" begin # Issue #260 data = Vector{Any}() @@ -1685,6 +1726,34 @@ end @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"] @@ -2031,6 +2100,47 @@ end isfile("output.xlsx") && rm("output.xlsx") + 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")) + + 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 + end @testset "setFormat" begin @@ -2102,6 +2212,42 @@ end 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,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 + + end + @testset "Width and height" begin f = XLSX.open_empty_template() From 01e937952b6a639caa92006101e39cb3579c743f Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Wed, 23 Apr 2025 19:46:56 +0100 Subject: [PATCH 090/154] A bunch more tests for coverage. --- docs/make.jl | 3 +- src/cellformats.jl | 14 ++--- src/cellref.jl | 2 +- src/worksheet.jl | 31 +++++++++-- test/runtests.jl | 127 +++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 158 insertions(+), 19 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index dbe4c6c4..c5a2d834 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -14,8 +14,7 @@ makedocs( checkdocs=:none, ) -#=deploydocs( +deploydocs( repo = "github.com/felipenoris/XLSX.jl.git", target = "build", ) -=# \ No newline at end of file diff --git a/src/cellformats.jl b/src/cellformats.jl index e8985073..f23d4772 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -497,13 +497,13 @@ 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]` -- `[outside::Vector{Pair{String,String} = nothing]` +- `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 diff --git a/src/cellref.jl b/src/cellref.jl index a4842656..a24c21bc 100644 --- a/src/cellref.jl +++ b/src/cellref.jl @@ -460,7 +460,7 @@ 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.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) diff --git a/src/worksheet.jl b/src/worksheet.jl index 018ae9c0..494aff97 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -145,6 +145,24 @@ function getdata(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer} getdata(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col)))) end end +function getdata(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, ::Colon) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + col=dim.start.column_number:dim.stop.column_number + end + return getdata(ws, row, col) +end +function getdata(ws::Worksheet, ::Colon, col::Union{Vector{Int},StepRange{<:Integer}}) + dim = get_dimension(ws) + if dim === nothing + throw(XLSXError("No worksheet dimension found")) + else + row=dim.start.row_number:dim.stop.row_number + end + return getdata(ws, row, col) +end function getdata(ws::Worksheet, rng::CellRange)::Array{Any,2} result = Array{Any,2}(undef, size(rng)) @@ -217,7 +235,7 @@ getdata(ws::Worksheet, s::SheetCellRange) = do_sheet_names_match(ws, s) && getda 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)::Union{Array{Any,2},Any} +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) @@ -257,7 +275,7 @@ function getdata(ws::Worksheet, ref::AbstractString)::Union{Array{Any,2},Any} 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) + return do_sheet_names_match(ws, nc) && getdata(ws, nc) else throw(XLSXError("`$ref` is not a valid cell or range reference.")) end @@ -271,6 +289,9 @@ function getdata(ws::Worksheet) end end +#Base.getindex(f::Function, ws::Worksheet, r) = f(ws, r) +#Base.getindex(f::Function, ws::Worksheet, r, c) = f(ws, r, c) +#Base.getindex(f::Function, ws::Worksheet, ::Colon) = f::Function, (ws) Base.getindex(ws::Worksheet, r) = getdata(ws, r) Base.getindex(ws::Worksheet, r, c) = getdata(ws, r, c) Base.getindex(ws::Worksheet, ::Colon) = getdata(ws) @@ -344,9 +365,9 @@ 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}}) = [getcell(ws, a, b) for a in row, b in col] -getcell(ws::Worksheet, row::Union{Vector{Int},StepRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}) = [getcell(ws, a, b) for a in row, b in col] -getcell(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] +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) diff --git a/test/runtests.jl b/test/runtests.jl index c3c7ab31..db85a6d8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -402,13 +402,108 @@ end s["A1:A3"] = "Hello world" s[2, 1:3] = 42 s[[1,3], 2:3] = true - @test XLSX.getdata(s, 1:3, 1:3) == Any["Hello world" true true; 42 42 42; "Hello world" true true] + @test s[1:3, [1, 2, 3]] == Any["Hello world" true true; 42 42 42; "Hello world" true true] s[2, :] = 44 - @test XLSX.getdata(s, 1:3, 1:3) == Any["Hello world" true true; 44 44 44; "Hello world" true true] + @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 XLSX.getdata(s, 1:3, 1:3) == Any[0 0 0; 0 0 0; 0 0 0] + @test s[:, :] == Any[0 0 0; 0 0 0; 0 0 0] s[:] = 1 - @test XLSX.getdata(s, 1:3, 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: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"] + + 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", "")] end @testset "Time and DateTime" begin @@ -517,6 +612,25 @@ end @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 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") + end @testset "Book1.xlsx" begin @@ -2346,6 +2460,7 @@ end @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") # Skip empty cells silently in ranges @test XLSX.setFont(s, 2:3, 1:3; color="grey42") == -1 @@ -2379,6 +2494,7 @@ end @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 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) @@ -2403,6 +2519,7 @@ end @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 XLSX.setFill(s, [2,3], 1:3; pattern="lightVertical", fgColor="Red", bgColor="blue") == -1 @test_throws XLSX.XLSXError XLSX.getFill(s, 2, 1) @@ -2428,6 +2545,7 @@ end @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 XLSX.setAlignment(s, [2,3], 1:3; horizontal="right", vertical="justify", wrapText=true) == -1 @test_throws XLSX.XLSXError XLSX.getAlignment(s, 2, 1) @@ -2453,6 +2571,7 @@ end @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 XLSX.setFormat(s, [2,3], 1:3; format="Percentage") == -1 @test_throws XLSX.XLSXError XLSX.getFormat(s, 2, 1) From eddb247604d72f81ecb475338990c013d9cb51e0 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Thu, 24 Apr 2025 18:15:52 +0100 Subject: [PATCH 091/154] Continue adding tests for coverage... --- docs/src/formatting.md | 4 +- src/cellformats.jl | 2 + src/conditional-formats.jl | 170 +++++++++++++++------ test/runtests.jl | 293 ++++++++++++++++++++++++++++++++++++- 4 files changed, 418 insertions(+), 51 deletions(-) diff --git a/docs/src/formatting.md b/docs/src/formatting.md index 1f87ad4b..73c910ea 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -292,7 +292,7 @@ In XLSX.jl, the twelve built-in scales are named by their start/mid/end colors a | bluewhitered | redwhiteblue | whitered | redwhite | | greenwhite | whitegreen | greenyellow | yellowgreen | -The default colorscale is `greenyellowred`. To use a different built-in color scale, +The default colorscale is `greenyellow`. To use a different built-in color scale, specify the name using the keyword `colorScale`, thus: ```julia @@ -315,7 +315,7 @@ a `percentile` or as a `min` or `max`. For the first three options, a value must Thus, you can apply a custom 3-color scale using, for example: ```julia -julia> XLSX.addConditionalFormat(f["Sheet1"], "A13:F18", "colorScale"; +julia> XLSX.addConditionalFormat(f["Sheet1"], "A13:F18", :colorScale; min_type="num", min_val="2", min_col="tomato", diff --git a/src/cellformats.jl b/src/cellformats.jl index f23d4772..52f83655 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -903,6 +903,8 @@ function setOutsideBorder(ws::Worksheet, rng::CellRange; 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) diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index 55b283ec..86ea8ea6 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -120,7 +120,7 @@ type of the conditional format applies. """ function getConditionalFormats(ws::Worksheet)::Vector{Pair{CellRange,Vector{String}}} - sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet" * string(ws.sheetId) * ".xml") # find all the blocks in the worksheet's xml file + sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet" * string(ws.sheetId) * ".xml") # find all the blocks in the worksheet's xml file allcfnodes = find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":worksheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":conditionalFormatting", sheetdoc) allcfs = Vector{Pair{CellRange,Vector{String}}}() for (i, cf) in enumerate(allcfnodes) @@ -139,11 +139,14 @@ function getConditionalFormats(ws::Worksheet)::Vector{Pair{CellRange,Vector{Stri end """ - addConditionalFormat!(ws::Worksheet, rng::CellRange; kw...) -> nothing + addConditionalFormat!(ws::Worksheet, rng::CellRange, type::Symbol; kw...) -> nothing Add a new conditional format to a worksheet. -Keyword arguments `colorScale`, `dataBar`, `iconSet`, and `formula` are mutually exclusive. +Valid optins for `type` are `:colorScale`, `:dataBar`, `:iconSet`, or `:formula` and these +determine which type of conditional formatting is being defined. + +Keyword options differ according to the `type` specified Valid values for `colorScale` are: @@ -163,50 +166,121 @@ Valid values for `colorScale` are: These are the 12 built-in color scales in Excel. """ -function addConditionalFormat(ws::Worksheet, ref_or_rng::AbstractString, type::AbstractString; kw...) - if type=="colorScale" - process_ranges(addCfColorScale, ws, ref_or_rng; kw...)::Int - elseif type=="dataBar" - throw(XLSXError("Data bars are not yet implemented.")) - elseif type=="iconSet" - throw(XLSXError("Icon sets are not yet implemented.")) - elseif type=="formula" - throw(XLSXError("Formulas are not yet implemented.")) +function setConditionalFormat(xf::XLSXFile, ref_or_rng, type::Symbol; kw...) + if type == :colorScale + process_sheetcell(setCfColorScale, xf, ref_or_rng; kw...) +# elseif type == :dataBar +# throw(XLSXError("Data bars are not yet implemented.")) +# elseif type == :iconSet +# throw(XLSXError("Icon sets are not yet implemented.")) +# elseif type == :formula +# throw(XLSXError("Formulas are not yet implemented.")) + else + throw(XLSXError("Invalid conditional format type: $type. Valid options are: colorScale, dataBar, iconSet, formula.")) + end +end +function setConditionalFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon, type::Symbol; kw...) + if type == :colorScale + process_colon(setCfColorScale, ws, row, nothing; kw...) +# elseif type == :dataBar +# throw(XLSXError("Data bars are not yet implemented.")) +# elseif type == :iconSet +# throw(XLSXError("Icon sets are not yet implemented.")) +# elseif type == :formula +# throw(XLSXError("Formulas are not yet implemented.")) + else + throw(XLSXError("Invalid conditional format type: $type. Valid options are: colorScale, dataBar, iconSet, formula.")) + end +end +function setConditionalFormat(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}, type::Symbol; kw...) + if type == :colorScale + process_colon(setCfColorScale, ws, nothing, col; kw...) +# elseif type == :dataBar +# throw(XLSXError("Data bars are not yet implemented.")) +# elseif type == :iconSet +# throw(XLSXError("Icon sets are not yet implemented.")) +# elseif type == :formula +# throw(XLSXError("Formulas are not yet implemented.")) + else + throw(XLSXError("Invalid conditional format type: $type. Valid options are: colorScale, dataBar, iconSet, formula.")) + end +end +function setConditionalFormat(ws::Worksheet, ::Colon, ::Colon, type::Symbol; kw...) + if type == :colorScale + process_colon(setCfColorScale, ws, nothing, nothing; kw...) +# elseif type == :dataBar +# throw(XLSXError("Data bars are not yet implemented.")) +# elseif type == :iconSet +# throw(XLSXError("Icon sets are not yet implemented.")) +# elseif type == :formula +# throw(XLSXError("Formulas are not yet implemented.")) else throw(XLSXError("Invalid conditional format type: $type. Valid options are: colorScale, dataBar, iconSet, formula.")) end end -addCfColorScale(ws::Worksheet, ref::SheetCellRef; kw...) = do_sheet_names_match(ws, ref) && addCfColorScale(ws, ref.cellref; kw...) -addCfColorScale(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && addCfColorScale(ws, rng.rng; kw...) -addCfColorScale(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && addCfColorScale(ws, rng.colrng; kw...) -addCfColorScale(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && addCfColorScale(ws, rng.rowrng; kw...) -addCfColorScale(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(addCfColorScale, ws, rng; kw...) -addCfColorScale(ws::Worksheet, rng::ColumnRange; kw...) = process_colranges(addCfColorScale, ws, rng; kw...) -addCfColorScale(xl::XLSXFile, sheetcell::AbstractString)::Int = process_sheetcell(addCfColorScale, xl, sheetcell) -addCfColorScale(ws::Worksheet, ref_or_rng::AbstractString)::Int = process_ranges(addCfColorScale, ws, ref_or_rng) -addCfColorScale(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) = process_colon(addCfColorScale, ws, row, nothing) -addCfColorScale(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}) = process_colon(addCfColorScale, ws, nothing, col) -addCfColorScale(ws::Worksheet, ::Colon, ::Colon) = process_colon(addCfColorScale, ws, nothing, nothing) -addCfColorScale(ws::Worksheet, ::Colon) = process_colon(addCfColorScale, ws, nothing, nothing) -addCfColorScale(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}) = addCfColorScale(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col)))) -function addCfColorScale(ws::Worksheet, rng::CellRange; - 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}="percentile", - mid_val::Union{Nothing,String}="50", - mid_col::Union{Nothing,String}="FFFFEB84", - max_type::Union{Nothing,String}="max", - max_val::Union{Nothing,String}=nothing, - max_col::Union{Nothing,String}="FF63BE7B", - )::Int +function setConditionalFormat(ws::Worksheet, ::Colon, type::Symbol; kw...) + if type == :colorScale + process_colon(setCfColorScale, ws, nothing, nothing; kw...) +# elseif type == :dataBar +# throw(XLSXError("Data bars are not yet implemented.")) +# elseif type == :iconSet +# throw(XLSXError("Icon sets are not yet implemented.")) +# elseif type == :formula +# throw(XLSXError("Formulas are not yet implemented.")) + else + throw(XLSXError("Invalid conditional format type: $type. Valid options are: colorScale, dataBar, iconSet, formula.")) -# if isnothing(colorScale) && isnothing(dataBar) && isnothing(iconSet) && isnothing(formula) -# throw(XLSXError("At least one of colorScale, dataBar, iconSet, or formula must be specified.")) -# end + end +end +function setConditionalFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}, type::Symbol; kw...) + if type == :colorScale + setCfColorScale(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...) +# elseif type == :dataBar +# throw(XLSXError("Data bars are not yet implemented.")) +# elseif type == :iconSet +# throw(XLSXError("Icon sets are not yet implemented.")) +# elseif type == :formula +# throw(XLSXError("Formulas are not yet implemented.")) + else + throw(XLSXError("Invalid conditional format type: $type. Valid options are: colorScale, dataBar, iconSet, formula.")) + end +end +function setConditionalFormat(ws::Worksheet, ref_or_rng::AbstractString, type::Symbol; kw...) + if type == :colorScale + process_ranges(setCfColorScale, ws, ref_or_rng; kw...)::Int +# elseif type == :dataBar +# throw(XLSXError("Data bars are not yet implemented.")) +# elseif type == :iconSet +# throw(XLSXError("Icon sets are not yet implemented.")) +# elseif type == :formula +# throw(XLSXError("Formulas are not yet implemented.")) + else + throw(XLSXError("Invalid conditional format type: $type. Valid options are: colorScale, dataBar, iconSet, formula.")) + end +end +setCfColorScale(ws::Worksheet, ref::SheetCellRef; kw...) = do_sheet_names_match(ws, ref) && setCfColorScale(ws, ref.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; + 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", +)::Int !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) + length(rng) <=1 && throw(XLSXError("Range `$rng` must have more than one cell.")) old_cf = getConditionalFormats(ws) for cf in old_cf @@ -218,16 +292,16 @@ function addCfColorScale(ws::Worksheet, rng::CellRange; new_cf = XML.Element("conditionalFormatting"; sqref=rng) if isnothing(colorScale) push!(new_cf, XML.h.cfRule(type="colorScale", priority="1", - XML.h.colorScale( - isnothing(min_val) ? XML.h.cfvo(type=min_type) : XML.h.cfvo(type=min_type, val=min_val), - isnothing(mid_type) ? nothing : 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) ? nothing : XML.h.color(rgb=get_color(mid_col)), - XML.h.color(rgb=get_color(max_col)) - ) + XML.h.colorScale( + isnothing(min_val) ? XML.h.cfvo(type=min_type) : XML.h.cfvo(type=min_type, val=min_val), + isnothing(mid_type) ? nothing : 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) ? nothing : XML.h.color(rgb=get_color(mid_col)), + XML.h.color(rgb=get_color(max_col)) ) ) + ) else if !haskey(colorscales, colorScale) throw(XLSXError("Invalid color scale: $colorScale. Valid options are: $(keys(colorscales)).")) diff --git a/test/runtests.jl b/test/runtests.jl index db85a6d8..7bcee6a7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1390,7 +1390,7 @@ end @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) == nothing + @test XLSX.deletesheet!(f, 1) === nothing @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") @@ -1968,12 +1968,61 @@ 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")) + + 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")) + 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) @@ -2028,6 +2077,36 @@ end @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) + f = XLSX.open_xlsx_template(joinpath(data_directory, "Borders.xlsx")) s = f["Sheet1"] @@ -2110,6 +2189,152 @@ end diagonal=["style" => "none"] ) + 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 @testset "setFill" begin @@ -2658,6 +2883,72 @@ end end end +@testset "Conditional Formats" begin + + @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 MethodError XLSX.setConditionalFormat(s, [1], 1, :colorScale) # Vectors may be non-contiguous + @test_throws MethodError XLSX.setConditionalFormat(s, "A1,A2", :colorScale) # Non-contiguous ranges not allowed + @test_throws MethodError XLSX.setConditionalFormat(s, "A1", :colorScale) # Single cell not allowed + @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "A1:A1", :colorScale) # One cell cellrange not allowed + XLSX.setConditionalFormat(s, "1:1", :colorScale) + XLSX.setConditionalFormat(s, 2, :, :colorScale; colorScale = "redwhiteblue") + 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" + ) + XLSX.setConditionalFormat(s, "Sheet1!A4:E4", :colorScale; + min_type="min", + min_col="tomato", + max_type="max", + max_col="gold4" + ) + XLSX.setConditionalFormat(f, "Sheet1!A5:E5", :colorScale; + min_type="min", + min_col="yellow", + max_type="max", + max_col="darkgreen" + ) + @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("A5:E5") => ["colorScale"],XLSX.CellRange("A4:E4") => ["colorScale"],XLSX.CellRange("A3:E3") => ["colorScale"],XLSX.CellRange("A2:E2") => ["colorScale"],XLSX.CellRange("A1:E1") => ["colorScale"]] + @test_throws MethodError XLSX.setConditionalFormat(s, "A1", :colorScale) # Single cell not allowed + @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "Sheet1!A1:A2", :colorScale) # Overlaps with existing conditionalFormat range + @test_throws XLSX.XLSXError XLSX.setConditionalFormat(f, "Sheet1!1:2", :colorScale) # Overlaps with existing conditionalFormat range + @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, :, :colorScale) # Overlaps with existing conditionalFormat range + @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, :, :, :colorScale) # Overlaps with existing conditionalFormat range + + 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", :colorScale) + XLSX.setConditionalFormat(s, :, 2, :colorScale; colorScale = "redwhiteblue") + XLSX.setConditionalFormat(s, "Sheet1!E:E", :colorScale; colorScale = "greenwhitered") + 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" + ) + @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("C1:E4") => ["colorScale"],XLSX.CellRange("E1:E5") => ["colorScale"],XLSX.CellRange("B1:B5") => ["colorScale"],XLSX.CellRange("A1:A5") => ["colorScale"]] + + 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 From 1ba87d0cc2084e9ed2b31f1aa4c060d7c0038315 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 25 Apr 2025 20:50:33 +0100 Subject: [PATCH 092/154] Now over 90% code-cov according to Coverage.jl! --- data/Template File.xltx | Bin 0 -> 11094 bytes docs/src/formatting.md | 20 +- src/cellformats.jl | 2 +- src/conditional-formats.jl | 36 +-- src/worksheet.jl | 4 +- test/runtests.jl | 486 ++++++++++++++++++++++++++++++++++--- 6 files changed, 478 insertions(+), 70 deletions(-) create mode 100644 data/Template File.xltx diff --git a/data/Template File.xltx b/data/Template File.xltx new file mode 100644 index 0000000000000000000000000000000000000000..a39c059948219c65b318e5a4befcef7b1df3d21b GIT binary patch literal 11094 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 APXGV_ literal 0 HcmV?d00001 diff --git a/docs/src/formatting.md b/docs/src/formatting.md index 73c910ea..b270736b 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -82,7 +82,7 @@ hex RGB value or by name using any of the colors provided by [Colors.jl](https:/ The other set attribute functions behave in similar ways. See [`XLSX.setBorder`](@ref), [`XLSX.setFill`](@ref), [`XLSX.setFormat`](@ref) and [`XLSX.setAlignment`](@ref). -## Indexing multiple cells at once +## Formatting multiple cells at once 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 @@ -208,7 +208,7 @@ widths in Excel itself. 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 described here. +`setAttribute()` functions or the `setConditionalFormat()` function described here. !!! note @@ -220,8 +220,8 @@ but not otherwise. Such conditional formatting is generally straightforward to a can be updated by re-running the conditional formatting functions described but otherwise remain unchanged. - Some dynamic conditional formatting is possible, using Excel native functions, but the range of - functions is currently more limited than Excel itself can provide. + Some dynamic conditional formatting is possible in `XLSX.jl`, using Excel native functions, but the range of + functions is currently more limited than Excel itself can provide (work in progress). ### Static conditional formats @@ -271,9 +271,9 @@ XLSX.jl provides a function to create native Excel conditional formats that will an `XLSXFile` and which will update dynamically if the values in the cell range to which the formatting is applied are updated. -`XLSX.addConditionalFormat(sheet, CellRange, "formatting_type"; kwargs...)` +`XLSX.setConditionalFormat(sheet, CellRange, :formatting_type; kwargs...)` -Each of the available `formatting_type`s is described in the following sections. +Each of the available `:formatting_type`s is described in the following sections. #### Color Scale @@ -296,13 +296,13 @@ The default colorscale is `greenyellow`. To use a different built-in color scale specify the name using the keyword `colorScale`, thus: ```julia -julia> XLSX.addConditionalFormat(f["Sheet1"], "A1:F12", "colorScale") # Defaults to the `greenyellowred` built-in scale. +julia> XLSX.setConditionalFormat(f["Sheet1"], "A1:F12", :colorScale) # Defaults to the `greenyellowred` built-in scale. 0 -julia> XLSX.addConditionalFormat(f["Sheet1"], "A13:C18", "colorScale"; colorScale="whitered") +julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:C18", :colorScale; colorScale="whitered") 0 -julia> XLSX.addConditionalFormat(f["Sheet1"], "D13:F18", "colorScale"; colorScale="bluewhitered") +julia> XLSX.setConditionalFormat(f["Sheet1"], "D13:F18", :colorScale; colorScale="bluewhitered") 0 ``` @@ -315,7 +315,7 @@ a `percentile` or as a `min` or `max`. For the first three options, a value must Thus, you can apply a custom 3-color scale using, for example: ```julia -julia> XLSX.addConditionalFormat(f["Sheet1"], "A13:F18", :colorScale; +julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:F18", :colorScale; min_type="num", min_val="2", min_col="tomato", diff --git a/src/cellformats.jl b/src/cellformats.jl index 52f83655..bf90dc4f 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -903,7 +903,7 @@ function setOutsideBorder(ws::Worksheet, rng::CellRange; throw(XLSXError("Cannot set borders because cache is not enabled.")) end - length(rng) <= 1 && throw(XLSXError("Cannot set outside border for a single cell.")) +# 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) diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index 86ea8ea6..33b2f7ef 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -128,9 +128,6 @@ function getConditionalFormats(ws::Worksheet)::Vector{Pair{CellRange,Vector{Stri for child in XML.children(cf) if XML.tag(child) == "cfRule" push!(cf_types, child["type"]) - # if any(XML.tag(c) == "extLst" for c in XML.children(child)) - # println(" extras: ", true) - # end end end push!(allcfs, CellRange(cf["sqref"]) => cf_types) @@ -139,29 +136,34 @@ function getConditionalFormats(ws::Worksheet)::Vector{Pair{CellRange,Vector{Stri end """ - addConditionalFormat!(ws::Worksheet, rng::CellRange, type::Symbol; kw...) -> nothing + addConditionalFormat!(ws::Worksheet, rng::CellRange, type::Symbol; kw...) Add a new conditional format to a worksheet. -Valid optins for `type` are `:colorScale`, `:dataBar`, `:iconSet`, or `:formula` and these +!!! warning "In Develpment + + This function is still in development and may not work as expected. + It is not yet implemented for all types of conditional formats. + +Valid options for `type` are `:colorScale` (others in develpment) and these determine which type of conditional formatting is being defined. Keyword options differ according to the `type` specified Valid values for `colorScale` are: -- `"redyellowgreen"`: Red, Yellow, Green color scale. -- `"greenyellowred"`: Green, Yellow, Red color scale. -- `"redwhitegreen"` : Red, White, Green color scale. -- `"greenwhitered"` : Green, White, Red color scale. -- `"redwhiteblue"` : Red, White, Blue color scale. -- `"bluewhitered"` : Blue, White, Red color scale. -- `"redwhite"` : Red, White color scale. -- `"whitered"` : White, Red color scale. -- `"whitegreen"` : White, Green color scale. -- `"greenwhite"` : Green, White color scale. -- `"yellowgreen"` : Yellow, Green color scale. -- `"greenyellow"` : Green, Yellow color scale. +- `:redyellowgreen`: Red, Yellow, Green color scale. +- `:greenyellowred`: Green, Yellow, Red color scale. +- `:redwhitegreen` : Red, White, Green color scale. +- `:greenwhitered` : Green, White, Red color scale. +- `:redwhiteblue` : Red, White, Blue color scale. +- `:bluewhitered` : Blue, White, Red color scale. +- `:redwhite` : Red, White color scale. +- `:whitered` : White, Red color scale. +- `:whitegreen` : White, Green color scale. +- `:greenwhite` : Green, White color scale. +- `:yellowgreen` : Yellow, Green color scale. +- `:greenyellow` : Green, Yellow color scale. These are the 12 built-in color scales in Excel. diff --git a/src/worksheet.jl b/src/worksheet.jl index 494aff97..5ca98ccb 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -360,7 +360,7 @@ getcell(ws::Worksheet, s::SheetCellRef) = do_sheet_names_match(ws, s) && getcell 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.rng) +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) @@ -449,7 +449,7 @@ function getcellrange(ws::Worksheet, rng::CellRange)::Array{AbstractCell,2} return result end -getcellrange(ws::Worksheet, s::SheetCellRef) = do_sheet_names_match(ws, s) && getcellrange(ws, s.cellref) +#getcellrange(ws::Worksheet, s::SheetCellRef) = do_sheet_names_match(ws, s) && getcellrange(ws, s.cellref) 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) diff --git a/test/runtests.jl b/test/runtests.jl index 7bcee6a7..84f3c630 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -140,6 +140,13 @@ data_directory = joinpath(dirname(pathof(XLSX)), "..", "data") 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 end @@ -377,6 +384,11 @@ end @test XLSX.getcell(sheet1, "B2") == XLSX.Cell(XLSX.CellRef("B2"), "s", "", "0", "") XLSX.getcellrange(sheet1, "B2:C3") XLSX.getcellrange(f, "Sheet1!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 @@ -394,6 +406,8 @@ 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 @@ -503,7 +517,17 @@ end @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 XLSX.getcell(s, [2], [1,3]) == [XLSX.Cell(XLSX.CellRef("A2"), "", "", "3", "") XLSX.Cell(XLSX.CellRef("C2"), "", "", "5", "")] + + 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 @@ -2342,6 +2366,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")) @@ -2407,10 +2433,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"] @@ -2442,7 +2570,42 @@ end 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")) @@ -2453,7 +2616,6 @@ end 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")) @@ -2468,7 +2630,6 @@ end 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")) @@ -2549,6 +2710,74 @@ 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, 23:2:27, [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")) + + 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, 23:2:27, [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 @@ -2573,7 +2802,7 @@ end XLSX.setFont(s, "A16"; size=80, name="Ariel") cell_style=parse(Int, XLSX.getcell(s, "A16").style) - @test XLSX.setUniformStyle(s, "A16,A15,D20,F25")==cell_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 @@ -2585,6 +2814,59 @@ end @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 + end @testset "Width and height" begin @@ -2641,6 +2923,86 @@ end @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 @@ -2973,6 +3335,7 @@ end @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")) @@ -2982,57 +3345,100 @@ end 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 + 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 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.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") - @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 From 02373da0e03cebf083fee581ebf8f53aac0c0fa9 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 25 Apr 2025 21:44:20 +0100 Subject: [PATCH 093/154] Small changes --- docs/src/formatting.md | 5 ++-- src/conditional-formats.jl | 50 ++++++++++++++++++++++++++------------ 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/docs/src/formatting.md b/docs/src/formatting.md index b270736b..dab35d5a 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -151,8 +151,8 @@ julia> XLSX.setUniformBorder(s, "A1:CV100"; allsides = ["color" => "green"], dia 2 # This is the `borderId` that has now been uniformly applied to every cell. ``` -This updates the border color in cell A1 to be green and adds red diagonal lines across the cell. -It then applies all the `font` attributes of cell A1 uniformly to all the other cells in the range, +This updates the border color in cell `A1` to be green and adds red diagonal lines across the cell. +It then applies all the `font` 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), @@ -170,6 +170,7 @@ julia> XLSX.setUniformStyle(s, "A1:CV100") # set all formatting attributes to be 7 # this is the `styleId` that has now been applied to all cells in the range ``` + ## Copying formatting attributes It is possible to use non-contiguous ranges to copy format attributes from any cell to any other cells, diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index 33b2f7ef..95917b14 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -140,7 +140,7 @@ end Add a new conditional format to a worksheet. -!!! warning "In Develpment +!!! warning "In Develpment..." This function is still in development and may not work as expected. It is not yet implemented for all types of conditional formats. @@ -150,22 +150,37 @@ determine which type of conditional formatting is being defined. Keyword options differ according to the `type` specified -Valid values for `colorScale` are: +# type = :colorScale -- `:redyellowgreen`: Red, Yellow, Green color scale. -- `:greenyellowred`: Green, Yellow, Red color scale. -- `:redwhitegreen` : Red, White, Green color scale. -- `:greenwhitered` : Green, White, Red color scale. -- `:redwhiteblue` : Red, White, Blue color scale. -- `:bluewhitered` : Blue, White, Red color scale. -- `:redwhite` : Red, White color scale. -- `:whitered` : White, Red color scale. -- `:whitegreen` : White, Green color scale. -- `:greenwhite` : Green, White color scale. -- `:yellowgreen` : Yellow, Green color scale. -- `:greenyellow` : Green, Yellow color scale. +Define a 2-color or 3-color color scale conditional format. -These are the 12 built-in color scales in Excel. +Use the keyword `colorscale` to choose one of the 12 built-in Excel colorscales: + +- `"redyellowgreen"`: Red, Yellow, Green color scale. +- `"greenyellowred"`: Green, Yellow, Red color scale. +- `"redwhitegreen"` : Red, White, Green color scale. +- `"greenwhitered"` : Green, White, Red color scale. +- `"redwhiteblue"` : Red, White, Blue color scale. +- `"bluewhitered"` : Blue, White, Red color scale. +- `"redwhite"` : Red, White color scale. +- `"whitered"` : White, Red color scale. +- `"whitegreen"` : White, Green color scale. +- `"greenwhite"` : Green, White color scale. +- `"yellowgreen"` : Yellow, Green color scale. +- `"greenyellow"` : Green, Yellow color scale. (default) + +Alternatively, you can define a custom color scale by omitting the `colorscale` keyword and +instead using the following keywords: + +- `min_type`: Valid values are: `min`, `percentile`, `percent`, `num`, and `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`, and `formula`. Omit for a 2-color scale. +- `mid_val`: The value of the middle value. Omit for a 2-color scale. +- `mid_col`: The color of the middle value. Omit for a 2-color scale. +- `max_type`: The type of the maximum value. Valid values are: `max`, `percentile`, `num`, and `formula`. +- `max_val`: The value of the maximum value. Omit if `max_type="max"`. +- `max_col`: The color of the maximum value. """ function setConditionalFormat(xf::XLSXFile, ref_or_rng, type::Symbol; kw...) @@ -293,6 +308,11 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; new_cf = XML.Element("conditionalFormatting"; sqref=rng) 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.")) + isnothing(mid_type) || mid_type in ["percentile", "percent", "num", "formula"] || throw(XLSXError("Invalid mid_type: $mid_type. Valid options are: percentile, percent, num, formula.")) + max_type in ["max", "percentile", "num", "formula"] || throw(XLSXError("Invalid max_type: $max_type. Valid options are: max, percentile, num, formula.")) + push!(new_cf, XML.h.cfRule(type="colorScale", priority="1", XML.h.colorScale( isnothing(min_val) ? XML.h.cfvo(type=min_type) : XML.h.cfvo(type=min_type, val=min_val), From 65dc77aca12849463ca1da7e5a0d5f907d3fccea Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 26 Apr 2025 10:17:54 +0100 Subject: [PATCH 094/154] Additions to formatting guide --- docs/src/formatting.md | 203 ++++++++++++++++++--- docs/src/images/Written-to-merged-cell.png | Bin 9575 -> 6265 bytes docs/src/images/after-merge.png | Bin 0 -> 5833 bytes docs/src/images/multicell.png | Bin 0 -> 9400 bytes docs/src/images/multicell2.png | Bin 0 -> 9551 bytes docs/src/images/multicell3.png | Bin 0 -> 9054 bytes docs/src/images/multicell4.png | Bin 0 -> 7069 bytes docs/src/images/simple-unmerged.png | Bin 0 -> 2634 bytes 8 files changed, 182 insertions(+), 21 deletions(-) create mode 100644 docs/src/images/after-merge.png create mode 100644 docs/src/images/multicell.png create mode 100644 docs/src/images/multicell2.png create mode 100644 docs/src/images/multicell3.png create mode 100644 docs/src/images/multicell4.png create mode 100644 docs/src/images/simple-unmerged.png diff --git a/docs/src/formatting.md b/docs/src/formatting.md index dab35d5a..dce8fa37 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -84,6 +84,8 @@ The other set attribute functions behave in similar ways. See [`XLSX.setBorder`] ## 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 @@ -136,28 +138,27 @@ with existing on a cell by cell basis. If you set the font name on a range of ce all had different font colors, the color differences will persist even as the font name is applied to the range consistently. -## Setting uniform attributes +### Setting uniform attributes -Sometime it is useful to be able to apply a fully consistent set of format attributes to a range of +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.setBorder(s, "A1:CV100"; allsides = ["style" => "thin", "color" => "black"]) # set every cell individually --1 - 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 updates the border color in cell `A1` to be green and adds red diagonal lines across the cell. -It then applies all the `font` attributes of cell `A1` uniformly to all the other cells in the range, +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 these 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 @@ -170,6 +171,115 @@ julia> XLSX.setUniformStyle(s, "A1:CV100") # set all formatting attributes to be 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, whice has very hetrogeneous formatting across the three cells: + +![image|320x500](./images/multicell.png) + +We can apply `setBorder()` to add a top border to each cell: + +``` +julia> XLSX.setBorder(s, "B2,D2,F2"; top=["style"=>"thick", "color"=>"red"]) +-1 +``` +to merge the top border with the other attributes, to get + +![image|320x500](./images/multicell2.png) + +Alternatively, we can apply `setUniformBorder()`, which will update the borders of cell `B2` +and then apply all the border formatting to the other cells, overwriting the previous settings: + +``` +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 as they were. + +![image|320x500](./images/multicell3.png) + +Finally, we can set `B2` to have the formatting we want, and then apply a uniform style to all three cells. + +``` +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. + +![image|320x500](./images/multicell4.png) + +### Performance differences between methods + +To illustrtate the relative performance of these three methods, applied to a million cells: +``` +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 `setUniformStyles` : ") + @time do_format_styles(f) + return f +end +f=timeit() +``` + +which yields the following timings: + +``` +Using `setFormat` : 39.925697 seconds (1.04 G allocations: 71.940 GiB, 19.13% gc time) +Using `setUniformFormat` : 27.875646 seconds (711.00 M allocations: 48.195 GiB, 18.46% gc time) +Using `setUniformStyles` : 0.589316 seconds (14.00 M allocations: 416.628 MiB, 16.98% gc time) +``` + +The same test, using the more involved `setBorder` function + +``` +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` : 96.824494 seconds (2.82 G allocations: 194.342 GiB, 18.82% gc time) +Using `setUniformBorder` : 32.182135 seconds (787.00 M allocations: 62.081 GiB, 20.85% gc time) +Using `setUniformStyles` : 0.606058 seconds (14.00 M allocations: 416.660 MiB, 16.19% gc time) +``` +If maintaining heterogeneous formatting attributes is not important, it is much 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 @@ -316,7 +426,7 @@ a `percentile` or as a `min` or `max`. For the first three options, a value must Thus, you can apply a custom 3-color scale using, for example: ```julia -julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:F18", :colorScale; +julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:F22", :colorScale; min_type="num", min_val="2", min_col="tomato", @@ -415,30 +525,81 @@ It is not allowed to create new merged cells that overlap at all with any existi !!! warning - It is possible to write into a merged cell using `XLSX.jl`. + It is possible to write into a merged cell using `XLSX.jl`. This is illustrated below: ```julia - julia> XLSX.isMergedCell(f[1], "J8") - true + 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. + + ![image|320x500](./images/simple-unmerged.png) + + 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 + ``` + + ![image|320x500](./images/after-merge.png) + + 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> f[1]["J8"] = "This cell is merged" - "This cell is merged" + julia> s["A1"] + 5 - julia> XLSX.isMergedCell(f[1], "J8") - true + julia> s["A2"] + "text here now" - julia> XLSX.getMergedBaseCell(f[1], "J8") - (baseCell = F5, baseValue = 3.141592653589793) + julia> s["A3"] + missing - julia> f[1]["J8"] - "This cell is merged" + julia> XLSX.getMergedBaseCell(s, "A2") + (baseCell = A1, baseValue = 5) ``` - The cell remains merged, and this is how Excel will display it. The assigned cell value + 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 L8 references cell J8 in its formula ("=J8"): + cell `B2` references cell `A2` in its formula ("=A2"): ![image|320x500](./images/Written-to-merged-cell.png) diff --git a/docs/src/images/Written-to-merged-cell.png b/docs/src/images/Written-to-merged-cell.png index 0d58103f239914762538469656a3ce4505e111b8..6490039a48fcf6205f0cb95e4c485794200ede71 100644 GIT binary patch literal 6265 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 M6~i2y~GexW`_( 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 Xl6^dZaA*bmeg^4k8bPZy9HagZ0}3!R literal 0 HcmV?d00001 diff --git a/docs/src/images/multicell.png b/docs/src/images/multicell.png new file mode 100644 index 0000000000000000000000000000000000000000..6c952f3ffe06bc194bea92f918694f19605e7190 GIT binary patch literal 9400 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 Cl8k8p literal 0 HcmV?d00001 diff --git a/docs/src/images/multicell2.png b/docs/src/images/multicell2.png new file mode 100644 index 0000000000000000000000000000000000000000..d6013b7204f47d866f6c1fb184f930aab8a28852 GIT binary patch literal 9551 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 Wj8hP+wBtUya>@8&!TCS_>wf^eGwNLc literal 0 HcmV?d00001 diff --git a/docs/src/images/multicell3.png b/docs/src/images/multicell3.png new file mode 100644 index 0000000000000000000000000000000000000000..5639e68b9cfc944bde13fbb384e8a72cf5d944f3 GIT binary patch literal 9054 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 Md2@s8v)Ata52a)HW&i*H literal 0 HcmV?d00001 diff --git a/docs/src/images/multicell4.png b/docs/src/images/multicell4.png new file mode 100644 index 0000000000000000000000000000000000000000..990b1af8e76494e94a1126717527e25334bb29db GIT binary patch literal 7069 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 J;>-7+{132RKF0t6 literal 0 HcmV?d00001 diff --git a/docs/src/images/simple-unmerged.png b/docs/src/images/simple-unmerged.png new file mode 100644 index 0000000000000000000000000000000000000000..aab765e4fc7a5c45f529ec62182ba7533ab20e4d GIT binary patch literal 2634 zcma)8c|4Ts7k@4JNlkU93E4{4GO{$dkt|tem_gRbHq0nus4LrG42ha1$)2mhtt8oI zigD2xY9zVV!dPz8qU+j{u_cE4s#~AW{qOh3yFBM}&U4=H_k7QJUaFIWEksIA3IG5I z3TfpG03v$edyb?8c-JlJR0SU*7o2S^fvPTr32-4EXo0Z+0A~8OHUBN(dh2=QsS5zG z1vd>rFdL`0z+9xvfQj0Fxncn`!hTMme z=_zaAn9C}Zjm-4l-1^|7^IiEK|9ifD)8S|ZiOl8{gz}pM4Nq8bOhD%~rS88;W6RrW zDs0!4DGC%>Q#mvK@Z}9*^aHO7);n!2`09lBs;{Kh>2=4a>tiTp;Nm_S~=Gu6=H`&&D_d=n&f6kUr^_7!OX-uuf6_=(>)mwGjzm_H3+sQu`+h_k&dRt852Xwhm+R6RO#dlgUFq9v%5(A>Mcrn6i}CkAh*|4e7YC>q~87;2qsuwLn1*EdA9S zd8J8*bHsTxXrfH0c!tu1y6s}HkVS)>xmkUZ`axTFuVLuvOk@`Cr9htXDULA?X9-UezJCOa-j3_X|~itzAVBh%16Am*Ja+C$Y;PUD_`R+wK|6-940` z4}MYDBQyTI49fe|Q_PX}y7SaZ7e9UCu$`5Ll)=LPKh*%-|rrUh~u1flkH6i>!pl zgF9H`X1&x~GB=UcGgZIdazmSLR6?J*QJ)W?3e|33QXEeRw z#J_qgXyl^FpFdgbR62&-_Y9)^U7C;8S%dx`+aSQ?3aw2BBv%p9|HY3X#js4je9sBu{{}YLx@dM`XO-iTKhj$=1&w*q#5_)z+BF-g#3ZpHGK`rvL{_r9Ka z4US#+`n|Rbwn-z0Jg9Ws*ks4-V9=hGI6cD!6=2stlTJnK<=9lZkxV6{jekbaB6vZcy1zOZ^|J zIpwXzyZp)&)COElg33uNXUMq!6~Z{BT!83%9V!OI?a^TdRJ?Z*L?q&9x$~Yw%phJbQX3M)fZ&}Zb#PUqhFXjj zkAo*65-EVz={#Pm<$8bOjSl%_%R?<-ezm3DBT)5KJ>{>U_|r%90EX;$RC=1?i7&A{ zK2&|%oy`%a-=0^R1>D*M`XA)E)!e>h%Lc++U{=4sljM1}f?94+JfipY8@v;;Crc(h zf{UYg&@7ajosZg8R0*G+nCUkBDI_@0LK%Q(+~GR&2>TMP1v$L6U$KFA>AuZMt8N4* zNrr0^w0w5zQ+P~RPu3T-NcA@|wU}ElwQ+^8!Ygtk;vlN|0Zlz#l7(_2C?|dQ=(i0F ztM~Ic9$Km@Ce9?f7A_@ z9sj+S&x&^e4ut=s-YrlXVu8Gp;6Y^WY?er|gYVs^MMS(p@xbED5P$SJ}i)f*DiW|KYyf55FG2oyT{J0kqA`U-g)5 z3C^VyA?Toc^&1Z)5^@3TejHVe5NV<^v+$Rp?XgiAlR1F5 ziI^>Sa48o5UJ78fpTuW4ShkOpv;aJfMsB86X)sd5Cv{wu;SfbdW#Z}U3-5wWE5;8h zDN>ch7-x^#qlfePPMmrzN9Jo6JYWcK<$qEhT67NROeD_^C-<)*cD{UF7B*R0p!~f@Lo>>%+xn$c>Y+y6nZ;AT%^qXo`iwb S6lH<`e*ntb!K%vA@8UnRA@7m^ literal 0 HcmV?d00001 From a482de3ef86aa682ddd032075c6a4c7ddd141c5a Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 26 Apr 2025 12:33:28 +0100 Subject: [PATCH 095/154] Finish first version of `colorscale` conditional formatting --- docs/src/formatting.md | 3 +- src/conditional-formats.jl | 169 ++++++++++++++++++------------------- test/runtests.jl | 18 +++- 3 files changed, 100 insertions(+), 90 deletions(-) diff --git a/docs/src/formatting.md b/docs/src/formatting.md index dce8fa37..88797118 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -407,7 +407,7 @@ 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 `greenyellowred` built-in scale. +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") @@ -423,6 +423,7 @@ in [Colors.jl](https://juliagraphics.github.io/Colors.jl/stable/namedcolors/). 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 diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index 95917b14..958fdd91 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -108,6 +108,19 @@ const colorscales = Dict( # Defines the 12 standard, built-in Excel color sca ) ) ) + +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 + + """ Get the conditional formats for a worksheet. @@ -172,110 +185,82 @@ Use the keyword `colorscale` to choose one of the 12 built-in Excel colorscales: Alternatively, you can define a custom color scale by omitting the `colorscale` keyword and instead using the following keywords: -- `min_type`: Valid values are: `min`, `percentile`, `percent`, `num`, and `formula`. +- `min_type`: The type of the minimum value. Valid values are: `min`, `percentile`, `percent` or `num`. - `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`, and `formula`. Omit for a 2-color scale. -- `mid_val`: The value of the middle value. Omit for a 2-color scale. -- `mid_col`: The color of the middle value. Omit for a 2-color scale. -- `max_type`: The type of the maximum value. Valid values are: `max`, `percentile`, `num`, and `formula`. -- `max_val`: The value of the maximum value. Omit if `max_type="max"`. -- `max_col`: The color of the maximum value. +- `mid_type`: Valid values are: `percentile`, `percent` or `num`. Omit for a 2-color scale. +- `mid_val` : The value of the middle value. Omit for a 2-color scale. +- `mid_col` : The color of the middle value. Omit for a 2-color scale. +- `max_type`: The type of the maximum value. Valid values are: `max`, `percentile`, `percent` or `num`. +- `max_val` : The value of the maximum value. Omit if `max_type="max"`. +- `max_col` : The color of the maximum value. -""" -function setConditionalFormat(xf::XLSXFile, ref_or_rng, type::Symbol; kw...) - if type == :colorScale - process_sheetcell(setCfColorScale, xf, ref_or_rng; kw...) -# elseif type == :dataBar -# throw(XLSXError("Data bars are not yet implemented.")) -# elseif type == :iconSet -# throw(XLSXError("Icon sets are not yet implemented.")) -# elseif type == :formula -# throw(XLSXError("Formulas are not yet implemented.")) - else - throw(XLSXError("Invalid conditional format type: $type. Valid options are: colorScale, dataBar, iconSet, formula.")) - end -end -function setConditionalFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon, type::Symbol; kw...) - if type == :colorScale - process_colon(setCfColorScale, ws, row, nothing; kw...) -# elseif type == :dataBar -# throw(XLSXError("Data bars are not yet implemented.")) -# elseif type == :iconSet -# throw(XLSXError("Icon sets are not yet implemented.")) -# elseif type == :formula -# throw(XLSXError("Formulas are not yet implemented.")) - else - throw(XLSXError("Invalid conditional format type: $type. Valid options are: colorScale, dataBar, iconSet, formula.")) - end -end -function setConditionalFormat(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}, type::Symbol; kw...) - if type == :colorScale - process_colon(setCfColorScale, ws, nothing, col; kw...) -# elseif type == :dataBar -# throw(XLSXError("Data bars are not yet implemented.")) -# elseif type == :iconSet -# throw(XLSXError("Icon sets are not yet implemented.")) -# elseif type == :formula -# throw(XLSXError("Formulas are not yet implemented.")) - else - throw(XLSXError("Invalid conditional format type: $type. Valid options are: colorScale, dataBar, iconSet, formula.")) - end -end -function setConditionalFormat(ws::Worksheet, ::Colon, ::Colon, type::Symbol; kw...) - if type == :colorScale - process_colon(setCfColorScale, ws, nothing, nothing; kw...) -# elseif type == :dataBar -# throw(XLSXError("Data bars are not yet implemented.")) -# elseif type == :iconSet -# throw(XLSXError("Icon sets are not yet implemented.")) -# elseif type == :formula -# throw(XLSXError("Formulas are not yet implemented.")) - else - throw(XLSXError("Invalid conditional format type: $type. Valid options are: colorScale, dataBar, iconSet, formula.")) - end -end -function setConditionalFormat(ws::Worksheet, ::Colon, type::Symbol; kw...) - if type == :colorScale - process_colon(setCfColorScale, ws, nothing, nothing; kw...) -# elseif type == :dataBar -# throw(XLSXError("Data bars are not yet implemented.")) -# elseif type == :iconSet -# throw(XLSXError("Icon sets are not yet implemented.")) -# elseif type == :formula -# throw(XLSXError("Formulas are not yet implemented.")) - else - throw(XLSXError("Invalid conditional format type: $type. Valid options are: colorScale, dataBar, iconSet, formula.")) +The keywords `min_val`, `mid_val`, and `max_val` can be either a cell reference (e.g. `A1`) +or a number. If a cell reference is used, it will be converted to an absolute cell reference +when writing to an XLSXFile. - end -end -function setConditionalFormat(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}, type::Symbol; kw...) +Colors can be specified using an 8-digit hex string (e.g. `FF0000FF` for blue) or any named +color from Colors.jl ([here](https://juliagraphics.github.io/Colors.jl/stable/namedcolors/)). + +# Example +```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 + +``` + + +""" +function setConditionalFormat(f, r, type::Symbol; kw...) if type == :colorScale - setCfColorScale(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...) + setCfColorScale(f, r; kw...) # elseif type == :dataBar # throw(XLSXError("Data bars are not yet implemented.")) # elseif type == :iconSet # throw(XLSXError("Icon sets are not yet implemented.")) -# elseif type == :formula -# throw(XLSXError("Formulas are not yet implemented.")) - else +# elseif type == :Cell +# throw(XLSXError("Cell conditional formats are not yet implemented.")) +else throw(XLSXError("Invalid conditional format type: $type. Valid options are: colorScale, dataBar, iconSet, formula.")) end end -function setConditionalFormat(ws::Worksheet, ref_or_rng::AbstractString, type::Symbol; kw...) +function setConditionalFormat(f, r, c, type::Symbol; kw...) if type == :colorScale - process_ranges(setCfColorScale, ws, ref_or_rng; kw...)::Int + setCfColorScale(f, r, c; kw...) # elseif type == :dataBar # throw(XLSXError("Data bars are not yet implemented.")) # elseif type == :iconSet # throw(XLSXError("Icon sets are not yet implemented.")) -# elseif type == :formula -# throw(XLSXError("Formulas are not yet implemented.")) +# elseif type == :Cell +# throw(XLSXError("Cell conditional formats are not yet implemented.")) else throw(XLSXError("Invalid conditional format type: $type. Valid options are: colorScale, dataBar, iconSet, formula.")) end end -setCfColorScale(ws::Worksheet, ref::SheetCellRef; kw...) = do_sheet_names_match(ws, ref) && setCfColorScale(ws, ref.cellref; kw...) +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, 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...) @@ -309,9 +294,17 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; new_cf = XML.Element("conditionalFormatting"; sqref=rng) 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.")) - isnothing(mid_type) || mid_type in ["percentile", "percent", "num", "formula"] || throw(XLSXError("Invalid mid_type: $mid_type. Valid options are: percentile, percent, num, formula.")) - max_type in ["max", "percentile", "num", "formula"] || throw(XLSXError("Invalid max_type: $max_type. Valid options are: max, percentile, num, formula.")) + min_type in ["min", "percentile", "percent", "num"] || throw(XLSXError("Invalid min_type: $min_type. Valid options are: min, percentile, percent, num.")) + isnothing(min_val) || is_valid_cellname(min_val) || !isnothing(tryparse(Float64,min_val)) || throw(XLSXError("Invalid mid_type: $min_val. Valid options are a CellRef (e.g. `A1`) or a number.")) + isnothing(mid_type) || mid_type in ["percentile", "percent", "num"] || throw(XLSXError("Invalid mid_type: $mid_type. Valid options are: percentile, percent, num.")) + isnothing(mid_val) || is_valid_cellname(mid_val) || !isnothing(tryparse(Float64,mid_val)) || throw(XLSXError("Invalid mid_type: $mid_val. Valid options are a CellRef (e.g. `A1`) or a number.")) + max_type in ["max", "percentile", "percent", "num"] || throw(XLSXError("Invalid max_type: $max_type. Valid options are: max, percentile, percent, num.")) + isnothing(max_val) || is_valid_cellname(max_val) || !isnothing(tryparse(Float64,max_val)) || throw(XLSXError("Invalid mid_type: $max_val. Valid options are a CellRef (e.g. `A1`) or a number.")) + + + min_val = convertref(min_val) + mid_val = convertref(mid_val) + max_val = convertref(max_val) push!(new_cf, XML.h.cfRule(type="colorScale", priority="1", XML.h.colorScale( @@ -322,8 +315,8 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; isnothing(mid_type) ? nothing : XML.h.color(rgb=get_color(mid_col)), XML.h.color(rgb=get_color(max_col)) ) - ) - ) + )) + else if !haskey(colorscales, colorScale) throw(XLSXError("Invalid color scale: $colorScale. Valid options are: $(keys(colorscales)).")) diff --git a/test/runtests.jl b/test/runtests.jl index 84f3c630..73b4ca8a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3253,8 +3253,8 @@ end for i in 1:5, j in 1:5 s[i,j] = i+j end + @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, "A1,A2", :colorScale) # Non-contiguous ranges not allowed @test_throws MethodError XLSX.setConditionalFormat(s, "A1", :colorScale) # Single cell not allowed @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "A1:A1", :colorScale) # One cell cellrange not allowed XLSX.setConditionalFormat(s, "1:1", :colorScale) @@ -3306,6 +3306,22 @@ end ) @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("C1:E4") => ["colorScale"],XLSX.CellRange("E1:E5") => ["colorScale"],XLSX.CellRange("B1:B5") => ["colorScale"],XLSX.CellRange("A1:A5") => ["colorScale"]] + f=XLSX.newxlsx() + s=f[1] + for i in 1:5, j in 1:5 + s[i,j] = i+j + end + + XLSX.setConditionalFormat(s, :, 1:4, :colorScale; + min_type="min", + min_col="green", + mid_type="percentile", + mid_val="E4", + mid_col="red", + max_type="max", + max_col="blue" + ) + end From 2c6678c9e655ee28e83b598b1aeb34b38bb3c549 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 26 Apr 2025 12:56:52 +0100 Subject: [PATCH 096/154] Tweaks --- docs/src/api.md | 1 + src/conditional-formats.jl | 17 ++++++++++------- src/write.jl | 7 ------- test/runtests.jl | 7 +++---- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index 8199ce3c..2925ab72 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -32,6 +32,7 @@ XLSX.gettable XLSX.eachtablerow XLSX.writetable XLSX.writetable! +XLSX.setConditionalFormat XLSX.setFormat XLSX.setUniformFormat XLSX.setFont diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index 958fdd91..e07c925a 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -149,7 +149,10 @@ function getConditionalFormats(ws::Worksheet)::Vector{Pair{CellRange,Vector{Stri end """ - addConditionalFormat!(ws::Worksheet, rng::CellRange, type::Symbol; kw...) + setConditionalFormat(ws::Worksheet, cr::String, type::Symbol; kw...) -> ::Int} + setConditionalFormat(xf::XLSXFile, cr::String, type::Symbol; kw...) -> ::Int + + setConditionalFormat(ws::Worksheet, row, col, type::Symbol; kw...) -> ::Int} Add a new conditional format to a worksheet. @@ -207,10 +210,10 @@ color from Colors.jl ([here](https://juliagraphics.github.io/Colors.jl/stable/na 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") +julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:C18", :colorScale; colorscale="whitered") 0 -julia> XLSX.setConditionalFormat(f["Sheet1"], "D13:F18", :colorScale; colorScale="bluewhitered") +julia> XLSX.setConditionalFormat(f["Sheet1"], "D13:F18", :colorScale; colorscale="bluewhitered") 0 julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:F22", :colorScale; @@ -269,7 +272,7 @@ setCfColorScale(ws::Worksheet, rng::ColumnRange; kw...) = process_columnranges(s 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; - colorScale::Union{Nothing,String}=nothing, + colorscale::Union{Nothing,String}=nothing, min_type::Union{Nothing,String}="min", min_val::Union{Nothing,String}=nothing, min_col::Union{Nothing,String}="FFF8696B", @@ -292,7 +295,7 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; end new_cf = XML.Element("conditionalFormatting"; sqref=rng) - if isnothing(colorScale) + if isnothing(colorscale) min_type in ["min", "percentile", "percent", "num"] || throw(XLSXError("Invalid min_type: $min_type. Valid options are: min, percentile, percent, num.")) isnothing(min_val) || is_valid_cellname(min_val) || !isnothing(tryparse(Float64,min_val)) || throw(XLSXError("Invalid mid_type: $min_val. Valid options are a CellRef (e.g. `A1`) or a number.")) @@ -318,11 +321,11 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; )) else - if !haskey(colorscales, colorScale) + if !haskey(colorscales, colorscale) throw(XLSXError("Invalid color scale: $colorScale. Valid options are: $(keys(colorscales)).")) end new_cf = XML.Element("conditionalFormatting"; sqref=rng) - push!(new_cf, colorscales[colorScale]) + push!(new_cf, colorscales[colorscale]) end # Insert the new conditional formatting into the worksheet XML diff --git a/src/write.jl b/src/write.jl index df0b0a8d..e7cc5dcc 100644 --- a/src/write.jl +++ b/src/write.jl @@ -1050,13 +1050,6 @@ end Delete the given worksheet. The workbook can be saved back to file using, for example, `XLSX.writexlsx("myfile.xlsx", xf)`. -!!! warning "Experimental" - `deletesheet!` is an experimental function. - It removes the most obvious manifestations of a sheet but - there may be archaeological remains with unknown effects! - - Please report any issues. - """ deletesheet!(ws::Worksheet) = deletesheet!(get_workbook(ws), ws.name) deletesheet!(xl::XLSXFile, idx::Integer) = deletesheet!(get_workbook(xl), xl[idx].name) diff --git a/test/runtests.jl b/test/runtests.jl index 73b4ca8a..9fc3916b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3258,7 +3258,7 @@ end @test_throws MethodError XLSX.setConditionalFormat(s, "A1", :colorScale) # Single cell not allowed @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "A1:A1", :colorScale) # One cell cellrange not allowed XLSX.setConditionalFormat(s, "1:1", :colorScale) - XLSX.setConditionalFormat(s, 2, :, :colorScale; colorScale = "redwhiteblue") + XLSX.setConditionalFormat(s, 2, :, :colorScale; colorscale = "redwhiteblue") XLSX.setConditionalFormat(s, 3, 1:5, :colorScale; min_type="min", min_col="green", @@ -3293,8 +3293,8 @@ end s[i,j] = i+j end XLSX.setConditionalFormat(s, "A1:A5", :colorScale) - XLSX.setConditionalFormat(s, :, 2, :colorScale; colorScale = "redwhiteblue") - XLSX.setConditionalFormat(s, "Sheet1!E:E", :colorScale; colorScale = "greenwhitered") + XLSX.setConditionalFormat(s, :, 2, :colorScale; colorscale = "redwhiteblue") + XLSX.setConditionalFormat(s, "Sheet1!E:E", :colorScale; colorscale = "greenwhitered") XLSX.setConditionalFormat(s, 1:5, 3:4, :colorScale; min_type="min", min_col="green", @@ -3324,7 +3324,6 @@ end end - end @testset "merged cells" begin From 5cd216f3a76dc126171142b7df3401471303d1ad Mon Sep 17 00:00:00 2001 From: TimG1964 <157401228+TimG1964@users.noreply.github.com> Date: Sat, 26 Apr 2025 13:14:15 +0100 Subject: [PATCH 097/154] Remove stray `=` following conflict resolution --- src/write.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/write.jl b/src/write.jl index ce461e23..e7cc5dcc 100644 --- a/src/write.jl +++ b/src/write.jl @@ -45,7 +45,6 @@ function open_empty_template( 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) if sheetname != "" From 2810baee58b2d18e20569c4d9b4786b962b3c414 Mon Sep 17 00:00:00 2001 From: TimG1964 <157401228+TimG1964@users.noreply.github.com> Date: Sat, 26 Apr 2025 13:20:20 +0100 Subject: [PATCH 098/154] Typo in file name in runtests.jl --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 9fc3916b..eced0fe4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1423,7 +1423,7 @@ end 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 + 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) From 1c3e3f6711ff52a21b5971652022ec48707cc8bf Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 27 Apr 2025 11:53:13 +0100 Subject: [PATCH 099/154] Correct type in runtests.jl --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 9fc3916b..eced0fe4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1423,7 +1423,7 @@ end 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 + 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) From 9d0fa2f784007c92d24a5c3f341fd4f46141888a Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 28 Apr 2025 23:27:26 +0100 Subject: [PATCH 100/154] Begin to add `:cell` type conditional formats --- src/conditional-formats.jl | 267 ++++++++++++++++++++++++++++++++++++- src/types.jl | 5 + 2 files changed, 265 insertions(+), 7 deletions(-) diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index e07c925a..98b5fc23 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -1,4 +1,28 @@ -const colorscales = Dict( # Defines the 12 standard, built-in Excel color scales for conditional formatting. +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"=>"FFA51E00"), + "fill" => Dict("pattern" => "solid", "bgColor"=>"FF9C5700") + ), + "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"), + ) +) # for type = :Cell + +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"), @@ -109,6 +133,56 @@ const colorscales = Dict( # Defines the 12 standard, built-in Excel color sca ) ) +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. Push everything lower down one. + 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 + # Check new_dx doesn't duplicate any existing dxf. 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.parse(XML.Node, XML.write(node))[1] == XML.parse(XML.Node, XML.write(new_dx))[1] # XML.jl defines `Base.:(==)` + return DxFormat(k - 1) # CellDataFormat is zero-indexed + end + end + 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 convertref(c) if !isnothing(c) if is_valid_cellname(c) @@ -231,32 +305,78 @@ julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:F22", :colorScale; ``` +# type = :cell + +Defines a conditional format based on the value of a cell. + +Valid keywords are: +- `operator` : Defines the comparison to make. +- `formula1` : defines the first value to compare against. This can be a cell reference (e.g. `A1`) or a number. +- `formula2` : defines the second value to compare against. This can be a cell reference (e.g. `A1`) or a number. +- `dxStyle` : Used 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 `operator` defines the comparison to use in the conditiopnal formatting. +If the condition is met, the format is applied. Valid options are: +- `greaterThan` +- `lessThan` +- `between` +- `notBetween` +- `equal` +- `notEqual` +- `greaterEqual` +- `lessEqual` + +The comparison is made against the value in `formula1` and, if `operator` is either +`between` or `notBetween`, `formula2` sets the other bound on the condition. If not specified, +`formula1` 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 two ways. Use the keyword +`dxStyle` to select one of the built-in Excel formats. Valid options are: +- `redfilltext` +- `yellowfilltext` +- `greenfilltext` +- `redfill` +- `redtext` +- `redborder`. + +Alternatively, you can define a custom format by using the keywords `format`, `font`, +`border`, and `fill`. + +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`. """ function setConditionalFormat(f, r, type::Symbol; kw...) if type == :colorScale setCfColorScale(f, r; kw...) -# elseif type == :dataBar -# throw(XLSXError("Data bars are not yet implemented.")) + elseif type == :cell + setCfCell(f, r; kw...) # elseif type == :iconSet # throw(XLSXError("Icon sets are not yet implemented.")) # elseif type == :Cell # throw(XLSXError("Cell conditional formats are not yet implemented.")) else - throw(XLSXError("Invalid conditional format type: $type. Valid options are: colorScale, dataBar, iconSet, formula.")) + throw(XLSXError("Invalid conditional format type: $type. Valid options are: :colorScale, :cell")) end end function setConditionalFormat(f, r, c, type::Symbol; kw...) if type == :colorScale setCfColorScale(f, r, c; kw...) -# elseif type == :dataBar -# throw(XLSXError("Data bars are not yet implemented.")) + elseif type == :cell + setCfCell(f, r, c; kw...) # elseif type == :iconSet # throw(XLSXError("Icon sets are not yet implemented.")) # elseif type == :Cell # throw(XLSXError("Cell conditional formats are not yet implemented.")) else - throw(XLSXError("Invalid conditional format type: $type. Valid options are: colorScale, dataBar, iconSet, formula.")) + throw(XLSXError("Invalid conditional format type: $type. Valid options are: :colorScale, :cell.")) end end setCfColorScale(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setCfColorScale, ws, row, nothing; kw...) @@ -346,5 +466,138 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; update_worksheets_xml!(get_xlsxfile(ws)) + return 0 +end +setCfCell(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setCfCell, ws, row, nothing; kw...) +setCfCell(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setCfCell, ws, nothing, col; kw...) +setCfCell(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setCfCell, ws, nothing, nothing; kw...) +setCfCell(ws::Worksheet, ::Colon; kw...) = process_colon(setCfCell, ws, nothing, nothing; kw...) +setCfCell(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setCfCell(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...) +setCfCell(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setCfCell(ws, rng.rng; kw...) +setCfCell(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setCfCell(ws, rng.colrng; kw...) +setCfCell(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setCfCell(ws, rng.rowrng; kw...) +setCfCell(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCfCell, ws, rng; kw...) +setCfCell(ws::Worksheet, rng::ColumnRange; kw...) = process_columnranges(setCfCell, ws, rng; kw...) +setCfCell(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetcell(setCfCell, xl, sheetcell; kw...) +setCfCell(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setCfCell, ws, ref_or_rng; kw...) +function setCfCell(ws::Worksheet, rng::CellRange; + operator::Union{Nothing,String}="greaterThan", + formula1::Union{Nothing,String}=nothing, + formula2::Union{Nothing,String}=nothing, + dxStyle::Union{Nothing,String}="redfilltext", + format::Union{Dict{String,Dict{String, String}},Nothing}=nothing, + font::Union{Dict{String,Dict{String, String}},Nothing}=nothing, + border::Union{Dict{String,Dict{String, String}},Nothing}=nothing, + fill::Union{Dict{String,Dict{String, String}},Nothing}=nothing +)::Int + + !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) + length(rng) <=1 && throw(XLSXError("Range `$rng` must have more than one cell.")) + + old_cf = getConditionalFormats(ws) + for cf in old_cf + if cf.first != rng && intersects(cf.first, rng) + throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)` but is not the same. Must be the same as the existing range or entirely separate.")) + end + end + +# new_cf = XML.Element("conditionalFormatting"; sqref=rng) + if haskey(highlights, dxStyle) + dx = highlights[dxStyle] + new_dx = XML.Element("dxf") + for (k, v) in dx + if k in ["format", "fill", "font", "border"] + if k=="fill" + if !isnothing(v) + filldx=XML.Element("fill") + patterndx=XML.Element("patternFill") + for (y, z) in v + if y in ["fgColor", "bgColor"] + push!(patterndx, XML.Element(y, rgb=get_color(z))) + elseif y == "pattern" + push!(patterndx, XML.Element(y, val = 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 + 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")) + elseif y == "under" + z != "none" && push!(fontdx, XML.Element("u"; val="v")) + elseif y == "strike" + strike=="true" && push!(fontdx, XML.Element(y; val="0")) + end + end + end + push!(new_dx, fontdx) + end + elseif k=="border" + if !isnothing(v) + 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!(new_dx, borderdx) + end + + end + end + dxid = Add_Cf_Dx(get_workbook(ws), new_dx) + if isnothing(formula1) + formula1 = all(ismissing.(ws[rng])) ? nothing : string(sum(skipmissing(ws[rng]))/count(!ismissing, ws[rng])) + end + new_cf = XML.Element("conditionalFormatting"; sqref=rng) + cfx = XML.Element("cfRule"; type="cellIs", dxfId=Int(dxid.id), priority="1", operator=operator) + if !isnothing(formula1) + push!(cfx, XML.Element("formula", XML.Text(formula1))) + end + if !isnothing(formula2) + push!(cfx, XML.Element("formula", XML.Text(formula2))) + end + push!(new_cf, cfx) + + # Insert the new conditional formatting into 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 + + update_worksheets_xml!(get_xlsxfile(ws)) + return 0 end \ No newline at end of file diff --git a/src/types.jl b/src/types.jl index 5689b0e8..1a57e1cd 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 From 14d7dd0b1a39c2c87213d25eb063ae5f2b7b98e2 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 29 Apr 2025 23:47:34 +0100 Subject: [PATCH 101/154] `:cell` type now basically working --- src/conditional-formats.jl | 129 ++++++++++++++++++++++++------------- 1 file changed, 84 insertions(+), 45 deletions(-) diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index 98b5fc23..cfd1f223 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -18,7 +18,7 @@ const highlights::Dict{String,Dict{String,Dict{String, String}}} = Dict( "font" => Dict("color"=>"FF9C0006"), ), "redborder" => Dict( - "border" => Dict("color"=>"FF9C0006", "style"=>"thin"), + "border" => Dict("color"=>"FF9C0006", "style"=>"thin") ) ) # for type = :Cell @@ -206,9 +206,13 @@ type of the conditional format applies. """ -function getConditionalFormats(ws::Worksheet)::Vector{Pair{CellRange,Vector{String}}} +function allCfs(ws::Worksheet) sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet" * string(ws.sheetId) * ".xml") # find all the blocks in the worksheet's xml file - allcfnodes = find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":worksheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":conditionalFormatting", sheetdoc) + return find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":worksheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":conditionalFormatting", sheetdoc) +end + +function getConditionalFormats(ws::Worksheet)::Vector{Pair{CellRange,Vector{String}}} + allcfnodes = allCfs(ws::Worksheet) allcfs = Vector{Pair{CellRange,Vector{String}}}() for (i, cf) in enumerate(allcfnodes) cf_types = Vector{String}() @@ -484,11 +488,11 @@ function setCfCell(ws::Worksheet, rng::CellRange; operator::Union{Nothing,String}="greaterThan", formula1::Union{Nothing,String}=nothing, formula2::Union{Nothing,String}=nothing, - dxStyle::Union{Nothing,String}="redfilltext", - format::Union{Dict{String,Dict{String, String}},Nothing}=nothing, - font::Union{Dict{String,Dict{String, String}},Nothing}=nothing, - border::Union{Dict{String,Dict{String, String}},Nothing}=nothing, - fill::Union{Dict{String,Dict{String, String}},Nothing}=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 )::Int !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) @@ -500,46 +504,63 @@ function setCfCell(ws::Worksheet, rng::CellRange; throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)` but is not the same. Must be the same as the existing range or entirely separate.")) end end - -# new_cf = XML.Element("conditionalFormatting"; sqref=rng) - if haskey(highlights, dxStyle) + 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] - new_dx = XML.Element("dxf") - for (k, v) in dx - if k in ["format", "fill", "font", "border"] - if k=="fill" - if !isnothing(v) - filldx=XML.Element("fill") - patterndx=XML.Element("patternFill") - for (y, z) in v - if y in ["fgColor", "bgColor"] - push!(patterndx, XML.Element(y, rgb=get_color(z))) - elseif y == "pattern" - push!(patterndx, XML.Element(y, val = z)) - end + else + throw(XLSXError("Invalid dxStyle: $dxStyle. Valid options are: $(keys(highlights)).")) + end + new_dx = XML.Element("dxf") + for k in ["font", "fill", "format", "border"] # Order is 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 + if y in ["fgColor", "bgColor"] + push!(patterndx, XML.Element(y, rgb=get_color(z))) + elseif y == "pattern" && z != "none" + patterndx["patternType"] = z end - push!(filldx, patterndx) end - push!(new_dx, filldx) - elseif k=="font" - if !isnothing(v) - fontdx=XML.Element("font") - for (y, z) in v - 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")) - elseif y == "under" - z != "none" && push!(fontdx, XML.Element("u"; val="v")) - elseif y == "strike" - strike=="true" && push!(fontdx, XML.Element(y; val="0")) - end + push!(filldx, patterndx) + end + push!(new_dx, filldx) + elseif k=="font" + if !isnothing(v) + fontdx=XML.Element("font") + for (y, z) in v + 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" + strike=="true" && push!(fontdx, XML.Element(y; val="0")) end end - push!(new_dx, fontdx) end + push!(new_dx, fontdx) + elseif k=="border" if !isnothing(v) borderdx=XML.Element("border") @@ -549,7 +570,7 @@ function setCfCell(ws::Worksheet, rng::CellRange; rightdx = XML.Element("right") topdx = XML.Element("top") bottomdx = XML.Element("bottom") - if isnothing(sdx) + if !isnothing(sdx) leftdx["style"]=sdx rightdx["style"]=sdx topdx["style"]=sdx @@ -562,16 +583,20 @@ function setCfCell(ws::Worksheet, rng::CellRange; push!(bottomdx, cdx) end end + push!(borderdx, leftdx) + push!(borderdx, rightdx) + push!(borderdx, topdx) + push!(borderdx, bottomdx) push!(new_dx, borderdx) end - end + end + dxid = Add_Cf_Dx(get_workbook(ws), new_dx) if isnothing(formula1) formula1 = all(ismissing.(ws[rng])) ? nothing : string(sum(skipmissing(ws[rng]))/count(!ismissing, ws[rng])) end - new_cf = XML.Element("conditionalFormatting"; sqref=rng) cfx = XML.Element("cfRule"; type="cellIs", dxfId=Int(dxid.id), priority="1", operator=operator) if !isnothing(formula1) push!(cfx, XML.Element("formula", XML.Text(formula1))) @@ -579,7 +604,21 @@ function setCfCell(ws::Worksheet, rng::CellRange; if !isnothing(formula2) push!(cfx, XML.Element("formula", XML.Text(formula2))) end + + allcfs = filter(x->x["sqref"]==string(rng), allCfs(ws)) # Match range with existing conditional formatting blocks. + if length(allcfs) == 0 # No existing conditional formatting blocks for this range so create a new one. + new_cf = XML.Element("conditionalFormatting"; sqref=rng) + elseif length(allcfs) == 1 # Existing conditional formatting block found for this range so add new rule to that. + children=XML.children(allcfs[1]) + cfx["priority"] = string(maximum([parse(Int, c["priority"]) for c in children])+1) + new_cf = allcfs[1] + else + throw(XLSXError("Multiple conditional formatting blocks found for range `$rng`. This should not happen.")) + end + + push!(new_cf, cfx) + println("Conditional formatting: ", XML.write(new_cf)) # Insert the new conditional formatting into the worksheet XML sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet" * string(ws.sheetId) * ".xml") # The blocks come after the From 691069f211aed6829b64a9d1ac8fd0f2b83abf33 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Wed, 30 Apr 2025 18:21:01 +0100 Subject: [PATCH 102/154] Further work on `:cell` type conditional formats --- docs/src/formatting.md | 18 +++- docs/src/images/cell.png | Bin 0 -> 22947 bytes src/cellformat-helpers.jl | 28 ++++++ src/cellformats.jl | 28 +----- src/conditional-formats.jl | 186 +++++++++++++++++++++++-------------- src/styles.jl | 3 + 6 files changed, 163 insertions(+), 100 deletions(-) create mode 100644 docs/src/images/cell.png diff --git a/docs/src/formatting.md b/docs/src/formatting.md index 88797118..cc0e7600 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -388,7 +388,7 @@ Each of the available `:formatting_type`s is described in the following sections #### Color Scale -It is possible to apply a `colorScale` formatting type to a range of cells. +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. @@ -404,16 +404,16 @@ In XLSX.jl, the twelve built-in scales are named by their start/mid/end colors a | 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: +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") +julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:C18", :colorScale; colorscale="whitered") 0 -julia> XLSX.setConditionalFormat(f["Sheet1"], "D13:F18", :colorScale; colorScale="bluewhitered") +julia> XLSX.setConditionalFormat(f["Sheet1"], "D13:F18", :colorScale; colorscale="bluewhitered") 0 ``` @@ -442,6 +442,16 @@ julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:F22", :colorScale; ``` ![image|320x500](./images/custom-colorscale.png) +### Cell Value + +It is possible to apply a conditional format to a range of cells that applies when a cell's +value meets a specified condition using the `:cell` type. + +![image|320x500](./images/cell.png) + +In Excel there are twelve built-in color scales available, but it is possible to create +custom color scales, too. + ## Working with Merged Cells Worksheets may contain merged cells. XLSX.jl provides functions to identify the merged cells in a worksheet, diff --git a/docs/src/images/cell.png b/docs/src/images/cell.png new file mode 100644 index 0000000000000000000000000000000000000000..7d206bfa76257faf716f88d5c825dddbcda2c6d8 GIT binary patch literal 22947 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�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 occursin(x, code), DATETIME_CODES)) + 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) diff --git a/src/cellformats.jl b/src/cellformats.jl index 32a95cce..a339c02f 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -1777,33 +1777,7 @@ function setFormat(sh::Worksheet, cellref::CellRef; return cell_format.numFmtId end - if haskey(builtinFormatNames, uppercasefirst(format)) # User specified a format by name - new_formatid = 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)) - 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 - new_formatid = 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) - ) - - 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"] diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index cfd1f223..f4c4124c 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -132,6 +132,22 @@ const colorscales::Dict{String,XML.Node} = Dict( # Defines the 12 standard, b ) ) ) +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 Add_Cf_Dx(wb::Workbook, new_dx::XML.Node)::DxFormat # Check if the workbook already has a dxfs element. If not, add one. @@ -283,7 +299,8 @@ when writing to an XLSXFile. Colors can be specified using an 8-digit hex string (e.g. `FF0000FF` for blue) or any named color from Colors.jl ([here](https://juliagraphics.github.io/Colors.jl/stable/namedcolors/)). -# Example +# Examples + ```julia julia> XLSX.setConditionalFormat(f["Sheet1"], "A1:F12", :colorScale) # Defaults to the `greenyellow` built-in scale. 0 @@ -315,8 +332,8 @@ Defines a conditional format based on the value of a cell. Valid keywords are: - `operator` : Defines the comparison to make. -- `formula1` : defines the first value to compare against. This can be a cell reference (e.g. `A1`) or a number. -- `formula2` : defines the second value to compare against. This can be a cell reference (e.g. `A1`) or a number. +- `value1` : 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. - `dxStyle` : Used 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. @@ -325,36 +342,87 @@ Valid keywords are: The keyword `operator` defines the comparison to use in the conditiopnal formatting. If the condition is met, the format is applied. Valid options are: -- `greaterThan` -- `lessThan` -- `between` -- `notBetween` -- `equal` -- `notEqual` -- `greaterEqual` -- `lessEqual` - -The comparison is made against the value in `formula1` and, if `operator` is either -`between` or `notBetween`, `formula2` sets the other bound on the condition. If not specified, -`formula1` will be the arithmetic average of the (non-missing) cell values in the range if +- `greaterThan` (>) +- `greaterEqual` (>=) +- `lessThan` (<) +- `lessEqual` (<=) +- `between` (requires `value2`) +- `notBetween` (requires `value2`) +- `equal` (==) +- `notEqual` (!=) + +The comparison is made against the value in `value1` and, if `operator` is either +`between` or `notBetween`, `value2` sets the other bound on the condition. If not specified, +`value1` 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 two ways. Use the keyword `dxStyle` to select one of the built-in Excel formats. Valid options are: -- `redfilltext` -- `yellowfilltext` -- `greenfilltext` -- `redfill` -- `redtext` -- `redborder`. +- `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`. +`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 nor 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`. +If neither `dxStyle` nor custom formatting keywords are specified, the default +is `dxStyle="redfilltext"`. + +# Examples + +```julia +julia> XLSX.setConditionalFormat(s, "B1:B5", :cell) # Defaults to `operator="greaterThan"`, `dxStyle`="redfilltext"` and `value1` set to the arithmetic agverage of cell values in `rng`. + +julia> XLSX.setConditionalFormat(s, "B1:B5", :cell; + operator="between", + value1="2", + value2="3", + fill = ["pattern" => "none", "bgColor"=>"FFFFC7CE"], + format = ["format"=>"0.00%"], + font = ["color"=>"blue", "bold"=>"true"] + ) + +julia> XLSX.setConditionalFormat(s, "B1:B5", :cell; + operator="greaterThan", + value1="4", + fill = ["pattern" => "none", "bgColor"=>"green"], + format = ["format"=>"0.0"], + font = ["color"=>"red", "italic"=>"true"] + ) + +julia> XLSX.setConditionalFormat(s, "B1:B5", :cell; + operator="lessThan", + value1="2", + fill = ["pattern" => "none", "bgColor"=>"yellow"], + format = ["format"=>"0.0"], + font = ["color"=>"green"], + border = ["style"=>"thick", "color"=>"coral"] + ) + +``` """ function setConditionalFormat(f, r, type::Symbol; kw...) @@ -452,26 +520,13 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; push!(new_cf, colorscales[colorscale]) end - # Insert the new conditional formatting into 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 + add_cf_to_XML(ws, new_cf) # Insert the new conditional formatting into the worksheet XML update_worksheets_xml!(get_xlsxfile(ws)) return 0 end + setCfCell(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setCfCell, ws, row, nothing; kw...) setCfCell(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setCfCell, ws, nothing, col; kw...) setCfCell(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setCfCell, ws, nothing, nothing; kw...) @@ -486,8 +541,8 @@ setCfCell(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetce setCfCell(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setCfCell, ws, ref_or_rng; kw...) function setCfCell(ws::Worksheet, rng::CellRange; operator::Union{Nothing,String}="greaterThan", - formula1::Union{Nothing,String}=nothing, - formula2::Union{Nothing,String}=nothing, + value1::Union{Nothing,String}=nothing, + value2::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, @@ -504,6 +559,7 @@ function setCfCell(ws::Worksheet, rng::CellRange; throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)` but is not the same. Must be the same as the existing range or entirely separate.")) end end + wb=get_workbook(ws) if isnothing(dxStyle) if all(isnothing.([border, fill, font, format])) dx=highlights["redfilltext"] @@ -525,7 +581,7 @@ function setCfCell(ws::Worksheet, rng::CellRange; throw(XLSXError("Invalid dxStyle: $dxStyle. Valid options are: $(keys(highlights)).")) end new_dx = XML.Element("dxf") - for k in ["font", "fill", "format", "border"] # Order is important to Excel. + for k in ["font", "format", "fill", "border"] # Order is important to Excel. if haskey(dx, k) v = dx[k] if k=="fill" @@ -560,7 +616,6 @@ function setCfCell(ws::Worksheet, rng::CellRange; end end push!(new_dx, fontdx) - elseif k=="border" if !isnothing(v) borderdx=XML.Element("border") @@ -588,53 +643,46 @@ function setCfCell(ws::Worksheet, rng::CellRange; 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 dxid = Add_Cf_Dx(get_workbook(ws), new_dx) - if isnothing(formula1) - formula1 = all(ismissing.(ws[rng])) ? nothing : string(sum(skipmissing(ws[rng]))/count(!ismissing, ws[rng])) + if isnothing(value1) + value1 = 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), priority="1", operator=operator) - if !isnothing(formula1) - push!(cfx, XML.Element("formula", XML.Text(formula1))) + if !isnothing(value1) + push!(cfx, XML.Element("formula", XML.Text(value1))) end - if !isnothing(formula2) - push!(cfx, XML.Element("formula", XML.Text(formula2))) + if !isnothing(value2) + push!(cfx, XML.Element("formula", XML.Text(value2))) end allcfs = filter(x->x["sqref"]==string(rng), allCfs(ws)) # Match range with existing conditional formatting blocks. if length(allcfs) == 0 # No existing conditional formatting blocks for this range so create a new one. new_cf = XML.Element("conditionalFormatting"; sqref=rng) - elseif length(allcfs) == 1 # Existing conditional formatting block found for this range so add new rule to that. + else # Existing conditional formatting block found for this range so add new rule to that. children=XML.children(allcfs[1]) cfx["priority"] = string(maximum([parse(Int, c["priority"]) for c in children])+1) new_cf = allcfs[1] - else - throw(XLSXError("Multiple conditional formatting blocks found for range `$rng`. This should not happen.")) end push!(new_cf, cfx) - println("Conditional formatting: ", XML.write(new_cf)) - - # Insert the new conditional formatting into 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 + + add_cf_to_XML(ws, new_cf) # Add the new conditional formatting to the worksheet XML. update_worksheets_xml!(get_xlsxfile(ws)) diff --git a/src/styles.jl b/src/styles.jl index d6ca1eb1..1e59c61e 100644 --- a/src/styles.jl +++ b/src/styles.jl @@ -131,6 +131,9 @@ const FontAttribute = Union{String, Pair{String, Pair{String, String}}} # Queries numFmt formatCode field by numFmtId. 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) From 251457926370a3b17c011e1fb63fba424a77ce66 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 2 May 2025 18:45:24 +0100 Subject: [PATCH 103/154] All conditional formats except `dataBars` and `iconSets`. Still need to add tests and examples. --- src/conditional-formats.jl | 1077 +++++++++++++++++++++++++++++------- test/runtests.jl | 8 +- 2 files changed, 882 insertions(+), 203 deletions(-) diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index f4c4124c..1313a155 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -1,3 +1,4 @@ +const needsValue2::Vector{String} = ["between", "notBetween"] const highlights::Dict{String,Dict{String,Dict{String, String}}} = Dict( "redfilltext" => Dict( "font" => Dict("color"=>"FF9C0006"), @@ -132,6 +133,127 @@ const colorscales::Dict{String,XML.Node} = Dict( # Defines the 12 standard, b ) ) ) + +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)))" +) + +function get_dx(dxStyle::Union{Nothing, String}, border::Union{Nothing, Vector{Pair{String, String}}}, fill::Union{Nothing, Vector{Pair{String, String}}}, font::Union{Nothing, Vector{Pair{String, String}}}, format::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 is 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: $k. 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" + strike=="true" && push!(fontdx, XML.Element(y; val="0")) + end + end + end + push!(new_dx, fontdx) + elseif k=="border" + if !isnothing(v) + all[y in ["color", "style"] for y in values(v)] && throw(XLSXError("Invalid border attribute: $k. 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 + 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") @@ -243,22 +365,55 @@ function getConditionalFormats(ws::Worksheet)::Vector{Pair{CellRange,Vector{Stri end """ - setConditionalFormat(ws::Worksheet, cr::String, type::Symbol; kw...) -> ::Int} + setConditionalFormat(ws::Worksheet, cr::String, type::Symbol; kw...) -> ::Int setConditionalFormat(xf::XLSXFile, cr::String, type::Symbol; kw...) -> ::Int - setConditionalFormat(ws::Worksheet, row, col, type::Symbol; kw...) -> ::Int} + setConditionalFormat(ws::Worksheet, rows, cols, type::Symbol; kw...) -> ::Int -Add a new conditional format to a worksheet. +Add a new conditional format to a worksheet. `cr` specifies CellRange, RowRange or +ColumnRange to apply the conditional format to or, if an `XLSXFile` is specified, +a SheetCellRange, SheetRowRange or SheetColumnRange. Alternatively, rows and columns +can be specified separately. !!! warning "In Develpment..." This function is still in development and may not work as expected. It is not yet implemented for all types of conditional formats. -Valid options for `type` are `:colorScale` (others in develpment) and these -determine which type of conditional formatting is being defined. +Valid options for `type` are (others in develpment): +- `:colorScale` +- `:cellIs` +- `:top10` +- `:aboveAverage` +- `:containsText` +- `:notContainsText` +- `:beginsWith` +- `:endsWith` +- `:timePeriod` +- `:containsErrors` +- `:notContainsErrors` +- `:containsBlanks` +- `:notContainsBlanks` +- `:uniqueValues` +- `:duplicateValues` + +The `type` keyword determines which type of conditional formatting is being defined. +Keyword options differ according to the `type` specified as set out below. + +!!! note "Ovrlaying conditional formats" + + Conditional formats are applied to a cell range and it is possible to apply multiple + conditional formats to the same range or to overlapping ranges. Each format is applied + in turn to each cell in priority order which, here, is the order in which they are + created. Different format options may complement or override each other and the + finished appearance will be the resuilt of all formats overlaying each other. -Keyword options differ according to the `type` specified + It is possible to terminate the sequential application of conditional formats to a + cell if the condition related to any format is met. This is achieved by setting the + keyword option `stopIfTrue=true` in the relevant conditional format. + + While the `stopIfTrue` keyword is available for most conditional formats, it is not + available for `:colorScale` conditional formats. # type = :colorScale @@ -292,7 +447,7 @@ instead using the following keywords: - `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 either a cell reference (e.g. `A1`) +The keywords `min_val`, `mid_val`, and `max_val` can be either a cell reference (e.g. `"A1"`) or a number. If a cell reference is used, it will be converted to an absolute cell reference when writing to an XLSXFile. @@ -326,35 +481,39 @@ julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:F22", :colorScale; ``` -# type = :cell +# type = :cellIs -Defines a conditional format based on the value of a cell. +Defines a conditional format based on the value of each cell in a range. Valid keywords are: -- `operator` : Defines the comparison to make. -- `value1` : 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. -- `dxStyle` : Used 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 `operator` defines the comparison to use in the conditiopnal formatting. +- `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` (>) -- `greaterEqual` (>=) -- `lessThan` (<) -- `lessEqual` (<=) -- `between` (requires `value2`) -- `notBetween` (requires `value2`) -- `equal` (==) -- `notEqual` (!=) - -The comparison is made against the value in `value1` and, if `operator` is either -`between` or `notBetween`, `value2` sets the other bound on the condition. If not specified, -`value1` 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. +- `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`) + +The comparison is made against `value` and, if `operator` is either `between` +or `notBetween`, `value2` sets the other bound on the condition. +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 two ways. Use the keyword `dxStyle` to select one of the built-in Excel formats. Valid options are: @@ -374,7 +533,7 @@ Valid attributes for each keyword are: - `fill` : `pattern`, `bgColor`, `fgColor` - `border` : `style`, `color` -Refer to [`setFormat()`](@ref), [`setFont)`](@ref), [`setFill`](@ref) and [`setBorder@ref) for +Refer to [`setFormat()`](@ref), [`setFont()`](@ref), [`setFill()`](@ref) and [`setBorder()](@ref) for more details on the valid attributes and values. !!! Note @@ -394,11 +553,11 @@ is `dxStyle="redfilltext"`. # Examples ```julia -julia> XLSX.setConditionalFormat(s, "B1:B5", :cell) # Defaults to `operator="greaterThan"`, `dxStyle`="redfilltext"` and `value1` set to the arithmetic agverage of cell values in `rng`. +julia> XLSX.setConditionalFormat(s, "B1:B5", :cell) # Defaults to `operator="greaterThan"`, `dxStyle`="redfilltext"` and `value` set to the arithmetic agverage of cell values in `rng`. julia> XLSX.setConditionalFormat(s, "B1:B5", :cell; operator="between", - value1="2", + value="2", value2="3", fill = ["pattern" => "none", "bgColor"=>"FFFFC7CE"], format = ["format"=>"0.00%"], @@ -407,7 +566,7 @@ julia> XLSX.setConditionalFormat(s, "B1:B5", :cell; julia> XLSX.setConditionalFormat(s, "B1:B5", :cell; operator="greaterThan", - value1="4", + value="4", fill = ["pattern" => "none", "bgColor"=>"green"], format = ["format"=>"0.0"], font = ["color"=>"red", "italic"=>"true"] @@ -415,7 +574,7 @@ julia> XLSX.setConditionalFormat(s, "B1:B5", :cell; julia> XLSX.setConditionalFormat(s, "B1:B5", :cell; operator="lessThan", - value1="2", + value="2", fill = ["pattern" => "none", "bgColor"=>"yellow"], format = ["format"=>"0.0"], font = ["color"=>"green"], @@ -424,31 +583,200 @@ julia> XLSX.setConditionalFormat(s, "B1:B5", :cell; ``` +# 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) + +The remaining keywords are defined as above for the `:cellIs` conditional format type. + +# Examples + +```julia +``` + +# 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) +- `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 the `:cellIs` conditional format type. + +# Examples + +```julia +``` + +# type = :containsText +# type = :notContainsText +# type = :beginsWith +# type = :endsWith + +Highlight cells in the range that contain (or do not contain), begin or end with +a specific text string. + +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. + +`value` gives the literal text to compare (eg. "Hello World") or provides a cell reference (e.g. `"A1"`). + +The remaining keywords are optional and are defined as above for the `:cellIs` conditional format type. + +# Examples + +```julia +``` + +# 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` +- `lastWeek` +- `thisWeek` +- `nextWeek` +- `lastMonth` +- `thisMonth` +- `nextMonth` + +The remaining keywords are defined as above for the `:cellIs` conditional format type. + +# Examples + +```julia +``` + +# type = :containsErrors +# type = :notContainsErrors +# type = :containsBlanks +# type = :notContainsBlanks +# type = :uniqueValues +# type = :duplicateValues + +These conditional formattimg options highlight cells that contain or don't contain errors, +are blank or not blank, are unique in the range or 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 +``` + + """ function setConditionalFormat(f, r, type::Symbol; kw...) if type == :colorScale setCfColorScale(f, r; kw...) - elseif type == :cell - setCfCell(f, r; kw...) + elseif type == :cellIs + setCfCellIs(f, r; kw...) + elseif type == :top10 + setCfTop10(f, r; kw...) + elseif type == :top10 + setCfAboveAverage(f, r; kw...) + elseif type == :timePeriod + setCfTimePeriod(f, r; kw...) + elseif type ∈ [:containsText, :notContainsText, :beginsWith, :endsWith] + setCfContainsText(f, r; operator=String(type), kw...) + elseif type ∈ [:containsBlanks, :notContainsBlanks, :containsErrors, :notContainsErrors, duplicateValues, uniqueValues] + setCfContainsBlankErrorUniqDup(f, r; operator=String(type), kw...) # elseif type == :iconSet # throw(XLSXError("Icon sets are not yet implemented.")) -# elseif type == :Cell -# throw(XLSXError("Cell conditional formats are not yet implemented.")) +# elseif type == :dataBar +# throw(XLSXError("Data bars are not yet implemented.")) else - throw(XLSXError("Invalid conditional format type: $type. Valid options are: :colorScale, :cell")) + throw(XLSXError("Invalid conditional format type: $type. Valid options are: `:colorScale`, `:cellIs`")) end end + function setConditionalFormat(f, r, c, type::Symbol; kw...) if type == :colorScale setCfColorScale(f, r, c; kw...) - elseif type == :cell - setCfCell(f, r, c; kw...) + elseif type == :cellIs + setCfCellIs(f, r, c; kw...) + elseif type == :top10 + setCfTop10(f, r, c; kw...) + elseif type == :top10 + setCfAboveAverage(f, r; kw...) + elseif type == :timePeriod + setCfTimePeriod(f, r, c; kw...) + elseif type ∈ [:containsText, :notContainsText, :beginsWith, :endsWith] + setCfContainsText(f, r, c; operator=String(type), kw...) + elseif type ∈ [:containsBlanks, :notContainsBlanks, :containsErrors, :notContainsErrors, duplicateValues, uniqueValues] + setCfContainsBlankErrorUniqDup(f, r, c; operator=String(type), kw...) # elseif type == :iconSet # throw(XLSXError("Icon sets are not yet implemented.")) -# elseif type == :Cell -# throw(XLSXError("Cell conditional formats are not yet implemented.")) +# elseif type == :dataBar +# throw(XLSXError("Data bars are not yet implemented.")) else - throw(XLSXError("Invalid conditional format type: $type. Valid options are: :colorScale, :cell.")) + throw(XLSXError("Invalid conditional format type: $type. Valid options are: `:colorScale`, `:cellIs`.")) end end setCfColorScale(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setCfColorScale, ws, row, nothing; kw...) @@ -477,72 +805,159 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; )::Int !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) - length(rng) <=1 && throw(XLSXError("Range `$rng` must have more than one cell.")) +# length(rng) <=1 && throw(XLSXError("Range `$rng` must have more than one cell.")) old_cf = getConditionalFormats(ws) for cf in old_cf - if intersects(cf.first, rng) - throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)`.")) + if cf.first != rng && intersects(cf.first, rng) + throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)` but is not the same. Must be the same as the existing range or entirely separate.")) end end - new_cf = XML.Element("conditionalFormatting"; sqref=rng) - if isnothing(colorscale) + !isnothing(min_val) && isnothing(tryparse(Float64, min_val)) && throw(XLSXError("Invalid `min_val`: $min_val. Must be a number.")) + !isnothing(mid_val) && isnothing(tryparse(Float64, mid_val)) && throw(XLSXError("Invalid `mid_val`: $mid_val. Must be a number.")) + !isnothing(max_val) && isnothing(tryparse(Float64, max_val)) && throw(XLSXError("Invalid `max_val`: $max_val. Must be a number.")) + + let new_pr=1, new_cf + allcfs = filter(x->x["sqref"]==string(rng), allCfs(ws)) # Match range with existing conditional formatting blocks. + if length(allcfs) == 0 # No existing conditional formatting blocks for this range so create a new one. + new_cf = XML.Element("conditionalFormatting"; sqref=rng) + new_pr = 1 + else # Existing conditional formatting block found for this range so add new rule to that block. + new_cf = allcfs[1] + new_pr = string(maximum([parse(Int, c["priority"]) for c in XML.children(new_cf)])+1) + end - min_type in ["min", "percentile", "percent", "num"] || throw(XLSXError("Invalid min_type: $min_type. Valid options are: min, percentile, percent, num.")) - isnothing(min_val) || is_valid_cellname(min_val) || !isnothing(tryparse(Float64,min_val)) || throw(XLSXError("Invalid mid_type: $min_val. Valid options are a CellRef (e.g. `A1`) or a number.")) - isnothing(mid_type) || mid_type in ["percentile", "percent", "num"] || throw(XLSXError("Invalid mid_type: $mid_type. Valid options are: percentile, percent, num.")) - isnothing(mid_val) || is_valid_cellname(mid_val) || !isnothing(tryparse(Float64,mid_val)) || throw(XLSXError("Invalid mid_type: $mid_val. Valid options are a CellRef (e.g. `A1`) or a number.")) - max_type in ["max", "percentile", "percent", "num"] || throw(XLSXError("Invalid max_type: $max_type. Valid options are: max, percentile, percent, num.")) - isnothing(max_val) || is_valid_cellname(max_val) || !isnothing(tryparse(Float64,max_val)) || throw(XLSXError("Invalid mid_type: $max_val. Valid options are a CellRef (e.g. `A1`) or a number.")) + if isnothing(colorscale) + + min_type in ["min", "percentile", "percent", "num"] || throw(XLSXError("Invalid min_type: $min_type. Valid options are: min, percentile, percent, num.")) + isnothing(min_val) || is_valid_cellname(min_val) || !isnothing(tryparse(Float64,min_val)) || throw(XLSXError("Invalid mid_type: $min_val. Valid options are a CellRef (e.g. `A1`) or a number.")) + isnothing(mid_type) || mid_type in ["percentile", "percent", "num"] || throw(XLSXError("Invalid mid_type: $mid_type. Valid options are: percentile, percent, num.")) + isnothing(mid_val) || is_valid_cellname(mid_val) || !isnothing(tryparse(Float64,mid_val)) || throw(XLSXError("Invalid mid_type: $mid_val. Valid options are a CellRef (e.g. `A1`) or a number.")) + max_type in ["max", "percentile", "percent", "num"] || throw(XLSXError("Invalid max_type: $max_type. Valid options are: max, percentile, percent, num.")) + isnothing(max_val) || is_valid_cellname(max_val) || !isnothing(tryparse(Float64,max_val)) || throw(XLSXError("Invalid mid_type: $max_val. Valid options are a CellRef (e.g. `A1`) or a number.")) + + min_val = convertref(min_val) + mid_val = convertref(mid_val) + max_val = convertref(max_val) + + push!(new_cf, 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) ? nothing : 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) ? nothing : XML.h.color(rgb=get_color(mid_col)), + XML.h.color(rgb=get_color(max_col)) + ) + )) - - min_val = convertref(min_val) - mid_val = convertref(mid_val) - max_val = convertref(max_val) - - push!(new_cf, XML.h.cfRule(type="colorScale", priority="1", - XML.h.colorScale( - isnothing(min_val) ? XML.h.cfvo(type=min_type) : XML.h.cfvo(type=min_type, val=min_val), - isnothing(mid_type) ? nothing : 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) ? nothing : XML.h.color(rgb=get_color(mid_col)), - XML.h.color(rgb=get_color(max_col)) - ) - )) + else + if !haskey(colorscales, colorscale) + throw(XLSXError("Invalid color scale: $colorScale. Valid options are: $(keys(colorscales)).")) + end +# new_cf = XML.Element("conditionalFormatting"; sqref=rng) + new_dx=colorscales[colorscale] + new_dx["priority"] = new_pr + push!(new_cf, new_dx) + end - else - if !haskey(colorscales, colorscale) - throw(XLSXError("Invalid color scale: $colorScale. Valid options are: $(keys(colorscales)).")) + add_cf_to_XML(ws, new_cf) # Insert the new conditional formatting into the worksheet XML + end + + update_worksheets_xml!(get_xlsxfile(ws)) + + return 0 +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, 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; + 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 +)::Int + + !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) +# length(rng) <=1 && throw(XLSXError("Range `$rng` must have more than one cell.")) + + old_cf = getConditionalFormats(ws) + for cf in old_cf + if cf.first != rng && intersects(cf.first, rng) + throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)` but is not the same. Must be the same as the existing range or entirely separate.")) end + end + + !isnothing(value) && isnothing(tryparse(Float64, value)) && throw(XLSXError("Invalid `value`: $value. Must be a number.")) + !isnothing(value2) && isnothing(tryparse(Float64, value2)) && throw(XLSXError("Invalid `value2`: $value2. Must be a number.")) + + 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), priority="1", operator=operator) + if !isnothing(stopIfTrue) && stopIfTrue == "true" + cfx["stopIfTrue"] = "1" + end + push!(cfx, XML.Element("formula", XML.Text(value))) + if !isnothing(value2) && operator ∈ needsValue2 + push!(cfx, XML.Element("formula", XML.Text(value2))) + end + + allcfs = filter(x->x["sqref"]==string(rng), allCfs(ws)) # Match range with existing conditional formatting blocks. + if length(allcfs) == 0 # No existing conditional formatting blocks for this range so create a new one. new_cf = XML.Element("conditionalFormatting"; sqref=rng) - push!(new_cf, colorscales[colorscale]) + else # Existing conditional formatting block found for this range so add new rule to that block. +# children=XML.children(allcfs[1]) + cfx["priority"] = string(maximum([parse(Int, c["priority"]) for c in XML.children(allcfs[1])])+1) + new_cf = allcfs[1] end - add_cf_to_XML(ws, new_cf) # Insert the new conditional formatting into the worksheet XML + push!(new_cf, cfx) + + add_cf_to_XML(ws, new_cf) # Add the new conditional formatting to the worksheet XML. update_worksheets_xml!(get_xlsxfile(ws)) return 0 end -setCfCell(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setCfCell, ws, row, nothing; kw...) -setCfCell(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; kw...) = process_colon(setCfCell, ws, nothing, col; kw...) -setCfCell(ws::Worksheet, ::Colon, ::Colon; kw...) = process_colon(setCfCell, ws, nothing, nothing; kw...) -setCfCell(ws::Worksheet, ::Colon; kw...) = process_colon(setCfCell, ws, nothing, nothing; kw...) -setCfCell(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Integer,UnitRange{<:Integer}}; kw...) = setCfCell(ws, CellRange(CellRef(first(row), first(col)), CellRef(last(row), last(col))); kw...) -setCfCell(ws::Worksheet, rng::SheetCellRange; kw...) = do_sheet_names_match(ws, rng) && setCfCell(ws, rng.rng; kw...) -setCfCell(ws::Worksheet, rng::SheetColumnRange; kw...) = do_sheet_names_match(ws, rng) && setCfCell(ws, rng.colrng; kw...) -setCfCell(ws::Worksheet, rng::SheetRowRange; kw...) = do_sheet_names_match(ws, rng) && setCfCell(ws, rng.rowrng; kw...) -setCfCell(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCfCell, ws, rng; kw...) -setCfCell(ws::Worksheet, rng::ColumnRange; kw...) = process_columnranges(setCfCell, ws, rng; kw...) -setCfCell(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process_sheetcell(setCfCell, xl, sheetcell; kw...) -setCfCell(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setCfCell, ws, ref_or_rng; kw...) -function setCfCell(ws::Worksheet, rng::CellRange; - operator::Union{Nothing,String}="greaterThan", - value1::Union{Nothing,String}=nothing, - value2::Union{Nothing,String}=nothing, +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, 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; + operator::Union{Nothing,String}="containsText", + value::Union{Nothing,String}="", + 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, @@ -551,7 +966,7 @@ function setCfCell(ws::Worksheet, rng::CellRange; )::Int !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) - length(rng) <=1 && throw(XLSXError("Range `$rng` must have more than one cell.")) +# length(rng) <=1 && throw(XLSXError("Range `$rng` must have more than one cell.")) old_cf = getConditionalFormats(ws) for cf in old_cf @@ -559,126 +974,370 @@ function setCfCell(ws::Worksheet, rng::CellRange; throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)` but is not the same. Must be the same as the existing range or entirely separate.")) end end + wb=get_workbook(ws) - 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 + dx = get_dx(dxStyle, format, font, border, fill) + new_dx= get_new_dx(wb, dx) + dxid = Add_Cf_Dx(wb, new_dx) + + 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: $operator. Valid options are: `containsText`, `notContainsText`, `beginsWith`, `endsWith`.")) + end + formula = replace(formula, "__txt__" => value, "__CR__" => string(first(rng))) + + cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", operator=operator, text=value) + if !isnothing(stopIfTrue) && stopIfTrue == "true" + cfx["stopIfTrue"] = "1" + end + push!(cfx, XML.Element("formula", XML.Text(formula))) + allcfs = filter(x->x["sqref"]==string(rng), allCfs(ws)) # Match range with existing conditional formatting blocks. + l = length(allcfs) + if l == 0 # No existing conditional formatting blocks for this range so create a new one. + new_cf = XML.Element("conditionalFormatting"; sqref=rng) + elseif l==1 # Existing conditional formatting block found for this range so add new rule to that block. + cfx["priority"] = string(maximum([parse(Int, c["priority"]) for c in XML.children(allcfs[1])])+1) + new_cf = allcfs[1] + else + throw(XLSXError("Too many conditional formatting blocks for range `$rng`. Must be one or none, found `$l`.")) + end + + push!(new_cf, cfx) + + add_cf_to_XML(ws, new_cf) # Add the new conditional formatting to the worksheet XML. + + update_worksheets_xml!(get_xlsxfile(ws)) + + 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, 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; + 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 +)::Int + + !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) +# length(rng) <=1 && throw(XLSXError("Range `$rng` must have more than one cell.")) + + old_cf = getConditionalFormats(ws) + for cf in old_cf + if cf.first != rng && intersects(cf.first, rng) + throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)` but is not the same. Must be the same as the existing range or entirely separate.")) end - elseif haskey(highlights, dxStyle) - dx = highlights[dxStyle] + end + + isnothing(tryparse(Float64, value)) && throw(XLSXError("Invalid `value`: $value. Must be a number.")) + + 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 == "topN" + cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", rank=value) + elseif operator == "topN%" + cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", percent="1", rank=value) + elseif operator == "bottomN" + cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", bottom="1", rank=value) + elseif operator == "bottomN%" + cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", percent="1", bottom="1", rank=value) else - throw(XLSXError("Invalid dxStyle: $dxStyle. Valid options are: $(keys(highlights)).")) + throw(XLSXError("Invalid operator: $operator. Valid options are: `topN`, `topN%`, `bottomN`, `bottomN%`.")) end - new_dx = XML.Element("dxf") - for k in ["font", "format", "fill", "border"] # Order is 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 - 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 - 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" - strike=="true" && push!(fontdx, XML.Element(y; val="0")) - end - end - end - push!(new_dx, fontdx) - elseif k=="border" - if !isnothing(v) - 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 + + if !isnothing(stopIfTrue) && stopIfTrue == "true" + cfx["stopIfTrue"] = "1" + end + + allcfs = filter(x->x["sqref"]==string(rng), allCfs(ws)) # Match range with existing conditional formatting blocks. + l = length(allcfs) + if l == 0 # No existing conditional formatting blocks for this range so create a new one. + new_cf = XML.Element("conditionalFormatting"; sqref=rng) + elseif l==1 # Existing conditional formatting block found for this range so add new rule to that block. + cfx["priority"] = string(maximum([parse(Int, c["priority"]) for c in XML.children(allcfs[1])])+1) + new_cf = allcfs[1] + else + throw(XLSXError("Too many conditional formatting blocks for range `$rng`. Must be one or none, found `$l`.")) + end + + push!(new_cf, cfx) + + add_cf_to_XML(ws, new_cf) # Add the new conditional formatting to the worksheet XML. + + update_worksheets_xml!(get_xlsxfile(ws)) + + 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, 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; + 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 +)::Int + + !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) +# length(rng) <=1 && throw(XLSXError("Range `$rng` must have more than one cell.")) + + old_cf = getConditionalFormats(ws) + for cf in old_cf + if cf.first != rng && intersects(cf.first, rng) + throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)` but is not the same. Must be the same as the existing range or entirely separate.")) end - end - dxid = Add_Cf_Dx(get_workbook(ws), new_dx) - if isnothing(value1) - value1 = all(ismissing.(ws[rng])) ? nothing : string(sum(skipmissing(ws[rng]))/count(!ismissing, ws[rng])) + isnothing(tryparse(Float64, value)) && throw(XLSXError("Invalid `value`: $value. Must be a number.")) + + 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=operator, dxfId=Int(dxid.id), priority="1", equalAverage="1") + elseif operator == "plus1StdDev" + cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", bottom="1", stdDev="1") + elseif operator == "plus2StdDev" + cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", percent="1", stdDev="2") + elseif operator == "plus3StdDev" + cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", percent="1", stdDev="3") + elseif operator == "belowAverage" + cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", aboveAverage="0", ) + elseif operator == "belowEqAverage" + cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", aboveAverage="0", equalAverage="1") + elseif operator == "minus1StdDev" + cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", aboveAverage="0", stdDev="1") + elseif operator == "minus2StdDev" + cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", aboveAverage="0", stdDev="2") + elseif operator == "minus3StdDev" + cfx = XML.Element("cfRule"; type=operator, 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 = XML.Element("cfRule"; type="cellIs", dxfId=Int(dxid.id), priority="1", operator=operator) - if !isnothing(value1) - push!(cfx, XML.Element("formula", XML.Text(value1))) + + if !isnothing(stopIfTrue) && stopIfTrue == "true" + cfx["stopIfTrue"] = "1" end - if !isnothing(value2) - push!(cfx, XML.Element("formula", XML.Text(value2))) + + allcfs = filter(x->x["sqref"]==string(rng), allCfs(ws)) # Match range with existing conditional formatting blocks. + l = length(allcfs) + if l == 0 # No existing conditional formatting blocks for this range so create a new one. + new_cf = XML.Element("conditionalFormatting"; sqref=rng) + elseif l==1 # Existing conditional formatting block found for this range so add new rule to that block. + cfx["priority"] = string(maximum([parse(Int, c["priority"]) for c in XML.children(allcfs[1])])+1) + new_cf = allcfs[1] + else + throw(XLSXError("Too many conditional formatting blocks for range `$rng`. Must be one or none, found `$l`.")) end + push!(new_cf, cfx) + + add_cf_to_XML(ws, new_cf) # Add the new conditional formatting to the worksheet XML. + + update_worksheets_xml!(get_xlsxfile(ws)) + + 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, 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; + 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 +)::Int + + !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) +# length(rng) <=1 && throw(XLSXError("Range `$rng` must have more than one cell.")) + + old_cf = getConditionalFormats(ws) + for cf in old_cf + if cf.first != rng && intersects(cf.first, rng) + throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)` but is not the same. Must be the same as the existing range or entirely separate.")) + end + end + + 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), priority="1", operator=operator) + if !isnothing(stopIfTrue) && stopIfTrue == "true" + cfx["stopIfTrue"] = "1" + end + push!(cfx, XML.Element("formula", XML.Text(formula))) + allcfs = filter(x->x["sqref"]==string(rng), allCfs(ws)) # Match range with existing conditional formatting blocks. if length(allcfs) == 0 # No existing conditional formatting blocks for this range so create a new one. new_cf = XML.Element("conditionalFormatting"; sqref=rng) - else # Existing conditional formatting block found for this range so add new rule to that. - children=XML.children(allcfs[1]) - cfx["priority"] = string(maximum([parse(Int, c["priority"]) for c in children])+1) + else # Existing conditional formatting block found for this range so add new rule to that block. +# children=XML.children(allcfs[1]) + cfx["priority"] = string(maximum([parse(Int, c["priority"]) for c in XML.children(allcfs[1])])+1) new_cf = allcfs[1] end + push!(new_cf, cfx) + + add_cf_to_XML(ws, new_cf) # Add the new conditional formatting to the worksheet XML. + + update_worksheets_xml!(get_xlsxfile(ws)) + + 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, 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; + operator::Union{Nothing,String}="containsBlank", + 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 +)::Int + + !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) +# length(rng) <=1 && throw(XLSXError("Range `$rng` must have more than one cell.")) + + old_cf = getConditionalFormats(ws) + for cf in old_cf + if cf.first != rng && intersects(cf.first, rng) + throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)` but is not the same. Must be the same as the existing range or entirely separate.")) + end + end + + 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 = "" + else + throw(XLSXError("Invalid operator: $operator. Valid options are: `containsBlanks`, `notContainsBlanks`, `containsErrors`, `notContainsErrors`, `uniqueValues`, `duplicateValues`.")) + 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), priority="1") + if !isnothing(stopIfTrue) && stopIfTrue == "true" + cfx["stopIfTrue"] = "1" + end + formula !="" && push!(cfx, XML.Element("formula", XML.Text(formula))) + + allcfs = filter(x->x["sqref"]==string(rng), allCfs(ws)) # Match range with existing conditional formatting blocks. + if length(allcfs) == 0 # No existing conditional formatting blocks for this range so create a new one. + new_cf = XML.Element("conditionalFormatting"; sqref=rng) + else # Existing conditional formatting block found for this range so add new rule to that block. +# children=XML.children(allcfs[1]) + cfx["priority"] = string(maximum([parse(Int, c["priority"]) for c in XML.children(allcfs[1])])+1) + new_cf = allcfs[1] + end push!(new_cf, cfx) @@ -687,4 +1346,24 @@ function setCfCell(ws::Worksheet, rng::CellRange; update_worksheets_xml!(get_xlsxfile(ws)) return 0 -end \ No newline at end of file +end + +""" + + +NOT(ISERROR(D1)) + + +LEN(TRIM(D1))=0 + + + +ISERROR(D1) + + +LEN(TRIM(D1))=0 + + + + +""" \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index eced0fe4..5f41e53b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3247,7 +3247,7 @@ end @testset "Conditional Formats" begin - @testset "colorScale" begin + @testset "ColorScale" begin f=XLSX.newxlsx() s=f[1] for i in 1:5, j in 1:5 @@ -3255,8 +3255,8 @@ end end @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, "A1", :colorScale) # Single cell not allowed - @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "A1:A1", :colorScale) # One cell cellrange not allowed +# @test_throws MethodError XLSX.setConditionalFormat(s, "A1", :colorScale) # Single cell not allowed +# @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "A1:A1", :colorScale) # One cell cellrange not allowed XLSX.setConditionalFormat(s, "1:1", :colorScale) XLSX.setConditionalFormat(s, 2, :, :colorScale; colorscale = "redwhiteblue") XLSX.setConditionalFormat(s, 3, 1:5, :colorScale; @@ -3281,7 +3281,7 @@ end max_col="darkgreen" ) @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("A5:E5") => ["colorScale"],XLSX.CellRange("A4:E4") => ["colorScale"],XLSX.CellRange("A3:E3") => ["colorScale"],XLSX.CellRange("A2:E2") => ["colorScale"],XLSX.CellRange("A1:E1") => ["colorScale"]] - @test_throws MethodError XLSX.setConditionalFormat(s, "A1", :colorScale) # Single cell not allowed +# @test_throws MethodError XLSX.setConditionalFormat(s, "A1", :colorScale) # Single cell not allowed @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "Sheet1!A1:A2", :colorScale) # Overlaps with existing conditionalFormat range @test_throws XLSX.XLSXError XLSX.setConditionalFormat(f, "Sheet1!1:2", :colorScale) # Overlaps with existing conditionalFormat range @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, :, :colorScale) # Overlaps with existing conditionalFormat range From 78af273b5ef3d1f36085c01e2379c5340eec3591 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 2 May 2025 19:45:13 +0100 Subject: [PATCH 104/154] Minor revisions to docs. --- docs/src/formatting.md | 11 ++-- docs/src/images/{cell.png => cell1.png} | Bin docs/src/images/cell2.png | Bin 0 -> 24450 bytes src/cellformats.jl | 2 +- src/conditional-formats.jl | 67 ++++++++---------------- 5 files changed, 27 insertions(+), 53 deletions(-) rename docs/src/images/{cell.png => cell1.png} (100%) create mode 100644 docs/src/images/cell2.png diff --git a/docs/src/formatting.md b/docs/src/formatting.md index cc0e7600..583807ee 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -174,7 +174,7 @@ julia> XLSX.setUniformStyle(s, "A1:CV100") # set all formatting attributes to be ### Illustrating the different approaches To illustrate the differences between applying `setAttribute`, `setUniformAttribute` and `setUinformStyle`, -consider the following worksheet, whice has very hetrogeneous formatting across the three cells: +consider the following worksheet, which has very hetrogeneous formatting across the three cells: ![image|320x500](./images/multicell.png) @@ -289,7 +289,7 @@ 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 has the border format I want! +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 @@ -442,15 +442,14 @@ julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:F22", :colorScale; ``` ![image|320x500](./images/custom-colorscale.png) -### Cell Value +#### Cell Value It is possible to apply a conditional format to a range of cells that applies when a cell's value meets a specified condition using the `:cell` type. -![image|320x500](./images/cell.png) +![image|320x500](./images/cell1.png) ![image|320x500](./images/cell2.png) -In Excel there are twelve built-in color scales available, but it is possible to create -custom color scales, too. +All the functions of `Highlight Cell Rules` and `Top/Bottom Rules` are provided. ## Working with Merged Cells diff --git a/docs/src/images/cell.png b/docs/src/images/cell1.png similarity index 100% rename from docs/src/images/cell.png rename to docs/src/images/cell1.png diff --git a/docs/src/images/cell2.png b/docs/src/images/cell2.png new file mode 100644 index 0000000000000000000000000000000000000000..7d35ea4b6fb1e07848348d6b07e3d64f861073c9 GIT binary patch literal 24450 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%0DL#U!KU9~RZA#qmzoMfdSmG-K3l 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 literal 0 HcmV?d00001 diff --git a/src/cellformats.jl b/src/cellformats.jl index a339c02f..a85d02f8 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -539,7 +539,7 @@ Allowed values for `style` are: - `slantDashDot` The `color` attribute can be set by specifying an 8-digit hexadecimal value -in the format "AARRGGBB". The transparency ("AA") is ignored by Excel but +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/)). diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index 1313a155..f22e8402 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -370,10 +370,9 @@ end setConditionalFormat(ws::Worksheet, rows, cols, type::Symbol; kw...) -> ::Int -Add a new conditional format to a worksheet. `cr` specifies CellRange, RowRange or -ColumnRange to apply the conditional format to or, if an `XLSXFile` is specified, -a SheetCellRange, SheetRowRange or SheetColumnRange. Alternatively, rows and columns -can be specified separately. +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 can be specified separately. !!! warning "In Develpment..." @@ -397,8 +396,8 @@ Valid options for `type` are (others in develpment): - `:uniqueValues` - `:duplicateValues` -The `type` keyword determines which type of conditional formatting is being defined. -Keyword options differ according to the `type` specified as set out below. +The `type` argument determines which type of conditional formatting is being defined. +Keyword options differ according to the `type` specified, as set out below. !!! note "Ovrlaying conditional formats" @@ -499,7 +498,9 @@ Valid keywords are: 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: +If the condition is met, the format is applied. +Valid options are: + - `greaterThan` (cell > `value`) - `greaterEqual` (cell >= `value`) - `lessThan` (cell < `value`) @@ -509,14 +510,14 @@ If the condition is met, the format is applied. Valid options are: - `between` (cell between `value` and `value2`) - `notBetween` (cell not between `value` and `value2`) -The comparison is made against `value` and, if `operator` is either `between` -or `notBetween`, `value2` sets the other bound on the condition. 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 two ways. Use the keyword -`dxStyle` to select one of the built-in Excel formats. Valid options are: +`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) @@ -528,6 +529,7 @@ Alternatively, you can define a custom format by using the keywords `format`, `f `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` @@ -536,7 +538,7 @@ Valid attributes for each keyword are: Refer to [`setFormat()`](@ref), [`setFont()`](@ref), [`setFill()`](@ref) and [`setBorder()](@ref) for more details on the valid attributes and values. -!!! Note +!!! 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 nor is it possible to set @@ -588,7 +590,8 @@ julia> XLSX.setConditionalFormat(s, "B1:B5", :cell; 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: +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. @@ -599,6 +602,7 @@ The available keywords are: - `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) @@ -616,7 +620,8 @@ The remaining keywords are defined as above for the `:cellIs` conditional format This conditional format can be used to compare cell values in the range with the average value for the range. -The available keywords are: +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. @@ -739,7 +744,7 @@ function setConditionalFormat(f, r, type::Symbol; kw...) setCfCellIs(f, r; kw...) elseif type == :top10 setCfTop10(f, r; kw...) - elseif type == :top10 + elseif type == :aboveAverage setCfAboveAverage(f, r; kw...) elseif type == :timePeriod setCfTimePeriod(f, r; kw...) @@ -763,7 +768,7 @@ function setConditionalFormat(f, r, c, type::Symbol; kw...) setCfCellIs(f, r, c; kw...) elseif type == :top10 setCfTop10(f, r, c; kw...) - elseif type == :top10 + elseif type == :aboveAverage setCfAboveAverage(f, r; kw...) elseif type == :timePeriod setCfTimePeriod(f, r, c; kw...) @@ -805,7 +810,6 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; )::Int !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) -# length(rng) <=1 && throw(XLSXError("Range `$rng` must have more than one cell.")) old_cf = getConditionalFormats(ws) for cf in old_cf @@ -856,7 +860,6 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; if !haskey(colorscales, colorscale) throw(XLSXError("Invalid color scale: $colorScale. Valid options are: $(keys(colorscales)).")) end -# new_cf = XML.Element("conditionalFormatting"; sqref=rng) new_dx=colorscales[colorscale] new_dx["priority"] = new_pr push!(new_cf, new_dx) @@ -895,7 +898,6 @@ function setCfCellIs(ws::Worksheet, rng::CellRange; )::Int !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) -# length(rng) <=1 && throw(XLSXError("Range `$rng` must have more than one cell.")) old_cf = getConditionalFormats(ws) for cf in old_cf @@ -928,7 +930,6 @@ function setCfCellIs(ws::Worksheet, rng::CellRange; if length(allcfs) == 0 # No existing conditional formatting blocks for this range so create a new one. new_cf = XML.Element("conditionalFormatting"; sqref=rng) else # Existing conditional formatting block found for this range so add new rule to that block. -# children=XML.children(allcfs[1]) cfx["priority"] = string(maximum([parse(Int, c["priority"]) for c in XML.children(allcfs[1])])+1) new_cf = allcfs[1] end @@ -966,7 +967,6 @@ function setCfContainsText(ws::Worksheet, rng::CellRange; )::Int !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) -# length(rng) <=1 && throw(XLSXError("Range `$rng` must have more than one cell.")) old_cf = getConditionalFormats(ws) for cf in old_cf @@ -1045,7 +1045,6 @@ function setCfTop10(ws::Worksheet, rng::CellRange; )::Int !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) -# length(rng) <=1 && throw(XLSXError("Range `$rng` must have more than one cell.")) old_cf = getConditionalFormats(ws) for cf in old_cf @@ -1120,7 +1119,6 @@ function setCfAboveAverage(ws::Worksheet, rng::CellRange; )::Int !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) -# length(rng) <=1 && throw(XLSXError("Range `$rng` must have more than one cell.")) old_cf = getConditionalFormats(ws) for cf in old_cf @@ -1256,7 +1254,6 @@ function setCfTimePeriod(ws::Worksheet, rng::CellRange; if length(allcfs) == 0 # No existing conditional formatting blocks for this range so create a new one. new_cf = XML.Element("conditionalFormatting"; sqref=rng) else # Existing conditional formatting block found for this range so add new rule to that block. -# children=XML.children(allcfs[1]) cfx["priority"] = string(maximum([parse(Int, c["priority"]) for c in XML.children(allcfs[1])])+1) new_cf = allcfs[1] end @@ -1293,7 +1290,6 @@ function setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::CellRange; )::Int !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) -# length(rng) <=1 && throw(XLSXError("Range `$rng` must have more than one cell.")) old_cf = getConditionalFormats(ws) for cf in old_cf @@ -1334,7 +1330,6 @@ function setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::CellRange; if length(allcfs) == 0 # No existing conditional formatting blocks for this range so create a new one. new_cf = XML.Element("conditionalFormatting"; sqref=rng) else # Existing conditional formatting block found for this range so add new rule to that block. -# children=XML.children(allcfs[1]) cfx["priority"] = string(maximum([parse(Int, c["priority"]) for c in XML.children(allcfs[1])])+1) new_cf = allcfs[1] end @@ -1346,24 +1341,4 @@ function setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::CellRange; update_worksheets_xml!(get_xlsxfile(ws)) return 0 -end - -""" - - -NOT(ISERROR(D1)) - - -LEN(TRIM(D1))=0 - - - -ISERROR(D1) - - -LEN(TRIM(D1))=0 - - - - -""" \ No newline at end of file +end \ No newline at end of file From a710c756c57acb151f5c193dbe6ecb28f16b1a78 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 5 May 2025 23:08:14 +0100 Subject: [PATCH 105/154] Unify priority to allow overlapping conditional formats --- src/conditional-formats.jl | 253 +++++++++++-------------------------- test/runtests.jl | 12 +- 2 files changed, 83 insertions(+), 182 deletions(-) diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index f22e8402..4eee3209 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -180,7 +180,7 @@ function get_new_dx(wb::Workbook, dx::Dict{String,Dict{String, String}})::XML.No 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`.")) + 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" @@ -194,7 +194,7 @@ function get_new_dx(wb::Workbook, dx::Dict{String,Dict{String, String}})::XML.No if !isnothing(v) fontdx=XML.Element("font") for (y, z) in v - y in ["color", "bold", "italic", "under", "strike"] && throw(XLSXError("Invalid font attribute: $k. Valid options are: `color`, `bold`, `italic`, `under`, `strike`.")) + 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" @@ -211,7 +211,7 @@ function get_new_dx(wb::Workbook, dx::Dict{String,Dict{String, String}})::XML.No push!(new_dx, fontdx) elseif k=="border" if !isnothing(v) - all[y in ["color", "style"] for y in values(v)] && throw(XLSXError("Invalid border attribute: $k. Valid options are: `color`, `style`.")) + 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 @@ -271,6 +271,21 @@ function add_cf_to_XML(ws, new_cf) # Add a new conditional formatting to the wor 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 + 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) @@ -332,6 +347,11 @@ function convertref(c) return c end +function allCfs(ws::Worksheet) + 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 + """ Get the conditional formats for a worksheet. @@ -339,27 +359,19 @@ Get the conditional formats for a worksheet. # Arguments - `ws::Worksheet`: The worksheet to get the conditional formats for. -Return a vector of pairs: CellRange => Vector{String}, where String is the -type of the conditional format applies. +Return a vector of pairs: CellRange => NamedTuple{type::String, priority::Int}}. """ -function allCfs(ws::Worksheet) - 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 getConditionalFormats(ws::Worksheet)::Vector{Pair{CellRange,Vector{String}}} - allcfnodes = allCfs(ws::Worksheet) - allcfs = Vector{Pair{CellRange,Vector{String}}}() - for (i, cf) in enumerate(allcfnodes) - cf_types = Vector{String}() +getConditionalFormats(ws::Worksheet) = getConditionalFormats(allCfs(ws)) +function getConditionalFormats(allcfnodes::Vector{XML.Node})::Vector{Pair{CellRange,NamedTuple}} + allcfs = Vector{Pair{CellRange,NamedTuple}}() + for cf in allcfnodes for child in XML.children(cf) if XML.tag(child) == "cfRule" - push!(cf_types, child["type"]) + push!(allcfs, CellRange(cf["sqref"]) => (; type=child["type"], priority=parse(Int, child["priority"]))) end end - push!(allcfs, CellRange(cf["sqref"]) => cf_types) end return allcfs end @@ -811,41 +823,27 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) - old_cf = getConditionalFormats(ws) - for cf in old_cf - if cf.first != rng && intersects(cf.first, rng) - throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)` but is not the same. Must be the same as the existing range or entirely separate.")) - end - end + allcfs = allCfs(ws) # get all conditional format blocks + old_cf = getConditionalFormats(allcfs) # extract conditional format info - !isnothing(min_val) && isnothing(tryparse(Float64, min_val)) && throw(XLSXError("Invalid `min_val`: $min_val. Must be a number.")) - !isnothing(mid_val) && isnothing(tryparse(Float64, mid_val)) && throw(XLSXError("Invalid `mid_val`: $mid_val. Must be a number.")) - !isnothing(max_val) && isnothing(tryparse(Float64, max_val)) && throw(XLSXError("Invalid `max_val`: $max_val. Must be a number.")) - - let new_pr=1, new_cf - allcfs = filter(x->x["sqref"]==string(rng), allCfs(ws)) # Match range with existing conditional formatting blocks. - if length(allcfs) == 0 # No existing conditional formatting blocks for this range so create a new one. - new_cf = XML.Element("conditionalFormatting"; sqref=rng) - new_pr = 1 - else # Existing conditional formatting block found for this range so add new rule to that block. - new_cf = allcfs[1] - new_pr = string(maximum([parse(Int, c["priority"]) for c in XML.children(new_cf)])+1) - end + 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"] || throw(XLSXError("Invalid min_type: $min_type. Valid options are: min, percentile, percent, num.")) - isnothing(min_val) || is_valid_cellname(min_val) || !isnothing(tryparse(Float64,min_val)) || throw(XLSXError("Invalid mid_type: $min_val. Valid options are a CellRef (e.g. `A1`) or a number.")) + isnothing(min_val) || is_valid_cellname(min_val) || !is_valid_sheet_cellname(min_val) || !isnothing(tryparse(Float64,min_val)) || throw(XLSXError("Invalid mid_type: $min_val. Valid options are a CellRef (e.g. `A1`) or a number.")) isnothing(mid_type) || mid_type in ["percentile", "percent", "num"] || throw(XLSXError("Invalid mid_type: $mid_type. Valid options are: percentile, percent, num.")) - isnothing(mid_val) || is_valid_cellname(mid_val) || !isnothing(tryparse(Float64,mid_val)) || throw(XLSXError("Invalid mid_type: $mid_val. Valid options are a CellRef (e.g. `A1`) or a number.")) + isnothing(mid_val) || is_valid_cellname(mid_val) || !is_valid_sheet_cellname(mid_val) || !isnothing(tryparse(Float64,mid_val)) || throw(XLSXError("Invalid mid_type: $mid_val. Valid options are a CellRef (e.g. `A1`) or a number.")) max_type in ["max", "percentile", "percent", "num"] || throw(XLSXError("Invalid max_type: $max_type. Valid options are: max, percentile, percent, num.")) - isnothing(max_val) || is_valid_cellname(max_val) || !isnothing(tryparse(Float64,max_val)) || throw(XLSXError("Invalid mid_type: $max_val. Valid options are a CellRef (e.g. `A1`) or a number.")) + isnothing(max_val) || is_valid_cellname(max_val) || !is_valid_sheet_cellname(max_val) || !isnothing(tryparse(Float64,max_val)) || throw(XLSXError("Invalid mid_type: $max_val. Valid options are a CellRef (e.g. `A1`) or a number.")) min_val = convertref(min_val) mid_val = convertref(mid_val) max_val = convertref(max_val) - push!(new_cf, XML.h.cfRule(type="colorScale", priority=new_pr, + 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) ? nothing : XML.h.cfvo(type=mid_type, val=mid_val), @@ -854,21 +852,19 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; isnothing(mid_type) ? nothing : XML.h.color(rgb=get_color(mid_col)), XML.h.color(rgb=get_color(max_col)) ) - )) + ) else if !haskey(colorscales, colorscale) throw(XLSXError("Invalid color scale: $colorScale. Valid options are: $(keys(colorscales)).")) end - new_dx=colorscales[colorscale] - new_dx["priority"] = new_pr - push!(new_cf, new_dx) + cfx=colorscales[colorscale] + cfx["priority"] = new_pr end - add_cf_to_XML(ws, new_cf) # Insert the new conditional formatting into the worksheet XML - end + update_worksheet_cfx!(allcfs, cfx, ws, rng) - update_worksheets_xml!(get_xlsxfile(ws)) + end return 0 end @@ -899,15 +895,11 @@ function setCfCellIs(ws::Worksheet, rng::CellRange; !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) - old_cf = getConditionalFormats(ws) - for cf in old_cf - if cf.first != rng && intersects(cf.first, rng) - throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)` but is not the same. Must be the same as the existing range or entirely separate.")) - end - end + allcfs = allCfs(ws) # get all conditional format blocks + old_cf = getConditionalFormats(allcfs) # extract conditional format info - !isnothing(value) && isnothing(tryparse(Float64, value)) && throw(XLSXError("Invalid `value`: $value. Must be a number.")) - !isnothing(value2) && isnothing(tryparse(Float64, value2)) && throw(XLSXError("Invalid `value2`: $value2. Must be a number.")) + !isnothing(value) && !is_valid_cellname(value) && !is_valid_sheet_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_sheet_cellname(value2) && isnothing(tryparse(Float64, value2)) && throw(XLSXError("Invalid `value2`: $value2. Must be a number or a CellRef.")) wb=get_workbook(ws) dx = get_dx(dxStyle, format, font, border, fill) @@ -917,28 +909,18 @@ function setCfCellIs(ws::Worksheet, rng::CellRange; 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), priority="1", operator=operator) + cfx = XML.Element("cfRule"; type="cellIs", dxfId=Int(dxid.id), operator=operator) if !isnothing(stopIfTrue) && stopIfTrue == "true" cfx["stopIfTrue"] = "1" end + cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)])+1) : 1 + push!(cfx, XML.Element("formula", XML.Text(value))) if !isnothing(value2) && operator ∈ needsValue2 push!(cfx, XML.Element("formula", XML.Text(value2))) end - allcfs = filter(x->x["sqref"]==string(rng), allCfs(ws)) # Match range with existing conditional formatting blocks. - if length(allcfs) == 0 # No existing conditional formatting blocks for this range so create a new one. - new_cf = XML.Element("conditionalFormatting"; sqref=rng) - else # Existing conditional formatting block found for this range so add new rule to that block. - cfx["priority"] = string(maximum([parse(Int, c["priority"]) for c in XML.children(allcfs[1])])+1) - new_cf = allcfs[1] - end - - push!(new_cf, cfx) - - add_cf_to_XML(ws, new_cf) # Add the new conditional formatting to the worksheet XML. - - update_worksheets_xml!(get_xlsxfile(ws)) + update_worksheet_cfx!(allcfs, cfx, ws, rng) return 0 end @@ -968,12 +950,10 @@ function setCfContainsText(ws::Worksheet, rng::CellRange; !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) - old_cf = getConditionalFormats(ws) - for cf in old_cf - if cf.first != rng && intersects(cf.first, rng) - throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)` but is not the same. Must be the same as the existing range or entirely separate.")) - end - end + allcfs = allCfs(ws) # get all conditional format blocks + old_cf = getConditionalFormats(allcfs) # extract conditional format info + + !isnothing(value) && !is_valid_cellname(value) && !is_valid_sheet_cellname(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) @@ -1000,23 +980,10 @@ function setCfContainsText(ws::Worksheet, rng::CellRange; if !isnothing(stopIfTrue) && stopIfTrue == "true" cfx["stopIfTrue"] = "1" end + cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)])+1) : 1 push!(cfx, XML.Element("formula", XML.Text(formula))) - allcfs = filter(x->x["sqref"]==string(rng), allCfs(ws)) # Match range with existing conditional formatting blocks. - l = length(allcfs) - if l == 0 # No existing conditional formatting blocks for this range so create a new one. - new_cf = XML.Element("conditionalFormatting"; sqref=rng) - elseif l==1 # Existing conditional formatting block found for this range so add new rule to that block. - cfx["priority"] = string(maximum([parse(Int, c["priority"]) for c in XML.children(allcfs[1])])+1) - new_cf = allcfs[1] - else - throw(XLSXError("Too many conditional formatting blocks for range `$rng`. Must be one or none, found `$l`.")) - end - - push!(new_cf, cfx) - add_cf_to_XML(ws, new_cf) # Add the new conditional formatting to the worksheet XML. - - update_worksheets_xml!(get_xlsxfile(ws)) + update_worksheet_cfx!(allcfs, cfx, ws, rng) return 0 end @@ -1046,14 +1013,10 @@ function setCfTop10(ws::Worksheet, rng::CellRange; !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) - old_cf = getConditionalFormats(ws) - for cf in old_cf - if cf.first != rng && intersects(cf.first, rng) - throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)` but is not the same. Must be the same as the existing range or entirely separate.")) - end - end + allcfs = allCfs(ws) # get all conditional format blocks + old_cf = getConditionalFormats(allcfs) # extract conditional format info - isnothing(tryparse(Float64, value)) && throw(XLSXError("Invalid `value`: $value. Must be a number.")) + !isnothing(value) && !is_valid_cellname(value) && !is_valid_sheet_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) @@ -1075,23 +1038,9 @@ function setCfTop10(ws::Worksheet, rng::CellRange; if !isnothing(stopIfTrue) && stopIfTrue == "true" cfx["stopIfTrue"] = "1" end + cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)])+1) : 1 - allcfs = filter(x->x["sqref"]==string(rng), allCfs(ws)) # Match range with existing conditional formatting blocks. - l = length(allcfs) - if l == 0 # No existing conditional formatting blocks for this range so create a new one. - new_cf = XML.Element("conditionalFormatting"; sqref=rng) - elseif l==1 # Existing conditional formatting block found for this range so add new rule to that block. - cfx["priority"] = string(maximum([parse(Int, c["priority"]) for c in XML.children(allcfs[1])])+1) - new_cf = allcfs[1] - else - throw(XLSXError("Too many conditional formatting blocks for range `$rng`. Must be one or none, found `$l`.")) - end - - push!(new_cf, cfx) - - add_cf_to_XML(ws, new_cf) # Add the new conditional formatting to the worksheet XML. - - update_worksheets_xml!(get_xlsxfile(ws)) + update_worksheet_cfx!(allcfs, cfx, ws, rng) return 0 end @@ -1120,12 +1069,8 @@ function setCfAboveAverage(ws::Worksheet, rng::CellRange; !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) - old_cf = getConditionalFormats(ws) - for cf in old_cf - if cf.first != rng && intersects(cf.first, rng) - throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)` but is not the same. Must be the same as the existing range or entirely separate.")) - end - end + allcfs = allCfs(ws) # get all conditional format blocks + old_cf = getConditionalFormats(allcfs) # extract conditional format info isnothing(tryparse(Float64, value)) && throw(XLSXError("Invalid `value`: $value. Must be a number.")) @@ -1161,23 +1106,9 @@ function setCfAboveAverage(ws::Worksheet, rng::CellRange; if !isnothing(stopIfTrue) && stopIfTrue == "true" cfx["stopIfTrue"] = "1" end + cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)])+1) : 1 - allcfs = filter(x->x["sqref"]==string(rng), allCfs(ws)) # Match range with existing conditional formatting blocks. - l = length(allcfs) - if l == 0 # No existing conditional formatting blocks for this range so create a new one. - new_cf = XML.Element("conditionalFormatting"; sqref=rng) - elseif l==1 # Existing conditional formatting block found for this range so add new rule to that block. - cfx["priority"] = string(maximum([parse(Int, c["priority"]) for c in XML.children(allcfs[1])])+1) - new_cf = allcfs[1] - else - throw(XLSXError("Too many conditional formatting blocks for range `$rng`. Must be one or none, found `$l`.")) - end - - push!(new_cf, cfx) - - add_cf_to_XML(ws, new_cf) # Add the new conditional formatting to the worksheet XML. - - update_worksheets_xml!(get_xlsxfile(ws)) + update_worksheet_cfx!(allcfs, cfx, ws, rng) return 0 end @@ -1205,14 +1136,9 @@ function setCfTimePeriod(ws::Worksheet, rng::CellRange; )::Int !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) -# length(rng) <=1 && throw(XLSXError("Range `$rng` must have more than one cell.")) - old_cf = getConditionalFormats(ws) - for cf in old_cf - if cf.first != rng && intersects(cf.first, rng) - throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)` but is not the same. Must be the same as the existing range or entirely separate.")) - end - end +allcfs = allCfs(ws) # get all conditional format blocks +old_cf = getConditionalFormats(allcfs) # extract conditional format info if operator == "yesterday" formula = "FLOOR(__CR__,1)=TODAY()-1" @@ -1244,25 +1170,15 @@ function setCfTimePeriod(ws::Worksheet, rng::CellRange; new_dx = get_new_dx(wb, dx) dxid = Add_Cf_Dx(wb, new_dx) - cfx = XML.Element("cfRule"; type="timePeriod", dxfId=Int(dxid.id), priority="1", operator=operator) + cfx = XML.Element("cfRule"; type="timePeriod", dxfId=Int(dxid.id), operator=operator) if !isnothing(stopIfTrue) && stopIfTrue == "true" cfx["stopIfTrue"] = "1" end - push!(cfx, XML.Element("formula", XML.Text(formula))) - - allcfs = filter(x->x["sqref"]==string(rng), allCfs(ws)) # Match range with existing conditional formatting blocks. - if length(allcfs) == 0 # No existing conditional formatting blocks for this range so create a new one. - new_cf = XML.Element("conditionalFormatting"; sqref=rng) - else # Existing conditional formatting block found for this range so add new rule to that block. - cfx["priority"] = string(maximum([parse(Int, c["priority"]) for c in XML.children(allcfs[1])])+1) - new_cf = allcfs[1] - end + cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)])+1) : 1 - push!(new_cf, cfx) - - add_cf_to_XML(ws, new_cf) # Add the new conditional formatting to the worksheet XML. + push!(cfx, XML.Element("formula", XML.Text(formula))) - update_worksheets_xml!(get_xlsxfile(ws)) + update_worksheet_cfx!(allcfs, cfx, ws, rng) return 0 end @@ -1291,12 +1207,8 @@ function setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::CellRange; !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) - old_cf = getConditionalFormats(ws) - for cf in old_cf - if cf.first != rng && intersects(cf.first, rng) - throw(XLSXError("Range `$rng` intersects with existing conditional format range `$(cf.first)` but is not the same. Must be the same as the existing range or entirely separate.")) - end - end + allcfs = allCfs(ws) # get all conditional format blocks + old_cf = getConditionalFormats(allcfs) # extract conditional format info if operator == "containsBlanks" formula = "LEN(TRIM(__CR__))=0" @@ -1320,25 +1232,14 @@ function setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::CellRange; new_dx = get_new_dx(wb, dx) dxid = Add_Cf_Dx(wb, new_dx) - cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1") + cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id)) if !isnothing(stopIfTrue) && stopIfTrue == "true" cfx["stopIfTrue"] = "1" end + cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)])+1) : 1 formula !="" && push!(cfx, XML.Element("formula", XML.Text(formula))) - allcfs = filter(x->x["sqref"]==string(rng), allCfs(ws)) # Match range with existing conditional formatting blocks. - if length(allcfs) == 0 # No existing conditional formatting blocks for this range so create a new one. - new_cf = XML.Element("conditionalFormatting"; sqref=rng) - else # Existing conditional formatting block found for this range so add new rule to that block. - cfx["priority"] = string(maximum([parse(Int, c["priority"]) for c in XML.children(allcfs[1])])+1) - new_cf = allcfs[1] - end - - push!(new_cf, cfx) - - add_cf_to_XML(ws, new_cf) # Add the new conditional formatting to the worksheet XML. - - update_worksheets_xml!(get_xlsxfile(ws)) + update_worksheet_cfx!(allcfs, cfx, ws, rng) return 0 end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 5f41e53b..dfa9e2a7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3280,12 +3280,12 @@ end max_type="max", max_col="darkgreen" ) - @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("A5:E5") => ["colorScale"],XLSX.CellRange("A4:E4") => ["colorScale"],XLSX.CellRange("A3:E3") => ["colorScale"],XLSX.CellRange("A2:E2") => ["colorScale"],XLSX.CellRange("A1:E1") => ["colorScale"]] + @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_throws MethodError XLSX.setConditionalFormat(s, "A1", :colorScale) # Single cell not allowed - @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "Sheet1!A1:A2", :colorScale) # Overlaps with existing conditionalFormat range - @test_throws XLSX.XLSXError XLSX.setConditionalFormat(f, "Sheet1!1:2", :colorScale) # Overlaps with existing conditionalFormat range - @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, :, :colorScale) # Overlaps with existing conditionalFormat range - @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, :, :, :colorScale) # Overlaps with existing conditionalFormat range +# @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "Sheet1!A1:A2", :colorScale) # Overlaps with existing conditionalFormat range +# @test_throws XLSX.XLSXError XLSX.setConditionalFormat(f, "Sheet1!1:2", :colorScale) # Overlaps with existing conditionalFormat range +# @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, :, :colorScale) # Overlaps with existing conditionalFormat range +# @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, :, :, :colorScale) # Overlaps with existing conditionalFormat range f=XLSX.newxlsx() s=f[1] @@ -3304,7 +3304,7 @@ end max_type="max", max_col="blue" ) - @test XLSX.getConditionalFormats(s) == [XLSX.CellRange("C1:E4") => ["colorScale"],XLSX.CellRange("E1:E5") => ["colorScale"],XLSX.CellRange("B1:B5") => ["colorScale"],XLSX.CellRange("A1:A5") => ["colorScale"]] + @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] From 29676d5d1e17c7752abbdc627e3943e97682cf25 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 6 May 2025 20:14:40 +0100 Subject: [PATCH 106/154] Add tp formatting.md --- docs/src/formatting.md | 169 +++++++++++++++++- docs/src/images/blank.png | Bin 0 -> 189 bytes docs/src/images/cellvalue-formats.png | Bin 0 -> 12696 bytes docs/src/images/custom-cellvalue-example.png | Bin 0 -> 6496 bytes docs/src/images/custom-formats.png | Bin 0 -> 26110 bytes .../src/images/multiple-cellvalue-example.png | Bin 0 -> 5575 bytes docs/src/images/simple-cellvalue-example.png | Bin 0 -> 5032 bytes docs/src/index.md | 4 +- src/conditional-formats.jl | 34 ++-- 9 files changed, 184 insertions(+), 23 deletions(-) create mode 100644 docs/src/images/blank.png create mode 100644 docs/src/images/cellvalue-formats.png create mode 100644 docs/src/images/custom-cellvalue-example.png create mode 100644 docs/src/images/custom-formats.png create mode 100644 docs/src/images/multiple-cellvalue-example.png create mode 100644 docs/src/images/simple-cellvalue-example.png diff --git a/docs/src/formatting.md b/docs/src/formatting.md index 583807ee..f8ca1321 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -184,7 +184,7 @@ We can apply `setBorder()` to add a top border to each cell: julia> XLSX.setBorder(s, "B2,D2,F2"; top=["style"=>"thick", "color"=>"red"]) -1 ``` -to merge the top border with the other attributes, to get +This merges the new top border definition with the other, existing attributes, to get ![image|320x500](./images/multicell2.png) @@ -444,12 +444,171 @@ julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:F22", :colorScale; #### Cell Value -It is possible to apply a conditional format to a range of cells that applies when a cell's -value meets a specified condition using the `:cell` type. +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. All the functions of +`Highlight Cell Rules` and `Top/Bottom Rules` are provided. -![image|320x500](./images/cell1.png) ![image|320x500](./images/cell2.png) +![image|320x500](./images/cell1.png) ![image|100x500](./images/blank.png) ![image|320x500](./images/cell2.png) -All the functions of `Highlight Cell Rules` and `Top/Bottom Rules` are provided. +The six built-in formats in Excel are offered for each of these formatting options as illustrated here for the +`greaterThan` comparison. + +![image|320x500](./images/cellvalue-formats.png) + +The six built-in formatting options are available by name in XLSX.jl as follows +* `redfilltext` +* `yellowfilltext` +* `greenfilltext` +* `redfill` +* `redtext` +* `redborder` + +Thus, for example, we can create a simple `XLSXFile` from scratch and then apply some +conditional formats to its cells, as follows: + +```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; + operator="greaterThan", + value="2", + dxStyle="redfilltext") +0 + +julia> XLSX.setConditionalFormat(s, "B2:B5", :containsText; + value="u", + dxStyle="greenfilltext") +0 + +julia> XLSX.setConditionalFormat(s, "C2:C5", :top10; + operator ="topN%", + value="10" + dxStyle="redborder") +0 + +``` + +![image|320x500](./images/simple-cellvalue-example.png) + +Alternatively, it is possible to specify custom format options to match the options offered in Excel +under the `Custom Format...` option: + +![image|320x500](./images/custom-formats.png) + +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) + +``` + +![image|320x500](./images/custom-cellvalue-example.png) + +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. + +```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"=>"solid", "bgColor"=>"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) + +``` +to give this result: + +![image|320x500](./images/multiple-cellvalue-example.png) + +The `formatting_type` needed for these different functions varies, as do the keyword options. +Refer to [XLSX.setConditionalFormat()](@ref) for full details. ## Working with Merged Cells diff --git a/docs/src/images/blank.png b/docs/src/images/blank.png new file mode 100644 index 0000000000000000000000000000000000000000..89ee7cae6a8d808d29c100cca9f30cb3bd73e93a GIT binary patch literal 189 zcmeAS@N?(olHy`uVBq!ia0vp^CP1vq!3HEBF7(&|q!^2X+?^QKos)S9a~60+7BevL9R^{>!G@ZfI)z4*} HQ$iB}Z)`!v literal 0 HcmV?d00001 diff --git a/docs/src/images/cellvalue-formats.png b/docs/src/images/cellvalue-formats.png new file mode 100644 index 0000000000000000000000000000000000000000..f34cd791932196b87cb29b68c35651071ad5bfdd GIT binary patch literal 12696 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 literal 0 HcmV?d00001 diff --git a/docs/src/images/custom-cellvalue-example.png b/docs/src/images/custom-cellvalue-example.png new file mode 100644 index 0000000000000000000000000000000000000000..b4df58db024628ac922d47198ae319a317a68a96 GIT binary patch literal 6496 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 literal 0 HcmV?d00001 diff --git a/docs/src/images/custom-formats.png b/docs/src/images/custom-formats.png new file mode 100644 index 0000000000000000000000000000000000000000..3807698e61fab230f5ccedfac89917682a0e499b GIT binary patch literal 26110 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 literal 0 HcmV?d00001 diff --git a/docs/src/images/multiple-cellvalue-example.png b/docs/src/images/multiple-cellvalue-example.png new file mode 100644 index 0000000000000000000000000000000000000000..95cdb62986196ec49d85b8fdec3338da14257d8d GIT binary patch literal 5575 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 literal 0 HcmV?d00001 diff --git a/docs/src/images/simple-cellvalue-example.png b/docs/src/images/simple-cellvalue-example.png new file mode 100644 index 0000000000000000000000000000000000000000..5f1c6d57b7f5f1ceafed0098cb9bf73c148da379 GIT binary patch literal 5032 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 f8xzCVvvVd7eweR2{ Dict("pattern" => "solid", "bgColor"=>"FFFFC7CE") ), "yellowfilltext" => Dict( - "font" => Dict("color"=>"FFA51E00"), - "fill" => Dict("pattern" => "solid", "bgColor"=>"FF9C5700") + "font" => Dict("color"=>"FF9C5700"), + "fill" => Dict("pattern" => "solid", "bgColor"=>"FFFEB9C") ), "greenfilltext" => Dict( "font" => Dict("color"=>"FF006100"), @@ -34,7 +34,7 @@ const colorscales::Dict{String,XML.Node} = Dict( # Defines the 12 standard, b XML.h.color(rgb="FF63BE7B") ) ), - "redyellowgreengreenyellowred" => XML.h.cfRule(type="colorScale", priority="1", + "redyellowgreen" => XML.h.cfRule(type="colorScale", priority="1", XML.h.colorScale( XML.h.cfvo(type="min"), XML.h.cfvo(type="percentile", val="50"), @@ -147,7 +147,7 @@ const timeperiods::Dict{String,String} = Dict( "nextMonth" => "AND(MONTH(__CR__)=MONTH(EDATE(TODAY(),0+1)),YEAR(__CR__)=YEAR(EDATE(TODAY(),0+1)))" ) -function get_dx(dxStyle::Union{Nothing, String}, border::Union{Nothing, Vector{Pair{String, String}}}, fill::Union{Nothing, Vector{Pair{String, String}}}, font::Union{Nothing, Vector{Pair{String, String}}}, format::Union{Nothing, Vector{Pair{String, String}}})::Dict{String,Dict{String, String}} +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"] @@ -204,7 +204,7 @@ function get_new_dx(wb::Workbook, dx::Dict{String,Dict{String, String}})::XML.No elseif y == "under" z != "none" && push!(fontdx, XML.Element("u"; val="v")) elseif y == "strike" - strike=="true" && push!(fontdx, XML.Element(y; val="0")) + z=="true" && push!(fontdx, XML.Element(y)) end end end @@ -498,7 +498,7 @@ 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. +- `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 @@ -567,9 +567,9 @@ is `dxStyle="redfilltext"`. # Examples ```julia -julia> XLSX.setConditionalFormat(s, "B1:B5", :cell) # 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) # Defaults to `operator="greaterThan"`, `dxStyle="redfilltext"` and `value` set to the arithmetic agverage of cell values in `rng`. -julia> XLSX.setConditionalFormat(s, "B1:B5", :cell; +julia> XLSX.setConditionalFormat(s, "B1:B5", :cellIs; operator="between", value="2", value2="3", @@ -578,7 +578,7 @@ julia> XLSX.setConditionalFormat(s, "B1:B5", :cell; font = ["color"=>"blue", "bold"=>"true"] ) -julia> XLSX.setConditionalFormat(s, "B1:B5", :cell; +julia> XLSX.setConditionalFormat(s, "B1:B5", :cellIs; operator="greaterThan", value="4", fill = ["pattern" => "none", "bgColor"=>"green"], @@ -953,7 +953,7 @@ function setCfContainsText(ws::Worksheet, rng::CellRange; allcfs = allCfs(ws) # get all conditional format blocks old_cf = getConditionalFormats(allcfs) # extract conditional format info - !isnothing(value) && !is_valid_cellname(value) && !is_valid_sheet_cellname(value) && throw(XLSXError("Invalid `value`: $value. Must be a number or a CellRef.")) +# !isnothing(value) && !is_valid_cellname(value) && !is_valid_sheet_cellname(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) @@ -1024,13 +1024,13 @@ function setCfTop10(ws::Worksheet, rng::CellRange; dxid = Add_Cf_Dx(wb, new_dx) if operator == "topN" - cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", rank=value) + cfx = XML.Element("cfRule"; type="top10", dxfId=Int(dxid.id), priority="1", rank=value) elseif operator == "topN%" - cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", percent="1", rank=value) + cfx = XML.Element("cfRule"; type="top10", dxfId=Int(dxid.id), priority="1", percent="1", rank=value) elseif operator == "bottomN" - cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", bottom="1", rank=value) + cfx = XML.Element("cfRule"; type="top10", dxfId=Int(dxid.id), priority="1", bottom="1", rank=value) elseif operator == "bottomN%" - cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", percent="1", bottom="1", rank=value) + cfx = XML.Element("cfRule"; type="top10", dxfId=Int(dxid.id), priority="1", percent="1", bottom="1", rank=value) else throw(XLSXError("Invalid operator: $operator. Valid options are: `topN`, `topN%`, `bottomN`, `bottomN%`.")) end @@ -1072,7 +1072,7 @@ function setCfAboveAverage(ws::Worksheet, rng::CellRange; allcfs = allCfs(ws) # get all conditional format blocks old_cf = getConditionalFormats(allcfs) # extract conditional format info - isnothing(tryparse(Float64, value)) && throw(XLSXError("Invalid `value`: $value. Must be a number.")) +# isnothing(tryparse(Float64, value)) && throw(XLSXError("Invalid `value`: $value. Must be a number.")) wb=get_workbook(ws) dx = get_dx(dxStyle, format, font, border, fill) @@ -1137,8 +1137,8 @@ function setCfTimePeriod(ws::Worksheet, rng::CellRange; !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) -allcfs = allCfs(ws) # get all conditional format blocks -old_cf = getConditionalFormats(allcfs) # extract conditional format info + allcfs = allCfs(ws) # get all conditional format blocks + old_cf = getConditionalFormats(allcfs) # extract conditional format info if operator == "yesterday" formula = "FLOOR(__CR__,1)=TODAY()-1" From 72fb1e5d563e20e101f5dc9be4944601ec6a142b Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Wed, 7 May 2025 23:25:19 +0100 Subject: [PATCH 107/154] More documentation. --- docs/src/formatting.md | 275 +++++++++++++----- .../src/images/multiple-cellvalue-example.png | Bin 5575 -> 5800 bytes docs/src/images/no-stop-if-true.png | Bin 0 -> 5061 bytes docs/src/images/stop-if-true.png | Bin 0 -> 4898 bytes src/conditional-formats.jl | 45 ++- 5 files changed, 235 insertions(+), 85 deletions(-) create mode 100644 docs/src/images/no-stop-if-true.png create mode 100644 docs/src/images/stop-if-true.png diff --git a/docs/src/formatting.md b/docs/src/formatting.md index f8ca1321..6dd8f731 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -384,78 +384,77 @@ is applied are updated. `XLSX.setConditionalFormat(sheet, CellRange, :formatting_type; kwargs...)` -Each of the available `:formatting_type`s is described in the following sections. - -#### 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. - -![image|320x500](./images/colorScales.png) - -In XLSX.jl, the twelve built-in scales are named by their start/mid/end 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/). - -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 -``` -![image|320x500](./images/custom-colorscale.png) +Excel uses range of `:formatting_type` values describe these conditional formats and the same values +are used here, as follows: +- `:colorScale` +- `:cellIs` +- `:top10` +- `:aboveAverage` +- `:containsText` +- `:notContainsText` +- `:beginsWith` +- `:endsWith` +- `:timePeriod` +- `:containsErrors` +- `:notContainsErrors` +- `:containsBlanks` +- `:notContainsBlanks` +- `:uniqueValues` +- `:duplicateValues` + +Use of these different `:formatting_type`s is illustrated in the following sections. +For more details on the range of `:formatting_types` 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. All the functions of -`Highlight Cell Rules` and `Top/Bottom Rules` are provided. +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 Cell Rules` and `Top/Bottom Rules` are provided. ![image|320x500](./images/cell1.png) ![image|100x500](./images/blank.png) ![image|320x500](./images/cell2.png) -The six built-in formats in Excel are offered for each of these formatting options as illustrated here for the -`greaterThan` comparison. +Excel uses range of `:formatting_type` values 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` + +Each of these formatting types needs a set of keyword options to fully define its operation. For example, +the `:cellIs` type needs an `operator` keyword, set to define the test to make to determine whether or not +to apply the formatting. Valid `operator` values 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`) + +Eac of these need a keyword `value` to be specified and, for `between` and `notBetween`, `value2` +must also be specified. + +All the cell value formatting types can use one of six built-in formats in Excel as illustrated here +for the `greaterThan` comparison. ![image|320x500](./images/cellvalue-formats.png) -The six built-in formatting options are available by name in XLSX.jl as follows +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` @@ -499,18 +498,18 @@ julia> s[1:5, 1:3] 3 "Out" 30.4 4 "There" 40.5 -julia> XLSX.setConditionalFormat(s, "A2:A5", :cellIs; +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; +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; +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") @@ -525,6 +524,16 @@ under the `Custom Format...` option: ![image|320x500](./images/custom-formats.png) +!!! 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 @@ -562,10 +571,81 @@ julia> XLSX.getConditionalFormats(s) ![image|320x500](./images/custom-cellvalue-example.png) + +The `formatting_type` needed for these different functions varies, as do the keyword options. +Refer to [XLSX.setConditionalFormat()](@ref) for full details. + +#### Data Bar + +(In development) + +#### 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. + +![image|320x500](./images/colorScales.png) + +In XLSX.jl, the twelve built-in scales are named by their start/mid/end 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/). + +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 +``` +![image|320x500](./images/custom-colorscale.png) + +#### Icon Sets + +(In development) + +#### 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. +order (priority 1 is higher priority than priority 2) which is the same as the order in +which they were defined with `setConditionalFormat`. ```julia @@ -592,7 +672,7 @@ julia> XLSX.setConditionalFormat(s, "A2:A5", :cellIs; operator="lessThan", value="2", font=["color"=>"coral", "bold"=>"true"], - fill=["pattern"=>"solid", "bgColor"=>"cornsilk"], + fill=["pattern"=>"lightHorizontal", "fgColor"=>"cornsilk"], border=["style"=>"dashed", "color"=>"orangered4"]) 0 @@ -603,12 +683,67 @@ julia> XLSX.getConditionalFormats(s) A2:C5 => (type = "top10", priority = 2) ``` -to give this result: ![image|320x500](./images/multiple-cellvalue-example.png) -The `formatting_type` needed for these different functions varies, as do the keyword options. -Refer to [XLSX.setConditionalFormat()](@ref) for full details. +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 te `stopIfTrue` keyword. + +For example: + +```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 +``` + +![image|320x500](./images/stop-if-true.png) + +Overlaying the same three conditional formats without setting the `stopIfTrue` option +will result in the following, instead: + +![image|320x500](./images/no-stop-if-true.png) + +#### Specifying cell references in Conditional Formats + +The specified range to which a conditional format is to be applied is always converted to use +absolute cell references so that, for example +```julia +julia> XLSX.setConditionalFormat(s, "A2:C5", :colorScale; colorscale="greenyellow") +``` +will alsways actually refer to the range "$A$2:$C$5". + +However, cell references used to specify `value` or `value2` or in any `formula` may be either +absolute or relative. XLSX.jl makes no assumption about whether these cell references should be +relative or absolute and will accept whatever is specified. + +Relative references (e.g. "G8") used here are interpreted (by Excel) relative to the top-left +cell in the range. For each of the other cells in the formatted range, the reference will be to +the cell with the same offset as cell G8 has to the top-left cell. + +On the other hand, if an absolute cell reference (e.g. "$G$8") is provided, the cell referred to +will be the same for each cell in the formatted range. ## Working with Merged Cells diff --git a/docs/src/images/multiple-cellvalue-example.png b/docs/src/images/multiple-cellvalue-example.png index 95cdb62986196ec49d85b8fdec3338da14257d8d..85da5cb2aaacc609db24d48d4732d467111ce5dd 100644 GIT binary patch literal 5800 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 EKgGVJ5&!@I literal 5575 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 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 0000000000000000000000000000000000000000..0c68da28c21c5f5471ad78ccb3daa4623477fa61 GIT binary patch literal 5061 zcmZu#2Q-{b_a|7L=w*digw=vzH+oqtqU|ecM2#M6wX7}?Wwl83-WJh`5D8W$=R4=l^UORmckY~L?(fd;4#wC}hn|L;hJ=KKUKb8SkdTmq ziS<}20P)#cdo7gsBK1Y+Xpqzn^Q;paLer$&uGu=DTr-qZ@8r|2?;~bpF%o- zeCb3&!eXThQ#VE5-<}KhHocoqxrh90yF7W;09ASo((SpbH(iexo6z-WE%toa4SMz( zmN-O{S%_GV$m#_3=-b0ro*Ncvtnj=0#a`p~gWuH7Et)e#wA0Bj3*Ztmhjy(G|cNE|ivs=C~X`&|`YW7vux%2xIHYVz)8ZMYp75*7zo|gPB zh#(qvyckp8Y*Fs}Up@P+!DA8Ja_*)t&jwLW7(dFm8>dU`deM+){bStKsPvkv9j z5AK8#9@Nix)_*06yPeNu(aa8;j+>$L_*5dGk<0%j_4riyxZ3HTIr=n2&D8jZ<;u&@ z+pYoGb}z-%v*t0wbh;G@70ApG&N~qK}%gS>#Oi zR@Q;|*=`nI{lQlxbpH)4Zwl$}K@FSouVkdE zGj4xAcf;WB&8jv3TQlnPhrneCji8a8r0TNRW(oHG$~C{AsRg>Qoh7UFr!yh-7s^ zYR^e!JmIc+^~n%Jg=y$HVX9@lET`8QLA6;WN%Yl*jhww)(^n1#2__r=;9%i(lxa0a zTJPFFL#kSRzQ=w3@k@QXq_x%hYz78Hl|D4Atf`@QZnX5g5p{5MBp@uTL#);R!O_BN zn;j-5p-6r$SayFh2xQ<0XL?>n?*YFp>Nm1Zl!W?2Ogk2wM6>3{<{i7OuF+AVL208k zKFWo}hKF0dFowScev}exIy5R^yD86JjGLk?_)PHut74IO*2Ty;ng3ef8BW2D&e&0V zUODgeT7WS{7<29Yayjq;2#0h`VM7#A%zNB%Fn6aZ2SE-Tn8hXeLW1MEqm32n53x3{ zG(GyclX+bgO%#zFQ>kjgKk$YQvS7(KuY^cRiFq zM88A$ssH)=6}2eC7@C;N@bjk12m!Puhdu9Jw$U$~-5i-?aP&JTg01FBQ{-j-bHU-G zfuo0GPEo02afiov4+%<|&C$(x0`+`S<&sx5fnv4BE~=qLOzIgM^d9>inq2DF>0}~E z!W*6Q%eF9CcD^p90!9^Ev7yme2OG(EQzx5~Rzy`TGZ@Zxlyb9J^fjf&x^?_cWk30e zR%ku9uSpsQ&#opilp`*26c<*uA0FN{bF%p@cF7KZupgQeXHQ)LOMSDWzBVB5Ci8P7 zY1L6+xu&MMXok)HKAwaBSkYLvj>mOZ8+jbQH1&w?({|L$OaJbDd{uvET%)jRfUi4bp$ztmpRJZEIHYE@6B4A2!`cvCT0pR9qWr$32{q&NtxBF!I0z+7a?nH`;|$CI(W$T<(^zEG2v}aos7-0TvD-pC z%QtT7X0|MlmiC(oVT>RH-s;r1cR^giAz46T*fT~{wtcPdm7>t~$}b&_b?6wuH#C7Y zk``Aou4#4D>J)hoB)>3us@zUc+xsc_2IvvIDb!)Hf1;IQhawc?#JnlR{1TECLsG<{ z7PhhdOZX!KsBBVyb&a?l>ZufNeutU?%?iiR3AQTSt6^+xY&5NAT&!&39|)vRCUSmc zJkvTm!6t{6^`s9TVL*1RP@fYE?i6sTd zp3%?dj^O#8SA-wEk4@fFRfL$%h(6~F$&%xvHHdJD;7^lGVl$JSCrAb5u+38~E1+

cz(AlVEslo%y=;P?o%wMlbU3a?O>|JGLG~%MIM~B<@Lib3Rt*BBo zZq&|1+zqDV6bcyG8{~?&-j{3R=t~-8W(3z=`()VZM3raF^Z<7>&z+r8xz_v0K~+fD zMchkEn4ujN__F4bdBt{1hzD_h`jLE>W2L&#tvKyiOjY!1vDsTulXZlv8ljRRw z{h(B{gphdeTOE6lFfKS=aG(AI`K6fT1oPbQ8Te;-;=p-KI)2!?7H_IWX6Q#xaIzGw z3p9+=ZxZ})nLEBe;-Q?*YnybNNs>@3F~O{gW$jKzaA@k;(18hBv|!7OU>K|?>fa8h zzo|_Z9{BRk*$E6r5<#h??7)(|gZb5EONje~O)gl#kv>8NKh7?8D=jZH5&pWJ`aLa)b7q#5Lt(4251WLphYQqEV z7uKJQa!<#0nUSTGQ!Bn@ihL5ZH@xk!rx!oX5WX<0FQU*+(Q1h7sx4`hF8c288GdSw zx-L|8Dabze?a3?|{c$e~lwl!$e+e45#B;bhVYk`p(7KS7*aVHeT^}&9_{hfJ;@nuc zS>;d~(D&G6kg7ubL;RzEZzG zmP0?u4c{6V|MA9dYEzs1=u4sBMnB&?K+ic!1t6k+YjSGw5t2U2IP(HI5}AW)^m9w! zk{;AnZ99L_%t;IkeuhJ1Pq(;%Q&2uhGD)Zv$#Q#2AFrPFC1kCSyzt?Vk&sjzbNr#c zJVPOtJ_z)XOaWkdD1GX;dQX@bOmwhxUCiC4W?<&;c41Mcq3WH{)+bf2=)$LxF}?$rkybhNMe&=6PTJ(6|~ zbYOX0u5@NX-CmmCi?bz2ODvDmcleA$EFa>&Vr5T${Lk*4g4;jJ=-ilTMAmIo5QC<( zM50v~=<3QEEyqM}ztFzWV zIQJd7dLgLqaH&=CubAZNY_eaYG-oXLrd5tOC1dBhE;@UZ?o|r0W$Yh5F+i{T?KLsA zDwRs=z8SDWrAYegAggi(~KW!6_v_=6(c6|%6y9|jJ$z$@kJ zn^2S?EDQ5*SpLnZ{yqno>A(d2@U7e+;}7!7$EQh zNLgOjZ3_=OB7b zcHRwfCk=YW_1x4VuD|BLCSJSMr;dHP`%Sg?(HHTS8nvyWMUVitt$x?@%8*_Q^AGPV zDVC(JqT67lS9B#+t-U6xG8`%umvk%zsezMPZcM7!SqWDSF0(yxJ%OL~%Xz^OkV~c4 zWBlNyQ_WKpudnoD7n5ve2zEBlu=5Lfywid*qmt_@Z)-7b-7^=Hl1kp?IgdmP5b-;$ z>-_LaOcPzunu&9hP}}frp~Y?;5t?Ah4dwvkB_}#WftLyh2d{fCDe)zSC$hZufWbi2Y` z5h{?TZR>}}o*T|ubBpzh+%9Rrk8mp`3RuVn3OO6hKS}BH7&+`7$zlVqol$#+l3C<- zdg&1Jmk@tLv7r%i+DK7@+CtwFG7A*Mr%w{09S$)JyHe=Kn~$Skn{T5a7^1G0?l1=^ z2UF!4nL3zX`7ks@Q{ioy*)c+nzUO_bbbM;Rz`Kx&7R_Fo6ecX*m}J*If6XnTM{ zK;c6qX_Cal&c!g4ROwP=nukdwB6p7eQk>>}l&P`&D|DohfBW!XotBi<(Zmh4h#@#r z0;EV{0BL1>I!U%bxxDFx)c=L83@My7qwJnC15F17G=Fe@$-WBCri z-kcM{y^$O7z7x|YJ808H4OA40si9hy=GAI8m~M&VhyZ`!im6yrYhC%d^C9vf6rlXd z7fS}VEQ)wB|6M;Kfyj{R&8V@A0x$`PtcT?4_5iBoQF5QU79n)+YpOB0I z{7sGgrLp5264)hMVq!}#@EA&7NQf>j!d6!H z^50theMxll4>rF1FKmRt1GitMd-Zn`u(2Za|DyH!EU!ES;};(0JnOya&5RX6T{1$) X%qIwkJRgZyZzQ@}hOk-(9c`W)6)CeKbVax?fg6lyZl&3&c5FK^N#GAe3zqHmo+CtUYu(G0GYs-Sd{CDi#u@Q-WIkX~>%ETf zJ(25h(Cl%ReB$B3nnizuWw%vI$$n;SO|!nPslGp?`51qD#=D&!Ud4YSOpLC>nO>;4 zGV|E=8Knij52I$-Hg2ON6M87hgB$%E9hXD-F!R`^k?B$t69ah>TA8>)Aa9s-uO`al zv(yHc+koS}!+xG1@!*89QOgwIHoL>!Blc4CKt6HGjEZ|BXY%<71-z&e^wl}@n9W5u z3Y?xR^Cye;*6I`28%*44eZvVM-nC;txxUT|J#aiJMq}~7JsL>*8og}bF^@sb%tfwK zx<9G5yEgbBpJ@J!!e(trruYs(H>wiimA6N4!9{IAwkCu)6ie=qb7>B;fFW9?y()|) zRAk}~$7jAwl+__wMDMkG+W(l#13< z=D`besn;0D!Udte>{#(gC6G^8%pd0yc)EcN0=s!-cE*gnrvWE)T{(<^QnBecE{Q9Z zUt?Z`hMV|Cs`ZSH)|CLZ28dQcuZpRW-%d0v2!VRATbN>n5K)u6p2EPeDaqQWqBZ?P zb78gDdgMh30}iFu(J!s8`5!5DeqP2oP_jyjwz%`1;c$19%E9r34GP9RtO=VAaO^d)Z(V#QK4*Xj1!UWG0Fm*nuy zp}pE-wERlzXg2@ooxiD`R&cQQ#W+n7mRu*)- zK9$cPFGH>0a}Ml=4*rMaa7;qbopR~tP_Jx0CZ{4+jDM_tA1ziHy(8s;vPP>l96T){<0AK-xN0weGcreqfhTy%GJst7J$ znOP7PCOE~U7uV$c&k(PkDPul7Fm#Y_EBV@?2z2@Yf3hl-5(^eNon|Pd<{e#;3S9^O zymH?u`qcfXYr)0ny3=YL+@<1=vjZ!|YO*k~jCH&LlDTkoKtOTSTKbi1XpQi`DV_L_WA>d#0m7cFiX~Ub zLC9;W&Cp_`&wdmU*&l*zimzE?@V} zAWQ#A2F9E*KSIBgc|XxI=VAe^|3DJ!K=Zp}tiajNh!$gI6XFrEaV!BI;!@`OPuylP zSH)3sPwL-(zNm=|Eu^ffaU`(${Tu0v^(hA-Paa7rfv243?XQ6l&%Qi+m91MTk)avbYv*)4NK442nSi8J(3d^z`jD+eWj&=1qcxe;B~{$r)T$E zly-RQXc?23nj+qd3p!_mX^Y=x(97I$ox)2xhMUDo~N9F}=^=td0%#xfY2 zi?vnG_y_kIDe=p=UH8X)&0c4oyxVQ}^o~&te{(zpz0+;5Yr<}6unufmK&Ew8k>zh4 z?Xt76woU0GIOozbOYD6|s7Db#CQXTa#Au2;&9tTXIk?f?2SISS%`$4TDXIWw+40BR z>;!t1agAWNL_@&$uaJuMUmi@SqWrnju7z)oJ$@?NQHJ$Zu(7p{yFH9o#GaSJ4vI*h z^)USL+?6anupIKB zdQywiCMaCrz2s7zxfD9$LcH(&X~lo+2Y`_&$JyPWO0+;D6$cQXkkINs#}1c5?-nsi z;9OsUP^y%ZGMn?CkT}h$GZwN~(utp#b2(*q2>)-@y#3AN{^Y3lXtr{NQ86ucaIlNGdyB8x61ttVaL*^2h6a?XHYgQW?#;14 zUTSh8l^~+PRiA6?D|KtuwLIV7$84Ep%nVO2v-PFhGRYw0*;QN&Zw1BkVP7ywoz-)- zke*y7Z@Jnf%gLhwzBn)jb=hfajh>L-wDYiC$WK0G-I~&>OpA~H+ChJdx@!mjGn0$? z%0{o;gA01fIQ5Safy`Y)p=FF>19Bo=0estlj?iwdc(n};NJqcS_bjfvIhYS&bT{Yw zhc?aw=Szll^9&@03}3jeKO4JK#w8ti$j1lU+40>Zn$H(t5j^2~EayiI=6CejSARSb zQ~ONYt$opoGWn!E{n^2Xv?Fc*d|$ohK7X2E#QlfSE+2)QU9tGU5)cjXj?uHux-Nfg1S2;v5j!%^}pc%k4H9e ze6)eSJg&n~`6ZLLy33Ym^*Y5cXmUBr=t>YVU0PzFt=9U5&*Ozj65FFr3A6DqtYZOF zMX#LUO&Qb+GA7lX&!C&3Boyz03KSJp`9AB#lj0c^i^+ji${IxJUfNze+ta5L+@k$o zB0T04E^HWwLuGPQ9ZuLhS-XZV^fU#BK1pJAASU;;MC#7+LuKs5P5E)06)tz1@mh)s z_2?*6kC(zw%!kcE(y&-R@4@F#grvDre2J`z9|hmf7M2r2tg>0mC+wUb5U3$bWbFS9 z8$7D#lci^mH@hSbn&_C|mdmla10zk`#-;j~!KuuPH#g+oY1!Fve`>hs5v_@tYwZCE zy$A2peXNA^$?T@xQ3z4y$2BGQZn?l$2zvK#h}R;6PkV>sJXDiCG}O)YjSXWBfzl^6 zpp<*l**ko(z2_W!HPM}Kx)8vZwjB+ghN?PQ1fiV(}M4B+(BJO-k+wF;m zCdTW@<6^5n)(wCo_`K4F^fR2 zmd@oWD<>vJJzaKhrwOl}rWkNN;uH|eor|DCN`>iauy=%3=SgN6Ne7CHsnn(KwNr96 z%08*K)0n+|&oM77*#8!7g#pFisL*Y(qsIp@WS!G$rOKwAHL;QYJ5@*kio@yMtF>9A%qZ4E8Z`eXDvG4xwsPhPu5v*YuV`WajDO&bS?EkM($p!pc6O`K-4bOA6;q} zw{~Fe+*xd5cy)!G$iczJ_xwWo9a^{|rjt5zqJp8vkiK$ta1W zXuKavX1<;LiO!;1*~+2c^H-b2=5bYCV8+s~LE2#0yET(n#I;v9-_eWPz;911{>A7F zst~SiFb=i2vWqe7i8&WOWxRXmbv8f154}PC#<`2+TjE0T=M|u$V8U8>eCrU?d5FUU z6X21w%)=7BjLhRbIxIXAN9PJ1FnSGf~A(01QBnV=W3fejPh;mbj0E`#CXc$a1;EsnFg`SkE9( zSM4@#a#SVWaH~D3Hn)SZ_Uldz^lgcFjQ!SoCqn1tQ*Vh^s)DuoONH0Qc?Ug>qk1mH z``sHtQLO{D&p}9J+N5R)iFefq#5G=)>E_}XAm@!&IxTv_ZC*q_VE+<8S`+tby&e$4 zajzn?hQ)EIRyVSEwFoajXZ(m#I3wCuit{%`qOv1&sWIB}y(+c4#}YGM$izCNlX7Jh zM`n1+(#T=ZCUwKmKxc_x1$P#sYG{{+ zi>(eyMGH^cVnG(ApsYr2+yRu?K>S3T&_=7uu)C?xCnbRx=jrTWC*0q>NhQnoGpdsP z_t(lT^)Cw78H8jUkfnS5<$|IPz%9aAUp+T1>fGUkHZ z2)_C}n*MBE=79E<*ERCf6`&Dl7ckPdkHA1;?y@pYVYq3@m{g zYK490T;pa=6}oeDKb-#5jBSeF!K`Gw`a&5rrhfCyZi?muC?zdt)-N#H#&_XH3m2L{ zV%bkJnyAE)o9k8SN5yu+&5<@@ZB<<~D&-_gyticOC7H~C^W;%a8|+&4GmAdCuu=Q| zt&G*qjuk|<_dngXS*6)?%AyDoaxq61sV{Fp3H_EPRH<&b?h{7 z8+oqyPXE`vcnSz?-K4Zy>=sAy1g$L} z%WFA{lSLh;Md$4&-W1)9=%KO_3iqu5SX4aS^|d1R&$cDCA8iKhDU$zd0WfVNt#S>g Gu>S*Je@buw literal 0 HcmV?d00001 diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index 58b55e5b..d8ea2325 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -762,7 +762,7 @@ function setConditionalFormat(f, r, type::Symbol; kw...) setCfTimePeriod(f, r; kw...) elseif type ∈ [:containsText, :notContainsText, :beginsWith, :endsWith] setCfContainsText(f, r; operator=String(type), kw...) - elseif type ∈ [:containsBlanks, :notContainsBlanks, :containsErrors, :notContainsErrors, duplicateValues, uniqueValues] + elseif type ∈ [:containsBlanks, :notContainsBlanks, :containsErrors, :notContainsErrors, :duplicateValues, :uniqueValues] setCfContainsBlankErrorUniqDup(f, r; operator=String(type), kw...) # elseif type == :iconSet # throw(XLSXError("Icon sets are not yet implemented.")) @@ -909,11 +909,13 @@ function setCfCellIs(ws::Worksheet, rng::CellRange; 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), operator=operator) + 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["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)])+1) : 1 + cfx["operator"] = operator + push!(cfx, XML.Element("formula", XML.Text(value))) if !isnothing(value2) && operator ∈ needsValue2 @@ -976,11 +978,13 @@ function setCfContainsText(ws::Worksheet, rng::CellRange; end formula = replace(formula, "__txt__" => value, "__CR__" => string(first(rng))) - cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", operator=operator, text=value) + 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 - cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)])+1) : 1 + cfx["operator"]=operator + cfx["text"]=value push!(cfx, XML.Element("formula", XML.Text(formula))) update_worksheet_cfx!(allcfs, cfx, ws, rng) @@ -1023,22 +1027,32 @@ function setCfTop10(ws::Worksheet, rng::CellRange; 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" - cfx = XML.Element("cfRule"; type="top10", dxfId=Int(dxid.id), priority="1", rank=value) elseif operator == "topN%" - cfx = XML.Element("cfRule"; type="top10", dxfId=Int(dxid.id), priority="1", percent="1", rank=value) + percent="1" elseif operator == "bottomN" - cfx = XML.Element("cfRule"; type="top10", dxfId=Int(dxid.id), priority="1", bottom="1", rank=value) + bottom="1" elseif operator == "bottomN%" - cfx = XML.Element("cfRule"; type="top10", dxfId=Int(dxid.id), priority="1", percent="1", bottom="1", rank=value) + 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 - cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)])+1) : 1 + if percent != "" + cfx["percent"] = percent + end + if bottom != "" + cfx["bottom"] = bottom + end + cfx["rank"] = value update_worksheet_cfx!(allcfs, cfx, ws, rng) @@ -1103,10 +1117,10 @@ function setCfAboveAverage(ws::Worksheet, rng::CellRange; 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 - cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)])+1) : 1 update_worksheet_cfx!(allcfs, cfx, ws, rng) @@ -1170,12 +1184,13 @@ function setCfTimePeriod(ws::Worksheet, rng::CellRange; new_dx = get_new_dx(wb, dx) dxid = Add_Cf_Dx(wb, new_dx) - cfx = XML.Element("cfRule"; type="timePeriod", dxfId=Int(dxid.id), operator=operator) + 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["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)])+1) : 1 - + cfx["operator"] = operator + push!(cfx, XML.Element("formula", XML.Text(formula))) update_worksheet_cfx!(allcfs, cfx, ws, rng) @@ -1233,10 +1248,10 @@ function setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::CellRange; 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 - cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)])+1) : 1 formula !="" && push!(cfx, XML.Element("formula", XML.Text(formula))) update_worksheet_cfx!(allcfs, cfx, ws, rng) From 9fbfa6ef512d5b212571fb0771820595c3f1e7ab Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 9 May 2025 17:06:04 +0100 Subject: [PATCH 108/154] More minor changes to docstrings --- docs/src/index.md | 2 +- docs/src/migration.md | 7 +++++++ src/conditional-formats.jl | 4 ++-- src/read.jl | 4 ++-- src/worksheet.jl | 3 ++- src/write.jl | 2 +- 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index b22bf54e..2d131cf1 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -16,7 +16,7 @@ This package follows the EMCA-376 to parse and generate XLSX files. ## Requirements -* Julia v1.6 +* Julia v1.8 * Linux, macOS or Windows. diff --git a/docs/src/migration.md b/docs/src/migration.md index d0f0921f..3c79888d 100644 --- a/docs/src/migration.md +++ b/docs/src/migration.md @@ -1,5 +1,12 @@ # Migration Guides +!!! 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 Version `v0.8` introduced a breaking change on methods [`XLSX.gettable`](@ref) and [`XLSX.readtable`](@ref). diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index d8ea2325..0dcba375 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -542,12 +542,12 @@ Alternatively, you can define a custom format by using the keywords `format`, `f 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`` +- `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 +Refer to [`setFormat()`](@ref), [`setFont()`](@ref), [`setFill()`](@ref) and [`setBorder()`](@ref) for more details on the valid attributes and values. !!! note diff --git a/src/read.jl b/src/read.jl index ba5d1dd3..7a2d75bb 100644 --- a/src/read.jl +++ b/src/read.jl @@ -545,7 +545,7 @@ julia> XLSX.readdata("myfile.xlsx", "mysheet!A2:B4") 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" @@ -622,7 +622,7 @@ The default behavior is `stop_in_empty_row=true`. Example for `stop_in_row_function`: -``` +```julia function stop_function(r) v = r[:col_label] return !ismissing(v) && v == "unwanted value" diff --git a/src/worksheet.jl b/src/worksheet.jl index f5f55170..a8e06ccf 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -330,9 +330,10 @@ julia> cell = XLSX.getcell(sheet, "A1") julia> cell = XLSX.getcell(sheet, 1:3, [2,4,6]) -Other examples are as [`getdata()`](@ref). ``` +Other examples are as [`getdata()`](@ref). + """ function getcell(ws::Worksheet, single::CellRef)::AbstractCell diff --git a/src/write.jl b/src/write.jl index e7cc5dcc..9ba1397a 100644 --- a/src/write.jl +++ b/src/write.jl @@ -32,7 +32,7 @@ saving to a file with `XLSX.writexlsx`. # Examples ```julia -julia> xf = newxlsx() +julia> xf = XLSX.newxlsx() ``` """ From 24483664a021c87bd97c8e5d8821e230da69616a Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 9 May 2025 23:41:18 +0100 Subject: [PATCH 109/154] More docs! --- docs/src/images/containsText.png | Bin 0 -> 4225 bytes docs/src/images/errorBlank.png | Bin 0 -> 3731 bytes docs/src/images/timePeriod-9thMay2025.png | Bin 0 -> 9744 bytes src/conditional-formats.jl | 320 +++++++++++++++++----- 4 files changed, 247 insertions(+), 73 deletions(-) create mode 100644 docs/src/images/containsText.png create mode 100644 docs/src/images/errorBlank.png create mode 100644 docs/src/images/timePeriod-9thMay2025.png diff --git a/docs/src/images/containsText.png b/docs/src/images/containsText.png new file mode 100644 index 0000000000000000000000000000000000000000..ab103a2ec3465d04cd7b473d0fb1c25b5770a8c6 GIT binary patch literal 4225 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 DaKkcr literal 0 HcmV?d00001 diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index 0dcba375..1fd818ca 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -6,7 +6,7 @@ const highlights::Dict{String,Dict{String,Dict{String, String}}} = Dict( ), "yellowfilltext" => Dict( "font" => Dict("color"=>"FF9C5700"), - "fill" => Dict("pattern" => "solid", "bgColor"=>"FFFEB9C") + "fill" => Dict("pattern" => "solid", "bgColor"=>"FFFFEB9C") ), "greenfilltext" => Dict( "font" => Dict("color"=>"FF006100"), @@ -312,13 +312,8 @@ function Add_Cf_Dx(wb::Workbook, new_dx::XML.Node)::DxFormat throw(XLSXError("Wrong number of xf elements found: $existing_cellxf_elements_count. Expected $(parse(Int, xroot[i][j]["count"])).")) end end - # Check new_dx doesn't duplicate any existing dxf. 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.parse(XML.Node, XML.write(node))[1] == XML.parse(XML.Node, XML.write(new_dx))[1] # XML.jl defines `Base.:(==)` - return DxFormat(k - 1) # CellDataFormat is zero-indexed - 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 @@ -384,14 +379,9 @@ end 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 can be specified separately. - -!!! warning "In Develpment..." +and columns separately. - This function is still in development and may not work as expected. - It is not yet implemented for all types of conditional formats. - -Valid options for `type` are (others in develpment): +Valid options for `type` are: - `:colorScale` - `:cellIs` - `:top10` @@ -411,39 +401,24 @@ Valid options for `type` are (others in develpment): The `type` argument determines which type of conditional formatting is being defined. Keyword options differ according to the `type` specified, as set out below. -!!! note "Ovrlaying conditional formats" - - Conditional formats are applied to a cell range and it is possible to apply multiple - conditional formats to the same range or to overlapping ranges. Each format is applied - in turn to each cell in priority order which, here, is the order in which they are - created. Different format options may complement or override each other and the - finished appearance will be the resuilt of all formats overlaying each other. - - It is possible to terminate the sequential application of conditional formats to a - cell if the condition related to any format is met. This is achieved by setting the - keyword option `stopIfTrue=true` in the relevant conditional format. - - While the `stopIfTrue` keyword is available for most conditional formats, it is not - available for `:colorScale` conditional formats. - # type = :colorScale -Define a 2-color or 3-color color scale conditional format. +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 color scale. -- `"greenyellowred"`: Green, Yellow, Red color scale. -- `"redwhitegreen"` : Red, White, Green color scale. -- `"greenwhitered"` : Green, White, Red color scale. -- `"redwhiteblue"` : Red, White, Blue color scale. -- `"bluewhitered"` : Blue, White, Red color scale. -- `"redwhite"` : Red, White color scale. -- `"whitered"` : White, Red color scale. -- `"whitegreen"` : White, Green color scale. -- `"greenwhite"` : Green, White color scale. -- `"yellowgreen"` : Yellow, Green color scale. -- `"greenyellow"` : Green, Yellow color scale. (default) +- `"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: @@ -526,8 +501,8 @@ 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 two ways. Use the keyword -`dxStyle` to select one of the built-in Excel formats. +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) @@ -620,7 +595,7 @@ 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) -The remaining keywords are defined as above for the `:cellIs` conditional format type. +The remaining keywords are defined as above for `type = :cellIs`. # Examples @@ -655,17 +630,114 @@ Valid values for the `operator` keyword are the following: - `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 the `:cellIs` conditional format type. +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> XLSX.writetable!(s, [columns], ["normal"]) + +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 -# type = :notContainsText -# type = :beginsWith -# type = :endsWith +# type = :containsText, :notContains, :beginsWith or :endsWith Highlight cells in the range that contain (or do not contain), begin or end with a specific text string. @@ -682,12 +754,45 @@ Valid keywords are: `value` gives the literal text to compare (eg. "Hello World") or provides a cell reference (e.g. `"A1"`). -The remaining keywords are optional and are defined as above for the `:cellIs` conditional format type. +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 + ``` +![image|320x500](./images/containsText.png) + # type = :timePeriod @@ -715,19 +820,47 @@ Valid values for the keyword `operator` are the following: - `thisMonth` - `nextMonth` -The remaining keywords are defined as above for the `:cellIs` conditional format type. +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 + ``` +![image|320x500](./images/timePeriod-9thMay2025.png) -# type = :containsErrors -# type = :notContainsErrors -# type = :containsBlanks -# type = :notContainsBlanks -# type = :uniqueValues -# type = :duplicateValues + +# type = :containsErrors, :notContainsErrors, :containsBlanks, :notContainsBlanks, :uniqueValues or :duplicateValues These conditional formattimg options highlight cells that contain or don't contain errors, are blank or not blank, are unique in the range or duplicates within the range. @@ -745,8 +878,47 @@ 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 + ``` +![image|320x500](./images/errorBlank.png) +!!! note "Overlaying conditional formats" + + It is possible to overlay multiple conditional formats to the same range or to + overlapping ranges. Each format is applied in turn to each cell in priority + order which, here, is the order in which they are created. Different format + options may complement or override each other and the finished appearance will + be the resuilt of all formats overlaying each other. + + It is possible to terminate the sequential application of conditional formats to a + cell if the condition related to any format is met. This is achieved by setting the + keyword option `stopIfTrue="true"` in the relevant conditional format. + + While the `stopIfTrue` keyword is available for most conditional formats, it is not + available for `:colorScale`, `:dataBar` or `:iconSet` conditional formats since these + do not apply a specific test in each cell. """ function setConditionalFormat(f, r, type::Symbol; kw...) @@ -962,23 +1134,24 @@ function setCfContainsText(ws::Worksheet, rng::CellRange; 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" +# operator = "beginsWith" formula = "LEFT(__CR__,LEN(\"__txt__\"))=\"__txt__\"" elseif operator == "endsWith" - operator = "endsWith" +# operator = "endsWith" formula = "RIGHT(__CR__,LEN(\"__txt__\"))=\"__txt__\"" else - throw(XLSXError("Invalid operator: $operator. Valid options are: `containsText`, `notContainsText`, `beginsWith`, `endsWith`.")) + throw(XLSXError("Invalid operator: $operator. Valid options are: `containsText`, `notContains`, `beginsWith`, `endsWith`.")) end formula = replace(formula, "__txt__" => value, "__CR__" => string(first(rng))) - cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id)) + 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" @@ -1093,26 +1266,27 @@ function setCfAboveAverage(ws::Worksheet, rng::CellRange; new_dx= get_new_dx(wb, dx) dxid = Add_Cf_Dx(wb, new_dx) - if operator == "aboveAverage" + + if operator == "aboveAverage" cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1") elseif operator == "aboveEqAverage" - cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", equalAverage="1") + cfx = XML.Element("cfRule"; type="aboveAverage", dxfId=Int(dxid.id), priority="1", equalAverage="1") elseif operator == "plus1StdDev" - cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", bottom="1", stdDev="1") + cfx = XML.Element("cfRule"; type="aboveAverage", dxfId=Int(dxid.id), priority="1", stdDev="1") elseif operator == "plus2StdDev" - cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", percent="1", stdDev="2") + cfx = XML.Element("cfRule"; type="aboveAverage", dxfId=Int(dxid.id), priority="1", stdDev="2") elseif operator == "plus3StdDev" - cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", percent="1", stdDev="3") + cfx = XML.Element("cfRule"; type="aboveAverage", dxfId=Int(dxid.id), priority="1", stdDev="3") elseif operator == "belowAverage" - cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", aboveAverage="0", ) + cfx = XML.Element("cfRule"; type="aboveAverage", dxfId=Int(dxid.id), priority="1", aboveAverage="0" ) elseif operator == "belowEqAverage" - cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", aboveAverage="0", equalAverage="1") + cfx = XML.Element("cfRule"; type="aboveAverage", dxfId=Int(dxid.id), priority="1", aboveAverage="0", equalAverage="1") elseif operator == "minus1StdDev" - cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", aboveAverage="0", stdDev="1") + cfx = XML.Element("cfRule"; type="aboveAverage", dxfId=Int(dxid.id), priority="1", aboveAverage="0", stdDev="1") elseif operator == "minus2StdDev" - cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", aboveAverage="0", stdDev="2") + cfx = XML.Element("cfRule"; type="aboveAverage", dxfId=Int(dxid.id), priority="1", aboveAverage="0", stdDev="2") elseif operator == "minus3StdDev" - cfx = XML.Element("cfRule"; type=operator, dxfId=Int(dxid.id), priority="1", aboveAverage="0", stdDev="3") + 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 From c28bca433544799c8f2b6ca046da6e54c2087b41 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 10 May 2025 10:28:03 +0100 Subject: [PATCH 110/154] Trying to finish docs! --- docs/src/formatting.md | 142 ++++++++++++++++++++------- docs/src/images/relative-CellRef.png | Bin 0 -> 8493 bytes docs/src/images/topN.png | Bin 0 -> 16910 bytes src/conditional-formats.jl | 51 +++++++++- 4 files changed, 154 insertions(+), 39 deletions(-) create mode 100644 docs/src/images/relative-CellRef.png create mode 100644 docs/src/images/topN.png diff --git a/docs/src/formatting.md b/docs/src/formatting.md index 6dd8f731..e483840c 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -305,7 +305,7 @@ 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.setRowHeight(s, "A2:A5"; height=25) # Rows 1 to 5 (columns ignored) julia> XLSX.setColumnWidth(s, 5:5:100; width=50) # Every 5th column. ``` @@ -326,13 +326,16 @@ but not otherwise. Such conditional formatting is generally straightforward to a 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 are mainly static. They apply formatting based on the - current cell values, 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. + 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. - Some dynamic conditional formatting is possible in `XLSX.jl`, using Excel native functions, but the range of - functions is currently more limited than Excel itself can provide (work in progress). + Dynamic conditional formatting, using the native Excel conditional format functionality, is possible + using the `setConditionalFormat()` function, giving access to many of Excels's options (but not yet + all of them). ### Static conditional formats @@ -380,7 +383,7 @@ blankmissing(sheet, XLSX.CellRange("B3:L6")) 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 updated. +is applied are subsequently updated. `XLSX.setConditionalFormat(sheet, CellRange, :formatting_type; kwargs...)` @@ -415,7 +418,7 @@ used. All the functions of `Highlight Cell Rules` and `Top/Bottom Rules` are pro ![image|320x500](./images/cell1.png) ![image|100x500](./images/blank.png) ![image|320x500](./images/cell2.png) -Excel uses range of `:formatting_type` values describe these conditional formats and the same values +Excel uses a range of `:formatting_type` values describe these conditional formats and the same values are used here, as follows: - `:cellIs` - `:top10` @@ -445,7 +448,7 @@ to apply the formatting. Valid `operator` values are: - `between` (cell between `value` and `value2`) - `notBetween` (cell not between `value` and `value2`) -Eac of these need a keyword `value` to be specified and, for `between` and `notBetween`, `value2` +Each of these need the keyword `value` to be specified and, for `between` and `notBetween`, `value2` must also be specified. All the cell value formatting types can use one of six built-in formats in Excel as illustrated here @@ -462,8 +465,8 @@ keyword with one of the following values: * `redtext` * `redborder` -Thus, for example, we can create a simple `XLSXFile` from scratch and then apply some -conditional formats to its cells, as follows: +Thus, for example, to create a simple `XLSXFile` from scratch and then apply some +conditional formats to its cells: ```julia julia> columns = [ [1, 2, 3, 4], ["Hey", "You", "Out", "There"], [10.2, 20.3, 30.4, 40.5] ] @@ -571,7 +574,6 @@ julia> XLSX.getConditionalFormats(s) ![image|320x500](./images/custom-cellvalue-example.png) - The `formatting_type` needed for these different functions varies, as do the keyword options. Refer to [XLSX.setConditionalFormat()](@ref) for full details. @@ -587,7 +589,7 @@ custom color scales, too. ![image|320x500](./images/colorScales.png) -In XLSX.jl, the twelve built-in scales are named by their start/mid/end colors as follows +In XLSX.jl, the twelve built-in scales are named by their end/mid/start colors as follows (layout follows image) | | | | | @@ -639,6 +641,96 @@ julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:F22", :colorScale; (In development) +#### formulas + +(In development) + +#### Specifying cell references in Conditional Formats + +##### Cell Ranges + +Cell ranges for conditional formats always use absolute refences. The specified range to which a +conditional format is to be applied is always converted to use 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". There is therefore no need to specify +an absolute cell range when calling `setCondtionalFormat()` + +##### Relative and absolute cell references + +Cell references used to specify `value` or `value2` or in any `formula` may be either absolute +or relative and both can be very useful. it is important to understand which reference style you +need and to specify accordingly. 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: +``` +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 + +``` +![image|320x500](./images/relative-CellRef.png) + +!!! 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 implemented in XLSX.jl yet. + #### Overlaying conditional formats It is possible to overlay multiple conditional formats over each other in a cell range @@ -688,7 +780,7 @@ julia> XLSX.getConditionalFormats(s) 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 te `stopIfTrue` keyword. +achieved with the `stopIfTrue` keyword. For example: @@ -725,26 +817,6 @@ will result in the following, instead: ![image|320x500](./images/no-stop-if-true.png) -#### Specifying cell references in Conditional Formats - -The specified range to which a conditional format is to be applied is always converted to use -absolute cell references so that, for example -```julia -julia> XLSX.setConditionalFormat(s, "A2:C5", :colorScale; colorscale="greenyellow") -``` -will alsways actually refer to the range "$A$2:$C$5". - -However, cell references used to specify `value` or `value2` or in any `formula` may be either -absolute or relative. XLSX.jl makes no assumption about whether these cell references should be -relative or absolute and will accept whatever is specified. - -Relative references (e.g. "G8") used here are interpreted (by Excel) relative to the top-left -cell in the range. For each of the other cells in the formatted range, the reference will be to -the cell with the same offset as cell G8 has to the top-left cell. - -On the other hand, if an absolute cell reference (e.g. "$G$8") is provided, the cell referred to -will be the same for each cell in the formatted range. - ## Working with Merged Cells Worksheets may contain merged cells. XLSX.jl provides functions to identify the merged cells in a worksheet, diff --git a/docs/src/images/relative-CellRef.png b/docs/src/images/relative-CellRef.png new file mode 100644 index 0000000000000000000000000000000000000000..fa6e9a0d3e2583a4b524270e827e97cd668092c9 GIT binary patch literal 8493 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 W0f?4gJLtbSV5li+DOM_&h5jG6nXFU* literal 0 HcmV?d00001 diff --git a/docs/src/images/topN.png b/docs/src/images/topN.png new file mode 100644 index 0000000000000000000000000000000000000000..14d0e94a0551f0618c17453bef564f189fc076ec GIT binary patch literal 16910 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 literal 0 HcmV?d00001 diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index 1fd818ca..eacf38f5 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -600,7 +600,51 @@ 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 + ``` +![image|320x500](./images/topN.png) # type = :aboveAverage @@ -793,7 +837,6 @@ julia> XLSX.setConditionalFormat(s, "A1:A4", :endsWith ; ``` ![image|320x500](./images/containsText.png) - # type = :timePeriod When cells contain dates, this conditional format can be used to highlight cells. @@ -1070,8 +1113,8 @@ function setCfCellIs(ws::Worksheet, rng::CellRange; allcfs = allCfs(ws) # get all conditional format blocks old_cf = getConditionalFormats(allcfs) # extract conditional format info - !isnothing(value) && !is_valid_cellname(value) && !is_valid_sheet_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_sheet_cellname(value2) && isnothing(tryparse(Float64, value2)) && throw(XLSXError("Invalid `value2`: $value2. Must be a number or a CellRef.")) + !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.")) wb=get_workbook(ws) dx = get_dx(dxStyle, format, font, border, fill) @@ -1193,7 +1236,7 @@ function setCfTop10(ws::Worksheet, rng::CellRange; allcfs = allCfs(ws) # get all conditional format blocks old_cf = getConditionalFormats(allcfs) # extract conditional format info - !isnothing(value) && !is_valid_cellname(value) && !is_valid_sheet_cellname(value) && isnothing(tryparse(Float64, value)) && throw(XLSXError("Invalid `value`: $value. Must be a number or a CellRef.")) + !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) From e77a8c28423e040f016d7eba227ec252d5d94c4c Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 10 May 2025 23:29:57 +0100 Subject: [PATCH 111/154] Start adding some tests --- src/conditional-formats.jl | 26 +- test/runtests.jl | 551 +++++++++++++++++++++++++++++++++++-- 2 files changed, 549 insertions(+), 28 deletions(-) diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index eacf38f5..0e08cbef 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -580,7 +580,7 @@ range or in the top (bottom) n% (ie in the top 5 or in the top 5% of values in t The available keywords are: - `operator` : Defines the comparison to make. -- `value` : Gives the for comparison or a cell reference (e.g. `"A1"`). +- `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. @@ -781,14 +781,14 @@ julia> XLSX.setConditionalFormat(s, "A2:A1001", :aboveAverage ; ``` -# type = :containsText, :notContains, :beginsWith or :endsWith +# 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. Valid keywords are: -- `value` : Gives the literal text to match or provides a cell reference (e.g. `"A1"`). +- `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. @@ -1016,6 +1016,8 @@ setCfColorScale(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}} 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...) @@ -1089,6 +1091,8 @@ setCfCellIs(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; 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...) @@ -1147,6 +1151,8 @@ setCfContainsText(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer 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...) @@ -1156,7 +1162,7 @@ setCfContainsText(xl::XLSXFile, sheetcell::AbstractString; kw...)::Int = process setCfContainsText(ws::Worksheet, ref_or_rng::AbstractString; kw...)::Int = process_ranges(setCfContainsText, ws, ref_or_rng; kw...) function setCfContainsText(ws::Worksheet, rng::CellRange; operator::Union{Nothing,String}="containsText", - value::Union{Nothing,String}="", + value::Union{Nothing,String}=nothing, stopIfTrue::Union{Nothing,String}=nothing, dxStyle::Union{Nothing,String}=nothing, format::Union{Nothing,Vector{Pair{String,String}}}=nothing, @@ -1170,7 +1176,7 @@ function setCfContainsText(ws::Worksheet, rng::CellRange; allcfs = allCfs(ws) # get all conditional format blocks old_cf = getConditionalFormats(allcfs) # extract conditional format info -# !isnothing(value) && !is_valid_cellname(value) && !is_valid_sheet_cellname(value) && throw(XLSXError("Invalid `value`: $value. Must be a number or a CellRef.")) + 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) @@ -1190,7 +1196,7 @@ function setCfContainsText(ws::Worksheet, rng::CellRange; # operator = "endsWith" formula = "RIGHT(__CR__,LEN(\"__txt__\"))=\"__txt__\"" else - throw(XLSXError("Invalid operator: $operator. Valid options are: `containsText`, `notContains`, `beginsWith`, `endsWith`.")) + throw(XLSXError("Invalid operator: $type. Valid options are: `containsText`, `notContainsText`, `beginsWith`, `endsWith`.")) end formula = replace(formula, "__txt__" => value, "__CR__" => string(first(rng))) @@ -1213,6 +1219,8 @@ setCfTop10(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}; 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...) @@ -1280,6 +1288,8 @@ setCfAboveAverage(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer 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...) @@ -1349,6 +1359,8 @@ setCfTimePeriod(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}} 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...) @@ -1420,6 +1432,8 @@ setCfContainsBlankErrorUniqDup(ws::Worksheet, ::Colon, col::Union{Integer,UnitRa 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...) diff --git a/test/runtests.jl b/test/runtests.jl index dfa9e2a7..a030ff52 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3247,7 +3247,7 @@ end @testset "Conditional Formats" begin - @testset "ColorScale" begin + @testset "colorScale" begin f=XLSX.newxlsx() s=f[1] for i in 1:5, j in 1:5 @@ -3255,11 +3255,10 @@ end end @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, "A1", :colorScale) # Single cell not allowed -# @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "A1:A1", :colorScale) # One cell cellrange not allowed - XLSX.setConditionalFormat(s, "1:1", :colorScale) - XLSX.setConditionalFormat(s, 2, :, :colorScale; colorscale = "redwhiteblue") - XLSX.setConditionalFormat(s, 3, 1:5, :colorScale; + @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", @@ -3267,35 +3266,72 @@ end mid_col="red", max_type="max", max_col="blue" - ) - XLSX.setConditionalFormat(s, "Sheet1!A4:E4", :colorScale; + )==0 + @test XLSX.setConditionalFormat(s, "Sheet1!A4:E4", :colorScale; min_type="min", min_col="tomato", max_type="max", max_col="gold4" - ) - XLSX.setConditionalFormat(f, "Sheet1!A5:E5", :colorScale; + )==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_throws MethodError XLSX.setConditionalFormat(s, "A1", :colorScale) # Single cell not allowed -# @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, "Sheet1!A1:A2", :colorScale) # Overlaps with existing conditionalFormat range -# @test_throws XLSX.XLSXError XLSX.setConditionalFormat(f, "Sheet1!1:2", :colorScale) # Overlaps with existing conditionalFormat range -# @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, :, :colorScale) # Overlaps with existing conditionalFormat range -# @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, :, :, :colorScale) # Overlaps with existing conditionalFormat range + @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 - XLSX.setConditionalFormat(s, "A1:A5", :colorScale) - XLSX.setConditionalFormat(s, :, 2, :colorScale; colorscale = "redwhiteblue") - XLSX.setConditionalFormat(s, "Sheet1!E:E", :colorScale; colorscale = "greenwhitered") - XLSX.setConditionalFormat(s, 1:5, 3:4, :colorScale; + @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", @@ -3303,7 +3339,7 @@ end 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() @@ -3312,7 +3348,7 @@ end s[i,j] = i+j end - XLSX.setConditionalFormat(s, :, 1:4, :colorScale; + @test XLSX.setConditionalFormat(s, :, 1:4, :colorScale; min_type="min", min_col="green", mid_type="percentile", @@ -3320,6 +3356,477 @@ end mid_col="red", max_type="max", max_col="blue" + ) == 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", :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" + ) + + 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 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) + ] + + 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="beween", + 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"] + ) + + 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 + 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"=>"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="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"] ) end From 323e4244c3f76ff86fb125fa6d7b1147f748d031 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 11 May 2025 17:18:05 +0100 Subject: [PATCH 112/154] Add more tests for conditional formats --- Project.toml | 6 +- src/conditional-formats.jl | 32 ++- test/runtests.jl | 550 ++++++++++++++++++++++++++++++++++++- 3 files changed, 573 insertions(+), 15 deletions(-) diff --git a/Project.toml b/Project.toml index 47e32798..aeffd944 100644 --- a/Project.toml +++ b/Project.toml @@ -17,6 +17,8 @@ ZipArchives = "49080126-0e18-4c2a-b176-c102e4b3760c" [compat] Colors = "0.13.0" +Distributions = "0.25.120" +Random = "1.11.0" Tables = "1" XML = "0.3.5" ZipArchives = "2" @@ -24,7 +26,9 @@ 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/src/conditional-formats.jl b/src/conditional-formats.jl index 0e08cbef..5db57c23 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -291,7 +291,9 @@ function Add_Cf_Dx(wb::Workbook, new_dx::XML.Node)::DxFormat xroot = styles_xmlroot(wb) i, j = get_idces(xroot, "styleSheet", "dxfs") - if isnothing(j) # No existing conditional formats so need to add a block. Push everything lower down one. + 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.")) + #= k, l = get_idces(xroot, "styleSheet", "cellStyles") l += 1 # The dxfs block comes after the cellXfs block. len = length(xroot[k]) @@ -305,6 +307,7 @@ function Add_Cf_Dx(wb::Workbook, new_dx::XML.Node)::DxFormat 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])) @@ -349,6 +352,8 @@ end """ + getConditionalFormats(ws::Worksheet) + Get the conditional formats for a worksheet. # Arguments @@ -427,8 +432,8 @@ instead using the following keywords: - `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` or `num`. Omit for a 2-color scale. -- `mid_val` : The value of the middle value. Omit for a 2-color scale. -- `mid_col` : The color of the middle value. 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` or `num`. - `max_val` : The value of the maximum value. Omit if `max_type="max"`. - `max_col` : The color of the maximum value. @@ -488,7 +493,7 @@ The keyword `operator` defines the comparison to use in the conditional formatti If the condition is met, the format is applied. Valid options are: -- `greaterThan` (cell > `value`) +- `greaterThan` (cell > `value`) (default) - `greaterEqual` (cell >= `value`) - `lessThan` (cell < `value`) - `lessEqual` (cell <= `value`) @@ -595,6 +600,8 @@ 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) +Default keyowrds are `operator="TopN"` and `value="10"`. + The remaining keywords are defined as above for `type = :cellIs`. # Examples @@ -663,7 +670,7 @@ The available keywords are: Valid values for the `operator` keyword are the following: -- `aboveAverage` (cell is above the average of the range) +- `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) @@ -711,8 +718,6 @@ julia> columns=rand(d,1000) 0.7691442442244849 1.0061732938516454 -julia> XLSX.writetable!(s, [columns], ["normal"]) - julia> f=XLSX.newxlsx() XLSXFile("C:\\...\\blank.xlsx") containing 1 Worksheet sheetname size range @@ -784,7 +789,7 @@ julia> XLSX.setConditionalFormat(s, "A2:A1001", :aboveAverage ; # 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. +a specific text string. The default is `containsText`. Valid keywords are: @@ -796,7 +801,8 @@ Valid keywords are: - `border` : defines the border to apply if opting for a custom format. - `fill` : defines the fill to apply if opting for a custom format. -`value` gives the literal text to compare (eg. "Hello World") or provides a cell reference (e.g. `"A1"`). +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`. @@ -855,7 +861,7 @@ Valid values for the keyword `operator` are the following: - `yesterday` - `today` - `tomorrow` -- `last7Days` +- `last7Days` (default) - `lastWeek` - `thisWeek` - `nextWeek` @@ -906,7 +912,7 @@ julia> XLSX.setConditionalFormat(s, "A1:A13", :timePeriod; # type = :containsErrors, :notContainsErrors, :containsBlanks, :notContainsBlanks, :uniqueValues or :duplicateValues These conditional formattimg options highlight cells that contain or don't contain errors, -are blank or not blank, are unique in the range or duplicates within the range. +are blank (default) or not blank, are unique in the range or duplicates within the range. The available keywords are: - `stopIfTrue` : Stops evaluating the conditional formats if this one is true. @@ -996,12 +1002,12 @@ function setConditionalFormat(f, r, c, type::Symbol; kw...) elseif type == :top10 setCfTop10(f, r, c; kw...) elseif type == :aboveAverage - setCfAboveAverage(f, r; kw...) + setCfAboveAverage(f, r, c; kw...) elseif type == :timePeriod setCfTimePeriod(f, r, c; kw...) elseif type ∈ [:containsText, :notContainsText, :beginsWith, :endsWith] setCfContainsText(f, r, c; operator=String(type), kw...) - elseif type ∈ [:containsBlanks, :notContainsBlanks, :containsErrors, :notContainsErrors, duplicateValues, uniqueValues] + elseif type ∈ [:containsBlanks, :notContainsBlanks, :containsErrors, :notContainsErrors, :duplicateValues, :uniqueValues] setCfContainsBlankErrorUniqDup(f, r, c; operator=String(type), kw...) # elseif type == :iconSet # throw(XLSXError("Icon sets are not yet implemented.")) diff --git a/test/runtests.jl b/test/runtests.jl index a030ff52..1c539f69 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 @@ -3831,6 +3832,553 @@ end 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 + XLSX.setConditionalFormat(s, "A1:A5", :aboveAverage) + XLSX.setConditionalFormat(s, :, 2, :aboveAverage; dxStyle = "redborder") + XLSX.setConditionalFormat(s, "Sheet1!E:E", :aboveAverage; dxStyle = "redfilltext") + 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 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 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="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("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)) == 25 + @test XLSX.getConditionalFormats(s) == [ + XLSX.CellRange("A1:J10") => (type = "timePeriod", priority = 24), + XLSX.CellRange("A1:J10") => (type = "timePeriod", priority = 25), + XLSX.CellRange("A1:J3") => (type = "timePeriod", priority = 20), + XLSX.CellRange("A2:J4") => (type = "timePeriod", priority = 14), + XLSX.CellRange("A2:J4") => (type = "timePeriod", priority = 21), + XLSX.CellRange("A1:J2") => (type = "timePeriod", priority = 13), + XLSX.CellRange("A1:J2") => (type = "timePeriod", priority = 17), + XLSX.CellRange("A1:A2") => (type = "timePeriod", priority = 12), + XLSX.CellRange("A1:C3") => (type = "timePeriod", priority = 10), + XLSX.CellRange("A1:A1") => (type = "timePeriod", priority = 9), + XLSX.CellRange("A1:A1") => (type = "timePeriod", priority = 11), + 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 = 15), + XLSX.CellRange("A1:C10") => (type = "timePeriod", priority = 16), + XLSX.CellRange("A1:C10") => (type = "timePeriod", priority = 18), + XLSX.CellRange("A1:C10") => (type = "timePeriod", priority = 19), + XLSX.CellRange("A1:C10") => (type = "timePeriod", priority = 22), + XLSX.CellRange("A1:C10") => (type = "timePeriod", priority = 23), + 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 "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 From 8ec009c988100c5a0de7566c1acfa9f4542576cc Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 11 May 2025 20:02:40 +0100 Subject: [PATCH 113/154] Escape formulas properly --- src/conditional-formats.jl | 12 ++++++------ test/runtests.jl | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index 5db57c23..301b67ba 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -1142,9 +1142,9 @@ function setCfCellIs(ws::Worksheet, rng::CellRange; cfx["operator"] = operator - push!(cfx, XML.Element("formula", XML.Text(value))) + push!(cfx, XML.Element("formula", XML.Text(XML.escape(value)))) if !isnothing(value2) && operator ∈ needsValue2 - push!(cfx, XML.Element("formula", XML.Text(value2))) + push!(cfx, XML.Element("formula", XML.Text(XML.escape(value2)))) end update_worksheet_cfx!(allcfs, cfx, ws, rng) @@ -1213,7 +1213,7 @@ function setCfContainsText(ws::Worksheet, rng::CellRange; end cfx["operator"]=operator cfx["text"]=value - push!(cfx, XML.Element("formula", XML.Text(formula))) + push!(cfx, XML.Element("formula", XML.Text(XML.escape(formula)))) update_worksheet_cfx!(allcfs, cfx, ws, rng) @@ -1424,9 +1424,9 @@ function setCfTimePeriod(ws::Worksheet, rng::CellRange; if !isnothing(stopIfTrue) && stopIfTrue == "true" cfx["stopIfTrue"] = "1" end - cfx["operator"] = operator + cfx["timePeriod"] = operator - push!(cfx, XML.Element("formula", XML.Text(formula))) + push!(cfx, XML.Element("formula", XML.Text(XML.escape(formula)))) update_worksheet_cfx!(allcfs, cfx, ws, rng) @@ -1489,7 +1489,7 @@ function setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::CellRange; if !isnothing(stopIfTrue) && stopIfTrue == "true" cfx["stopIfTrue"] = "1" end - formula !="" && push!(cfx, XML.Element("formula", XML.Text(formula))) + formula !="" && push!(cfx, XML.Element("formula", XML.Text(XML.escape(formula)))) update_worksheet_cfx!(allcfs, cfx, ws, rng) diff --git a/test/runtests.jl b/test/runtests.jl index 1c539f69..c5f8f1ca 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3406,7 +3406,7 @@ end fill = ["pattern" => "none", "bgColor"=>"FFFFC7CE"], format = ["format"=>"0.00%"], font = ["color"=>"blue", "bold"=>"true"] - ) ==0 + )==0 @test XLSX.setConditionalFormat(s, "Sheet1!A4:E4", :cellIs; operator="greaterThan", @@ -3548,7 +3548,7 @@ end fill = ["pattern" => "none", "bgColor"=>"FFFFC7CE"], format = ["format"=>"0.00%"], font = ["color"=>"blue", "bold"=>"true"] - ) ==0 + )==0 @test XLSX.setConditionalFormat(s, "Sheet1!A4:E4", :containsText; operator = "notContainsText", @@ -3694,7 +3694,7 @@ end operator="topN", value="5", stopIfTrue="true", - fill = ["pattern" => "none", "bgColor"=>"FFFFC7CE"], + fill = ["pattern" => "none", "bgColor"=>"green"], border = ["style"=>"thick", "color"=>"coral"], font = ["color"=>"blue", "bold"=>"true", "strike"=>"true"] )==0 @@ -3712,7 +3712,7 @@ end fill = ["pattern" => "none", "bgColor"=>"FFFFC7CE"], border = ["style"=>"thick", "color"=>"coral"], font = ["color"=>"blue", "bold"=>"true", "strike"=>"true"] - ) ==0 + )==0 @test XLSX.setConditionalFormat(s, 1:10, 1:10, :top10; operator="bottomN%", value="30", @@ -3877,7 +3877,7 @@ end fill = ["pattern" => "none", "bgColor"=>"FFFFCFCE"], border = ["style"=>"thick", "color"=>"coral"], font = ["color"=>"yellow", "bold"=>"true", "strike"=>"true"] - ) ==0 + )==0 @test XLSX.setConditionalFormat(s, 1:1001, 1:3, :aboveAverage; operator="minus1StdDev", stopIfTrue="true", @@ -3890,7 +3890,7 @@ end fill = ["pattern" => "none", "bgColor"=>"FFFFCFCE"], border = ["style"=>"thick", "color"=>"gray"], font = ["color"=>"yellow", "bold"=>"true", "strike"=>"true"] - ) ==0 + )==0 @test XLSX.setConditionalFormat(s, 1:1001, 1:3, :aboveAverage; operator="belowAverage", fill = ["pattern" => "none", "bgColor"=>"yellow"], @@ -4059,7 +4059,7 @@ end fill = ["pattern" => "none", "bgColor"=>"FFFFC7CE"], border = ["style"=>"thick", "color"=>"coral"], font = ["color"=>"blue", "bold"=>"true", "strike"=>"true"] - ) ==0 + )==0 @test XLSX.setConditionalFormat(s, 1:10, 1:3, :timePeriod; operator="lastMonth", stopIfTrue="true", @@ -4073,7 +4073,7 @@ end fill = ["pattern" => "none", "bgColor"=>"FFFFCFCE"], border = ["style"=>"thick", "color"=>"coral"], font = ["color"=>"yellow", "bold"=>"true", "strike"=>"true"] - ) ==0 + )==0 @test XLSX.setConditionalFormat(s, 1:10, 1:3, :timePeriod; operator="last7Days", stopIfTrue="true", From 0b8297b7c5f43ea6fdeacb1cff1f789cbc9708cb Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 11 May 2025 20:17:08 +0100 Subject: [PATCH 114/154] Change compats but I don't fully umderstand the compat system! --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index aeffd944..8fdcd1ee 100644 --- a/Project.toml +++ b/Project.toml @@ -17,7 +17,7 @@ ZipArchives = "49080126-0e18-4c2a-b176-c102e4b3760c" [compat] Colors = "0.13.0" -Distributions = "0.25.120" +Distributions = "0.25.0" Random = "1.11.0" Tables = "1" XML = "0.3.5" From 19f8451ff8f902d34fb51ebb39b7dcfab1d5b595 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 11 May 2025 20:19:08 +0100 Subject: [PATCH 115/154] Changed for Random as well as Distributions! --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 8fdcd1ee..62a5d4e7 100644 --- a/Project.toml +++ b/Project.toml @@ -18,7 +18,7 @@ ZipArchives = "49080126-0e18-4c2a-b176-c102e4b3760c" [compat] Colors = "0.13.0" Distributions = "0.25.0" -Random = "1.11.0" +Random = "1.10.0" Tables = "1" XML = "0.3.5" ZipArchives = "2" From bab91ea2694120fd3efcb280afa5154a6c1e51a3 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 16 May 2025 18:01:36 +0100 Subject: [PATCH 116/154] Add `expression` to the list of supported types for conditional formats --- docs/src/formatting.md | 165 +- docs/src/images/averageComparison.png | Bin 0 -> 30453 bytes docs/src/images/caseSensitiveComparison.png | Bin 0 -> 4988 bytes docs/src/images/expression.png | Bin 0 -> 17766 bytes docs/src/images/relativeComparison.png | Bin 0 -> 18203 bytes docs/src/images/simpleComparison.png | Bin 0 -> 24114 bytes src/cellformat-helpers.jl | 443 ++-- src/cellformats.jl | 72 +- src/cellref.jl | 19 +- src/conditional-formats.jl | 124 +- src/styles.jl | 62 +- src/workbook.jl | 61 +- src/worksheet.jl | 133 +- src/write.jl | 92 +- test/runtests.jl | 2312 +++++++++++-------- 15 files changed, 1986 insertions(+), 1497 deletions(-) create mode 100644 docs/src/images/averageComparison.png create mode 100644 docs/src/images/caseSensitiveComparison.png create mode 100644 docs/src/images/expression.png create mode 100644 docs/src/images/relativeComparison.png create mode 100644 docs/src/images/simpleComparison.png diff --git a/docs/src/formatting.md b/docs/src/formatting.md index e483840c..3d633b67 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -110,7 +110,7 @@ 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 . +-1 # Returns -1 on a range. julia> XLSX.setBorder(s, "A1:CV100"; allsides = ["style" => "thin", "color" => "black"]) -1 @@ -166,7 +166,6 @@ it uniformly to each cell in the range. This ensures that all of font, fill, bor 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 ``` @@ -180,7 +179,7 @@ consider the following worksheet, which has very hetrogeneous formatting across 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 ``` @@ -191,7 +190,7 @@ This merges the new top border definition with the other, existing attributes, t Alternatively, we can apply `setUniformBorder()`, which will update the borders of cell `B2` and then apply all the border formatting to the other cells, overwriting the previous settings: -``` +```julia julia> XLSX.setUniformBorder(s, "B2,D2,F2"; top=["style"=>"thick", "color"=>"red"]) 4 ``` @@ -203,7 +202,7 @@ attributes 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 @@ -217,7 +216,7 @@ Which results in all formatting attributes being entirely consistent across the ### 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() @@ -260,7 +259,7 @@ Using `setUniformStyles` : 0.589316 seconds (14.00 M allocations: 416.628 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"], @@ -287,7 +286,6 @@ It is possible to use non-contiguous ranges to copy format attributes from any c 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! @@ -304,7 +302,6 @@ Two functions offer the ability to set the column width and row height within a 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. @@ -334,7 +331,7 @@ but not otherwise. Such conditional formatting is generally straightforward to a 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 many of Excels's options (but not yet + using the `setConditionalFormat()` function, giving access to many of Excel's options (but not yet all of them). ### Static conditional formats @@ -387,9 +384,8 @@ is applied are subsequently updated. `XLSX.setConditionalFormat(sheet, CellRange, :formatting_type; kwargs...)` -Excel uses range of `:formatting_type` values describe these conditional formats and the same values +Excel uses a range of `:formatting_type` values to describe these conditional formats and the same values are used here, as follows: -- `:colorScale` - `:cellIs` - `:top10` - `:aboveAverage` @@ -404,6 +400,10 @@ are used here, as follows: - `:notContainsBlanks` - `:uniqueValues` - `:duplicateValues` +- `:expression` +- `:dataBar` +- `:colorScale` +- `:iconSet` Use of these different `:formatting_type`s is illustrated in the following sections. For more details on the range of `:formatting_types` and their associated keyword @@ -414,7 +414,7 @@ options, refer to [XLSX.setConditionalFormat()](@ref). 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 Cell Rules` and `Top/Bottom Rules` are provided. +used. All the functions of `Highlight Cells Rules` and `Top/Bottom Rules` are provided. ![image|320x500](./images/cell1.png) ![image|100x500](./images/blank.png) ![image|320x500](./images/cell2.png) @@ -482,12 +482,11 @@ julia> colnames = [ "integers", "strings", "floats" ] "floats" julia> f=XLSX.newxlsx() -XLSXFile(""C:\...\blank.xlsx"") containing 1 Worksheet +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) @@ -577,6 +576,135 @@ julia> XLSX.getConditionalFormats(s) The `formatting_type` needed for these different functions varies, as do the keyword options. Refer to [XLSX.setConditionalFormat()](@ref) for full details. +#### Expressions + +It is possible to use an Excel formula directly to determine whether to apply a conditional formula. +Any expression that evaluates to true or false can be used. + +![image|320x500](./images/expression.png) + +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 +``` +![image|320x500](./images/simpleComparison.png) + +Column A uses relative referencing. Columns C and D use an absolute reference for the column but not the row. + +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 +``` +![image|320x500](./images/averageComparison.png) + +(Row 13 above is the average of each column, calculated in Excel) + +When a formula uses relative references, the relative position 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 +``` +![image|320x500](./images/relativeComparison.png) + +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 +``` +![image|320x500](./images/caseSensitiveComparison.png) + #### Data Bar (In development) @@ -637,11 +765,7 @@ julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:F22", :colorScale; ``` ![image|320x500](./images/custom-colorscale.png) -#### Icon Sets - -(In development) - -#### formulas +#### Icon Set (In development) @@ -740,7 +864,6 @@ order (priority 1 is higher priority than priority 2) which is the same as the o which they were defined with `setConditionalFormat`. ```julia - julia> s[1:5, 1:3] 5×3 Matrix{Any}: "first" "middle" "last" diff --git a/docs/src/images/averageComparison.png b/docs/src/images/averageComparison.png new file mode 100644 index 0000000000000000000000000000000000000000..ef3ca499bcfb7d579c53ef55d08be775d622c256 GIT binary patch literal 30453 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

Jv~nd1Ofp+?))e6{5zOa4}QK@IjbM5 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 Bwch{$ literal 0 HcmV?d00001 diff --git a/docs/src/images/dataBarOptions.png b/docs/src/images/dataBarOptions.png new file mode 100644 index 0000000000000000000000000000000000000000..a06fc418ecb603e861bd88f2cd2391006b0f0d89 GIT binary patch literal 15045 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 literal 0 HcmV?d00001 diff --git a/docs/src/images/dataBars.png b/docs/src/images/dataBars.png new file mode 100644 index 0000000000000000000000000000000000000000..230d5dc0ae9ce18acd2d9f617b1e8fd02c11c69a GIT binary patch literal 23073 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 literal 0 HcmV?d00001 diff --git a/docs/src/images/iconKey.png b/docs/src/images/iconKey.png new file mode 100644 index 0000000000000000000000000000000000000000..29e73f665d7692371039e3be4338e83f00280444 GIT binary patch literal 23678 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 literal 0 HcmV?d00001 diff --git a/docs/src/images/iconSets.png b/docs/src/images/iconSets.png new file mode 100644 index 0000000000000000000000000000000000000000..0971748ef23417bfa6893713004d10be35b695fa GIT binary patch literal 50539 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

I77vbMcpyOw!WyHwn5`cv>u7E(X%L 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 R;Gt_2B{@~uB59*H{|`m3^|Sy0 literal 0 HcmV?d00001 diff --git a/src/XLSX.jl b/src/XLSX.jl index d29ae627..ebc3aa6e 100644 --- a/src/XLSX.jl +++ b/src/XLSX.jl @@ -10,6 +10,7 @@ import Tables import Unicode import Colors import Base.convert +import UUIDs 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 @@ -28,8 +29,9 @@ include("workbook.jl") include("worksheet.jl") include("cell.jl") include("styles.jl") -include("cellformats.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") diff --git a/src/conditional-format-helpers.jl b/src/conditional-format-helpers.jl new file mode 100644 index 00000000..09ed539c --- /dev/null +++ b/src/conditional-format-helpers.jl @@ -0,0 +1,321 @@ +# +# ---- 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 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 index 4c12892f..8f22445a 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -1,28 +1,27 @@ const needsValue2::Vector{String} = ["between", "notBetween"] -const highlights::Dict{String,Dict{String,Dict{String, String}}} = Dict( +const highlights::Dict{String,Dict{String,Dict{String,String}}} = Dict( "redfilltext" => Dict( - "font" => Dict("color"=>"FF9C0006"), - "fill" => Dict("pattern" => "solid", "bgColor"=>"FFFFC7CE") + "font" => Dict("color" => "FF9C0006"), + "fill" => Dict("pattern" => "solid", "bgColor" => "FFFFC7CE") ), "yellowfilltext" => Dict( - "font" => Dict("color"=>"FF9C5700"), - "fill" => Dict("pattern" => "solid", "bgColor"=>"FFFFEB9C") + "font" => Dict("color" => "FF9C5700"), + "fill" => Dict("pattern" => "solid", "bgColor" => "FFFFEB9C") ), "greenfilltext" => Dict( - "font" => Dict("color"=>"FF006100"), - "fill" => Dict("pattern" => "solid", "bgColor"=>"FFC6EFCE") + "font" => Dict("color" => "FF006100"), + "fill" => Dict("pattern" => "solid", "bgColor" => "FFC6EFCE") ), "redfill" => Dict( - "fill" => Dict("pattern" => "solid", "bgColor"=>"FFFFC7CE") + "fill" => Dict("pattern" => "solid", "bgColor" => "FFFFC7CE") ), "redtext" => Dict( - "font" => Dict("color"=>"FF9C0006"), + "font" => Dict("color" => "FF9C0006"), ), "redborder" => Dict( - "border" => Dict("color"=>"FF9C0006", "style"=>"thin") + "border" => Dict("color" => "FF9C0006", "style" => "thin") ) -) # for type = :Cell - +) 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( @@ -133,249 +132,212 @@ const colorscales::Dict{String,XML.Node} = Dict( # Defines the 12 standard, b ) ) ) - +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"), + ) + ), + "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"), + ) + ), + "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"), + ) + ), + "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"), + ) + ), + "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"), + ) + ), + "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"), + ) + ), + "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"), + ) + ), + "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="33"), + XML.h.cfvo(type="percent", val="67"), + ) + ), + "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="20"), + XML.h.cfvo(type="percent", val="40"), + XML.h.cfvo(type="percent", val="60"), + XML.h.cfvo(type="percent", val="80"), + ) + ), + "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"), + ) + ), + # 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())))", + "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)))" ) -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 -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 is 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 - -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 - -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.")) - #= - 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 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 allCfs(ws::Worksheet) - 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 - """ getConditionalFormats(ws::Worksheet) @@ -389,7 +351,7 @@ Return a vector of pairs: CellRange => NamedTuple{type::String, priority::Int}}. """ -getConditionalFormats(ws::Worksheet) = getConditionalFormats(allCfs(ws)) +getConditionalFormats(ws::Worksheet) = append!(getConditionalFormats(allCfs(ws)), getConditionalExtFormats(allExtCfs(ws))) function getConditionalFormats(allcfnodes::Vector{XML.Node})::Vector{Pair{CellRange,NamedTuple}} allcfs = Vector{Pair{CellRange,NamedTuple}}() for cf in allcfnodes @@ -401,19 +363,47 @@ function getConditionalFormats(allcfnodes::Vector{XML.Node})::Vector{Pair{CellRa end return allcfs end +function getConditionalExtFormats(allcfnodes::Vector{XML.Node})::Vector{Pair{CellRange,NamedTuple}} + allcfs = Vector{Pair{CellRange,NamedTuple}}() + for cf in allcfnodes + let t, p, r, rule = false, ref = false + if XML.tag(cf) != "x14:conditionalFormatting" + throw(XLSXEror("Something wrong here")) + end + 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"] + p = parse(Int, child["priority"]) + rule = true + 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 + 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. +The `type` argument specifies which of Excel's conditional format types will be applied. + Valid options for `type` are: -- `:colorScale` - `:cellIs` - `:top10` - `:aboveAverage` @@ -428,76 +418,12 @@ Valid options for `type` are: - `:notContainsBlanks` - `:uniqueValues` - `:duplicateValues` +- `:dataBar` +- `:colorScale` +- `:iconSet` -The `type` argument determines which type of conditional formatting is being defined. Keyword options differ according to the `type` specified, as set out below. -# 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` or `num`. -- `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` or `num`. 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` or `num`. -- `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 either a cell reference (e.g. `"A1"`) -or a number. If a cell reference is used, it will be converted to an absolute cell reference -when writing to an XLSXFile. - -Colors can be specified using an 8-digit hex string (e.g. `FF0000FF` for blue) or any named -color from Colors.jl ([here](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 = :cellIs Defines a conditional format based on the value of each cell in a range. @@ -1018,6 +944,145 @@ julia> XLSX.setConditionalFormat(s, "A1:E5", :expression; formula = "E5<50", dxS ``` +# type = :dataBar + +(In development) + +# 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) should usually 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 ([here](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 kwyword `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", "percentile" or "formula" +- `min_val` (default: "33" (3 icons), "25" (4 icons) or "20" (5 icons)) +- `mid_type` = "percent" (default), "percentile", "num", "percentile" or "formula" +- `mid_val` (default: "50" (4 icons), "40" (5 icons)) +- `mid2_type` = "percent" (default), "percentile", "num", "percentile" or "formula" +- `mid2_val` (default: "60" (5 icons)) +- `max_type` = "percent" (default), "percentile", "num", "percentile" 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_* and max_*), four-icon sets require three +thresholds (with the addition of mid_*) and five-icon sets require four thresholds (mid2_*). +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 `mid_value` and `mid_type` keywords and `mid2_value` and +`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 is 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. + !!! note "Overlaying conditional formats" It is possible to overlay multiple conditional formats to the same range or to @@ -1052,11 +1117,11 @@ function setConditionalFormat(f, r, type::Symbol; kw...) setCfContainsBlankErrorUniqDup(f, r; operator=String(type), kw...) elseif type == :expression setCfFormula(f, r; kw...) -# elseif type == :iconSet -# throw(XLSXError("Icon sets are not yet implemented.")) -# elseif type == :dataBar -# throw(XLSXError("Data bars are not yet implemented.")) -else + elseif type == :iconSet + setCfIconSet(f, r; kw...) + # elseif type == :dataBar + # throw(XLSXError("Data bars are not yet implemented.")) + else throw(XLSXError("Invalid conditional format type: $type.")) end end @@ -1078,88 +1143,14 @@ function setConditionalFormat(f, r, c, type::Symbol; kw...) setCfContainsBlankErrorUniqDup(f, r, c; operator=String(type), kw...) elseif type == :expression setCfFormula(f, r, c; kw...) -# elseif type == :iconSet -# throw(XLSXError("Icon sets are not yet implemented.")) -# elseif type == :dataBar -# throw(XLSXError("Data bars are not yet implemented.")) + elseif type == :iconSet + setCfIconSet(f, r, c; kw...) + # elseif type == :dataBar + # throw(XLSXError("Data bars are not yet implemented.")) else throw(XLSXError("Invalid conditional format type: $type.")) end 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; - 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", -)::Int - - !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) - - allcfs = allCfs(ws) # get all conditional format blocks - old_cf = getConditionalFormats(allcfs) # 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"] || throw(XLSXError("Invalid min_type: $min_type. Valid options are: min, percentile, percent, num.")) - isnothing(min_val) || is_valid_cellname(min_val) || !is_valid_sheet_cellname(min_val) || !isnothing(tryparse(Float64,min_val)) || throw(XLSXError("Invalid mid_type: $min_val. Valid options are a CellRef (e.g. `A1`) or a number.")) - isnothing(mid_type) || mid_type in ["percentile", "percent", "num"] || throw(XLSXError("Invalid mid_type: $mid_type. Valid options are: percentile, percent, num.")) - isnothing(mid_val) || is_valid_cellname(mid_val) || !is_valid_sheet_cellname(mid_val) || !isnothing(tryparse(Float64,mid_val)) || throw(XLSXError("Invalid mid_type: $mid_val. Valid options are a CellRef (e.g. `A1`) or a number.")) - max_type in ["max", "percentile", "percent", "num"] || throw(XLSXError("Invalid max_type: $max_type. Valid options are: max, percentile, percent, num.")) - isnothing(max_val) || is_valid_cellname(max_val) || !is_valid_sheet_cellname(max_val) || !isnothing(tryparse(Float64,max_val)) || throw(XLSXError("Invalid mid_type: $max_val. Valid options are a CellRef (e.g. `A1`) or a number.")) - - min_val = convertref(min_val) - mid_val = convertref(mid_val) - max_val = convertref(max_val) - - 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) ? nothing : 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) ? nothing : XML.h.color(rgb=get_color(mid_col)), - XML.h.color(rgb=get_color(max_col)) - ) - ) - - else - if !haskey(colorscales, colorscale) - throw(XLSXError("Invalid color scale: $colorscale. Valid options are: $(keys(colorscales)).")) - end - cfx=colorscales[colorscale] - cfx["priority"] = new_pr - end - - update_worksheet_cfx!(allcfs, cfx, ws, rng) - - end - - return 0 -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...) @@ -1190,21 +1181,21 @@ function setCfCellIs(ws::Worksheet, rng::CellRange; !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) allcfs = allCfs(ws) # get all conditional format blocks - old_cf = getConditionalFormats(allcfs) # extract conditional format info + 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.")) - wb=get_workbook(ws) + wb = get_workbook(ws) dx = get_dx(dxStyle, format, font, border, fill) - new_dx= get_new_dx(wb, dx) + 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])) + 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 + 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 @@ -1249,26 +1240,26 @@ function setCfContainsText(ws::Worksheet, rng::CellRange; !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) allcfs = allCfs(ws) # get all conditional format blocks - old_cf = getConditionalFormats(allcfs) # extract conditional format info + 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) + wb = get_workbook(ws) dx = get_dx(dxStyle, format, font, border, fill) - new_dx= get_new_dx(wb, dx) + new_dx = get_new_dx(wb, dx) dxid = Add_Cf_Dx(wb, new_dx) - type=operator + 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" + # operator = "beginsWith" formula = "LEFT(__CR__,LEN(\"__txt__\"))=\"__txt__\"" elseif operator == "endsWith" -# operator = "endsWith" + # operator = "endsWith" formula = "RIGHT(__CR__,LEN(\"__txt__\"))=\"__txt__\"" else throw(XLSXError("Invalid operator: $type. Valid options are: `containsText`, `notContainsText`, `beginsWith`, `endsWith`.")) @@ -1276,12 +1267,12 @@ function setCfContainsText(ws::Worksheet, rng::CellRange; 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 + 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 + cfx["operator"] = operator + cfx["text"] = value push!(cfx, XML.Element("formula", XML.Text(XML.escape(formula)))) update_worksheet_cfx!(allcfs, cfx, ws, rng) @@ -1317,31 +1308,31 @@ function setCfTop10(ws::Worksheet, rng::CellRange; !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) allcfs = allCfs(ws) # get all conditional format blocks - old_cf = getConditionalFormats(allcfs) # extract conditional format info + 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) + wb = get_workbook(ws) dx = get_dx(dxStyle, format, font, border, fill) - new_dx= get_new_dx(wb, dx) + 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" + percent = "1" elseif operator == "bottomN" - bottom="1" + bottom = "1" elseif operator == "bottomN%" - percent="1" - bottom="1" + 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 + 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 @@ -1385,13 +1376,13 @@ function setCfAboveAverage(ws::Worksheet, rng::CellRange; !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) allcfs = allCfs(ws) # get all conditional format blocks - old_cf = getConditionalFormats(allcfs) # extract conditional format info + old_cf = getConditionalFormats(ws) # extract conditional format info -# isnothing(tryparse(Float64, value)) && throw(XLSXError("Invalid `value`: $value. Must be a number.")) + # isnothing(tryparse(Float64, value)) && throw(XLSXError("Invalid `value`: $value. Must be a number.")) - wb=get_workbook(ws) + wb = get_workbook(ws) dx = get_dx(dxStyle, format, font, border, fill) - new_dx= get_new_dx(wb, dx) + new_dx = get_new_dx(wb, dx) dxid = Add_Cf_Dx(wb, new_dx) @@ -1406,7 +1397,7 @@ function setCfAboveAverage(ws::Worksheet, rng::CellRange; 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" ) + 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" @@ -1419,7 +1410,7 @@ function setCfAboveAverage(ws::Worksheet, rng::CellRange; 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 + 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 @@ -1456,7 +1447,7 @@ function setCfTimePeriod(ws::Worksheet, rng::CellRange; !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) allcfs = allCfs(ws) # get all conditional format blocks - old_cf = getConditionalFormats(allcfs) # extract conditional format info + old_cf = getConditionalFormats(ws) # extract conditional format info if operator == "yesterday" formula = "FLOOR(__CR__,1)=TODAY()-1" @@ -1483,18 +1474,18 @@ function setCfTimePeriod(ws::Worksheet, rng::CellRange; end formula = replace(formula, "__CR__" => string(first(rng))) - wb=get_workbook(ws) + 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 + 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) @@ -1529,7 +1520,7 @@ function setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::CellRange; !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) allcfs = allCfs(ws) # get all conditional format blocks - old_cf = getConditionalFormats(allcfs) # extract conditional format info + old_cf = getConditionalFormats(ws) # extract conditional format info if operator == "containsBlanks" formula = "LEN(TRIM(__CR__))=0" @@ -1548,17 +1539,17 @@ function setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::CellRange; end formula = replace(formula, "__CR__" => string(first(rng))) - wb=get_workbook(ws) + 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 + 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)))) + formula != "" && push!(cfx, XML.Element("formula", XML.Text(XML.escape(formula)))) update_worksheet_cfx!(allcfs, cfx, ws, rng) @@ -1592,22 +1583,254 @@ function setCfFormula(ws::Worksheet, rng::CellRange; !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) allcfs = allCfs(ws) # get all conditional format blocks - old_cf = getConditionalFormats(allcfs) # extract conditional format info + old_cf = getConditionalFormats(ws) # extract conditional format info - wb=get_workbook(ws) + 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 + 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))*")"))) + + 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; + 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", +)::Int + + !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 + + 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.")) + min_type == "formula" || isnothing(min_val) || is_valid_cellname(min_val) || is_valid_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_cellname(mid_val) || is_valid_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.")) + max_type == "formula" || isnothing(max_val) || is_valid_cellname(max_val) || is_valid_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.")) + + min_val = isnothing(min_val) ? nothing : XML.escape(uppercase_unquoted(min_val)) + mid_val = isnothing(mid_val) ? nothing : XML.escape(uppercase_unquoted(mid_val)) + max_val = isnothing(max_val) ? nothing : XML.escape(uppercase_unquoted(max_val)) + + 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 color scale: $iconset. Valid options are: $(keys(iconsets)).")) + 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; + 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 + )::Int + + !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 + 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(mid_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_cellname(min_val) || is_valid_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_cellname(mid_val) || !is_valid_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_cellname(mid2_val) || is_valid_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_cellname(max_val) || is_valid_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] + val = isnothing(val) ? nothing : XML.escape(uppercase_unquoted(val)) + end + if !haskey(iconsets, iconset) + throw(XLSXError("Invalid color scale: $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"))) + for _ in 1:2 + push!(cfx[1], copynode(cfvo)) + end + if 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)) + 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)) + 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) + cfx[1][i+1][1] = XML.Element("xm:f", XML.Text(val)) + end + if !isnothing(gte) && gte == "false" + cfx[1][i+1]["gte"] = "0" + end + end + if iconset == "Custom" + licons=length(icon_list) + if licons == 0 + throw(XLSXError("No custom icons specified. Must specify between two and four icons.")) + elseif licons Date: Fri, 23 May 2025 12:58:54 +0100 Subject: [PATCH 118/154] Add tests for `:iconSet` conditional formats --- docs/src/formatting.md | 38 ++-- src/cell.jl | 2 +- src/cellformat-helpers.jl | 91 +++++---- src/conditional-formats.jl | 166 +++++++++------- src/sst.jl | 5 +- src/table.jl | 7 +- test/runtests.jl | 396 +++++++++++++++++++++++++++++++++++-- 7 files changed, 556 insertions(+), 149 deletions(-) diff --git a/docs/src/formatting.md b/docs/src/formatting.md index 49c7a145..8bcb4b22 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -840,8 +840,8 @@ relate the cell value to the range of values in the cell range to which the cond 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 + Range ┌─────────────────┬─────────────────┬─────────────────┬────────────────┐ Range + Minimum ->│ Icon 1 │ Icon 2 │ Icon 3 │ Icon 4 │<- Maximum `min_val` `mid_val` `max_val` threshold threshold threshold ``` @@ -865,7 +865,9 @@ formula. 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") +julia> XLSX.setConditionalFormat(s, "A1:A10", :iconSet; + min_type="num", max_type="num", + min_val="2", max_val="9") 0 ``` ![image|320x500](./images/newValIconSet.png) @@ -875,8 +877,7 @@ use `reverse="true"` and to change the default comparison from `>=` to `>` set ` 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\""] - ) + ["normal", "showVal=\"false\"", "reverse=\"true\"", "min_gte=\"false\""]) julia> XLSX.setConditionalFormat(s, "A2:A11", :iconSet; min_type="num", max_type="num", @@ -943,23 +944,23 @@ Specifying too few icons throws an error, any extra will simply be ignored. ##### Cell Ranges -Cell ranges for conditional formats always use absolute refences. The specified range to which a -conditional format is to be applied is always converted to use absolute cell references so that, +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". There is therefore no need to specify -an absolute cell range when calling `setCondtionalFormat()` +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` may be either absolute -or relative and both can be very useful. it is important to understand which reference style you -need and to specify accordingly. 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: -``` +Cell references used to specify `value` or `value2` or in any `formula` (for `:expression` type +conditional formats only) may be either absolute or relative, and both can be useful. It is +important to understand which reference style you need and to specify accordingly. 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 ``` @@ -1018,11 +1019,16 @@ julia> XLSX.setConditionalFormat(s, "B2:B6", :cellIs; operator="greaterThan", va ``` ![image|320x500](./images/relative-CellRef.png) +!!! 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 implemented in XLSX.jl yet. + same sheet. This functionality is not universally implemented in XLSX.jl yet. #### Overlaying conditional formats diff --git a/src/cell.jl b/src/cell.jl index 7a3ba466..ba763622 100644 --- a/src/cell.jl +++ b/src/cell.jl @@ -317,7 +317,7 @@ end # See also XLSX.isdate1904. function excel_value_to_datetime(x::Float64, _is_date_1904::Bool) :: Dates.DateTime if x < 0 - throw(XLSXError("Cannot have a datetime value < 0. Got $XML")) + throw(XLSXError("Cannot have a datetime value < 0. Got $x")) end local dt::Dates.Date diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl index dc6898cc..05b83d05 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -471,26 +471,28 @@ end # 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)) - elseif isnothing(row) - rng = CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))) else - throw(XLSXError("Something wrong here!")) + 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 - elseif isnothing(row) - row = dim.start.row_number:dim.stop.row_number else - throw(XLSXError("Something wrong here!")) + 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 @@ -586,18 +588,23 @@ function process_uniform_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguo newid = nothing first = true 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 + @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 = process_uniform_core(f, ws, r, atts, newid, first; kw...) - elseif r isa CellRange + else for c in r newid, first = process_uniform_core(f, ws, c, atts, newid, first; kw...) end - else - throw(XLSXError("Something wrong here!")) +# else +# throw(XLSXError("Something wrong here!")) end end if first @@ -611,12 +618,13 @@ function process_uniform_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguo 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!" if isnothing(col) col = dim.start.column_number:dim.stop.column_number - elseif isnothing(row) - row = dim.start.row_number:dim.stop.row_number else - throw(XLSXError("Something wrong here!")) + 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 @@ -690,18 +698,23 @@ function process_uniform_ncranges(ws::Worksheet, ncrng::NonContiguousRange)::Int newid = nothing first = true 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 + @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 = process_uniform_core(ws, r, newid, first) - elseif r isa CellRange + else for c in r newid, first = process_uniform_core(ws, c, newid, first) end - else - throw(XLSXError("Something wrong here!")) +# else +# throw(XLSXError("Something wrong here!")) end end if first @@ -715,26 +728,28 @@ function process_uniform_ncranges(ws::Worksheet, ncrng::NonContiguousRange)::Int 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)) - elseif isnothing(row) - rng = CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))) else - throw(XLSXError("Something wrong here!")) + 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 - elseif isnothing(row) - row = dim.start.row_number:dim.stop.row_number else - throw(XLSXError("Something wrong here!")) + 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 @@ -835,18 +850,23 @@ function process_uniform_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguo 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, r, newid, first, alignment_node; kw...) - elseif r isa CellRange + else for c in r newid, first, alignment_node = process_uniform_core(f, ws, c, newid, first, alignment_node; kw...) end - else - throw(XLSXError("Something wrong here!")) +# else +# throw(XLSXError("Something wrong here!")) end end if first @@ -863,12 +883,13 @@ function process_uniform_veccolon(f::Function, ws::Worksheet, row, col; kw...) if dim === nothing throw(XLSXError("No worksheet dimension found")) else + @assert isnothing(row) || isnothing(col) "Something wrong here!" if isnothing(col) col = dim.start.column_number:dim.stop.column_number - elseif isnothing(row) - row = dim.start.row_number:dim.stop.row_number else - throw(XLSXError("Something wrong here!")) + 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} diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index 8f22445a..59fa14e3 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -140,8 +140,16 @@ const iconsets::Dict{String,XML.Node} = Dict( # Defines the 20 standard, buil XML.h.cfvo(type="percent", val="67"), ) ), - "5ArrowsGray" => XML.h.cfRule(type="iconSet", priority="1", - XML.h.iconSet(iconSet="5ArrowsGray", + "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"), @@ -149,39 +157,23 @@ const iconsets::Dict{String,XML.Node} = Dict( # Defines the 20 standard, buil 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"), - ) - ), - "3Flags" => XML.h.cfRule(type="iconSet", priority="1", - XML.h.iconSet(iconSet="3Flags", + "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"), ) ), - "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", + "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"), ) ), - "5Rating" => XML.h.cfRule(type="iconSet", priority="1", - XML.h.iconSet(iconSet="5Rating", + "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"), @@ -189,25 +181,26 @@ const iconsets::Dict{String,XML.Node} = Dict( # Defines the 20 standard, buil XML.h.cfvo(type="percent", val="80"), ) ), - "3Symbols" => XML.h.cfRule(type="iconSet", priority="1", - XML.h.iconSet(iconSet="3Symbols", + "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"), ) ), - "3Symbols2" => XML.h.cfRule(type="iconSet", priority="1", - XML.h.iconSet(iconSet="3Symbols2", + "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"), ) ), - "3Signs" => XML.h.cfRule(type="iconSet", priority="1", - XML.h.iconSet(iconSet="3Signs", + "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="33"), - XML.h.cfvo(type="percent", val="67"), + 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", @@ -225,31 +218,29 @@ const iconsets::Dict{String,XML.Node} = Dict( # Defines the 20 standard, buil XML.h.cfvo(type="percent", val="75"), ) ), - "4RedToBlack" => XML.h.cfRule(type="iconSet", priority="1", - XML.h.iconSet(iconSet="4RedToBlack", + "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="25"), - XML.h.cfvo(type="percent", val="50"), - XML.h.cfvo(type="percent", val="75"), + 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", + "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="25"), - XML.h.cfvo(type="percent", val="50"), - XML.h.cfvo(type="percent", val="75"), + XML.h.cfvo(type="percent", val="33"), + XML.h.cfvo(type="percent", val="67"), ) ), - "5Arrows" => XML.h.cfRule(type="iconSet", priority="1", - XML.h.iconSet(iconSet="5Arrows", + "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"), ) ), - "3ArrowsGray" => XML.h.cfRule(type="iconSet", priority="1", - XML.h.iconSet(iconSet="3ArrowsGray", + "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"), @@ -257,14 +248,23 @@ const iconsets::Dict{String,XML.Node} = Dict( # Defines the 20 standard, buil XML.h.cfvo(type="percent", val="80"), ) ), - "4ArrowsGray" => XML.h.cfRule(type="iconSet", priority="1", - XML.h.iconSet(iconSet="4ArrowsGray", + "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"), @@ -367,9 +367,7 @@ function getConditionalExtFormats(allcfnodes::Vector{XML.Node})::Vector{Pair{Cel allcfs = Vector{Pair{CellRange,NamedTuple}}() for cf in allcfnodes let t, p, r, rule = false, ref = false - if XML.tag(cf) != "x14:conditionalFormatting" - throw(XLSXEror("Something wrong here")) - end + @assert XML.tag(cf) == "x14:conditionalFormatting" "Something wrong here" sqref = cf[end] if XML.tag(sqref) == "xm:sqref" r = XML.simple_value(sqref) @@ -1083,6 +1081,25 @@ The cell value can be suppressed, so that only the icon is shown in the Excel ce 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)") + +``` + !!! note "Overlaying conditional formats" It is possible to overlay multiple conditional formats to the same range or to @@ -1185,7 +1202,7 @@ function setCfCellIs(ws::Worksheet, rng::CellRange; !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) @@ -1534,8 +1551,6 @@ function setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::CellRange; formula = "" elseif operator == "duplicateValues" formula = "" - else - throw(XLSXError("Invalid operator: $operator. Valid options are: `containsBlanks`, `notContainsBlanks`, `containsErrors`, `notContainsErrors`, `uniqueValues`, `duplicateValues`.")) end formula = replace(formula, "__CR__" => string(first(rng))) @@ -1665,7 +1680,7 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; else if !haskey(colorscales, colorscale) - throw(XLSXError("Invalid color scale: $iconset. Valid options are: $(keys(iconsets)).")) + throw(XLSXError("Invalid color scale: $colorscale. Valid options are: $(keys(colorscales)).")) end cfx = copynode(colorscales[colorscale]) cfx["priority"] = new_pr @@ -1722,13 +1737,13 @@ function setCfIconSet(ws::Worksheet, rng::CellRange; new_pr = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : 1 isnothing(mid_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_cellname(min_val) || is_valid_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(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_cellname(mid_val) || !is_valid_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(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_cellname(mid2_val) || is_valid_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(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_cellname(max_val) || is_valid_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.")) + (!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] val = isnothing(val) ? nothing : XML.escape(uppercase_unquoted(val)) @@ -1741,19 +1756,21 @@ function setCfIconSet(ws::Worksheet, rng::CellRange; if l=='C' cfvo = XML.Element("x14:cfvo", type="percent") push!(cfvo, XML.Element("xm:f", XML.Text("dummy"))) - for _ in 1:2 - push!(cfx[1], copynode(cfvo)) - end - if isnothing(mid_type) || isnothing(mid_val) + 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)) + 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)) + 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)] @@ -1783,18 +1800,21 @@ function setCfIconSet(ws::Worksheet, rng::CellRange; cfx[1][i+1]["type"] = type # Need +1 because the first is always 0 percent. end if !isnothing(val) - cfx[1][i+1][1] = XML.Element("xm:f", XML.Text(val)) + if !isnothing(type) && type=="formula" + cfx[1][i+1][1] = XML.Element("xm:f", XML.Text("(" * val * ")")) + else + cfx[1][i+1][1] = XML.Element("xm:f", XML.Text(val)) + end end if !isnothing(gte) && gte == "false" cfx[1][i+1]["gte"] = "0" end end if iconset == "Custom" - licons=length(icon_list) - if licons == 0 + if isnothing(icon_list) throw(XLSXError("No custom icons specified. Must specify between two and four icons.")) - elseif licons (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 TypeError 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 end @testset "cellIs" begin @@ -3508,6 +3803,7 @@ 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") # StepRange is non-contiguous @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; @@ -3578,6 +3874,13 @@ end 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] @@ -3588,7 +3891,7 @@ end 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="beween", + operator="between", value="2", value2="4", fill=["pattern" => "none", "bgColor" => "yellow"], @@ -3638,8 +3941,17 @@ end 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] @@ -3777,6 +4089,14 @@ end 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", @@ -3941,6 +4261,14 @@ end 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 @@ -4077,16 +4405,23 @@ end s[i, j] = i * j end end - XLSX.setConditionalFormat(s, "A1:A5", :aboveAverage) - XLSX.setConditionalFormat(s, :, 2, :aboveAverage; dxStyle="redborder") - XLSX.setConditionalFormat(s, "Sheet1!E:E", :aboveAverage; dxStyle="redfilltext") - XLSX.setConditionalFormat(s, 1:5, 3:4, :aboveAverage; + @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)] @@ -4152,6 +4487,13 @@ end @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", @@ -4180,6 +4522,13 @@ end 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", @@ -4201,6 +4550,7 @@ end 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) ] @@ -4222,31 +4572,33 @@ end @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)) == 25 + @test length(XLSX.getConditionalFormats(s)) == 26 + @test XLSX.getConditionalFormats(s) == [ - XLSX.CellRange("A1:J10") => (type="timePeriod", priority=24), XLSX.CellRange("A1:J10") => (type="timePeriod", priority=25), - XLSX.CellRange("A1:J3") => (type="timePeriod", priority=20), - XLSX.CellRange("A2:J4") => (type="timePeriod", priority=14), - XLSX.CellRange("A2:J4") => (type="timePeriod", priority=21), - XLSX.CellRange("A1:J2") => (type="timePeriod", priority=13), - XLSX.CellRange("A1:J2") => (type="timePeriod", priority=17), - XLSX.CellRange("A1:A2") => (type="timePeriod", priority=12), - XLSX.CellRange("A1:C3") => (type="timePeriod", priority=10), - XLSX.CellRange("A1:A1") => (type="timePeriod", priority=9), - XLSX.CellRange("A1:A1") => (type="timePeriod", priority=11), + 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=15), + XLSX.CellRange("A1:C10") => (type="timePeriod", priority=9), XLSX.CellRange("A1:C10") => (type="timePeriod", priority=16), - XLSX.CellRange("A1:C10") => (type="timePeriod", priority=18), + XLSX.CellRange("A1:C10") => (type="timePeriod", priority=17), XLSX.CellRange("A1:C10") => (type="timePeriod", priority=19), - XLSX.CellRange("A1:C10") => (type="timePeriod", priority=22), + 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) ] @@ -4342,6 +4694,12 @@ end @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 UndefKeywordError 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", From 33edd02268101f7615896002c6504b88567ca130 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Fri, 23 May 2025 13:05:53 +0100 Subject: [PATCH 119/154] Fix compats on UUIDs --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 71c43605..630b3705 100644 --- a/Project.toml +++ b/Project.toml @@ -21,7 +21,7 @@ Colors = "0.13.0" Distributions = "0.25.0" Random = "1.10.0" Tables = "1" -UUIDs = "1.11.0" +UUIDs = "1.8" XML = "0.3.5" ZipArchives = "2" julia = "1.8" From b7c145ee9f5064607f7a0da3fcaf7b22fce280b2 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Fri, 23 May 2025 18:14:01 +0100 Subject: [PATCH 120/154] Tweaks to docs --- docs/src/formatting.md | 57 ++++++++++++++++------------- src/conditional-formats.jl | 75 ++++++++++++++++++++------------------ 2 files changed, 71 insertions(+), 61 deletions(-) diff --git a/docs/src/formatting.md b/docs/src/formatting.md index 8bcb4b22..9d2b32ac 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -1,7 +1,7 @@ # Formatting Guide -## Excel Formatting +## 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 @@ -435,8 +435,9 @@ The following `:type` values are used to set conditional formats by making direc - `:duplicateValues` Each of these formatting types needs a set of keyword options to fully define its operation. -For example, the `:cellIs` type needs an `operator` keyword, set to define the test to make -to determine whether or not to apply the formatting. Valid `operator` values are: +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`) @@ -450,8 +451,8 @@ to determine whether or not to apply the formatting. Valid `operator` values are Each of these need the keyword `value` to be specified and, for `between` and `notBetween`, `value2` must also be specified. -All the cell value formatting types can use one of six built-in formats in Excel as illustrated here -for the `greaterThan` comparison. +Like all the cell value formatting types, `:cellIs` can use one of six built-in Excel formats, as +illustrated here for the `greaterThan` comparison. ![image|320x500](./images/cellvalue-formats.png) @@ -465,7 +466,7 @@ keyword with one of the following values: * `redborder` Thus, for example, to create a simple `XLSXFile` from scratch and then apply some -conditional formats to its cells: +`: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] ] @@ -572,8 +573,8 @@ julia> XLSX.getConditionalFormats(s) ![image|320x500](./images/custom-cellvalue-example.png) -The `formatting_type` needed for these different functions varies, as do the keyword options. -Refer to [XLSX.setConditionalFormat()](@ref) for full details. +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 @@ -618,7 +619,8 @@ julia> XLSX.setConditionalFormat(s, "C2:D10", :expression; formula = "C2>\$B2", ``` ![image|320x500](./images/simpleComparison.png) -Column A uses relative referencing. Columns C and D use an absolute reference for the column but not the row. +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. @@ -645,8 +647,8 @@ julia> XLSX.setConditionalFormat(s, "B2:D11", :expression; formula = "average(B\ (Row 13 above is the average of each column, calculated in Excel) -When a formula uses relative references, the relative position of the reference to the base cell in the range to which -the condition is applied is used consistently throughout the range. +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 @@ -845,17 +847,17 @@ format is being applied. This can be illustrated (for a 4-icon set) as follows: `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 stoping +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. +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 valid Excel -formula. +Alternatively, specifying the type as `formula` allows the value to be determined by any +valid Excel formula. !!! note @@ -906,10 +908,11 @@ julia> XLSX.setConditionalFormat(s, "D2:D11", :iconSet; ![image|320x500](./images/showValIcons.png) 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 definingwhich 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 and mid2_type, then a 4-icon will be used. If both -sets are defined, a 5-icon set is used and if neither, a 3-icon set. +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: @@ -938,7 +941,7 @@ XLSX.writexlsx("iconKey.xlsx", f, overwrite=true) ``` ![image|320x500](./images/iconKey.png) -Specifying too few icons throws an error, any extra will simply be ignored. +Specifying too few icons throws an error while any extra will simply be ignored. #### Specifying cell references in Conditional Formats @@ -956,14 +959,15 @@ 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, and both can be useful. It is -important to understand which reference style you need and to specify accordingly. 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: +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`. @@ -1081,7 +1085,8 @@ one of the conditions, so that lower proirity conditional formats are not applie achieved with the `stopIfTrue` keyword. It is not possible to apply `stopIfTrue` to dataBars, colorScales or iconSets. -For example: +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] @@ -1116,7 +1121,7 @@ will result in the following, instead: ![image|320x500](./images/no-stop-if-true.png) -## Working with Merged Cells +## 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 diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index 59fa14e3..115f474a 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -456,6 +456,26 @@ 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. + +!!! note "Overlaying conditional formats" + + It is possible to overlay multiple conditional formats to the same range or to + overlapping ranges. Each format is applied in turn to each cell in priority + order which, here, is the order in which they are created. Different format + options may complement or override each other and the finished appearance will + be the resuilt of all formats overlaying each other. + + It is possible to terminate the sequential application of conditional formats to a + cell if the condition related to any format is met. This is achieved by setting the + keyword option `stopIfTrue="true"` in the relevant conditional format. + + While the `stopIfTrue` keyword is available for most conditional formats, it is not + available for `:colorScale`, `:dataBar` or `:iconSet` conditional formats since these + do not apply a specific test in each cell. + + For example usage of the `stopIfTrue` keyword, refer to [Overlaying conditional formats](@ref) + in the Formatting Guide. + 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: @@ -483,7 +503,7 @@ 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 nor is it possible to set + 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 @@ -516,7 +536,7 @@ julia> XLSX.setConditionalFormat(s, "B1:B5", :cellIs; font = ["color"=>"red", "italic"=>"true"] ) -julia> XLSX.setConditionalFormat(s, "B1:B5", :cell; +julia> XLSX.setConditionalFormat(s, "B1:B5", :cellIs; operator="lessThan", value="2", fill = ["pattern" => "none", "bgColor"=>"yellow"], @@ -861,8 +881,8 @@ julia> XLSX.setConditionalFormat(s, "A1:A13", :timePeriod; # type = :containsErrors, :notContainsErrors, :containsBlanks, :notContainsBlanks, :uniqueValues or :duplicateValues -These conditional formattimg options highlight cells that contain or don't contain errors, -are blank (default) or not blank, are unique in the range or duplicates within the range. +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. @@ -917,7 +937,7 @@ The available keywords are: - `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 +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. @@ -980,10 +1000,10 @@ instead using the following keywords: 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) should usually be specified as absolute references. +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 ([here](https://juliagraphics.github.io/Colors.jl/stable/namedcolors/)). +color from [Colors.jl](https://juliagraphics.github.io/Colors.jl/stable/namedcolors/). # Examples @@ -1014,7 +1034,7 @@ julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:F22", :colorScale; # type = :iconSet -Apply a set of icons to cells in a range depending on their values. The kwyword `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` @@ -1039,23 +1059,24 @@ can be used to select one of 20 built-in icon sets Excel provides by name. Valid 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: +by the following keywords to override the default values for each icon set: -- `min_type` = "percent" (default), "percentile", "num", "percentile" or "formula" +- `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", "percentile" or "formula" +- `mid_type` = "percent" (default), "percentile", "num" or "formula" - `mid_val` (default: "50" (4 icons), "40" (5 icons)) -- `mid2_type` = "percent" (default), "percentile", "num", "percentile" or "formula" +- `mid2_type` = "percent" (default), "percentile", "num" or "formula" - `mid2_val` (default: "60" (5 icons)) -- `max_type` = "percent" (default), "percentile", "num", "percentile" or "formula" +- `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_* and max_*), four-icon sets require three -thresholds (with the addition of mid_*) and five-icon sets require four thresholds (mid2_*). -Thresholds defined (using val and type keywords) that are unnecessary are simply ignored. +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 @@ -1065,15 +1086,15 @@ 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 `mid_value` and `mid_type` keywords and `mid2_value` and -`mid2_type` keywords are provided. +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 is appiled can be reversed from the default order (or, for +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. @@ -1100,22 +1121,6 @@ XLSX.setConditionalFormat(s, "A2:A11", :iconSet; ``` -!!! note "Overlaying conditional formats" - - It is possible to overlay multiple conditional formats to the same range or to - overlapping ranges. Each format is applied in turn to each cell in priority - order which, here, is the order in which they are created. Different format - options may complement or override each other and the finished appearance will - be the resuilt of all formats overlaying each other. - - It is possible to terminate the sequential application of conditional formats to a - cell if the condition related to any format is met. This is achieved by setting the - keyword option `stopIfTrue="true"` in the relevant conditional format. - - While the `stopIfTrue` keyword is available for most conditional formats, it is not - available for `:colorScale`, `:dataBar` or `:iconSet` conditional formats since these - do not apply a specific test in each cell. - """ function setConditionalFormat(f, r, type::Symbol; kw...) if type == :colorScale From 6ebe9af1053388a8c9febb1abda4163a3e7a2b56 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Thu, 29 May 2025 00:31:49 +0100 Subject: [PATCH 121/154] Add `dataBar` conditional formatting (with tests) --- docs/src/formatting.md | 193 +++++++++- docs/src/images/axisOptions.png | Bin 0 -> 9675 bytes docs/src/images/borderAndGrad.png | Bin 0 -> 7951 bytes docs/src/images/customColors.png | Bin 0 -> 8760 bytes docs/src/images/minmaxDataBar.png | Bin 0 -> 9285 bytes docs/src/images/moreMixed.png | Bin 0 -> 37839 bytes docs/src/images/negAndAxisOptions.png | Bin 0 -> 18552 bytes docs/src/images/simpleDataBar.png | Bin 0 -> 6284 bytes src/conditional-format-helpers.jl | 29 +- src/conditional-formats.jl | 488 ++++++++++++++++++++++++-- test/runtests.jl | 265 +++++++++++++- 11 files changed, 917 insertions(+), 58 deletions(-) create mode 100644 docs/src/images/axisOptions.png create mode 100644 docs/src/images/borderAndGrad.png create mode 100644 docs/src/images/customColors.png create mode 100644 docs/src/images/minmaxDataBar.png create mode 100644 docs/src/images/moreMixed.png create mode 100644 docs/src/images/negAndAxisOptions.png create mode 100644 docs/src/images/simpleDataBar.png diff --git a/docs/src/formatting.md b/docs/src/formatting.md index 9d2b32ac..7d6ab1d6 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -708,12 +708,158 @@ julia> XLSX.setConditionalFormat(s, "C1:C3", :expression; formula = "exact(\"Hel #### Data Bar -(In development) +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. ![image|320x500](./images/dataBars.png) +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 + +``` +![image|320x500](./images/simpleDataBar.png) + +All of the options provided by Excel can be adjusted using the provided keyword options. + ![image|320x500](./images/dataBarOptions.png) +![image|320x500](./images/negAndAxisOptions.png) + +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 +``` + +![image|320x500](./images/minmaxDataBar.png) + +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 +``` +![image|320x500](./images/borderAndGrad.png) + +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 + +``` +![image|320x500](./images/customColors.png) + +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 + +``` +![image|320x500](./images/axisOptions.png) + #### Color Scale It is possible to apply a `:colorScale` formatting type to a range of cells. @@ -815,19 +961,6 @@ julia> s=f[1] julia> s[1:10, 1]=1:10 1:10 -julia> s[1:10, 1]=collect(1:10) -10-element Vector{Int64}: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - julia> XLSX.setConditionalFormat(s, "A1:A10", :iconSet) 0 ``` @@ -941,7 +1074,7 @@ XLSX.writexlsx("iconKey.xlsx", f, overwrite=true) ``` ![image|320x500](./images/iconKey.png) -Specifying too few icons throws an error while any extra will simply be ignored. +Specifying too few icons in `icon_list` throws an error while any extra will simply be ignored. #### Specifying cell references in Conditional Formats @@ -1121,6 +1254,36 @@ will result in the following, instead: ![image|320x500](./images/no-stop-if-true.png) +It is possible to overlay `:colorScale`s, `:dataBar`s and `:iconSets` in the same or +overlapping cell ranges. + +```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),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 + +``` +![image|320x500](./images/moreMixed.png) + ## Working with merged cells Worksheets may contain merged cells. XLSX.jl provides functions to identify the merged cells in a worksheet, diff --git a/docs/src/images/axisOptions.png b/docs/src/images/axisOptions.png new file mode 100644 index 0000000000000000000000000000000000000000..1f4f4b5b1aedfb40d072c113d3a83aba86c8da98 GIT binary patch literal 9675 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

4{!BB&(n| zq#KQOa6h0}*I`EP(wfh-SV5_#yuUz>(eFJ<}9N z&>P#24}H`^+FZiavSl%xx-l>$^;jn29YZoY7&%!)Hk(=v!sEw7X%Bez8Ta-?Bd9EM zbVh%JxTQ%Q2d36IPV|I*iE3P;Ipp+pN2gJ;!NZFp|8E(% z5yP?>T(oW;1`$BVzlH9T`2s zE2O4u*2P85x^yBG0{KQn0iNfWOZ+hxnXs_O%ErK8UHk~7a=>FRs=)2inTmU=k}eJ; zB%7u{EH-uAY(}TY{VD0s!6thE_y5O=LzY<6Q literal 0 HcmV?d00001 diff --git a/docs/src/images/borderAndGrad.png b/docs/src/images/borderAndGrad.png new file mode 100644 index 0000000000000000000000000000000000000000..f423594994b39877898a8f7ebb46fd89ed68a0b6 GIT binary patch literal 7951 zcma)h2UJtp+V){k7)3zD3Q`nN0TByDx=IHr1`w%H5$S{;YJy`yrAZ&D(wh*eflvYh zf}uB~v`7g^LXi>@NJ9RD-kCdJS@-+b+G*=#?S0nX@6+D%(!@xId%wVb006l4bTuIW zz(xeWul~*fzEc2{HTc2i1JSt&l=Yrm0DtUpykU3)0Lo*y829&rzxR3STKWJ0PwVcB ztpis05CDX8^)zpo1=uZ3gt|S%2NG5ZudP1$Kj8FuP%qb5pHO9XIB@ENm+rgd_T$`E zoL#^5TrqgZelXEWK{S4J&g7t0-Y0GIbVEVY3vH)J`wjN^Jxq1(@osTvk1zWCa;jWU z=B$xzC;8=r!9ged%x4~!Nt_agTk~SwRmbP>!l~B=t93G?D>|44)YaBd&wz}&`HW!W zIbt$&GA>JG=SR4b_v{O3>wU4NninYPY1ataXvGUyc&12-2;&UI+7lT?OPPYhy147-P`4S#gCS}Vkz4{CjMRkdo4?0FNho=UJu=WJpz zSI86;vJq-fJN@o?{(gu*B*H!vVPBDVswpvVA91b=(PN+&Ib?>2sMOg13Z}9DLE3X# z)9EsEOT&xN5)0zjq={$;q1Zo;Tene7dK0YEPvP?xQmt04ZhUpSBWYtg05iZ0x1odY zZnbMk-zm^@;4)3QTTDy;lo(ArDyKYqDBR|dM_a%^skr*bA_;8Eu{2M&%Dsl%hUXWw zt{GBbK0aU?x#!4ebmeqYxt1ZX|5o)weX?AiXifarin&ZHNSuL})lBNqqpP9jdV#2? zCv^?73J+VD_hxu6WE+GsCzc;73Rx;9B7H|>t}oBdmXJyMD!<)Z|M09 zepgm_x4c&TYF{tTIT|K#w~gAY7pT5Z@pbfE{Hs?bMD4imB(eqk{;5?%)5pWI#)#h7)2v;=MyJ+Ue($^% zcwQ!uN!U%E-04)SyuuOhJ5l`TGBpZ3xjJPQ)oka|v~Q)>PB!oYjXlkfQ61bX>js{3 z(W{kcrPJJ@?$Xo2U3y!04ta}mKg%BVEqyzq&9t^Mx}np&=5l!eAMu-W>2R?j^5UEx zR{u4PmjBJIG`%W`P<*%wlB?7tagUE+t&c;eyk`FdQ>Q*-`^fd{+4@-d9Ukq6Ws{Msn_|q4QVQ6=QHn`l~2Yb z$lfoYq<#ZW|oR z%Ud_{^dusn6ON_ZHd0Dq6xUCt%$fbmD{fwFIsO{-a!o>W#s-bUO6|C4EkZV(A>8Wx z0%oXXz+c^Uky|1lKv!UTY;EdP0zo73b9gv7XJ2l}>#MV#keyz5Qx3Xu$sNHR9z`1k zG4vBo;LQiiLkWTN=_6-?rA&|aockMszhL&(I@@Ef5clqG{{=70HoS_&x?#`XU~9uC zX@y)(O-)@6MHj<7E+^`Xvu*>x+jI>#qR6NLM1gu7(|blnwq9~RWgCluNxxY0YKo;AeV zpye1uDC2EdsnzBPV(pwmr*#8JeT;)$x|d{UIRkHGIqSD852@FqLp)@IJCF~p5n zXCH+d4GGK&7wMYS&%&0{?j+6#O8}RSSTF^h*C|le60hI74C@XQxqzy=vu#kySU^+W z$TZ~E5&e*RfaVXwv-FJ>;gA@$?H&3Ct$wnM5JDEDe2KXdG&{!X6M1~5vw^YJWsUgS zHd10yV)*TSi|Fd#6;km+MyL@0bYFDO&CPw58tdVONha!(mX;_YoB-gjvF?W(P;mg+ z{8zO_(6o~hki;Sxx-=&>m=}we$%_83hn=S;LGa7fK-E{8qLio4Dqu@6i|D;%MDWs| z(W5&g?}Gee28UlAEgW{cUGz1ZF}gpQpd*4Uj#ii?<|~h^&LD)}g(nNrKXkz*saUMW zmU~M!V{m}$?*l*M0B(MED5UIybM|(Ft9s8-CXeO=G1Lxe{SH$WGYtoTTkU478aHt;zHE!r;oUMY);mU`hM@YsZ;Xd+pNRSmh-|mm{dcH<^+_M~#MvNOgdic25PT`c-ocQv~z7T>Pu zJnJ@7F9?Y4=lhJM-@+Y{R+U3}`$Oz!XOFv`XZ6>9)=u(!nRWo+dsVYDVF9_=J12{} zPyEzbe)q)EK<%R{BW;c_T`gPI@|y%uzfvS0$|wV+;T` zrwfr|XNlT3d8i+>9}4U}JH~I)T6Ak0U)gL|F>}Ri*xXZagTI9T27^Tj0_Ra z@pJAwr1-Vi-pJ_jf!nD{%mz#YeN&m-CB3;czO%w2`|aP$a|B!#hbr~lN~cA!in__m zg+@Xs^@@4#`q0v(jwXgQZspY5oMV4OP!|UPXEf=#HkN~JCg1Du{07tIc)roj5_QJ- z$WBARq8y+pn!k-Ugp@fg6q!@z$$bqOW;H6Pg5y!APq#FWaw%c$u=Bx|Vy-hmF9yy7 z&38j6u+-e~yq1L@-}pn75)*L>dbojWS>rCWsd-0Ve@!c?Qo0`y7DXcwm{8MA4aG;s zmn*z4FiakZIi9dQ0h~T^pIOgx#nlB0rP>T1S5#D*auraWQL1amGnjn{ygz2j98;4} zh$>Q%Fsz$ZvEER=U*>$P&%wF*Nc6xFEI)AXZ2mT!)q^JueR&946@F($9!Fp2K+Tl; z(}Pz4O$p34_2@1Oy{`&}@RXfXm`Ju6fRq*_D?2`~`xc}<6zV02!v_h}uDAif)yto; z_2-CSrK`)MY-XlTDfWn@4xd(S%=Q9n{anY0>59|qg#p?p2ls&?F2Jq*F0019X<4uY zzP84{PZ8pHaYXq9?d8)0K9S0R+g8g@8>jM)`5o@mC=*nh*9C81N{=dcL=xQES z4m_XOujAw6E-nh{vqff-MQAi0c@m^^?jF2^&U(T>N{gOC{$8Dox6;|O^WAM^#$c=Z z##Xh~R(0Zb?)cZ(0ipDk;c``8ck+Ri5)WRX`MGMm0<5H$x+Q#!_09G0{_#VSa@Gk@ z>!~&6RWdTSJ3SpY4wB2t7l*GXw2l9eZGEH13YFG*3}%9$K905n%Vp0$CFK@K9Xxyc zuo?Y>o4+4<(fvxhqz}4efuQdLt-8+u2ycB{V&2{(nEH_=JYQ)PL#roUt#k+>1RRVi zBoi9ry&d$7dkk9V=}Zny7co@$$GkUO^Gpul`k4oXNa`(Id)EzZ#N8*h2=>#}YY$CM zXWTC9fB}H!>F8~AD8%3K6W6nMz45U#Q%!lcX@UdK#`j^_fP*jpBoXBG>az1(>M4qs zUwNODPe9eAsz_?z<_J3vlGt)aLPWA0>b`2XrMJqRjN#fx8wVUe0S8NulL@x*-k{Bu z?31!+Jc2W<3rw6qt}CL!Lepu%;Sv>{-ECAy%4i;>6ZjN5tf50Xri_*Y0O8WT+i-mD zxI#@~dwYL^$Q@79Vb4kFn1Q$MOWk=wt@q0M&VNr(uUt-&8FnY!{q1TI&p9K_SO{^3l* zk6<=%D~gd+HPc=r`KY6EJ*}u5xr%XL0#%Y1%Ms?(X2Kc_;r`1#ya4Ovz6(8Ntu3xL z`(%0D<0V%CW}ufA$Qp+)mx+?4M#jbAy}Jk)@@6Jx(^NXC48U zj#)5+xq9%n0xFq_xDv^@M0?Mhb)aUA{-I`Yha>EFEen?Lx0JQZegkaE+UNX4gx<4> za%@somi6kcm1KR*hVDBor)>VZqF{I)-Q15Wypkh@5PzRN@EDY|cfB-e0AT;c7)?F` zO?;$TIoUkH)Ae?_~GuWI)4y*N4?)ql;V?=T{~!$(bejinwV9TyV6HP z9fm8@OnDMp(=_2;pVwC^3?18y?MsHP6Y)$}HsJc9e^|nD_^wTPP`JZzRk)ntT_wLp zf~g=!3?mB;00S?wcHA=)o;*!U#g~8Tzxuv_vz`CF*rCHOGQ84lcqZtLDg4IG9Dx1h z5v)6i-n9nYSG~)Z%s^zH#E*7X96!Dv$Ob*)*jb5Vex*twS57DjOJN{FP4SW7!f4je23xk%m-_ftbl z+CPSV+U3#lx+|gBY)@@Ro!shYee%l@W1Z09GyO$X4t8%e#VYIS>Z&ImA!w#WW)K(*#kvNHtY8YtGkh$!ffUCphU{+mg7X}s9ARU3B7cJ2)nq8j(T*;m z{?3C6MM9k-9ll(P9U}G8oA8dlqfGct=F^X~J;2E_@Qkconl9R9SSSYDJ(-=o`%Xo% ztU8tp7aPwe3b{RkrT@$QUU(wWVK0V?@EQF5i3I!gs?+a~)bs?YcJsvgaKGrZlMzvU z7oCK;#i3v+5WWi7`h)XQFLENs6lP&VUr zvu;G%-8Oql0K}8=uKCqIzP()iI7@IhyZ(%?V8XrPW>WO^ABmfe*{zQ+UAz1wr|L_8 zT>vS{ot(5|c>%yZ1zBjp$sW`H8nPaE-xd1H`=U<}6@<=XOU3IL+9?TFPZYH>zkl~M zeQ%S#a<^>7)hmt8O6(ZHn{EgjhmaH{s}tTrT%*=fd8XFrojF}NUw_^7GMr`A)Q8la)br`V2g7Oh<~8A?t44G25)D>MI$P!GV} z8D8u+p`>`ar}$YPOg|F!=n76Y|27%~kxa%fd+hu`|Jh>h(PPI-h91_uEqW=A=G>G| zSP|(T@ARI@SRuwLa&OE}2m@j1p!2QK+j?Y)%NP}o%w93OBS|jIw}yhq5N6iYFt@aJBU`j_0kU>?bqWsoS5!qRC_$KyJn41z?_lw$!l zo+?ru!{ruxQ@Yj_dj^N3lms(JKBT0^bUS?vb+r~;891-n2sGQ>`U#4P+?V4G(S{c> zl`}WspBLxiEsx}%t!mTyi6Uwwd3G&{?w?e`UDK$NUH2G4Tv*r(_=7QO>X1#AFW_bknYD}((X%I4;F3_AeSXKg7FoiZx^q0S56 z6YtR%{-!&0^qQbLZl$H&93f_idlNZ4h48Hn^dwk>TRUCgq!*K0RPXOxLr>n_qziUn z0`r3yn>(WzD9>CLbD5R2Lb&#Mg%^u-pOE3~s1uB=gG+W^?54axIb1U@W151*x-z~?EO_f3)24-A^zguHWU_Dx>W6%2A?ENDA z-6dl;JNEccM@P3e%g4s-0^OEs=iHTJ+;@bSO+uNr6ZimZe|Z+wb#ZJJw)11&?nNmB zLjVoz8Mc1}f?s{4n#@pD+43_SFJ2A4(!TnJ`suTnTsTHN0jR@V299^8bN%kiRUUdG7-rs5?F|q+_ zQb)?_e7Sv)J6b)?n#Tl;ud8Q`KjU*=ensEm04{;4JlGYVP-1_9VDe0UF~F>Y)=&oP zf6fg`ec?~3=l8t9Wsr5irmm<0`m`K<62(&ydRau0jVxZer3Hk=?h3D7pvh3YW#YWg zm{8+Uk6O(rUB)nH@e?;x6x9tbTN^NOBKqm$PJ)1kD`-v7zJ_%HrJ@eTMSUnSD=CY)+}KK`89W=PwZ%IVNU+V} zQrurU<&z7lNTkcX<*G-*;^LqffKmZ&nGi3}8%)*|qe=j-+j7@4GYy->cZ(e_SJ_mo z%^M2dHApL9;UVJfY8h%*$}jj~w#5oX&?E!w7JQ?uUgYyJ=621o4LWPX5JO#Q)Pu7o z6pHEY_S+2vb|8R~!Hl7j4cTMJ#Zba#oSK@qjk-Un5b=&G6x13lJXDE@$rIA6lmETl ztOd1flGfvF*NUUh-#325m6CGTyGVd@UD39IdXAFCnzn$SSK0Ki9bO?B@AN^*?JT;& zcAGl=sjvX87Kcy=zP)WK9GBUNe7@6(TK=&ZzcQ4&6O~c#Z%UvK!4_~bp;nF}X+sK3 zgp?teVwCf4q75+dmRRxJiri=SzJCnoG#5$EUKxLn>DmoMyt4*aN*9JviOo@G^zm*< zpCT4eR=3n_J6TdA^i5_fZ|JR+Et~^$l?%ZRe7jzz#w^)p(pl9_+ijeTq}RA}8}+Oi zM>z_O(+@@1*x}6VFPh&mky2oAh=J?Fy3^T=Vdo~!CZ;YQSo#z5L3sjq8BIB>w{?A| z1qLIPu$C-r4_F7R%50dczO7%`BtNbX_Qk%kLJoi}Wu93P)+nB0ArkyP);Bf8bz-E7 zS~4$$Q1#rfvk*gS_36Ax7%MWfVxF~C8kL?Xe4WTUW+0QmfP85rZ7D8naT_JFHRHGx zSRZ7bP2|uC;-QUh!E9axkidX1qj1PF$z`O|YQT`1H+9!ERi3AHHCXtE!F|H;wnZ-8 zczhSD;Y%4g>PyKV?E2UH^i~#^H_7sw$oz3Xvk6RAev%8?yVYG)-$*S4&Z?R$jk7xd zRY!Q)_LUf`PLaETuvSX6u*HxXiUQlBH21(T0$cOG$S$Mn;qyo1yfe4!yHc2 z%(}oC36Z8{Xn%9?D-3O8Gu>^*lI% z9=}Z&3Hey6ngb;xDklRO%N; zg&#}18Z^Y8>E)U^Aj7;FGf9i-C99jSPk4oXUoigu#9}89T+C4+qaS~f5%PZMaE|8Q z{|obu358CV^b>?|C{^v3`$1gq!_FPS8Zc{xP(|34Uc6QGp?0w0s-5>JCf6# z=$()U1H!+bu|VN=JOB_Ux*JD`oi6_=45xrf9>6X3vrYZe>HXtL#fv1py3GTNmhQZ& z18yiL0DSwrqziVtIM@yM%-pW3!Xp5{)8t=}qP|}PA(i*lj?M-&x5$YT!3UJmM!YWQ psaWbLFZf_O`oFi%ejj9U9@zHrD-d%7y9NNDr)8vBcJtmJ{|_A8HT?hp literal 0 HcmV?d00001 diff --git a/docs/src/images/customColors.png b/docs/src/images/customColors.png new file mode 100644 index 0000000000000000000000000000000000000000..13b60cf8a14f01883ff0a3db5d4802b7715368b3 GIT binary patch literal 8760 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/src/images/simpleDataBar.png b/docs/src/images/simpleDataBar.png new file mode 100644 index 0000000000000000000000000000000000000000..9f64fd8b68cc03fb0e64e4a9e01b7f91f2335ac2 GIT binary patch literal 6284 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 Dict( "font" => Dict("color" => "FF9C0006"), @@ -22,6 +22,122 @@ const highlights::Dict{String,Dict{String,Dict{String,String}}} = 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( @@ -352,19 +468,19 @@ 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}} - allcfs = Vector{Pair{CellRange,NamedTuple}}() +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"]))) + 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}} - allcfs = Vector{Pair{CellRange,NamedTuple}}() +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" @@ -376,11 +492,13 @@ function getConditionalExtFormats(allcfnodes::Vector{XML.Node})::Vector{Pair{Cel for child in XML.children(cf) if XML.tag(child) == "x14:cfRule" t = child["type"] - p = parse(Int, child["priority"]) - rule = true + 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)) + push!(allcfs, CellRange(r) => (type=t, priority=p)) rule = false end end @@ -964,7 +1082,105 @@ julia> XLSX.setConditionalFormat(s, "A1:E5", :expression; formula = "E5<50", dxS # type = :dataBar -(In development) +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 @@ -1141,8 +1357,8 @@ function setConditionalFormat(f, r, type::Symbol; kw...) setCfFormula(f, r; kw...) elseif type == :iconSet setCfIconSet(f, r; kw...) - # elseif type == :dataBar - # throw(XLSXError("Data bars are not yet implemented.")) + elseif type == :dataBar + setCfDataBar(f, r; kw...) else throw(XLSXError("Invalid conditional format type: $type.")) end @@ -1167,8 +1383,8 @@ function setConditionalFormat(f, r, c, type::Symbol; kw...) setCfFormula(f, r, c; kw...) elseif type == :iconSet setCfIconSet(f, r, c; kw...) - # elseif type == :dataBar - # throw(XLSXError("Data bars are not yet implemented.")) + elseif type == :dataBar + setCfDataBar(f, r, c; kw...) else throw(XLSXError("Invalid conditional format type: $type.")) end @@ -1225,7 +1441,8 @@ function setCfCellIs(ws::Worksheet, rng::CellRange; push!(cfx, XML.Element("formula", XML.Text(XML.escape(value)))) - if !isnothing(value2) && operator ∈ needsValue2 + if !isnothing(value2) && operator ∈ ["between", "notBetween"] + push!(cfx, XML.Element("formula", XML.Text(XML.escape(value2)))) end @@ -1650,7 +1867,7 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; max_col::Union{Nothing,String}="FFFFEB84", )::Int - !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) + !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 @@ -1662,15 +1879,27 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; 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.")) - min_type == "formula" || isnothing(min_val) || is_valid_cellname(min_val) || is_valid_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.")) + 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_cellname(mid_val) || is_valid_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(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.")) - max_type == "formula" || isnothing(max_val) || is_valid_cellname(max_val) || is_valid_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.")) + 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.")) - min_val = isnothing(min_val) ? nothing : XML.escape(uppercase_unquoted(min_val)) - mid_val = isnothing(mid_val) ? nothing : XML.escape(uppercase_unquoted(mid_val)) - max_val = isnothing(max_val) ? nothing : XML.escape(uppercase_unquoted(max_val)) + 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( @@ -1685,7 +1914,7 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; else if !haskey(colorscales, colorscale) - throw(XLSXError("Invalid color scale: $colorscale. Valid options are: $(keys(colorscales)).")) + throw(XLSXError("Invalid colorscale option chosen: $colorscale. Valid options are: $(keys(colorscales)).")) end cfx = copynode(colorscales[colorscale]) cfx["priority"] = new_pr @@ -1731,7 +1960,7 @@ function setCfIconSet(ws::Worksheet, rng::CellRange; icon_list::Union{Nothing,Vector{Int64}}=nothing )::Int - !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) + !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 @@ -1741,7 +1970,7 @@ function setCfIconSet(ws::Worksheet, rng::CellRange; new_pr = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : 1 - isnothing(mid_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 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.")) @@ -1751,10 +1980,16 @@ function setCfIconSet(ws::Worksheet, rng::CellRange; (!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] - val = isnothing(val) ? nothing : XML.escape(uppercase_unquoted(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 color scale: $iconset. Valid options are: $(keys(iconsets))")) + throw(XLSXError("Invalid iconset option chosen: $iconset. Valid options are: $(keys(iconsets))")) end l = first(iconset) cfx = copynode(iconsets[iconset]) @@ -1863,3 +2098,202 @@ function setCfIconSet(ws::Worksheet, rng::CellRange; 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; + 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, + )::Int + + !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() + ) +# cfx = XML.Element("cfRule", type="dataBar", priority=new_pr) +# cfx_db = XML.Element("dataBar") +# if isnothing(allkws["min_val"]) +# push!(cfx_db, XML.Element("cfvo", type=mnt)) +# else +# push!(cfx_db, XML.Element("cfvo", type=mnt, val=allkws["min_val"])) +# end +# if isnothing(allkws["max_val"]) +# push!(cfx_db, XML.Element("cfvo", type=mxt)) +# else +# push!(cfx_db, XML.Element("cfvo", type=mxt, val=allkws["max_val"])) +# end +# push!(cfx_db, XML.Element("color", rgb=get_color(allkws["fill_col"]))) +# push!(cfx, cfx_db) +# push!(cfx, XML.Element("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/test/runtests.jl b/test/runtests.jl index 0a5ea47a..f46e7ef1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1661,7 +1661,7 @@ end isfile(f) && rm(f) end end - +@time begin @testset "Styles" begin @testset "Original" begin @@ -3369,8 +3369,212 @@ end @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 +end +@time begin +@testset "Conditional Formats" verbose=true 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 -@testset "Conditional Formats" begin + @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 @@ -3482,11 +3686,29 @@ end min_type="min", min_col="green", mid_type="percentile", - mid_val="E4", + 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] @@ -3792,6 +4014,39 @@ end @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 @@ -3803,7 +4058,7 @@ 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") # 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; @@ -5010,7 +5265,7 @@ end end 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 From 18716c0623f12641fdefdd76d6d1d547a6866429 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Thu, 29 May 2025 00:42:43 +0100 Subject: [PATCH 122/154] Update `ci.yml` to match xlsx master --- .github/workflows/ci.yml | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdd9e602..75c001a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,9 +18,6 @@ jobs: version: - '1.8' - '1.9' - - '1.10' - - '1.11' - - 'lts' - '1' # automatically expands to the latest stable 1.x release of Julia - 'pre' - 'nightly' @@ -30,39 +27,12 @@ jobs: - macOS-latest arch: - x64 -# include: # must be a better way! -# - os: macOS-latest -# arch: aarch64 -# version: '1.8' -# - os: macOS-latest -# arch: aarch64 -# version: '1.9' -# - os: macOS-latest -# arch: aarch64 -# version: '1.10' -# - os: macOS-latest -# arch: aarch64 -# version: '1.11' -# - os: macOS-latest -# arch: aarch64 -# version: 'lts' -# - os: macOS-latest -# arch: aarch64 -# version: '1' -# - os: macOS-latest -# arch: aarch64 -# version: 'pre' -# - os: macOS-latest -# arch: aarch64 -# version: 'nightly' - steps: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v4 env: cache-name: cache-artifacts From 0b46a3c8293b6b01f1ef2f410e3de301c87bbe89 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Thu, 29 May 2025 00:52:08 +0100 Subject: [PATCH 123/154] Reinstate 'lts' --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75c001a0..f993cdba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,7 @@ jobs: - '1.8' - '1.9' - '1' # automatically expands to the latest stable 1.x release of Julia + - 'lts' - 'pre' - 'nightly' os: From 5d7342792a501f96d545cbf4c89c359ac4936815 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Thu, 29 May 2025 14:28:55 +0100 Subject: [PATCH 124/154] Minor tweaks to docs --- docs/src/formatting.md | 47 ++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/docs/src/formatting.md b/docs/src/formatting.md index 7d6ab1d6..7d19f25a 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -159,11 +159,11 @@ All the format setter functions have `setUniformAttribute` versions, too. See [` ### Setting uniform styles -It is possible to use each of these 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: +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. @@ -183,12 +183,13 @@ We can apply `setBorder()` to add a top border to each cell: julia> XLSX.setBorder(s, "B2,D2,F2"; top=["style"=>"thick", "color"=>"red"]) -1 ``` -This merges the new top border definition with the other, existing attributes, to get +This merges the new top border definition with the other, existing border attributes, to get ![image|320x500](./images/multicell2.png) Alternatively, we can apply `setUniformBorder()`, which will update the borders of cell `B2` -and then apply all the border formatting to the other cells, overwriting the previous settings: +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"]) @@ -196,7 +197,7 @@ julia> XLSX.setUniformBorder(s, "B2,D2,F2"; top=["style"=>"thick", "color"=>"red ``` This makes the border formatting entirely consistent across the cells but leaves the other formatting -attributes as they were. +attributes (font, fill, format, alignment) as they were. ![image|320x500](./images/multicell3.png) @@ -276,9 +277,9 @@ Using `setBorder` : 96.824494 seconds (2.82 G allocations: 194.342 GiB, Using `setUniformBorder` : 32.182135 seconds (787.00 M allocations: 62.081 GiB, 20.85% gc time) Using `setUniformStyles` : 0.606058 seconds (14.00 M allocations: 416.660 MiB, 16.19% gc time) ``` -If maintaining heterogeneous formatting attributes is not important, it is much more efficient to +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`. +cell ranges, and much more efficient still to use `setUniformStyle`. ## Copying formatting attributes @@ -331,13 +332,12 @@ but not otherwise. Such conditional formatting is generally straightforward to a 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 many of Excel's options (but not yet - all of them). + using the `setConditionalFormat()` function, giving access to all of Excel's options. ### Static conditional formats -As an example, a 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: +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 @@ -833,10 +833,11 @@ julia> XLSX.setConditionalFormat(s, "E1:E11", :dataBar; fill_col="cyan", border_ ``` ![image|320x500](./images/customColors.png) -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`. +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 @@ -896,6 +897,7 @@ mid-point color, too. Colors can be specified using hex RGB values or by name us in [Colors.jl](https://juliagraphics.github.io/Colors.jl/stable/namedcolors/). In Excel, the colorScale options (for a 3 color scale) look like this: + ![image|320x500](./images/colorScaleOptions.png) The end points (and optional mid-point) can be defined using an absolute number (`num`), a `percent`, @@ -968,6 +970,7 @@ julia> XLSX.setConditionalFormat(s, "A1:A10", :iconSet) All of the options to control an iconSet in Excel are available. The iconSet options (for a 4-icon set) look like this: + ![image|320x500](./images/iconSetOptions.png) Each icon set includes a default set of thresholds defining which symbol to use. These @@ -995,7 +998,7 @@ 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 (but are not checked). + 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: @@ -1215,8 +1218,8 @@ julia> XLSX.getConditionalFormats(s) 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 dataBars, colorScales or -iconSets. +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: @@ -1254,7 +1257,7 @@ will result in the following, instead: ![image|320x500](./images/no-stop-if-true.png) -It is possible to overlay `:colorScale`s, `:dataBar`s and `:iconSets` in the same or +It is possible to overlay `:colorScale`s, `:dataBar`s and `:iconSet`s in the same or overlapping cell ranges. ```julia From ed4fe64f73f306de07f9ffb0c3aa61ae3b763854 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Thu, 29 May 2025 14:43:03 +0100 Subject: [PATCH 125/154] take timer out of tests --- test/runtests.jl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index f46e7ef1..87f30e4f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1661,7 +1661,7 @@ end isfile(f) && rm(f) end end -@time begin + @testset "Styles" begin @testset "Original" begin @@ -3369,8 +3369,7 @@ end @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 -end -@time begin + @testset "Conditional Formats" verbose=true begin @testset "DataBar" begin @@ -5265,7 +5264,7 @@ end end 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 From 864825b98828d20c8a80c4433aafdaf32a616c75 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Thu, 29 May 2025 22:22:31 +0100 Subject: [PATCH 126/154] Tweaks and tidy-up --- src/cellformat-helpers.jl | 15 +++------------ src/conditional-formats.jl | 21 +-------------------- src/styles.jl | 10 +--------- 3 files changed, 5 insertions(+), 41 deletions(-) diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl index 05b83d05..f72b25f2 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -60,6 +60,7 @@ const floatformats = r""" 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) +# n = XML.Node(o.nodetype, o.tag, isnothing(o.attributes) ? XML.OrderedDict{String,String}() : o.attributes, o.value, isnothing(o.children) ? Vector{XML.Node}() : o.children) return n end function do_sheet_names_match(ws::Worksheet, rng::T) where {T<:Union{SheetCellRef,AbstractSheetCellRange}} @@ -235,13 +236,11 @@ function styles_add_cell_attribute(wb::Workbook, new_att::XML.Node, att::String) # 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.:(==)` - # if node["formatCode"] == new_att["formatCode"] # XML.jl defines `Base.:(==)` + if node["formatCode"] == new_att["formatCode"] 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.:(==)` - # if node == new_att # XML.jl defines `Base.:(==)` + if node == new_att return k - 1 # CellDataFormat is zero-indexed end end @@ -589,10 +588,6 @@ function process_uniform_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguo first = true 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.")) @@ -699,10 +694,6 @@ function process_uniform_ncranges(ws::Worksheet, ncrng::NonContiguousRange)::Int first = true 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.")) diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index 387a0880..8ad00b6a 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -1439,7 +1439,6 @@ function setCfCellIs(ws::Worksheet, rng::CellRange; end cfx["operator"] = operator - push!(cfx, XML.Element("formula", XML.Text(XML.escape(value)))) if !isnothing(value2) && operator ∈ ["between", "notBetween"] @@ -1617,14 +1616,11 @@ function setCfAboveAverage(ws::Worksheet, rng::CellRange; allcfs = allCfs(ws) # get all conditional format blocks old_cf = getConditionalFormats(ws) # extract conditional format info - # isnothing(tryparse(Float64, value)) && throw(XLSXError("Invalid `value`: $value. Must be a number.")) - 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" @@ -1819,7 +1815,7 @@ function setCfFormula(ws::Worksheet, rng::CellRange; !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) - allcfs = allCfs(ws) # get all conditional format blocks + allcfs = allCfs(ws) # get all conditional format blocks old_cf = getConditionalFormats(ws) # extract conditional format info wb = get_workbook(ws) @@ -2209,21 +2205,6 @@ function setCfDataBar(ws::Worksheet, rng::CellRange; XML.h.color(rgb=get_color(allkws["fill_col"]))), XML.h.extLst() ) -# cfx = XML.Element("cfRule", type="dataBar", priority=new_pr) -# cfx_db = XML.Element("dataBar") -# if isnothing(allkws["min_val"]) -# push!(cfx_db, XML.Element("cfvo", type=mnt)) -# else -# push!(cfx_db, XML.Element("cfvo", type=mnt, val=allkws["min_val"])) -# end -# if isnothing(allkws["max_val"]) -# push!(cfx_db, XML.Element("cfvo", type=mxt)) -# else -# push!(cfx_db, XML.Element("cfvo", type=mxt, val=allkws["max_val"])) -# end -# push!(cfx_db, XML.Element("color", rgb=get_color(allkws["fill_col"]))) -# push!(cfx, cfx_db) -# push!(cfx, XML.Element("extLst")) if haskey(allkws, "showVal") && !isnothing(allkws["showVal"]) && allkws["showVal"] == "false" cfx[1]["showValue"] = "0" end diff --git a/src/styles.jl b/src/styles.jl index 51b372a7..0f6a6d8f 100644 --- a/src/styles.jl +++ b/src/styles.jl @@ -105,14 +105,8 @@ function styles_add_numFmt(wb::Workbook, format_code::AbstractString)::Integer # 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") XML.pushfirst!(stylesheet, numfmts) - # push!(stylesheet, stylesheet[end]) - # for i in nchildren-1:-1:1 - # stylesheet[i+1]=stylesheet[i] - # end - # stylesheet[1]=numfmts else numfmts = numfmts[1] end @@ -270,10 +264,8 @@ function styles_add_cell_xf(wb::Workbook, new_xf::XML.Node)::CellDataFormat 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.parse(XML.Node, XML.write(node))[1] == XML.parse(XML.Node, XML.write(new_xf))[1] # XML.jl defines `Base.:(==)` - # if node == new_xf # XML.jl defines `Base.:(==)` + if node == new_xf return CellDataFormat(k - 1) # CellDataFormat is zero-indexed end end From 0c18e30fe23a006ff43f8b9df9a225cf91f602b6 Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Fri, 30 May 2025 09:13:25 +0100 Subject: [PATCH 127/154] Update README.md --- README.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 5 deletions(-) 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) From 44c078dadaff23469146eaa8cb1bf3327cc9bc2b Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 31 May 2025 07:17:28 +0100 Subject: [PATCH 128/154] Eliminate statements like `XML.parse(XML.Node, XML.write(node))` --- docs/src/formatting.md | 15 ++++++++------- src/cellformat-helpers.jl | 16 +++++++++++++--- src/cellformats.jl | 9 +++++---- src/conditional-formats.jl | 13 +++++++++---- 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/docs/src/formatting.md b/docs/src/formatting.md index 7d19f25a..8c5e712f 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -243,7 +243,7 @@ function timeit() print("Using `setUniformFormat` : ") @time do_uniform_format(f) f = setup() - print("Using `setUniformStyles` : ") + print("Using `setUniformStyle` : ") @time do_format_styles(f) return f end @@ -253,9 +253,9 @@ f=timeit() which yields the following timings: ``` -Using `setFormat` : 39.925697 seconds (1.04 G allocations: 71.940 GiB, 19.13% gc time) -Using `setUniformFormat` : 27.875646 seconds (711.00 M allocations: 48.195 GiB, 18.46% gc time) -Using `setUniformStyles` : 0.589316 seconds (14.00 M allocations: 416.628 MiB, 16.98% gc time) +Using `setFormat` : 26.109917 seconds (653.00 M allocations: 53.515 GiB, 22.08% gc time) +Using `setUniformFormat` : 17.602014 seconds (428.00 M allocations: 34.881 GiB, 22.89% gc time) +Using `setUniformStyle` : 0.571542 seconds (14.00 M allocations: 416.621 MiB, 15.63% gc time) ``` The same test, using the more involved `setBorder` function @@ -273,10 +273,11 @@ do_format(f) = XLSX.setBorder(f[1], 1:1000, 1:1000; gives ``` -Using `setBorder` : 96.824494 seconds (2.82 G allocations: 194.342 GiB, 18.82% gc time) -Using `setUniformBorder` : 32.182135 seconds (787.00 M allocations: 62.081 GiB, 20.85% gc time) -Using `setUniformStyles` : 0.606058 seconds (14.00 M allocations: 416.660 MiB, 16.19% gc time) +Using `setBorder` : 48.136904 seconds (1.23 G allocations: 111.901 GiB, 23.39% gc time) +Using `setUniformBorder` : 23.961719 seconds (504.00 M allocations: 48.812 GiB, 23.93% gc time) +Using `setUniformStyle` : 0.668181 seconds (14.00 M allocations: 416.626 MiB, 14.36% 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 much more efficient still to use `setUniformStyle`. diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl index f72b25f2..d731d220 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -58,9 +58,17 @@ const floatformats = r""" # 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) +# 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) +# if isnothing(XML.children(o)) +# n = XML.Node(o, children=nothing) +# elseif length(XML.children(o)) == 0 +# n = XML.Node(o, children=nothing) +# else +# n = XML.Node(o, children=[x for x in o.children]) +# end # n = XML.Node(o.nodetype, o.tag, isnothing(o.attributes) ? XML.OrderedDict{String,String}() : o.attributes, o.value, isnothing(o.children) ? Vector{XML.Node}() : o.children) + n = deepcopy(o) return n end function do_sheet_names_match(ws::Worksheet, rng::T) where {T<:Union{SheetCellRef,AbstractSheetCellRange}} @@ -210,7 +218,9 @@ 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 + 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 diff --git a/src/cellformats.jl b/src/cellformats.jl index 6fa4ed73..74513892 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -2516,6 +2516,10 @@ 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: @@ -2534,7 +2538,7 @@ mergeCells(ws::Worksheet, rng::SheetColumnRange) = do_sheet_names_match(ws, rng) 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(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) @@ -2586,12 +2590,9 @@ function mergeCells(ws::Worksheet, cr::CellRange) throw(XLSXError("Unexpected number of mergeCells found: $(length(c)). Expected $(sheetdoc[i][j]["count"]).")) end for child in c - # for cell in cr - # if cell in CellRange(child["ref"]) if intersects(cr, CellRange(child["ref"])) throw(XLSXError("Merged range (`$cr`) cannot overlap with existing merged range (`" * child["ref"] * "`).")) end - # end end end diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index 8ad00b6a..dd86b9d7 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -2037,9 +2037,14 @@ function setCfIconSet(ws::Worksheet, rng::CellRange; end if !isnothing(val) if !isnothing(type) && type=="formula" - cfx[1][i+1][1] = XML.Element("xm:f", XML.Text("(" * val * ")")) + c=XML.Element("xm:f", XML.Text("(" * val * ")")) else - cfx[1][i+1][1] = XML.Element("xm:f", XML.Text(val)) + 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" @@ -2077,7 +2082,7 @@ function setCfIconSet(ws::Worksheet, rng::CellRange; if !isnothing(type) && type=="formula" cfx[1][i+1]["val"] = "(" * val * ")" else - cfx[1][i+1]["val"] = val + cfx[1][i+1]["val"] = val end end if !isnothing(type) @@ -2086,7 +2091,7 @@ function setCfIconSet(ws::Worksheet, rng::CellRange; if !isnothing(gte) && gte == "false" cfx[1][i+1]["gte"] = "0" end - end + end update_worksheet_cfx!(allcfs, cfx, ws, rng) end From f484de2f3b3d5fa5ee23de264b18bfa169cc2c4b Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 31 May 2025 07:22:11 +0100 Subject: [PATCH 129/154] Remove some dead code. --- src/cellformat-helpers.jl | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl index d731d220..616e87de 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -58,16 +58,6 @@ const floatformats = r""" # 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) -# if isnothing(XML.children(o)) -# n = XML.Node(o, children=nothing) -# elseif length(XML.children(o)) == 0 -# n = XML.Node(o, children=nothing) -# else -# n = XML.Node(o, children=[x for x in o.children]) -# end -# n = XML.Node(o.nodetype, o.tag, isnothing(o.attributes) ? XML.OrderedDict{String,String}() : o.attributes, o.value, isnothing(o.children) ? Vector{XML.Node}() : o.children) n = deepcopy(o) return n end From 8fa8349be20c5d08476bfb9ea36075c248d464d5 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 31 May 2025 07:22:53 +0100 Subject: [PATCH 130/154] Remove some dead code. --- src/conditional-format-helpers.jl | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/conditional-format-helpers.jl b/src/conditional-format-helpers.jl index e6511668..ab4c6c29 100644 --- a/src/conditional-format-helpers.jl +++ b/src/conditional-format-helpers.jl @@ -1,16 +1,16 @@ # # ---- 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 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 From 005acaa4dbc882228e109865c86bcce1c45c4a58 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 31 May 2025 15:51:35 +0100 Subject: [PATCH 131/154] Use new `copynode(::Node)` instead of `deepcopy()` --- src/cellformat-helpers.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl index 616e87de..cc880efc 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -58,7 +58,8 @@ const floatformats = r""" # function copynode(o::XML.Node) - n = deepcopy(o) + 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}} From 08f7ca4d54e6e22e6c6d6f47491eb545d0c83075 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 1 Jun 2025 20:45:50 +0100 Subject: [PATCH 132/154] Pack conditional format keywords into a Dict to improve compilation time. Reduce number of xalls to `find_all_nodes()` to improve performance. --- docs/src/formatting.md | 18 +- src/cellformat-helpers.jl | 68 ++-- src/cellformats.jl | 40 ++- src/conditional-formats.jl | 665 +++++++++++++++++++++++++------------ src/styles.jl | 29 +- test/runtests.jl | 6 +- 6 files changed, 570 insertions(+), 256 deletions(-) diff --git a/docs/src/formatting.md b/docs/src/formatting.md index 8c5e712f..a2c718c0 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -253,9 +253,9 @@ f=timeit() which yields the following timings: ``` -Using `setFormat` : 26.109917 seconds (653.00 M allocations: 53.515 GiB, 22.08% gc time) -Using `setUniformFormat` : 17.602014 seconds (428.00 M allocations: 34.881 GiB, 22.89% gc time) -Using `setUniformStyle` : 0.571542 seconds (14.00 M allocations: 416.621 MiB, 15.63% gc time) +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 @@ -273,14 +273,14 @@ do_format(f) = XLSX.setBorder(f[1], 1:1000, 1:1000; gives ``` -Using `setBorder` : 48.136904 seconds (1.23 G allocations: 111.901 GiB, 23.39% gc time) -Using `setUniformBorder` : 23.961719 seconds (504.00 M allocations: 48.812 GiB, 23.93% gc time) -Using `setUniformStyle` : 0.668181 seconds (14.00 M allocations: 416.626 MiB, 14.36% gc time) +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 much more efficient still to use `setUniformStyle`. +cell ranges, and more efficient still to use `setUniformStyle`. ## Copying formatting attributes @@ -1261,8 +1261,8 @@ 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. -```julia -julia> f=XLSX.newxlsx() +```juliaf=XLSX.newxlsx() +julia> XLSXFile("C:\...\blank.xlsx") containing 1 Worksheet sheetname size range ------------------------------------------------- diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl index cc880efc..56c6de14 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -206,6 +206,17 @@ function update_template_xf(ws::Worksheet, existing_style::CellDataFormat, attri 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(ws.package.workbook, 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) @@ -218,6 +229,18 @@ function update_template_xf(ws::Worksheet, existing_style::CellDataFormat, align 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(ws.package.workbook, 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 @@ -381,9 +404,6 @@ function process_rowranges(f::Function, ws::Worksheet, rowrng::RowRange; kw...): end end function process_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int - # if occursin("Uniform", string(nameof(f))) # Shouldn't happen! - # throw(XLSXError("Cannot apply `setUniformAttribute()` functions to a non-contiguous range.\nUse the equivalent `setAttribute()` function instead.")) - # end bounds = nc_bounds(ncrng) if length(ncrng) == 1 single = true @@ -536,9 +556,9 @@ end # # -# Most set functions (but not Style or Alignment) +# Most setUniform functions (but not Style or Alignment - see below) # -function process_uniform_core(f::Function, ws::Worksheet, cellref::CellRef, atts::Vector{String}, newid::Union{Int,Nothing}, first::Bool; kw...) +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 @@ -548,9 +568,9 @@ function process_uniform_core(f::Function, ws::Worksheet, cellref::CellRef, atts 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) + cell.style = string(get_num_style_index(ws, allXfNodes, 0).id) end - cell.style = string(update_template_xf(ws, CellDataFormat(parse(Int, cell.style)), atts, [string(newid), "1"]).id) + cell.style = string(update_template_xf(ws, allXfNodes, CellDataFormat(parse(Int, cell.style)), atts, [string(newid), "1"]).id) end return newid, first end @@ -558,12 +578,13 @@ function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange, a 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, cellref, atts, newid, first; kw...) + newid, first = process_uniform_core(f, ws, allXfNodes, cellref, atts, newid, first; kw...) end if first newid = -1 @@ -572,6 +593,7 @@ function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange, a 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 @@ -594,10 +616,10 @@ function process_uniform_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguo single && throw(XLSXError("Cannot set format for an `EmptyCell`: $(r.name). Set the value first.")) continue end - newid, first = process_uniform_core(f, ws, r, atts, newid, first; kw...) + 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, c, atts, newid, first; kw...) + newid, first = process_uniform_core(f, ws, allXfNodes, c, atts, newid, first; kw...) end # else # throw(XLSXError("Something wrong here!")) @@ -615,6 +637,7 @@ 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 @@ -632,7 +655,7 @@ function process_uniform_veccolon(f::Function, ws::Worksheet, row, col, atts::Ve if getcell(ws, cellref) isa EmptyCell continue end - newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) + newid, first = process_uniform_core(f, ws, allXfNodes, cellref, atts, newid, first; kw...) end end if first @@ -642,6 +665,7 @@ function process_uniform_veccolon(f::Function, ws::Worksheet, row, col, atts::Ve 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 @@ -652,7 +676,7 @@ function process_uniform_vecint(f::Function, ws::Worksheet, row, col, atts::Vect if getcell(ws, cellref) isa EmptyCell continue end - newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) + newid, first = process_uniform_core(f, ws, allXfNodes, cellref, atts, newid, first; kw...) end if first newid = -1 @@ -785,7 +809,7 @@ end # # Alignment is different # -function process_uniform_core(f::Function, ws::Worksheet, cellref::CellRef, newid::Union{Int,Nothing}, first::Bool, alignment_node::Union{XML.Node,Nothing}; kw...) # setUniformAlignment 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 @@ -797,9 +821,9 @@ function process_uniform_core(f::Function, ws::Worksheet, cellref::CellRef, newi 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) + cell.style = string(get_num_style_index(ws, allXfNodes, 0).id) end - cell.style = string(update_template_xf(ws, CellDataFormat(parse(Int, cell.style)), alignment_node).id) + cell.style = string(update_template_xf(ws, allXfNodes, CellDataFormat(parse(Int, cell.style)), alignment_node).id) end return newid, first, alignment_node end @@ -807,6 +831,7 @@ function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange; k 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 @@ -816,7 +841,7 @@ function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange; k if getcell(ws, cellref) isa EmptyCell continue end - newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) + newid, first, alignment_node = process_uniform_core(f, ws, allXfNodes, cellref, newid, first, alignment_node; kw...) end if first newid = -1 @@ -825,6 +850,7 @@ function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange; k 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 @@ -852,10 +878,10 @@ function process_uniform_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguo 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, r, newid, first, alignment_node; kw...) + 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, c, newid, first, alignment_node; kw...) + newid, first, alignment_node = process_uniform_core(f, ws, allXfNodes, c, newid, first, alignment_node; kw...) end # else # throw(XLSXError("Something wrong here!")) @@ -876,6 +902,7 @@ function process_uniform_veccolon(f::Function, ws::Worksheet, row, col; kw...) 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 @@ -894,7 +921,7 @@ function process_uniform_veccolon(f::Function, ws::Worksheet, row, col; kw...) if getcell(ws, cellref) isa EmptyCell continue end - newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) + newid, first, alignment_node = process_uniform_core(f, ws, allXfNodes, cellref, newid, first, alignment_node; kw...) end end if first @@ -910,6 +937,7 @@ function process_uniform_vecint(f::Function, ws::Worksheet, row, col; kw...) 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 @@ -919,7 +947,7 @@ function process_uniform_vecint(f::Function, ws::Worksheet, row, col; kw...) if getcell(ws, cellref) isa EmptyCell continue end - newid, first, alignment_node = process_uniform_core(f, ws, cellref, newid, first, alignment_node; kw...) + newid, first, alignment_node = process_uniform_core(f, ws, allXfNodes, cellref, newid, first, alignment_node; kw...) end if first newid = -1 diff --git a/src/cellformats.jl b/src/cellformats.jl index 74513892..4be59c13 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -126,11 +126,13 @@ function setFont(sh::Worksheet, cellref::CellRef; 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(wb, allXfNodes, parse(Int, cell.style)) new_font_atts = Dict{String,Union{Dict{String,String},Nothing}}() cell_font = getFont(wb, cell_style) @@ -192,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"], [string(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 @@ -704,11 +706,13 @@ function setBorder(sh::Worksheet, cellref::CellRef; 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(wb, allXfNodes, parse(Int, cell.style)) new_border_atts = Dict{String,Union{Dict{String,String},Nothing}}() cell_borders = getBorder(wb, cell_style) @@ -763,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"], [string(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 @@ -1160,11 +1164,13 @@ function setFill(sh::Worksheet, cellref::CellRef; 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(wb, allXfNodes, parse(Int, cell.style)) new_fill_atts = Dict{String,Union{Dict{String,String},Nothing}}() patternFill = Dict{String,String}() @@ -1211,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"], [string(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 @@ -1469,11 +1475,13 @@ function setAlignment(sh::Worksheet, cellref::CellRef; 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(wb, allXfNodes, parse(Int, cell.style)) atts = XML.OrderedDict{String,String}() cell_alignment = getAlignment(wb, cell_style) @@ -1523,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) @@ -1763,11 +1771,13 @@ function setFormat(sh::Worksheet, cellref::CellRef; 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)) + cell_style = styles_cell_xf(wb, allXfNodes, parse(Int, cell.style)) # new_format_atts = Dict{String,Union{Dict{String,String},Nothing}}() new_format = XML.OrderedDict{String,String}() @@ -1789,7 +1799,7 @@ function setFormat(sh::Worksheet, cellref::CellRef; atts = ["numFmtId", "applyNumberFormat"] 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 diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index dd86b9d7..c28573c3 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -22,7 +22,7 @@ const highlights::Dict{String,Dict{String,Dict{String,String}}} = Dict( "border" => Dict("color" => "FF9C0006", "style" => "thin") ) ) -const databars::Dict{String, Dict{String, String}} = Dict( +const databars::Dict{String,Dict{String,String}} = Dict( "bluegrad" => Dict( "min_type" => "automatic", "max_type" => "automatic", @@ -55,7 +55,7 @@ const databars::Dict{String, Dict{String, String}} = Dict( "neg_fill_col" => "FFFF0000", "neg_border_col" => "FFFF0000", "axis_col" => "FF000000" - ), + ), "orangegrad" => Dict( "min_type" => "automatic", "max_type" => "automatic", @@ -88,7 +88,7 @@ const databars::Dict{String, Dict{String, String}} = Dict( "neg_fill_col" => "FFFF0000", "neg_border_col" => "FFFF0000", "axis_col" => "FF000000" - ), + ), "blue" => Dict( "min_type" => "automatic", "max_type" => "automatic", @@ -112,7 +112,7 @@ const databars::Dict{String, Dict{String, String}} = Dict( "gradient" => "false", "neg_fill_col" => "FFFF0000", "axis_col" => "FF000000" - ), + ), "orange" => Dict( "min_type" => "automatic", "max_type" => "automatic", @@ -136,7 +136,7 @@ const databars::Dict{String, Dict{String, String}} = Dict( "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", @@ -388,15 +388,15 @@ const iconsets::Dict{String,XML.Node} = Dict( # Defines the 20 standard, buil "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"), + "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"), @@ -468,8 +468,8 @@ 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}}}}() +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" @@ -479,8 +479,8 @@ function getConditionalFormats(allcfnodes::Vector{XML.Node})::Vector{Pair{CellRa 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}}}}() +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" @@ -1339,52 +1339,56 @@ XLSX.setConditionalFormat(s, "A2:A11", :iconSet; """ function setConditionalFormat(f, r, type::Symbol; kw...) + _allkws = Dict{Symbol,Any}(k => v for (k, v) in kw) if type == :colorScale - setCfColorScale(f, r; kw...) + setCfColorScale(f, r; allkws=_allkws) elseif type == :cellIs - setCfCellIs(f, r; kw...) + setCfCellIs(f, r; allkws=_allkws) elseif type == :top10 - setCfTop10(f, r; kw...) + setCfTop10(f, r; allkws=_allkws) elseif type == :aboveAverage - setCfAboveAverage(f, r; kw...) + setCfAboveAverage(f, r; allkws=_allkws) elseif type == :timePeriod - setCfTimePeriod(f, r; kw...) + setCfTimePeriod(f, r; allkws=_allkws) elseif type ∈ [:containsText, :notContainsText, :beginsWith, :endsWith] - setCfContainsText(f, r; operator=String(type), kw...) + setCfContainsText(f, r; allkws=_allkws) elseif type ∈ [:containsBlanks, :notContainsBlanks, :containsErrors, :notContainsErrors, :duplicateValues, :uniqueValues] - setCfContainsBlankErrorUniqDup(f, r; operator=String(type), kw...) + push!(_allkws, :operator => string(type)) + setCfContainsBlankErrorUniqDup(f, r; allkws=_allkws) elseif type == :expression - setCfFormula(f, r; kw...) + setCfFormula(f, r; allkws=_allkws) elseif type == :iconSet - setCfIconSet(f, r; kw...) + setCfIconSet(f, r; allkws=_allkws) elseif type == :dataBar - setCfDataBar(f, r; kw...) + 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; kw...) + setCfColorScale(f, r, c; allkws=_allkws) elseif type == :cellIs - setCfCellIs(f, r, c; kw...) + setCfCellIs(f, r, c; allkws=_allkws) elseif type == :top10 - setCfTop10(f, r, c; kw...) + setCfTop10(f, r, c; allkws=_allkws) elseif type == :aboveAverage - setCfAboveAverage(f, r, c; kw...) + setCfAboveAverage(f, r, c; allkws=_allkws) elseif type == :timePeriod - setCfTimePeriod(f, r, c; kw...) + setCfTimePeriod(f, r, c; allkws=_allkws) elseif type ∈ [:containsText, :notContainsText, :beginsWith, :endsWith] - setCfContainsText(f, r, c; operator=String(type), kw...) + setCfContainsText(f, r, c; allkws=_allkws) elseif type ∈ [:containsBlanks, :notContainsBlanks, :containsErrors, :notContainsErrors, :duplicateValues, :uniqueValues] - setCfContainsBlankErrorUniqDup(f, r, c; operator=String(type), kw...) + push!(_allkws, :operator => string(type)) + setCfContainsBlankErrorUniqDup(f, r, c; allkws=_allkws) elseif type == :expression - setCfFormula(f, r, c; kw...) + setCfFormula(f, r, c; allkws=_allkws) elseif type == :iconSet - setCfIconSet(f, r, c; kw...) + setCfIconSet(f, r, c; allkws=_allkws) elseif type == :dataBar - setCfDataBar(f, r, c; kw...) + setCfDataBar(f, r, c; allkws=_allkws) else throw(XLSXError("Invalid conditional format type: $type.")) end @@ -1404,17 +1408,41 @@ setCfCellIs(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCfCellIs 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; - 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, +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 -)::Int + + 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.")) @@ -1464,16 +1492,37 @@ setCfContainsText(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCf 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; - 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, +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 -)::Int + 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.")) @@ -1532,16 +1581,37 @@ setCfTop10(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCfTop10, 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; - 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, +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 -)::Int + 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.")) @@ -1601,15 +1671,34 @@ setCfAboveAverage(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCf 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; - 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, +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 -)::Int + 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.")) @@ -1669,15 +1758,34 @@ setCfTimePeriod(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCfTi 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; - 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, +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 -)::Int + 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.")) @@ -1742,21 +1850,39 @@ setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::RowRange; kw...) = process_ro 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; - operator::Union{Nothing,String}="containsBlank", - 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, +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 -)::Int + 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" @@ -1803,15 +1929,35 @@ setCfFormula(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCfFormu 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; - formula::Union{Nothing,String}, - 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, +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 -)::Int + 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.")) @@ -1850,18 +1996,43 @@ setCfColorScale(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCfCo 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; - 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", -)::Int +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))).")) @@ -1875,14 +2046,14 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; 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" + 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" + 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.")) @@ -1891,9 +2062,9 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; if !isnothing(val) if is_valid_fixed_sheet_cellname(val) do_sheet_names_match(ws, SheetCellRef(val)) - val=string(SheetCellRef(val).cellref) + val = string(SheetCellRef(val).cellref) end - val = XML.escape(uppercase_unquoted(val)) + val = XML.escape(uppercase_unquoted(val)) end end @@ -1937,25 +2108,63 @@ setCfIconSet(ws::Worksheet, rng::RowRange; kw...) = process_rowranges(setCfIconS 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; - 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, +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 - )::Int + 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 @@ -1979,9 +2188,9 @@ function setCfIconSet(ws::Worksheet, rng::CellRange; if !isnothing(val) if is_valid_fixed_sheet_cellname(val) do_sheet_names_match(ws, SheetCellRef(val)) - val=string(SheetCellRef(val).cellref) + val = string(SheetCellRef(val).cellref) end - val = XML.escape(uppercase_unquoted(val)) + val = XML.escape(uppercase_unquoted(val)) end end if !haskey(iconsets, iconset) @@ -1989,7 +2198,7 @@ function setCfIconSet(ws::Worksheet, rng::CellRange; end l = first(iconset) cfx = copynode(iconsets[iconset]) - if l=='C' + 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 @@ -1998,19 +2207,19 @@ function setCfIconSet(ws::Worksheet, rng::CellRange; 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 + nicons = 3 elseif isnothing(mid2_type) || isnothing(mid2_val) push!(cfx[1], copynode(cfvo)) # for mid_val cfx[1]["iconSet"] = "4Arrows" - nicons=4 + 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 + 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 + 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' @@ -2025,7 +2234,7 @@ function setCfIconSet(ws::Worksheet, rng::CellRange; cfx[1]["showValue"] = "0" end if !isnothing(reverse) && reverse == "true" - if iconset=="Custom" + if iconset == "Custom" reverse!(icon_list) else cfx[1]["reverse"] = "1" @@ -2036,10 +2245,10 @@ function setCfIconSet(ws::Worksheet, rng::CellRange; 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 * ")")) + if !isnothing(type) && type == "formula" + c = XML.Element("xm:f", XML.Text("(" * val * ")")) else - c=XML.Element("xm:f", XML.Text(val)) + 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) @@ -2054,17 +2263,17 @@ function setCfIconSet(ws::Worksheet, rng::CellRange; if iconset == "Custom" if isnothing(icon_list) throw(XLSXError("No custom icons specified. Must specify between two and four icons.")) - elseif length(icon_list) 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.")) + 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 + 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 + 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.")) @@ -2158,31 +2407,31 @@ function setCfDataBar(ws::Worksheet, rng::CellRange; if !isnothing(val) if is_valid_fixed_sheet_cellname(val) do_sheet_names_match(ws, SheetCellRef(val)) - val=string(SheetCellRef(val).cellref) + val = string(SheetCellRef(val).cellref) end - val = XML.escape(uppercase_unquoted(val)) + 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( + + 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, + "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, + "neg_fill_col" => neg_fill_col, + "neg_border_col" => neg_border_col, + "axis_pos" => axis_pos, "axis_col" => axis_col ) @@ -2200,30 +2449,30 @@ function setCfDataBar(ws::Worksheet, rng::CellRange; haskey(allkws, "axis_pos") && isValidKw("axis_pos", allkws["axis_pos"], ["middle", "none"]) # Define basic elements of dataBar definition - id="{" * uppercase(string(UUIDs.uuid4())) * "}" + 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() - ) + 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}" + 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"] + 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) @@ -2235,7 +2484,7 @@ function setCfDataBar(ws::Worksheet, rng::CellRange; if allkws["gradient"] == "false" ext_db["gradient"] = "0" end - do_borders=haskey(allkws, "borders") && allkws["borders"] == "true" + do_borders = haskey(allkws, "borders") && allkws["borders"] == "true" if do_borders ext_db["border"] = "1" if haskey(allkws, "border_col") && !isnothing(allkws["border_col"]) @@ -2246,13 +2495,13 @@ function setCfDataBar(ws::Worksheet, rng::CellRange; end if haskey(allkws, "direction") if allkws["direction"] == "leftToRight" - ext_db["direction"]="leftToRight" + ext_db["direction"] = "leftToRight" elseif allkws["direction"] == "rightToLeft" - ext_db["direction"]="rightToLeft" + ext_db["direction"] = "rightToLeft" end end - if haskey(allkws, "sameNegFill") && allkws["sameNegFill"]=="true" - ext_db["negativeBarColorSameAsPositive"]="1" + 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"]))) @@ -2260,8 +2509,8 @@ function setCfDataBar(ws::Worksheet, rng::CellRange; 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 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 @@ -2270,9 +2519,9 @@ function setCfDataBar(ws::Worksheet, rng::CellRange; end if haskey(allkws, "axis_pos") if allkws["axis_pos"] == "none" - ext_db["axisPosition"]="none" + ext_db["axisPosition"] = "none" elseif allkws["axis_pos"] == "middle" - ext_db["axisPosition"]="middle" + ext_db["axisPosition"] = "middle" end end haskey(allkws, "axis_col") && push!(ext_db, XML.Element("x14:axisColor", rgb=get_color(allkws["axis_col"]))) diff --git a/src/styles.jl b/src/styles.jl index 0f6a6d8f..02578986 100644 --- a/src/styles.jl +++ b/src/styles.jl @@ -53,6 +53,18 @@ function get_num_style_index(ws::Worksheet, numformatid::Integer) 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 +end # get styles document for workbook function styles_xmlroot(workbook::Workbook) @@ -84,6 +96,10 @@ function styles_cell_xf(wb::Workbook, index::Int)::XML.Node 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(wb::Workbook, allXfNodes::Vector{XML.Node}, index::Int)::XML.Node + xf_elements = allXfNodes + return xf_elements[index+1] +end # Queries numFmtId from cellXfs -> xf nodes." function styles_cell_xf_numFmtId(wb::Workbook, index::Int)::Int @@ -93,6 +109,13 @@ function styles_cell_xf_numFmtId(wb::Workbook, index::Int)::Int end return parse(Int, el["numFmtId"]) end +function styles_cell_xf_numFmtId(wb::Workbook, allXfNodes::Vector{XML.Node}, index::Int)::Int + el = styles_cell_xf(wb, 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. @@ -229,7 +252,11 @@ Returns -1 if not found. =# 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) + 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 + elements_found = allXfNodes if isempty(elements_found) return EmptyCellDataFormat() diff --git a/test/runtests.jl b/test/runtests.jl index 87f30e4f..9d535d04 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3370,7 +3370,7 @@ end end end -@testset "Conditional Formats" verbose=true begin +@testset "Conditional Formats" begin @testset "DataBar" begin @@ -3938,7 +3938,7 @@ end min_type="percent", mid_type="percent", max_type="percent", min_val="25", mid_val="50", max_val="75" ) - @test_throws TypeError XLSX.setConditionalFormat(s, 1:4, 1, :iconSet; + @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, 1:4, 1, :iconSet; iconset="Custom", icon_list=[], min_type="percent", mid_type="percent", max_type="percent", @@ -4948,7 +4948,7 @@ end @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 UndefKeywordError XLSX.setConditionalFormat(s, 1:10, 1:3, :expression; + @test_throws XLSX.XLSXError XLSX.setConditionalFormat(s, 1:10, 1:3, :expression; stopIfTrue="true", fill=["pattern" => "none", "bgColor" => "FFFFC7CE"], border=["style" => "thick", "color" => "coral"], From 5de2dc9b847da08f0221894d7648d550406e4bad Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 1 Jun 2025 21:13:33 +0100 Subject: [PATCH 133/154] Rationalise some function calls. --- src/cellformat-helpers.jl | 4 ++-- src/cellformats.jl | 10 +++++----- src/styles.jl | 17 +++++++---------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/cellformat-helpers.jl b/src/cellformat-helpers.jl index 56c6de14..d29bf600 100644 --- a/src/cellformat-helpers.jl +++ b/src/cellformat-helpers.jl @@ -207,7 +207,7 @@ function update_template_xf(ws::Worksheet, existing_style::CellDataFormat, attri 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(ws.package.workbook, allXfNodes, Int(existing_style.id)) + 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.")) @@ -230,7 +230,7 @@ function update_template_xf(ws::Worksheet, existing_style::CellDataFormat, align 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(ws.package.workbook, allXfNodes, Int(existing_style.id)) + 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) diff --git a/src/cellformats.jl b/src/cellformats.jl index 4be59c13..2becd509 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -132,7 +132,7 @@ function setFont(sh::Worksheet, cellref::CellRef; cell.style = string(get_num_style_index(sh, allXfNodes, 0).id) end - cell_style = styles_cell_xf(wb, allXfNodes, 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) @@ -712,7 +712,7 @@ function setBorder(sh::Worksheet, cellref::CellRef; cell.style = string(get_num_style_index(sh, allXfNodes, 0).id) end - cell_style = styles_cell_xf(wb, allXfNodes, 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) @@ -1170,7 +1170,7 @@ function setFill(sh::Worksheet, cellref::CellRef; cell.style = string(get_num_style_index(sh, allXfNodes, 0).id) end - cell_style = styles_cell_xf(wb, allXfNodes, 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}() @@ -1481,7 +1481,7 @@ function setAlignment(sh::Worksheet, cellref::CellRef; cell.style = string(get_num_style_index(sh, allXfNodes, 0).id) end - cell_style = styles_cell_xf(wb, allXfNodes, 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) @@ -1777,7 +1777,7 @@ function setFormat(sh::Worksheet, cellref::CellRef; cell.style = string(get_num_style_index(sh, allXfNodes, 0).id) end - cell_style = styles_cell_xf(wb, allXfNodes, parse(Int, cell.style)) + 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}() diff --git a/src/styles.jl b/src/styles.jl index 02578986..1ca22338 100644 --- a/src/styles.jl +++ b/src/styles.jl @@ -96,9 +96,8 @@ function styles_cell_xf(wb::Workbook, index::Int)::XML.Node 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(wb::Workbook, allXfNodes::Vector{XML.Node}, index::Int)::XML.Node - xf_elements = allXfNodes - return xf_elements[index+1] +function styles_cell_xf(allXfNodes::Vector{XML.Node}, index::Int)::XML.Node + return allXfNodes[index+1] end # Queries numFmtId from cellXfs -> xf nodes." @@ -109,8 +108,8 @@ function styles_cell_xf_numFmtId(wb::Workbook, index::Int)::Int end return parse(Int, el["numFmtId"]) end -function styles_cell_xf_numFmtId(wb::Workbook, allXfNodes::Vector{XML.Node}, index::Int)::Int - el = styles_cell_xf(wb, allXfNodes, index) +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 @@ -256,13 +255,11 @@ function styles_get_cellXf_with_numFmtId(wb::Workbook, numFmtId::Int)::AbstractC return styles_get_cellXf_with_numFmtId(allXfNodes, numFmtId) end function styles_get_cellXf_with_numFmtId(allXfNodes::Vector{XML.Node}, numFmtId::Int)::AbstractCellDataFormat - elements_found = allXfNodes - - if isempty(elements_found) + 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 From 80acab2116fdec6970c8b02935372237e4b87ca0 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 2 Jun 2025 00:33:22 +0100 Subject: [PATCH 134/154] Generate a new uuid when adding a new worksheets. --- src/write.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/write.jl b/src/write.jl index fc21c45f..862de3fd 100644 --- a/src/write.jl +++ b/src/write.jl @@ -962,6 +962,9 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path: 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 From 58ddf08fd2e7dbf0219343fee487c4fed332fbdd Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Wed, 4 Jun 2025 23:43:36 +0100 Subject: [PATCH 135/154] Add `copysheet!` with docs and tests. Also include code from #300 and #302 --- data/empty_v.xlsx | Bin 0 -> 8841 bytes docs/src/api.md | 1 + src/cell.jl | 6 +- src/relationship.jl | 2 +- src/workbook.jl | 4 +- src/worksheet.jl | 54 ++++++++---- src/write.jl | 198 ++++++++++++++++++++++++++++++++++---------- test/runtests.jl | 80 +++++++++++++++--- 8 files changed, 266 insertions(+), 79 deletions(-) create mode 100644 data/empty_v.xlsx diff --git a/data/empty_v.xlsx b/data/empty_v.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..43b397f715e98b27225819da7e25e5fbcea6cff3 GIT binary patch literal 8841 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 and 0 || length(wb.workbook_names) > 0 # skip if no defined names present + 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) @@ -917,31 +912,159 @@ If `name` is not provided, a unique name is created. """ addsheet!(xl::XLSXFile, name::AbstractString="")::Worksheet = addsheet!(get_workbook(xl), name) function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path::String=_relocatable_data_path())::Worksheet - xf = get_xlsxfile(wb) - !is_writable(xf) && throw(XLSXError("XLSXFile instance is not writable.")) - file_sheet_template = joinpath(relocatable_data_path, "sheet_template.xml") !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) + # 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(get_xlsxfile(wb), "xl/worksheets/sheet1.xml") # could be any file + state = SheetRowStreamIteratorState(reader, nothing, 0, nothing) + new_ws.cache = XLSX.WorksheetCache( + Dict{Int64,Dict{Int64,XLSX.Cell}}(), + Int64[], + Dict{Int,Union{Float64,Nothing}}(), + Dict{Int64,Int64}(), + SheetRowStreamIterator(new_ws), + state, + 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. + +# 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="") + 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 + reader = open_internal_file_stream(xl, "xl/worksheets/sheet1.xml") # could be any file + state = SheetRowStreamIteratorState(reader, nothing, 0, nothing) + new_ws.cache = XLSX.WorksheetCache( + ws.cache.cells, + ws.cache.rows_in_cache, + ws.cache.row_ht, + ws.cache.row_index, + SheetRowStreamIterator(new_ws), + state, + 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) == parse(Int, ws.relationship_id[4:end])] + 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" * string(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) +# if !in(name, current_sheet_names) + # found a unique name +# break +# end + i += 1 end - name == "" && throw(XLSXError("Something wrong here!")) + new_name == "" && throw(XLSXError("Something wrong here!")) # checks if name is a unique sheet name - name ∈ sheetnames(wb) && throw(XLSXError("A sheet named `$name` already exists in this workbook.")) +# name ∈ sheetnames(wb) && throw(XLSXError("A sheet named `$name` already exists in this workbook.")) function check_valid_sheetname(n::AbstractString) max_length = 31 @@ -954,14 +1077,12 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path: end end - check_valid_sheetname(name) + check_valid_sheetname(new_name) # generate sheetId 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()) * "}" @@ -970,7 +1091,8 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path: i = 1 while true xml_filename = "xl/worksheets/sheet" * string(i) * ".xml" - if !in(xml_filename, keys(xf.files)) +# if !in(xml_filename, keys(xf.files)) + if !haskey(xf.files, xml_filename) break end i += 1 @@ -980,28 +1102,12 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path: 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) @@ -1010,8 +1116,8 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path: ctype_root = xmlroot(get_xlsxfile(wb), "[Content_Types].xml")[end] 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" * string(sheetId) * ".xml" + PartName="/xl/worksheets/sheet" * string(sheetId) * ".xml", + ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" ) push!(ctype_root, override_node) diff --git a/test/runtests.jl b/test/runtests.jl index 9d535d04..d756f01a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1388,6 +1388,7 @@ end end @testset "addsheet!" begin + new_filename = "template_with_new_sheet.xlsx" f = XLSX.open_empty_template() s = XLSX.addsheet!(f, "new_sheet") @@ -1395,7 +1396,6 @@ end @testset "check invalid sheet names" begin invalid_names = [ - "new_sheet", "aaaaaaaaaabbbbbbbbbbccccccccccd1", "abc:def", "abcdef/", @@ -1408,20 +1408,73 @@ end for invalid_name in invalid_names @test_throws XLSX.XLSXError XLSX.addsheet!(f, invalid_name) end + + 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 - big_sheetname = "aaaaaaaaaabbbbbbbbbbccccccccccd" - s2 = XLSX.addsheet!(f, big_sheetname) + @testset "copysheet!" begin - XLSX.writexlsx(new_filename, f, overwrite=true) - f = XLSX.opentemplate(new_filename) - @test XLSX.sheetnames(f) == ["Sheet1", "new_sheet", big_sheetname] + 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 + + end + isfile("copied_sheets.xlsx") && rm("copied_sheets.xlsx") @testset "deletesheet!" begin - XLSX.deletesheet!(f, big_sheetname) - @test XLSX.sheetnames(f) == ["Sheet1", "new_sheet"] - XLSX.writexlsx(new_filename, f, overwrite=true) + 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"] @@ -1463,7 +1516,7 @@ end end - rm(new_filename) + isfile("template_with_new_sheet.xlsx") && rm("template_with_new_sheet.xlsx") end @@ -5741,6 +5794,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"] From e00700891a1af59c140cb149b805f3660756551a Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 6 Jun 2025 16:11:57 +0100 Subject: [PATCH 136/154] Restructured docs a bit --- docs/{src => }/api.md | 0 docs/{src => }/formatting.md | 0 docs/make.jl | 15 +- docs/src/api/data.md | 31 + docs/src/api/files.md | 25 + docs/src/api/formats.md | 42 + docs/src/formatting/cellFormatting.md | 316 ++++ docs/src/formatting/conditionalFormatting.md | 1439 ++++++++++++++++++ docs/src/formatting/examples.md | 297 ++++ docs/src/formatting/mergedCells.md | 168 ++ docs/src/migration.md | 2 +- src/cellformats.jl | 8 +- src/conditional-formats.jl | 41 +- src/read.jl | 2 - src/stream.jl | 2 +- src/types.jl | 2 +- src/write.jl | 121 +- test/runtests.jl | 2 +- 18 files changed, 2456 insertions(+), 57 deletions(-) rename docs/{src => }/api.md (100%) rename docs/{src => }/formatting.md (100%) create mode 100644 docs/src/api/data.md create mode 100644 docs/src/api/files.md create mode 100644 docs/src/api/formats.md create mode 100644 docs/src/formatting/cellFormatting.md create mode 100644 docs/src/formatting/conditionalFormatting.md create mode 100644 docs/src/formatting/examples.md create mode 100644 docs/src/formatting/mergedCells.md diff --git a/docs/src/api.md b/docs/api.md similarity index 100% rename from docs/src/api.md rename to docs/api.md diff --git a/docs/src/formatting.md b/docs/formatting.md similarity index 100% rename from docs/src/formatting.md rename to docs/formatting.md diff --git a/docs/make.jl b/docs/make.jl index c5a2d834..f62c794d 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -7,9 +7,18 @@ makedocs( pages = [ "Home" => "index.md", "Tutorial" => "tutorial.md", - "Formatting Guide" => "formatting.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..135d8d9a --- /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.readdf +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..a8d17ece --- /dev/null +++ b/docs/src/api/files.md @@ -0,0 +1,25 @@ +# Files and worksheets + +## Files + +```@docs +XLSX.XLSXFile +XLSX.readxlsx +XLSX.openxlsx +XLSX.opentemplate +XLSX.newxlsx +XLSX.writexlsx +``` + +## 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: + +![image|320x500](../images/multicell.png) + +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 + +![image|320x500](../images/multicell2.png) + +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. + +![image|320x500](../images/multicell3.png) + +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. + +![image|320x500](../images/multicell4.png) + +### 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. + +![image|320x500](../images/cell1.png) ![image|100x500](../images/blank.png) ![image|320x500](../images/cell2.png) + +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. + +![image|320x500](../images/cellvalue-formats.png) + +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 + +``` + +![image|320x500](../images/simple-cellvalue-example.png) + +Alternatively, it is possible to specify custom format options to match the options offered in Excel +under the `Custom Format...` option: + +![image|320x500](../images/custom-formats.png) + +!!! 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) + +``` + +![image|320x500](../images/custom-cellvalue-example.png) + +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. + +![image|320x500](../images/expression.png) + +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 +``` +![image|320x500](../images/simpleComparison.png) + +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 +``` +![image|320x500](../images/averageComparison.png) + +(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 +``` +![image|320x500](../images/relativeComparison.png) + +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 +``` +![image|320x500](../images/caseSensitiveComparison.png) + +#### 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. + +![image|320x500](../images/dataBars.png) + +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 + +``` +![image|320x500](../images/simpleDataBar.png) + +All of the options provided by Excel can be adjusted using the provided keyword options. + +![image|320x500](../images/dataBarOptions.png) + +![image|320x500](../images/negAndAxisOptions.png) + +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 +``` + +![image|320x500](../images/minmaxDataBar.png) + +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 +``` +![image|320x500](../images/borderAndGrad.png) + +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 + +``` +![image|320x500](../images/customColors.png) + +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 + +``` +![image|320x500](../images/axisOptions.png) + +#### 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. + +![image|320x500](../images/colorScales.png) + +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: + +![image|320x500](../images/colorScaleOptions.png) + +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 +``` +![image|320x500](../images/custom-colorscale.png) + +#### 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. + +![image|320x500](../images/iconSets.png) + +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 +``` +![image|320x500](../images/basicIconSet.png) + +All of the options to control an iconSet in Excel are available. The iconSet options +(for a 4-icon set) look like this: + +![image|320x500](../images/iconSetOptions.png) + +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 +``` +![image|320x500](../images/newValIconSet.png) + +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 +``` + +![image|320x500](../images/showValIcons.png) + +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) +``` +![image|320x500](../images/iconKey.png) + +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 + +``` +![image|320x500](../images/relative-CellRef.png) + +!!! 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) + +``` + +![image|320x500](../images/multiple-cellvalue-example.png) + +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 +``` + +![image|320x500](../images/stop-if-true.png) + +Overlaying the same three conditional formats without setting the `stopIfTrue` option +will result in the following, instead: + +![image|320x500](../images/no-stop-if-true.png) + +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 + +``` +![image|320x500](../images/moreMixed.png) + +## 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. + + ![image|320x500](../images/simple-unmerged.png) + + 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 + ``` + + ![image|320x500](../images/after-merge.png) + + 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"): + + ![image|320x500](../images/Written-to-merged-cell.png) + + 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: + +![image|320x500](../images/unformatted-table.png) + +We can apply some formatting choices to change the table's appearance: + +![image|320x500](../images/formatted-table.png) + +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: + +![image|320x500](../images/unformatted-table.png) + +We can apply some formatting choices to change the table's appearance: + +![image|320x500](../images/formatted-table.png) + +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. + + ![image|320x500](../images/simple-unmerged.png) + + 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 + ``` + + ![image|320x500](../images/after-merge.png) + + 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"): + + ![image|320x500](../images/Written-to-merged-cell.png) + + 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/migration.md b/docs/src/migration.md index 3c79888d..aeffb3d1 100644 --- a/docs/src/migration.md +++ b/docs/src/migration.md @@ -1,4 +1,4 @@ -# Migration Guides +# Migration Guide !!! note diff --git a/src/cellformats.jl b/src/cellformats.jl index 2becd509..7663d761 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -1693,10 +1693,10 @@ end setFormat(sh::Worksheet, row, col; kw...) -> ::Int -Set the 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 `:`. +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 diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index c28573c3..94fb4151 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -461,7 +461,7 @@ const timeperiods::Dict{String,String} = Dict( Get the conditional formats for a worksheet. # Arguments -- `ws::Worksheet`: The worksheet to get the conditional formats for. +- `ws::Worksheet`: The worksheet for which to get the conditional formats. Return a vector of pairs: CellRange => NamedTuple{type::String, priority::Int}}. @@ -517,6 +517,9 @@ 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: @@ -574,26 +577,6 @@ 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. - -!!! note "Overlaying conditional formats" - - It is possible to overlay multiple conditional formats to the same range or to - overlapping ranges. Each format is applied in turn to each cell in priority - order which, here, is the order in which they are created. Different format - options may complement or override each other and the finished appearance will - be the resuilt of all formats overlaying each other. - - It is possible to terminate the sequential application of conditional formats to a - cell if the condition related to any format is met. This is achieved by setting the - keyword option `stopIfTrue="true"` in the relevant conditional format. - - While the `stopIfTrue` keyword is available for most conditional formats, it is not - available for `:colorScale`, `:dataBar` or `:iconSet` conditional formats since these - do not apply a specific test in each cell. - - For example usage of the `stopIfTrue` keyword, refer to [Overlaying conditional formats](@ref) - in the Formatting Guide. - 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: @@ -689,6 +672,14 @@ Valid values for the `operator` keyword are the following: - `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`. @@ -739,7 +730,7 @@ julia> XLSX.setConditionalFormat(s, "A1:J10", :top10; 0 ``` -![image|320x500](./images/topN.png) +![image|320x500](../images/topN.png) # type = :aboveAverage @@ -929,7 +920,7 @@ julia> XLSX.setConditionalFormat(s, "A1:A4", :endsWith ; 0 ``` -![image|320x500](./images/containsText.png) +![image|320x500](../images/containsText.png) # type = :timePeriod @@ -994,7 +985,7 @@ julia> XLSX.setConditionalFormat(s, "A1:A13", :timePeriod; 0 ``` -![image|320x500](./images/timePeriod-9thMay2025.png) +![image|320x500](../images/timePeriod-9thMay2025.png) # type = :containsErrors, :notContainsErrors, :containsBlanks, :notContainsBlanks, :uniqueValues or :duplicateValues @@ -1039,7 +1030,7 @@ julia> XLSX.setConditionalFormat(s, "A1:A7", :duplicateValues; 0 ``` -![image|320x500](./images/errorBlank.png) +![image|320x500](../images/errorBlank.png) # type = :expressiom diff --git a/src/read.jl b/src/read.jl index 7a2d75bb..09ca1d45 100644 --- a/src/read.jl +++ b/src/read.jl @@ -155,8 +155,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). """ diff --git a/src/stream.jl b/src/stream.jl index f87e4ead..4830989e 100644 --- a/src/stream.jl +++ b/src/stream.jl @@ -317,7 +317,7 @@ 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 Base.eachrow(ws::Worksheet) :: SheetRowIterator +function eachrow(ws::Worksheet) :: SheetRowIterator if is_cache_enabled(ws) if ws.cache === nothing ws.cache = WorksheetCache(ws) diff --git a/src/types.jl b/src/types.jl index 1a57e1cd..f0e4fe74 100644 --- a/src/types.jl +++ b/src/types.jl @@ -353,7 +353,7 @@ struct DefinedNameValue end # Workbook is the result of parsing file `xl/workbook.xml`. -# The `xl/workbook.xml` wi9ll need to be updated using the Workbook_names and +# 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 diff --git a/src/write.jl b/src/write.jl index 101d2de2..4cc3dffd 100644 --- a/src/write.jl +++ b/src/write.jl @@ -3,12 +3,15 @@ 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`. +and saving to another file with [XLSX.writexlsx](@ref). -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. +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 @@ -28,7 +31,7 @@ end newxlsx() :: XLSXFile Return an empty, writable `XLSXFile` with 1 worksheet (`Sheet1`) for editing and -saving to a file with `XLSX.writexlsx`. +subsequent saving to a file with [XLSX.writexlsx](@ref). # Examples ```julia @@ -904,13 +907,16 @@ function rename!(ws::Worksheet, name::AbstractString) end """ - 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. +See also [copysheet!](@ref), [deletesheet!](@ref) + """ -addsheet!(xl::XLSXFile, name::AbstractString="")::Worksheet = addsheet!(get_workbook(xl), name) +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") !isfile(file_sheet_template) && throw(XLSXError("Couldn't find template file $file_sheet_template.")) @@ -935,7 +941,7 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path: end """ - copysheet!(ws::Worksheet, name::AbstractString="") --> Worksheet + 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. @@ -950,6 +956,8 @@ See also [`XLSX.openxlsx`](@ref) and [XLSX.opentemplate](@ref). 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") @@ -995,7 +1003,7 @@ XLSXFile("C:\\...\\general.xlsx") containing 14 Worksheets ``` """ -function copysheet!(ws::Worksheet, name::AbstractString="") +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.")) @@ -1127,19 +1135,94 @@ function insertsheet!(wb::Workbook, xdoc::XML.Node, name::AbstractString=""; dim end """ - deletesheet!(ws::Worksheet) -> ::Nothing - deletesheet!(wb::Workbook, name::AbstractString) -> ::Nothing - deletesheet!(xf::XLSXFile, name::AbstractString) -> ::Nothing - deletesheet!(xf::XLSXFile, idx::Integer) -> ::Nothing + 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 -Delete the given worksheet. The workbook can be saved back to file using, -for example, `XLSX.writexlsx("myfile.xlsx", xf)`. + +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, idx::Integer) = deletesheet!(get_workbook(xl), xl[idx].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) +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!")) @@ -1218,7 +1301,7 @@ function deletesheet!(wb::Workbook, name::AbstractString) update_workbook_xml!(xf) - return nothing + return xf end # diff --git a/test/runtests.jl b/test/runtests.jl index d756f01a..beda392e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1499,7 +1499,7 @@ end @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) === nothing + @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") From 0cfe0579bbc9e65700422a062b5f314171ef5747 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 7 Jun 2025 00:14:08 +0100 Subject: [PATCH 137/154] Restructure `eachrow` iterator. Separate stream interator from cache iterator --- docs/formatting.md | 1752 -------------------------------------------- src/read.jl | 9 +- src/stream.jl | 65 +- src/types.jl | 6 + src/write.jl | 18 +- test/runtests.jl | 37 +- 6 files changed, 84 insertions(+), 1803 deletions(-) delete mode 100644 docs/formatting.md diff --git a/docs/formatting.md b/docs/formatting.md deleted file mode 100644 index a2c718c0..00000000 --- a/docs/formatting.md +++ /dev/null @@ -1,1752 +0,0 @@ - -# Formatting Guide - -## 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: - -![image|320x500](./images/multicell.png) - -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 - -![image|320x500](./images/multicell2.png) - -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. - -![image|320x500](./images/multicell3.png) - -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. - -![image|320x500](./images/multicell4.png) - -### 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. - -## 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. - -![image|320x500](./images/cell1.png) ![image|100x500](./images/blank.png) ![image|320x500](./images/cell2.png) - -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. - -![image|320x500](./images/cellvalue-formats.png) - -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 - -``` - -![image|320x500](./images/simple-cellvalue-example.png) - -Alternatively, it is possible to specify custom format options to match the options offered in Excel -under the `Custom Format...` option: - -![image|320x500](./images/custom-formats.png) - -!!! 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) - -``` - -![image|320x500](./images/custom-cellvalue-example.png) - -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. - -![image|320x500](./images/expression.png) - -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 -``` -![image|320x500](./images/simpleComparison.png) - -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 -``` -![image|320x500](./images/averageComparison.png) - -(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 -``` -![image|320x500](./images/relativeComparison.png) - -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 -``` -![image|320x500](./images/caseSensitiveComparison.png) - -#### 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. - -![image|320x500](./images/dataBars.png) - -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 - -``` -![image|320x500](./images/simpleDataBar.png) - -All of the options provided by Excel can be adjusted using the provided keyword options. - -![image|320x500](./images/dataBarOptions.png) - -![image|320x500](./images/negAndAxisOptions.png) - -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 -``` - -![image|320x500](./images/minmaxDataBar.png) - -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 -``` -![image|320x500](./images/borderAndGrad.png) - -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 - -``` -![image|320x500](./images/customColors.png) - -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 - -``` -![image|320x500](./images/axisOptions.png) - -#### 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. - -![image|320x500](./images/colorScales.png) - -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: - -![image|320x500](./images/colorScaleOptions.png) - -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 -``` -![image|320x500](./images/custom-colorscale.png) - -#### 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. - -![image|320x500](./images/iconSets.png) - -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 -``` -![image|320x500](./images/basicIconSet.png) - -All of the options to control an iconSet in Excel are available. The iconSet options -(for a 4-icon set) look like this: - -![image|320x500](./images/iconSetOptions.png) - -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 -``` -![image|320x500](./images/newValIconSet.png) - -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 -``` - -![image|320x500](./images/showValIcons.png) - -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) -``` -![image|320x500](./images/iconKey.png) - -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 - -``` -![image|320x500](./images/relative-CellRef.png) - -!!! 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) - -``` - -![image|320x500](./images/multiple-cellvalue-example.png) - -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 -``` - -![image|320x500](./images/stop-if-true.png) - -Overlaying the same three conditional formats without setting the `stopIfTrue` option -will result in the following, instead: - -![image|320x500](./images/no-stop-if-true.png) - -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 - -``` -![image|320x500](./images/moreMixed.png) - -## 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. - - ![image|320x500](./images/simple-unmerged.png) - - 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 - ``` - - ![image|320x500](./images/after-merge.png) - - 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"): - - ![image|320x500](./images/Written-to-merged-cell.png) - - 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: - -![image|320x500](./images/unformatted-table.png) - -We can apply some formatting choices to change the table's appearance: - -![image|320x500](./images/formatted-table.png) - -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/src/read.jl b/src/read.jl index 09ca1d45..410e18c0 100644 --- a/src/read.jl +++ b/src/read.jl @@ -135,7 +135,7 @@ function openxlsx(f::F, source::Union{AbstractString, IO}; if !(source isa IO || isfile(source)) throw(XLSXError("File $source not found.")) end - xf = open_or_read_xlsx(source, _write, enable_cache, _write) # Why _write, _write here??? + xf = open_or_read_xlsx(source, _read, enable_cache, _write) # Why _write, _write here??? else xf = open_empty_template() end @@ -168,7 +168,7 @@ function openxlsx(source::Union{AbstractString, IO}; if !(source isa IO || isfile(source)) throw(XLSXError("File $source not found.")) end - return open_or_read_xlsx(source, _write, enable_cache, _write) # Why _write, _write here??? + return open_or_read_xlsx(source, _read, enable_cache, _write) else return open_empty_template() end @@ -193,6 +193,7 @@ function open_or_read_xlsx(source::Union{IO, AbstractString}, read_files::Bool, end xf = XLSXFile(source, enable_cache, read_as_template) + for f in ZipArchives.zip_names(xf.io) @@ -227,11 +228,11 @@ 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 + # 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 diff --git a/src/stream.jl b/src/stream.jl index 4830989e..0876ee7d 100644 --- a/src/stream.jl +++ b/src/stream.jl @@ -51,7 +51,7 @@ Base.show(io::IO, state::SheetRowStreamIteratorState) = print(io, "SheetRowStrea !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.filepath) was not found.")) + throw(XLSXError("Can't open internal file $filename for streaming because the XLSX file $(xf.source) was not found.")) end XML.LazyNode(XML.Raw(ZipArchives.zip_readentry(xf.io, filename))) @@ -170,8 +170,6 @@ function Base.iterate(itr::SheetRowStreamIterator, state::Union{Nothing, SheetRo state.ht = current_row_ht state.itr_state = lzstate - - return sheet_row, state end @@ -205,35 +203,53 @@ end # 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, row_from_last_iteration::Int=0) +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) @@ -249,7 +265,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 @@ -318,6 +336,7 @@ 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) diff --git a/src/types.jl b/src/types.jl index f0e4fe74..38f3fa8f 100644 --- a/src/types.jl +++ b/src/types.jl @@ -293,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 flase 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 diff --git a/src/write.jl b/src/write.jl index 4cc3dffd..52a12703 100644 --- a/src/write.jl +++ b/src/write.jl @@ -49,11 +49,12 @@ function open_empty_template( 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=joinpath(pwd(), "blank.xlsx") return xf end @@ -922,19 +923,14 @@ function addsheet!(wb::Workbook, name::AbstractString=""; relocatable_data_path: !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) - # 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(get_xlsxfile(wb), "xl/worksheets/sheet1.xml") # could be any file - state = SheetRowStreamIteratorState(reader, nothing, 0, nothing) new_ws.cache = XLSX.WorksheetCache( + false, Dict{Int64,Dict{Int64,XLSX.Cell}}(), Int64[], Dict{Int,Union{Float64,Nothing}}(), Dict{Int64,Int64}(), SheetRowStreamIterator(new_ws), - state, + nothing, false ) return new_ws @@ -1022,20 +1018,18 @@ function copysheet!(ws::Worksheet, name::AbstractString="")::Worksheet new_ws = insertsheet!(wb, xdoc, name, dim=dim) # copy the original worksheet cache to the new worksheet - reader = open_internal_file_stream(xl, "xl/worksheets/sheet1.xml") # could be any file - state = SheetRowStreamIteratorState(reader, nothing, 0, nothing) 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), - state, + 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) == parse(Int, ws.relationship_id[4:end])] 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 diff --git a/test/runtests.jl b/test/runtests.jl index beda392e..25d86680 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1387,14 +1387,32 @@ end rm(new_filename) end -@testset "addsheet!" begin +@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) - new_filename = "template_with_new_sheet.xlsx" - f = XLSX.open_empty_template() - s = XLSX.addsheet!(f, "new_sheet") - s["A1"] = 10 - @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 = [ "aaaaaaaaaabbbbbbbbbbccccccccccd1", "abc:def", @@ -1409,12 +1427,6 @@ end @test_throws XLSX.XLSXError XLSX.addsheet!(f, invalid_name) end - 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 "copysheet!" begin @@ -1470,6 +1482,7 @@ end @testset "deletesheet!" begin + new_filename = "template_with_new_sheet.xlsx" big_sheetname = "aaaaaaaaaabbbbbbbbbbccccccccccd" fx = XLSX.opentemplate(new_filename) XLSX.deletesheet!(fx, big_sheetname) From 6577b39587fe5df4219d683dafae1ba49a3625db Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 7 Jun 2025 08:34:36 +0100 Subject: [PATCH 138/154] Replace `eachrow` with direct cache access (sometimes). --- src/cellformats.jl | 21 ++++++++----------- src/worksheet.jl | 50 ++++++++++++++++++++++++++++------------------ 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/cellformats.jl b/src/cellformats.jl index 7663d761..4c3ca225 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -2234,20 +2234,20 @@ function setRowHeight(ws::Worksheet, rng::CellRange; height::Union{Nothing,Real} 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 end + return 0 end @@ -2294,13 +2294,8 @@ function getRowHeight(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real} throw(XLSXError("Cell specified is outside sheet dimension `$d`")) end - 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 - - 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) diff --git a/src/worksheet.jl b/src/worksheet.jl index 10ae5ea8..cbcb4407 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -177,23 +177,38 @@ 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 @@ -323,16 +338,13 @@ Other examples are as [`getdata()`](@ref). 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" * string(ws.sheetId) * ".xml") && get_xlsxfile(ws).files["xl/worksheets/sheet"*string(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 From c3877c64333922c7df704a491a5e6a250dbf868e Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 7 Jun 2025 11:50:19 +0100 Subject: [PATCH 139/154] Add minor test to worksheet `dimension` --- test/runtests.jl | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 25d86680..8af8d0a6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -777,6 +777,17 @@ end 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 From 0f9206109b28069884a837822d9c129abbb242c1 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 7 Jun 2025 13:03:08 +0100 Subject: [PATCH 140/154] Minor tidy-up! --- src/read.jl | 63 ++++++++++++++++++++++++++++++++++++++++- src/stream.jl | 3 +- src/types.jl | 2 +- src/workbook.jl | 2 -- src/worksheet.jl | 1 - src/write.jl | 73 ------------------------------------------------ 6 files changed, 64 insertions(+), 80 deletions(-) diff --git a/src/read.jl b/src/read.jl index 410e18c0..064f4d47 100644 --- a/src/read.jl +++ b/src/read.jl @@ -31,6 +31,67 @@ function check_for_xlsx_file_format(filepath::AbstractString) 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=joinpath(pwd(), "blank.xlsx") + return xf +end + """ readxlsx(source::Union{AbstractString, IO}) :: XLSXFile @@ -135,7 +196,7 @@ function openxlsx(f::F, source::Union{AbstractString, IO}; if !(source isa IO || isfile(source)) throw(XLSXError("File $source not found.")) end - xf = open_or_read_xlsx(source, _read, enable_cache, _write) # Why _write, _write here??? + xf = open_or_read_xlsx(source, _read, enable_cache, _write) else xf = open_empty_template() end diff --git a/src/stream.jl b/src/stream.jl index 0876ee7d..8491c5b7 100644 --- a/src/stream.jl +++ b/src/stream.jl @@ -199,7 +199,7 @@ 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) @@ -210,7 +210,6 @@ end @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 and a flag on whether the cache is already full or not -#function Base.iterate(ws_cache::WorksheetCache, row_from_last_iteration::Int=0) function Base.iterate(ws_cache::WorksheetCache, state::Union{Nothing, WorksheetCacheIteratorState}=nothing) # If first iteration, check if cache is full diff --git a/src/types.jl b/src/types.jl index 38f3fa8f..10392f17 100644 --- a/src/types.jl +++ b/src/types.jl @@ -299,7 +299,7 @@ mutable struct WorksheetCacheIteratorState end mutable struct WorksheetCache{I<:SheetRowIterator} <: SheetRowIterator - is_empty::Bool # true before first read from file, then flase + 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 diff --git a/src/workbook.jl b/src/workbook.jl index d5a5d93e..9e44840a 100644 --- a/src/workbook.jl +++ b/src/workbook.jl @@ -228,8 +228,6 @@ end @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 is_workbook_defined_name(wb::Workbook, name::AbstractString)::Bool = haskey(wb.workbook_names, name) -#@inline is_worksheet_defined_name(wb::Workbook, sheetId::Int, name::AbstractString)::Bool = haskey(wb.worksheet_names, (sheetId, name)) @inline get_defined_name_value(wb::Workbook, name::AbstractString)::DefinedNameValueTypes = wb.workbook_names[name].value diff --git a/src/worksheet.jl b/src/worksheet.jl index cbcb4407..bf4d74e2 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -442,7 +442,6 @@ function getcellrange(ws::Worksheet, rng::CellRange)::Array{AbstractCell,2} return result end -#getcellrange(ws::Worksheet, s::SheetCellRef) = do_sheet_names_match(ws, s) && getcellrange(ws, s.cellref) 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) diff --git a/src/write.jl b/src/write.jl index 52a12703..13d9498d 100644 --- a/src/write.jl +++ b/src/write.jl @@ -1,63 +1,4 @@ -""" - 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 (`Sheet1`) for editing and -subsequent saving to a file with [XLSX.writexlsx](@ref). - -# 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=joinpath(pwd(), "blank.xlsx") - return xf -end - """ writexlsx(output_source, xlsx_file; [overwrite=false]) @@ -288,10 +229,7 @@ function update_worksheets_xml!(xl::XLSXFile) # Every row has the `spans=1:` property. Set it to the whole range of columns by default d = get_dimension(sheet) - #if !isnothing(get_dimension(sheet)) - # spans_str = string(column_number(get_dimension(sheet).start), ":", column_number(get_dimension(sheet).stop)) spans_str = string(column_number(d.start), ":", column_number(d.stop)) - #end # iterates over WorksheetCache cells and write the XML for r in eachrow(sheet) @@ -441,11 +379,6 @@ 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) @@ -1056,18 +989,12 @@ function insertsheet!(wb::Workbook, xdoc::XML.Node, name::AbstractString=""; dim current_sheet_names = sheetnames(wb) while new_name ∈ current_sheet_names || new_name == "" new_name = (name=="" ? "Sheet" : name * " ") * string(i) -# if !in(name, current_sheet_names) - # found a unique name -# break -# end i += 1 end new_name == "" && throw(XLSXError("Something wrong here!")) # checks if name is a unique sheet name -# name ∈ sheetnames(wb) && throw(XLSXError("A sheet named `$name` already exists in this workbook.")) - function check_valid_sheetname(n::AbstractString) max_length = 31 if length(n) > max_length From 2cd7bf6df9b65ea1ab5e780c5e5da01b1258303c Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Wed, 11 Jun 2025 12:47:12 +0100 Subject: [PATCH 141/154] Fix #250 and add tests for same. --- src/read.jl | 7 +++++-- test/runtests.jl | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/read.jl b/src/read.jl index 064f4d47..a0dfa375 100644 --- a/src/read.jl +++ b/src/read.jl @@ -88,7 +88,7 @@ function open_empty_template( if sheetname != "" rename!(xf[1], sheetname) end - xf.source=joinpath(pwd(), "blank.xlsx") + xf.source="blank.xlsx" return xf end @@ -199,6 +199,7 @@ function openxlsx(f::F, source::Union{AbstractString, IO}; xf = open_or_read_xlsx(source, _read, enable_cache, _write) else xf = open_empty_template() + xf.source = source end try @@ -231,7 +232,9 @@ function openxlsx(source::Union{AbstractString, IO}; 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 diff --git a/test/runtests.jl b/test/runtests.jl index 8af8d0a6..096b0a51 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -156,6 +156,20 @@ data_directory = joinpath(dirname(pathof(XLSX)), "..", "data") @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=XLSX.newxlsx() + @test f.source == "blank.xlsx" + isfile("mytest.xlsx") && rm("mytest.xlsx") + end + end @testset "Cell names" begin From 7951e01ca4e496757dafbbbf37163cf0066bc3ec Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 15 Jun 2025 15:07:52 +0100 Subject: [PATCH 142/154] Add mmap support for files too big for memory. Also add `PrecompileTools` (#277) --- Project.toml | 4 ++++ docs/src/tutorial.md | 8 +++++--- src/XLSX.jl | 27 +++++++++++++++++++++++++ src/read.jl | 48 +++++++++++++++++++++++++++++++++----------- src/stream.jl | 9 ++++++++- src/types.jl | 13 +++++++++--- test/runtests.jl | 12 ++++++++--- 7 files changed, 99 insertions(+), 22 deletions(-) diff --git a/Project.toml b/Project.toml index 630b3705..4aca5d3b 100644 --- a/Project.toml +++ b/Project.toml @@ -9,6 +9,8 @@ version = "0.10.5-dev" 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" @@ -19,6 +21,8 @@ ZipArchives = "49080126-0e18-4c2a-b176-c102e4b3760c" [compat] Colors = "0.13.0" Distributions = "0.25.0" +Mmap = "1.11.0" +PrecompileTools = "1" Random = "1.10.0" Tables = "1" UUIDs = "1.8" diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index a9351ea5..c40e85f0 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -198,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. @@ -207,7 +209,7 @@ where `myfile.xlsx` is a spreadsheet that doesn't fit into memory. ```julia julia> XLSX.openxlsx("myfile.xlsx", enable_cache=false) do f sheet = f["mysheet"] - for r in eachrow(sheet) + for r in XLSX.eachrow(sheet) # r is a `SheetRow`, values are read using column references rn = XLSX.row_number(r) # `SheetRow` row number v1 = r[1] # will read value at column 1 diff --git a/src/XLSX.jl b/src/XLSX.jl index ebc3aa6e..3c3d6fed 100644 --- a/src/XLSX.jl +++ b/src/XLSX.jl @@ -11,6 +11,10 @@ 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 @@ -35,4 +39,27 @@ include("conditional-format-helpers.jl") # must load before conditional-formats. 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/read.jl b/src/read.jl index a0dfa375..0ed84d34 100644 --- a/src/read.jl +++ b/src/read.jl @@ -189,6 +189,15 @@ 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) @@ -196,7 +205,7 @@ function openxlsx(f::F, source::Union{AbstractString, IO}; if !(source isa IO || isfile(source)) throw(XLSXError("File $source not found.")) end - xf = open_or_read_xlsx(source, _read, enable_cache, _write) + xf = open_or_read_xlsx(source, _read, enable_cache, _write; use_stream=!enable_cache) else xf = open_empty_template() xf.source = source @@ -204,12 +213,15 @@ function openxlsx(f::F, source::Union{AbstractString, IO}; 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 @@ -250,13 +262,12 @@ function parse_file_mode(mode::AbstractString) :: Tuple{Bool, Bool} 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 !(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) @@ -361,7 +372,8 @@ function check_minimum_requirements(xf::XLSXFile) content_types = XML.write(xf.data[f]) else content_types = ZipArchives.zip_readentry(xf.io, f, String) - end + end + if occursin("spreadsheetml.sheet", content_types) return nothing elseif occursin("spreadsheetml.template", content_types) @@ -549,7 +561,14 @@ function internal_xml_file_read(xf::XLSXFile, filename::String) :: XML.Node if !internal_xml_file_isread(xf, filename) try - xf.data[filename] = XML.Node(XML.Raw(ZipArchives.zip_readentry(xf.io, filename))) +# if xf.use_cache_for_sheet_data + xf.data[filename] = XML.Node(XML.Raw(ZipArchives.zip_readentry(xf.io, filename))) +# else + # read from zip entry +# ZipArchives.zip_openentry(xf.io, filename) do io +# xf.data[filename] = XML.Node(XML.Raw(read(io))) +# end +# end xf.files[filename] = true # set file as read catch err throw(XLSXError("Failed to parse internal XML file `$filename`")) @@ -621,6 +640,7 @@ function readdata(source::Union{AbstractString, IO}, sheet::Union{AbstractString c = openxlsx(source, enable_cache=false) do xf getdata(getsheet(xf, sheet), ref) end +# GC.gc() return c end @@ -628,6 +648,7 @@ 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 @@ -712,22 +733,25 @@ julia> df = DataFrame(XLSX.readtable("myfile.xlsx", "mysheet")) See also: [`XLSX.gettable`](@ref). """ 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=enable_cache) do xf + 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 +# !enable_cache && GC.gc() + 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=enable_cache) do xf + 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 +# !enable_cache && GC.gc() return c end 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=enable_cache) do xf + 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 +# !enable_cache && GC.gc() return c end diff --git a/src/stream.jl b/src/stream.jl index 8491c5b7..36f33059 100644 --- a/src/stream.jl +++ b/src/stream.jl @@ -54,7 +54,14 @@ Base.show(io::IO, state::SheetRowStreamIteratorState) = print(io, "SheetRowStrea throw(XLSXError("Can't open internal file $filename for streaming because the XLSX file $(xf.source) was not found.")) end - XML.LazyNode(XML.Raw(ZipArchives.zip_readentry(xf.io, filename))) +# if xf.use_cache_for_sheet_data + lznode=XML.LazyNode(XML.Raw(ZipArchives.zip_readentry(xf.io, filename))) +# else +# lznode=ZipArchives.zip_openentry(xf.io, filename) do io +# XML.LazyNode(XML.Raw(read(io))) +# end +# end + return lznode end # Creates a reader for row elements in the Worksheet's XML. diff --git a/src/types.jl b/src/types.jl index 10392f17..5ad9dcae 100644 --- a/src/types.jl +++ b/src/types.jl @@ -394,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 @@ -402,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) + 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 diff --git a/test/runtests.jl b/test/runtests.jl index 096b0a51..9adbdf9e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -165,8 +165,16 @@ data_directory = joinpath(dirname(pathof(XLSX)), "..", "data") @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 @@ -1526,6 +1534,7 @@ end 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") @@ -1655,7 +1664,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") @@ -1672,7 +1680,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") @@ -1694,7 +1701,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") From 9057cdaa89736aba21763d3fdf039efdb35acd85 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 15 Jun 2025 15:13:35 +0100 Subject: [PATCH 143/154] Relax compat limits on `Mmap.jl` --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 4aca5d3b..6754c56c 100644 --- a/Project.toml +++ b/Project.toml @@ -21,7 +21,7 @@ ZipArchives = "49080126-0e18-4c2a-b176-c102e4b3760c" [compat] Colors = "0.13.0" Distributions = "0.25.0" -Mmap = "1.11.0" +Mmap = "1" PrecompileTools = "1" Random = "1.10.0" Tables = "1" From 1d457253c9b1fabb422469886893ba99ce2c550b Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 20 Jun 2025 13:07:07 +0100 Subject: [PATCH 144/154] Work around XML.jl issue 43 (https://github.com/JuliaComputing/XML.jl/issues/43) and allow cells containing only spaces retain their value. --- src/sst.jl | 17 ++++++++++++++--- src/stream.jl | 9 ++------- src/types.jl | 2 +- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/sst.jl b/src/sst.jl index 95d0b8dc..03080f1b 100644 --- a/src/sst.jl +++ b/src/sst.jl @@ -68,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("", str_unformatted, "") + else + str_formatted = string("", str_unformatted, "") + end return add_shared_string!(wb, str_unformatted, str_formatted) end @@ -130,8 +134,15 @@ 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=join(v_string) + + # work around issue 43 in XML.jl (https://github.com/JuliaComputing/XML.jl/issues/43) + if gatheredstrings == "" # Happens if a cell consists only of spaces + return " " + end + + return gatheredstrings end # Looks for a string inside the Shared Strings Table (sst). diff --git a/src/stream.jl b/src/stream.jl index 36f33059..b1f21c0f 100644 --- a/src/stream.jl +++ b/src/stream.jl @@ -54,14 +54,9 @@ Base.show(io::IO, state::SheetRowStreamIteratorState) = print(io, "SheetRowStrea throw(XLSXError("Can't open internal file $filename for streaming because the XLSX file $(xf.source) was not found.")) end -# if xf.use_cache_for_sheet_data lznode=XML.LazyNode(XML.Raw(ZipArchives.zip_readentry(xf.io, filename))) -# else -# lznode=ZipArchives.zip_openentry(xf.io, filename) do io -# XML.LazyNode(XML.Raw(read(io))) -# end -# end - return lznode + + return lznode end # Creates a reader for row elements in the Worksheet's XML. diff --git a/src/types.jl b/src/types.jl index 5ad9dcae..a8b21318 100644 --- a/src/types.jl +++ b/src/types.jl @@ -409,7 +409,7 @@ mutable struct XLSXFile <: MSOfficePackage stream = nothing io = ZipArchives.ZipReader(read(source)) else - stream = open(source) + 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) From 1a81a4cb732f397e356b384e902893cb4b6bed34 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 21 Jun 2025 14:51:24 +0100 Subject: [PATCH 145/154] Improve work around issue 43 in XML.jl (https://github.com/JuliaComputing/XML.jl/issues/43) Can now write cells containing only whitespage 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". --- src/read.jl | 9 +-------- src/sst.jl | 17 +++++++++++------ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/read.jl b/src/read.jl index 0ed84d34..8e2f7863 100644 --- a/src/read.jl +++ b/src/read.jl @@ -561,14 +561,7 @@ function internal_xml_file_read(xf::XLSXFile, filename::String) :: XML.Node if !internal_xml_file_isread(xf, filename) try -# if xf.use_cache_for_sheet_data - xf.data[filename] = XML.Node(XML.Raw(ZipArchives.zip_readentry(xf.io, filename))) -# else - # read from zip entry -# ZipArchives.zip_openentry(xf.io, filename) do io -# xf.data[filename] = XML.Node(XML.Raw(read(io))) -# end -# end + xf.data[filename] = XML.Node(XML.Raw(ZipArchives.zip_readentry(xf.io, filename))) xf.files[filename] = true # set file as read catch err throw(XLSXError("Failed to parse internal XML file `$filename`")) diff --git a/src/sst.jl b/src/sst.jl index 03080f1b..ca993151 100644 --- a/src/sst.jl +++ b/src/sst.jl @@ -108,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 whitespage characters or with leading or training 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. @@ -118,8 +125,11 @@ function unformatted_text(el::XML.Node) :: String c = XML.children(e) if length(c) == 1 push!(v, XML.value(c[1])) + elseif length(c) == 0 + push!(v, isnothing(XML.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 @@ -137,11 +147,6 @@ function unformatted_text(el::XML.Node) :: String gatheredstrings=join(v_string) - # work around issue 43 in XML.jl (https://github.com/JuliaComputing/XML.jl/issues/43) - if gatheredstrings == "" # Happens if a cell consists only of spaces - return " " - end - return gatheredstrings end From 40964fa9ba0f0b90f27829b9f5832d479af49339 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sat, 21 Jun 2025 14:58:39 +0100 Subject: [PATCH 146/154] Fix failing line! --- src/sst.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sst.jl b/src/sst.jl index ca993151..046bed2e 100644 --- a/src/sst.jl +++ b/src/sst.jl @@ -110,7 +110,7 @@ end # work around issue 43 in XML.jl (https://github.com/JuliaComputing/XML.jl/issues/43) -# Can now write cells containing only whitespage characters or with leading or training whitespace. +# Can now write cells containing only whitespage 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". @@ -128,7 +128,7 @@ function unformatted_text(el::XML.Node) :: String elseif length(c) == 0 push!(v, isnothing(XML.value(e)) ? "" : XML.value(e)) else - println([i, " => ", XML.write(x) for (i, x) in enumerate(c)]) +# 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 From dfcaa88b256f258c23740c68ecd73d7e7f6cf8b4 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 22 Jun 2025 21:55:11 +0100 Subject: [PATCH 147/154] Properly unescape strings in `getdata` --- src/cell.jl | 6 +++--- src/sst.jl | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cell.jl b/src/cell.jl index 24a52981..c0d402ec 100644 --- a/src/cell.jl +++ b/src/cell.jl @@ -190,7 +190,7 @@ function getdata(ws::Worksheet, cell::Cell) :: CellValueType if isempty(cell.value) return missing else - return cell.value + return XML.unescape(cell.value) end end @@ -207,7 +207,7 @@ function getdata(ws::Worksheet, cell::Cell) :: CellValueType if isempty(str) return missing else - return str + return XML.unescape(str) end elseif (isempty(cell.datatype) || cell.datatype == "n") @@ -247,7 +247,7 @@ function getdata(ws::Worksheet, cell::Cell) :: CellValueType if isempty(cell.value) return missing else - return cell.value + return XML.unescape(cell.value) end end diff --git a/src/sst.jl b/src/sst.jl index 046bed2e..939c2c63 100644 --- a/src/sst.jl +++ b/src/sst.jl @@ -110,7 +110,7 @@ end # work around issue 43 in XML.jl (https://github.com/JuliaComputing/XML.jl/issues/43) -# Can now write cells containing only whitespage characters or with leading or trailing whitespace. +# 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". From 3d2de98f1582422acd87c55d33cfb864536d0785 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 23 Jun 2025 10:02:40 +0100 Subject: [PATCH 148/154] Add a `savexlsx` function with docs and tests. --- docs/src/api/files.md | 1 + src/cell.jl | 6 +++--- src/sst.jl | 10 +++++----- src/write.jl | 40 ++++++++++++++++++++++++++++++++++++---- test/runtests.jl | 21 ++++++++++++++++++++- 5 files changed, 65 insertions(+), 13 deletions(-) diff --git a/docs/src/api/files.md b/docs/src/api/files.md index a8d17ece..d6e61fc6 100644 --- a/docs/src/api/files.md +++ b/docs/src/api/files.md @@ -9,6 +9,7 @@ XLSX.openxlsx XLSX.opentemplate XLSX.newxlsx XLSX.writexlsx +XLSX.savexlsx ``` ## Worksheets diff --git a/src/cell.jl b/src/cell.jl index c0d402ec..24a52981 100644 --- a/src/cell.jl +++ b/src/cell.jl @@ -190,7 +190,7 @@ function getdata(ws::Worksheet, cell::Cell) :: CellValueType if isempty(cell.value) return missing else - return XML.unescape(cell.value) + return cell.value end end @@ -207,7 +207,7 @@ function getdata(ws::Worksheet, cell::Cell) :: CellValueType if isempty(str) return missing else - return XML.unescape(str) + return str end elseif (isempty(cell.datatype) || cell.datatype == "n") @@ -247,7 +247,7 @@ function getdata(ws::Worksheet, cell::Cell) :: CellValueType if isempty(cell.value) return missing else - return XML.unescape(cell.value) + return cell.value end end diff --git a/src/sst.jl b/src/sst.jl index 939c2c63..a5e62d6b 100644 --- a/src/sst.jl +++ b/src/sst.jl @@ -69,9 +69,9 @@ end function add_shared_string!(wb::Workbook, str_unformatted::AbstractString) :: Int if startswith(str_unformatted, " ") || endswith(str_unformatted, " ") - str_formatted = string("", str_unformatted, "") + str_formatted = string("", XML.escape(str_unformatted), "") else - str_formatted = string("", str_unformatted, "") + str_formatted = string("", XML.escape(str_unformatted), "") end return add_shared_string!(wb, str_unformatted, str_formatted) end @@ -124,9 +124,9 @@ 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.value(e)) + push!(v, isnothing(XML.value(e)) ? "" : XML.is_simple(e) ? XML.simple_value(e) : XML.value(e)) else # 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.")) @@ -145,7 +145,7 @@ function unformatted_text(el::XML.Node) :: String v_string = Vector{String}() gather_strings!(v_string, el) - gatheredstrings=join(v_string) + gatheredstrings=XML.unescape(join(v_string)) return gatheredstrings end diff --git a/src/write.jl b/src/write.jl index 13d9498d..984581e8 100644 --- a/src/write.jl +++ b/src/write.jl @@ -1,9 +1,31 @@ +""" + savexlsx(f::XLSXFile) + +Save an `XLSXFile` instance back to the file from which it was opened, overwriting original content. + +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. + +Returns the filepath of the written file if a filename is supplied, or `nothing` if writing to an `IO`. + +""" +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) @@ -43,7 +65,14 @@ function writexlsx(output_source::Union{AbstractString,IO}, xf::XLSXFile; overwr 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) @@ -452,7 +481,7 @@ function xlsx_encode(ws::Worksheet, val::AbstractString) return ("", "") end - sst_ind = add_shared_string!(get_workbook(ws), strip_illegal_chars(XML.escape(val))) + sst_ind = add_shared_string!(get_workbook(ws), strip_illegal_chars(val)) return ("s", string(sst_ind)) end @@ -1237,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 @@ -1265,7 +1296,6 @@ function writetable(filename::Union{AbstractString,IO}, data, columnnames; overw # write output file writexlsx(filename, xf, overwrite=overwrite) - nothing end """ @@ -1277,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 @@ -1314,7 +1346,7 @@ function writetable(filename::Union{AbstractString,IO}; overwrite::Bool=false, k # 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}} diff --git a/test/runtests.jl b/test/runtests.jl index 9adbdf9e..d6a1f185 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1380,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 @@ -1393,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 customXml 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"]] From 197acbef17f00b05f60034fba899ee6347e94a5c Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 24 Jun 2025 13:49:13 +0100 Subject: [PATCH 149/154] Address #243 --- src/read.jl | 11 ++++++++++- src/worksheet.jl | 4 ++-- src/write.jl | 2 +- test/runtests.jl | 20 ++++++++++---------- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/read.jl b/src/read.jl index 8e2f7863..f26203df 100644 --- a/src/read.jl +++ b/src/read.jl @@ -561,7 +561,15 @@ function internal_xml_file_read(xf::XLSXFile, filename::String) :: XML.Node if !internal_xml_file_isread(xf, filename) try - xf.data[filename] = XML.Node(XML.Raw(ZipArchives.zip_readentry(xf.io, filename))) + xf.data[filename] = XML.Node(XML.Raw(ZipArchives.zip_readentry(xf.io, filename))) + + # Issue 243 - Need to remove characters that precede the XML declaration. + fd=XML.write(xf.data[filename]) + dec=findfirst(" and '", "I❤Julia", "\"<'&O-O&'>\"", "<&>"] - esc_sheetname = XML.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, XML.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 @@ -5761,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 From ee159c7177868d99bcd664ac70631d815f0373f5 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 24 Jun 2025 15:13:46 +0100 Subject: [PATCH 150/154] Remove need to write to a string on reading (again) --- src/read.jl | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/read.jl b/src/read.jl index f26203df..67e00664 100644 --- a/src/read.jl +++ b/src/read.jl @@ -555,25 +555,30 @@ function internal_xml_file_add!(xl::XLSXFile, filename::String) 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 !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))) - - # Issue 243 - Need to remove characters that precede the XML declaration. - fd=XML.write(xf.data[filename]) - dec=findfirst(" Date: Fri, 27 Jun 2025 17:16:24 +0100 Subject: [PATCH 151/154] Generalise `readto` to support StructArray as sink (mirrors CSV.jl) --- src/read.jl | 34 +++++++++++++++++----------------- src/table.jl | 5 +---- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/read.jl b/src/read.jl index 67e00664..15744738 100644 --- a/src/read.jl +++ b/src/read.jl @@ -568,7 +568,8 @@ function strip_bom_and_lf!(bytes::Vector{UInt8}) end end function internal_xml_file_read(xf::XLSXFile, filename::String) :: XML.Node - !internal_xml_file_exists(xf, filename) && throw(XLSXError("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) @@ -743,14 +744,12 @@ function readtable(source::Union{AbstractString, IO}; first_row::Union{Nothing, 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 -# !enable_cache && GC.gc() 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 -# !enable_cache && GC.gc() return c end @@ -758,7 +757,6 @@ function readtable(source::Union{AbstractString, IO}, sheet::Union{AbstractStrin 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 -# !enable_cache && GC.gc() return c end @@ -772,7 +770,7 @@ function readtable(source::Union{AbstractString, IO}, sheet::Union{AbstractStrin end """ - readdf( + readto( source, [sheet, [columns]], @@ -785,42 +783,44 @@ end [stop_in_row_function], [keep_empty_rows], [normalizenames] - ) -> DataFrame + ) -> sink Read and parse an Excel worksheet, materializing directly using -the `sink` function (e.g. `DataFrame`). +the `sink` function (e.g. `DataFrame` or `StructArray`). Takes the same keyword arguments as [`XLSX.readtable`](@ref) # Example ```julia -julia> using DataFrames, XLSX +julia> using DataFrames, StructArrays, XLSX + +julia> df = XLSX.readto("myfile.xlsx", DataFrame) -julia> df = XLSX.readdf("myfile.xlsx", DataFrame) +julia> df = XLSX.readto("myfile.xlsx", StructArray) -julia> df = XLSX.readdf("myfile.xlsx", "mysheet", DataFrame) +julia> df = XLSX.readto("myfile.xlsx", "mysheet", DataFrame) -julia> df = XLSX.readdf("myfile.xlsx", "mysheet", "A:C", DataFrame) +julia> df = XLSX.readto("myfile.xlsx", "mysheet", "A:C", DataFrame) ``` See also: [`XLSX.gettable`](@ref). """ -function readdf(source::Union{AbstractString, IO}, sheet::Union{AbstractString, Int}, range::AbstractString, sink=nothing; kw...) +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 readtable(source, sheet, range; kw...) |> sink + return Tables.CopiedColumns(readtable(source, sheet, range; kw...)) |> sink end -function readdf(source::Union{AbstractString, IO}, sheet::Union{AbstractString, Int}, sink=nothing; kw...) +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 readtable(source, sheet; kw...) |> sink + return Tables.CopiedColumns(readtable(source, sheet; kw...)) |> sink end -function readdf(source::Union{AbstractString, IO}, sink=nothing; kw...) +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 readtable(source; kw...) |> sink + return Tables.CopiedColumns(readtable(source; kw...)) |> sink end diff --git a/src/table.jl b/src/table.jl index 84b56831..8c776ce7 100644 --- a/src/table.jl +++ b/src/table.jl @@ -419,9 +419,7 @@ function Base.iterate(itr::TableRowIterator, state::TableRowIteratorState) # 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 "Something wrong here!" -# if is_empty_table_row(sheet_row) && !itr.keep_empty_rows -# throw(XLSXError("Something wrong here!")) -# end + table_row = TableRow(table_row_index, itr.index, sheet_row) # user asked to stop (or end of row range) @@ -497,7 +495,6 @@ function check_table_data_dimension(data::Vector) nothing end -#function gettable(itr::TableRowIterator; infer_eltypes::Bool=true, normalizenames::Bool=false) :: DataTable function gettable(itr::TableRowIterator; infer_eltypes::Bool=true) :: DataTable column_labels = get_column_labels(itr) columns_count = table_columns_count(itr) From d8d50a83bb131938f27b0cd300f73de4589218ac Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 27 Jun 2025 17:22:06 +0100 Subject: [PATCH 152/154] `readdf` -> `readto` in tests --- test/runtests.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 4d353544..e55f03d8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1308,7 +1308,7 @@ end @testset "Read DataFrame" begin - df = XLSX.readdf(joinpath(data_directory, "general.xlsx"), "table4", "F:G", DataFrames.DataFrame) + 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" @@ -1316,7 +1316,7 @@ end @test ismissing(df[1, 2]) @test ismissing(df[2, 1]) - df = XLSX.readdf(joinpath(data_directory, "general.xlsx"), "table4", DataFrames.DataFrame) + 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" @@ -1324,7 +1324,7 @@ end @test ismissing(df[1, :H1]) @test ismissing(df[2, :H2]) - df = XLSX.readdf(joinpath(data_directory, "general.xlsx"), DataFrames.DataFrame) + 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" @@ -1332,9 +1332,9 @@ end @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.readdf(joinpath(data_directory, "general.xlsx")) # No sink - @test_throws XLSX.XLSXError df = XLSX.readdf(joinpath(data_directory, "general.xlsx"), 3) # No sink - @test_throws XLSX.XLSXError df = XLSX.readdf(joinpath(data_directory, "general.xlsx"), 3, "F:G") # No sink + @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 From c3c2113eeccdb181c5de5805cd8fb3e1edd15e5c Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 27 Jun 2025 17:41:25 +0100 Subject: [PATCH 153/154] Update API documentation to `readto` --- docs/src/api/data.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/api/data.md b/docs/src/api/data.md index 135d8d9a..c3639141 100644 --- a/docs/src/api/data.md +++ b/docs/src/api/data.md @@ -19,7 +19,7 @@ XLSX.getcell XLSX.getcellrange XLSX.gettable XLSX.readtable -XLSX.readdf +XLSX.readto XLSX.writetable XLSX.writetable! ``` From 49aac9a3e1a4419e951ebcc74ed398f588199f72 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Fri, 27 Jun 2025 17:49:56 +0100 Subject: [PATCH 154/154] And again in docs :-( --- docs/src/tutorial.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index c40e85f0..4aa0edda 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -102,7 +102,7 @@ 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 are also two helper methods [`XLSX.readtable`](@ref) and [`XLSX.readdf`](@ref) to read from file +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 @@ -117,7 +117,7 @@ julia> df = DataFrame(XLSX.readtable("myfile.xlsx", "mysheet")) # Returns a `Tab 2 │ 2 second 3 │ 3 third -julia> df = XLSX.readdf("myfile.xlsx", "mysheet", DataFrame) # Returns a `DataFrame` directly. +julia> df = XLSX.readto("myfile.xlsx", "mysheet", DataFrame) # Returns a `DataFrame` directly. 3×2 DataFrame Row │ HeaderA HeaderB │ Int64 String

J+=d_{duAaGSo1rNIHZ-pu1^_O_s<}e>V>~@ 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 OqM@RrT&8Fp`u_m)j|b`i literal 0 HcmV?d00001 diff --git a/docs/src/images/caseSensitiveComparison.png b/docs/src/images/caseSensitiveComparison.png new file mode 100644 index 0000000000000000000000000000000000000000..0bc3a267d2f7f671f1bcecb758e84d6a441999dc GIT binary patch literal 4988 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 literal 0 HcmV?d00001 diff --git a/docs/src/images/expression.png b/docs/src/images/expression.png new file mode 100644 index 0000000000000000000000000000000000000000..8d23fb6a3928b5f1fd6bc88b19be3db4ededc22c GIT binary patch literal 17766 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 V6wu5Bz8eK3E2$_^DrV&O{{Z}|5&8fC literal 0 HcmV?d00001 diff --git a/docs/src/images/relativeComparison.png b/docs/src/images/relativeComparison.png new file mode 100644 index 0000000000000000000000000000000000000000..f23ea8c872dfb2d210427b15abc6461ebad46b41 GIT binary patch literal 18203 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 KF1}^%|9=4NQa7;x literal 0 HcmV?d00001 diff --git a/docs/src/images/simpleComparison.png b/docs/src/images/simpleComparison.png new file mode 100644 index 0000000000000000000000000000000000000000..1c5ca1deda201f4cdd59e6cf239f94b094ea93b6 GIT binary patch literal 24114 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 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)) + 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 @@ -221,10 +236,12 @@ function styles_add_cell_attribute(wb::Workbook, new_att::XML.Node, att::String) 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.:(==)` + # if node["formatCode"] == new_att["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.:(==)` + # if node == new_att # XML.jl defines `Base.:(==)` return k - 1 # CellDataFormat is zero-indexed end end @@ -326,72 +343,69 @@ end function process_columnranges(f::Function, ws::Worksheet, colrng::ColumnRange; kw...)::Int bounds = column_bounds(colrng) dim = (get_dimension(ws)) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - left = bounds[begin] - right = bounds[end] - top = dim.start.row_number - bottom = dim.stop.row_number + 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 + 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 + 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)) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - top = bounds[begin] - bottom = bounds[end] - left = dim.start.column_number - right = dim.stop.column_number + 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 + 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 + 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 - if occursin("Uniform", string(nameof(f))) # Shouldn't happen! - throw(XLSXError("Cannot apply `setUnifoirmAttribute()` functions to a non-contiguous range.\nUse the equivalent `setAttribute()` function instead.")) - end + # if occursin("Uniform", string(nameof(f))) # Shouldn't happen! + # throw(XLSXError("Cannot apply `setUniformAttribute()` functions to a non-contiguous range.\nUse the equivalent `setAttribute()` function instead.")) + # end bounds = nc_bounds(ncrng) - dim = (get_dimension(ws)) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) + if length(ncrng) == 1 + single = true else - 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 - _ = f(ws, r; kw...) + 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 - return -1 - else - throw(XLSXError("Non-contiguous range $ncrng is out of bounds. Worksheet `$(ws.name)` only has dimension `$dim`.")) + _ = 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 @@ -400,6 +414,7 @@ function process_cellranges(f::Function, ws::Worksheet, rng::CellRange; kw...):: 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.")) @@ -436,8 +451,8 @@ function process_get_cellname(f::Function, ws::Worksheet, ref_or_rng::AbstractSt 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? - throw(XLSXError("Can only assign borders to cells but `$(ref_or_rng)` is a constant: $(ref_or_rng)=$v.")) + 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 @@ -456,9 +471,6 @@ end # function process_colon(f::Function, ws::Worksheet, row, col; kw...) dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - end if isnothing(row) && isnothing(col) return f(ws, dim; kw...) elseif isnothing(col) @@ -473,30 +485,27 @@ function process_colon(f::Function, ws::Worksheet, row, col; kw...) end function process_veccolon(f::Function, ws::Worksheet, row, col; kw...) dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) + if isnothing(col) + col = dim.start.column_number:dim.stop.column_number + elseif isnothing(row) + row = dim.start.row_number:dim.stop.row_number else - if isnothing(col) - col = dim.start.column_number:dim.stop.column_number - elseif isnothing(row) - row = dim.start.row_number:dim.stop.row_number - else - throw(XLSXError("Something wrong here!")) - end - 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...) + 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 @@ -507,6 +516,8 @@ function process_vecint(f::Function, ws::Worksheet, row, col; kw...) 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 @@ -548,6 +559,7 @@ function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange, a 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, cellref, atts, newid, first; kw...) end @@ -559,61 +571,33 @@ function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange, a end function process_uniform_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguousRange, atts::Vector{String}; kw...)::Int bounds = nc_bounds(ncrng) - dim = (get_dimension(ws)) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) + if length(ncrng) == 1 + single = true else - 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 - if r isa CellRef - newid, first = process_uniform_core(f, ws, r, atts, newid, first; kw...) - elseif r isa CellRange - for c in r - newid, first = process_uniform_core(f, ws, 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 + single = false end -end -function process_uniform_veccolon(f::Function, ws::Worksheet, row, col, atts::Vector{String}; kw...) - dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - if isnothing(col) - col = dim.start.column_number:dim.stop.column_number - elseif isnothing(row) - row = dim.start.row_number:dim.stop.row_number - else - throw(XLSXError("Something wrong here!")) - 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 a in row - for b in col - cellref = CellRef(a, b) - if getcell(ws, cellref) isa EmptyCell - continue + 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 + if r isa CellRef + newid, first = process_uniform_core(f, ws, r, atts, newid, first; kw...) + elseif r isa CellRange + for c in r + newid, first = process_uniform_core(f, ws, c, atts, newid, first; kw...) end - newid, first = process_uniform_core(f, ws, cellref, atts, newid, first; kw...) + else + throw(XLSXError("Something wrong here!")) end end if first @@ -621,12 +605,44 @@ function process_uniform_veccolon(f::Function, ws::Worksheet, row, col, atts::Ve 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) + if isnothing(col) + col = dim.start.column_number:dim.stop.column_number + elseif isnothing(row) + 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, 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...) 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 @@ -659,44 +675,46 @@ function process_uniform_core(ws::Worksheet, cellref::CellRef, newid::Union{Int, end function process_uniform_ncranges(ws::Worksheet, ncrng::NonContiguousRange)::Int bounds = nc_bounds(ncrng) - dim = (get_dimension(ws)) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) + if length(ncrng) == 1 + single = true else - 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 - if r isa CellRef - newid, first = process_uniform_core(ws, r, newid, first) - elseif r isa CellRange - for c in r - newid, first = process_uniform_core(ws, c, newid, first) - end - else - throw(XLSXError("Something wrong here!")) - end + 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 + 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 first - newid = -1 + if r isa CellRef + newid, first = process_uniform_core(ws, r, newid, first) + elseif r isa CellRange + for c in r + newid, first = process_uniform_core(ws, c, newid, first) + end + else + throw(XLSXError("Something wrong here!")) end - return newid end - else - throw(XLSXError("Non-contiguous range $ncrng is out of bounds. Worksheet `$(ws.name)` only has dimension `$dim`.")) + 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) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - end if isnothing(row) && isnothing(col) return setUniformStyle(ws, dim) elseif isnothing(col) @@ -711,39 +729,38 @@ function process_colon(ws::Worksheet, row, col) end function process_uniform_veccolon(ws::Worksheet, row, col) dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) + if isnothing(col) + col = dim.start.column_number:dim.stop.column_number + elseif isnothing(row) + row = dim.start.row_number:dim.stop.row_number else - if isnothing(col) - col = dim.start.column_number:dim.stop.column_number - elseif isnothing(row) - row = dim.start.row_number:dim.stop.row_number - else - throw(XLSXError("Something wrong here!")) - end - 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) + 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 - if first - newid = -1 - end - return newid 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 @@ -787,6 +804,7 @@ function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange; k newid = nothing first = true alignment_node = nothing + isInDim(ws, get_dimension(ws), rng) for cellref in rng if getcell(ws, cellref) isa EmptyCell continue @@ -801,38 +819,43 @@ function process_uniform_attribute(f::Function, ws::Worksheet, rng::CellRange; k end function process_uniform_ncranges(f::Function, ws::Worksheet, ncrng::NonContiguousRange; kw...)::Int bounds = nc_bounds(ncrng) - dim = (get_dimension(ws)) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) + if length(ncrng) == 1 + single = true else - 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 - if r isa CellRef - newid, first, alignment_node = process_uniform_core(f, ws, r, newid, first, alignment_node; kw...) - elseif r isa CellRange - for c in r - newid, first, alignment_node = process_uniform_core(f, ws, c, newid, first, alignment_node; kw...) - end - else - throw(XLSXError("Something wrong here!")) - end + 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 + 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 first - newid = -1 + if r isa CellRef + newid, first, alignment_node = process_uniform_core(f, ws, r, newid, first, alignment_node; kw...) + elseif r isa CellRange + for c in r + newid, first, alignment_node = process_uniform_core(f, ws, c, newid, first, alignment_node; kw...) + end + else + throw(XLSXError("Something wrong here!")) end - return newid end - else - throw(XLSXError("Non-contiguous range $ncrng is out of bounds. Worksheet `$(ws.name)` only has dimension `$dim`.")) + 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...) @@ -847,6 +870,7 @@ function process_uniform_veccolon(f::Function, ws::Worksheet, row, col; kw...) 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 @@ -869,9 +893,14 @@ function process_uniform_veccolon(f::Function, ws::Worksheet, row, col; kw...) 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 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 diff --git a/src/cellformats.jl b/src/cellformats.jl index a85d02f8..6fa4ed73 100644 --- a/src/cellformats.jl +++ b/src/cellformats.jl @@ -339,7 +339,7 @@ function getFont(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellFont} 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] + 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 @@ -444,7 +444,7 @@ function getBorder(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellBorder 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] + 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 @@ -682,6 +682,13 @@ function setBorder(sh::Worksheet, cellref::CellRef; throw(XLSXError("Cannot set borders because cache is not enabled.")) end + 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) kwdict["left"] = isnothing(left) ? nothing : Dict{String,String}(p for p in left) @@ -690,13 +697,6 @@ 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) - 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 - wb = get_workbook(sh) cell = getcell(sh, cellref) @@ -903,7 +903,7 @@ function setOutsideBorder(ws::Worksheet, rng::CellRange; throw(XLSXError("Cannot set borders because cache is not enabled.")) end -# length(rng) <= 1 && throw(XLSXError("Cannot set outside border for a single cell.")) + # 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) @@ -1021,7 +1021,7 @@ function getFill(wb::Workbook, cell_style::XML.Node)::Union{Nothing,CellFill} 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] + 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 @@ -1178,6 +1178,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" @@ -1463,7 +1466,7 @@ function setAlignment(sh::Worksheet, cellref::CellRef; cell = getcell(sh, cellref) if cell isa EmptyCell - throw(XLSXError("Cannot set fill for an `EmptyCell`: $(cellref.name). Set the value first.")) + throw(XLSXError("Cannot set alignment for an `EmptyCell`: $(cellref.name). Set the value first.")) end if cell.style == "" @@ -1650,11 +1653,11 @@ 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] + 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] + 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 @@ -1918,6 +1921,7 @@ function setUniformStyle(ws::Worksheet, rng::CellRange)::Union{Nothing,Int} let newid::Union{Nothing,Int}, newid = nothing + first = true for cellref in rng @@ -2019,7 +2023,7 @@ function setColumnWidth(ws::Worksheet, rng::CellRange; width::Union{Nothing,Real return 0 end - sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet"*string(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. @@ -2113,7 +2117,7 @@ function getColumnWidth(ws::Worksheet, cellref::CellRef)::Union{Nothing,Real} throw(XLSXError("Cell specified is outside sheet dimension `$d`")) end - sheetdoc = xmlroot(ws.package, "xl/worksheets/sheet"*string(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. @@ -2333,7 +2337,7 @@ function getMergedCells(ws::Worksheet)::Union{Vector{CellRange},Nothing} 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 + 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. @@ -2541,7 +2545,7 @@ mergeCells(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{I 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.")) + # !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.")) @@ -2555,7 +2559,7 @@ function mergeCells(ws::Worksheet, cr::CellRange) 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 + 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. @@ -2564,7 +2568,7 @@ function mergeCells(ws::Worksheet, cr::CellRange) i != k && throw(XLSXError("Some problem here!")) if l != len push!(sheetdoc[k], sheetdoc[k][end]) - if l+1 < len + if l + 1 < len for pos = len-1:-1:l+1 sheetdoc[k][pos+1] = sheetdoc[k][pos] end @@ -2573,36 +2577,36 @@ function mergeCells(ws::Worksheet, cr::CellRange) else push!(sheetdoc[k], XML.Element("mergeCells")) end - j = l+1 - count=0 + j = l + 1 + count = 0 else # There are already some existing merged cells - c=XML.children(sheetdoc[i][j]) + 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 -# for cell in cr -# if cell in CellRange(child["ref"]) - if intersects(cr, CellRange(child["ref"])) - throw(XLSXError("Merged range (`$cr`) cannot overlap with existing merged range (`"*child["ref"]*"`).")) - end -# end + # for cell in cr + # if cell in CellRange(child["ref"]) + if intersects(cr, CellRange(child["ref"])) + throw(XLSXError("Merged range (`$cr`) cannot overlap with existing merged range (`" * child["ref"] * "`).")) + end + # end end end push!(sheetdoc[i][j], XML.Element("mergeCell", ref=string(cr))) # Add the new merged cell range. - count +=1 - sheetdoc[i][j]["count"]=count + count += 1 + sheetdoc[i][j]["count"] = count # All cells except the base cell are set to missing. - let first=true + let first = true for cell in cr if first - first=false + first = false continue else - ws[cell]="" + ws[cell] = "" end end end diff --git a/src/cellref.jl b/src/cellref.jl index a24c21bc..de632249 100644 --- a/src/cellref.jl +++ b/src/cellref.jl @@ -508,16 +508,25 @@ function nc_bounds(r::NonContiguousRange)::CellRange # Smallest rectangualar `Ce end return CellRange(CellRef(top, left), CellRef(bottom, right)) end -function Base.length(r::NonContiguousRange)::Int # Number of cells in `r`. - s = 0 +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 - s += 1 + push!(allcells, rng.name) else - s += length(rng) + for cell in rng + push!(allcells, cell.name) + end end end - return s +# 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]+$" diff --git a/src/conditional-formats.jl b/src/conditional-formats.jl index 301b67ba..4c12892f 100644 --- a/src/conditional-formats.jl +++ b/src/conditional-formats.jl @@ -147,6 +147,32 @@ const timeperiods::Dict{String,String} = Dict( "nextMonth" => "AND(MONTH(__CR__)=MONTH(EDATE(TODAY(),0+1)),YEAR(__CR__)=YEAR(EDATE(TODAY(),0+1)))" ) +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 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])) @@ -953,6 +979,45 @@ julia> XLSX.setConditionalFormat(s, "A1:A7", :duplicateValues; ``` ![image|320x500](./images/errorBlank.png) +# 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") + +``` + !!! note "Overlaying conditional formats" It is possible to overlay multiple conditional formats to the same range or to @@ -985,12 +1050,14 @@ function setConditionalFormat(f, r, type::Symbol; kw...) setCfContainsText(f, r; operator=String(type), kw...) elseif type ∈ [:containsBlanks, :notContainsBlanks, :containsErrors, :notContainsErrors, :duplicateValues, :uniqueValues] setCfContainsBlankErrorUniqDup(f, r; operator=String(type), kw...) + elseif type == :expression + setCfFormula(f, r; kw...) # elseif type == :iconSet # throw(XLSXError("Icon sets are not yet implemented.")) # elseif type == :dataBar # throw(XLSXError("Data bars are not yet implemented.")) else - throw(XLSXError("Invalid conditional format type: $type. Valid options are: `:colorScale`, `:cellIs`")) + throw(XLSXError("Invalid conditional format type: $type.")) end end @@ -1009,12 +1076,14 @@ function setConditionalFormat(f, r, c, type::Symbol; kw...) setCfContainsText(f, r, c; operator=String(type), kw...) elseif type ∈ [:containsBlanks, :notContainsBlanks, :containsErrors, :notContainsErrors, :duplicateValues, :uniqueValues] setCfContainsBlankErrorUniqDup(f, r, c; operator=String(type), kw...) + elseif type == :expression + setCfFormula(f, r, c; kw...) # elseif type == :iconSet # throw(XLSXError("Icon sets are not yet implemented.")) # elseif type == :dataBar # throw(XLSXError("Data bars are not yet implemented.")) else - throw(XLSXError("Invalid conditional format type: $type. Valid options are: `:colorScale`, `:cellIs`.")) + throw(XLSXError("Invalid conditional format type: $type.")) end end setCfColorScale(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon; kw...) = process_colon(setCfColorScale, ws, row, nothing; kw...) @@ -1079,7 +1148,7 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; else if !haskey(colorscales, colorscale) - throw(XLSXError("Invalid color scale: $colorScale. Valid options are: $(keys(colorscales)).")) + throw(XLSXError("Invalid color scale: $colorscale. Valid options are: $(keys(colorscales)).")) end cfx=colorscales[colorscale] cfx["priority"] = new_pr @@ -1494,4 +1563,51 @@ function setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::CellRange; update_worksheet_cfx!(allcfs, cfx, ws, rng) return 0 -end \ No newline at end of file +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; + formula::Union{Nothing,String}, + 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 +)::Int + + !issubset(rng, get_dimension(ws)) && throw(XLSXError("Range `$rng` goes outside worksheet dimension.")) + + allcfs = allCfs(ws) # get all conditional format blocks + old_cf = getConditionalFormats(allcfs) # 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 diff --git a/src/styles.jl b/src/styles.jl index 1e59c61e..51b372a7 100644 --- a/src/styles.jl +++ b/src/styles.jl @@ -48,7 +48,7 @@ function get_num_style_index(ws::Worksheet, numformatid::Integer) 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 @@ -76,17 +76,17 @@ function styles_xmlroot(workbook::Workbook) 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 # 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 @@ -96,23 +96,23 @@ 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") + # nchildren = length(XML.children(stylesheet)) + numfmts = XML.Element("numFmts", count="1") XML.pushfirst!(stylesheet, numfmts) -# push!(stylesheet, stylesheet[end]) -# for i in nchildren-1:-1:1 -# stylesheet[i+1]=stylesheet[i] -# end -# stylesheet[1]=numfmts + # push!(stylesheet, stylesheet[end]) + # for i in nchildren-1:-1:1 + # stylesheet[i+1]=stylesheet[i] + # end + # stylesheet[1]=numfmts else numfmts = numfmts[1] end @@ -120,23 +120,23 @@ 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 = XML.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) + 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 @@ -149,10 +149,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 @@ -163,7 +163,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 @@ -183,7 +183,7 @@ 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) @@ -233,9 +233,9 @@ 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) + 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) return EmptyCellDataFormat() @@ -244,7 +244,7 @@ function styles_get_cellXf_with_numFmtId(wb::Workbook, numFmtId::Int) :: Abstrac el = XML.attributes(elements_found[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 @@ -255,14 +255,14 @@ 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])) @@ -272,8 +272,8 @@ function styles_add_cell_xf(wb::Workbook, new_xf::XML.Node) :: CellDataFormat # 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 # XML.jl defines `Base.:(==)` return CellDataFormat(k - 1) # CellDataFormat is zero-indexed end end diff --git a/src/workbook.jl b/src/workbook.jl index 851395a1..0d3f058d 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. @@ -16,7 +16,7 @@ is_writable(xl::XLSXFile) = xl.is_writable 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) """ @@ -25,7 +25,7 @@ sheetnames(wb::Workbook) = [ s.name for s in wb.sheets ] 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 == unquoteit(sheetname) return true @@ -45,10 +45,10 @@ Count the number of sheets in the Workbook. @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 == unquoteit(sheetname) return ws @@ -57,9 +57,9 @@ function getsheet(wb::Workbook, sheetname::String) :: Worksheet 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 @@ -132,10 +132,10 @@ end function getdata(xl::XLSXFile, s::AbstractString) if is_workbook_defined_name(xl, s) v = get_defined_name_value(xl.workbook, s) - if is_defined_name_value_a_constant(v) + if is_defined_name_value_a_constant(v) return v elseif is_defined_name_value_a_reference(v) - return getdata(xl, v) + return getdata(xl, v) else throw(XLSXError("Unexpected Workbook defined name value: $v.")) end @@ -222,15 +222,18 @@ function getcellrange(xl::XLSXFile, rng_str::AbstractString) 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 is_workbook_defined_name(wb::Workbook, name::AbstractString)::Bool = haskey(wb.workbook_names, name) +#@inline is_worksheet_defined_name(wb::Workbook, sheetId::Int, name::AbstractString)::Bool = haskey(wb.worksheet_names, (sheetId, name)) - @inline get_defined_name_value(wb::Workbook, name::AbstractString) :: DefinedNameValueTypes = wb.workbook_names[name].value +@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 dn = wb.worksheet_names[(sheetId, name)] @@ -240,10 +243,16 @@ end @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 +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 @@ -329,8 +338,8 @@ 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=true) -addDefinedName(ws::Worksheet, name::AbstractString, value::Union{Int, Float64}; absolute=true) = addDefName(ws, name, value; absolute=true) +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.")) diff --git a/src/worksheet.jl b/src/worksheet.jl index a8e06ccf..902f04c9 100644 --- a/src/worksheet.jl +++ b/src/worksheet.jl @@ -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 @@ -59,18 +57,24 @@ end # Returns the dimension of this worksheet as a CellRange. # If the dimension is unknown, computes a dimension from cells in cache. -# If cache is not being used (or is empty), return `nothing`. +# 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 - (isnothing(ws.cache) || length(ws.cache.cells) < 1) && return nothing - # @warn "Dimension for worksheet $(ws.name) not found. Calculating from cells in cache." - 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))) + # 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 @@ -125,45 +129,25 @@ getdata(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, col::Union{Inte getdata(ws::Worksheet, ::Colon, ::Colon) = getdata(ws) function getdata(ws::Worksheet, ::Colon) dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - getdata(ws, dim) - end + getdata(ws, dim) end function getdata(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon) dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("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) - if dim === nothing - throw(XLSXError("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) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - col=dim.start.column_number:dim.stop.column_number - end - return getdata(ws, row, col) + 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) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - row=dim.start.row_number:dim.stop.row_number - end - return getdata(ws, row, col) + row = dim.start.row_number:dim.stop.row_number + return getdata(ws, row, col) end function getdata(ws::Worksheet, rng::CellRange)::Array{Any,2} @@ -197,23 +181,15 @@ end function getdata(ws::Worksheet, rng::ColumnRange)::Array{Any,2} dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - start = CellRef(dim.start.row_number, rng.start) - stop = CellRef(dim.stop.row_number, rng.stop) - return getdata(ws, CellRange(start, stop)) - end + 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) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - start = CellRef(rng.start, dim.start.column_number,) - stop = CellRef(rng.stop, dim.stop.column_number) - return getdata(ws, CellRange(start, stop)) - end + start = CellRef(rng.start, dim.start.column_number,) + stop = CellRef(rng.stop, dim.stop.column_number) + return getdata(ws, CellRange(start, stop)) end function getdata(ws::Worksheet, rng::NonContiguousRange)::Vector{Any} @@ -283,25 +259,16 @@ function getdata(ws::Worksheet, ref::AbstractString) end end -function getdata(ws::Worksheet) - if ws.dimension !== nothing - return getdata(ws, get_dimension(ws)) - else - throw(XLSXError("Worksheet dimension is unknown.")) - end -end +getdata(ws::Worksheet) = getdata(ws, get_dimension(ws)) -#Base.getindex(f::Function, ws::Worksheet, r) = f(ws, r) -#Base.getindex(f::Function, ws::Worksheet, r, c) = f(ws, r, c) -#Base.getindex(f::Function, ws::Worksheet, ::Colon) = f::Function, (ws) Base.getindex(ws::Worksheet, r) = getdata(ws, r) Base.getindex(ws::Worksheet, r, c) = getdata(ws, r, c) 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 @@ -369,25 +336,17 @@ 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{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) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - getcellrange(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number))) - end + 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) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - getcellrange(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col)))) - end + getcellrange(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col)))) end function getcell(ws::Worksheet, ref::AbstractString) @@ -467,23 +426,15 @@ getcellrange(ws::Worksheet, ::Colon, col::Union{Integer,UnitRange{<:Integer}}) = function getcellrange(ws::Worksheet, rng::ColumnRange)::Array{AbstractCell,2} dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - start = CellRef(dim.start.row_number, rng.start) - stop = CellRef(dim.stop.row_number, rng.stop) - return getcellrange(ws, CellRange(start, stop)) - end + 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) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - start = CellRef(rng.start, dim.start.column_number,) - stop = CellRef(rng.stop, dim.stop.column_number) - return getcellrange(ws, CellRange(start, stop)) - end + 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} diff --git a/src/write.jl b/src/write.jl index 9ba1397a..fc21c45f 100644 --- a/src/write.jl +++ b/src/write.jl @@ -73,7 +73,7 @@ function writexlsx(output_source::Union{AbstractString,IO}, xf::XLSXFile; overwr update_worksheets_xml!(xf) update_workbook_xml!(xf) -# update_relationships(xf) + # update_relationships(xf) ZipArchives.ZipWriter(output_source) do xlsx # write XML files @@ -284,9 +284,11 @@ 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) + #if !isnothing(get_dimension(sheet)) + # spans_str = string(column_number(get_dimension(sheet).start), ":", column_number(get_dimension(sheet).stop)) + spans_str = string(column_number(d.start), ":", column_number(d.stop)) + #end # iterates over WorksheetCache cells and write the XML for r in eachrow(sheet) @@ -337,14 +339,16 @@ function update_worksheets_xml!(xl::XLSXFile) doc[i][j] = sheetData_node # updates worksheet dimension - if get_dimension(sheet) !== nothing - i, j = get_idces(doc, "worksheet", "dimension") - if !isnothing(j) - dimension_node = doc[i][j] - dimension_node["ref"] = string(get_dimension(sheet)) - doc[i][j] = dimension_node - end + + # if get_dimension(sheet) !== nothing + 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 + # end set_worksheet_xml_document!(sheet, doc) end @@ -438,10 +442,10 @@ 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 + # 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) @@ -652,23 +656,15 @@ function setdata!(ws::Worksheet, rng::CellRange, value) end function setdata!(ws::Worksheet, rng::RowRange, value) dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - start = CellRef(rng.start, dim.start.column_number,) - stop = CellRef(rng.stop, dim.stop.column_number) - setdata!(ws, CellRange(start, stop), value) - end + 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) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - start = CellRef(dim.start.row_number, rng.start) - stop = CellRef(dim.stop.row_number, rng.stop) - setdata!(ws, CellRange(start, stop), value) - end + 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 @@ -684,49 +680,29 @@ end setdata!(ws::Worksheet, ::Colon, ::Colon, v) = setdata!(ws::Worksheet, :, v) function setdata!(ws::Worksheet, ::Colon, v) dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - setdata!(ws, dim, v) - end + setdata!(ws, dim, v) end function setdata!(ws::Worksheet, row::Union{Integer,UnitRange{<:Integer}}, ::Colon, v) dim = get_dimension(ws) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - setdata!(ws, CellRange(CellRef(first(row), dim.start.column_number), CellRef(last(row), dim.stop.column_number)), v) - end + 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) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - setdata!(ws, CellRange(CellRef(dim.start.row_number, first(col)), CellRef(dim.stop.row_number, last(col))), v) - end + 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) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - for a in row - for b in dim.start.column_number:dim.stop.column_number - setdata!(ws, CellRef(a, b), v) - end + 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) - if dim === nothing - throw(XLSXError("No worksheet dimension found")) - else - for b in col - for a in dim.start.row_number:dim.stop.row_number - setdata!(ws, CellRef(a, b), v) - end + for b in col + for a in dim.start.row_number:dim.stop.row_number + setdata!(ws, CellRef(a, b), v) end end end diff --git a/test/runtests.jl b/test/runtests.jl index c5f8f1ca..4b9efce9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -211,9 +211,9 @@ end @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 + @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" @@ -385,7 +385,7 @@ end @test XLSX.getcell(sheet1, "B2") == XLSX.Cell(XLSX.CellRef("B2"), "s", "", "0", "") XLSX.getcellrange(sheet1, "B2:C3") XLSX.getcellrange(f, "Sheet1!B2:C3") - XLSX.getcellrange(sheet1, 2, 2) + XLSX.getcellrange(sheet1, 2, 2) XLSX.getcellrange(sheet1, 2, :) XLSX.getcellrange(sheet1, :, 3) XLSX.getcellrange(sheet1, 3, :) @@ -412,17 +412,17 @@ end end @testset "setindex" begin - f=XLSX.newxlsx() - s=f[1] + 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[[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] + @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 @@ -433,10 +433,10 @@ end 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 + 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" @@ -450,10 +450,10 @@ end 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 + 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" @@ -466,12 +466,16 @@ end @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] + + f = XLSX.newxlsx() + s = f[1] for i in 1:5 for j in 1:5 - s[i,j] = i+j + 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] @@ -479,18 +483,18 @@ end @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 + 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] + @test s[[2, 4], [3, 5]] == [0 0; 0 0] end @testset "getcell" begin - f=XLSX.newxlsx() - s=f[1] + f = XLSX.newxlsx() + s = f[1] for i in 1:3 for j in 1:3 - s[i,j] = i+j + s[i, j] = i + j end end @test XLSX.getcell(s, "A1") == XLSX.Cell(XLSX.CellRef("A1"), "", "", "2", "") @@ -498,27 +502,31 @@ end @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 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") @@ -615,14 +623,14 @@ end @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")) + @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")) + @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) @@ -637,25 +645,34 @@ end @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] + 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, "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;] + @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;] + @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 @@ -795,6 +812,7 @@ end @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")) @@ -809,6 +827,7 @@ end @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")) @@ -1270,8 +1289,8 @@ end @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 df[3, 2] == Dates.Date(1983, 04, 16) + @test df[5, 2] == Dates.DateTime(2018, 04, 16, 19, 19, 51) end @testset "normalizenames" begin # Issue #260 @@ -1393,11 +1412,11 @@ end XLSX.writexlsx(new_filename, f, 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) + sc = XLSX.sheetcount(f) XLSX.deletesheet!(f, "empty") - @test XLSX.sheetcount(f) == sc-1 # Check it's gone. + @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. @@ -1426,7 +1445,7 @@ end 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] + s = f[1] @test_throws XLSX.XLSXError XLSX.deletesheet!(s) @test_throws XLSX.XLSXError XLSX.deletesheet!(f, "Sheet1") @@ -1851,8 +1870,8 @@ end @testset "setFont" begin - f=XLSX.newxlsx() - s=f[1] + f = XLSX.newxlsx() + s = f[1] s["A1:B2,D1:E2"] = "" XLSX.setFont(s, "A1:A2"; bold=true, italic=true, size=24, name="Arial") @@ -1865,8 +1884,8 @@ end @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] + 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") @@ -1884,15 +1903,15 @@ end 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")) + @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")) - + @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"] @@ -1994,8 +2013,8 @@ end isfile("output.xlsx") && rm("output.xlsx") - f=XLSX.newxlsx() - sheet=f[1] + 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") @@ -2003,9 +2022,15 @@ end @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] + 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")) @@ -2020,18 +2045,18 @@ end 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] + 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") + 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") + 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")) @@ -2039,7 +2064,14 @@ end @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 @@ -2062,7 +2094,7 @@ end @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: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"]) + 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")) @@ -2109,11 +2141,11 @@ end @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"]) + 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"]) + 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"]) + 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) @@ -2121,16 +2153,18 @@ end f = XLSX.newxlsx() s = f[1] s[1:6, 1:6] = "" - XLSX.setBorder(s, 1, :; left = ["color" => "FFFF0001", "style" => "thick"]) + 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"]) + 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"]) + 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"]) + 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"]) + 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"] @@ -2159,7 +2193,7 @@ end diagonal=["style" => "none"] ) - @test XLSX.setUniformBorder(s, "Mock-up!A1:B4,Mock-up!D4:E6"; left=["style" => "dotted", "color" => "darkseagreen3"], + @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"], @@ -2180,19 +2214,51 @@ end 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="chocolate4") == -1 - @test 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"] - ) == -1 + 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" => "chocolate4"], + diagonal=["style" => "none"] + ) == -1 @test XLSX.setUniformFill(s, "B2:D4"; pattern="gray125", bgColor="FF000000") == -1 - @test XLSX.setFont(s, "A1:F20"; size=18, name="Arial") == -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, "A1:F20"; horizontal="right", wrapText=true) == -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"]) @@ -2214,8 +2280,8 @@ end diagonal=["style" => "none"] ) - f=XLSX.newxlsx() - s=f[1] + f = XLSX.newxlsx() + s = f[1] s[1:6, 1:6] = "" XLSX.setUniformBorder(s, "Sheet1!A:B"; left=["style" => "dotted", "color" => "darkseagreen3"], @@ -2322,41 +2388,41 @@ end ) @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] + f = XLSX.newxlsx() + s = f[1] s[1:6, 1:6] = "" - XLSX.setOutsideBorder(s, "Sheet1!A1:A2"; outside = ["style" => "dotted", "color" => "FF003FF0"]) + 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"]) + 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"]) + 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"]) + 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"]) + 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] + f = XLSX.newxlsx() + s = f[1] s[1:6, 1:6] = "" - XLSX.setOutsideBorder(s, 1, :; outside = ["style" => "dotted", "color" => "FF002FF0"]) + 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"]) + 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"]) + 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"]) + 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"]) + 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) @@ -2377,6 +2443,10 @@ 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") # Default colors unchanged @@ -2434,8 +2504,8 @@ end isfile("output.xlsx") && rm("output.xlsx") - f=XLSX.newxlsx() - s=f[1] + 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")) @@ -2463,8 +2533,8 @@ end 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] + 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")) @@ -2510,8 +2580,8 @@ end @testset "setAlignment" begin - f=XLSX.newxlsx() - s=f[1] + 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")) @@ -2526,8 +2596,8 @@ end @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] + 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")) @@ -2568,8 +2638,8 @@ end isfile("output.xlsx") && rm("output.xlsx") - f=XLSX.newxlsx() - s=f[1] + 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")) @@ -2583,10 +2653,10 @@ end 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] + f = XLSX.newxlsx() + s = f[1] s["A1:Z26"] = "" - XLSX.setUniformAlignment(s, 2,:; horizontal="right", vertical="justify", wrapText=true) + 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")) @@ -2604,8 +2674,8 @@ end 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] + 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") @@ -2613,9 +2683,20 @@ end @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] + 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) @@ -2628,8 +2709,8 @@ end @test XLSX.getAlignment(s, 1, 22) === nothing @test XLSX.getAlignment(s, 1, 24) === nothing - f=XLSX.newxlsx() - s=f[1] + 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, :) @@ -2641,6 +2722,20 @@ end @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 @@ -2697,6 +2792,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) @@ -2711,8 +2810,8 @@ end isfile("test.xlsx") && rm("test.xlsx") - f=XLSX.newxlsx() - s=f[1] + 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)")) @@ -2725,8 +2824,8 @@ end 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] + 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)")) @@ -2738,11 +2837,11 @@ end @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, 23:2:27, [15,16]; format="#,##0.0") + 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] + 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)")) @@ -2752,9 +2851,12 @@ end @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] + 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)")) @@ -2766,11 +2868,11 @@ end @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, 23:2:27, [15,16]; format="#,##0.0") + 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] + 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)")) @@ -2782,91 +2884,99 @@ end end @testset "UniformStyle" begin - f=XLSX.newxlsx() - s=f[1] + 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 + 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 + 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 + 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 + 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] + 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 + 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] + 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 + 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 @@ -2924,8 +3034,8 @@ end @test XLSX.getRowHeight(f, "Mock-up!J20") ≈ 50.2109375 @test XLSX.getColumnWidth(f, "Mock-up!J20") ≈ 60.7109375 - f=XLSX.newxlsx() - s=f[1] + f = XLSX.newxlsx() + s = f[1] s["A1:Z26"] = "" XLSX.setColumnWidth(s, "Sheet1!A1"; width=60) @test XLSX.getColumnWidth(s, "A1") ≈ 60.7109375 @@ -2964,8 +3074,8 @@ end XLSX.setColumnWidth(s, 11, 7:13; width=11.1) @test XLSX.getColumnWidth(s, "K15") ≈ 11.8109375 - f=XLSX.newxlsx() - s=f[1] + f = XLSX.newxlsx() + s = f[1] s["A1:Z26"] = "" XLSX.setRowHeight(s, "Sheet1!A1"; height=10.1) @test XLSX.getRowHeight(s, "A1") ≈ 10.3109375 @@ -3035,8 +3145,8 @@ end end @testset "indexing setAttribute" begin - f=XLSX.newxlsx() # Empty XLSXFile - s=f[1] #1×1 XLSX.Worksheet: ["Sheet1"](A1:A1) + 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") @@ -3050,29 +3160,31 @@ end @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, 1) - @test_throws XLSX.XLSXError XLSX.getFont(s, 3, 2) - @test_throws XLSX.XLSXError XLSX.getFont(s, 2, 3) + @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] = "" + 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")) + @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, 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] + 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"]) @@ -3083,22 +3195,22 @@ end @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 XLSX.setBorder(s, [2,3], 1:3; allsides=["color" => "grey42", "style" => "thick"]) == -1 + @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] = "" + 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"]) + 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] + 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") @@ -3109,22 +3221,22 @@ end @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 XLSX.setFill(s, [2,3], 1:3; pattern="lightVertical", fgColor="Red", bgColor="blue") == -1 + @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] = "" + 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") + 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] + 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) @@ -3135,22 +3247,22 @@ end @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 XLSX.setAlignment(s, [2,3], 1:3; horizontal="right", vertical="justify", wrapText=true) == -1 + @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] = "" + 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) + 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] + 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") @@ -3161,49 +3273,49 @@ end @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 XLSX.setFormat(s, [2,3], 1:3; format="Percentage") == -1 + @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] = "" + 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") + 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] + f = XLSX.newxlsx() + s = f[1] @test_throws XLSX.XLSXError XLSX.getColumnWidth(s, "B2") # Cell outside sheet dimension - s[1:3, 1:3]="" + 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) + 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] + f = XLSX.newxlsx() + s = f[1] @test_throws XLSX.XLSXError XLSX.getRowHeight(s, "B2") # Cell outside sheet dimension - s[1:3, 1:3]="" + 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) + 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]="" + 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")) @@ -3223,42 +3335,42 @@ end 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"] - ) + 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] + 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) + @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 "colorScale" begin - f=XLSX.newxlsx() - s=f[1] + f = XLSX.newxlsx() + s = f[1] for i in 1:5, j in 1:5 - s[i,j] = i+j + s[i, j] = i + j end @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, "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", @@ -3267,71 +3379,71 @@ end mid_col="red", max_type="max", max_col="blue" - )==0 + ) == 0 @test XLSX.setConditionalFormat(s, "Sheet1!A4:E4", :colorScale; min_type="min", min_col="tomato", max_type="max", max_col="gold4" - )==0 + ) == 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 + ) == 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) + 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] + f = XLSX.newxlsx() + s = f[1] for i in 1:5, j in 1:5 - s[i,j] = i+j + 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, "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", @@ -3340,13 +3452,13 @@ end 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)] + ) == 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] + f = XLSX.newxlsx() + s = f[1] for i in 1:5, j in 1:5 - s[i,j] = i+j + s[i, j] = i + j end @test XLSX.setConditionalFormat(s, :, 1:4, :colorScale; @@ -3359,10 +3471,10 @@ end max_col="blue" ) == 0 - f=XLSX.newxlsx() - s=f[1] + f = XLSX.newxlsx() + s = f[1] for i in 1:5, j in 1:5 - s[i,j] = i+j + s[i, j] = i + j end XLSX.addDefinedName(s, "myRange", "A1:B5") @test XLSX.setConditionalFormat(s, "myRange", :colorScale; @@ -3386,151 +3498,151 @@ end ) end - + @testset "cellIs" begin - f=XLSX.newxlsx() - s=f[1] + f = XLSX.newxlsx() + s = f[1] for i in 1:5, j in 1:5 - s[i,j] = i+j + 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 XLSX.setConditionalFormat(s, "1:1", :cellIs)==0 - @test XLSX.setConditionalFormat(s, 2, :, :cellIs; dxStyle = "greenfilltext")==0 + @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 + 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 + 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 + 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) + 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) ] - f=XLSX.newxlsx() - s=f[1] + f = XLSX.newxlsx() + s = f[1] for i in 1:5, j in 1:5 - s[i,j] = i+j + 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, :, 2, :cellIs; dxStyle="redborder") + XLSX.setConditionalFormat(s, "Sheet1!E:E", :cellIs; dxStyle="redfilltext") XLSX.setConditionalFormat(s, 1:5, 3:4, :cellIs; operator="beween", 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] + 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 + 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] + 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 + 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 + 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"] + fill=["pattern" => "none", "bgColor" => "yellow"], + format=["format" => "0.0"], + font=["color" => "green"], + border=["style" => "hair", "color" => "cyan"] ) end - + @testset "containsText" begin - f=XLSX.newxlsx() - s=f[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" @@ -3540,99 +3652,99 @@ end @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, "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 + 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 + 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 + 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] + 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, :, 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] + 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" @@ -3643,14 +3755,14 @@ end 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] + 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" @@ -3660,369 +3772,370 @@ end @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 + 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"] + 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 + 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: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 + 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 + 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 + 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 + 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) + 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 + 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, :, 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 + 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 + 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 + 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"] + 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)] + 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: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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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) + 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 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) + 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 + 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", :aboveAverage) - XLSX.setConditionalFormat(s, :, 2, :aboveAverage; dxStyle = "redborder") - XLSX.setConditionalFormat(s, "Sheet1!E:E", :aboveAverage; dxStyle = "redfilltext") + XLSX.setConditionalFormat(s, :, 2, :aboveAverage; dxStyle="redborder") + XLSX.setConditionalFormat(s, "Sheet1!E:E", :aboveAverage; dxStyle="redfilltext") 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 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 + 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 + 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 + 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"] + 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() + 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) @@ -4037,109 +4150,109 @@ end @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 XLSX.setConditionalFormat(s, "2:2", :timePeriod) == 0 + @test XLSX.setConditionalFormat(s, 2, :, :timePeriod; dxStyle="greenfilltext") == 0 @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 + 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 + 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 + 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 + 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="nextMonth", stopIfTrue="true", - fill = ["pattern" => "none", "bgColor"=>"FFFFCFCE"], - border = ["style"=>"thick", "color"=>"coral"], - font = ["color"=>"yellow", "bold"=>"true", "strike"=>"true"] - )==0 + 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 + 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("A2:J2") => (type = "timePeriod", priority = 1), - XLSX.CellRange("A2:J2") => (type = "timePeriod", priority = 2) + 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("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 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)) == 25 @test XLSX.getConditionalFormats(s) == [ - XLSX.CellRange("A1:J10") => (type = "timePeriod", priority = 24), - XLSX.CellRange("A1:J10") => (type = "timePeriod", priority = 25), - XLSX.CellRange("A1:J3") => (type = "timePeriod", priority = 20), - XLSX.CellRange("A2:J4") => (type = "timePeriod", priority = 14), - XLSX.CellRange("A2:J4") => (type = "timePeriod", priority = 21), - XLSX.CellRange("A1:J2") => (type = "timePeriod", priority = 13), - XLSX.CellRange("A1:J2") => (type = "timePeriod", priority = 17), - XLSX.CellRange("A1:A2") => (type = "timePeriod", priority = 12), - XLSX.CellRange("A1:C3") => (type = "timePeriod", priority = 10), - XLSX.CellRange("A1:A1") => (type = "timePeriod", priority = 9), - XLSX.CellRange("A1:A1") => (type = "timePeriod", priority = 11), - 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 = 15), - XLSX.CellRange("A1:C10") => (type = "timePeriod", priority = 16), - XLSX.CellRange("A1:C10") => (type = "timePeriod", priority = 18), - XLSX.CellRange("A1:C10") => (type = "timePeriod", priority = 19), - XLSX.CellRange("A1:C10") => (type = "timePeriod", priority = 22), - XLSX.CellRange("A1:C10") => (type = "timePeriod", priority = 23), - XLSX.CellRange("A2:J2") => (type = "timePeriod", priority = 1), - XLSX.CellRange("A2:J2") => (type = "timePeriod", priority = 2) + XLSX.CellRange("A1:J10") => (type="timePeriod", priority=24), + XLSX.CellRange("A1:J10") => (type="timePeriod", priority=25), + XLSX.CellRange("A1:J3") => (type="timePeriod", priority=20), + XLSX.CellRange("A2:J4") => (type="timePeriod", priority=14), + XLSX.CellRange("A2:J4") => (type="timePeriod", priority=21), + XLSX.CellRange("A1:J2") => (type="timePeriod", priority=13), + XLSX.CellRange("A1:J2") => (type="timePeriod", priority=17), + XLSX.CellRange("A1:A2") => (type="timePeriod", priority=12), + XLSX.CellRange("A1:C3") => (type="timePeriod", priority=10), + XLSX.CellRange("A1:A1") => (type="timePeriod", priority=9), + XLSX.CellRange("A1:A1") => (type="timePeriod", priority=11), + 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=15), + XLSX.CellRange("A1:C10") => (type="timePeriod", priority=16), + XLSX.CellRange("A1:C10") => (type="timePeriod", priority=18), + XLSX.CellRange("A1:C10") => (type="timePeriod", priority=19), + XLSX.CellRange("A1:C10") => (type="timePeriod", priority=22), + XLSX.CellRange("A1:C10") => (type="timePeriod", priority=23), + XLSX.CellRange("A2:J2") => (type="timePeriod", priority=1), + XLSX.CellRange("A2:J2") => (type="timePeriod", priority=2) ] - f=XLSX.newxlsx() - s=f[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) @@ -4151,20 +4264,20 @@ end 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, :, 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] + 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) @@ -4178,14 +4291,14 @@ end @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] + 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) @@ -4199,182 +4312,341 @@ end 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 + 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"] + 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 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 + 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, "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 + 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 + 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 + 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 + 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 + 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 + 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) + 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 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) + 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 + 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, :, 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 + 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 + 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 + 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"] + fill=["pattern" => "none", "bgColor" => "yellow"], + format=["format" => "0.0"], + font=["color" => "green"], + border=["style" => "hair", "color" => "cyan"] ) end @@ -4417,7 +4689,7 @@ end @test !XLSX.isMergedCell(s, "C5"; mergedCells=XLSX.getMergedCells(f["Document History"])) end - f=XLSX.opentemplate(joinpath(data_directory, "testmerge.xlsx")) + 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"]) @@ -4462,47 +4734,47 @@ end @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 + end isfile("outfile.xlsx") && rm("outfile.xlsx") - f=XLSX.newxlsx() - s=f[1] + f = XLSX.newxlsx() + s = f[1] for i in 1:3, j in 1:3 - s[i,j] = i+j + 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] + f = XLSX.newxlsx() + s = f[1] for i in 1:4, j in 1:4 - s[i,j] = i+j + 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] + + f = XLSX.newxlsx() + s = f[1] for i in 1:3, j in 1:3 - s[i,j] = i+j + 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] + f = XLSX.newxlsx() + s = f[1] for i in 1:3, j in 1:3 - s[i,j] = i+j + 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] + f = XLSX.newxlsx() + s = f[1] for i in 1:3, j in 1:3 - s[i,j] = i+j + s[i, j] = i + j end XLSX.mergeCells(s, :) @test XLSX.getMergedBaseCell(f, "Sheet1!B2") == (baseCell=XLSX.CellRef("A1"), baseValue=2) @@ -4593,7 +4865,7 @@ end @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. + @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) From 5ad0a6ab341dcfde5ed7c4134014e39f7e8393ba Mon Sep 17 00:00:00 2001 From: TimG1964 Date: Wed, 21 May 2025 22:41:44 +0100 Subject: [PATCH 117/154] Add `iconSets` (no tests yet) --- Project.toml | 2 + docs/src/formatting.md | 217 ++++- docs/src/images/allIcons.png | Bin 0 -> 14659 bytes docs/src/images/basicIconSet.png | Bin 0 -> 5421 bytes docs/src/images/colorScaleOptions.png | Bin 0 -> 10605 bytes docs/src/images/dataBarOptions.png | Bin 0 -> 15045 bytes docs/src/images/dataBars.png | Bin 0 -> 23073 bytes docs/src/images/iconKey.png | Bin 0 -> 23678 bytes docs/src/images/iconSetOptions.png | Bin 0 -> 19584 bytes docs/src/images/iconSets.png | Bin 0 -> 50539 bytes docs/src/images/newValIconSet.png | Bin 0 -> 5254 bytes docs/src/images/showValIcons.png | Bin 0 -> 15807 bytes src/XLSX.jl | 4 +- src/conditional-format-helpers.jl | 321 +++++++ src/conditional-formats.jl | 1107 +++++++++++++++---------- 15 files changed, 1185 insertions(+), 466 deletions(-) create mode 100644 docs/src/images/allIcons.png create mode 100644 docs/src/images/basicIconSet.png create mode 100644 docs/src/images/colorScaleOptions.png create mode 100644 docs/src/images/dataBarOptions.png create mode 100644 docs/src/images/dataBars.png create mode 100644 docs/src/images/iconKey.png create mode 100644 docs/src/images/iconSetOptions.png create mode 100644 docs/src/images/iconSets.png create mode 100644 docs/src/images/newValIconSet.png create mode 100644 docs/src/images/showValIcons.png create mode 100644 src/conditional-format-helpers.jl diff --git a/Project.toml b/Project.toml index 62a5d4e7..71c43605 100644 --- a/Project.toml +++ b/Project.toml @@ -11,6 +11,7 @@ Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" 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" @@ -20,6 +21,7 @@ Colors = "0.13.0" Distributions = "0.25.0" Random = "1.10.0" Tables = "1" +UUIDs = "1.11.0" XML = "0.3.5" ZipArchives = "2" julia = "1.8" diff --git a/docs/src/formatting.md b/docs/src/formatting.md index 3d633b67..49c7a145 100644 --- a/docs/src/formatting.md +++ b/docs/src/formatting.md @@ -378,13 +378,13 @@ 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.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, :formatting_type; kwargs...)` +`XLSX.setConditionalFormat(sheet, CellRange, :type; kwargs...)` -Excel uses a range of `:formatting_type` values to describe these conditional formats and the same values +Excel uses a range of `:type` values to describe these conditional formats and the same values are used here, as follows: - `:cellIs` - `:top10` @@ -405,8 +405,8 @@ are used here, as follows: - `:colorScale` - `:iconSet` -Use of these different `:formatting_type`s is illustrated in the following sections. -For more details on the range of `:formatting_types` and their associated keyword +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 @@ -418,8 +418,7 @@ used. All the functions of `Highlight Cells Rules` and `Top/Bottom Rules` are pr ![image|320x500](./images/cell1.png) ![image|100x500](./images/blank.png) ![image|320x500](./images/cell2.png) -Excel uses a range of `:formatting_type` values describe these conditional formats and the same values -are used here, as follows: +The following `:type` values are used to set conditional formats by making direct comparisons to a cell's value: - `:cellIs` - `:top10` - `:aboveAverage` @@ -435,9 +434,9 @@ are used here, as follows: - `:uniqueValues` - `:duplicateValues` -Each of these formatting types needs a set of keyword options to fully define its operation. For example, -the `:cellIs` type needs an `operator` keyword, set to define the test to make to determine whether or not -to apply the formatting. Valid `operator` values are: +Each of these formatting types needs a set of keyword options to fully define its operation. +For example, the `:cellIs` type needs an `operator` keyword, set to define the test to make +to determine whether or not to apply the formatting. Valid `operator` values are: - `greaterThan` (cell > `value`) - `greaterEqual` (cell >= `value`) @@ -578,7 +577,7 @@ Refer to [XLSX.setConditionalFormat()](@ref) for full details. #### Expressions -It is possible to use an Excel formula directly to determine whether to apply a conditional formula. +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. ![image|320x500](./images/expression.png) @@ -662,7 +661,7 @@ julia> s=f[1] 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") +julia> XLSX.setConditionalFormat(s, "A1:E5", :expression; formula = "E5 < 50", dxStyle = "redfilltext") 0 ``` ![image|320x500](./images/relativeComparison.png) @@ -709,6 +708,10 @@ julia> XLSX.setConditionalFormat(s, "C1:C3", :expression; formula = "exact(\"Hel (In development) +![image|320x500](./images/dataBars.png) + +![image|320x500](./images/dataBarOptions.png) + #### Color Scale It is possible to apply a `:colorScale` formatting type to a range of cells. @@ -744,6 +747,9 @@ A custom color scale may be defined by the colors at each end of the scale and ( 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: +![image|320x500](./images/colorScaleOptions.png) + 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. @@ -767,7 +773,171 @@ julia> XLSX.setConditionalFormat(f["Sheet1"], "A13:F22", :colorScale; #### Icon Set -(In development) +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. + +![image|320x500](./images/iconSets.png) + +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> s[1:10, 1]=collect(1:10) +10-element Vector{Int64}: + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + +julia> XLSX.setConditionalFormat(s, "A1:A10", :iconSet) +0 +``` +![image|320x500](./images/basicIconSet.png) + +All of the options to control an iconSet in Excel are available. The iconSet options +(for a 4-icon set) look like this: +![image|320x500](./images/iconSetOptions.png) + +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 stoping +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 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 (but are not checked). + +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 +``` +![image|320x500](./images/newValIconSet.png) + +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 +``` + +![image|320x500](./images/showValIcons.png) + +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 definingwhich 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 and mid2_type, then a 4-icon will be used. If both +sets are defined, a 5-icon set is used and if neither, a 3-icon set. + +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) +``` +![image|320x500](./images/iconKey.png) + +Specifying too few icons throws an error, any extra will simply be ignored. #### Specifying cell references in Conditional Formats @@ -806,7 +976,6 @@ XLSXFile("C:\...\blank.xlsx") containing 1 Worksheet ------------------------------------------------- Sheet1 1x1 A1:A1 - julia> s=f[1] 1×1 XLSX.Worksheet: ["Sheet1"](A1:A1) @@ -857,11 +1026,11 @@ julia> XLSX.setConditionalFormat(s, "B2:B6", :cellIs; operator="greaterThan", va #### 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`. +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] @@ -903,7 +1072,8 @@ julia> XLSX.getConditionalFormats(s) 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. +achieved with the `stopIfTrue` keyword. It is not possible to apply `stopIfTrue` to dataBars, colorScales or +iconSets. For example: @@ -1024,7 +1194,8 @@ It is not allowed to create new merged cells that overlap at all with any existi !!! warning - It is possible to write into a merged cell using `XLSX.jl`. This is illustrated below: + 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 diff --git a/docs/src/images/allIcons.png b/docs/src/images/allIcons.png new file mode 100644 index 0000000000000000000000000000000000000000..0aadd892940d1f1b12c990b7c702d32d1381d875 GIT binary patch literal 14659 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 literal 0 HcmV?d00001 diff --git a/docs/src/images/colorScaleOptions.png b/docs/src/images/colorScaleOptions.png new file mode 100644 index 0000000000000000000000000000000000000000..70db1a1b22d18792b8fcece9343cb2bc62e33b5d GIT binary patch literal 10605 zcmb7qcQjmY)b1dOsG}1^8Dd0q(R(k^dx#Mw5u%G;6NDHwB8V0xN|ZqmM2RjzL>MEZ zcZOk<(aSx4zi+MYes_J}z3culYtH+wv(7pDec!#GXFq$tG1S+jAY~>6fj|`6T5851 z5WyL6-GB4O)yyvwR^X4o$5>MZRP~E>9T;45Qr1%jfohV-&g`xOV-ioThdv+>W!Ke@ zpwFY!5d^wRsI8`K8UVvid$_W!=0I`V-7m4@LYu`J^s@aOS#L=Tv?S~gA7wO$)_ni_ zt>{c;UHlq@)R;SU>c*ydfh|*Y$~AZ|sbjWn23?i!4JR=Ue)Epu6b>ueS}uPF<__ud?_03p+h1nn!Lc3dC!%;hO#aw#&h1` z{E}(RWyL#1N-t!Fw`73NW#6~l)+y@?qZQ}gNrid{yZ$N{1cE3{!4W)G)bMz3$XAwh zr0mta#{2#YME4?#Vryyn8uGv9aUjG9=a^G|i&H zx#F-aqsv!GO)cl6ey2rhY({CZz0l*Z{bKaMZIB>|PnrCnq?hRig{}O>yGUo0*GVBk zVNBThzGctGyk4uOn|{@}LYpUL`hjA?b{s9{*}kvoY|rQ^VzKbDP~nTa1F1tvFz#tt z)Z4OEd3wN@#I~|FCx@dYnU%bYJ31CgT`lAL&MUq}iS`qf!?kf({8O<0W!Lq0vmefb zppXK5p?;?%*mmQM3u$Wl!Qyc{lIDTBNAs;y5t*Vpt&v1hae;L8j?#f7jr*Mvu9FRn zC5ZIY=bv^ve#|84qqzsIvfK0p{MNmrqm{TJbla@LzYT zMDp~qh!=|^sUQ%{3tyVBZ=T))FJYSUh_>31XHW#!(UDC{=sE9=_-1oAx#&fU2aU)3 z(so*Tv9ZtGDmdoLRVO?)Y`V*|;hdq8BnS(XiPXh_(I5*W){L@gQplqaK?E|1#mZB} zQ#u#2*G~puj)X*-ZMV}5*5-($9lFFkxz7||P*$)KgaZtU zIX>A9^!X;S4_`T0crmP9ibRrK1noB`zTF7h57`G}oZ+M2*M#t+4E3nhj>dIgScc?~ zrkWaz6Yv&w{vyBo(Gv|m0VgYF*oc?0vHjAMY;7~;jO>8Nj0H|~K_QS`&?K7zH8pi5 z`@eqlSh`5juj2&%{OXpA*NGP4>_Zas?LjV<{5%jyfrS5xYp3YE&()&!6J_Au#Ky`+ zko^A-KK_Uj;^Naz9!x6bH@AAC&V9lXVk1qe{gx^%`qIMX6PBKKEuDKG8n^Jy%t&`% z_3cTGj1bf{hQ3A8_n0}WFGZt)gnC2<>DY-f*c3B4X(R2*vbIBnD};hL zeiOBe+=)4Ub?MeiT=G(iUN=^@`hB9(^V-<}MPG z(XO%0rot0)=r5sy>O3<+z64p z>0PTzP7!^MijoCKX6ez|NyL|vO~B&vPCs&5pu9Gc=y{fDX@`{0{t(1B(~evG4kCQnC0SFP7Qi- zJlLL|KPc9BwTx4Wi6FT%w#=#OeP&Qw&ovtS!FijV?!AX(iy zH&JhJa)GyRrsC^BFNM_I>pq-CR7xRJ#f({`w>IulvG-LEhy4|pth26qO!GHZK@F?P zJg$1O$hfR1 zHrjzva2{#WWiJB6l=>zyCt4<%FjIcmr+