Skip to content

Commit

Permalink
Merge pull request #110 from mjdupont/additional-type-coercion
Browse files Browse the repository at this point in the history
Additional type coercion
  • Loading branch information
quintusm authored Sep 11, 2024
2 parents 25104a1 + 1fe3849 commit 3e19661
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 13 deletions.
27 changes: 23 additions & 4 deletions src/ExcelProvider.Runtime/ExcelProvider.Runtime.fs
Original file line number Diff line number Diff line change
Expand Up @@ -348,10 +348,17 @@ type Row(documentId, sheetname, rowIndex, getCellValue: int -> int -> obj, colum

try
match value with
| null when typeof<'a> = typeof<string> -> value :?> 'a
| null -> value :?> 'a
| _ when typeof<'a> = typeof<string> -> (box (string value)) :?> 'a
//| :? string as valueStr when typeof<'a> = typeof<double> -> (box (Double.Parse valueStr)) :?> 'a
| :? double as valueDbl when typeof<'a> = typeof<DateTime> -> (box (DateTime.FromOADate valueDbl)) :?> 'a
| :? string as valueStr when typeof<'a> = typeof<DateTime> -> (box (DateTime.Parse valueStr)) :?> 'a
| _ -> value :?> 'a
with :? InvalidCastException ->
with
| :? InvalidCastException
| :? ArgumentException
| :? ArgumentNullException
| :? InvalidCastException ->
failInvalidCast value (value.GetType()) typeof<'a> columnName rowIndex documentId sheetname

member this.TryGetNullableValue<'a when 'a: (new: unit -> 'a) and 'a: struct and 'a :> ValueType>
Expand All @@ -361,10 +368,22 @@ type Row(documentId, sheetname, rowIndex, getCellValue: int -> int -> obj, colum
let value = this.GetValue columnIndex

try
(value :?> Nullable<'a>).GetValueOrDefault()
with :? InvalidCastException ->
match value with
| null -> (value :?> Nullable<'a>).GetValueOrDefault()
| _ when typeof<'a> = typeof<string> -> (box (string value)) :?> 'a
//| :? string as valueStr when typeof<'a> = typeof<double> -> (box (Double.Parse valueStr)) :?> 'a
| :? double as valueDbl when typeof<'a> = typeof<DateTime> -> (box (DateTime.FromOADate valueDbl)) :?> 'a
| :? string as valueStr when typeof<'a> = typeof<DateTime> -> (box (DateTime.Parse valueStr)) :?> 'a
| _ -> value :?> 'a

with
| :? InvalidCastException
| :? ArgumentException
| :? ArgumentNullException
| :? FormatException ->
failInvalidCast value (value.GetType()) typeof<'a> columnName rowIndex documentId sheetname


override this.ToString() =
let columnValueList =
[ for column in columns do
Expand Down
116 changes: 107 additions & 9 deletions tests/ExcelProvider.Tests/ExcelProvider.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ type MultiLine = ExcelFile<"MultilineHeader.xlsx">
type MultipleSheetsFirst = ExcelFile<"MultipleSheets.xlsx", "A">
type MultipleSheetsSecond = ExcelFile<"MultipleSheets.xlsx", "B">
type MultipleSheetsSecondRange = ExcelFile<"MultipleSheets.xlsx", "B", "A2">
type MixedDataTypes = ExcelFile<"MixedDataTypes.xlsx">

[<Test>]
let ``Read Text as String``() =
Expand Down Expand Up @@ -385,6 +384,113 @@ let ``HashHeader false with header removed``() =
row.Column5 |> should equal expectedTime
row.Column6 |> should equal 100.0M

[<Test>]
let ``Reading all rows in a well-formed excel file does not return empty rows``() =
let fileWithUnboundRange = DifferentMainSheet()
// For reference, DifferentMainSheet has 1 header row and 6 data rows.
fileWithUnboundRange.Data |> Seq.length |> should equal 6

let fileWithBoundRange = MultipleRegions()
fileWithBoundRange |> Seq.length |> should equal 4


module AutomaticCoercion =
type MixedDataTypes = ExcelFile<"MixedDataTypes.xlsx", "ValidDataTypes">

type DateTimeFromTextNumeric = ExcelFile<"MixedDataTypes.xlsx", "ValidDataTypes", Range="H2:H3", HasHeaders=false>
type DateTimeFromTextString = ExcelFile<"MixedDataTypes.xlsx", "ValidDataTypes", Range="H4:H5", HasHeaders=false>
type DateTimeFromNumeric = ExcelFile<"MixedDataTypes.xlsx", "ValidDataTypes", Range="H6:H7", HasHeaders=false>
type DateTimeFromGeneralNumeric = ExcelFile<"MixedDataTypes.xlsx", "ValidDataTypes", Range="H8:H9", HasHeaders=false>

type DoubleCoercionErrorFromNumberWords = ExcelFile<"MixedDataTypes.xlsx", "InvalidDataTypes", Range="B2:B3", HasHeaders=false>
type DoubleCoercionErrorFromString = ExcelFile<"MixedDataTypes.xlsx", "InvalidDataTypes", Range="B4:B5", HasHeaders=false>
type DoubleCoercionErrorFromCurrencyNumber = ExcelFile<"MixedDataTypes.xlsx", "InvalidDataTypes", Range="B6:B7", HasHeaders=false>

type DateCoercionErrorFromDateWords = ExcelFile<"MixedDataTypes.xlsx", "InvalidDataTypes", Range="H2:H3", HasHeaders=false>
type DateCoercionErrorFromString = ExcelFile<"MixedDataTypes.xlsx", "InvalidDataTypes", Range="H4:H5", HasHeaders=false>
type DateCoercionErrorFromNegativeNumber = ExcelFile<"MixedDataTypes.xlsx", "InvalidDataTypes", Range="H6:H7", HasHeaders=false>


// See https://github.com/fsprojects/ExcelProvider/issues/14
type MixedStringTypes = ExcelFile<"MixedDataTypes.xlsx", "ValidDataTypes", Range="A1:A10">
[<Test>]
let ``Automatically coerces non-string data in string columns to strings`` () =


let file = MixedStringTypes()
let writeTitles data =
for (row:MixedStringTypes.Row) in data do
(sprintf "%s" row.Title) |> ignore
(fun () -> writeTitles file.Data) |> should (not' << throw) typeof<System.InvalidCastException>

type MixedDoubleTypes = ExcelFile<"MixedDataTypes.xlsx", "ValidDataTypes", Range="B1:B10">
[<Test>]
let ``Coerces valid string data in numeric columns to double`` () =
let file = MixedDoubleTypes()
let getYear data =
for (row:MixedDoubleTypes.Row) in data do
sprintf "%f" (row.Year) |> ignore
(fun () -> getYear file.Data) |> should (not' << throw) typeof<System.InvalidCastException>

type MixedDateTimeTypes = ExcelFile<"MixedDataTypes.xlsx", "ValidDataTypes", Range="H1:H10">
[<Test>]
let ``Coerces valid data in datetime columns to double`` () =
let file = MixedDateTimeTypes()
let getYear data =
for (row:MixedDateTimeTypes.Row) in data do
sprintf "%A" (row.``Date Watched``) |> ignore
(fun () -> getYear file.Data) |> should (not' << throw) typeof<System.InvalidCastException>

[<Test>]
let ``Throws when coercing invalid data to double`` () =
let filenw = DoubleCoercionErrorFromNumberWords()
let files = DoubleCoercionErrorFromString()
let filecn = DoubleCoercionErrorFromCurrencyNumber()
let getYear data =
for (row: DoubleCoercionErrorFromNumberWords.Row) in data do
sprintf "%f" (row.``Column1``) |> ignore
(fun () -> getYear filenw.Data) |> should throw typeof<System.InvalidCastException>

let getYear data =
for (row: DoubleCoercionErrorFromString.Row) in data do
sprintf "%f" (row.``Column1``) |> ignore
(fun () -> getYear files.Data) |> should throw typeof<System.InvalidCastException>

let getYear data =
for (row: DoubleCoercionErrorFromCurrencyNumber.Row) in data do
sprintf "%f" (row.``Column1``) |> ignore
(fun () -> getYear filecn.Data) |> should throw typeof<System.InvalidCastException>


[<Test>]
let ``Throws when coercing invalid data to datetime`` () =
let filedw = DateCoercionErrorFromDateWords()
let files = DateCoercionErrorFromString()
let filenn = DateCoercionErrorFromNegativeNumber()
let getYear data =
for (row: DateCoercionErrorFromDateWords.Row) in data do
sprintf "%A" (row.``Column1``) |> ignore
(fun () -> getYear filedw.Data) |> should throw typeof<System.InvalidCastException>

let getYear data =
for (row: DateCoercionErrorFromString.Row) in data do
sprintf "%A" (row.``Column1``) |> ignore
(fun () -> getYear files.Data) |> should throw typeof<System.InvalidCastException>

let getYear data =
for (row: DateCoercionErrorFromNegativeNumber.Row) in data do
sprintf "%A" (row.``Column1``) |> ignore
(fun () -> getYear filenn.Data) |> should throw typeof<System.InvalidCastException>


[<Test>]
let ``Automatic conversions do not cause InvalidCastExceptions`` () =
let file = MixedDataTypes()
let printTitles data =
for (row:MixedDataTypes.Row) in data do
sprintf "%s (%f) %i" row.Title (row.Year) (row.``Date Watched``.Year) |> ignore
(fun () -> printTitles file.Data) |> should (not' << throw) typeof<System.InvalidCastException>

// See https://github.com/fsprojects/ExcelProvider/issues/14
[<Test>]
let ``Can automatically coerce non-string cells in a column of string data to their string form`` () =
Expand All @@ -394,11 +500,3 @@ let ``Can automatically coerce non-string cells in a column of string data to th
sprintf "%s (%i)" row.Title ((int) row.Year) |> ignore
(fun () -> printTitles file.Data) |> should (not' << throw) typeof<System.InvalidCastException>

[<Test>]
let ``Reading all rows in a well-formed excel file does not return empty rows``() =
let fileWithUnboundRange = DifferentMainSheet()
// For reference, DifferentMainSheet has 1 header row and 6 data rows.
fileWithUnboundRange.Data |> Seq.length |> should equal 6

let fileWithBoundRange = MultipleRegions()
fileWithBoundRange |> Seq.length |> should equal 4
Binary file modified tests/ExcelProvider.Tests/MixedDataTypes.xlsx
Binary file not shown.

0 comments on commit 3e19661

Please sign in to comment.