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
11 changes: 11 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# ExcelReaders.jl v1.0.0 Release Notes
* Drop the PyCall/xlrd backend: legacy xls files are now read natively via
LibXLS.jl and modern xlsx files via XLSX.jl, with the file format detected
from the content of the file
* Restore support for modern xlsx files
* Error cells are returned as `ExcelErrorCell` for both file formats
* `readxlnames` and `readxlrange` work for xlsx files (they error for xls
files, where the underlying library does not expose defined names)
* `close` releases the resources of an `ExcelFile`
* Minimum supported Julia version is 1.10

# ExcelReaders.jl v0.12.0 Release Notes
* Drop julia 0.7 support
* Migrate to Project.toml
Expand Down
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0"
[compat]
DataValues = "0.4.4, 0.5, 1"
Dates = "1"
LibXLS = "0.1, 0.2, 1"
LibXLS = "1.0.1"
XLSX = "0.12"
julia = "1.6"
julia = "1.10"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,35 @@ This will read all content on Sheet1 in the file Filename.xls. Eventual blank ro

``readxlsheet`` also accepts an ExcelFile (as obtained from ``openxl``) as its first argument.

## Defined names

For xlsx files, workbook-level defined names can be listed and read:

````julia
f = openxl("Filename.xlsx")

readxlnames(f) # all defined names
readxlrange(f, "MyRange") # content of the range a name refers to
````

Defined names are not available for legacy xls files, because the underlying
C library does not expose them.

## Closing a file

``close(f)`` releases the resources of an ``ExcelFile`` obtained from
``openxl`` (for xlsx files this is a no-op, for xls files it closes the
underlying C library handle; files also close themselves when garbage
collected).

## Limitations

* Reading only — for writing xlsx files use
[XLSX.jl](https://github.com/JuliaData/XLSX.jl) or
[ExcelFiles.jl](https://github.com/queryverse/ExcelFiles.jl).
* Cell formulas are not exposed, only their cached results.
* Reading from a byte buffer or ``IO`` is not supported, only from files.

## Alternatives

[XLSX.jl](https://github.com/JuliaData/XLSX.jl) provides excellent, more
Expand Down
37 changes: 37 additions & 0 deletions src/ExcelReaders.jl
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,21 @@ end

isblank(v) = v isa DataValue && DataValues.isna(v)

"""
readxlsheet(file, sheet; skipstartrows=:blanks, skipstartcols=:blanks, nrows=:all, ncols=:all)

Read a whole sheet from an Excel file and return its content as a matrix.
`file` is either a filename or an `ExcelFile` from [`openxl`](@ref); `sheet`
is a sheet name or (1-based) index.

Blank rows and columns at the top and left are skipped by default. The
keyword arguments control the range that is read:

- `skipstartrows`/`skipstartcols`: `:blanks` (default) skips empty initial
rows/columns; an integer skips exactly that many.
- `nrows`/`ncols`: `:all` (default) reads everything after the skipped
rows/columns; an integer reads exactly that many.
"""
function readxlsheet(filename::AbstractString, sheetindex::Int; args...)
file = openxl(filename)
return readxlsheet(file, sheetindex; args...)
Expand Down Expand Up @@ -260,6 +275,14 @@ function convert_ref_to_sheet_row_col(range::AbstractString)
return sheetname, startrow, startcol, endrow, endcol
end

"""
readxl(file, range)

Read the given range from an Excel file and return its content as a matrix
(or a single value for a single-cell range). `file` is either a filename or
an `ExcelFile` from [`openxl`](@ref); `range` is a full Excel range
specification such as `"Sheet1!A1:C4"`.
"""
function readxl(filename::AbstractString, range::AbstractString)
excelfile = openxl(filename)

Expand Down Expand Up @@ -292,11 +315,25 @@ function readxl_internal(ws, startrow::Integer, startcol::Integer, endrow::Integ
end
end

"""
readxlnames(f::ExcelFile)

Return the workbook-level defined names in the Excel file, sorted
alphabetically. Only supported for xlsx files; for legacy xls files an error
is thrown, because the underlying C library does not expose defined names.
"""
function readxlnames(f::ExcelFile)
f.workbook isa XLSX.XLSXFile || error("Defined names are not supported for legacy xls files.")
return sort!(collect(keys(f.workbook.workbook.workbook_names)))
end

"""
readxlrange(f::ExcelFile, name)

Read the range that the defined name `name` refers to and return its content
(a matrix, or a single value for a single-cell name). Only supported for
xlsx files; for legacy xls files an error is thrown.
"""
function readxlrange(f::ExcelFile, range::AbstractString)
f.workbook isa XLSX.XLSXFile || error("Defined names are not supported for legacy xls files.")
data = XLSX.getdata(f.workbook, range)
Expand Down
43 changes: 43 additions & 0 deletions test/test_excelreaders.jl
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,47 @@ end
for sheet in ["Second Sheet", 2]
compare_cells(readxlsheet(xls, sheet), readxlsheet(xlsx, sheet))
end

# the internal entry point ExcelFiles relies on
@test ExcelReaders.readxl_internal(xls, "Sheet1", 4, 3, 4, 3) == 1.0
@test ExcelReaders.readxl_internal(xlsx, "Sheet1", 4, 3, 4, 3) == 1.0

# close works for both backends (a no-op for xlsx)
close(xls)
close(xlsx)
@test_throws ErrorException readxl(xls, "Sheet1!C4")
end

@testitem "Defined names" begin
using Dates, DataValues
import ExcelReaders.XLSX

filename = joinpath(mktempdir(), "named.xlsx")
XLSX.openxlsx(filename, mode="w") do xf
sh = xf[1]
XLSX.rename!(sh, "Sheet1")
sh["B2"] = 1.0
sh["C2"] = 2.0
sh["B3"] = 3.0
sh["C3"] = 4.0
sh["E1"] = "hello"
XLSX.addDefinedName(xf, "block", "Sheet1!B2:C3")
XLSX.addDefinedName(xf, "single", "Sheet1!E1")
end

f = openxl(filename)
@test readxlnames(f) == ["block", "single"]

data = readxlrange(f, "block")
@test size(data) == (2, 2)
@test data[1, 1] == 1.0
@test data[2, 2] == 4.0

@test readxlrange(f, "single") == "hello"

# Defined names are not available for legacy xls files, because the
# underlying C library does not expose them.
xls = openxl(normpath(@__DIR__, "TestData.xls"))
@test_throws ErrorException readxlnames(xls)
@test_throws ErrorException readxlrange(xls, "block")
end
Loading