Skip to content

Commit 243e97b

Browse files
github-actions[bot]Repo AssistCopilotCopilotdsyme
authored
[Repo Assist] Add DateOnly/TimeOnly inference support (closes #1461) (#1609)
* Add DateOnly/TimeOnly inference support (closes #1461) - Multi-target library projects to netstandard2.0;net8.0 to enable NET6_0_OR_GREATER conditional compilation - Add AsDateOnly/AsTimeOnly to TextConversions with guards against datetime strings being accepted by DateOnly/TimeOnly parsers - Add DateOnly to automatic type inference (matchValue); TimeOnly is NOT auto-inferred (ambiguous with TimeSpan) but available via explicit schema annotation (dateonly?/timeonly? in CSV headers) - Add DateOnly as subtype of DateTime in conversionTable; columns with mixed DateOnly+DateTime values unify to DateTime - Add ConvertDateOnly/ConvertTimeOnly to TextRuntime and JsonRuntime - Add DateOnly/TimeOnly cases to StructuralTypes, ConversionsGenerator, JsonConversionsGenerator, JsonConversions, JsonRuntime, CsvInference - Update tests for new DateOnly inference behaviour Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Initial plan * Update documentation and samples for DateOnly/TimeOnly inference Co-authored-by: dsyme <7204669+dsyme@users.noreply.github.com> * Initial plan * Fix DateOnly design-time type resolution error for netstandard2.0 targets Co-authored-by: dsyme <7204669+dsyme@users.noreply.github.com> * apply fantomas formatting * make optional * fix tests * fix tests * fix tests --------- Co-authored-by: Repo Assist <repo-assist@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: dsyme <7204669+dsyme@users.noreply.github.com> Co-authored-by: Don Syme <dsyme@users.noreply.github.com> Co-authored-by: Don Syme <dsyme@github.com>
1 parent 77b8539 commit 243e97b

34 files changed

Lines changed: 569 additions & 131 deletions

docs/library/CsvProvider.fsx

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ The following sample calls the `Load` method with an URL that points to a live C
8989
let msft = Stocks.Load(__SOURCE_DIRECTORY__ + "/../data/MSFT.csv").Cache()
9090

9191
// Look at the most recent row. Note the 'Date' property
92-
// is of type 'DateTime' and 'Open' has a type 'decimal'
92+
// is of type 'DateTime' by default. Set PreferDateOnly = true
93+
// to use 'DateOnly' on .NET 6+
94+
// and 'Open' has a type 'decimal'
9395
let firstRow = msft.Rows |> Seq.head
9496
let lastDate = firstRow.Date
9597
let lastOpen = firstRow.Open
@@ -107,8 +109,9 @@ collection of rows. We iterate over the rows using a `for` loop. As you can see
107109
to the columns in the CSV file.
108110
109111
As you can see, the type provider also infers types of individual rows. The `Date`
110-
property is inferred to be a `DateTime` (because the values in the sample file can all
111-
be parsed as dates) while HLOC prices are inferred as `decimal`.
112+
property is inferred as `DateTime` by default. When you set `PreferDateOnly = true`
113+
on .NET 6 and later, date-only strings (without a time component) are inferred as `DateOnly`.
114+
HLOC prices are inferred as `decimal`.
112115
113116
## Using units of measure
114117
@@ -269,8 +272,12 @@ it by specifying the `InferRows` static parameter of `CsvProvider`. If you speci
269272
Columns with only `0`, `1`, `Yes`, `No`, `True`, or `False` will be set to `bool`. Columns with numerical values
270273
will be set to either `int`, `int64`, `decimal`, or `float`, in that order of preference.
271274
272-
If a value is missing in any row, by default the CSV type provider will infer a nullable (for `int` and `int64`) or an optional
273-
(for `bool`, `DateTime` and `Guid`). When a `decimal` would be inferred but there are missing values, we will infer a
275+
On .NET 6 and later, when you set `PreferDateOnly = true`, columns whose values are all date-only strings (without a time component, e.g. `2023-01-15`)
276+
are inferred as `DateOnly`. Time-only strings are inferred as `TimeOnly`. If a column mixes `DateOnly` and `DateTime` values, it is unified to `DateTime`.
277+
By default (`PreferDateOnly = false`), all date values are inferred as `DateTime` for backward compatibility.
278+
279+
If a value is missing in any row, by default the CSV type provider will infer a nullable (for `int`, `int64`, and `DateOnly`) or an optional
280+
(for `bool`, `DateTime`, `DateTimeOffset`, and `Guid`). When a `decimal` would be inferred but there are missing values, we will infer a
274281
`float` instead, and use `Double.NaN` to represent those missing values. The `string` type is already inherently nullable,
275282
so by default, we won't generate a `string option`. If you prefer to use optionals in all cases, you can set the static parameter
276283
`PreferOptionals` to `true`. In that case, you'll never get an empty string or a `Double.NaN` and will always get a `None` instead.
@@ -303,6 +310,12 @@ specify the units of measure. This will override both `AssumeMissingValues` and
303310
* `guid`
304311
* `guid?`
305312
* `guid option`
313+
* `dateonly` (.NET 6+ only)
314+
* `dateonly?` (.NET 6+ only)
315+
* `dateonly option` (.NET 6+ only)
316+
* `timeonly` (.NET 6+ only)
317+
* `timeonly?` (.NET 6+ only)
318+
* `timeonly option` (.NET 6+ only)
306319
* `string`
307320
* `string option`.
308321
@@ -373,6 +386,23 @@ for row in titanic2.Rows |> Seq.truncate 10 do
373386
374387
You can even mix and match the two syntaxes like this `Schema="int64,DidSurvive,PClass->Passenger Class=string"`
375388
389+
### DateOnly and TimeOnly (on .NET 6+)
390+
391+
On .NET 6 and later, when you set `PreferDateOnly = true`, date-only strings are inferred as `DateOnly`
392+
and time-only strings as `TimeOnly`. For example, a column like `EventDate` containing values such as
393+
`2023-06-01` will be given the type `DateOnly`. By default (`PreferDateOnly = false`), dates are
394+
inferred as `DateTime` for backward compatibility.
395+
396+
You can also explicitly request a `DateOnly` or `TimeOnly` column using schema annotations:
397+
398+
[lang=text]
399+
EventDate (dateonly),Duration (timeonly?)
400+
2023-06-01,08:30:00
401+
2023-06-02,
402+
403+
In the example above, `EventDate` is explicitly annotated as `dateonly` and `Duration` is explicitly
404+
annotated as `timeonly?` (a nullable `TimeOnly`).
405+
376406
## Transforming CSV files
377407
378408
In addition to reading, `CsvProvider` also has support for transforming the row collection of CSV files. The operations

docs/library/JsonProvider.fsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,13 @@ type-safe access to the values, but not in the original order (if order matters,
119119
you can use the `mixed.JsonValue` property to get the underlying `JsonValue` and
120120
process it dynamically as described in [the documentation for `JsonValue`](JsonValue.html).
121121
122+
### Inferring date types
123+
124+
String values in JSON that look like dates are inferred as `DateTime` or `DateTimeOffset`.
125+
On .NET 6 and later, when you set `PreferDateOnly = true`, strings that represent a date without a time component (e.g. `"2023-01-15"`)
126+
are inferred as `DateOnly`, and time-only strings are inferred as `TimeOnly`. By default (`PreferDateOnly = false`),
127+
all dates are inferred as `DateTime` for backward compatibility.
128+
122129
### Inferring record types
123130
124131
Now let's look at a sample JSON document that contains a list of records. The

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@ let private nameToTypeForCsv =
3232
"datetimeoffset option", (typeof<DateTimeOffset>, TypeWrapper.Option)
3333
"timespan option", (typeof<TimeSpan>, TypeWrapper.Option)
3434
"guid option", (typeof<Guid>, TypeWrapper.Option)
35-
"string option", (typeof<string>, TypeWrapper.Option) ]
35+
"string option", (typeof<string>, TypeWrapper.Option)
36+
#if NET6_0_OR_GREATER
37+
"dateonly?", (typeof<DateOnly>, TypeWrapper.Nullable)
38+
"timeonly?", (typeof<TimeOnly>, TypeWrapper.Nullable)
39+
"dateonly option", (typeof<DateOnly>, TypeWrapper.Option)
40+
"timeonly option", (typeof<TimeOnly>, TypeWrapper.Option)
41+
#endif
42+
]
3643
|> dict
3744

3845
let private nameAndTypeRegex =

src/FSharp.Data.Csv.Core/FSharp.Data.Csv.Core.fsproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Project Sdk="Microsoft.NET.Sdk">
33
<PropertyGroup>
44
<OutputType>Library</OutputType>
5-
<TargetFramework>netstandard2.0</TargetFramework>
5+
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
66
<OtherFlags>$(OtherFlags) --warnon:1182 --nowarn:10001 --nowarn:44</OtherFlags>
77
<GenerateDocumentationFile>true</GenerateDocumentationFile>
88
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>

src/FSharp.Data.DesignTime/CommonProviderImplementation/ConversionsGenerator.fs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ let getConversionQuotation missingValuesStr cultureStr typ (value: Expr<string o
3232
<@@ TextRuntime.ConvertDateTimeOffset(cultureStr, %value) @@>
3333
elif typ = typeof<TimeSpan> then
3434
<@@ TextRuntime.ConvertTimeSpan(cultureStr, %value) @@>
35+
#if NET6_0_OR_GREATER
36+
elif typ = typeof<DateOnly> then
37+
<@@ TextRuntime.ConvertDateOnly(cultureStr, %value) @@>
38+
elif typ = typeof<TimeOnly> then
39+
<@@ TextRuntime.ConvertTimeOnly(cultureStr, %value) @@>
40+
#endif
3541
elif typ = typeof<Guid> then
3642
<@@ TextRuntime.ConvertGuid(%value) @@>
3743
else
@@ -58,6 +64,12 @@ let getBackConversionQuotation missingValuesStr cultureStr typ value : Expr<stri
5864
<@ TextRuntime.ConvertDateTimeOffsetBack(cultureStr, %%value) @>
5965
elif typ = typeof<TimeSpan> then
6066
<@ TextRuntime.ConvertTimeSpanBack(cultureStr, %%value) @>
67+
#if NET6_0_OR_GREATER
68+
elif typ = typeof<DateOnly> then
69+
<@ TextRuntime.ConvertDateOnlyBack(cultureStr, %%value) @>
70+
elif typ = typeof<TimeOnly> then
71+
<@ TextRuntime.ConvertTimeOnlyBack(cultureStr, %%value) @>
72+
#endif
6173
else
6274
failwith "getBackConversionQuotation: Unsupported primitive type"
6375

src/FSharp.Data.DesignTime/CommonProviderImplementation/Helpers.fs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,17 @@ module internal ProviderHelpers =
150150
member x.Inverse(denominator) : Type =
151151
ProvidedMeasureBuilder.Inverse(denominator) }
152152

153+
#if NET6_0_OR_GREATER
154+
/// Returns true when the target runtime assembly is a .NET 6+ build and therefore
155+
/// supports System.DateOnly / System.TimeOnly in generated types.
156+
let runtimeSupportsNet6Types (runtimeAssemblyPath: string) =
157+
// The assembly path contains the TFM, e.g. "…/net8.0/FSharp.Data.dll".
158+
// Anything matching "/net<N>." where N ≥ 6 is a net6+ target.
159+
let path = runtimeAssemblyPath.Replace('\\', '/').ToLowerInvariant()
160+
let m = System.Text.RegularExpressions.Regex.Match(path, @"/net(\d+)\.")
161+
m.Success && (int m.Groups.[1].Value) >= 6
162+
#endif
163+
153164
let asyncMap (resultType: Type) (valueAsync: Expr<Async<'T>>) (body: Expr<'T> -> Expr) =
154165
let (?) = QuotationBuilder.(?)
155166
let convFunc = ReflectionHelpers.makeDelegate (Expr.Cast >> body) typeof<'T>

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

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type public CsvProvider(cfg: TypeProviderConfig) as this =
5555
let encodingStr = args.[13] :?> string
5656
let resolutionFolder = args.[14] :?> string
5757
let resource = args.[15] :?> string
58+
let preferDateOnly = args.[16] :?> bool
5859

5960
// This provider already has a schema mechanism, so let's disable inline schemas.
6061
let inferenceMode = InferenceMode'.ValuesOnly
@@ -103,16 +104,25 @@ type public CsvProvider(cfg: TypeProviderConfig) as this =
103104
let inferredFields =
104105
use _holder = IO.logTime "Inference" sample
105106

106-
sampleCsv.InferColumnTypes(
107-
inferRows,
108-
TextRuntime.GetMissingValues missingValuesStr,
109-
inferenceMode,
110-
TextRuntime.GetCulture cultureStr,
111-
schema,
112-
assumeMissingValues,
113-
preferOptionals,
114-
unitsOfMeasureProvider
115-
)
107+
let fields =
108+
sampleCsv.InferColumnTypes(
109+
inferRows,
110+
TextRuntime.GetMissingValues missingValuesStr,
111+
inferenceMode,
112+
TextRuntime.GetCulture cultureStr,
113+
schema,
114+
assumeMissingValues,
115+
preferOptionals,
116+
unitsOfMeasureProvider
117+
)
118+
#if NET6_0_OR_GREATER
119+
if preferDateOnly && ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then
120+
fields
121+
else
122+
fields |> List.map StructuralInference.downgradeNet6PrimitiveProperty
123+
#else
124+
fields
125+
#endif
116126

117127
use _holder = IO.logTime "TypeGeneration" sample
118128

@@ -223,7 +233,8 @@ type public CsvProvider(cfg: TypeProviderConfig) as this =
223233
ProvidedStaticParameter("Culture", typeof<string>, parameterDefaultValue = "")
224234
ProvidedStaticParameter("Encoding", typeof<string>, parameterDefaultValue = "")
225235
ProvidedStaticParameter("ResolutionFolder", typeof<string>, parameterDefaultValue = "")
226-
ProvidedStaticParameter("EmbeddedResource", typeof<string>, parameterDefaultValue = "") ]
236+
ProvidedStaticParameter("EmbeddedResource", typeof<string>, parameterDefaultValue = "")
237+
ProvidedStaticParameter("PreferDateOnly", typeof<bool>, parameterDefaultValue = false) ]
227238

