diff --git a/tests/FSharp.Data.Core.Tests/FSharp.Data.Core.Tests.fsproj b/tests/FSharp.Data.Core.Tests/FSharp.Data.Core.Tests.fsproj index fee4b9d26..66aac207d 100644 --- a/tests/FSharp.Data.Core.Tests/FSharp.Data.Core.Tests.fsproj +++ b/tests/FSharp.Data.Core.Tests/FSharp.Data.Core.Tests.fsproj @@ -38,6 +38,7 @@ + diff --git a/tests/FSharp.Data.Core.Tests/StringExtensions.fs b/tests/FSharp.Data.Core.Tests/StringExtensions.fs new file mode 100644 index 000000000..7cd682e85 --- /dev/null +++ b/tests/FSharp.Data.Core.Tests/StringExtensions.fs @@ -0,0 +1,139 @@ +module FSharp.Data.Tests.StringExtensions + +open System +open System.Globalization +open NUnit.Framework +open FsUnit +open FSharp.Data + +// Tests for FSharp.Data.StringExtensions — the extension methods on System.String +// that wrap TextConversions and throw on failure. + +[] +let ``AsInteger succeeds for valid integers`` () = + "42".AsInteger() |> should equal 42 + "-123".AsInteger() |> should equal -123 + "0".AsInteger() |> should equal 0 + " 5 ".AsInteger() |> should equal 5 + +[] +let ``AsInteger uses provided culture`` () = + // The culture affects decimal separator, but AsInteger uses NumberStyles.Integer + // Test that a culture with different number separators doesn't break integer parsing + "42".AsInteger(CultureInfo.GetCultureInfo("fr-FR")) |> should equal 42 + "-7".AsInteger(CultureInfo.GetCultureInfo("de-DE")) |> should equal -7 + +[] +let ``AsInteger throws for invalid input`` () = + (fun () -> "abc".AsInteger() |> ignore) |> should throw typeof + (fun () -> "12.5".AsInteger() |> ignore) |> should throw typeof + (fun () -> "".AsInteger() |> ignore) |> should throw typeof + +[] +let ``AsInteger64 succeeds for valid int64s`` () = + "9223372036854775807".AsInteger64() |> should equal Int64.MaxValue + "-9223372036854775808".AsInteger64() |> should equal Int64.MinValue + "0".AsInteger64() |> should equal 0L + +[] +let ``AsInteger64 throws for invalid input`` () = + (fun () -> "not-a-number".AsInteger64() |> ignore) |> should throw typeof + +[] +let ``AsDecimal succeeds for valid decimals`` () = + "3.14".AsDecimal() |> should equal 3.14M + "-99.99".AsDecimal() |> should equal -99.99M + "0".AsDecimal() |> should equal 0M + +[] +let ``AsDecimal uses provided culture`` () = + "1.234,56".AsDecimal(CultureInfo.GetCultureInfo("de-DE")) |> should equal 1234.56M + +[] +let ``AsDecimal throws for invalid input`` () = + (fun () -> "abc".AsDecimal() |> ignore) |> should throw typeof + +[] +let ``AsFloat succeeds for valid floats`` () = + "3.14".AsFloat() |> should (equalWithin 1e-10) 3.14 + "-2.718".AsFloat() |> should (equalWithin 1e-10) -2.718 + "0.0".AsFloat() |> should equal 0.0 + +[] +let ``AsFloat treats default missing values as NaN`` () = + "N/A".AsFloat() |> Double.IsNaN |> should equal true + "NA".AsFloat() |> Double.IsNaN |> should equal true + +[] +let ``AsFloat uses custom missing values`` () = + "MISSING".AsFloat(missingValues = [| "MISSING" |]) |> Double.IsNaN |> should equal true + +[] +let ``AsFloat throws for non-numeric non-missing input`` () = + (fun () -> "abc".AsFloat() |> ignore) |> should throw typeof + +[] +let ``AsBoolean succeeds for recognised values`` () = + "true".AsBoolean() |> should equal true + "True".AsBoolean() |> should equal true + "yes".AsBoolean() |> should equal true + "1".AsBoolean() |> should equal true + "false".AsBoolean() |> should equal false + "False".AsBoolean() |> should equal false + "no".AsBoolean() |> should equal false + "0".AsBoolean() |> should equal false + +[] +let ``AsBoolean throws for unrecognised input`` () = + (fun () -> "maybe".AsBoolean() |> ignore) |> should throw typeof + (fun () -> "".AsBoolean() |> ignore) |> should throw typeof + +[] +let ``AsDateTime succeeds for valid ISO 8601 date strings`` () = + let dt = "2024-06-15T12:00:00Z".AsDateTime() + dt.Year |> should equal 2024 + dt.Month |> should equal 6 + dt.Day |> should equal 15 + +[] +let ``AsDateTime uses provided culture`` () = + let dt = "2024-01-01".AsDateTime(CultureInfo.InvariantCulture) + dt.Year |> should equal 2024 + +[] +let ``AsDateTime throws for invalid input`` () = + (fun () -> "not-a-date".AsDateTime() |> ignore) |> should throw typeof + +[] +let ``AsDateTimeOffset succeeds for valid offset strings`` () = + let dto = "2024-06-15T12:00:00+05:30".AsDateTimeOffset() + dto.Year |> should equal 2024 + dto.Offset |> should equal (TimeSpan.FromHours 5.5) + +[] +let ``AsDateTimeOffset throws for invalid input`` () = + (fun () -> "2024-01-01".AsDateTimeOffset() |> ignore) |> should throw typeof + +[] +let ``AsTimeSpan succeeds for valid time span strings`` () = + let ts = "01:30:00".AsTimeSpan() + ts |> should equal (TimeSpan(1, 30, 0)) + +[] +let ``AsTimeSpan throws for invalid input`` () = + (fun () -> "not-a-timespan".AsTimeSpan() |> ignore) |> should throw typeof + +[] +let ``AsGuid succeeds for valid GUID strings`` () = + let g = "6F9619FF-8B86-D011-B42D-00C04FC964FF".AsGuid() + g |> should not' (equal Guid.Empty) + +[] +let ``AsGuid succeeds for braced GUID strings`` () = + let g = "{6F9619FF-8B86-D011-B42D-00C04FC964FF}".AsGuid() + g |> should not' (equal Guid.Empty) + +[] +let ``AsGuid throws for invalid input`` () = + (fun () -> "not-a-guid".AsGuid() |> ignore) |> should throw typeof + (fun () -> "".AsGuid() |> ignore) |> should throw typeof