diff --git a/NEWS.md b/NEWS.md index 7d342cc4..36aade55 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 diff --git a/Project.toml b/Project.toml index 667eaf49..c48700e1 100644 --- a/Project.toml +++ b/Project.toml @@ -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" diff --git a/README.md b/README.md index 3d2c0400..8b6f00e0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/ExcelReaders.jl b/src/ExcelReaders.jl index 22845c77..ae95c231 100644 --- a/src/ExcelReaders.jl +++ b/src/ExcelReaders.jl @@ -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...) @@ -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) @@ -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) diff --git a/test/test_excelreaders.jl b/test/test_excelreaders.jl index 115d3885..4ebfb00b 100644 --- a/test/test_excelreaders.jl +++ b/test/test_excelreaders.jl @@ -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