From bdc0de89a9213ec26f872ccd62c025209c4a9147 Mon Sep 17 00:00:00 2001 From: Repo Assist Date: Sun, 22 Feb 2026 19:20:10 +0000 Subject: [PATCH] Add StrictBooleans static parameter to CsvProvider When StrictBooleans=true: - Columns with only 0/1 values are inferred as int (not bool) - Columns with only yes/no values are inferred as string (not bool) - Only columns with exclusively true/false values become bool This implements the feature requested in issue #1417, endorsed by @dsyme. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/FSharp.Data.Csv.Core/CsvInference.fs | 36 +++++++++++++++++-- src/FSharp.Data.DesignTime/Csv/CsvProvider.fs | 10 ++++-- src/FSharp.Data.Html.Core/HtmlInference.fs | 1 + .../InferenceTests.fs | 2 +- .../TypeProviderInstantiation.fs | 9 +++-- tests/FSharp.Data.Tests/CsvProvider.fs | 32 +++++++++++++++++ 6 files changed, 81 insertions(+), 9 deletions(-) diff --git a/src/FSharp.Data.Csv.Core/CsvInference.fs b/src/FSharp.Data.Csv.Core/CsvInference.fs index 911bf3eae..14c4cb488 100644 --- a/src/FSharp.Data.Csv.Core/CsvInference.fs +++ b/src/FSharp.Data.Csv.Core/CsvInference.fs @@ -124,6 +124,7 @@ let internal inferCellType preferOptionals missingValues inferenceMode + strictBooleans cultureInfo unit (value: string) @@ -138,7 +139,32 @@ let internal inferCellType elif String.IsNullOrWhiteSpace value then InferedType.Null else - StructuralInference.getInferedTypeFromString unitsOfMeasureProvider inferenceMode cultureInfo value unit + let inferedType = + StructuralInference.getInferedTypeFromString unitsOfMeasureProvider inferenceMode cultureInfo value unit + + if strictBooleans then + // With StrictBooleans=true, only "true"/"false" trigger bool inference. + // 0/1 are treated as integers, and "yes"/"no" as strings. + match inferedType with + | InferedType.Primitive(typ, unt, optional, overrides) -> + if typ = typeof || typ = typeof then + // 0 and 1 become plain integers + InferedType.Primitive(typeof, unt, optional, overrides) + elif typ = typeof then + let trimmed = value.Trim() + + if + String.Compare(trimmed, "true", StringComparison.OrdinalIgnoreCase) = 0 + || String.Compare(trimmed, "false", StringComparison.OrdinalIgnoreCase) = 0 + then + inferedType // "true"/"false" remain bool + else + InferedType.Primitive(typeof, None, optional, overrides) // "yes"/"no" become string + else + inferedType + | _ -> inferedType + else + inferedType let internal parseHeaders headers numberOfColumns schema unitsOfMeasureProvider = @@ -278,6 +304,7 @@ let internal inferType inferRows missingValues inferenceMode + strictBooleans cultureInfo assumeMissingValues preferOptionals @@ -330,6 +357,7 @@ let internal inferType preferOptionals missingValues inferenceMode + strictBooleans cultureInfo unit value @@ -427,6 +455,7 @@ let internal inferColumnTypes inferRows missingValues inferenceMode + strictBooleans cultureInfo assumeMissingValues preferOptionals @@ -439,6 +468,7 @@ let internal inferColumnTypes inferRows missingValues inferenceMode + strictBooleans cultureInfo assumeMissingValues preferOptionals @@ -466,7 +496,8 @@ type CsvFile with schema, assumeMissingValues, preferOptionals, - unitsOfMeasureProvider + unitsOfMeasureProvider, + ?strictBooleans ) = let headerNamesAndUnits, schema = @@ -479,6 +510,7 @@ type CsvFile with inferRows missingValues inferenceMode + (defaultArg strictBooleans false) cultureInfo assumeMissingValues preferOptionals diff --git a/src/FSharp.Data.DesignTime/Csv/CsvProvider.fs b/src/FSharp.Data.DesignTime/Csv/CsvProvider.fs index 12066e7a3..9ac15ef05 100644 --- a/src/FSharp.Data.DesignTime/Csv/CsvProvider.fs +++ b/src/FSharp.Data.DesignTime/Csv/CsvProvider.fs @@ -56,6 +56,7 @@ type public CsvProvider(cfg: TypeProviderConfig) as this = let resolutionFolder = args.[14] :?> string let resource = args.[15] :?> string let preferDateOnly = args.[16] :?> bool + let strictBooleans = args.[17] :?> bool // This provider already has a schema mechanism, so let's disable inline schemas. let inferenceMode = InferenceMode'.ValuesOnly @@ -113,7 +114,8 @@ type public CsvProvider(cfg: TypeProviderConfig) as this = schema, assumeMissingValues, preferOptionals, - unitsOfMeasureProvider + unitsOfMeasureProvider, + strictBooleans ) #if NET6_0_OR_GREATER if preferDateOnly && ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then @@ -234,7 +236,8 @@ type public CsvProvider(cfg: TypeProviderConfig) as this = ProvidedStaticParameter("Encoding", typeof, parameterDefaultValue = "") ProvidedStaticParameter("ResolutionFolder", typeof, parameterDefaultValue = "") ProvidedStaticParameter("EmbeddedResource", typeof, parameterDefaultValue = "") - ProvidedStaticParameter("PreferDateOnly", typeof, parameterDefaultValue = false) ] + ProvidedStaticParameter("PreferDateOnly", typeof, parameterDefaultValue = false) + ProvidedStaticParameter("StrictBooleans", typeof, parameterDefaultValue = false) ] let helpText = """Typed representation of a CSV file. @@ -258,7 +261,8 @@ type public CsvProvider(cfg: TypeProviderConfig) as this = A directory that is used when resolving relative file references (at design time and in hosted execution). When specified, the type provider first attempts to load the sample from the specified resource (e.g. 'MyCompany.MyAssembly, resource_name.csv'). This is useful when exposing types generated by the type provider. - When true on .NET 6+, date-only strings are inferred as DateOnly and time-only strings as TimeOnly. Defaults to false for backward compatibility.""" + When true on .NET 6+, date-only strings are inferred as DateOnly and time-only strings as TimeOnly. Defaults to false for backward compatibility. + When true, only true and false (case-insensitive) are inferred as boolean. Values such as 0, 1, yes, and no are treated as integers or strings respectively. Defaults to false.""" do csvProvTy.AddXmlDoc helpText do csvProvTy.DefineStaticParameters(parameters, buildTypes) diff --git a/src/FSharp.Data.Html.Core/HtmlInference.fs b/src/FSharp.Data.Html.Core/HtmlInference.fs index 71e47e22b..43e45375c 100644 --- a/src/FSharp.Data.Html.Core/HtmlInference.fs +++ b/src/FSharp.Data.Html.Core/HtmlInference.fs @@ -27,6 +27,7 @@ let internal inferColumns parameters (headerNamesAndUnits: _[]) rows = inferRows parameters.MissingValues parameters.InferenceMode + false parameters.CultureInfo assumeMissingValues parameters.PreferOptionals diff --git a/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs b/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs index 7b2795697..560e41d32 100644 --- a/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs +++ b/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs @@ -24,7 +24,7 @@ let internal unitsOfMeasureProvider = ProviderHelpers.unitsOfMeasureProvider let internal inferType (csv:CsvFile) inferRows missingValues cultureInfo schema assumeMissingValues preferOptionals = let headerNamesAndUnits, schema = parseHeaders csv.Headers csv.NumberOfColumns schema unitsOfMeasureProvider - inferType headerNamesAndUnits schema (csv.Rows |> Seq.map (fun x -> x.Columns)) inferRows missingValues inferenceMode cultureInfo assumeMissingValues preferOptionals unitsOfMeasureProvider + inferType headerNamesAndUnits schema (csv.Rows |> Seq.map (fun x -> x.Columns)) inferRows missingValues inferenceMode false cultureInfo assumeMissingValues preferOptionals unitsOfMeasureProvider let internal toRecord fields = InferedType.Record(None, fields, false) diff --git a/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs b/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs index e2714bcc1..d3af515b3 100644 --- a/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs +++ b/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs @@ -26,7 +26,8 @@ type internal CsvProviderArgs = Encoding : string ResolutionFolder : string EmbeddedResource : string - PreferDateOnly : bool } + PreferDateOnly : bool + StrictBooleans : bool } type internal XmlProviderArgs = { Sample : string @@ -98,7 +99,8 @@ type internal TypeProviderInstantiation = box x.Encoding box x.ResolutionFolder box x.EmbeddedResource - box x.PreferDateOnly |] + box x.PreferDateOnly + box x.StrictBooleans |] | Xml x -> (fun cfg -> new XmlProvider(cfg) :> TypeProviderForNamespaces), [| box x.Sample @@ -232,7 +234,8 @@ type internal TypeProviderInstantiation = CacheRows = false ResolutionFolder = "" EmbeddedResource = "" - PreferDateOnly = false } + PreferDateOnly = false + StrictBooleans = false } | "Xml" -> Xml { Sample = args.[1] SampleIsList = args.[2] |> bool.Parse diff --git a/tests/FSharp.Data.Tests/CsvProvider.fs b/tests/FSharp.Data.Tests/CsvProvider.fs index ec67c88a5..6827f13c3 100644 --- a/tests/FSharp.Data.Tests/CsvProvider.fs +++ b/tests/FSharp.Data.Tests/CsvProvider.fs @@ -17,6 +17,17 @@ let [] simpleCsv = """ type SimpleCsv = CsvProvider +let [] csvWithBitValues = """ + Flag,Status,Score + 0,yes,42 + 1,no,7 + 1,yes,3 """ + +// With StrictBooleans=true, 0/1 infer as int and yes/no infer as string +type StrictBoolCsv = CsvProvider +// Without StrictBooleans, 0/1 infer as bool and yes/no infer as bool (default) +type NonStrictBoolCsv = CsvProvider + [] let ``Bool column correctly inferred and accessed`` () = let csv = SimpleCsv.GetSample() @@ -24,6 +35,27 @@ let ``Bool column correctly inferred and accessed`` () = let actual:bool = first.Column1 actual |> should be True +[] +let ``StrictBooleans: 0 and 1 are inferred as int not bool`` () = + let csv = StrictBoolCsv.GetSample() + let first = csv.Rows |> Seq.head + let flagAsInt: int = first.Flag // Should compile: Flag is int, not bool + flagAsInt |> should equal 0 + +[] +let ``StrictBooleans: yes and no are inferred as string not bool`` () = + let csv = StrictBoolCsv.GetSample() + let first = csv.Rows |> Seq.head + let statusAsString: string = first.Status // Should compile: Status is string, not bool + statusAsString |> should equal "yes" + +[] +let ``Without StrictBooleans: 0 and 1 are inferred as bool by default`` () = + let csv = NonStrictBoolCsv.GetSample() + let first = csv.Rows |> Seq.head + let flagAsBool: bool = first.Flag // Should compile: Flag is bool + flagAsBool |> should be False + [] let ``Decimal column correctly inferred and accessed`` () = let csv = SimpleCsv.GetSample()