Skip to content

Commit eb28d36

Browse files
github-actions[bot]Repo AssistCopilotdsyme
authored
Add StrictBooleans static parameter to CsvProvider (#1621)
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: Repo Assist <repo-assist@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Don Syme <dsyme@users.noreply.github.com>
1 parent 827c9b4 commit eb28d36

6 files changed

Lines changed: 81 additions & 9 deletions

File tree

src/FSharp.Data.Csv.Core/CsvInference.fs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ let internal inferCellType
124124
preferOptionals
125125
missingValues
126126
inferenceMode
127+
strictBooleans
127128
cultureInfo
128129
unit
129130
(value: string)
@@ -138,7 +139,32 @@ let internal inferCellType
138139
elif String.IsNullOrWhiteSpace value then
139140
InferedType.Null
140141
else
141-
StructuralInference.getInferedTypeFromString unitsOfMeasureProvider inferenceMode cultureInfo value unit
142+
let inferedType =
143+
StructuralInference.getInferedTypeFromString unitsOfMeasureProvider inferenceMode cultureInfo value unit
144+
145+
if strictBooleans then
146+
// With StrictBooleans=true, only "true"/"false" trigger bool inference.
147+
// 0/1 are treated as integers, and "yes"/"no" as strings.
148+
match inferedType with
149+
| InferedType.Primitive(typ, unt, optional, overrides) ->
150+
if typ = typeof<Bit0> || typ = typeof<Bit1> then
151+
// 0 and 1 become plain integers
152+
InferedType.Primitive(typeof<int>, unt, optional, overrides)
153+
elif typ = typeof<bool> then
154+
let trimmed = value.Trim()
155+
156+
if
157+
String.Compare(trimmed, "true", StringComparison.OrdinalIgnoreCase) = 0
158+
|| String.Compare(trimmed, "false", StringComparison.OrdinalIgnoreCase) = 0
159+
then
160+
inferedType // "true"/"false" remain bool
161+
else
162+
InferedType.Primitive(typeof<string>, None, optional, overrides) // "yes"/"no" become string
163+
else
164+
inferedType
165+
| _ -> inferedType
166+
else
167+
inferedType
142168

143169
let internal parseHeaders headers numberOfColumns schema unitsOfMeasureProvider =
144170

@@ -278,6 +304,7 @@ let internal inferType
278304
inferRows
279305
missingValues
280306
inferenceMode
307+
strictBooleans
281308
cultureInfo
282309
assumeMissingValues
283310
preferOptionals
@@ -330,6 +357,7 @@ let internal inferType
330357
preferOptionals
331358
missingValues
332359
inferenceMode
360+
strictBooleans
333361
cultureInfo
334362
unit
335363
value
@@ -427,6 +455,7 @@ let internal inferColumnTypes
427455
inferRows
428456
missingValues
429457
inferenceMode
458+
strictBooleans
430459
cultureInfo
431460
assumeMissingValues
432461
preferOptionals
@@ -439,6 +468,7 @@ let internal inferColumnTypes
439468
inferRows
440469
missingValues
441470
inferenceMode
471+
strictBooleans
442472
cultureInfo
443473
assumeMissingValues
444474
preferOptionals
@@ -466,7 +496,8 @@ type CsvFile with
466496
schema,
467497
assumeMissingValues,
468498
preferOptionals,
469-
unitsOfMeasureProvider
499+
unitsOfMeasureProvider,
500+
?strictBooleans
470501
) =
471502

472503
let headerNamesAndUnits, schema =
@@ -479,6 +510,7 @@ type CsvFile with
479510
inferRows
480511
missingValues
481512
inferenceMode
513+
(defaultArg strictBooleans false)
482514
cultureInfo
483515
assumeMissingValues
484516
preferOptionals

src/FSharp.Data.DesignTime/Csv/CsvProvider.fs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type public CsvProvider(cfg: TypeProviderConfig) as this =
5656
let resolutionFolder = args.[14] :?> string
5757
let resource = args.[15] :?> string
5858
let preferDateOnly = args.[16] :?> bool
59+
let strictBooleans = args.[17] :?> bool
5960

6061
// This provider already has a schema mechanism, so let's disable inline schemas.
6162
let inferenceMode = InferenceMode'.ValuesOnly
@@ -113,7 +114,8 @@ type public CsvProvider(cfg: TypeProviderConfig) as this =
113114
schema,
114115
assumeMissingValues,
115116
preferOptionals,
116-
unitsOfMeasureProvider
117+
unitsOfMeasureProvider,
118+
strictBooleans
117119
)
118120
#if NET6_0_OR_GREATER
119121
if preferDateOnly && ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then
@@ -234,7 +236,8 @@ type public CsvProvider(cfg: TypeProviderConfig) as this =
234236
ProvidedStaticParameter("Encoding", typeof<string>, parameterDefaultValue = "")
235237
ProvidedStaticParameter("ResolutionFolder", typeof<string>, parameterDefaultValue = "")
236238
ProvidedStaticParameter("EmbeddedResource", typeof<string>, parameterDefaultValue = "")
237-
ProvidedStaticParameter("PreferDateOnly", typeof<bool>, parameterDefaultValue = false) ]
239+
ProvidedStaticParameter("PreferDateOnly", typeof<bool>, parameterDefaultValue = false)
240+
ProvidedStaticParameter("StrictBooleans", typeof<bool>, parameterDefaultValue = false) ]
238241

