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 dc1bf9cd1..fee4b9d26 100644 --- a/tests/FSharp.Data.Core.Tests/FSharp.Data.Core.Tests.fsproj +++ b/tests/FSharp.Data.Core.Tests/FSharp.Data.Core.Tests.fsproj @@ -44,6 +44,7 @@ + diff --git a/tests/FSharp.Data.Core.Tests/HtmlInference.fs b/tests/FSharp.Data.Core.Tests/HtmlInference.fs new file mode 100644 index 000000000..942c6ea4b --- /dev/null +++ b/tests/FSharp.Data.Core.Tests/HtmlInference.fs @@ -0,0 +1,180 @@ +module FSharp.Data.Tests.HtmlInference + +open System +open System.Globalization +open NUnit.Framework +open FsUnit +open FSharp.Data +open FSharp.Data.Runtime +open FSharp.Data.Runtime.StructuralTypes +open FSharp.Data.Runtime.StructuralInference + +// ─── Helpers ──────────────────────────────────────────────────────────────── + +let private mkParams preferOptionals : HtmlInference.Parameters = + { MissingValues = TextConversions.DefaultMissingValues + CultureInfo = CultureInfo.InvariantCulture + UnitsOfMeasureProvider = defaultUnitsOfMeasureProvider + PreferOptionals = preferOptionals + InferenceMode = InferenceMode'.ValuesOnly } + +let private defaultParams = mkParams false +let private optionalParams = mkParams true + +let private prim typ = + InferedType.Primitive(typ, None, false, false) + +// ─── inferListType ─────────────────────────────────────────────────────────── + +[] +let ``inferListType returns Null for empty array`` () = + HtmlInference.inferListType defaultParams [||] |> should equal InferedType.Null + +[] +let ``inferListType returns Null for whitespace-only values`` () = + HtmlInference.inferListType defaultParams [| " "; "\t"; "" |] + |> should equal InferedType.Null + +[] +let ``inferListType treats nbsp as missing value`` () = + HtmlInference.inferListType defaultParams [| " "; " " |] + |> should equal InferedType.Null + +[] +let ``inferListType infers int type`` () = + HtmlInference.inferListType defaultParams [| "1"; "2"; "3" |] + |> should equal (prim typeof) + +[] +let ``inferListType infers decimal type`` () = + HtmlInference.inferListType defaultParams [| "1.5"; "2.3"; "3.7" |] + |> should equal (prim typeof) + +[] +let ``inferListType infers float type for scientific notation`` () = + HtmlInference.inferListType defaultParams [| "1e100"; "2.5e-10" |] + |> should equal (prim typeof) + +[] +let ``inferListType infers string type`` () = + HtmlInference.inferListType defaultParams [| "hello"; "world" |] + |> should equal (prim typeof) + +[] +let ``inferListType infers bool type`` () = + HtmlInference.inferListType defaultParams [| "true"; "false" |] + |> should equal (prim typeof) + +[] +let ``inferListType widens int to decimal when mixed with decimal`` () = + HtmlInference.inferListType defaultParams [| "1"; "2.5"; "3" |] + |> should equal (prim typeof) + +[] +let ``inferListType widens to float for scientific notation mixed with int`` () = + HtmlInference.inferListType defaultParams [| "1"; "1.5e10"; "3" |] + |> should equal (prim typeof) + +[] +let ``inferListType produces Heterogeneous type for mixed numeric and non-numeric`` () = + let result = HtmlInference.inferListType defaultParams [| "42"; "hello" |] + + match result with + | InferedType.Heterogeneous _ -> () + | _ -> Assert.Fail(sprintf "Expected Heterogeneous, got %A" result) + +[] +let ``inferListType treats NaN as float when preferOptionals is false`` () = + let result = HtmlInference.inferListType defaultParams [| "NaN" |] + result |> should equal (prim typeof) + +[] +let ``inferListType treats NaN as Null when preferOptionals is true`` () = + let result = HtmlInference.inferListType optionalParams [| "NaN" |] + result |> should equal InferedType.Null + +[] +let ``inferListType treats NA as float when preferOptionals is false`` () = + let result = HtmlInference.inferListType defaultParams [| "NA" |] + result |> should equal (prim typeof) + +[] +let ``inferListType infers date type for ISO date strings`` () = + let result = + HtmlInference.inferListType defaultParams [| "2023-01-01"; "2023-06-15" |] + + match result with + | InferedType.Primitive(typ, _, false, _) -> + (typ = typeof || typ = typeof) + |> should equal true + | _ -> Assert.Fail(sprintf "Expected date primitive, got %A" result) + +[] +let ``inferListType with mixed missing and integer values infers float (allowEmptyValues path)`` () = + // NaN + int values → float because NaN is treated as float when preferOptionals=false + let result = HtmlInference.inferListType defaultParams [| "NaN"; "1"; "2" |] + result |> should equal (prim typeof) + +[] +let ``inferListType with single integer value returns int`` () = + HtmlInference.inferListType defaultParams [| "42" |] + |> should equal (prim typeof) + +// ─── inferHeaders ──────────────────────────────────────────────────────────── + +[] +let ``inferHeaders returns false for empty rows`` () = + let hasHeaders, names, units, _ = HtmlInference.inferHeaders defaultParams [||] + hasHeaders |> should equal false + names |> should equal None + units |> should equal None + +[] +let ``inferHeaders returns false for single row`` () = + let hasHeaders, names, _, _ = + HtmlInference.inferHeaders defaultParams [| [| "Name"; "Age" |] |] + + hasHeaders |> should equal false + names |> should equal None + +[] +let ``inferHeaders returns false for exactly two rows`` () = + let rows = [| [| "Name"; "Age" |]; [| "Alice"; "30" |] |] + let hasHeaders, names, _, _ = HtmlInference.inferHeaders defaultParams rows + hasHeaders |> should equal false + names |> should equal None + +[] +let ``inferHeaders returns true when header row differs from data rows`` () = + // First row is text (string) headers, data rows are numeric → types differ → headers detected + let rows = + [| [| "Name"; "Score" |] + [| "Alice"; "95" |] + [| "Bob"; "87" |] |] + + let hasHeaders, names, _, _ = HtmlInference.inferHeaders defaultParams rows + hasHeaders |> should equal true + names |> should equal (Some [| "Name"; "Score" |]) + +[] +let ``inferHeaders returns false when all rows have same type`` () = + // All rows are strings → header row type = data rows type → no headers inferred + let rows = + [| [| "Alice"; "Bob" |] + [| "Carol"; "Dave" |] + [| "Eve"; "Frank" |] |] + + let hasHeaders, names, _, _ = HtmlInference.inferHeaders defaultParams rows + hasHeaders |> should equal false + names |> should equal None + +[] +let ``inferHeaders returns inferred type for data rows`` () = + let rows = + [| [| "Name"; "Age" |] + [| "Alice"; "30" |] + [| "Bob"; "25" |] |] + + let hasHeaders, _, _, dataType = HtmlInference.inferHeaders defaultParams rows + hasHeaders |> should equal true + dataType |> should not' (equal None)