From 727c729bdf44199ef556c919e8aaf07eed550b83 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 3 Mar 2025 15:47:09 +0000 Subject: [PATCH 01/24] 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 02/24] 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 03/24] 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 04/24] 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 05/24] 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 06/24] 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 07/24] 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 08/24] 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 09/24] 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 10/24] 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 11/24] 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 12/24] 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 13/24] 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 14/24] 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 15/24] 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 16/24] 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 17/24] 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 18/24] 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 19/24] 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 20/24] 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 21/24] 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 22/24] 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 23/24] 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 24/24] 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