228239
let helpText =
229240
"""<summary>Typed representation of a CSV file.</summary>
@@ -246,7 +257,8 @@ type public CsvProvider(cfg: TypeProviderConfig) as this =
246257
<param name='Encoding'>The encoding used to read the sample. You can specify either the character set name or the codepage number. Defaults to UTF8 for files, and to ISO-8859-1 the for HTTP requests, unless <c>charset</c> is specified in the <c>Content-Type</c> response header.</param>
247258
<param name='ResolutionFolder'>A directory that is used when resolving relative file references (at design time and in hosted execution).</param>
248259
<param name='EmbeddedResource'>When specified, the type provider first attempts to load the sample from the specified resource
249-
(e.g. 'MyCompany.MyAssembly, resource_name.csv'). This is useful when exposing types generated by the type provider.</param>"""
260+
(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>"""
250262

251263
do csvProvTy.AddXmlDoc helpText
252264
do csvProvTy.DefineStaticParameters(parameters, buildTypes)

src/FSharp.Data.DesignTime/FSharp.Data.DesignTime.fsproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Project Sdk="Microsoft.NET.Sdk">
33
<PropertyGroup>
44
<IsPackable>false</IsPackable>
5-
<TargetFramework>netstandard2.0</TargetFramework>
5+
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
66
<DefineConstants>IS_DESIGNTIME;NO_GENERATIVE;$(DefineConstants)</DefineConstants>
77
<OtherFlags>$(OtherFlags) --warnon:1182 --nowarn:44</OtherFlags>
88
<GenerateDocumentationFile>false</GenerateDocumentationFile>

src/FSharp.Data.DesignTime/Html/HtmlGenerator.fs

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ module internal HtmlGenerator =
3737
let private createTableType
3838
getTableTypeName
3939
(inferenceParameters, missingValuesStr, cultureStr)
40+
supportsNet6Types
4041
(table: HtmlTable)
4142
=
4243

43-
let columns =
44+
let rawColumns =
4445
match table.InferedProperties with
4546
| Some inferedProperties -> inferedProperties
4647
| None ->
@@ -52,6 +53,16 @@ module internal HtmlGenerator =
5253
else
5354
table.Rows)
5455

56+
let columns =
57+
#if NET6_0_OR_GREATER
58+
if supportsNet6Types then
59+
rawColumns
60+
else
61+
rawColumns |> List.map StructuralInference.downgradeNet6PrimitiveProperty
62+
#else
63+
rawColumns
64+
#endif
65+
5566
let fields =
5667
columns
5768
|> List.mapi (fun index field ->
@@ -128,9 +139,24 @@ module internal HtmlGenerator =
128139

129140
create, tableType
130141

131-
let private createListType getListTypeName (inferenceParameters, missingValuesStr, cultureStr) (list: HtmlList) =
142+
let private createListType
143+
getListTypeName
144+
(inferenceParameters, missingValuesStr, cultureStr)
145+
supportsNet6Types
146+
(list: HtmlList)
147+
=
148+
149+
let rawColumns = HtmlInference.inferListType inferenceParameters list.Values
132150

133-
let columns = HtmlInference.inferListType inferenceParameters list.Values
151+
let columns =
152+
#if NET6_0_OR_GREATER
153+
if supportsNet6Types then
154+
rawColumns
155+
else
156+
StructuralInference.downgradeNet6Types rawColumns
157+
#else
158+
rawColumns
159+
#endif
134160

135161
let listItemType, conv =
136162
match columns with
@@ -185,14 +211,25 @@ module internal HtmlGenerator =
185211
let private createDefinitionListType
186212
getDefinitionListTypeName
187213
(inferenceParameters, missingValuesStr, cultureStr)
214+
supportsNet6Types
188215
(definitionList: HtmlDefinitionList)
189216
=
190217

191218
let getListTypeName = typeNameGenerator ()
192219

193220
let createListType index (list: HtmlList) =
194221

195-
let columns = HtmlInference.inferListType inferenceParameters list.Values
222+
let rawColumns = HtmlInference.inferListType inferenceParameters list.Values
223+
224+
let columns =
225+
#if NET6_0_OR_GREATER
226+
if supportsNet6Types then
227+
rawColumns
228+
else
229+
StructuralInference.downgradeNet6Types rawColumns
230+
#else
231+
rawColumns
232+
#endif
196233

197234
let listItemType, conv =
198235
match columns with
@@ -264,7 +301,7 @@ module internal HtmlGenerator =
264301

265302
definitionListType
266303

267-
let generateTypes asm ns typeName parameters htmlObjects =
304+
let generateTypes asm ns typeName parameters supportsNet6Types htmlObjects =
268305

269306
let htmlType =
270307
ProvidedTypeDefinition(
@@ -303,7 +340,10 @@ module internal HtmlGenerator =
303340
match htmlObj with
304341
| Table table ->
305342
let containerType = getOrCreateContainer "Tables"
306-
let create, tableType = createTableType getTypeName parameters table
343+
344+
let create, tableType =
345+
createTableType getTypeName parameters supportsNet6Types table
346+
307347
htmlType.AddMember tableType
308348

309349
containerType.AddMember
@@ -315,7 +355,7 @@ module internal HtmlGenerator =
315355

316356
| List list ->
317357
let containerType = getOrCreateContainer "Lists"
318-
let create, tableType = createListType getTypeName parameters list
358+
let create, tableType = createListType getTypeName parameters supportsNet6Types list
319359
htmlType.AddMember tableType
320360

321361
containerType.AddMember
@@ -326,7 +366,10 @@ module internal HtmlGenerator =
326366
)
327367
| DefinitionList definitionList ->
328368
let containerType = getOrCreateContainer "DefinitionLists"
329-
let tableType = createDefinitionListType getTypeName parameters definitionList
369+
370+
let tableType =
371+
createDefinitionListType getTypeName parameters supportsNet6Types definitionList
372+
330373
htmlType.AddMember tableType
331374

332375
containerType.AddMember

0 commit comments

Comments
 (0)