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
7 changes: 7 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# ExcelReaders.jl v1.1.0 Release Notes
* `readxl` supports open-ended ranges: the row number may be omitted on
either end, so `"Sheet1!A3:A"` reads from row 3 to the last used row and
`"Sheet1!A:A"` reads the whole column
* Ranges that extend beyond the used part of a sheet return NA-filled cells
for both backends (this already worked since v1.0.0; now covered by tests)

# 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
Expand Down
24 changes: 19 additions & 5 deletions src/ExcelReaders.jl
Original file line number Diff line number Diff line change
Expand Up @@ -260,16 +260,24 @@ function convert_ref_to_sheet_row_col(range::AbstractString)
m = match(r, range)
m === nothing && error("Invalid Excel range specified.")
sheetname = String(m.captures[1])
startrow = parse(Int, m.captures[3])
startcol = colnum(m.captures[2])
if m.captures[4] === nothing
# a single cell reference needs both a column and a row
(isempty(m.captures[2]) || isempty(m.captures[3])) && error("Invalid Excel range specified.")
startrow = parse(Int, m.captures[3])
startcol = colnum(m.captures[2])
endrow = startrow
endcol = startcol
else
endrow = parse(Int, m.captures[6])
(isempty(m.captures[2]) || isempty(m.captures[5])) && error("Invalid Excel range specified.")
startcol = colnum(m.captures[2])
endcol = colnum(m.captures[5])
# Row numbers may be omitted on either end ("A3:A", "A:A"): a missing
# start row means the first row, and a missing end row is returned as
# 0 and resolved to the last used row of the sheet by the caller.
startrow = isempty(m.captures[3]) ? 1 : parse(Int, m.captures[3])
endrow = isempty(m.captures[6]) ? 0 : parse(Int, m.captures[6])
end
if (startrow > endrow) || (startcol > endcol)
if (endrow != 0 && startrow > endrow) || (startcol > endcol)
error("Please provide rectangular region from top left to bottom right corner")
end
return sheetname, startrow, startcol, endrow, endcol
Expand All @@ -281,7 +289,9 @@ end
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"`.
specification such as `"Sheet1!A1:C4"`. The row number may be omitted on
either end of the range: `"Sheet1!A3:A"` reads column A from row 3 to the
last used row of the sheet, and `"Sheet1!A:A"` reads the whole column.
"""
function readxl(filename::AbstractString, range::AbstractString)
excelfile = openxl(filename)
Expand All @@ -292,6 +302,10 @@ end
function readxl(file::ExcelFile, range::AbstractString)
sheetname, startrow, startcol, endrow, endcol = convert_ref_to_sheet_row_col(range)
ws = sheet_handle(file, sheetname)
if endrow == 0
# open-ended range like "A3:A": read to the last used row
endrow = max(sheet_dims(ws)[1], startrow)
end
readxl_internal(ws, startrow, startcol, endrow, endcol)
end

Expand Down
46 changes: 46 additions & 0 deletions test/test_excelreaders.jl
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,49 @@ end
@test_throws ErrorException readxlnames(xls)
@test_throws ErrorException readxlrange(xls, "block")
end

@testitem "Open-ended ranges" begin
using Dates, DataValues

for name in ["TestData.xls", "TestData.xlsx"]
f = openxl(normpath(@__DIR__, name))

data = readxl(f, "Sheet1!C4:C")
@test size(data, 2) == 1
@test data[1, 1] == 1.0
@test data[2, 1] == 1.5
@test data[3, 1] == 2.0

# a fully open range spans all rows of the sheet
data2 = readxl(f, "Sheet1!C:C")
@test size(data2, 1) == size(data, 1) + 3
@test DataValues.isna(data2[1, 1])
@test data2[4, 1] == 1.0

data3 = readxl(f, "Sheet1!C3:D")
@test data3[1, 1] == "Some Float64s"
@test data3[2, 2] == "A"

@test_throws ErrorException readxl(f, "Sheet1!C4:G3")
@test_throws ErrorException readxl(f, "Sheet1!:C")
@test_throws ErrorException readxl(f, "Sheet1!C4:")
end
end

@testitem "Ranges beyond the data" begin
using DataValues

# Ranges that extend beyond the used part of the sheet come back as
# NA-filled cells rather than erroring (issue #49).
for name in ["TestData.xls", "TestData.xlsx"]
f = openxl(normpath(@__DIR__, name))

data = readxl(f, "Sheet1!A1:AB60")
@test size(data) == (60, 28)
@test data[4, 3] == 1.0
@test DataValues.isna(data[1, 1])
@test DataValues.isna(data[60, 28])

@test DataValues.isna(readxl(f, "Sheet1!AB60"))
end
end
Loading