diff --git a/NEWS.md b/NEWS.md index 36aade55..9c822ba9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 diff --git a/src/ExcelReaders.jl b/src/ExcelReaders.jl index ae95c231..77b0f8fe 100644 --- a/src/ExcelReaders.jl +++ b/src/ExcelReaders.jl @@ -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 @@ -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) @@ -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 diff --git a/test/test_excelreaders.jl b/test/test_excelreaders.jl index 4ebfb00b..bb15c2be 100644 --- a/test/test_excelreaders.jl +++ b/test/test_excelreaders.jl @@ -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