239242
let helpText =
240243
"""<summary>Typed representation of a CSV file.</summary>
@@ -258,7 +261,8 @@ type public CsvProvider(cfg: TypeProviderConfig) as this =
258261
<param name='ResolutionFolder'>A directory that is used when resolving relative file references (at design time and in hosted execution).</param>
259262
<param name='EmbeddedResource'>When specified, the type provider first attempts to load the sample from the specified resource
260263
(e.g. 'MyCompany.MyAssembly, resource_name.csv'). This is useful when exposing types generated by the type provider.</param>
261-
<param name='PreferDateOnly'>When true on .NET 6+, date-only strings are inferred as DateOnly and time-only strings as TimeOnly. Defaults to false for backward compatibility.</param>"""
264+
<param name='PreferDateOnly'>When true on .NET 6+, date-only strings are inferred as DateOnly and time-only strings as TimeOnly. Defaults to false for backward compatibility.</param>
265+
<param name='StrictBooleans'>When true, only <c>true</c> and <c>false</c> (case-insensitive) are inferred as boolean. Values such as <c>0</c>, <c>1</c>, <c>yes</c>, and <c>no</c> are treated as integers or strings respectively. Defaults to false.</param>"""
262266

263267
do csvProvTy.AddXmlDoc helpText
264268
do csvProvTy.DefineStaticParameters(parameters, buildTypes)

src/FSharp.Data.Html.Core/HtmlInference.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ let internal inferColumns parameters (headerNamesAndUnits: _[]) rows =
2727
inferRows
2828
parameters.MissingValues
2929
parameters.InferenceMode
30+
false
3031
parameters.CultureInfo
3132
assumeMissingValues
3233
parameters.PreferOptionals

tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ let internal unitsOfMeasureProvider = ProviderHelpers.unitsOfMeasureProvider
2424

2525
let internal inferType (csv:CsvFile) inferRows missingValues cultureInfo schema assumeMissingValues preferOptionals =
2626
let headerNamesAndUnits, schema = parseHeaders csv.Headers csv.NumberOfColumns schema unitsOfMeasureProvider
27-
inferType headerNamesAndUnits schema (csv.Rows |> Seq.map (fun x -> x.Columns)) inferRows missingValues inferenceMode cultureInfo assumeMissingValues preferOptionals unitsOfMeasureProvider
27+
inferType headerNamesAndUnits schema (csv.Rows |> Seq.map (fun x -> x.Columns)) inferRows missingValues inferenceMode false cultureInfo assumeMissingValues preferOptionals unitsOfMeasureProvider
2828

2929
let internal toRecord fields = InferedType.Record(None, fields, false)
3030

tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ type internal CsvProviderArgs =
2626
Encoding : string
2727
ResolutionFolder : string
2828
EmbeddedResource : string
29-
PreferDateOnly : bool }
29+
PreferDateOnly : bool
30+
StrictBooleans : bool }
3031

3132
type internal XmlProviderArgs =
3233
{ Sample : string
@@ -98,7 +99,8 @@ type internal TypeProviderInstantiation =
9899
box x.Encoding
99100
box x.ResolutionFolder
100101
box x.EmbeddedResource
101-
box x.PreferDateOnly |]
102+
box x.PreferDateOnly
103+
box x.StrictBooleans |]
102104
| Xml x ->
103105
(fun cfg -> new XmlProvider(cfg) :> TypeProviderForNamespaces),
104106
[| box x.Sample
@@ -232,7 +234,8 @@ type internal TypeProviderInstantiation =
232234
CacheRows = false
233235
ResolutionFolder = ""
234236
EmbeddedResource = ""
235-
PreferDateOnly = false }
237+
PreferDateOnly = false
238+
StrictBooleans = false }
236239
| "Xml" ->
237240
Xml { Sample = args.[1]
238241
SampleIsList = args.[2] |> bool.Parse

tests/FSharp.Data.Tests/CsvProvider.fs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,45 @@ let [<Literal>] simpleCsv = """
1717

1818
type SimpleCsv = CsvProvider<simpleCsv>
1919

20+
let [<Literal>] csvWithBitValues = """
21+
Flag,Status,Score
22+
0,yes,42
23+
1,no,7
24+
1,yes,3 """
25+
26+
// With StrictBooleans=true, 0/1 infer as int and yes/no infer as string
27+
type StrictBoolCsv = CsvProvider<csvWithBitValues, StrictBooleans=true>
28+
// Without StrictBooleans, 0/1 infer as bool and yes/no infer as bool (default)
29+
type NonStrictBoolCsv = CsvProvider<csvWithBitValues>
30+
2031
[<Test>]
2132
let ``Bool column correctly inferred and accessed`` () =
2233
let csv = SimpleCsv.GetSample()
2334
let first = csv.Rows |> Seq.head
2435
let actual:bool = first.Column1
2536
actual |> should be True
2637

38+
[<Test>]
39+
let ``StrictBooleans: 0 and 1 are inferred as int not bool`` () =
40+
let csv = StrictBoolCsv.GetSample()
41+
let first = csv.Rows |> Seq.head
42+
let flagAsInt: int = first.Flag // Should compile: Flag is int, not bool
43+
flagAsInt |> should equal 0
44+
45+
[<Test>]
46+
let ``StrictBooleans: yes and no are inferred as string not bool`` () =
47+
let csv = StrictBoolCsv.GetSample()
48+
let first = csv.Rows |> Seq.head
49+
let statusAsString: string = first.Status // Should compile: Status is string, not bool
50+
statusAsString |> should equal "yes"
51+
52+
[<Test>]
53+
let ``Without StrictBooleans: 0 and 1 are inferred as bool by default`` () =
54+
let csv = NonStrictBoolCsv.GetSample()
55+
let first = csv.Rows |> Seq.head
56+
let flagAsBool: bool = first.Flag // Should compile: Flag is bool
57+
flagAsBool |> should be False
58+
2759
[<Test>]
2860
let ``Decimal column correctly inferred and accessed`` () =
2961
let csv = SimpleCsv.GetSample()

0 commit comments

Comments
 (0)