diff --git a/docs/library/CsvProvider.fsx b/docs/library/CsvProvider.fsx index 26895df50..95e728047 100644 --- a/docs/library/CsvProvider.fsx +++ b/docs/library/CsvProvider.fsx @@ -89,7 +89,9 @@ The following sample calls the `Load` method with an URL that points to a live C let msft = Stocks.Load(__SOURCE_DIRECTORY__ + "/../data/MSFT.csv").Cache() // Look at the most recent row. Note the 'Date' property -// is of type 'DateTime' and 'Open' has a type 'decimal' +// is of type 'DateTime' by default. Set PreferDateOnly = true +// to use 'DateOnly' on .NET 6+ +// and 'Open' has a type 'decimal' let firstRow = msft.Rows |> Seq.head let lastDate = firstRow.Date let lastOpen = firstRow.Open @@ -107,8 +109,9 @@ collection of rows. We iterate over the rows using a `for` loop. As you can see to the columns in the CSV file. As you can see, the type provider also infers types of individual rows. The `Date` -property is inferred to be a `DateTime` (because the values in the sample file can all -be parsed as dates) while HLOC prices are inferred as `decimal`. +property is inferred as `DateTime` by default. When you set `PreferDateOnly = true` +on .NET 6 and later, date-only strings (without a time component) are inferred as `DateOnly`. +HLOC prices are inferred as `decimal`. ## Using units of measure @@ -269,8 +272,12 @@ it by specifying the `InferRows` static parameter of `CsvProvider`. If you speci Columns with only `0`, `1`, `Yes`, `No`, `True`, or `False` will be set to `bool`. Columns with numerical values will be set to either `int`, `int64`, `decimal`, or `float`, in that order of preference. -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 -(for `bool`, `DateTime` and `Guid`). When a `decimal` would be inferred but there are missing values, we will infer a +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`) +are inferred as `DateOnly`. Time-only strings are inferred as `TimeOnly`. If a column mixes `DateOnly` and `DateTime` values, it is unified to `DateTime`. +By default (`PreferDateOnly = false`), all date values are inferred as `DateTime` for backward compatibility. + +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 +(for `bool`, `DateTime`, `DateTimeOffset`, and `Guid`). When a `decimal` would be inferred but there are missing values, we will infer a `float` instead, and use `Double.NaN` to represent those missing values. The `string` type is already inherently nullable, 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 `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 * `guid` * `guid?` * `guid option` +* `dateonly` (.NET 6+ only) +* `dateonly?` (.NET 6+ only) +* `dateonly option` (.NET 6+ only) +* `timeonly` (.NET 6+ only) +* `timeonly?` (.NET 6+ only) +* `timeonly option` (.NET 6+ only) * `string` * `string option`. @@ -373,6 +386,23 @@ for row in titanic2.Rows |> Seq.truncate 10 do You can even mix and match the two syntaxes like this `Schema="int64,DidSurvive,PClass->Passenger Class=string"` +### DateOnly and TimeOnly (on .NET 6+) + +On .NET 6 and later, when you set `PreferDateOnly = true`, date-only strings are inferred as `DateOnly` +and time-only strings as `TimeOnly`. For example, a column like `EventDate` containing values such as +`2023-06-01` will be given the type `DateOnly`. By default (`PreferDateOnly = false`), dates are +inferred as `DateTime` for backward compatibility. + +You can also explicitly request a `DateOnly` or `TimeOnly` column using schema annotations: + + [lang=text] + EventDate (dateonly),Duration (timeonly?) + 2023-06-01,08:30:00 + 2023-06-02, + +In the example above, `EventDate` is explicitly annotated as `dateonly` and `Duration` is explicitly +annotated as `timeonly?` (a nullable `TimeOnly`). + ## Transforming CSV files In addition to reading, `CsvProvider` also has support for transforming the row collection of CSV files. The operations diff --git a/docs/library/JsonProvider.fsx b/docs/library/JsonProvider.fsx index e753d2b01..51bb97d90 100644 --- a/docs/library/JsonProvider.fsx +++ b/docs/library/JsonProvider.fsx @@ -119,6 +119,13 @@ type-safe access to the values, but not in the original order (if order matters, you can use the `mixed.JsonValue` property to get the underlying `JsonValue` and process it dynamically as described in [the documentation for `JsonValue`](JsonValue.html). +### Inferring date types + +String values in JSON that look like dates are inferred as `DateTime` or `DateTimeOffset`. +On .NET 6 and later, when you set `PreferDateOnly = true`, strings that represent a date without a time component (e.g. `"2023-01-15"`) +are inferred as `DateOnly`, and time-only strings are inferred as `TimeOnly`. By default (`PreferDateOnly = false`), +all dates are inferred as `DateTime` for backward compatibility. + ### Inferring record types Now let's look at a sample JSON document that contains a list of records. The diff --git a/src/FSharp.Data.Csv.Core/CsvInference.fs b/src/FSharp.Data.Csv.Core/CsvInference.fs index e862b0a67..911bf3eae 100644 --- a/src/FSharp.Data.Csv.Core/CsvInference.fs +++ b/src/FSharp.Data.Csv.Core/CsvInference.fs @@ -32,7 +32,14 @@ let private nameToTypeForCsv = "datetimeoffset option", (typeof, TypeWrapper.Option) "timespan option", (typeof, TypeWrapper.Option) "guid option", (typeof, TypeWrapper.Option) - "string option", (typeof, TypeWrapper.Option) ] + "string option", (typeof, TypeWrapper.Option) +#if NET6_0_OR_GREATER + "dateonly?", (typeof, TypeWrapper.Nullable) + "timeonly?", (typeof, TypeWrapper.Nullable) + "dateonly option", (typeof, TypeWrapper.Option) + "timeonly option", (typeof, TypeWrapper.Option) +#endif + ] |> dict let private nameAndTypeRegex = diff --git a/src/FSharp.Data.Csv.Core/FSharp.Data.Csv.Core.fsproj b/src/FSharp.Data.Csv.Core/FSharp.Data.Csv.Core.fsproj index d6f065cab..214e21279 100644 --- a/src/FSharp.Data.Csv.Core/FSharp.Data.Csv.Core.fsproj +++ b/src/FSharp.Data.Csv.Core/FSharp.Data.Csv.Core.fsproj @@ -2,7 +2,7 @@ Library - netstandard2.0 + netstandard2.0;net8.0 $(OtherFlags) --warnon:1182 --nowarn:10001 --nowarn:44 true false diff --git a/src/FSharp.Data.DesignTime/CommonProviderImplementation/ConversionsGenerator.fs b/src/FSharp.Data.DesignTime/CommonProviderImplementation/ConversionsGenerator.fs index 71090a64b..007350767 100644 --- a/src/FSharp.Data.DesignTime/CommonProviderImplementation/ConversionsGenerator.fs +++ b/src/FSharp.Data.DesignTime/CommonProviderImplementation/ConversionsGenerator.fs @@ -32,6 +32,12 @@ let getConversionQuotation missingValuesStr cultureStr typ (value: Expr elif typ = typeof then <@@ TextRuntime.ConvertTimeSpan(cultureStr, %value) @@> +#if NET6_0_OR_GREATER + elif typ = typeof then + <@@ TextRuntime.ConvertDateOnly(cultureStr, %value) @@> + elif typ = typeof then + <@@ TextRuntime.ConvertTimeOnly(cultureStr, %value) @@> +#endif elif typ = typeof then <@@ TextRuntime.ConvertGuid(%value) @@> else @@ -58,6 +64,12 @@ let getBackConversionQuotation missingValuesStr cultureStr typ value : Expr elif typ = typeof then <@ TextRuntime.ConvertTimeSpanBack(cultureStr, %%value) @> +#if NET6_0_OR_GREATER + elif typ = typeof then + <@ TextRuntime.ConvertDateOnlyBack(cultureStr, %%value) @> + elif typ = typeof then + <@ TextRuntime.ConvertTimeOnlyBack(cultureStr, %%value) @> +#endif else failwith "getBackConversionQuotation: Unsupported primitive type" diff --git a/src/FSharp.Data.DesignTime/CommonProviderImplementation/Helpers.fs b/src/FSharp.Data.DesignTime/CommonProviderImplementation/Helpers.fs index 958fc6980..0bb778c95 100644 --- a/src/FSharp.Data.DesignTime/CommonProviderImplementation/Helpers.fs +++ b/src/FSharp.Data.DesignTime/CommonProviderImplementation/Helpers.fs @@ -150,6 +150,17 @@ module internal ProviderHelpers = member x.Inverse(denominator) : Type = ProvidedMeasureBuilder.Inverse(denominator) } +#if NET6_0_OR_GREATER + /// Returns true when the target runtime assembly is a .NET 6+ build and therefore + /// supports System.DateOnly / System.TimeOnly in generated types. + let runtimeSupportsNet6Types (runtimeAssemblyPath: string) = + // The assembly path contains the TFM, e.g. "…/net8.0/FSharp.Data.dll". + // Anything matching "/net." where N ≥ 6 is a net6+ target. + let path = runtimeAssemblyPath.Replace('\\', '/').ToLowerInvariant() + let m = System.Text.RegularExpressions.Regex.Match(path, @"/net(\d+)\.") + m.Success && (int m.Groups.[1].Value) >= 6 +#endif + let asyncMap (resultType: Type) (valueAsync: Expr>) (body: Expr<'T> -> Expr) = let (?) = QuotationBuilder.(?) let convFunc = ReflectionHelpers.makeDelegate (Expr.Cast >> body) typeof<'T> diff --git a/src/FSharp.Data.DesignTime/Csv/CsvProvider.fs b/src/FSharp.Data.DesignTime/Csv/CsvProvider.fs index daf5ea51b..12066e7a3 100644 --- a/src/FSharp.Data.DesignTime/Csv/CsvProvider.fs +++ b/src/FSharp.Data.DesignTime/Csv/CsvProvider.fs @@ -55,6 +55,7 @@ type public CsvProvider(cfg: TypeProviderConfig) as this = let encodingStr = args.[13] :?> string let resolutionFolder = args.[14] :?> string let resource = args.[15] :?> string + let preferDateOnly = args.[16] :?> bool // This provider already has a schema mechanism, so let's disable inline schemas. let inferenceMode = InferenceMode'.ValuesOnly @@ -103,16 +104,25 @@ type public CsvProvider(cfg: TypeProviderConfig) as this = let inferredFields = use _holder = IO.logTime "Inference" sample - sampleCsv.InferColumnTypes( - inferRows, - TextRuntime.GetMissingValues missingValuesStr, - inferenceMode, - TextRuntime.GetCulture cultureStr, - schema, - assumeMissingValues, - preferOptionals, - unitsOfMeasureProvider - ) + let fields = + sampleCsv.InferColumnTypes( + inferRows, + TextRuntime.GetMissingValues missingValuesStr, + inferenceMode, + TextRuntime.GetCulture cultureStr, + schema, + assumeMissingValues, + preferOptionals, + unitsOfMeasureProvider + ) +#if NET6_0_OR_GREATER + if preferDateOnly && ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then + fields + else + fields |> List.map StructuralInference.downgradeNet6PrimitiveProperty +#else + fields +#endif use _holder = IO.logTime "TypeGeneration" sample @@ -223,7 +233,8 @@ type public CsvProvider(cfg: TypeProviderConfig) as this = ProvidedStaticParameter("Culture", typeof, parameterDefaultValue = "") ProvidedStaticParameter("Encoding", typeof, parameterDefaultValue = "") ProvidedStaticParameter("ResolutionFolder", typeof, parameterDefaultValue = "") - ProvidedStaticParameter("EmbeddedResource", typeof, parameterDefaultValue = "") ] + ProvidedStaticParameter("EmbeddedResource", typeof, parameterDefaultValue = "") + ProvidedStaticParameter("PreferDateOnly", typeof, parameterDefaultValue = false) ] let helpText = """Typed representation of a CSV file. @@ -246,7 +257,8 @@ type public CsvProvider(cfg: TypeProviderConfig) as this = 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 charset is specified in the Content-Type response header. 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.""" + (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.""" do csvProvTy.AddXmlDoc helpText do csvProvTy.DefineStaticParameters(parameters, buildTypes) diff --git a/src/FSharp.Data.DesignTime/FSharp.Data.DesignTime.fsproj b/src/FSharp.Data.DesignTime/FSharp.Data.DesignTime.fsproj index 3a6aceff3..ab6324947 100755 --- a/src/FSharp.Data.DesignTime/FSharp.Data.DesignTime.fsproj +++ b/src/FSharp.Data.DesignTime/FSharp.Data.DesignTime.fsproj @@ -2,7 +2,7 @@ false - netstandard2.0 + netstandard2.0;net8.0 IS_DESIGNTIME;NO_GENERATIVE;$(DefineConstants) $(OtherFlags) --warnon:1182 --nowarn:44 false diff --git a/src/FSharp.Data.DesignTime/Html/HtmlGenerator.fs b/src/FSharp.Data.DesignTime/Html/HtmlGenerator.fs index 3d3625d56..f3362b694 100644 --- a/src/FSharp.Data.DesignTime/Html/HtmlGenerator.fs +++ b/src/FSharp.Data.DesignTime/Html/HtmlGenerator.fs @@ -37,10 +37,11 @@ module internal HtmlGenerator = let private createTableType getTableTypeName (inferenceParameters, missingValuesStr, cultureStr) + supportsNet6Types (table: HtmlTable) = - let columns = + let rawColumns = match table.InferedProperties with | Some inferedProperties -> inferedProperties | None -> @@ -52,6 +53,16 @@ module internal HtmlGenerator = else table.Rows) + let columns = +#if NET6_0_OR_GREATER + if supportsNet6Types then + rawColumns + else + rawColumns |> List.map StructuralInference.downgradeNet6PrimitiveProperty +#else + rawColumns +#endif + let fields = columns |> List.mapi (fun index field -> @@ -128,9 +139,24 @@ module internal HtmlGenerator = create, tableType - let private createListType getListTypeName (inferenceParameters, missingValuesStr, cultureStr) (list: HtmlList) = + let private createListType + getListTypeName + (inferenceParameters, missingValuesStr, cultureStr) + supportsNet6Types + (list: HtmlList) + = + + let rawColumns = HtmlInference.inferListType inferenceParameters list.Values - let columns = HtmlInference.inferListType inferenceParameters list.Values + let columns = +#if NET6_0_OR_GREATER + if supportsNet6Types then + rawColumns + else + StructuralInference.downgradeNet6Types rawColumns +#else + rawColumns +#endif let listItemType, conv = match columns with @@ -185,6 +211,7 @@ module internal HtmlGenerator = let private createDefinitionListType getDefinitionListTypeName (inferenceParameters, missingValuesStr, cultureStr) + supportsNet6Types (definitionList: HtmlDefinitionList) = @@ -192,7 +219,17 @@ module internal HtmlGenerator = let createListType index (list: HtmlList) = - let columns = HtmlInference.inferListType inferenceParameters list.Values + let rawColumns = HtmlInference.inferListType inferenceParameters list.Values + + let columns = +#if NET6_0_OR_GREATER + if supportsNet6Types then + rawColumns + else + StructuralInference.downgradeNet6Types rawColumns +#else + rawColumns +#endif let listItemType, conv = match columns with @@ -264,7 +301,7 @@ module internal HtmlGenerator = definitionListType - let generateTypes asm ns typeName parameters htmlObjects = + let generateTypes asm ns typeName parameters supportsNet6Types htmlObjects = let htmlType = ProvidedTypeDefinition( @@ -303,7 +340,10 @@ module internal HtmlGenerator = match htmlObj with | Table table -> let containerType = getOrCreateContainer "Tables" - let create, tableType = createTableType getTypeName parameters table + + let create, tableType = + createTableType getTypeName parameters supportsNet6Types table + htmlType.AddMember tableType containerType.AddMember @@ -315,7 +355,7 @@ module internal HtmlGenerator = | List list -> let containerType = getOrCreateContainer "Lists" - let create, tableType = createListType getTypeName parameters list + let create, tableType = createListType getTypeName parameters supportsNet6Types list htmlType.AddMember tableType containerType.AddMember @@ -326,7 +366,10 @@ module internal HtmlGenerator = ) | DefinitionList definitionList -> let containerType = getOrCreateContainer "DefinitionLists" - let tableType = createDefinitionListType getTypeName parameters definitionList + + let tableType = + createDefinitionListType getTypeName parameters supportsNet6Types definitionList + htmlType.AddMember tableType containerType.AddMember diff --git a/src/FSharp.Data.DesignTime/Html/HtmlProvider.fs b/src/FSharp.Data.DesignTime/Html/HtmlProvider.fs index 62828d91e..bedb1a34b 100644 --- a/src/FSharp.Data.DesignTime/Html/HtmlProvider.fs +++ b/src/FSharp.Data.DesignTime/Html/HtmlProvider.fs @@ -41,6 +41,7 @@ type public HtmlProvider(cfg: TypeProviderConfig) as this = let encodingStr = args.[5] :?> string let resolutionFolder = args.[6] :?> string let resource = args.[7] :?> string + let preferDateOnly = args.[8] :?> bool // Allowing inline schemas does not seem very valuable for this provider. // Let's stick to the default values for now. @@ -62,9 +63,21 @@ type public HtmlProvider(cfg: TypeProviderConfig) as this = PreferOptionals = preferOptionals InferenceMode = inferenceMode } +#if NET6_0_OR_GREATER + let supportsNet6Types = + preferDateOnly && ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly +#else + let supportsNet6Types = false +#endif + doc |> HtmlRuntime.getHtmlObjects (Some inferenceParameters) includeLayoutTables - |> HtmlGenerator.generateTypes asm ns typeName (inferenceParameters, missingValuesStr, cultureStr) + |> HtmlGenerator.generateTypes + asm + ns + typeName + (inferenceParameters, missingValuesStr, cultureStr) + supportsNet6Types use _holder = IO.logTime "TypeGeneration" sample @@ -86,7 +99,8 @@ type public HtmlProvider(cfg: TypeProviderConfig) as this = ProvidedStaticParameter("Culture", typeof, parameterDefaultValue = "") ProvidedStaticParameter("Encoding", typeof, parameterDefaultValue = "") ProvidedStaticParameter("ResolutionFolder", typeof, parameterDefaultValue = "") - ProvidedStaticParameter("EmbeddedResource", typeof, parameterDefaultValue = "") ] + ProvidedStaticParameter("EmbeddedResource", typeof, parameterDefaultValue = "") + ProvidedStaticParameter("PreferDateOnly", typeof, parameterDefaultValue = false) ] let helpText = """Typed representation of an HTML file. @@ -101,6 +115,7 @@ type public HtmlProvider(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.html'). 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. """ do htmlProvTy.AddXmlDoc helpText diff --git a/src/FSharp.Data.DesignTime/Json/JsonConversionsGenerator.fs b/src/FSharp.Data.DesignTime/Json/JsonConversionsGenerator.fs index 7255167a0..a6e0f3480 100644 --- a/src/FSharp.Data.DesignTime/Json/JsonConversionsGenerator.fs +++ b/src/FSharp.Data.DesignTime/Json/JsonConversionsGenerator.fs @@ -34,6 +34,12 @@ let getConversionQuotation missingValuesStr cultureStr typ (value: Expr elif typ = typeof then <@@ JsonRuntime.ConvertTimeSpan(cultureStr, %value) @@> +#if NET6_0_OR_GREATER + elif typ = typeof then + <@@ JsonRuntime.ConvertDateOnly(cultureStr, %value) @@> + elif typ = typeof then + <@@ JsonRuntime.ConvertTimeOnly(cultureStr, %value) @@> +#endif elif typ = typeof then <@@ JsonRuntime.ConvertGuid(%value) @@> else diff --git a/src/FSharp.Data.DesignTime/Json/JsonProvider.fs b/src/FSharp.Data.DesignTime/Json/JsonProvider.fs index de6c2f5e4..70272d63e 100644 --- a/src/FSharp.Data.DesignTime/Json/JsonProvider.fs +++ b/src/FSharp.Data.DesignTime/Json/JsonProvider.fs @@ -57,6 +57,7 @@ type public JsonProvider(cfg: TypeProviderConfig) as this = let preferDictionaries = args.[8] :?> bool let inferenceMode = args.[9] :?> InferenceMode let schema = args.[10] :?> string + let preferDateOnly = args.[11] :?> bool let inferenceMode = InferenceMode'.FromPublicApi(inferenceMode, inferTypesFromValues) @@ -76,28 +77,37 @@ type public JsonProvider(cfg: TypeProviderConfig) as this = let inferedType = use _holder = IO.logTime "Inference" (if schema <> "" then schema else sample) - if schema <> "" then - // Use the JSON Schema for type inference - use _holder = IO.logTime "SchemaInference" schema - - let schemaValue = JsonValue.Parse(value) - let jsonSchema = JsonSchema.parseSchema schemaValue - JsonSchema.schemaToInferedType unitsOfMeasureProvider jsonSchema + let rawInfered = + if schema <> "" then + // Use the JSON Schema for type inference + use _holder = IO.logTime "SchemaInference" schema + + let schemaValue = JsonValue.Parse(value) + let jsonSchema = JsonSchema.parseSchema schemaValue + JsonSchema.schemaToInferedType unitsOfMeasureProvider jsonSchema + else + // Use sample-based inference + let samples = + use _holder = IO.logTime "Parsing" sample + + if sampleIsList then + JsonDocument.CreateList(new StringReader(value)) + |> Array.map (fun doc -> doc.JsonValue) + else + [| JsonValue.Parse(value) |] + + samples + |> Array.map (fun sampleJson -> + JsonInference.inferType unitsOfMeasureProvider inferenceMode cultureInfo "" sampleJson) + |> Array.fold (StructuralInference.subtypeInfered false) InferedType.Top +#if NET6_0_OR_GREATER + if preferDateOnly && ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then + rawInfered else - // Use sample-based inference - let samples = - use _holder = IO.logTime "Parsing" sample - - if sampleIsList then - JsonDocument.CreateList(new StringReader(value)) - |> Array.map (fun doc -> doc.JsonValue) - else - [| JsonValue.Parse(value) |] - - samples - |> Array.map (fun sampleJson -> - JsonInference.inferType unitsOfMeasureProvider inferenceMode cultureInfo "" sampleJson) - |> Array.fold (StructuralInference.subtypeInfered false) InferedType.Top + StructuralInference.downgradeNet6Types rawInfered +#else + rawInfered +#endif use _holder = IO.logTime "TypeGeneration" (if schema <> "" then schema else sample) @@ -153,7 +163,8 @@ type public JsonProvider(cfg: TypeProviderConfig) as this = typeof, parameterDefaultValue = InferenceMode.BackwardCompatible ) - ProvidedStaticParameter("Schema", typeof, parameterDefaultValue = "") ] + ProvidedStaticParameter("Schema", typeof, parameterDefaultValue = "") + ProvidedStaticParameter("PreferDateOnly", typeof, parameterDefaultValue = false) ] let helpText = """Typed representation of a JSON document. @@ -176,7 +187,8 @@ type public JsonProvider(cfg: TypeProviderConfig) as this = | ValuesAndInlineSchemasHints -> Types of values are inferred from both values and inline schemas. Inline schemas are special string values that can define a type and/or unit of measure. Supported syntax: typeof<type> or typeof{type} or typeof<type<measure>> or typeof{type{measure}}. Valid measures are the default SI units, and valid types are int, int64, bool, float, decimal, date, datetimeoffset, timespan, guid and string. | ValuesAndInlineSchemasOverrides -> Same as ValuesAndInlineSchemasHints, but value inferred types are ignored when an inline schema is present. - Location of a JSON Schema file or a string containing a JSON Schema document. When specified, Sample and SampleIsList must not be used.""" + Location of a JSON Schema file or a string containing a JSON Schema document. When specified, Sample and SampleIsList must not be used. + When true on .NET 6+, date-only strings (e.g. "2023-01-15") are inferred as DateOnly and time-only strings as TimeOnly. Defaults to false for backward compatibility.""" do jsonProvTy.AddXmlDoc helpText do jsonProvTy.DefineStaticParameters(parameters, buildTypes) diff --git a/src/FSharp.Data.DesignTime/Xml/XmlProvider.fs b/src/FSharp.Data.DesignTime/Xml/XmlProvider.fs index dfbf2e766..7b5766806 100644 --- a/src/FSharp.Data.DesignTime/Xml/XmlProvider.fs +++ b/src/FSharp.Data.DesignTime/Xml/XmlProvider.fs @@ -50,6 +50,7 @@ type public XmlProvider(cfg: TypeProviderConfig) as this = let inferTypesFromValues = args.[7] :?> bool let schema = args.[8] :?> string let inferenceMode = args.[9] :?> InferenceMode + let preferDateOnly = args.[10] :?> bool let inferenceMode = InferenceMode'.FromPublicApi(inferenceMode, inferTypesFromValues) @@ -74,7 +75,16 @@ type public XmlProvider(cfg: TypeProviderConfig) as this = let inferedType = use _holder = IO.logTime "Inference" sample - schemaSet |> XsdParsing.getElements |> List.ofSeq |> XsdInference.inferElements + let t = + schemaSet |> XsdParsing.getElements |> List.ofSeq |> XsdInference.inferElements +#if NET6_0_OR_GREATER + if preferDateOnly && ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then + t + else + StructuralInference.downgradeNet6Types t +#else + t +#endif use _holder = IO.logTime "TypeGeneration" sample @@ -113,14 +123,23 @@ type public XmlProvider(cfg: TypeProviderConfig) as this = let inferedType = use _holder = IO.logTime "Inference" sample - samples - |> XmlInference.inferType - unitsOfMeasureProvider - inferenceMode - (TextRuntime.GetCulture cultureStr) - false - globalInference - |> Array.fold (StructuralInference.subtypeInfered false) InferedType.Top + let t = + samples + |> XmlInference.inferType + unitsOfMeasureProvider + inferenceMode + (TextRuntime.GetCulture cultureStr) + false + globalInference + |> Array.fold (StructuralInference.subtypeInfered false) InferedType.Top +#if NET6_0_OR_GREATER + if preferDateOnly && ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then + t + else + StructuralInference.downgradeNet6Types t +#else + t +#endif use _holder = IO.logTime "TypeGeneration" sample @@ -175,7 +194,8 @@ type public XmlProvider(cfg: TypeProviderConfig) as this = "InferenceMode", typeof, parameterDefaultValue = InferenceMode.BackwardCompatible - ) ] + ) + ProvidedStaticParameter("PreferDateOnly", typeof, parameterDefaultValue = false) ] let helpText = """Typed representation of a XML file. @@ -198,7 +218,8 @@ type public XmlProvider(cfg: TypeProviderConfig) as this = | ValuesAndInlineSchemasHints -> Types of values are inferred from both values and inline schemas. Inline schemas are special string values that can define a type and/or unit of measure. Supported syntax: typeof<type> or typeof{type} or typeof<type<measure>> or typeof{type{measure}}. Valid measures are the default SI units, and valid types are int, int64, bool, float, decimal, date, datetimeoffset, timespan, guid and string. | ValuesAndInlineSchemasOverrides -> Same as ValuesAndInlineSchemasHints, but value inferred types are ignored when an inline schema is present. Note inline schemas are not used from Xsd documents. - """ + + When true on .NET 6+, date-only strings are inferred as DateOnly and time-only strings as TimeOnly. Defaults to false for backward compatibility.""" do xmlProvTy.AddXmlDoc helpText diff --git a/src/FSharp.Data.Json.Core/FSharp.Data.Json.Core.fsproj b/src/FSharp.Data.Json.Core/FSharp.Data.Json.Core.fsproj index 5b02f1a24..b015d2d04 100644 --- a/src/FSharp.Data.Json.Core/FSharp.Data.Json.Core.fsproj +++ b/src/FSharp.Data.Json.Core/FSharp.Data.Json.Core.fsproj @@ -2,7 +2,7 @@ Library - netstandard2.0 + netstandard2.0;net8.0 $(OtherFlags) --warnon:1182 --nowarn:10001 --nowarn:44 true false diff --git a/src/FSharp.Data.Json.Core/JsonConversions.fs b/src/FSharp.Data.Json.Core/JsonConversions.fs index 93e3f08dc..651400145 100644 --- a/src/FSharp.Data.Json.Core/JsonConversions.fs +++ b/src/FSharp.Data.Json.Core/JsonConversions.fs @@ -85,6 +85,18 @@ type JsonConversions = | JsonValue.String s -> TextConversions.AsTimeSpan cultureInfo s | _ -> None +#if NET6_0_OR_GREATER + static member AsDateOnly cultureInfo = + function + | JsonValue.String s -> TextConversions.AsDateOnly cultureInfo s + | _ -> None + + static member AsTimeOnly cultureInfo = + function + | JsonValue.String s -> TextConversions.AsTimeOnly cultureInfo s + | _ -> None +#endif + static member AsGuid = function | JsonValue.String s -> TextConversions.AsGuid s diff --git a/src/FSharp.Data.Json.Core/JsonRuntime.fs b/src/FSharp.Data.Json.Core/JsonRuntime.fs index 7211737b2..c2c2931a5 100644 --- a/src/FSharp.Data.Json.Core/JsonRuntime.fs +++ b/src/FSharp.Data.Json.Core/JsonRuntime.fs @@ -62,6 +62,16 @@ type JsonRuntime = json |> Option.bind (JsonConversions.AsTimeSpan(TextRuntime.GetCulture cultureStr)) +#if NET6_0_OR_GREATER + static member ConvertDateOnly(cultureStr, json) = + json + |> Option.bind (JsonConversions.AsDateOnly(TextRuntime.GetCulture cultureStr)) + + static member ConvertTimeOnly(cultureStr, json) = + json + |> Option.bind (JsonConversions.AsTimeOnly(TextRuntime.GetCulture cultureStr)) +#endif + static member ConvertGuid(json) = json |> Option.bind JsonConversions.AsGuid @@ -229,6 +239,10 @@ type JsonRuntime = fun json -> (JsonConversions.AsDateTimeOffset cultureInfo json).IsSome | InferedTypeTag.TimeSpan -> JsonConversions.AsTimeSpan(TextRuntime.GetCulture cultureStr) >> Option.isSome | InferedTypeTag.Guid -> JsonConversions.AsGuid >> Option.isSome +#if NET6_0_OR_GREATER + | InferedTypeTag.DateOnly -> JsonConversions.AsDateOnly(TextRuntime.GetCulture cultureStr) >> Option.isSome + | InferedTypeTag.TimeOnly -> JsonConversions.AsTimeOnly(TextRuntime.GetCulture cultureStr) >> Option.isSome +#endif | InferedTypeTag.Collection -> function | JsonValue.Array _ -> true diff --git a/src/FSharp.Data.Runtime.Utilities/FSharp.Data.Runtime.Utilities.fsproj b/src/FSharp.Data.Runtime.Utilities/FSharp.Data.Runtime.Utilities.fsproj index dd959164a..b0004f66c 100644 --- a/src/FSharp.Data.Runtime.Utilities/FSharp.Data.Runtime.Utilities.fsproj +++ b/src/FSharp.Data.Runtime.Utilities/FSharp.Data.Runtime.Utilities.fsproj @@ -2,7 +2,7 @@ Library - netstandard2.0 + netstandard2.0;net8.0 $(OtherFlags) --warnon:1182 --nowarn:10001 --nowarn:44 true false diff --git a/src/FSharp.Data.Runtime.Utilities/IO.fs b/src/FSharp.Data.Runtime.Utilities/IO.fs index c8a650c59..5e9097dc3 100644 --- a/src/FSharp.Data.Runtime.Utilities/IO.fs +++ b/src/FSharp.Data.Runtime.Utilities/IO.fs @@ -133,7 +133,7 @@ let internal logTime (_: string) (_: string) = dummyDisposable #endif -type private FileWatcher(path) = +type private FileWatcher(path: string) = let subscriptions = Dictionary unit>() diff --git a/src/FSharp.Data.Runtime.Utilities/StructuralInference.fs b/src/FSharp.Data.Runtime.Utilities/StructuralInference.fs index acebf69bb..71a6b4df8 100644 --- a/src/FSharp.Data.Runtime.Utilities/StructuralInference.fs +++ b/src/FSharp.Data.Runtime.Utilities/StructuralInference.fs @@ -82,6 +82,9 @@ let private primitiveTypes = typeof typeof ] @ numericTypes +#if NET6_0_OR_GREATER + @ [ typeof; typeof ] +#endif /// Checks whether a type supports unit of measure [] @@ -110,6 +113,12 @@ let typeTag inferredType = InferedTypeTag.TimeSpan elif typ = typeof then InferedTypeTag.Guid +#if NET6_0_OR_GREATER + elif typ = typeof then + InferedTypeTag.DateOnly + elif typ = typeof then + InferedTypeTag.TimeOnly +#endif else failwith "typeTag: Unknown primitive type" | InferedType.Json _ -> InferedTypeTag.Json @@ -138,7 +147,16 @@ let private conversionTable = typeof typeof typeof ] - typeof, [ typeof ] ] + typeof, + [ typeof +#if NET6_0_OR_GREATER + typeof +#endif + ] +#if NET6_0_OR_GREATER + typeof, [ typeof ] +#endif + ] let private subtypePrimitives typ1 typ2 = Debug.Assert(List.exists ((=) typ1) primitiveTypes) @@ -425,7 +443,12 @@ let nameToType = "datetimeoffset", (typeof, TypeWrapper.None) "timespan", (typeof, TypeWrapper.None) "guid", (typeof, TypeWrapper.None) - "string", (typeof, TypeWrapper.None) ] + "string", (typeof, TypeWrapper.None) +#if NET6_0_OR_GREATER + "dateonly", (typeof, TypeWrapper.None) + "timeonly", (typeof, TypeWrapper.None) +#endif + ] |> dict // type is valid while it shouldn't, but well... @@ -545,6 +568,10 @@ let inferPrimitiveType | Parse TextConversions.AsTimeSpan _ -> makePrimitive typeof | Parse TextConversions.AsDateTimeOffset dateTimeOffset when not (isFakeDate dateTimeOffset.UtcDateTime value) -> makePrimitive typeof +#if NET6_0_OR_GREATER + | Parse TextConversions.AsDateOnly dateOnly when not (isFakeDate (dateOnly.ToDateTime(TimeOnly.MinValue)) value) -> + makePrimitive typeof +#endif | Parse TextConversions.AsDateTime date when not (isFakeDate date value) -> makePrimitive typeof | Parse TextConversions.AsDecimal _ -> makePrimitive typeof | Parse (TextConversions.AsFloat [||] false) _ -> makePrimitive typeof @@ -596,3 +623,83 @@ let inferPrimitiveType [] let getInferedTypeFromString unitsOfMeasureProvider inferenceMode cultureInfo value unit = inferPrimitiveType unitsOfMeasureProvider inferenceMode cultureInfo value unit + +#if NET6_0_OR_GREATER +/// Replaces DateOnly → DateTime and TimeOnly → TimeSpan throughout an InferedType tree. +/// Used in design-time code when the target framework does not support these .NET 6+ types. +let internal downgradeNet6Types (inferedType: InferedType) : InferedType = + let downgradeTag tag = + match tag with + | InferedTypeTag.DateOnly -> InferedTypeTag.DateTime + | InferedTypeTag.TimeOnly -> InferedTypeTag.TimeSpan + | _ -> tag + + let downgradeType (typ: Type) = + if typ = typeof then typeof + elif typ = typeof then typeof + else typ + + // Use reference-equality-based visited set to handle cyclic InferedType graphs + // (e.g. recursive XML schemas). When a cycle is detected we return the original node. + let visited = + System.Collections.Generic.HashSet( + { new System.Collections.Generic.IEqualityComparer with + member _.Equals(x, y) = obj.ReferenceEquals(x, y) + + member _.GetHashCode(x) = + System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(x) } + ) + + let rec convert infType = + if not (visited.Add(infType)) then + infType // cycle detected – return original to avoid infinite recursion + else + let result = + match infType with + | InferedType.Primitive(typ, unit, optional, overrideOnMerge) -> + InferedType.Primitive(downgradeType typ, unit, optional, overrideOnMerge) + | InferedType.Record(name, props, optional) -> + InferedType.Record(name, props |> List.map (fun p -> { p with Type = convert p.Type }), optional) + | InferedType.Collection(order, types) -> + InferedType.Collection( + order |> List.map downgradeTag, + types + |> Map.toSeq + |> Seq.map (fun (k, (m, t)) -> downgradeTag k, (m, convert t)) + |> Map.ofSeq + ) + | InferedType.Heterogeneous(types, containsOptional) -> + InferedType.Heterogeneous( + types + |> Map.toSeq + |> Seq.map (fun (k, t) -> downgradeTag k, convert t) + |> Map.ofSeq, + containsOptional + ) + | InferedType.Json(innerType, optional) -> InferedType.Json(convert innerType, optional) + | _ -> infType + + result + + convert inferedType + +/// Replaces DateOnly → DateTime and TimeOnly → TimeSpan in a PrimitiveInferedProperty. +/// Used in design-time code when the target framework does not support these .NET 6+ types. +let internal downgradeNet6PrimitiveProperty (field: StructuralTypes.PrimitiveInferedProperty) = + let v = field.Value + + if v.InferedType = typeof then + { field with + Value = + { v with + InferedType = typeof + RuntimeType = typeof } } + elif v.InferedType = typeof then + { field with + Value = + { v with + InferedType = typeof + RuntimeType = typeof } } + else + field +#endif diff --git a/src/FSharp.Data.Runtime.Utilities/StructuralTypes.fs b/src/FSharp.Data.Runtime.Utilities/StructuralTypes.fs index 7dee61c58..e8b1df393 100644 --- a/src/FSharp.Data.Runtime.Utilities/StructuralTypes.fs +++ b/src/FSharp.Data.Runtime.Utilities/StructuralTypes.fs @@ -45,6 +45,10 @@ type InferedTypeTag = | TimeSpan | DateTimeOffset | Guid +#if NET6_0_OR_GREATER + | DateOnly + | TimeOnly +#endif // Collections and sum types | Collection | Heterogeneous @@ -160,6 +164,10 @@ type internal InferedTypeTag with | Record None -> "Record" | Record(Some name) -> NameUtils.nicePascalName name | Json -> "Json" +#if NET6_0_OR_GREATER + | DateOnly -> "DateOnly" + | TimeOnly -> "TimeOnly" +#endif /// Converts tag to string code that can be passed to generated code member x.Code = @@ -182,6 +190,10 @@ type internal InferedTypeTag with | "Guid" -> Guid | "Array" -> Collection | "Choice" -> Heterogeneous +#if NET6_0_OR_GREATER + | "DateOnly" -> DateOnly + | "TimeOnly" -> TimeOnly +#endif | "Null" -> failwith "Null nodes should be skipped" | _ -> failwith "Invalid InferredTypeTag code" diff --git a/src/FSharp.Data.Runtime.Utilities/TextConversions.fs b/src/FSharp.Data.Runtime.Utilities/TextConversions.fs index e5ff9a7c4..ee7779266 100644 --- a/src/FSharp.Data.Runtime.Utilities/TextConversions.fs +++ b/src/FSharp.Data.Runtime.Utilities/TextConversions.fs @@ -41,7 +41,7 @@ module private Helpers = let dateTimeStyles = DateTimeStyles.AllowWhiteSpaces ||| DateTimeStyles.RoundtripKind - let ParseISO8601FormattedDateTime text cultureInfo = + let ParseISO8601FormattedDateTime (text: string) cultureInfo = match DateTime.TryParse(text, cultureInfo, dateTimeStyles) with | true, d -> d |> ValueSome | false, _ -> ValueNone @@ -174,7 +174,7 @@ type TextConversions private () = let min = (hourMin % 100) |> float |> TimeSpan.FromMinutes hr.Add min - let offset str = + let offset (str: string) = match Int32.TryParse str with | true, v -> getTimeSpanFromHourMin v |> ValueSome | false, _ -> ValueNone @@ -208,6 +208,32 @@ type TextConversions private () = | true, t -> Some t | _ -> None +#if NET6_0_OR_GREATER + static member AsDateOnly (cultureInfo: CultureInfo) (text: string) = + let mutable d = DateOnly.MinValue + + if DateOnly.TryParse(text.Trim(), cultureInfo, Globalization.DateTimeStyles.AllowWhiteSpaces, &d) then + // Reject strings that also parse as DateTime with a non-zero time component, + // e.g. "2022-06-12T01:02:03" should be DateTime, not DateOnly. + match DateTime.TryParse(text.Trim(), cultureInfo, Globalization.DateTimeStyles.AllowWhiteSpaces) with + | true, dt when dt.TimeOfDay <> TimeSpan.Zero -> None + | _ -> Some d + else + None + + static member AsTimeOnly (cultureInfo: CultureInfo) (text: string) = + let mutable t = TimeOnly.MinValue + + if TimeOnly.TryParse(text.Trim(), cultureInfo, Globalization.DateTimeStyles.AllowWhiteSpaces, &t) then + // Reject strings that also parse as DateTime with a specific real date + // (not today's date used as fill-in), e.g. "2016-10-05T04:05:03" is DateTime, not TimeOnly. + match DateTime.TryParse(text.Trim(), cultureInfo, Globalization.DateTimeStyles.AllowWhiteSpaces) with + | true, dt when dt.Date <> DateTime.Today -> None + | _ -> Some t + else + None +#endif + static member AsGuid(text: string) = Guid.TryParse(text.Trim()) |> asOption module internal UnicodeHelper = diff --git a/src/FSharp.Data.Runtime.Utilities/TextRuntime.fs b/src/FSharp.Data.Runtime.Utilities/TextRuntime.fs index 2f3b98ade..f6d860dc8 100644 --- a/src/FSharp.Data.Runtime.Utilities/TextRuntime.fs +++ b/src/FSharp.Data.Runtime.Utilities/TextRuntime.fs @@ -78,6 +78,16 @@ type TextRuntime = text |> Option.bind (TextConversions.AsTimeSpan(TextRuntime.GetCulture cultureStr)) +#if NET6_0_OR_GREATER + static member ConvertDateOnly(cultureStr, text) = + text + |> Option.bind (TextConversions.AsDateOnly(TextRuntime.GetCulture cultureStr)) + + static member ConvertTimeOnly(cultureStr, text) = + text + |> Option.bind (TextConversions.AsTimeOnly(TextRuntime.GetCulture cultureStr)) +#endif + static member ConvertGuid(text) = text |> Option.bind TextConversions.AsGuid @@ -136,6 +146,18 @@ type TextRuntime = | Some value -> value.ToString("g", TextRuntime.GetCulture cultureStr) | None -> "" +#if NET6_0_OR_GREATER + static member ConvertDateOnlyBack(cultureStr, value: DateOnly option) = + match value with + | Some value -> value.ToString("O", TextRuntime.GetCulture cultureStr) + | None -> "" + + static member ConvertTimeOnlyBack(cultureStr, value: TimeOnly option) = + match value with + | Some value -> value.ToString("O", TextRuntime.GetCulture cultureStr) + | None -> "" +#endif + static member ConvertGuidBack(value: Guid option) = match value with | Some value -> value.ToString() diff --git a/src/FSharp.Data.Xml.Core/FSharp.Data.Xml.Core.fsproj b/src/FSharp.Data.Xml.Core/FSharp.Data.Xml.Core.fsproj index ecc8f9f67..be6b0aba0 100644 --- a/src/FSharp.Data.Xml.Core/FSharp.Data.Xml.Core.fsproj +++ b/src/FSharp.Data.Xml.Core/FSharp.Data.Xml.Core.fsproj @@ -2,7 +2,7 @@ Library - netstandard2.0 + netstandard2.0;net8.0 $(OtherFlags) --warnon:1182 --nowarn:10001 --nowarn:44 true false diff --git a/src/FSharp.Data.Xml.Core/XmlRuntime.fs b/src/FSharp.Data.Xml.Core/XmlRuntime.fs index dbabc040d..537a4d6af 100644 --- a/src/FSharp.Data.Xml.Core/XmlRuntime.fs +++ b/src/FSharp.Data.Xml.Core/XmlRuntime.fs @@ -227,6 +227,10 @@ type XmlRuntime = v.ToString("O", cultureInfo) | :? DateTimeOffset as v -> v.ToString("O", cultureInfo) | :? TimeSpan as v -> v.ToString("g", cultureInfo) +#if NET6_0_OR_GREATER + | :? DateOnly as v -> v.ToString("yyyy-MM-dd") + | :? TimeOnly as v -> v.ToString("HH:mm:ss", cultureInfo) +#endif | :? int as v -> strWithCulture v | :? int64 as v -> strWithCulture v | :? float as v -> strWithCulture v @@ -249,6 +253,10 @@ type XmlRuntime = | :? option as v -> optionToArray serialize v | :? option as v -> optionToArray serialize v | :? option as v -> optionToArray serialize v +#if NET6_0_OR_GREATER + | :? option as v -> optionToArray serialize v + | :? option as v -> optionToArray serialize v +#endif | :? option as v -> optionToArray serialize v | :? option as v -> optionToArray serialize v | :? option as v -> optionToArray serialize v diff --git a/src/FSharp.Data.Xml.Core/XsdInference.fs b/src/FSharp.Data.Xml.Core/XsdInference.fs index 9fe3a7899..a25f6b787 100644 --- a/src/FSharp.Data.Xml.Core/XsdInference.fs +++ b/src/FSharp.Data.Xml.Core/XsdInference.fs @@ -214,7 +214,11 @@ module internal XsdInference = function | XmlTypeCode.Int -> typeof | XmlTypeCode.Long -> typeof +#if NET6_0_OR_GREATER + | XmlTypeCode.Date -> typeof +#else | XmlTypeCode.Date -> typeof +#endif | XmlTypeCode.DateTime -> typeof | XmlTypeCode.Boolean -> typeof | XmlTypeCode.Decimal -> typeof diff --git a/src/FSharp.Data/FSharp.Data.fsproj b/src/FSharp.Data/FSharp.Data.fsproj index 3d18d69b2..d2cc69492 100755 --- a/src/FSharp.Data/FSharp.Data.fsproj +++ b/src/FSharp.Data/FSharp.Data.fsproj @@ -2,7 +2,7 @@ Library - netstandard2.0 + netstandard2.0;net8.0 $(OtherFlags) --warnon:1182 --nowarn:10001 true false diff --git a/tests/FSharp.Data.Core.Tests/TextConversions.fs b/tests/FSharp.Data.Core.Tests/TextConversions.fs index 0c7bb599a..8ae3d0041 100644 --- a/tests/FSharp.Data.Core.Tests/TextConversions.fs +++ b/tests/FSharp.Data.Core.Tests/TextConversions.fs @@ -209,6 +209,33 @@ let ``TimeSpan conversions``() = TextConversions.AsTimeSpan culture "invalid:00:00" |> should equal None TextConversions.AsTimeSpan culture "" |> should equal None +#if NET6_0_OR_GREATER +[] +let ``DateOnly conversions`` () = + let culture = CultureInfo.InvariantCulture + + TextConversions.AsDateOnly culture "2023-01-15" |> should equal (Some (DateOnly(2023, 1, 15))) + TextConversions.AsDateOnly culture "2000-12-31" |> should equal (Some (DateOnly(2000, 12, 31))) + + // A datetime string should NOT match (DateOnly can't parse time components) + TextConversions.AsDateOnly culture "2023-01-15T10:30:00" |> should equal None + TextConversions.AsDateOnly culture "invalid" |> should equal None + TextConversions.AsDateOnly culture "" |> should equal None + +[] +let ``TimeOnly conversions`` () = + let culture = CultureInfo.InvariantCulture + + TextConversions.AsTimeOnly culture "10:30:45" |> should equal (Some (TimeOnly(10, 30, 45))) + TextConversions.AsTimeOnly culture "00:00:00" |> should equal (Some TimeOnly.MinValue) + TextConversions.AsTimeOnly culture "23:59:59" |> should equal (Some (TimeOnly(23, 59, 59))) + + // A date string should NOT match + TextConversions.AsTimeOnly culture "2023-01-15" |> should equal None + TextConversions.AsTimeOnly culture "invalid" |> should equal None + TextConversions.AsTimeOnly culture "" |> should equal None +#endif + [] let ``Guid conversions``() = let validGuid = Guid.NewGuid() diff --git a/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs b/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs index 539f539e0..7b2795697 100644 --- a/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs +++ b/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs @@ -202,7 +202,11 @@ let ``Inference of multiple nulls works``() = let ``Inference of DateTime``() = let source = CsvFile.Parse("date,int,float\n2012-12-19,2,3.0\n2012-12-12,4,5.0\n2012-12-1,6,10.0") let actual, _ = inferType source Int32.MaxValue [||] culture "" false false +#if NET6_0_OR_GREATER + let propDate = { Name = "date"; Type = InferedType.Primitive(typeof, None, false, false) } +#else let propDate = { Name = "date"; Type = InferedType.Primitive(typeof, None, false, false) } +#endif let propInt = { Name = "int"; Type = InferedType.Primitive(typeof, None, false, false) } let propFloat = { Name = "float"; Type = InferedType.Primitive(typeof, None, false, false) } let expected = toRecord [ propDate ; propInt ; propFloat ] @@ -212,7 +216,11 @@ let ``Inference of DateTime``() = let ``Inference of DateTime with timestamp``() = let source = CsvFile.Parse("date,timestamp\n2012-12-19,2012-12-19 12:00\n2012-12-12,2012-12-12 00:00\n2012-12-1,2012-12-1 07:00") let actual, _ = inferType source Int32.MaxValue [||] culture "" false false +#if NET6_0_OR_GREATER + let propDate = { Name = "date"; Type = InferedType.Primitive(typeof, None, false, false) } +#else let propDate = { Name = "date"; Type = InferedType.Primitive(typeof, None, false, false) } +#endif let propTimestamp = { Name = "timestamp"; Type = InferedType.Primitive(typeof, None, false, false) } let expected = toRecord [ propDate ; propTimestamp ] actual |> should equal expected @@ -221,7 +229,11 @@ let ``Inference of DateTime with timestamp``() = let ``Inference of DateTime with timestamp non default separator``() = let source = CsvFile.Parse("date;timestamp\n2012-12-19;2012-12-19 12:00\n2012-12-12;2012-12-12 00:00\n2012-12-1;2012-12-1 07:00", ";") let actual, _ = inferType source Int32.MaxValue [||] culture "" false false +#if NET6_0_OR_GREATER + let propDate = { Name = "date"; Type = InferedType.Primitive(typeof, None, false, false) } +#else let propDate = { Name = "date"; Type = InferedType.Primitive(typeof, None, false, false) } +#endif let propTimestamp = { Name = "timestamp"; Type = InferedType.Primitive(typeof, None, false, false) } let expected = toRecord [ propDate ; propTimestamp ] actual |> should equal expected @@ -249,7 +261,11 @@ let ``Inference of numbers with empty values``() = let propInt = { Name = "int"; Type = InferedType.Primitive(typeof, None, true, false) } let propFloat5 = { Name = "float5"; Type = InferedType.Primitive(typeof, None, false, false) } let propFloat6 = { Name = "float6"; Type = InferedType.Primitive(typeof, None, true, false) } +#if NET6_0_OR_GREATER + let propDate = { Name = "date"; Type = InferedType.Primitive(typeof, None, true, false) } +#else let propDate = { Name = "date"; Type = InferedType.Primitive(typeof, None, true, false) } +#endif let propBool = { Name = "bool"; Type = InferedType.Primitive(typeof, None, true, false) } let propInt64 = { Name = "int64"; Type = InferedType.Primitive(typeof, None, true, false) } let expected = toRecord [ propFloat1; propFloat2; propFloat3; propFloat4; propInt; propFloat5; propFloat6; propDate; propBool; propInt64 ] @@ -265,7 +281,11 @@ let ``Inference of numbers with empty values``() = let propInt = field "int" TypeWrapper.Nullable typeof let propFloat5 = field "float5" TypeWrapper.None typeof let propFloat6 = field "float6" TypeWrapper.None typeof +#if NET6_0_OR_GREATER + let propDate = field "date" TypeWrapper.Option typeof +#else let propDate = field "date" TypeWrapper.Option typeof +#endif let propBool = field "bool" TypeWrapper.Option typeof let propInt64 = field "int64" TypeWrapper.Nullable typeof let expected = [ propFloat1; propFloat2; propFloat3; propFloat4; propInt; propFloat5; propFloat6; propDate; propBool; propInt64 ] @@ -303,7 +323,11 @@ let ``Infers units of measure correctly``() = let propString = "String(metre)" , typeof , "string" let propFloat = "Float" , typeof , "float" +#if NET6_0_OR_GREATER + let propDate = "Date (second)" , typeof, "DateOnly" +#else let propDate = "Date (second)" , typeof, "date" +#endif let propInt = "Int" , typeof , "int" let propDecimal = "Decimal" , typeof , "decimal" let propBool = "Bool(N)" , typeof , "bool" diff --git a/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs b/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs index 6162da3ae..e2714bcc1 100644 --- a/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs +++ b/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs @@ -25,7 +25,8 @@ type internal CsvProviderArgs = Culture : string Encoding : string ResolutionFolder : string - EmbeddedResource : string } + EmbeddedResource : string + PreferDateOnly : bool } type internal XmlProviderArgs = { Sample : string @@ -37,7 +38,8 @@ type internal XmlProviderArgs = EmbeddedResource : string InferTypesFromValues : bool Schema : string - InferenceMode: InferenceMode } + InferenceMode: InferenceMode + PreferDateOnly : bool } type internal JsonProviderArgs = { Sample : string @@ -50,7 +52,8 @@ type internal JsonProviderArgs = InferTypesFromValues : bool PreferDictionaries : bool InferenceMode: InferenceMode - Schema: string } + Schema: string + PreferDateOnly : bool } type internal HtmlProviderArgs = { Sample : string @@ -60,7 +63,8 @@ type internal HtmlProviderArgs = Culture : string Encoding : string ResolutionFolder : string - EmbeddedResource : string } + EmbeddedResource : string + PreferDateOnly : bool } type internal WorldBankProviderArgs = { Sources : string @@ -93,7 +97,8 @@ type internal TypeProviderInstantiation = box x.Culture box x.Encoding box x.ResolutionFolder - box x.EmbeddedResource |] + box x.EmbeddedResource + box x.PreferDateOnly |] | Xml x -> (fun cfg -> new XmlProvider(cfg) :> TypeProviderForNamespaces), [| box x.Sample @@ -105,7 +110,8 @@ type internal TypeProviderInstantiation = box x.EmbeddedResource box x.InferTypesFromValues box x.Schema - box x.InferenceMode |] + box x.InferenceMode + box x.PreferDateOnly |] | Json x -> (fun cfg -> new JsonProvider(cfg) :> TypeProviderForNamespaces), [| box x.Sample @@ -118,7 +124,8 @@ type internal TypeProviderInstantiation = box x.InferTypesFromValues box x.PreferDictionaries box x.InferenceMode - box x.Schema |] + box x.Schema + box x.PreferDateOnly |] | Html x -> (fun cfg -> new HtmlProvider(cfg) :> TypeProviderForNamespaces), [| box x.Sample @@ -128,7 +135,8 @@ type internal TypeProviderInstantiation = box x.Culture box x.Encoding box x.ResolutionFolder - box x.EmbeddedResource |] + box x.EmbeddedResource + box x.PreferDateOnly |] | WorldBank x -> (fun cfg -> new WorldBankProvider(cfg) :> TypeProviderForNamespaces), [| box x.Sources @@ -223,7 +231,8 @@ type internal TypeProviderInstantiation = Encoding = args.[9] CacheRows = false ResolutionFolder = "" - EmbeddedResource = "" } + EmbeddedResource = "" + PreferDateOnly = false } | "Xml" -> Xml { Sample = args.[1] SampleIsList = args.[2] |> bool.Parse @@ -234,7 +243,8 @@ type internal TypeProviderInstantiation = EmbeddedResource = "" InferTypesFromValues = args.[5] |> bool.Parse Schema = args.[6] - InferenceMode = args.[7] |> InferenceMode.Parse } + InferenceMode = args.[7] |> InferenceMode.Parse + PreferDateOnly = false } | "Json" -> // Handle special case for Schema.json tests where some fields might be empty if args.Length > 5 && not (String.IsNullOrEmpty(args.[5])) then @@ -248,7 +258,8 @@ type internal TypeProviderInstantiation = InferTypesFromValues = args.[5] |> bool.Parse PreferDictionaries = args.[6] |> bool.Parse InferenceMode = args.[7] |> InferenceMode.Parse - Schema = if args.Length > 8 then args.[8] else "" } + Schema = if args.Length > 8 then args.[8] else "" + PreferDateOnly = false } else // This is for schema-based tests in the format "Json,,,,,true,false,BackwardCompatible,SimpleSchema.json" Json { Sample = args.[1] @@ -261,7 +272,8 @@ type internal TypeProviderInstantiation = InferTypesFromValues = true PreferDictionaries = false InferenceMode = InferenceMode.Parse "BackwardCompatible" - Schema = if args.Length > 8 then args.[8] else "" } + Schema = if args.Length > 8 then args.[8] else "" + PreferDateOnly = false } | "Html" -> Html { Sample = args.[1] PreferOptionals = args.[2] |> bool.Parse @@ -270,7 +282,8 @@ type internal TypeProviderInstantiation = Culture = args.[4] Encoding = "" ResolutionFolder = "" - EmbeddedResource = "" } + EmbeddedResource = "" + PreferDateOnly = false } | "WorldBank" -> WorldBank { Sources = args.[1] Asynchronous = args.[2] |> bool.Parse } diff --git a/tests/FSharp.Data.Tests/CsvProvider.fs b/tests/FSharp.Data.Tests/CsvProvider.fs index 80ee2eb2a..ec67c88a5 100644 --- a/tests/FSharp.Data.Tests/CsvProvider.fs +++ b/tests/FSharp.Data.Tests/CsvProvider.fs @@ -51,13 +51,13 @@ let [] csvWithGermanDate = """Preisregelung_ID;Messgebiet_Nr;gueltig_se [] let ``Inference of german dates`` () = - let csv = CsvProvider.GetSample() + let csv = CsvProvider.GetSample() let rows = csv.Rows |> Seq.toArray let row = rows.[1] - let d1:DateTime = row.Gueltig_seit - d1 |> should equal (DateTime(2006,02,01)) + let d1:DateOnly = row.Gueltig_seit + d1 |> should equal (DateOnly(2006,02,01)) let [] csvWithEmptyValues = """ Float1,Float2,Float3,Float4,Int,Float5,Float6,Date @@ -67,7 +67,7 @@ Float1,Float2,Float3,Float4,Int,Float5,Float6,Date [] let ``Inference of numbers with empty values`` () = - let csv = CsvProvider.GetSample() + let csv = CsvProvider.GetSample() let rows = csv.Rows |> Seq.toArray let row = rows.[0] @@ -79,19 +79,19 @@ let ``Inference of numbers with empty values`` () = let _i:Nullable = row.Int let _f5:float = row.Float5 let _f6:float = row.Float6 - let _d:option = row.Date + let _d:option = row.Date - let expected = 1.0, 1.0, 1.0, 1.0, Nullable(), Double.NaN, Double.NaN, (None: DateTime option) + let expected = 1.0, 1.0, 1.0, 1.0, Nullable(), Double.NaN, Double.NaN, (None: DateOnly option) let actual = row.Float1, row.Float2, row.Float3, row.Float4, row.Int, row.Float5, row.Float6, row.Date actual |> should equal expected let row = rows.[1] - let expected = 2.0, Double.NaN, Double.NaN, 1.0, Nullable 1, 1.0, Double.NaN, Some(new DateTime(2010, 01,10)) + let expected = 2.0, Double.NaN, Double.NaN, 1.0, Nullable 1, 1.0, Double.NaN, Some(DateOnly(2010, 01,10)) let actual = row.Float1, row.Float2, row.Float3, row.Float4, row.Int, row.Float5, row.Float6, row.Date actual |> should equal expected let row = rows.[2] - let expected = Double.NaN, Double.NaN, 2.0, Double.NaN, Nullable 1, Double.NaN, 2.0, (None: DateTime option) + let expected = Double.NaN, Double.NaN, 2.0, Double.NaN, Nullable 1, Double.NaN, 2.0, (None: DateOnly option) let actual = row.Float1, row.Float2, row.Float3, row.Float4, row.Int, row.Float5, row.Float6, row.Date actual |> should equal expected @@ -101,11 +101,11 @@ let [] csvData = """DateOnly,DateWithOffset,MixedDate,OffsetOption (dat [] let ``Can infer DateTime and DateTimeOffset types correctly`` () = - let csv = CsvProvider.GetSample() + let csv = CsvProvider.GetSample() let firstRow = csv.Rows |> Seq.head let secondRow = csv.Rows |> Seq.item 1 - firstRow.DateOnly.GetType() |> should equal typeof + firstRow.DateOnly.GetType() |> should equal typeof firstRow.DateWithOffset.GetType() |> should equal typeof firstRow.MixedDate.GetType() |> should equal typeof firstRow.OffsetOption.GetType() |> should equal typeof @@ -153,9 +153,9 @@ let ``Does not treat invariant culture number such as 3.14 as a date in cultures targetCulture.DateTimeFormat.DateSeparator |> should equal "." targetCulture.DateTimeFormat.TimeSeparator |> should equal ":" // See https://github.com/fsprojects/FSharp.Data/issues/767 targetCulture.NumberFormat.NumberDecimalSeparator |> should equal "," - let csv = CsvProvider<"Data/DnbHistoriskeKurser.csv", ",", 10, Culture=norwayCultureName>.GetSample() + let csv = CsvProvider<"Data/DnbHistoriskeKurser.csv", ",", 10, Culture=norwayCultureName, PreferDateOnly = true>.GetSample() let row = csv.Rows |> Seq.head - (row.Dato, row.USD) |> should equal (DateTime(2013, 2, 7), "5.4970") + (row.Dato, row.USD) |> should equal (DateOnly(2013, 2, 7), "5.4970") [] let ``Empty lines are skipped and don't make everything optional`` () = diff --git a/tests/FSharp.Data.Tests/HtmlProvider.fs b/tests/FSharp.Data.Tests/HtmlProvider.fs index b3c7ebfd8..95b33ee7d 100644 --- a/tests/FSharp.Data.Tests/HtmlProvider.fs +++ b/tests/FSharp.Data.Tests/HtmlProvider.fs @@ -51,12 +51,12 @@ let ``Can create type for simple table``() = let table = SimpleHtml().Tables.Table table.Rows.[0].``Column 1`` |> should equal 1 -type MarketDepth = HtmlProvider<"Data/MarketDepth.htm"> +type MarketDepth = HtmlProvider<"Data/MarketDepth.htm", PreferDateOnly = true> [] let ``Can infer tables out of the market depth file``() = let table = MarketDepth().Tables.Table1 - table.Rows.[0].``Settlement Day`` |> should equal (DateTime(2014, 1, 14, 0, 0,0)) + table.Rows.[0].``Settlement Day`` |> should equal (DateOnly(2014, 1, 14)) table.Rows.[0].Period |> should equal 1 [] @@ -367,8 +367,8 @@ let ``Can infer DateTime and DateTimeOffset types correctly`` () = - """>.GetSample() + """, PreferDateOnly = true>.GetSample() let table = html.Tables.Table1 - table.Rows.[0].DateOnly.GetType() |> should equal typeof + table.Rows.[0].DateOnly.GetType() |> should equal typeof table.Rows.[0].MixedDate.GetType() |> should equal typeof table.Rows.[0].DateWithOffset.GetType() |> should equal typeof diff --git a/tests/FSharp.Data.Tests/HtmlProviderList.fs b/tests/FSharp.Data.Tests/HtmlProviderList.fs index 723c56d1a..ac8620e05 100644 --- a/tests/FSharp.Data.Tests/HtmlProviderList.fs +++ b/tests/FSharp.Data.Tests/HtmlProviderList.fs @@ -73,8 +73,8 @@ let ``Simple List infers date type correctly ``() =
  • 03/03/2013
  • - """, PreferOptionals=true>.GetSample().Lists.List1 - list.Values |> should equal [|DateTime(2013,1,1);DateTime(2013,2,2);DateTime(2013,3,3)|] + """, PreferOptionals=true, PreferDateOnly=true>.GetSample().Lists.List1 + list.Values |> should equal [|DateOnly(2013,1,1);DateOnly(2013,2,2);DateOnly(2013,3,3)|] [] let ``Simple List infers hetergenous list as string type correctly ``() = @@ -144,9 +144,9 @@ let ``Handles simple definition list``() =
    - """, PreferOptionals=true>.GetSample().DefinitionLists.DefinitionList1 + """, PreferOptionals=true, PreferDateOnly=true>.GetSample().DefinitionLists.DefinitionList1 list.Count.Values |> should equal [|1;2|] - list.Dates.Values |> should equal [|DateTime(2014,1,1); DateTime(2014, 2,2)|] + list.Dates.Values |> should equal [|DateOnly(2014,1,1); DateOnly(2014, 2,2)|] list.Decimals.Values |> should equal [|1.23M; 2.23M|] list.Missing.Values |> should equal [|Some "Foobar"; None|] diff --git a/tests/FSharp.Data.Tests/JsonProvider.fs b/tests/FSharp.Data.Tests/JsonProvider.fs index af153ea0e..5bc372124 100644 --- a/tests/FSharp.Data.Tests/JsonProvider.fs +++ b/tests/FSharp.Data.Tests/JsonProvider.fs @@ -261,11 +261,11 @@ let ``Nulls, Missing, and "" should make the type optional``() = [] let ``Heterogeneous types with Nulls, Missing, and "" should return None on all choices``() = - let j = JsonProvider<"""[{"a":"","b":null},{"a":2,"b":"3.4","c":"true"},{"a":false,"b":"2002/10/10","c":"2"},{"a":[],"b":[1],"c":{"z":1}},{"b":"00:30:00"}]""">.GetSamples() + let j = JsonProvider<"""[{"a":"","b":null},{"a":2,"b":"3.4","c":"true"},{"a":false,"b":"2002/10/10","c":"2"},{"a":[],"b":[1],"c":{"z":1}},{"b":"00:30:00"}]""", PreferDateOnly = true>.GetSamples() j.[0].A.Boolean |> should equal None j.[0].A.Number |> should equal None j.[0].A.Array |> should equal None - j.[0].B.DateTime |> should equal None + j.[0].B.DateOnly |> should equal None j.[0].B.TimeSpan |> should equal None j.[0].B.Number |> should equal None j.[0].B.Array |> should equal None @@ -276,7 +276,7 @@ let ``Heterogeneous types with Nulls, Missing, and "" should return None on all j.[1].A.Boolean |> should equal None j.[1].A.Number |> should equal (Some 2) j.[1].A.Array |> should equal None - j.[1].B.DateTime |> should equal (Some (DateTime(DateTime.Today.Year,3,4))) + j.[1].B.DateOnly |> should equal (Some (DateOnly(DateTime.Today.Year,3,4))) j.[1].B.TimeSpan |> should equal None j.[1].B.Number |> should equal (Some 3.4m) j.[1].B.Array |> should equal None @@ -287,7 +287,7 @@ let ``Heterogeneous types with Nulls, Missing, and "" should return None on all j.[2].A.Boolean |> should equal (Some false) j.[2].A.Number |> should equal None j.[2].A.Array |> should equal None - j.[2].B.DateTime |> should equal (Some (DateTime(2002,10,10))) + j.[2].B.DateOnly |> should equal (Some (DateOnly(2002,10,10))) j.[2].B.TimeSpan |> should equal None j.[2].B.Number |> should equal None j.[2].B.Array |> should equal None @@ -298,7 +298,7 @@ let ``Heterogeneous types with Nulls, Missing, and "" should return None on all j.[3].A.Boolean |> should equal None j.[3].A.Number |> should equal None j.[3].A.Array |> should equal (Some (Array.zeroCreate 0)) - j.[3].B.DateTime |> should equal None + j.[3].B.DateOnly |> should equal None j.[3].B.TimeSpan |> should equal None j.[3].B.Number |> should equal None j.[3].B.Array |> should equal (Some [|1|]) @@ -635,16 +635,16 @@ let ``Can parse time span in different culture``() = [] let ``Parsing of values wrapped in quotes should work on heterogenous values``() = - let objs = JsonProvider<"""[{"a": "01/02/2000"}, {"a" : "3"}]""">.GetSamples() - objs.[0].A.DateTime |> should equal (Some (DateTime(2000,01,02))) + let objs = JsonProvider<"""[{"a": "01/02/2000"}, {"a" : "3"}]""", PreferDateOnly = true>.GetSamples() + objs.[0].A.DateOnly |> should equal (Some (DateOnly(2000,01,02))) objs.[0].A.Number |> should equal None - objs.[1].A.DateTime |> should equal None + objs.[1].A.DateOnly |> should equal None objs.[1].A.Number |> should equal (Some 3) [] let ``Parsing of values wrapped in quotes should work on arrays``() = - let objs = JsonProvider<"""["01/02/2000", "02/02/2001", "3", 4]""">.GetSample() - objs.DateTimes |> should equal [| DateTime(2000,01,02); DateTime(2001,02,02) |] + let objs = JsonProvider<"""["01/02/2000", "02/02/2001", "3", 4]""", PreferDateOnly = true>.GetSample() + objs.DateOnlies |> should equal [| DateOnly(2000,01,02); DateOnly(2001,02,02) |] objs.Numbers |> should equal [| 3; 4 |] [] @@ -837,7 +837,7 @@ type ServiceResponse = JsonProvider<"""[ """, SampleIsList = true> type FirstPayload = JsonProvider<"""{ "x" : 0.500, "y" : 0.000 }"""> -type SecondPayload = JsonProvider<"""{"user": "alice", "role": "admin", "registeredSince": "2021-11-01"}"""> +type SecondPayload = JsonProvider<"""{ "user": "alice", "role": "admin", "registeredSince": "2021-11-01" }""", PreferDateOnly = true> [] let ``Can re-load JsonValue`` () = @@ -855,7 +855,7 @@ let ``Can load different nested payloads`` () = payload1.Y |> should equal 12345 payload2.User |> should equal "alice" payload2.Role |> should equal "admin" - payload2.RegisteredSince |> should equal (DateTime(2021, 11, 1)) + payload2.RegisteredSince |> should equal (DateOnly(2021, 11, 1)) [] diff --git a/tests/FSharp.Data.Tests/XmlProvider.fs b/tests/FSharp.Data.Tests/XmlProvider.fs index c8ee18695..7e34da43a 100644 --- a/tests/FSharp.Data.Tests/XmlProvider.fs +++ b/tests/FSharp.Data.Tests/XmlProvider.fs @@ -1140,7 +1140,7 @@ let SimpleTypesXsd = """ """ -type SimpleTypes = XmlProvider +type SimpleTypes = XmlProvider open System.Xml.Schema @@ -1161,7 +1161,7 @@ let ``simple types are formatted properly``() = SimpleTypes.A( int = 0, long = 0L, - date = System.DateTime.Today, + date = System.DateOnly.FromDateTime(System.DateTime.Today), dateTime = System.DateTimeOffset.Now, boolean = false, decimal = 0M, @@ -1171,7 +1171,7 @@ let ``simple types are formatted properly``() = SimpleTypes.A( int = System.Int32.MinValue, long = System.Int64.MinValue, - date = System.DateTime.MinValue.Date, + date = System.DateOnly.MinValue, dateTime = System.DateTimeOffset.MinValue, boolean = false, decimal = System.Decimal.MinValue, @@ -1181,7 +1181,7 @@ let ``simple types are formatted properly``() = SimpleTypes.A( int = System.Int32.MaxValue, long = System.Int64.MaxValue, - date = System.DateTime.MaxValue.Date, + date = System.DateOnly.MaxValue, dateTime = System.DateTimeOffset.MaxValue, boolean = true, decimal = System.Decimal.MaxValue, @@ -1194,7 +1194,7 @@ let ``simple types are formatted properly``() = isValid maxValues.XElement |> should equal true [] -let ``time is omitted when zero``() = +let ``date is formatted properly``() = let schema = SimpleTypes.GetSchema() let simpleValues date = SimpleTypes.A( @@ -1207,19 +1207,12 @@ let ``time is omitted when zero``() = double = System.Double.NaN) let isValidWithMsg = isValid schema true - // Don't display the error message each time when we expect to see it: - let isValidWithoutMsg = isValid schema false - let validXml = System.DateTime(2018, 8, 29) |> simpleValues + let validXml = System.DateOnly(2018, 8, 29) |> simpleValues isValidWithMsg validXml.XElement |> should equal true validXml.XElement.Attribute(XName.Get "date").Value |> should equal "2018-08-29" - let invalidXml = System.DateTime(2018, 8, 29, 5, 30, 56) |> simpleValues - isValidWithoutMsg invalidXml.XElement |> should equal false - invalidXml.XElement.Attribute(XName.Get "date").Value - |> should equal "2018-08-29T05:30:56.0000000" - type TimeSpanXML = XmlProvider<"Data/TimeSpans.xml"> []