From 36169e246b2aabd662678d91ff926618744034d8 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Sun, 31 May 2026 08:49:16 +0100 Subject: [PATCH 01/13] Add FileIOExt.jl extension for FileIO support --- Project.toml | 7 +- ext/FileIOExt.jl | 226 +++++++++++++++++++++++++++++++++++++++++++++++ src/XLSX.jl | 1 + src/images.jl | 1 + src/read.jl | 70 ++++++++------- src/table.jl | 3 + test/runtests.jl | 14 +-- 7 files changed, 281 insertions(+), 41 deletions(-) create mode 100644 ext/FileIOExt.jl diff --git a/Project.toml b/Project.toml index d7bf04ca..65d305dd 100644 --- a/Project.toml +++ b/Project.toml @@ -19,15 +19,19 @@ XML = "72c71f33-b9b6-44de-8c94-c961784809e2" ZipArchives = "49080126-0e18-4c2a-b176-c102e4b3760c" [weakdeps] +FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" StyledStrings = "f489334b-da3d-4c2e-b8f0-e476e12c162b" + [extensions] StyledStringsSstsExt = "StyledStrings" +FileIOExt = "FileIO" [compat] CSV = "0.10.15" Colors = "0.12, 0.13" Distributions = "0.25.0" +FileIO = "1" OrderedCollections = "1" PrecompileTools = "1" StyledStrings = "1.0.3" @@ -41,9 +45,10 @@ julia = "1.8" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" StyledStrings = "f489334b-da3d-4c2e-b8f0-e476e12c162b" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["CSV", "DataFrames", "Distributions", "Random", "StyledStrings", "Test"] +test = ["CSV", "DataFrames", "Distributions", "FileIO", "Random", "StyledStrings", "Test"] diff --git a/ext/FileIOExt.jl b/ext/FileIOExt.jl new file mode 100644 index 00000000..b13957bf --- /dev/null +++ b/ext/FileIOExt.jl @@ -0,0 +1,226 @@ +""" +# Introduction + +This package provides support for Excel files under the +[FileIO.jl](https://github.com/JuliaIO/FileIO.jl) package. + +It provides functionality to read simple tabular data from +an Excel (.xlsx) file and to save simple tabular data to an +Excel file. + +For more extensive functionality when reading and writing Excel files, +consider using [XLSX.jl](https://juliadata.github.io/XLSX.jl/stable/). +Under the hood, `ExcelFiles.jl` uses the `XLSX.jl` functions `readtable` +and `writetable`. + +# Usage + +## Load an Excel file + +To read an Excel file into a `DataFrame`, use the following julia code: + +```julia +using ExcelFiles, DataFrames + +df = DataFrame(load("data.xlsx", "Sheet1")) +``` + +The call to `load` returns an object that is a [Tables.jl](https://github.com/JuliaData/Tables.jl) table, so it can be passed to any function that can handle Tables.jl tables. Here are some examples of materializing an Excel file into such data structures: + +```julia +using ExcelFiles, DataFrames, PrettyTables + +# Load into a DataFrame +julia> DataFrame(load("HTable.xlsx")) +5×10 DataFrame + Row │ Year 1940 1950 1960 1970 1980 1990 2000 2010 2020 + │ String Any Any Float64 Float64 Any Any Float64 Float64 Float64 +─────┼─────────────────────────────────────────────────────────────────────────────────────────── + 1 │ Col A 1 2 3.0 4.0 5 6 7.0 8.0 9.0 + 2 │ Col B 10 20 30.0 40.0 50 60 70.0 80.0 90.0 + 3 │ Col C 100 200 300.0 400.0 500 600 700.0 800.0 900.0 + 4 │ Col D 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 + 5 │ Col E Hello 2025-12-19 3.0 3.33 Hello 2025-12-19 3.0 3.33 1.0 + +julia> DataFrame(load("HTable.xlsx"; transpose=true)) +9×6 DataFrame + Row │ Year Col A Col B Col C Col D Col E + │ Int64 Int64 Int64 Int64 Float64 Any +─────┼───────────────────────────────────────────────── + 1 │ 1940 1 10 100 0.1 Hello + 2 │ 1950 2 20 200 0.2 2025-12-19 + 3 │ 1960 3 30 300 0.3 3 + 4 │ 1970 4 40 400 0.4 3.33 + 5 │ 1980 5 50 500 0.5 Hello + 6 │ 1990 6 60 600 0.6 2025-12-19 + 7 │ 2000 7 70 700 0.7 3 + 8 │ 2010 8 80 800 0.8 3.33 + 9 │ 2020 9 90 900 0.9 true + + +# Load into a PrettyTable +julia> PrettyTable(load("HTable.xlsx")) +┌───────┬───────┬────────────┬───────┬───────┬───────┬────────────┬───────┬───────┬───────┐ +│ Year │ 1940 │ 1950 │ 1960 │ 1970 │ 1980 │ 1990 │ 2000 │ 2010 │ 2020 │ +├───────┼───────┼────────────┼───────┼───────┼───────┼────────────┼───────┼───────┼───────┤ +│ Col A │ 1 │ 2 │ 3.0 │ 4.0 │ 5 │ 6 │ 7.0 │ 8.0 │ 9.0 │ +│ Col B │ 10 │ 20 │ 30.0 │ 40.0 │ 50 │ 60 │ 70.0 │ 80.0 │ 90.0 │ +│ Col C │ 100 │ 200 │ 300.0 │ 400.0 │ 500 │ 600 │ 700.0 │ 800.0 │ 900.0 │ +│ Col D │ 0.1 │ 0.2 │ 0.3 │ 0.4 │ 0.5 │ 0.6 │ 0.7 │ 0.8 │ 0.9 │ +│ Col E │ Hello │ 2025-12-19 │ 3.0 │ 3.33 │ Hello │ 2025-12-19 │ 3.0 │ 3.33 │ 1.0 │ +└───────┴───────┴────────────┴───────┴───────┴───────┴────────────┴───────┴───────┴───────┘ + +julia> PrettyTable(load("HTable.xlsx"; transpose=true)) +┌──────┬───────┬───────┬───────┬───────┬────────────┐ +│ Year │ Col A │ Col B │ Col C │ Col D │ Col E │ +├──────┼───────┼───────┼───────┼───────┼────────────┤ +│ 1940 │ 1 │ 10 │ 100 │ 0.1 │ Hello │ +│ 1950 │ 2 │ 20 │ 200 │ 0.2 │ 2025-12-19 │ +│ 1960 │ 3 │ 30 │ 300 │ 0.3 │ 3 │ +│ 1970 │ 4 │ 40 │ 400 │ 0.4 │ 3.33 │ +│ 1980 │ 5 │ 50 │ 500 │ 0.5 │ Hello │ +│ 1990 │ 6 │ 60 │ 600 │ 0.6 │ 2025-12-19 │ +│ 2000 │ 7 │ 70 │ 700 │ 0.7 │ 3 │ +│ 2010 │ 8 │ 80 │ 800 │ 0.8 │ 3.33 │ +│ 2020 │ 9 │ 90 │ 900 │ 0.9 │ true │ +└──────┴───────┴───────┴───────┴───────┴────────────┘ + +``` + +The `load` function takes a number of arguments and keywords: + +```julia + FileIO.load( + source::String, + [sheet::String, + [columns::String]]; + [first_row::Int], + [first_column::String] + [column_labels::Vector{String}], + [header::Bool], + [normalizenames::Bool], + [transpose::Bool] + ) +``` + +### Arguments: + +* `source`: The name of the file to be loaded. +* `sheet`: Specifies the sheet name to be loaded. If `sheet` is not given, the first Excel sheet in the file will be used. +* `columns`: Determines which columns to read. 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. A valid sheet **must** be specified when specifying columns. If `transpose = true` or is omitted, `columns` should be used to specify rows. For example, specifying `"2:4"` with `transpose = true` will read only from these rows. + +### Keywords: + +* `first_row`: Indicates the first row of the data table to be read. For example, `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 sheet (ignored if `transpose = true`). +* `first_column`: Indicates the first row of the data table to be read. For example, `first_column="B"` 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 sheet (ignored if `transpose = false` or is omitted). +* `column_labels`: Specifies column names for the header of the table. If `column_labels` are given and `header=true`, the headers given by `column_labels` will be used, and the first row of the table (containing headers) will be ignored. +* `header`: Indicates if the first row (column if `transpose = true`) is a header. If `header=true` and `column_labels` is not specified, the column labels for the table will be read from the first row (column) of the table. If `header=false` and `column_labels` is not specified, the algorithm will generate column labels. The default value is `header=true`. +* `normalizenames`: Set to `true` to normalize column names to valid Julia identifiers. Default=`false`. +* `transpose`: Set to `true` to transpose the table to read data from rows not columns. + +### Examples + +```julia +julia> PrettyTable(load("HTable.xlsx", "Offset"; first_row=2)) + +julia> df = DataFrame(load("HTable.xlsx", "Offset", "2:7"; transpose=true, first_column="B")) + +julia> df = DataFrame(load("HTable.xlsx"; normalizenames=true, transpose=true, column_labels=["Date", "Name1", "Name2", "Name3", "Name4", "Name5"])) + +``` +## Save an Excel file + +The following code saves any Tables.jl table (such as a `DataFrame`) as an Excel file: +```julia +using ExcelFiles + +save("output.xlsx", tbl) +``` + +The `save` function takes a number of arguments and keywords: + +```julia + FileIO.save( + source::String; + [sheetname::String], + [overwrite::Bool] + ) +``` + +### Arguments: + +* `source`: The name of the file to be created on save. + +### Keywords: + +* `sheetname`: Specify the sheetname to be used in the created file. By default, the sheetname will be `Sheet1`. +* `overwrite`: Set `overwrite=true` to overwite any existing file of the same name. Default = `false`. + +### Examples + +```julia +julia> save("myfile.xlsx", df; sheetname="myname", overwrite=true) +``` + +## Using the pipe syntax + +The `load` and `save` functions also support the pipe syntax. For example, to load an Excel file into a `DataFrame`, one can use the following code: + +```julia +using ExcelFiles, DataFrame + +df = load("data.xlsx", "Sheet1") |> DataFrame +``` + +To save any Tables.jl compatible table (such as a DataFrame), one can use the following form: + +```julia +using ExcelFiles, DataFrame + +df = # Aquire a DataFrame somehow + +df |> save("output.xlsx") +``` +""" + +module FileIOloaderExt +# Provides hooks for FileIO.jl to load XLSX files. This is not a dependency of XLSX.jl, so that users who do not want to use FileIO.jl can avoid installing it. + +using FileIO + +using XLSX + +import XLSX: load, save + +function load(f::File{format"Excel"}; transpose::Bool=false, kw...) + filename = FileIO.filename(f) + if transpose + return XLSX.readtransposedtable(filename; kw...) + else + return XLSX.readtable(filename; kw...) + end +end + +function load(f::File{format"Excel"}, sheet; transpose::Bool=false, kw...) + filename = FileIO.filename(f) + if transpose + return XLSX.readtransposedtable(filename, sheet; kw...) + else + return XLSX.readtable(filename, sheet; kw...) + end +end + +function load(f::File{format"Excel"}, sheet, columns; transpose::Bool=false, kw...) + filename = FileIO.filename(f) + if transpose + return XLSX.readtransposedtable(filename, sheet, columns; kw...) + else + return XLSX.readtable(filename, sheet, columns; kw...) + end +end + +function save(f::File{format"Excel"}, data; kw...) + XLSX.writetable(FileIO.filename(f), data; kw...) +end + +end # module \ No newline at end of file diff --git a/src/XLSX.jl b/src/XLSX.jl index c35b9444..ec0226d8 100644 --- a/src/XLSX.jl +++ b/src/XLSX.jl @@ -65,6 +65,7 @@ include("images.jl") include("write.jl") include("fileArray.jl") + PCT.@setup_workload begin # Putting some things in `@setup_workload` instead of `@compile_workload` can reduce the size of the # precompile file and potentially make loading faster. diff --git a/src/images.jl b/src/images.jl index a2650d14..44db5bcd 100644 --- a/src/images.jl +++ b/src/images.jl @@ -97,6 +97,7 @@ If multiple, overlapping images are added, newer images overly older ones. # Arguments `s::Worksheet`: the target worksheet. + `ref::AbstractString`: Either a valid cell reference (e.g. `"A1"`) or a valid cell range (e.g. `"B2:D4"`). The image will be anchored to the top left of the reference and sized to fit within the reference bounds. If a cell range is given, the `size` keyword argument is ignored. diff --git a/src/read.jl b/src/read.jl index eff42f0a..bda14b6d 100644 --- a/src/read.jl +++ b/src/read.jl @@ -528,11 +528,12 @@ function openxlsx(source::Union{AbstractString,IO}; end function parse_file_mode(mode::AbstractString)::Tuple{Bool,Bool} - if mode == "r" + m = lowercase(mode) + if m == "r" return (true, false) - elseif mode == "w" + elseif m == "w" return (false, true) - elseif mode == "rw" || mode == "wr" + elseif m == "rw" || m == "wr" return (true, true) else throw(XLSXError("Couldn't parse file mode $mode.")) @@ -553,35 +554,35 @@ function convert_strict_to_transitional!(xf::XLSXFile, pass::Int) occursin(r"xl/worksheets/sheet\d+\.xml", filename) end - if should_process - data = xf.data[filename] - xroot = data[end] - attrs = XML.attributes(xroot) - - for (k, v) in attrs - if k == "conformance" && v == "strict" - delete!(attrs, "conformance") - elseif startswith(v, "http://purl.oclc.org/ooxml") - if haskey(STRICT_TO_TRANSITIONAL, v) - attrs[k] = STRICT_TO_TRANSITIONAL[v] - else - throw(XLSXError("Unsupported strict OOXML namespace or relationship type: \"$v\" in $filename. Please open an issue at https://github.com/JuliaData/XLSX.jl/issues")) - end + should_process || continue + + data = xf.data[filename] + xroot = data[end] + attrs = XML.attributes(xroot) + + for (k, v) in attrs + if k == "conformance" && v == "strict" + delete!(attrs, "conformance") + elseif startswith(v, "http://purl.oclc.org/ooxml") + if haskey(STRICT_TO_TRANSITIONAL, v) + attrs[k] = STRICT_TO_TRANSITIONAL[v] + else + throw(XLSXError("Unsupported strict OOXML namespace or relationship type: \"$v\" in $filename. Please open an issue at https://github.com/JuliaData/XLSX.jl/issues")) end end + end - # For .rels files, also patch Type= on child Relationship elements - for el in XML.children(xroot) - el_attrs = XML.attributes(el) - if !isnothing(el_attrs) - haskey(el_attrs, "conformance") && delete!(el_attrs, "conformance") - type_val = get(el_attrs, "Type", "") - if startswith(type_val, "http://purl.oclc.org/ooxml") - if haskey(STRICT_TO_TRANSITIONAL, type_val) - el_attrs["Type"] = STRICT_TO_TRANSITIONAL[type_val] - else - throw(XLSXError("Unsupported strict OOXML relationship type: \"$type_val\" in $filename. Please open an issue at https://github.com/JuliaData/XLSX.jl/issues")) - end + # For .rels files, also patch Type= on child Relationship elements + for el in XML.children(xroot) + el_attrs = XML.attributes(el) + if !isnothing(el_attrs) + haskey(el_attrs, "conformance") && delete!(el_attrs, "conformance") + type_val = get(el_attrs, "Type", "") + if startswith(type_val, "http://purl.oclc.org/ooxml") + if haskey(STRICT_TO_TRANSITIONAL, type_val) + el_attrs["Type"] = STRICT_TO_TRANSITIONAL[type_val] + else + throw(XLSXError("Unsupported strict OOXML relationship type: \"$type_val\" in $filename. Please open an issue at https://github.com/JuliaData/XLSX.jl/issues")) end end end @@ -957,10 +958,6 @@ function stream_files(xf::XLSXFile, zip_io::ZipArchives.ZipReader; pass::Int, end end -# list of filename prefixes to pass through as binary files. -const BINARY_PREFIXES = ["customXml"] - - # Read xml files in three passes # pass 1 - read all but worksheets and sharedStrings # pass 2 - only read sharedStrings (needed before worksheets) @@ -1004,7 +1001,7 @@ function load_files!(xf::XLSXFile, zip_io::ZipArchives.ZipReader; pass::Int, if has_sst(wb) sst_load!(wb) end - elseif xf.use_cache_for_sheet_data && !occursin(r"^xl/sharedStrings\.xml$", file.name) + elseif xf.use_cache_for_sheet_data# && !occursin(r"^xl/sharedStrings\.xml$", file.name) rid = get_relationship_id_by_target(wb, file.name) for sheet in wb.sheets if sheet.relationship_id == rid @@ -1526,3 +1523,8 @@ function unescape(x::AbstractString) end return result end + +# Hooks for FileIOloaderExt.jl + +function load end # forward declaration +function save end # forward declaration \ No newline at end of file diff --git a/src/table.jl b/src/table.jl index 5f1b1167..deaebfdb 100644 --- a/src/table.jl +++ b/src/table.jl @@ -3,6 +3,9 @@ # Table # +Base.show(io::IO, ::MIME"text/plain", dt::DataTable) = + print(io, "DataTable with $(length(dt.data)) columns and $(length(dt.data[1])) rows.") + # Returns a tuple with the first and last index of the columns for a `SheetRow`. function column_bounds(sr::SheetRow) isempty(sr) && throw(XLSXError("Can't get column bounds from an empty row.")) diff --git a/test/runtests.jl b/test/runtests.jl index fa14f596..4c33b4c5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -69,7 +69,7 @@ src_data_directory = joinpath(dirname(pathof(XLSX)), "data") # Issue #293 @testset "read .xltx file" begin - xf = XLSX.openxlsx(joinpath(data_directory, "Template File.xltx"); mode="rw") + xf = XLSX.openxlsx(joinpath(data_directory, "Template File.xltx"); mode="RW") s=xf[1] @test s["P5"] == 5 @test XLSX.getFormula(s, "B5") == "=RANDBETWEEN(0,100)" @@ -84,7 +84,7 @@ src_data_directory = joinpath(dirname(pathof(XLSX)), "data") @test xf.template_type == XLSX.NotATemplate isfile(joinpath(data_directory, "Template File.xlsx")) && rm(joinpath(data_directory, "Template File.xlsx")) - XLSX.openxlsx(joinpath(data_directory, "Template File.xltx"); mode="rw") do xf + XLSX.openxlsx(joinpath(data_directory, "Template File.xltx"); mode="RW") do xf s=xf[1] @test s["P5"] == 5 @test XLSX.getFormula(s, "B5") == "=RANDBETWEEN(0,100)" @@ -96,14 +96,14 @@ src_data_directory = joinpath(dirname(pathof(XLSX)), "data") # Issue #401 @testset "macro enabled files" begin - mf = XLSX.openxlsx(joinpath(data_directory, "macro-enabled.xlsm"); mode="rw") + mf = XLSX.openxlsx(joinpath(data_directory, "macro-enabled.xlsm"); mode="Rw") @test mf[1]["A1"] == "hello" XLSX.writexlsx("mytest.xlsm", mf; overwrite=true) mf = XLSX.openxlsx("mytest.xlsm"; mode="rw") @test mf[1]["A1"] == "hello" isfile("mytest.xlsm") && rm("mytest.xlsm") - mf = XLSX.openxlsx(joinpath(data_directory, "macro-enabled2.xltm"); mode="rw") + mf = XLSX.openxlsx(joinpath(data_directory, "macro-enabled2.xltm"); mode="rW") @test mf[1]["A1"] == "hello" @test mf.template_type == XLSX.XLTMTemplate XLSX.savexlsx(mf) @@ -114,12 +114,12 @@ src_data_directory = joinpath(dirname(pathof(XLSX)), "data") @test mf.template_type == XLSX.NotATemplate isfile(joinpath(data_directory, "macro-enabled2.xlsm")) && rm(joinpath(data_directory, "macro-enabled2.xlsm")) - XLSX.openxlsx(joinpath(data_directory, "macro-enabled2.xltm"); mode="rw") do mf + XLSX.openxlsx(joinpath(data_directory, "macro-enabled2.xltm"); mode="wr") do mf @test mf[1]["A1"] == "hello" @test mf.template_type == XLSX.XLTMTemplate end @test isfile(joinpath(data_directory, "macro-enabled2.xlsm")) - mf = XLSX.openxlsx(joinpath(data_directory, "macro-enabled2.xlsm"); mode="rw") + mf = XLSX.openxlsx(joinpath(data_directory, "macro-enabled2.xlsm"); mode="WR") @test mf[1]["A1"] == "hello" @test mf.template_type == XLSX.NotATemplate isfile(joinpath(data_directory, "macro-enabled2.xlsm")) && rm(joinpath(data_directory, "macro-enabled2.xlsm")) @@ -815,6 +815,8 @@ end end isfile("mytest.xlsx") && rm("mytest.xlsx") + + # Issue #395 @testset "Multi-threaded read" begin N_FORMULAS = 5000 # Should be a multiple of ROW_CHUNKSIZE N_ITER = 5 From 5e3300deac374c4b803709d57a37b3c03a3a8ced Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 1 Jun 2026 11:06:49 +0100 Subject: [PATCH 02/13] update license --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index cd757018..adf7eeab 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2023 Felipe Noronha Tavares +Copyright (c) 2018-2026 Felipe Noronha Tavares and other contributors: https://github.com/juliadata/XLSX.jl/contributors From 86e56502497b50364a9894c78e9b7fd7eb0b7bb0 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 1 Jun 2026 12:12:32 +0100 Subject: [PATCH 03/13] Add docs and tests for FileIO.jl integration. --- CHANGELOG.md | 1 + Project.toml | 2 +- docs/make.jl | 5 +- docs/src/api/files.md | 6 + docs/src/tutorial/FileIOtutorial.md | 132 +++++++++++++++ .../{tutorial.md => tutorial/XLSXtutorial.md} | 4 +- ext/FileIOExt.jl | 14 +- src/read.jl | 80 ++++++++- src/table.jl | 6 +- test/data/TestData.xlsx | Bin 0 -> 13598 bytes test/runtests.jl | 155 ++++++++++++++++++ 11 files changed, 390 insertions(+), 15 deletions(-) create mode 100644 docs/src/tutorial/FileIOtutorial.md rename docs/src/{tutorial.md => tutorial/XLSXtutorial.md} (99%) create mode 100644 test/data/TestData.xlsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 77204dfc..bde04f5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +- add a package extension to support [FileIO.jl](https://github.com/JuliaIO/FileIO.jl) ## [v0.11.10](https://github.com/JuliaData/XLSX.jl/tree/v0.11.10) - 2026-05-28 - support macro-enabled files ([#401](https://github.com/JuliaData/XLSX.jl/issues/401)) diff --git a/Project.toml b/Project.toml index 65d305dd..1be8094c 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "XLSX" uuid = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" license = "MIT" -version = "0.11.10" +version = "0.11.11" authors = ["Felipe Noronha "] repo = "https://github.com/juliadata/XLSX.jl.git" diff --git a/docs/make.jl b/docs/make.jl index add284ae..3b876d0f 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -6,7 +6,10 @@ makedocs( modules = [ XLSX ], pages = [ "Home" => "index.md", - "Tutorial" => "tutorial.md", + "Tutorial" => Any[ + "Using XLSX" => "tutorial/XLSXtutorial.md", + "Using FileIO" => "tutorial/FileIOtutorial.md", + ], "Formatting Guide" => Any[ "Cell formats" => "formatting/cellFormatting.md", "Conditional formats" => "formatting/conditionalFormatting.md", diff --git a/docs/src/api/files.md b/docs/src/api/files.md index 9c581ffe..0173faa3 100644 --- a/docs/src/api/files.md +++ b/docs/src/api/files.md @@ -13,6 +13,12 @@ XLSX.writexlsx XLSX.savexlsx ``` +## Files (using FileIO) +```@docs +XLSX.load +XLSX.save +``` + ## Worksheets ```@docs diff --git a/docs/src/tutorial/FileIOtutorial.md b/docs/src/tutorial/FileIOtutorial.md new file mode 100644 index 00000000..51cd1080 --- /dev/null +++ b/docs/src/tutorial/FileIOtutorial.md @@ -0,0 +1,132 @@ +# FileIO Tutorial + +## Introduction + +A package extension to XLSX.jl provides support for Excel +files under the [FileIO.jl](https://github.com/JuliaIO/FileIO.jl) package. + +Through [FileIO.jl](https://github.com/JuliaIO/FileIO.jl), +you can read simple tabular data from an Excel (.xlsx) file and save +tabular data to an Excel file using simple `load` and `save` +functions without needing to know anything about XLSX.jl itself. + +XLSX.jl provides much more extensive functionality if you need it. +Check out the rest of the documentation for full details. + +## Setup + +First, make sure you have the **FileIO.jl** and **XLSX.jl** packages installed. + +```julia +julia> using Pkg + +julia> Pkg.add(["FileIO", "XLSX"]) +``` + +## Usage + +### Load an Excel file + +To read an Excel file into a `DataFrame`, use the following julia code: + +```julia +using FileIO, XLSX, DataFrames + +df = DataFrame(load("data.xlsx", "Sheet1")) +``` + +The call to `load` returns an object that is a [Tables.jl](https://github.com/JuliaData/Tables.jl) table, so it can be passed to any function that can handle Tables.jl tables. Here are some examples of materializing an Excel file into such data structures: + +```julia +using FileIO, XLSX, DataFrames, PrettyTables + +# Load into a DataFrame +julia> DataFrame(load("HTable.xlsx")) +5×10 DataFrame + Row │ Year 1940 1950 1960 1970 1980 1990 2000 2010 2020 + │ String Any Any Float64 Float64 Any Any Float64 Float64 Float64 +─────┼─────────────────────────────────────────────────────────────────────────────────────────── + 1 │ Col A 1 2 3.0 4.0 5 6 7.0 8.0 9.0 + 2 │ Col B 10 20 30.0 40.0 50 60 70.0 80.0 90.0 + 3 │ Col C 100 200 300.0 400.0 500 600 700.0 800.0 900.0 + 4 │ Col D 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 + 5 │ Col E Hello 2025-12-19 3.0 3.33 Hello 2025-12-19 3.0 3.33 1.0 + +julia> DataFrame(load("HTable.xlsx"; transpose=true)) +9×6 DataFrame + Row │ Year Col A Col B Col C Col D Col E + │ Int64 Int64 Int64 Int64 Float64 Any +─────┼───────────────────────────────────────────────── + 1 │ 1940 1 10 100 0.1 Hello + 2 │ 1950 2 20 200 0.2 2025-12-19 + 3 │ 1960 3 30 300 0.3 3 + 4 │ 1970 4 40 400 0.4 3.33 + 5 │ 1980 5 50 500 0.5 Hello + 6 │ 1990 6 60 600 0.6 2025-12-19 + 7 │ 2000 7 70 700 0.7 3 + 8 │ 2010 8 80 800 0.8 3.33 + 9 │ 2020 9 90 900 0.9 true + + +# Load into a PrettyTable +julia> PrettyTable(load("HTable.xlsx")) +┌───────┬───────┬────────────┬───────┬───────┬───────┬────────────┬───────┬───────┬───────┐ +│ Year │ 1940 │ 1950 │ 1960 │ 1970 │ 1980 │ 1990 │ 2000 │ 2010 │ 2020 │ +├───────┼───────┼────────────┼───────┼───────┼───────┼────────────┼───────┼───────┼───────┤ +│ Col A │ 1 │ 2 │ 3.0 │ 4.0 │ 5 │ 6 │ 7.0 │ 8.0 │ 9.0 │ +│ Col B │ 10 │ 20 │ 30.0 │ 40.0 │ 50 │ 60 │ 70.0 │ 80.0 │ 90.0 │ +│ Col C │ 100 │ 200 │ 300.0 │ 400.0 │ 500 │ 600 │ 700.0 │ 800.0 │ 900.0 │ +│ Col D │ 0.1 │ 0.2 │ 0.3 │ 0.4 │ 0.5 │ 0.6 │ 0.7 │ 0.8 │ 0.9 │ +│ Col E │ Hello │ 2025-12-19 │ 3.0 │ 3.33 │ Hello │ 2025-12-19 │ 3.0 │ 3.33 │ 1.0 │ +└───────┴───────┴────────────┴───────┴───────┴───────┴────────────┴───────┴───────┴───────┘ + +julia> PrettyTable(load("HTable.xlsx"; transpose=true)) +┌──────┬───────┬───────┬───────┬───────┬────────────┐ +│ Year │ Col A │ Col B │ Col C │ Col D │ Col E │ +├──────┼───────┼───────┼───────┼───────┼────────────┤ +│ 1940 │ 1 │ 10 │ 100 │ 0.1 │ Hello │ +│ 1950 │ 2 │ 20 │ 200 │ 0.2 │ 2025-12-19 │ +│ 1960 │ 3 │ 30 │ 300 │ 0.3 │ 3 │ +│ 1970 │ 4 │ 40 │ 400 │ 0.4 │ 3.33 │ +│ 1980 │ 5 │ 50 │ 500 │ 0.5 │ Hello │ +│ 1990 │ 6 │ 60 │ 600 │ 0.6 │ 2025-12-19 │ +│ 2000 │ 7 │ 70 │ 700 │ 0.7 │ 3 │ +│ 2010 │ 8 │ 80 │ 800 │ 0.8 │ 3.33 │ +│ 2020 │ 9 │ 90 │ 900 │ 0.9 │ true │ +└──────┴───────┴───────┴───────┴───────┴────────────┘ + +``` + +For more information, see [`XLSX.load`](@ref) + +### Save an Excel file + +The following code saves any Tables.jl table (such as a `DataFrame`) as an Excel file: + +```julia +using FileIO, XLSX, + +save("output.xlsx", myTable) +``` + +For more information, see [`XLSX.save`](@ref) + +### Using the pipe syntax + +The `load` and `save` functions also support the pipe syntax. For example, to load an Excel file into a `DataFrame`, one can use the following code: + +```julia +using FileIO, XLSX, DataFrame + +df = load("data.xlsx", "Sheet1") |> DataFrame +``` + +To save any Tables.jl compatible table (such as a DataFrame), one can use the following form: + +```julia +using FileIO, XLSX, DataFrame + +df = # Aquire a DataFrame somehow + +df |> save("output.xlsx") +``` diff --git a/docs/src/tutorial.md b/docs/src/tutorial/XLSXtutorial.md similarity index 99% rename from docs/src/tutorial.md rename to docs/src/tutorial/XLSXtutorial.md index 4cc4bd18..26623e91 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial/XLSXtutorial.md @@ -1,9 +1,9 @@ -# Tutorial +# XLSX Tutorial ## Setup -First, make sure you have **XLSX.jl** package installed. +First, make sure you have the **XLSX.jl** package installed. ```julia julia> using Pkg diff --git a/ext/FileIOExt.jl b/ext/FileIOExt.jl index b13957bf..15eb95f8 100644 --- a/ext/FileIOExt.jl +++ b/ext/FileIOExt.jl @@ -183,7 +183,7 @@ df |> save("output.xlsx") ``` """ -module FileIOloaderExt +module FileIOExt # Provides hooks for FileIO.jl to load XLSX files. This is not a dependency of XLSX.jl, so that users who do not want to use FileIO.jl can avoid installing it. using FileIO @@ -192,7 +192,7 @@ using XLSX import XLSX: load, save -function load(f::File{format"Excel"}; transpose::Bool=false, kw...) +function load(f::File{FileIO.format"Excel"}; transpose::Bool=false, kw...) filename = FileIO.filename(f) if transpose return XLSX.readtransposedtable(filename; kw...) @@ -201,7 +201,7 @@ function load(f::File{format"Excel"}; transpose::Bool=false, kw...) end end -function load(f::File{format"Excel"}, sheet; transpose::Bool=false, kw...) +function load(f::File{FileIO.format"Excel"}, sheet; transpose::Bool=false, kw...) filename = FileIO.filename(f) if transpose return XLSX.readtransposedtable(filename, sheet; kw...) @@ -210,16 +210,16 @@ function load(f::File{format"Excel"}, sheet; transpose::Bool=false, kw...) end end -function load(f::File{format"Excel"}, sheet, columns; transpose::Bool=false, kw...) +function load(f::File{FileIO.format"Excel"}, sheet, rows_or_columns; transpose::Bool=false, kw...) filename = FileIO.filename(f) if transpose - return XLSX.readtransposedtable(filename, sheet, columns; kw...) + return XLSX.readtransposedtable(filename, sheet, rows_or_columns; kw...) else - return XLSX.readtable(filename, sheet, columns; kw...) + return XLSX.readtable(filename, sheet, rows_or_columns; kw...) end end -function save(f::File{format"Excel"}, data; kw...) +function save(f::File{FileIO.format"Excel"}, data; kw...) XLSX.writetable(FileIO.filename(f), data; kw...) end diff --git a/src/read.jl b/src/read.jl index bda14b6d..96c1e57c 100644 --- a/src/read.jl +++ b/src/read.jl @@ -1524,7 +1524,81 @@ function unescape(x::AbstractString) return result end -# Hooks for FileIOloaderExt.jl +# Hooks for FileIOExt.jl -function load end # forward declaration -function save end # forward declaration \ No newline at end of file +""" +```julia + load( + source::String, + [sheet::String, + [columns::String]]; + [first_row::Int], + [first_column::String] + [column_labels::Vector{String}], + [header::Bool], + [normalizenames::Bool], + [transpose::Bool] + ) +``` +Read tabular data from an Excel file, `source`, and return it as a `Tables.jl` compatible table. +The resulting table object can be passed directly to any function that accepts `Tables.jl` data +(e.g. `DataFrame` from package `DataFrames.jl`). + +This function requires FileIO.jl to be active in the current environment. + +#### Arguments: + +* `source`: The name of the file to be loaded. +* `sheet`: Specifies the sheet name to be loaded. If `sheet` is not given, the first Excel sheet in the file will be used. +* `columns`: Determines which columns to read. 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. A valid sheet **must** be specified when specifying columns. If `transpose = true` or is omitted, `columns` should be used to specify rows. For example, specifying `"2:4"` with `transpose = true` will read only from these rows. + +#### Keywords: + +* `first_row`: Indicates the first row of the data table to be read. For example, `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 sheet (ignored if `transpose = true`). +* `first_column`: Indicates the first row of the data table to be read. For example, `first_column="B"` 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 sheet (ignored if `transpose = false` or is omitted). +* `column_labels`: Specifies column names for the header of the table. If `column_labels` are given and `header=true`, the headers given by `column_labels` will be used, and the first row of the table (containing headers) will be ignored. +* `header`: Indicates if the first row (column if `transpose = true`) is a header. If `header=true` and `column_labels` is not specified, the column labels for the table will be read from the first row (column) of the table. If `header=false` and `column_labels` is not specified, the algorithm will generate column labels. The default value is `header=true`. +* `normalizenames`: Set to `true` to normalize column names to valid Julia identifiers. Default=`false`. +* `transpose`: Set to `true` to transpose the table to read data from rows not columns. + +#### Examples + +```julia +julia> PrettyTable(load("HTable.xlsx", "Offset"; first_row=2)) + +julia> df = DataFrame(load("HTable.xlsx", "Offset", "2:7"; transpose=true, first_column="B")) + +julia> df = DataFrame(load("HTable.xlsx"; normalizenames=true, transpose=true, column_labels=["Date", "Name1", "Name2", "Name3", "Name4", "Name5"])) + +``` +""" +function load end + +""" +```julia + save( + source::String; + [sheetname::String], + [overwrite::Bool] + ) +``` +Save a `Tables.jl` compatible table to an Excel file, `source`. + +This function requires FileIO.jl to be active in the current environment. + +#### Arguments: + +* `source`: The name of the file to be created on save. + +#### Keywords: + +* `sheetname`: Specify the sheetname to be used in the created file. By default, the sheetname will be `Sheet1`. +* `overwrite`: Set `overwrite=true` to overwite any existing file of the same name. Default = `false`. + +#### Examples + +```julia +julia> save("myfile.xlsx", myTable; sheetname="myname", overwrite=true) +``` +""" +function save end \ No newline at end of file diff --git a/src/table.jl b/src/table.jl index deaebfdb..b760ace0 100644 --- a/src/table.jl +++ b/src/table.jl @@ -3,8 +3,12 @@ # Table # +Base.show(io::IO, dt::DataTable) = + Base.show(io, MIME"text/plain"(), dt) + Base.show(io::IO, ::MIME"text/plain", dt::DataTable) = - print(io, "DataTable with $(length(dt.data)) columns and $(length(dt.data[1])) rows.") + print(io, "XLSX.DataTable with $(length(dt.data)) columns and $(length(dt.data[1])) rows.") + # Returns a tuple with the first and last index of the columns for a `SheetRow`. function column_bounds(sr::SheetRow) diff --git a/test/data/TestData.xlsx b/test/data/TestData.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..420a8cb485ba0421407119a0e585a8a48fa428d6 GIT binary patch literal 13598 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 DataFrames.DataFrame + save("file.xlsx", input) + output = load("file.xlsx", "Sheet1") |> DataFrames.DataFrame + @test input == output + rm("file.xlsx") + + # Test for saving DataFrame to XLSX with sheetname keyword + input = (Day = ["Nov. 27", "Nov. 28", "Nov. 29"], Highest = [78, 79, 75]) |> DataFrames.DataFrame + save("file.xlsx", input, sheetname="SheetName") + output = load("file.xlsx", "SheetName") |> DataFrames.DataFrame + @test input == output + rm("file.xlsx") + + df, names = get_cols(load(filename, "Sheet1"; column_labels=good_colnames)) + @test names == good_colnames + @test length(df[1]) == 4 + @test length(df) == 13 + @test df[1] == [1., 1.5, 2., 2.5] + @test df[2] == ["A", "BB", "CCC", "DDDD"] + @test df[3] == [true, false, false, true] + @test isequal(df[4], [2, "EEEEE", false, 1.5]) + @test isequal(df[5], [9., "III", missing, true]) + @test isequal(df[6], [3., missing, 3.5, 4.]) + @test isequal(df[7], ["FF", missing, "GGG", "HHHH"]) + @test isequal(df[8], [missing, true, missing, false]) + @test df[9] == [Date(2015, 3, 3), DateTime(2015, 2, 4, 10, 14), DateTime(1988, 4, 9), Dates.Time(15, 2, 0)] + @test isequal(df[10], [Date(1965, 4, 3), DateTime(1950, 8, 9, 18, 40), Dates.Time(19, 0, 0), missing]) + @test all(ismissing, df[11]) + @test all(ismissing, df[12]) + @test isequal(df[13], [missing, 3.4, "HKEJW", missing]) + @test ismissing(df[12][4]) + + # Too few column labels - Note: Bypass FileIO here to avoid false "Fatal Error" from FileIO when the error is correctly thrown by ExcelFiles for mismatched column_labels length. + try + XLSX.load(File{FileIO.format"Excel"}(filename), "Sheet1", "C:O"; header=true, column_labels=[:c1, :c2, :c3, :c4]) + @test false # should error before this line + catch e + @test e isa XLSX.XLSXError && occursin("`column_range` (length=13) and `column_labels` (length=4) must have the same length.", e.msg) + end + + # Test for constructing DataFrame with empty header cell + data, names = get_cols(load(filename, "Sheet2", "C:E")) + @test names == [:Col1, Symbol("#Empty"), :Col3] + + # normalizenames keyword (XLSX.jl v0.11 only) + data, names = get_cols(load(filename, "Sheet2", "C:E"; normalizenames=true)) + @test names == [:Col1, :_Empty, :Col3] + + end + + @testset "Transposed tables" begin + # Note: readtransposedtable cannot handle entirely empty rows/columns, + # so the Transpose sheet omits those from the original Sheet1 data. + # Note: eltype of mixed date columns is Dates.TimeType (not Any) when + # there are no missing values, since a common supertype can be inferred. + + df, names = get_cols(load(filename, "Transpose"; transpose=true, first_column=2)) + @test length(df) == 5 + @test length(df[1]) == 4 + @test names == [Symbol("Some Float64s"), Symbol("Some Strings"), Symbol("Some Bools"), Symbol("Mixed with NA"), Symbol("Some dates")] + + @test df[1] == [1.0, 1.5, 2.0, 2.5] + @test df[2] == ["A", "BB", "CCC", "DDDD"] + @test df[3] == Bool[true, false, false, true] + @test isequal(df[4], Any[9, "III", missing, true]) + @test df[5] == Dates.TimeType[Date(2015, 3, 3), DateTime(2015, 2, 4, 10, 14), Date(1988, 4, 9), Dates.Time(15, 2, 0)] + end end \ No newline at end of file From 2ed54b9bb15f9c82e9211838e254811bdc794be8 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 1 Jun 2026 12:47:11 +0100 Subject: [PATCH 04/13] Extend tests a little. --- src/read.jl | 40 ++++++++++++++++++++++++++++++++++++---- test/runtests.jl | 21 ++++++++++++++++++++- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/read.jl b/src/read.jl index 96c1e57c..caf4b302 100644 --- a/src/read.jl +++ b/src/read.jl @@ -1528,7 +1528,7 @@ end """ ```julia - load( + FileIO.load( source::String, [sheet::String, [columns::String]]; @@ -1572,11 +1572,27 @@ julia> df = DataFrame(load("HTable.xlsx"; normalizenames=true, transpose=true, c ``` """ -function load end +function load(args...; kwargs...) + throw(XLSXError( + """ + load requires the FileIO.jl package. + + Please install and load it with: + + using Pkg + Pkg.add("FileIO") + using FileIO + + Then retry FileIO.load. + """ + )) + + return nothing +end """ ```julia - save( + FileIO.save( source::String; [sheetname::String], [overwrite::Bool] @@ -1601,4 +1617,20 @@ This function requires FileIO.jl to be active in the current environment. julia> save("myfile.xlsx", myTable; sheetname="myname", overwrite=true) ``` """ -function save end \ No newline at end of file +function save(args...; kwargs...) + throw(XLSXError( + """ + save requires the FileIO.jl package. + + Please install and load it with: + + using Pkg + Pkg.add("FileIO") + using FileIO + + Then retry FileIO.save. + """ + )) + + return nothing +end diff --git a/test/runtests.jl b/test/runtests.jl index b37d4c6a..c3c1c036 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -7,7 +7,6 @@ import DataFrames, Random import Distributions as Dist import CSV using StyledStrings -using FileIO data_directory = joinpath(dirname(pathof(XLSX)), "..", "test", "data") @@ -7779,6 +7778,26 @@ function get_cols(source::XLSX.DataTable) return source.data, source.column_labels end +@testset "No FileIO" verbose=true begin + + filename = joinpath(data_directory, "TestData.xlsx") + + try + XLSX.load(filename, "Sheet1") + @test false # should error before this line + catch e + @test e isa XLSX.XLSXError && occursin("requires the FileIO.jl package", e.msg) + end + try + XLSX.save(filename, "Sheet1") + @test false # should error before this line + catch e + @test e isa XLSX.XLSXError && occursin("requires the FileIO.jl package", e.msg) + end +end + +using FileIO + @testset "FileIO" verbose=true begin filename = joinpath(data_directory, "TestData.xlsx") From 1fdbed7e0d19603c33ab52cbb2c1ad5e5c138c69 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Mon, 1 Jun 2026 18:40:07 +0100 Subject: [PATCH 05/13] Fix tests to skip FileIO tests if version too old. --- CHANGELOG.md | 1 + Project.toml | 7 ++- src/read.jl | 4 +- test/runtests.jl | 160 ++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 166 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bde04f5b..19a5a017 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] - add a package extension to support [FileIO.jl](https://github.com/JuliaIO/FileIO.jl) +- update copyright notice end-date ## [v0.11.10](https://github.com/JuliaData/XLSX.jl/tree/v0.11.10) - 2026-05-28 - support macro-enabled files ([#401](https://github.com/JuliaData/XLSX.jl/issues/401)) diff --git a/Project.toml b/Project.toml index 1be8094c..95734b37 100644 --- a/Project.toml +++ b/Project.toml @@ -22,10 +22,9 @@ ZipArchives = "49080126-0e18-4c2a-b176-c102e4b3760c" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" StyledStrings = "f489334b-da3d-4c2e-b8f0-e476e12c162b" - [extensions] -StyledStringsSstsExt = "StyledStrings" FileIOExt = "FileIO" +StyledStringsSstsExt = "StyledStrings" [compat] CSV = "0.10.15" @@ -33,6 +32,7 @@ Colors = "0.12, 0.13" Distributions = "0.25.0" FileIO = "1" OrderedCollections = "1" +Pkg = "1.12.1" PrecompileTools = "1" StyledStrings = "1.0.3" Tables = "1" @@ -46,9 +46,10 @@ CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" StyledStrings = "f489334b-da3d-4c2e-b8f0-e476e12c162b" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["CSV", "DataFrames", "Distributions", "FileIO", "Random", "StyledStrings", "Test"] +test = ["CSV", "DataFrames", "Distributions", "FileIO", "Pkg", "Random", "StyledStrings", "Test"] diff --git a/src/read.jl b/src/read.jl index caf4b302..37ff9bc8 100644 --- a/src/read.jl +++ b/src/read.jl @@ -1544,7 +1544,7 @@ Read tabular data from an Excel file, `source`, and return it as a `Tables.jl` c The resulting table object can be passed directly to any function that accepts `Tables.jl` data (e.g. `DataFrame` from package `DataFrames.jl`). -This function requires FileIO.jl to be active in the current environment. +This function requires both FileIO.jl v1.20.0 or higher to be active in the current environment and a Julia version >= v1.9. #### Arguments: @@ -1600,7 +1600,7 @@ end ``` Save a `Tables.jl` compatible table to an Excel file, `source`. -This function requires FileIO.jl to be active in the current environment. +This function requires both FileIO.jl v1.20.0 or higher to be active in the current environment and a Julia version >= v1.9. #### Arguments: diff --git a/test/runtests.jl b/test/runtests.jl index c3c1c036..4f3169b1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -7796,6 +7796,162 @@ end end end +using Pkg +using FileIO + +@static if VERSION >= v"1.9-" + + if Pkg.pkgversion(FileIO) >= v"1.20.0" + + @testset "FileIO" verbose=true begin + + filename = joinpath(data_directory, "TestData.xlsx") + + efile = load(filename, "Sheet1") + + @test Tables.istable(efile) == true # Defined in XLSX.jl + + # Test show renders expected number of rows and columns. + @testset "show plain text" begin + s = sprint(show, efile) + @test s == "XLSX.DataTable with 13 columns and 4 rows." + end + + @testset "ReadTable" begin + for source in [load(filename, "Sheet1", "C:O"; first_row=3), load(filename, "Sheet1")] + df, names = get_cols(source) + @test length(df) == 13 + @test length(df[1]) == 4 + + @test df[1] == [1., 1.5, 2., 2.5] + @test df[2] == ["A", "BB", "CCC", "DDDD"] + @test df[3] == [true, false, false, true] + @test isequal(df[4], [2, "EEEEE", false, 1.5]) + @test isequal(df[5], [9., "III", missing, true]) + @test isequal(df[6], [3., missing, 3.5, 4.]) + @test isequal(df[7], ["FF", missing, "GGG", "HHHH"]) + @test isequal(df[8], [missing, true, missing, false]) + @test df[9] == [Date(2015, 3, 3), DateTime(2015, 2, 4, 10, 14), Date(1988, 4, 9), Dates.Time(15, 2, 0)] + @test isequal(df[10], [Date(1965, 4, 3), DateTime(1950, 8, 9, 18, 40), Dates.Time(19, 0, 0), missing]) + @test all(ismissing, df[11]) + @test isequal(df[12], [missing, missing, missing, missing]) + @test isequal(df[13], [missing, 3.4, "HKEJW", missing]) + end + + df, names = get_cols(load(filename, "Sheet1", "C:O"; first_row=4, header=false)) + @test names == [:C, :D, :E, :F, :G, :H, :I, :J, :K, :L, :M, :N, :O] + @test length(df[1]) == 4 + @test length(df) == 13 + @test df[1] == [1., 1.5, 2., 2.5] + @test df[2] == ["A", "BB", "CCC", "DDDD"] + @test df[3] == [true, false, false, true] + @test isequal(df[4], [2, "EEEEE", false, 1.5]) + @test isequal(df[5], [9., "III", missing, true]) + @test isequal(df[6], [3., missing, 3.5, 4.]) + @test isequal(df[7], ["FF", missing, "GGG", "HHHH"]) + @test isequal(df[8], [missing, true, missing, false]) + @test df[9] == [Date(2015, 3, 3), DateTime(2015, 2, 4, 10, 14), DateTime(1988, 4, 9), Dates.Time(15, 2, 0)] + @test isequal(df[10], [Date(1965, 4, 3), DateTime(1950, 8, 9, 18, 40), Dates.Time(19, 0, 0), missing]) + @test all(ismissing, df[11]) + @test all(ismissing, df[12]) + @test isequal(df[13], [missing, 3.4, "HKEJW", missing]) + @test ismissing(df[12][4]) + + good_colnames = [:c1, :c2, :c3, :c4, :c5, :c6, :c7, :c8, :c9, :c10, :c11, :c12, :c13] + + df, names = get_cols(load(filename, "Sheet1", "C:O"; first_row=4, header=false, column_labels=good_colnames)) + @test names == good_colnames + @test length(df[1]) == 4 + @test length(df) == 13 + @test df[1] == [1., 1.5, 2., 2.5] + @test df[2] == ["A", "BB", "CCC", "DDDD"] + @test df[3] == [true, false, false, true] + @test isequal(df[4], [2, "EEEEE", false, 1.5]) + @test isequal(df[5], [9., "III", missing, true]) + @test isequal(df[6], [3., missing, 3.5, 4.]) + @test isequal(df[7], ["FF", missing, "GGG", "HHHH"]) + @test isequal(df[8], [missing, true, missing, false]) + @test df[9] == [Date(2015, 3, 3), DateTime(2015, 2, 4, 10, 14), DateTime(1988, 4, 9), Dates.Time(15, 2, 0)] + @test isequal(df[10], [Date(1965, 4, 3), DateTime(1950, 8, 9, 18, 40), Dates.Time(19, 0, 0), missing]) + @test all(ismissing, df[11]) + @test all(ismissing, df[12]) + @test isequal(df[13], [missing, 3.4, "HKEJW", missing]) + @test ismissing(df[12][4]) + + # Test for saving DataFrame to XLSX + input = (Day = ["Nov. 27", "Nov. 28", "Nov. 29"], Highest = [78, 79, 75]) |> DataFrames.DataFrame + save("file.xlsx", input) + output = load("file.xlsx", "Sheet1") |> DataFrames.DataFrame + @test input == output + rm("file.xlsx") + + # Test for saving DataFrame to XLSX with sheetname keyword + input = (Day = ["Nov. 27", "Nov. 28", "Nov. 29"], Highest = [78, 79, 75]) |> DataFrames.DataFrame + save("file.xlsx", input, sheetname="SheetName") + output = load("file.xlsx", "SheetName") |> DataFrames.DataFrame + @test input == output + rm("file.xlsx") + + df, names = get_cols(load(filename, "Sheet1"; column_labels=good_colnames)) + @test names == good_colnames + @test length(df[1]) == 4 + @test length(df) == 13 + @test df[1] == [1., 1.5, 2., 2.5] + @test df[2] == ["A", "BB", "CCC", "DDDD"] + @test df[3] == [true, false, false, true] + @test isequal(df[4], [2, "EEEEE", false, 1.5]) + @test isequal(df[5], [9., "III", missing, true]) + @test isequal(df[6], [3., missing, 3.5, 4.]) + @test isequal(df[7], ["FF", missing, "GGG", "HHHH"]) + @test isequal(df[8], [missing, true, missing, false]) + @test df[9] == [Date(2015, 3, 3), DateTime(2015, 2, 4, 10, 14), DateTime(1988, 4, 9), Dates.Time(15, 2, 0)] + @test isequal(df[10], [Date(1965, 4, 3), DateTime(1950, 8, 9, 18, 40), Dates.Time(19, 0, 0), missing]) + @test all(ismissing, df[11]) + @test all(ismissing, df[12]) + @test isequal(df[13], [missing, 3.4, "HKEJW", missing]) + @test ismissing(df[12][4]) + + # Too few column labels - Note: Bypass FileIO here to avoid false "Fatal Error" from FileIO when the error is correctly thrown by ExcelFiles for mismatched column_labels length. + try + XLSX.load(File{FileIO.format"Excel"}(filename), "Sheet1", "C:O"; header=true, column_labels=[:c1, :c2, :c3, :c4]) + @test false # should error before this line + catch e + @test e isa XLSX.XLSXError && occursin("`column_range` (length=13) and `column_labels` (length=4) must have the same length.", e.msg) + end + + # Test for constructing DataFrame with empty header cell + data, names = get_cols(load(filename, "Sheet2", "C:E")) + @test names == [:Col1, Symbol("#Empty"), :Col3] + + # normalizenames keyword (XLSX.jl v0.11 only) + data, names = get_cols(load(filename, "Sheet2", "C:E"; normalizenames=true)) + @test names == [:Col1, :_Empty, :Col3] + end + @testset "Transposed tables" begin + # Note: readtransposedtable cannot handle entirely empty rows/columns, + # so the Transpose sheet omits those from the original Sheet1 data. + # Note: eltype of mixed date columns is Dates.TimeType (not Any) when + # there are no missing values, since a common supertype can be inferred. + + df, names = get_cols(load(filename, "Transpose"; transpose=true, first_column=2)) + @test length(df) == 5 + @test length(df[1]) == 4 + @test names == [Symbol("Some Float64s"), Symbol("Some Strings"), Symbol("Some Bools"), Symbol("Mixed with NA"), Symbol("Some dates")] + + @test df[1] == [1.0, 1.5, 2.0, 2.5] + @test df[2] == ["A", "BB", "CCC", "DDDD"] + @test df[3] == Bool[true, false, false, true] + @test isequal(df[4], Any[9, "III", missing, true]) + @test df[5] == Dates.TimeType[Date(2015, 3, 3), DateTime(2015, 2, 4, 10, 14), Date(1988, 4, 9), Dates.Time(15, 2, 0)] + end + + end + else + @info "Skipping FileIO tests (requires FileIO > v1.19.0, got $(pkgversion(FileIO)))" + end +end + +#= using FileIO @testset "FileIO" verbose=true begin @@ -7941,4 +8097,6 @@ using FileIO @test isequal(df[4], Any[9, "III", missing, true]) @test df[5] == Dates.TimeType[Date(2015, 3, 3), DateTime(2015, 2, 4, 10, 14), Date(1988, 4, 9), Dates.Time(15, 2, 0)] end -end \ No newline at end of file + +end +=# \ No newline at end of file From 009d5ff895a4667d42d9805231b12f62329e0b29 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 2 Jun 2026 13:10:37 +0100 Subject: [PATCH 06/13] Remove references to XLSXReader and ExcelFiles --- README.md | 5 ----- src/read.jl | 12 ++++++++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 36da5b39..16251d16 100644 --- a/README.md +++ b/README.md @@ -84,12 +84,7 @@ and send a Pull Request. ## Alternative Packages -* [ExcelFiles.jl](https://github.com/davidanthoff/ExcelFiles.jl) - * [FileIO.jl](https://github.com/JuliaIO/FileIO.jl) * [LibXLSXWriter.jl](https://github.com/jaakkor2/LibXLSXWriter.jl) -* [Taro.jl](https://github.com/aviks/Taro.jl) - -* [XLSXReader.jl](https://github.com/mpastell/XLSXReader.jl) diff --git a/src/read.jl b/src/read.jl index 37ff9bc8..c308cab8 100644 --- a/src/read.jl +++ b/src/read.jl @@ -1552,6 +1552,11 @@ This function requires both FileIO.jl v1.20.0 or higher to be active in the curr * `sheet`: Specifies the sheet name to be loaded. If `sheet` is not given, the first Excel sheet in the file will be used. * `columns`: Determines which columns to read. 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. A valid sheet **must** be specified when specifying columns. If `transpose = true` or is omitted, `columns` should be used to specify rows. For example, specifying `"2:4"` with `transpose = true` will read only from these rows. +!!! note + + The file extension provided in `source` must be `.xlsx`, `.xltx`, `.xlsm`, + or `.xltm` for FileIO to recognize the file format as an Excel file. + #### Keywords: * `first_row`: Indicates the first row of the data table to be read. For example, `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 sheet (ignored if `transpose = true`). @@ -1606,6 +1611,13 @@ This function requires both FileIO.jl v1.20.0 or higher to be active in the curr * `source`: The name of the file to be created on save. +!!! note + + The file extension provided in `source` must be `.xlsx`, `.xltx`, `.xlsm`, + or `.xltm` for FileIO to recognize the file format as an Excel file. The + file created will be a standard workbook (ie not an Excel template nor a + macro-enabled workbook) regardless of which of these four extensions is used. + #### Keywords: * `sheetname`: Specify the sheetname to be used in the created file. By default, the sheetname will be `Sheet1`. From c71b0312e0aedd4d50d2e14c248fd84648a7caf3 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 2 Jun 2026 13:25:59 +0100 Subject: [PATCH 07/13] Minor adjustments to FileIO docs. --- docs/src/tutorial/FileIOtutorial.md | 15 ++- ext/FileIOExt.jl | 187 +--------------------------- src/read.jl | 4 +- 3 files changed, 13 insertions(+), 193 deletions(-) diff --git a/docs/src/tutorial/FileIOtutorial.md b/docs/src/tutorial/FileIOtutorial.md index 51cd1080..5205b7f7 100644 --- a/docs/src/tutorial/FileIOtutorial.md +++ b/docs/src/tutorial/FileIOtutorial.md @@ -2,13 +2,16 @@ ## Introduction -A package extension to XLSX.jl provides support for Excel -files under the [FileIO.jl](https://github.com/JuliaIO/FileIO.jl) package. +A package extension to XLSX.jl provides support for Excel files +under the [FileIO.jl](https://github.com/JuliaIO/FileIO.jl) package. -Through [FileIO.jl](https://github.com/JuliaIO/FileIO.jl), -you can read simple tabular data from an Excel (.xlsx) file and save -tabular data to an Excel file using simple `load` and `save` -functions without needing to know anything about XLSX.jl itself. +[FileIO.jl](https://github.com/JuliaIO/FileIO.jl) aims to provide a common +framework for detecting file formats and dispatching to appropriate readers/writers. + +Through [FileIO.jl](https://github.com/JuliaIO/FileIO.jl), you can read +simple tabular data from an Excel (.xlsx) file and save tabular data +to an Excel file using simple `load` and `save` functions without needing +to know anything about XLSX.jl itself. XLSX.jl provides much more extensive functionality if you need it. Check out the rest of the documentation for full details. diff --git a/ext/FileIOExt.jl b/ext/FileIOExt.jl index 15eb95f8..7937e595 100644 --- a/ext/FileIOExt.jl +++ b/ext/FileIOExt.jl @@ -1,190 +1,5 @@ -""" -# Introduction - -This package provides support for Excel files under the -[FileIO.jl](https://github.com/JuliaIO/FileIO.jl) package. - -It provides functionality to read simple tabular data from -an Excel (.xlsx) file and to save simple tabular data to an -Excel file. - -For more extensive functionality when reading and writing Excel files, -consider using [XLSX.jl](https://juliadata.github.io/XLSX.jl/stable/). -Under the hood, `ExcelFiles.jl` uses the `XLSX.jl` functions `readtable` -and `writetable`. - -# Usage - -## Load an Excel file - -To read an Excel file into a `DataFrame`, use the following julia code: - -```julia -using ExcelFiles, DataFrames - -df = DataFrame(load("data.xlsx", "Sheet1")) -``` - -The call to `load` returns an object that is a [Tables.jl](https://github.com/JuliaData/Tables.jl) table, so it can be passed to any function that can handle Tables.jl tables. Here are some examples of materializing an Excel file into such data structures: - -```julia -using ExcelFiles, DataFrames, PrettyTables - -# Load into a DataFrame -julia> DataFrame(load("HTable.xlsx")) -5×10 DataFrame - Row │ Year 1940 1950 1960 1970 1980 1990 2000 2010 2020 - │ String Any Any Float64 Float64 Any Any Float64 Float64 Float64 -─────┼─────────────────────────────────────────────────────────────────────────────────────────── - 1 │ Col A 1 2 3.0 4.0 5 6 7.0 8.0 9.0 - 2 │ Col B 10 20 30.0 40.0 50 60 70.0 80.0 90.0 - 3 │ Col C 100 200 300.0 400.0 500 600 700.0 800.0 900.0 - 4 │ Col D 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 - 5 │ Col E Hello 2025-12-19 3.0 3.33 Hello 2025-12-19 3.0 3.33 1.0 - -julia> DataFrame(load("HTable.xlsx"; transpose=true)) -9×6 DataFrame - Row │ Year Col A Col B Col C Col D Col E - │ Int64 Int64 Int64 Int64 Float64 Any -─────┼───────────────────────────────────────────────── - 1 │ 1940 1 10 100 0.1 Hello - 2 │ 1950 2 20 200 0.2 2025-12-19 - 3 │ 1960 3 30 300 0.3 3 - 4 │ 1970 4 40 400 0.4 3.33 - 5 │ 1980 5 50 500 0.5 Hello - 6 │ 1990 6 60 600 0.6 2025-12-19 - 7 │ 2000 7 70 700 0.7 3 - 8 │ 2010 8 80 800 0.8 3.33 - 9 │ 2020 9 90 900 0.9 true - - -# Load into a PrettyTable -julia> PrettyTable(load("HTable.xlsx")) -┌───────┬───────┬────────────┬───────┬───────┬───────┬────────────┬───────┬───────┬───────┐ -│ Year │ 1940 │ 1950 │ 1960 │ 1970 │ 1980 │ 1990 │ 2000 │ 2010 │ 2020 │ -├───────┼───────┼────────────┼───────┼───────┼───────┼────────────┼───────┼───────┼───────┤ -│ Col A │ 1 │ 2 │ 3.0 │ 4.0 │ 5 │ 6 │ 7.0 │ 8.0 │ 9.0 │ -│ Col B │ 10 │ 20 │ 30.0 │ 40.0 │ 50 │ 60 │ 70.0 │ 80.0 │ 90.0 │ -│ Col C │ 100 │ 200 │ 300.0 │ 400.0 │ 500 │ 600 │ 700.0 │ 800.0 │ 900.0 │ -│ Col D │ 0.1 │ 0.2 │ 0.3 │ 0.4 │ 0.5 │ 0.6 │ 0.7 │ 0.8 │ 0.9 │ -│ Col E │ Hello │ 2025-12-19 │ 3.0 │ 3.33 │ Hello │ 2025-12-19 │ 3.0 │ 3.33 │ 1.0 │ -└───────┴───────┴────────────┴───────┴───────┴───────┴────────────┴───────┴───────┴───────┘ - -julia> PrettyTable(load("HTable.xlsx"; transpose=true)) -┌──────┬───────┬───────┬───────┬───────┬────────────┐ -│ Year │ Col A │ Col B │ Col C │ Col D │ Col E │ -├──────┼───────┼───────┼───────┼───────┼────────────┤ -│ 1940 │ 1 │ 10 │ 100 │ 0.1 │ Hello │ -│ 1950 │ 2 │ 20 │ 200 │ 0.2 │ 2025-12-19 │ -│ 1960 │ 3 │ 30 │ 300 │ 0.3 │ 3 │ -│ 1970 │ 4 │ 40 │ 400 │ 0.4 │ 3.33 │ -│ 1980 │ 5 │ 50 │ 500 │ 0.5 │ Hello │ -│ 1990 │ 6 │ 60 │ 600 │ 0.6 │ 2025-12-19 │ -│ 2000 │ 7 │ 70 │ 700 │ 0.7 │ 3 │ -│ 2010 │ 8 │ 80 │ 800 │ 0.8 │ 3.33 │ -│ 2020 │ 9 │ 90 │ 900 │ 0.9 │ true │ -└──────┴───────┴───────┴───────┴───────┴────────────┘ - -``` - -The `load` function takes a number of arguments and keywords: - -```julia - FileIO.load( - source::String, - [sheet::String, - [columns::String]]; - [first_row::Int], - [first_column::String] - [column_labels::Vector{String}], - [header::Bool], - [normalizenames::Bool], - [transpose::Bool] - ) -``` - -### Arguments: - -* `source`: The name of the file to be loaded. -* `sheet`: Specifies the sheet name to be loaded. If `sheet` is not given, the first Excel sheet in the file will be used. -* `columns`: Determines which columns to read. 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. A valid sheet **must** be specified when specifying columns. If `transpose = true` or is omitted, `columns` should be used to specify rows. For example, specifying `"2:4"` with `transpose = true` will read only from these rows. - -### Keywords: - -* `first_row`: Indicates the first row of the data table to be read. For example, `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 sheet (ignored if `transpose = true`). -* `first_column`: Indicates the first row of the data table to be read. For example, `first_column="B"` 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 sheet (ignored if `transpose = false` or is omitted). -* `column_labels`: Specifies column names for the header of the table. If `column_labels` are given and `header=true`, the headers given by `column_labels` will be used, and the first row of the table (containing headers) will be ignored. -* `header`: Indicates if the first row (column if `transpose = true`) is a header. If `header=true` and `column_labels` is not specified, the column labels for the table will be read from the first row (column) of the table. If `header=false` and `column_labels` is not specified, the algorithm will generate column labels. The default value is `header=true`. -* `normalizenames`: Set to `true` to normalize column names to valid Julia identifiers. Default=`false`. -* `transpose`: Set to `true` to transpose the table to read data from rows not columns. - -### Examples - -```julia -julia> PrettyTable(load("HTable.xlsx", "Offset"; first_row=2)) - -julia> df = DataFrame(load("HTable.xlsx", "Offset", "2:7"; transpose=true, first_column="B")) - -julia> df = DataFrame(load("HTable.xlsx"; normalizenames=true, transpose=true, column_labels=["Date", "Name1", "Name2", "Name3", "Name4", "Name5"])) - -``` -## Save an Excel file - -The following code saves any Tables.jl table (such as a `DataFrame`) as an Excel file: -```julia -using ExcelFiles - -save("output.xlsx", tbl) -``` - -The `save` function takes a number of arguments and keywords: - -```julia - FileIO.save( - source::String; - [sheetname::String], - [overwrite::Bool] - ) -``` - -### Arguments: - -* `source`: The name of the file to be created on save. - -### Keywords: - -* `sheetname`: Specify the sheetname to be used in the created file. By default, the sheetname will be `Sheet1`. -* `overwrite`: Set `overwrite=true` to overwite any existing file of the same name. Default = `false`. - -### Examples - -```julia -julia> save("myfile.xlsx", df; sheetname="myname", overwrite=true) -``` - -## Using the pipe syntax - -The `load` and `save` functions also support the pipe syntax. For example, to load an Excel file into a `DataFrame`, one can use the following code: - -```julia -using ExcelFiles, DataFrame - -df = load("data.xlsx", "Sheet1") |> DataFrame -``` - -To save any Tables.jl compatible table (such as a DataFrame), one can use the following form: - -```julia -using ExcelFiles, DataFrame - -df = # Aquire a DataFrame somehow - -df |> save("output.xlsx") -``` -""" - module FileIOExt -# Provides hooks for FileIO.jl to load XLSX files. This is not a dependency of XLSX.jl, so that users who do not want to use FileIO.jl can avoid installing it. +# Provides hooks for FileIO.jl to save and load XLSX files. using FileIO diff --git a/src/read.jl b/src/read.jl index c308cab8..e5d8cd23 100644 --- a/src/read.jl +++ b/src/read.jl @@ -1598,7 +1598,8 @@ end """ ```julia FileIO.save( - source::String; + source::String, + data; [sheetname::String], [overwrite::Bool] ) @@ -1610,6 +1611,7 @@ This function requires both FileIO.jl v1.20.0 or higher to be active in the curr #### Arguments: * `source`: The name of the file to be created on save. +* `data`: A `Tables.jl` compatible table to be saved to the file. For example, a `DataFrame` from package `DataFrames.jl`. !!! note From 382ff0ea0edcaa0fd039a652050f4c0122ddbf6e Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Tue, 2 Jun 2026 18:31:36 +0100 Subject: [PATCH 08/13] Test FileIO Excel template and macro file support --- docs/src/tutorial/FileIOtutorial.md | 10 +- test/runtests.jl | 169 +++------------------------- 2 files changed, 23 insertions(+), 156 deletions(-) diff --git a/docs/src/tutorial/FileIOtutorial.md b/docs/src/tutorial/FileIOtutorial.md index 5205b7f7..5d62f9f0 100644 --- a/docs/src/tutorial/FileIOtutorial.md +++ b/docs/src/tutorial/FileIOtutorial.md @@ -33,7 +33,7 @@ julia> Pkg.add(["FileIO", "XLSX"]) To read an Excel file into a `DataFrame`, use the following julia code: ```julia -using FileIO, XLSX, DataFrames +using FileIO, DataFrames df = DataFrame(load("data.xlsx", "Sheet1")) ``` @@ -41,7 +41,7 @@ df = DataFrame(load("data.xlsx", "Sheet1")) The call to `load` returns an object that is a [Tables.jl](https://github.com/JuliaData/Tables.jl) table, so it can be passed to any function that can handle Tables.jl tables. Here are some examples of materializing an Excel file into such data structures: ```julia -using FileIO, XLSX, DataFrames, PrettyTables +using FileIO, DataFrames, PrettyTables # Load into a DataFrame julia> DataFrame(load("HTable.xlsx")) @@ -107,7 +107,7 @@ For more information, see [`XLSX.load`](@ref) The following code saves any Tables.jl table (such as a `DataFrame`) as an Excel file: ```julia -using FileIO, XLSX, +using FileIO save("output.xlsx", myTable) ``` @@ -119,7 +119,7 @@ For more information, see [`XLSX.save`](@ref) The `load` and `save` functions also support the pipe syntax. For example, to load an Excel file into a `DataFrame`, one can use the following code: ```julia -using FileIO, XLSX, DataFrame +using FileIO, DataFrame df = load("data.xlsx", "Sheet1") |> DataFrame ``` @@ -127,7 +127,7 @@ df = load("data.xlsx", "Sheet1") |> DataFrame To save any Tables.jl compatible table (such as a DataFrame), one can use the following form: ```julia -using FileIO, XLSX, DataFrame +using FileIO, DataFrame df = # Aquire a DataFrame somehow diff --git a/test/runtests.jl b/test/runtests.jl index 4f3169b1..c7f9585a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -7817,7 +7817,7 @@ using FileIO @test s == "XLSX.DataTable with 13 columns and 4 rows." end - @testset "ReadTable" begin + @testset "read table" begin for source in [load(filename, "Sheet1", "C:O"; first_row=3), load(filename, "Sheet1")] df, names = get_cols(source) @test length(df) == 13 @@ -7927,7 +7927,7 @@ using FileIO data, names = get_cols(load(filename, "Sheet2", "C:E"; normalizenames=true)) @test names == [:Col1, :_Empty, :Col3] end - @testset "Transposed tables" begin + @testset "transposed tables" begin # Note: readtransposedtable cannot handle entirely empty rows/columns, # so the Transpose sheet omits those from the original Sheet1 data. # Note: eltype of mixed date columns is Dates.TimeType (not Any) when @@ -7945,158 +7945,25 @@ using FileIO @test df[5] == Dates.TimeType[Date(2015, 3, 3), DateTime(2015, 2, 4, 10, 14), Date(1988, 4, 9), Dates.Time(15, 2, 0)] end + @testset "template and macro files" begin + tbl = load(joinpath(data_directory, "Template File.xltx"), "Sheet1", "K:N") + @test length(tbl.data) == 4 + @test length(tbl.data[1]) == 9 + tbl = load(joinpath(data_directory, "macro-enabled2.xltm")) + @test length(tbl.data) == 1 + @test length(tbl.data[1]) == 0 + @test tbl.column_labels == [:hello] + tbl = load(joinpath(data_directory, "macro-enabled.xlsm")) + @test length(tbl.data) == 1 + @test length(tbl.data[1]) == 0 + @test tbl.column_labels == [:hello] + end + end else @info "Skipping FileIO tests (requires FileIO > v1.19.0, got $(pkgversion(FileIO)))" end +else + @info "Skipping FileIO tests (requires Julia > v1.9, got $(VERSION))" end -#= -using FileIO - -@testset "FileIO" verbose=true begin - - filename = joinpath(data_directory, "TestData.xlsx") - - efile = load(filename, "Sheet1") - - @test Tables.istable(efile) == true # Defined in XLSX.jl - - # Test show renders expected number of rows and columns. - @testset "show plain text" begin - s = sprint(show, efile) - @test s == "XLSX.DataTable with 13 columns and 4 rows." - end - - @testset "ReadTable" begin - for source in [load(filename, "Sheet1", "C:O"; first_row=3), load(filename, "Sheet1")] - df, names = get_cols(source) - @test length(df) == 13 - @test length(df[1]) == 4 - - @test df[1] == [1., 1.5, 2., 2.5] - @test df[2] == ["A", "BB", "CCC", "DDDD"] - @test df[3] == [true, false, false, true] - @test isequal(df[4], [2, "EEEEE", false, 1.5]) - @test isequal(df[5], [9., "III", missing, true]) - @test isequal(df[6], [3., missing, 3.5, 4.]) - @test isequal(df[7], ["FF", missing, "GGG", "HHHH"]) - @test isequal(df[8], [missing, true, missing, false]) - @test df[9] == [Date(2015, 3, 3), DateTime(2015, 2, 4, 10, 14), Date(1988, 4, 9), Dates.Time(15, 2, 0)] - @test isequal(df[10], [Date(1965, 4, 3), DateTime(1950, 8, 9, 18, 40), Dates.Time(19, 0, 0), missing]) - @test all(ismissing, df[11]) - @test isequal(df[12], [missing, missing, missing, missing]) - @test isequal(df[13], [missing, 3.4, "HKEJW", missing]) - end - - df, names = get_cols(load(filename, "Sheet1", "C:O"; first_row=4, header=false)) - @test names == [:C, :D, :E, :F, :G, :H, :I, :J, :K, :L, :M, :N, :O] - @test length(df[1]) == 4 - @test length(df) == 13 - @test df[1] == [1., 1.5, 2., 2.5] - @test df[2] == ["A", "BB", "CCC", "DDDD"] - @test df[3] == [true, false, false, true] - @test isequal(df[4], [2, "EEEEE", false, 1.5]) - @test isequal(df[5], [9., "III", missing, true]) - @test isequal(df[6], [3., missing, 3.5, 4.]) - @test isequal(df[7], ["FF", missing, "GGG", "HHHH"]) - @test isequal(df[8], [missing, true, missing, false]) - @test df[9] == [Date(2015, 3, 3), DateTime(2015, 2, 4, 10, 14), DateTime(1988, 4, 9), Dates.Time(15, 2, 0)] - @test isequal(df[10], [Date(1965, 4, 3), DateTime(1950, 8, 9, 18, 40), Dates.Time(19, 0, 0), missing]) - @test all(ismissing, df[11]) - @test all(ismissing, df[12]) - @test isequal(df[13], [missing, 3.4, "HKEJW", missing]) - @test ismissing(df[12][4]) - - good_colnames = [:c1, :c2, :c3, :c4, :c5, :c6, :c7, :c8, :c9, :c10, :c11, :c12, :c13] - - df, names = get_cols(load(filename, "Sheet1", "C:O"; first_row=4, header=false, column_labels=good_colnames)) - @test names == good_colnames - @test length(df[1]) == 4 - @test length(df) == 13 - @test df[1] == [1., 1.5, 2., 2.5] - @test df[2] == ["A", "BB", "CCC", "DDDD"] - @test df[3] == [true, false, false, true] - @test isequal(df[4], [2, "EEEEE", false, 1.5]) - @test isequal(df[5], [9., "III", missing, true]) - @test isequal(df[6], [3., missing, 3.5, 4.]) - @test isequal(df[7], ["FF", missing, "GGG", "HHHH"]) - @test isequal(df[8], [missing, true, missing, false]) - @test df[9] == [Date(2015, 3, 3), DateTime(2015, 2, 4, 10, 14), DateTime(1988, 4, 9), Dates.Time(15, 2, 0)] - @test isequal(df[10], [Date(1965, 4, 3), DateTime(1950, 8, 9, 18, 40), Dates.Time(19, 0, 0), missing]) - @test all(ismissing, df[11]) - @test all(ismissing, df[12]) - @test isequal(df[13], [missing, 3.4, "HKEJW", missing]) - @test ismissing(df[12][4]) - - # Test for saving DataFrame to XLSX - input = (Day = ["Nov. 27", "Nov. 28", "Nov. 29"], Highest = [78, 79, 75]) |> DataFrames.DataFrame - save("file.xlsx", input) - output = load("file.xlsx", "Sheet1") |> DataFrames.DataFrame - @test input == output - rm("file.xlsx") - - # Test for saving DataFrame to XLSX with sheetname keyword - input = (Day = ["Nov. 27", "Nov. 28", "Nov. 29"], Highest = [78, 79, 75]) |> DataFrames.DataFrame - save("file.xlsx", input, sheetname="SheetName") - output = load("file.xlsx", "SheetName") |> DataFrames.DataFrame - @test input == output - rm("file.xlsx") - - df, names = get_cols(load(filename, "Sheet1"; column_labels=good_colnames)) - @test names == good_colnames - @test length(df[1]) == 4 - @test length(df) == 13 - @test df[1] == [1., 1.5, 2., 2.5] - @test df[2] == ["A", "BB", "CCC", "DDDD"] - @test df[3] == [true, false, false, true] - @test isequal(df[4], [2, "EEEEE", false, 1.5]) - @test isequal(df[5], [9., "III", missing, true]) - @test isequal(df[6], [3., missing, 3.5, 4.]) - @test isequal(df[7], ["FF", missing, "GGG", "HHHH"]) - @test isequal(df[8], [missing, true, missing, false]) - @test df[9] == [Date(2015, 3, 3), DateTime(2015, 2, 4, 10, 14), DateTime(1988, 4, 9), Dates.Time(15, 2, 0)] - @test isequal(df[10], [Date(1965, 4, 3), DateTime(1950, 8, 9, 18, 40), Dates.Time(19, 0, 0), missing]) - @test all(ismissing, df[11]) - @test all(ismissing, df[12]) - @test isequal(df[13], [missing, 3.4, "HKEJW", missing]) - @test ismissing(df[12][4]) - - # Too few column labels - Note: Bypass FileIO here to avoid false "Fatal Error" from FileIO when the error is correctly thrown by ExcelFiles for mismatched column_labels length. - try - XLSX.load(File{FileIO.format"Excel"}(filename), "Sheet1", "C:O"; header=true, column_labels=[:c1, :c2, :c3, :c4]) - @test false # should error before this line - catch e - @test e isa XLSX.XLSXError && occursin("`column_range` (length=13) and `column_labels` (length=4) must have the same length.", e.msg) - end - - # Test for constructing DataFrame with empty header cell - data, names = get_cols(load(filename, "Sheet2", "C:E")) - @test names == [:Col1, Symbol("#Empty"), :Col3] - - # normalizenames keyword (XLSX.jl v0.11 only) - data, names = get_cols(load(filename, "Sheet2", "C:E"; normalizenames=true)) - @test names == [:Col1, :_Empty, :Col3] - - end - - @testset "Transposed tables" begin - # Note: readtransposedtable cannot handle entirely empty rows/columns, - # so the Transpose sheet omits those from the original Sheet1 data. - # Note: eltype of mixed date columns is Dates.TimeType (not Any) when - # there are no missing values, since a common supertype can be inferred. - - df, names = get_cols(load(filename, "Transpose"; transpose=true, first_column=2)) - @test length(df) == 5 - @test length(df[1]) == 4 - @test names == [Symbol("Some Float64s"), Symbol("Some Strings"), Symbol("Some Bools"), Symbol("Mixed with NA"), Symbol("Some dates")] - - @test df[1] == [1.0, 1.5, 2.0, 2.5] - @test df[2] == ["A", "BB", "CCC", "DDDD"] - @test df[3] == Bool[true, false, false, true] - @test isequal(df[4], Any[9, "III", missing, true]) - @test df[5] == Dates.TimeType[Date(2015, 3, 3), DateTime(2015, 2, 4, 10, 14), Date(1988, 4, 9), Dates.Time(15, 2, 0)] - end - -end -=# \ No newline at end of file From b0b8905e0149e0735923bec75e68a937ab70c0f5 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Wed, 3 Jun 2026 07:39:21 +0100 Subject: [PATCH 09/13] Revisions to docs for FileIO support --- docs/src/api/files.md | 6 ++++++ docs/src/tutorial/FileIOtutorial.md | 7 +++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/src/api/files.md b/docs/src/api/files.md index 0173faa3..dd5e6feb 100644 --- a/docs/src/api/files.md +++ b/docs/src/api/files.md @@ -14,6 +14,12 @@ XLSX.savexlsx ``` ## Files (using FileIO) + +!!! note + + These functions extend `FileIO.load` and `FileIO.save`. Call them as + `FileIO.load(...)` and `FileIO.save(...)` after doing `using FileIO`. + ```@docs XLSX.load XLSX.save diff --git a/docs/src/tutorial/FileIOtutorial.md b/docs/src/tutorial/FileIOtutorial.md index 5d62f9f0..b770ee0a 100644 --- a/docs/src/tutorial/FileIOtutorial.md +++ b/docs/src/tutorial/FileIOtutorial.md @@ -38,7 +38,9 @@ using FileIO, DataFrames df = DataFrame(load("data.xlsx", "Sheet1")) ``` -The call to `load` returns an object that is a [Tables.jl](https://github.com/JuliaData/Tables.jl) table, so it can be passed to any function that can handle Tables.jl tables. Here are some examples of materializing an Excel file into such data structures: +The call to `load` returns an object that is a [Tables.jl](https://github.com/JuliaData/Tables.jl) table, +so it can be passed to any function that can handle Tables.jl tables. Here are some examples of +materializing an Excel file into such data structures: ```julia using FileIO, DataFrames, PrettyTables @@ -116,7 +118,8 @@ For more information, see [`XLSX.save`](@ref) ### Using the pipe syntax -The `load` and `save` functions also support the pipe syntax. For example, to load an Excel file into a `DataFrame`, one can use the following code: +The `load` and `save` functions also support the pipe syntax. For example, to load an +Excel file into a `DataFrame`, one can use the following code: ```julia using FileIO, DataFrame From 9abd74934153f419e7219684eddc382c98e7b16f Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Wed, 3 Jun 2026 08:50:05 +0100 Subject: [PATCH 10/13] update version restrictions in FileIO tests. --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index c7f9585a..f6d62d8b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -7801,7 +7801,7 @@ using FileIO @static if VERSION >= v"1.9-" - if Pkg.pkgversion(FileIO) >= v"1.20.0" + if Pkg.pkgversion(FileIO) > v"1.19.0" @testset "FileIO" verbose=true begin From 969b16c1fce1fb96bc0b9dd76017e8e25d4a0c68 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Wed, 3 Jun 2026 09:17:38 +0100 Subject: [PATCH 11/13] update compats on Pkg (used for tests). --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 95734b37..8db6a7d9 100644 --- a/Project.toml +++ b/Project.toml @@ -32,7 +32,7 @@ Colors = "0.12, 0.13" Distributions = "0.25.0" FileIO = "1" OrderedCollections = "1" -Pkg = "1.12.1" +Pkg = "1" PrecompileTools = "1" StyledStrings = "1.0.3" Tables = "1" From a98dd448cbcc0ee9a962ca00c3198bdc7802d28c Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Wed, 3 Jun 2026 11:15:13 +0100 Subject: [PATCH 12/13] Update CHANGELOG for v0.11.11 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19a5a017..ff496c8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] + +## [v0.11.11](https://github.com/JuliaData/XLSX.jl/tree/v0.11.11) - 2026-06-03 - add a package extension to support [FileIO.jl](https://github.com/JuliaIO/FileIO.jl) - update copyright notice end-date From ec2afa16a24456aaa9d902ba7c018413b5e80219 Mon Sep 17 00:00:00 2001 From: Tim Gebbels Date: Wed, 17 Jun 2026 23:34:50 +0100 Subject: [PATCH 13/13] Update CHANGELOG.md --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff496c8e..31a5c9a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] -## [v0.11.11](https://github.com/JuliaData/XLSX.jl/tree/v0.11.11) - 2026-06-03 +## [v0.11.12](https://github.com/JuliaData/XLSX.jl/tree/v0.11.11) - 2026-06-19 - add a package extension to support [FileIO.jl](https://github.com/JuliaIO/FileIO.jl) - update copyright notice end-date +## [v0.11.11](https://github.com/JuliaData/XLSX.jl/tree/v0.11.11) - 2026-06-18 +- Fix [#410](https://github.com/JuliaData/XLSX.jl/issues/410) by making `is_binary_path` case insensitive + ## [v0.11.10](https://github.com/JuliaData/XLSX.jl/tree/v0.11.10) - 2026-05-28 - support macro-enabled files ([#401](https://github.com/JuliaData/XLSX.jl/issues/401)) - support pass-through of customXml files (again). ([#403](https://github.com/JuliaData/XLSX.jl/issues/403))