|
| 1 | +module FSharp.Data.Tests.HtmlInference |
| 2 | + |
| 3 | +open System |
| 4 | +open System.Globalization |
| 5 | +open NUnit.Framework |
| 6 | +open FsUnit |
| 7 | +open FSharp.Data |
| 8 | +open FSharp.Data.Runtime |
| 9 | +open FSharp.Data.Runtime.StructuralTypes |
| 10 | +open FSharp.Data.Runtime.StructuralInference |
| 11 | + |
| 12 | +// ─── Helpers ──────────────────────────────────────────────────────────────── |
| 13 | + |
| 14 | +let private mkParams preferOptionals : HtmlInference.Parameters = |
| 15 | + { MissingValues = TextConversions.DefaultMissingValues |
| 16 | + CultureInfo = CultureInfo.InvariantCulture |
| 17 | + UnitsOfMeasureProvider = defaultUnitsOfMeasureProvider |
| 18 | + PreferOptionals = preferOptionals |
| 19 | + InferenceMode = InferenceMode'.ValuesOnly } |
| 20 | + |
| 21 | +let private defaultParams = mkParams false |
| 22 | +let private optionalParams = mkParams true |
| 23 | + |
| 24 | +let private prim typ = |
| 25 | + InferedType.Primitive(typ, None, false, false) |
| 26 | + |
| 27 | +// ─── inferListType ─────────────────────────────────────────────────────────── |
| 28 | + |
| 29 | +[<Test>] |
| 30 | +let ``inferListType returns Null for empty array`` () = |
| 31 | + HtmlInference.inferListType defaultParams [||] |> should equal InferedType.Null |
| 32 | + |
| 33 | +[<Test>] |
| 34 | +let ``inferListType returns Null for whitespace-only values`` () = |
| 35 | + HtmlInference.inferListType defaultParams [| " "; "\t"; "" |] |
| 36 | + |> should equal InferedType.Null |
| 37 | + |
| 38 | +[<Test>] |
| 39 | +let ``inferListType treats nbsp as missing value`` () = |
| 40 | + HtmlInference.inferListType defaultParams [| " "; " " |] |
| 41 | + |> should equal InferedType.Null |
| 42 | + |
| 43 | +[<Test>] |
| 44 | +let ``inferListType infers int type`` () = |
| 45 | + HtmlInference.inferListType defaultParams [| "1"; "2"; "3" |] |
| 46 | + |> should equal (prim typeof<int>) |
| 47 | + |
| 48 | +[<Test>] |
| 49 | +let ``inferListType infers decimal type`` () = |
| 50 | + HtmlInference.inferListType defaultParams [| "1.5"; "2.3"; "3.7" |] |
| 51 | + |> should equal (prim typeof<decimal>) |
| 52 | + |
| 53 | +[<Test>] |
| 54 | +let ``inferListType infers float type for scientific notation`` () = |
| 55 | + HtmlInference.inferListType defaultParams [| "1e100"; "2.5e-10" |] |
| 56 | + |> should equal (prim typeof<float>) |
| 57 | + |
| 58 | +[<Test>] |
| 59 | +let ``inferListType infers string type`` () = |
| 60 | + HtmlInference.inferListType defaultParams [| "hello"; "world" |] |
| 61 | + |> should equal (prim typeof<string>) |
| 62 | + |
| 63 | +[<Test>] |
| 64 | +let ``inferListType infers bool type`` () = |
| 65 | + HtmlInference.inferListType defaultParams [| "true"; "false" |] |
| 66 | + |> should equal (prim typeof<bool>) |
| 67 | + |
| 68 | +[<Test>] |
| 69 | +let ``inferListType widens int to decimal when mixed with decimal`` () = |
| 70 | + HtmlInference.inferListType defaultParams [| "1"; "2.5"; "3" |] |
| 71 | + |> should equal (prim typeof<decimal>) |
| 72 | + |
| 73 | +[<Test>] |
| 74 | +let ``inferListType widens to float for scientific notation mixed with int`` () = |
| 75 | + HtmlInference.inferListType defaultParams [| "1"; "1.5e10"; "3" |] |
| 76 | + |> should equal (prim typeof<float>) |
| 77 | + |
| 78 | +[<Test>] |
| 79 | +let ``inferListType produces Heterogeneous type for mixed numeric and non-numeric`` () = |
| 80 | + let result = HtmlInference.inferListType defaultParams [| "42"; "hello" |] |
| 81 | + |
| 82 | + match result with |
| 83 | + | InferedType.Heterogeneous _ -> () |
| 84 | + | _ -> Assert.Fail(sprintf "Expected Heterogeneous, got %A" result) |
| 85 | + |
| 86 | +[<Test>] |
| 87 | +let ``inferListType treats NaN as float when preferOptionals is false`` () = |
| 88 | + let result = HtmlInference.inferListType defaultParams [| "NaN" |] |
| 89 | + result |> should equal (prim typeof<float>) |
| 90 | + |
| 91 | +[<Test>] |
| 92 | +let ``inferListType treats NaN as Null when preferOptionals is true`` () = |
| 93 | + let result = HtmlInference.inferListType optionalParams [| "NaN" |] |
| 94 | + result |> should equal InferedType.Null |
| 95 | + |
| 96 | +[<Test>] |
| 97 | +let ``inferListType treats NA as float when preferOptionals is false`` () = |
| 98 | + let result = HtmlInference.inferListType defaultParams [| "NA" |] |
| 99 | + result |> should equal (prim typeof<float>) |
| 100 | + |
| 101 | +[<Test>] |
| 102 | +let ``inferListType infers date type for ISO date strings`` () = |
| 103 | + let result = |
| 104 | + HtmlInference.inferListType defaultParams [| "2023-01-01"; "2023-06-15" |] |
| 105 | + |
| 106 | + match result with |
| 107 | + | InferedType.Primitive(typ, _, false, _) -> |
| 108 | + (typ = typeof<DateTime> || typ = typeof<DateOnly>) |
| 109 | + |> should equal true |
| 110 | + | _ -> Assert.Fail(sprintf "Expected date primitive, got %A" result) |
| 111 | + |
| 112 | +[<Test>] |
| 113 | +let ``inferListType with mixed missing and integer values infers float (allowEmptyValues path)`` () = |
| 114 | + // NaN + int values → float because NaN is treated as float when preferOptionals=false |
| 115 | + let result = HtmlInference.inferListType defaultParams [| "NaN"; "1"; "2" |] |
| 116 | + result |> should equal (prim typeof<float>) |
| 117 | + |
| 118 | +[<Test>] |
| 119 | +let ``inferListType with single integer value returns int`` () = |
| 120 | + HtmlInference.inferListType defaultParams [| "42" |] |
| 121 | + |> should equal (prim typeof<int>) |
| 122 | + |
| 123 | +// ─── inferHeaders ──────────────────────────────────────────────────────────── |
| 124 | + |
| 125 | +[<Test>] |
| 126 | +let ``inferHeaders returns false for empty rows`` () = |
| 127 | + let hasHeaders, names, units, _ = HtmlInference.inferHeaders defaultParams [||] |
| 128 | + hasHeaders |> should equal false |
| 129 | + names |> should equal None |
| 130 | + units |> should equal None |
| 131 | + |
| 132 | +[<Test>] |
| 133 | +let ``inferHeaders returns false for single row`` () = |
| 134 | + let hasHeaders, names, _, _ = |
| 135 | + HtmlInference.inferHeaders defaultParams [| [| "Name"; "Age" |] |] |
| 136 | + |
| 137 | + hasHeaders |> should equal false |
| 138 | + names |> should equal None |
| 139 | + |
| 140 | +[<Test>] |
| 141 | +let ``inferHeaders returns false for exactly two rows`` () = |
| 142 | + let rows = [| [| "Name"; "Age" |]; [| "Alice"; "30" |] |] |
| 143 | + let hasHeaders, names, _, _ = HtmlInference.inferHeaders defaultParams rows |
| 144 | + hasHeaders |> should equal false |
| 145 | + names |> should equal None |
| 146 | + |
| 147 | +[<Test>] |
| 148 | +let ``inferHeaders returns true when header row differs from data rows`` () = |
| 149 | + // First row is text (string) headers, data rows are numeric → types differ → headers detected |
| 150 | + let rows = |
| 151 | + [| [| "Name"; "Score" |] |
| 152 | + [| "Alice"; "95" |] |
| 153 | + [| "Bob"; "87" |] |] |
| 154 | + |
| 155 | + let hasHeaders, names, _, _ = HtmlInference.inferHeaders defaultParams rows |
| 156 | + hasHeaders |> should equal true |
| 157 | + names |> should equal (Some [| "Name"; "Score" |]) |
| 158 | + |
| 159 | +[<Test>] |
| 160 | +let ``inferHeaders returns false when all rows have same type`` () = |
| 161 | + // All rows are strings → header row type = data rows type → no headers inferred |
| 162 | + let rows = |
| 163 | + [| [| "Alice"; "Bob" |] |
| 164 | + [| "Carol"; "Dave" |] |
| 165 | + [| "Eve"; "Frank" |] |] |
| 166 | + |
| 167 | + let hasHeaders, names, _, _ = HtmlInference.inferHeaders defaultParams rows |
| 168 | + hasHeaders |> should equal false |
| 169 | + names |> should equal None |
| 170 | + |
| 171 | +[<Test>] |
| 172 | +let ``inferHeaders returns inferred type for data rows`` () = |
| 173 | + let rows = |
| 174 | + [| [| "Name"; "Age" |] |
| 175 | + [| "Alice"; "30" |] |
| 176 | + [| "Bob"; "25" |] |] |
| 177 | + |
| 178 | + let hasHeaders, _, _, dataType = HtmlInference.inferHeaders defaultParams rows |
| 179 | + hasHeaders |> should equal true |
| 180 | + dataType |> should not' (equal None) |
0 commit comments