Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

## [v0.12.0](https://github.com/JuliaData/XLSX.jl/tree/v0.12.0) - 2026-07-06
## [v0.12.0](https://github.com/JuliaData/XLSX.jl/tree/v0.12.0) - 2026-07-12
- Adopt XML.jl v0.4
- add a package extension to support [FileIO.jl](https://github.com/JuliaIO/FileIO.jl)
- Fix issues #425, #426, #427 and #428
- update copyright notice end-date
- modularize tests
- Julia floor raised to 1.10 (LTS)
Expand Down
38 changes: 33 additions & 5 deletions src/conditional-format-helpers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,10 @@ function allCfs(ws::Worksheet)::Vector{XML.Node}
xf = get_xlsxfile(ws)
target_file = get_relationship_target_by_id("xl", wb, ws.relationship_id)
v = xf.data[target_file]
# Parse ephemerally when the raw string is still needed intact — promoting
# it to a parsed XML.Node via xmlroot/get_xml_data here would break
# eachrow's first_cache_fill!, which requires the raw string the first
# time this sheet is read (issue #425).
sheetdoc = v isa String ? parse(v, XML.Node) : xmlroot(wb, ws.relationship_id)
return find_all_nodes("/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":worksheet/" * SPREADSHEET_NAMESPACE_XPATH_ARG * ":conditionalFormatting", sheetdoc)
end
function add_cf_to_XML(ws, new_cf) # Add a new conditional formatting to the worksheet XML.
function add_cf_to_XML(ws, new_cf)
wb = get_workbook(ws)
sheetdoc = xmlroot(get_workbook(ws), ws.relationship_id)
l = insert_index(sheetdoc[end], "conditionalFormatting", WORKSHEET_ORDER)
Expand All @@ -61,6 +57,20 @@ function add_cf_to_XML(ws, new_cf) # Add a new conditional formatting to the wor
push!(sheetdoc[end], new_cf)
end
end
function next_cf_priority!(ws::Worksheet)::Int
if ws.next_cf_priority === nothing
# One-time O(n) scan over whatever rules already exist (e.g. in a
# file opened with existing conditional formatting). Every
# subsequent call is O(1).
allcfs = allCfs(ws)
allextcfs = allExtCfs(ws)
old_cf = append!(getConditionalFormats(ws, allcfs), getConditionalExtFormats(ws, allextcfs))
ws.next_cf_priority = isempty(old_cf) ? 1 : maximum(last(x).priority for x in old_cf) + 1
end
pr = ws.next_cf_priority
ws.next_cf_priority += 1
return pr
end
function update_worksheet_cfx!(allcfs, cfx, ws, rng)
pfx = get_prefix(ws)
pfx = pfx == "" ? pfx : pfx * ":"
Expand Down Expand Up @@ -106,6 +116,24 @@ function allExtCfs(ws::Worksheet)::Vector{XML.Node}
return isnothing(cfs) ? Vector{XML.Node}() : xml_elements(cfs)
end
end

#=
function _extcfs_from_doc(sheetdoc::XML.Node)::Vector{XML.Node}
i, j = get_idces(sheetdoc, "worksheet", "extLst")
isnothing(j) && return Vector{XML.Node}()
extlst = sheetdoc[i][j]
cfs = nothing
for ext in XML.children(extlst)
for c in XML.children(ext)
if localname(c) == "conditionalFormattings"
cfs = c
break
end
end
end
return isnothing(cfs) ? Vector{XML.Node}() : xml_elements(cfs)
end
=#
function make_extLst!(s)
ext_list = XML.Element("extLst")
ext_element = XML.Element("ext")
Expand Down
75 changes: 24 additions & 51 deletions src/conditional-formats.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1445,8 +1445,7 @@ function setCfCellIs(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}=())
pfx = get_prefix(ws)
pfx = pfx == "" ? pfx : pfx * ":"

allcfs = allCfs(ws) # get all conditional format blocks
old_cf = getConditionalFormats(ws) # extract conditional format info
allcfs = allCfs(ws)

!isnothing(value) && !is_valid_cellname(value) && !is_valid_fixed_cellname(value) && isnothing(tryparse(Float64, value)) && throw(XLSXError("Invalid `value`: $value. Must be a number or a CellRef."))
!isnothing(value2) && !is_valid_cellname(value2) && !is_valid_fixed_cellname(value2) && isnothing(tryparse(Float64, value2)) && throw(XLSXError("Invalid `value2`: $value2. Must be a number or a CellRef."))
Expand All @@ -1460,7 +1459,7 @@ function setCfCellIs(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}=())
value = all(ismissing.(ws[rng])) ? nothing : string(sum(skipmissing(ws[rng])) / count(!ismissing, ws[rng]))
end
cfx = XML.Element("$(pfx)cfRule"; type="cellIs", dxfId=string(dxid.id))
cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : 1
cfx["priority"] = next_cf_priority!(ws)
if !isnothing(stopIfTrue) && stopIfTrue == "true"
cfx["stopIfTrue"] = "1"
end
Expand Down Expand Up @@ -1527,8 +1526,7 @@ function setCfContainsText(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,An
pfx = get_prefix(ws)
pfx = pfx == "" ? pfx : pfx * ":"

allcfs = allCfs(ws) # get all conditional format blocks
old_cf = getConditionalFormats(ws) # extract conditional format info
allcfs = allCfs(ws)

isnothing(value) && throw(XLSXError("Invalid `value`: $value. Must contain text or a CellRef."))

Expand All @@ -1555,7 +1553,7 @@ function setCfContainsText(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,An
formula = replace(formula, "__txt__" => value, "__CR__" => string(first(rng)))

cfx = XML.Element("$(pfx)cfRule"; type=type, dxfId=string(dxid.id))
cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : 1
cfx["priority"] = next_cf_priority!(ws)
if !isnothing(stopIfTrue) && stopIfTrue == "true"
cfx["stopIfTrue"] = "1"
end
Expand Down Expand Up @@ -1619,8 +1617,7 @@ function setCfTop10(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}=()):
pfx = get_prefix(ws)
pfx = pfx == "" ? pfx : pfx * ":"

allcfs = allCfs(ws) # get all conditional format blocks
old_cf = getConditionalFormats(ws) # extract conditional format info
allcfs = allCfs(ws)

!isnothing(value) && !is_valid_cellname(value) && !is_valid_fixed_cellname(value) && isnothing(tryparse(Float64, value)) && throw(XLSXError("Invalid `value`: $value. Must be a number or a CellRef."))

Expand All @@ -1644,7 +1641,7 @@ function setCfTop10(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}=()):
throw(XLSXError("Invalid operator: $operator. Valid options are: `topN`, `topN%`, `bottomN`, `bottomN%`."))
end

cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : 1
cfx["priority"] = next_cf_priority!(ws)
if !isnothing(stopIfTrue) && stopIfTrue == "true"
cfx["stopIfTrue"] = "1"
end
Expand Down Expand Up @@ -1709,8 +1706,7 @@ function setCfAboveAverage(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,An
pfx = get_prefix(ws)
pfx = pfx == "" ? pfx : pfx * ":"

allcfs = allCfs(ws) # get all conditional format blocks
old_cf = getConditionalFormats(ws) # extract conditional format info
allcfs = allCfs(ws)

wb = get_workbook(ws)
dx = get_dx(dxStyle, format, font, border, fill)
Expand Down Expand Up @@ -1741,7 +1737,7 @@ function setCfAboveAverage(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,An
throw(XLSXError("Invalid operator: $operator. Valid options are: `aboveAverage`, `aboveEqAverage`, `plus1sStdDev`, `plus2StdDev`, `plus3StdDev`, `belowAverage`, `belowEqAverage`, `minus1StdDev`, `minus2StdDev`, `minus3StdDev`."))
end

cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : 1
cfx["priority"] = next_cf_priority!(ws)
if !isnothing(stopIfTrue) && stopIfTrue == "true"
cfx["stopIfTrue"] = "1"
end
Expand Down Expand Up @@ -1799,8 +1795,7 @@ function setCfTimePeriod(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}
pfx = get_prefix(ws)
pfx = pfx == "" ? pfx : pfx * ":"

allcfs = allCfs(ws) # get all conditional format blocks
old_cf = getConditionalFormats(ws) # extract conditional format info
allcfs = allCfs(ws)

if haskey(timeperiods, operator)
formula = timeperiods[operator]
Expand All @@ -1816,7 +1811,7 @@ function setCfTimePeriod(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}
dxid = Add_Cf_Dx(wb, new_dx)

cfx = XML.Element("$(pfx)cfRule"; type="timePeriod", dxfId=string(dxid.id))
cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : 1
cfx["priority"] = next_cf_priority!(ws)
if !isnothing(stopIfTrue) && stopIfTrue == "true"
cfx["stopIfTrue"] = "1"
end
Expand Down Expand Up @@ -1877,8 +1872,8 @@ function setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::CellRange; allkws::D
pfx = get_prefix(ws)
pfx = pfx == "" ? pfx : pfx * ":"

allcfs = allCfs(ws) # get all conditional format blocks
old_cf = getConditionalFormats(ws) # extract conditional format info
allcfs = allCfs(ws)

if operator == "containsBlanks"
formula = "LEN(TRIM(__CR__))=0"
elseif operator == "notContainsBlanks"
Expand All @@ -1900,7 +1895,7 @@ function setCfContainsBlankErrorUniqDup(ws::Worksheet, rng::CellRange; allkws::D
dxid = Add_Cf_Dx(wb, new_dx)

cfx = XML.Element("$(pfx)cfRule"; type=operator, dxfId=string(dxid.id))
cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : 1
cfx["priority"] = next_cf_priority!(ws)
if !isnothing(stopIfTrue) && stopIfTrue == "true"
cfx["stopIfTrue"] = "1"
end
Expand Down Expand Up @@ -1960,16 +1955,15 @@ function setCfFormula(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}=()
pfx = get_prefix(ws)
pfx = pfx == "" ? pfx : pfx * ":"

allcfs = allCfs(ws) # get all conditional format blocks
old_cf = getConditionalFormats(ws) # extract conditional format info
allcfs = allCfs(ws)

wb = get_workbook(ws)
dx = get_dx(dxStyle, format, font, border, fill)
new_dx = get_new_dx(wb, dx)
dxid = Add_Cf_Dx(wb, new_dx)

cfx = XML.Element("$(pfx)cfRule"; type="expression", dxfId=string(dxid.id))
cfx["priority"] = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : 1
cfx["priority"] = next_cf_priority!(ws)
if !isnothing(stopIfTrue) && stopIfTrue == "true"
cfx["stopIfTrue"] = "1"
end
Expand Down Expand Up @@ -2038,12 +2032,11 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}
pfx = get_prefix(ws)
pfx = pfx == "" ? pfx : pfx * ":"

allcfs = allCfs(ws) # get all conditional format blocks
old_cf = getConditionalFormats(ws) # extract conditional format info
allcfs = allCfs(ws)

let new_pr, new_cf

new_pr = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : "1"
new_pr = string(next_cf_priority!(ws))

if isnothing(colorscale)

Expand Down Expand Up @@ -2091,17 +2084,6 @@ function setCfColorScale(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}
push!(csc, XML.Element("$(pfx)color"; rgb=get_color(min_col)))
!isnothing(mid_type) && push!(csc, XML.Element("$(pfx)color"; rgb=get_color(mid_col)))
push!(csc, XML.Element("$(pfx)color"; rgb=get_color(max_col)))

# cfx = XML.h.cfRule(type="colorScale", priority=new_pr,
# XML.h.colorScale(
# isnothing(min_val) ? XML.h.cfvo(type=min_type) : XML.h.cfvo(type=min_type, val=min_val),
# isnothing(mid_type) ? "" : XML.h.cfvo(type=mid_type, val=mid_val),
# isnothing(max_val) ? XML.h.cfvo(type=max_type) : XML.h.cfvo(type=max_type, val=max_val),
# XML.h.color(rgb=get_color(min_col)),
# isnothing(mid_type) ? "" : XML.h.color(rgb=get_color(mid_col)),
# XML.h.color(rgb=get_color(max_col))
# )
# )
push!(cfx, csc)
else
if !haskey(colorscales, colorscale)
Expand Down Expand Up @@ -2194,13 +2176,12 @@ function setCfIconSet(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}=()
pfx = get_prefix(ws)
pfx = pfx == "" ? pfx : pfx * ":"

allcfs = allCfs(ws) # get all conditional format blocks
old_cf = getConditionalFormats(ws) # extract conditional format info
allextcfs = allExtCfs(ws) # get all extended conditional format blocks
allcfs = allCfs(ws)
allextcfs = allExtCfs(ws)

let new_pr, new_cf

new_pr = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : "1"
new_pr = string(next_cf_priority!(ws))

isnothing(min_type) || min_type in ["percentile", "percent", "num", "formula"] || throw(XLSXError("Invalid min_type: $min_type. Valid options are: percentile, percent, num, formula."))
(!isnothing(min_type) && min_type == "formula") || isnothing(min_val) || is_valid_fixed_cellname(min_val) || is_valid_fixed_sheet_cellname(min_val) || !isnothing(tryparse(Float64, min_val)) || throw(XLSXError("Invalid min_val: `$min_val`. Valid options (unless min_type is `formula`) are a CellRef (e.g. `\$A\$1`) or a number."))
Expand Down Expand Up @@ -2416,13 +2397,13 @@ function setCfDataBar(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}=()
pfx = get_prefix(ws)
pfx = pfx == "" ? pfx : pfx * ":"

allcfs = allCfs(ws) # get all conditional format blocks
old_cf = getConditionalFormats(ws) # extract conditional format info
allextcfs = allExtCfs(ws) # get all extended conditional format blocks
allcfs = allCfs(ws)
allextcfs = allExtCfs(ws)

let new_pr, new_cf

new_pr = length(old_cf) > 0 ? string(maximum([last(x).priority for x in values(old_cf)]) + 1) : "1"
new_pr = string(next_cf_priority!(ws))

isnothing(min_type) || min_type in ["least", "percentile", "percent", "num", "formula", "automatic"] || throw(XLSXError("Invalid min_type: $min_type. Valid options are: least, percentile, percent, num, formula."))
if min_type in ["least", "automatic"]
min_val = nothing
Expand Down Expand Up @@ -2498,14 +2479,6 @@ function setCfDataBar(ws::Worksheet, rng::CellRange; allkws::Dict{Symbol,Any}=()

push!(cdb, XML.Element("$(pfx)color"; rgb=get_color(allkws["fill_col"])))

# cfx = XML.h.cfRule(type="dataBar", priority=new_pr,
# XML.h.dataBar(
# isnothing(allkws["min_val"]) ? XML.h.cfvo(type=mnt) : XML.h.cfvo(type=mnt, val=allkws["min_val"]),
# isnothing(allkws["max_val"]) ? XML.h.cfvo(type=mxt) : XML.h.cfvo(type=mxt, val=allkws["max_val"]),
# XML.h.color(rgb=get_color(allkws["fill_col"]))),
# XML.h.extLst()
# )

push!(cfx, cdb)

if haskey(allkws, "showVal") && !isnothing(allkws["showVal"]) && allkws["showVal"] == "false"
Expand Down
5 changes: 3 additions & 2 deletions src/sst.jl
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,9 @@ end
# Looks for a string inside the Shared Strings Table (sst).
# `index` starts at 0.
@inline function sst_unformatted_string(wb::Workbook, index::Int64)::String
sst_load!(wb)
return get_sst(wb).unformatted[index+1]
sst = get_sst(wb)
sst.is_loaded || sst_load!(wb)
return sst.unformatted[index+1]
end

@inline sst_unformatted_string(xl::XLSXFile, index::Int64) :: String = sst_unformatted_string(get_workbook(xl), index)
Expand Down
17 changes: 6 additions & 11 deletions src/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -356,12 +356,6 @@ mutable struct SheetRowStreamIteratorState{I}
local_formulas::Dict{SheetCellRef,AbstractFormula}
rows_since_merge::Int
end
#=
struct SheetRowStreamIteratorState{I}
row_iter::I
rowcells::Dict{Int,Cell}
end
=#

mutable struct WorksheetCacheIteratorState
row_from_last_iteration::Int
Expand Down Expand Up @@ -397,19 +391,20 @@ println( sh[:] ) # all data inside worksheet's dimension
```
"""
mutable struct Worksheet
package::MSOfficePackage # parent XLSXFile
package::MSOfficePackage
sheetId::Int
relationship_id::String # r:id="rId1"
relationship_id::String
name::String
dimension::Union{Nothing, CellRange}
is_hidden::Bool
cache::Union{WorksheetCache, Nothing}
next_formula_id::Int
unhandled_attributes::Union{Nothing,Dict{Int,Dict{String,String}}} # row => attributes(name=>value)
sst_count::Int # number of cells containing a shared string
unhandled_attributes::Union{Nothing,Dict{Int,Dict{String,String}}}
sst_count::Int
next_cf_priority::Union{Int, Nothing} # next priority to assign; nothing until first computed

function Worksheet(package::MSOfficePackage, sheetId::Int, relationship_id::String, name::String, dimension::Union{Nothing, CellRange}, is_hidden::Bool)
return new(package, sheetId, relationship_id, name, dimension, is_hidden, nothing, 0, nothing, 0)
return new(package, sheetId, relationship_id, name, dimension, is_hidden, nothing, 0, nothing, 0, nothing)
end
end

Expand Down
Loading
Loading