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
9 changes: 9 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# ExcelFiles.jl v1.1.0
* `load("file.xlsx")` without a sheet argument loads the first sheet
* New `transpose=true` keyword for `load`, for sheets organized in rows
rather than columns
* `save` accepts any Tables.jl source in addition to iterable tables, can
write multiple sheets via `save(file, "name" => table, ...)`, overwrites
existing files by default (pass `overwrite=false` for an error instead),
and passes keyword arguments through to `XLSX.writetable`

# ExcelFiles.jl v1.0.1
* Loading goes through the rewritten ExcelReaders 1.0: legacy xls files are
read natively via LibXLS.jl and modern xlsx files via XLSX.jl, without any
Expand Down
3 changes: 2 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ julia = "1.10"

[extras]
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a"

[targets]
test = ["Test", "DataFrames", "TestItemRunner"]
test = ["Test", "DataFrames", "Tables", "TestItemRunner"]
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,47 @@ function load(f::FileIO.File{FileIO.format"Excel"}, range; keywords...)
````
#### Arguments:

* ``range``: either the name of the sheet in the Excel file to read, or a full Excel range specification (i.e. "Sheetname!A1:B2").
* The ``keywords`` arguments are the same as in [ExcelReaders.jl](https://github.com/queryverse/ExcelReaders.jl) (which is used under the hood to read Excel files). When ``range`` is a sheet name, the keyword arguments for the ``readxlsheet`` function from ExcelReaders.jl apply, if ``range`` is a range specification, the keyword arguments for the ``readxl`` function apply.
* ``range``: either the name of the sheet in the Excel file to read, or a full Excel range specification (i.e. "Sheetname!A1:B2"). If omitted, the first sheet is loaded.
* ``header=true``: whether the first row holds the column names. With ``header=false``, columns are named ``x1``, ``x2``, ... unless ``colnames`` is given.
* ``colnames``: a ``Vector{Symbol}`` of column names to use (e.g. ``colnames=[:a, :b]``), instead of names from the header row.
* ``transpose=false``: with ``transpose=true`` the sheet or range is transposed before the table is constructed, for data organized in rows rather than columns.
* The remaining ``keywords`` arguments are the same as in [ExcelReaders.jl](https://github.com/queryverse/ExcelReaders.jl) (which is used under the hood to read Excel files). When ``range`` is a sheet name, the keyword arguments for the ``readxlsheet`` function from ExcelReaders.jl apply, if ``range`` is a range specification, the keyword arguments for the ``readxl`` function apply.

To read many sheets or ranges from the same file efficiently, open it once
with ``ExcelReaders.openxl`` and use the ExcelReaders functions directly:

````julia
using ExcelReaders

f = openxl("data.xlsx")
data1 = readxlsheet(f, "Sheet1")
data2 = readxlsheet(f, "Sheet2")
````

For advanced xlsx-only reading options (native Excel table names, row
callbacks, string-based missing values), [XLSX.jl](https://github.com/JuliaData/XLSX.jl)'s
``XLSX.readtable`` can always be used directly.

### Save an Excel file

The following code saves any iterable table as an excel file:
The following code saves any iterable table or Tables.jl source as an excel file:
````julia
using ExcelFiles

save("output.xlsx", it)
````
This will work as long as it is any of the types supported as sources in IterableTables.jl.
This will work as long as ``it`` is any of the types supported as sources in IterableTables.jl, or any Tables.jl table.

An existing file is overwritten by default; pass ``overwrite=false`` to get
an error instead. A ``sheetname`` keyword sets the name of the sheet, and any
further keyword arguments are passed on to ``XLSX.writetable``.

Multiple sheets can be written to the same file by passing ``name => table``
pairs:

````julia
save("output.xlsx", "Data" => df1, "Summary" => df2)
````

### Using the pipe syntax

Expand Down
37 changes: 30 additions & 7 deletions src/ExcelFiles.jl
Original file line number Diff line number Diff line change
Expand Up @@ -45,31 +45,49 @@
TableShowUtils.printHTMLtable(io, getiterator(source))
end

Base.Multimedia.showable(::MIME"text/html", source::ExcelFile) = true

Check notice on line 48 in src/ExcelFiles.jl

View workflow job for this annotation

GitHub Actions / julia-ci / lint

unused_function_argument

An argument is included in a function signature but not used within its body.

function Base.show(io::IO, ::MIME"application/vnd.dataresource+json", source::ExcelFile)
TableShowUtils.printdataresource(io, getiterator(source))
end

Base.Multimedia.showable(::MIME"application/vnd.dataresource+json", source::ExcelFile) = true

Check notice on line 54 in src/ExcelFiles.jl

View workflow job for this annotation

GitHub Actions / julia-ci / lint

unused_function_argument

An argument is included in a function signature but not used within its body.

const ExcelFileFormat = Union{FileIO.File{FileIO.format"Excel"},FileIO.File{FileIO.format"ExcelLegacy"}}

# Without a sheet or range argument the first sheet is loaded (resolved
# lazily at iteration time).
function fileio_load(f::ExcelFileFormat; keywords...)
return ExcelFile(f.filename, "", keywords)
end

function fileio_load(f::ExcelFileFormat, range; keywords...)
return ExcelFile(f.filename, range, keywords)
end

function fileio_save(f::FileIO.File{FileIO.format"Excel"}, data; sheetname::AbstractString="")
cols, colnames = TableTraitsUtils.create_columns_from_iterabletable(data, na_representation=:missing)
return XLSX.writetable(f.filename, cols, colnames; sheetname=sheetname)
# Any iterable table or Tables.jl source can be saved; keyword arguments
# (sheetname, anchor_cell, ...) are passed through to XLSX.writetable.
# Unlike XLSX.writetable, an existing file is overwritten by default, in line
# with the conventions of other FileIO formats.
function fileio_save(f::FileIO.File{FileIO.format"Excel"}, data; overwrite::Bool=true, kwargs...)
if TableTraits.isiterabletable(data) === true
cols, colnames = TableTraitsUtils.create_columns_from_iterabletable(data, na_representation=:missing)
return XLSX.writetable(f.filename, cols, colnames; overwrite=overwrite, kwargs...)
else
return XLSX.writetable(f.filename, data; overwrite=overwrite, kwargs...)
end
end

function fileio_save(f::FileIO.File{FileIO.format"Excel"}, tables::Pair...; overwrite::Bool=true, kwargs...)
return XLSX.writetable(f.filename, tables...; overwrite=overwrite, kwargs...)
end

function fileio_save(f::FileIO.File{FileIO.format"ExcelLegacy"}, data; kwargs...)

Check notice on line 85 in src/ExcelFiles.jl

View workflow job for this annotation

GitHub Actions / julia-ci / lint

unused_function_argument

An argument is included in a function signature but not used within its body.

Check notice on line 85 in src/ExcelFiles.jl

View workflow job for this annotation

GitHub Actions / julia-ci / lint

unused_function_argument

An argument is included in a function signature but not used within its body.
error("Writing legacy xls files is not supported. Save to an xlsx file instead.")
end

IteratorInterfaceExtensions.isiterable(x::ExcelFile) = true

Check notice on line 89 in src/ExcelFiles.jl

View workflow job for this annotation

GitHub Actions / julia-ci / lint

unused_function_argument

An argument is included in a function signature but not used within its body.
TableTraits.isiterabletable(x::ExcelFile) = true

Check notice on line 90 in src/ExcelFiles.jl

View workflow job for this annotation

GitHub Actions / julia-ci / lint

unused_function_argument

An argument is included in a function signature but not used within its body.

function gennames(n::Integer)
res = Vector{Symbol}(undef, n)
Expand All @@ -79,10 +97,14 @@
return res
end

function _readxl(file::ExcelReaders.ExcelFile, sheetname::AbstractString, startrow::Integer, startcol::Integer, endrow::Integer, endcol::Integer; header::Bool=true, colnames::Vector{Symbol}=Symbol[])
function _readxl(file::ExcelReaders.ExcelFile, sheetname::AbstractString, startrow::Integer, startcol::Integer, endrow::Integer, endcol::Integer; header::Bool=true, colnames::Vector{Symbol}=Symbol[], transpose::Bool=false)
data = ExcelReaders.readxl_internal(file, sheetname, startrow, startcol, endrow, endcol)

if transpose
data = permutedims(data)
end

nrow, ncol = size(data)

Check notice on line 107 in src/ExcelFiles.jl

View workflow job for this annotation

GitHub Actions / julia-ci / lint

unused_binding

Variable has been assigned but not used.

if length(colnames) == 0
if header
Expand Down Expand Up @@ -142,15 +164,16 @@
_readxl(excelfile, sheetname, startrow, startcol, endrow, endcol; file.keywords...)
else
excelfile = openxl(file.filename)
sheet = ExcelReaders.sheet_handle(excelfile, file.range)
sheetname = isempty(file.range) ? ExcelReaders.sheetnames(excelfile)[1] : file.range
sheet = ExcelReaders.sheet_handle(excelfile, sheetname)

keywords = filter(i -> !(i[1] in (:header, :colnames)), file.keywords)
keywords = filter(i -> !(i[1] in (:header, :colnames, :transpose)), file.keywords)
startrow, startcol, endrow, endcol = ExcelReaders.convert_args_to_row_col(sheet; keywords...)

keywords2 = copy(file.keywords)
keywords2 = filter(i -> !(i[1] in (:skipstartrows, :skipstartcols, :nrows, :ncols)), file.keywords)

_readxl(excelfile, file.range, startrow, startcol, endrow, endcol; keywords2...)
_readxl(excelfile, sheetname, startrow, startcol, endrow, endcol; keywords2...)
end

return create_tableiterator(column_data, col_names)
Expand Down
56 changes: 56 additions & 0 deletions test/test_excelfiles.jl
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,59 @@ end
@test size(df2) == (4, 13)
@test df2[!, 1] == [1., 1.5, 2., 2.5]
end

@testitem "FileIO parity features" begin
using ExcelReaders
using TableTraitsUtils
using Dates
using DataValues
using DataFrames
import Tables

filename = normpath(dirname(pathof(ExcelReaders)), "..", "test", "TestData.xlsx")
filename_xls = normpath(dirname(pathof(ExcelReaders)), "..", "test", "TestData.xls")

# zero-arg load defaults to the first sheet
df = DataFrame(load(filename))
@test size(df) == (4, 13)
@test df[!, 1] == [1., 1.5, 2., 2.5]
df_xls = DataFrame(load(filename_xls))
@test df_xls[!, 1] == [1., 1.5, 2., 2.5]

# transpose (issue #12)
data, names = create_columns_from_iterabletable(load(filename, "Sheet1!C3:D5", header=false, transpose=true))
@test length(data) == 3
@test data[1] == ["Some Float64s", "Some Strings"]
@test data[2] == [1.0, "A"]
@test data[3] == [1.5, "BB"]

# the lazy return is a Tables.jl source for any sink
ef = load(filename, "Sheet1")
cols = Tables.columns(ef)
@test length(Tables.columnnames(cols)) == 13
@test collect(skipmissing(Tables.getcolumn(cols, 1))) == [1., 1.5, 2., 2.5]

dir = mktempdir()

# save overwrites existing files by default (issue #24)
path = joinpath(dir, "overwrite.xlsx")
input = (a = [1.0, 2.0], b = ["x", "y"]) |> DataFrame
save(path, input)
save(path, input)
@test DataFrame(load(path, "Sheet1")) == input
@test_throws Exception save(path, input, overwrite = false)

# save accepts plain Tables.jl sources that are not iterable tables
path2 = joinpath(dir, "tables.xlsx")
save(path2, Tables.table([1 2; 3 4]))
back = DataFrame(load(path2, "Sheet1"))
@test back[!, 1] == [1.0, 3.0]
@test back[!, 2] == [2.0, 4.0]

# multi-sheet save (issue #38)
path3 = joinpath(dir, "multi.xlsx")
input2 = (c = [10.0, 20.0], d = ["u", "v"]) |> DataFrame
save(path3, "First" => input, "Second" => input2)
@test DataFrame(load(path3, "First")) == input
@test DataFrame(load(path3, "Second")) == input2
end
Loading