Skip to content
9 changes: 8 additions & 1 deletion src/FSharp.Data.Csv.Core/CsvInference.fs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ let private nameToTypeForCsv =
"datetimeoffset option", (typeof<DateTimeOffset>, TypeWrapper.Option)
"timespan option", (typeof<TimeSpan>, TypeWrapper.Option)
"guid option", (typeof<Guid>, TypeWrapper.Option)
"string option", (typeof<string>, TypeWrapper.Option) ]
"string option", (typeof<string>, TypeWrapper.Option)
#if NET6_0_OR_GREATER
"dateonly?", (typeof<DateOnly>, TypeWrapper.Nullable)
"timeonly?", (typeof<TimeOnly>, TypeWrapper.Nullable)
"dateonly option", (typeof<DateOnly>, TypeWrapper.Option)
"timeonly option", (typeof<TimeOnly>, TypeWrapper.Option)
#endif
]
|> dict

let private nameAndTypeRegex =
Expand Down
2 changes: 1 addition & 1 deletion src/FSharp.Data.Csv.Core/FSharp.Data.Csv.Core.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
Comment thread
dsyme marked this conversation as resolved.
<OtherFlags>$(OtherFlags) --warnon:1182 --nowarn:10001 --nowarn:44</OtherFlags>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ let getConversionQuotation missingValuesStr cultureStr typ (value: Expr<string o
<@@ TextRuntime.ConvertDateTimeOffset(cultureStr, %value) @@>
elif typ = typeof<TimeSpan> then
<@@ TextRuntime.ConvertTimeSpan(cultureStr, %value) @@>
#if NET6_0_OR_GREATER
elif typ = typeof<DateOnly> then
<@@ TextRuntime.ConvertDateOnly(cultureStr, %value) @@>
elif typ = typeof<TimeOnly> then
<@@ TextRuntime.ConvertTimeOnly(cultureStr, %value) @@>
#endif
elif typ = typeof<Guid> then
<@@ TextRuntime.ConvertGuid(%value) @@>
else
Expand All @@ -58,6 +64,12 @@ let getBackConversionQuotation missingValuesStr cultureStr typ value : Expr<stri
<@ TextRuntime.ConvertDateTimeOffsetBack(cultureStr, %%value) @>
elif typ = typeof<TimeSpan> then
<@ TextRuntime.ConvertTimeSpanBack(cultureStr, %%value) @>
#if NET6_0_OR_GREATER
elif typ = typeof<DateOnly> then
<@ TextRuntime.ConvertDateOnlyBack(cultureStr, %%value) @>
elif typ = typeof<TimeOnly> then
<@ TextRuntime.ConvertTimeOnlyBack(cultureStr, %%value) @>
#endif
else
failwith "getBackConversionQuotation: Unsupported primitive type"

Expand Down
2 changes: 1 addition & 1 deletion src/FSharp.Data.DesignTime/FSharp.Data.DesignTime.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<DefineConstants>IS_DESIGNTIME;NO_GENERATIVE;$(DefineConstants)</DefineConstants>
<OtherFlags>$(OtherFlags) --warnon:1182 --nowarn:44</OtherFlags>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
Expand Down
6 changes: 6 additions & 0 deletions src/FSharp.Data.DesignTime/Json/JsonConversionsGenerator.fs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ let getConversionQuotation missingValuesStr cultureStr typ (value: Expr<JsonValu
<@@ JsonRuntime.ConvertDateTime(cultureStr, %value) @@>
elif typ = typeof<TimeSpan> then
<@@ JsonRuntime.ConvertTimeSpan(cultureStr, %value) @@>
#if NET6_0_OR_GREATER
elif typ = typeof<DateOnly> then
<@@ JsonRuntime.ConvertDateOnly(cultureStr, %value) @@>
elif typ = typeof<TimeOnly> then
<@@ JsonRuntime.ConvertTimeOnly(cultureStr, %value) @@>
#endif
elif typ = typeof<Guid> then
<@@ JsonRuntime.ConvertGuid(%value) @@>
else
Expand Down
2 changes: 1 addition & 1 deletion src/FSharp.Data.Json.Core/FSharp.Data.Json.Core.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<OtherFlags>$(OtherFlags) --warnon:1182 --nowarn:10001 --nowarn:44</OtherFlags>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
Expand Down
12 changes: 12 additions & 0 deletions src/FSharp.Data.Json.Core/JsonConversions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/FSharp.Data.Json.Core/JsonRuntime.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<OtherFlags>$(OtherFlags) --warnon:1182 --nowarn:10001 --nowarn:44</OtherFlags>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
Expand Down
2 changes: 1 addition & 1 deletion src/FSharp.Data.Runtime.Utilities/IO.fs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ let internal logTime (_: string) (_: string) = dummyDisposable

#endif

type private FileWatcher(path) =
type private FileWatcher(path: string) =

let subscriptions = Dictionary<string, unit -> unit>()

Expand Down
31 changes: 29 additions & 2 deletions src/FSharp.Data.Runtime.Utilities/StructuralInference.fs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ let private primitiveTypes =
typeof<bool>
typeof<Bit> ]
@ numericTypes
#if NET6_0_OR_GREATER
@ [ typeof<DateOnly>; typeof<TimeOnly> ]
#endif

/// Checks whether a type supports unit of measure
[<Obsolete("This API will be made internal in a future release. Please file an issue at https://github.com/fsprojects/FSharp.Data/issues/1458 if you need this public.")>]
Expand Down Expand Up @@ -110,6 +113,12 @@ let typeTag inferredType =
InferedTypeTag.TimeSpan
elif typ = typeof<Guid> then
InferedTypeTag.Guid
#if NET6_0_OR_GREATER
elif typ = typeof<DateOnly> then
InferedTypeTag.DateOnly
elif typ = typeof<TimeOnly> then
InferedTypeTag.TimeOnly
#endif
else
failwith "typeTag: Unknown primitive type"
| InferedType.Json _ -> InferedTypeTag.Json
Expand Down Expand Up @@ -138,7 +147,16 @@ let private conversionTable =
typeof<int>
typeof<int64>
typeof<decimal> ]
typeof<DateTime>, [ typeof<DateTimeOffset> ] ]
typeof<DateTime>,
[ typeof<DateTimeOffset>
#if NET6_0_OR_GREATER
typeof<DateOnly>
#endif
]
#if NET6_0_OR_GREATER
typeof<TimeSpan>, [ typeof<TimeOnly> ]
#endif
]

let private subtypePrimitives typ1 typ2 =
Debug.Assert(List.exists ((=) typ1) primitiveTypes)
Expand Down Expand Up @@ -425,7 +443,12 @@ let nameToType =
"datetimeoffset", (typeof<DateTimeOffset>, TypeWrapper.None)
"timespan", (typeof<TimeSpan>, TypeWrapper.None)
"guid", (typeof<Guid>, TypeWrapper.None)
"string", (typeof<String>, TypeWrapper.None) ]
"string", (typeof<String>, TypeWrapper.None)
#if NET6_0_OR_GREATER
"dateonly", (typeof<DateOnly>, TypeWrapper.None)
"timeonly", (typeof<TimeOnly>, TypeWrapper.None)
#endif
]
|> dict

// type<unit} or type{unit> is valid while it shouldn't, but well...
Expand Down Expand Up @@ -545,6 +568,10 @@ let inferPrimitiveType
| Parse TextConversions.AsTimeSpan _ -> makePrimitive typeof<TimeSpan>
| Parse TextConversions.AsDateTimeOffset dateTimeOffset when not (isFakeDate dateTimeOffset.UtcDateTime value) ->
makePrimitive typeof<DateTimeOffset>
#if NET6_0_OR_GREATER
| Parse TextConversions.AsDateOnly dateOnly when not (isFakeDate (dateOnly.ToDateTime(TimeOnly.MinValue)) value) ->
makePrimitive typeof<DateOnly>
#endif
| Parse TextConversions.AsDateTime date when not (isFakeDate date value) -> makePrimitive typeof<DateTime>
| Parse TextConversions.AsDecimal _ -> makePrimitive typeof<decimal>
| Parse (TextConversions.AsFloat [||] false) _ -> makePrimitive typeof<float>
Expand Down
12 changes: 12 additions & 0 deletions src/FSharp.Data.Runtime.Utilities/StructuralTypes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ type InferedTypeTag =
| TimeSpan
| DateTimeOffset
| Guid
#if NET6_0_OR_GREATER
| DateOnly
| TimeOnly
#endif
// Collections and sum types
| Collection
| Heterogeneous
Expand Down Expand Up @@ -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 =
Expand All @@ -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"

Expand Down
30 changes: 28 additions & 2 deletions src/FSharp.Data.Runtime.Utilities/TextConversions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand Down
22 changes: 22 additions & 0 deletions src/FSharp.Data.Runtime.Utilities/TextRuntime.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion src/FSharp.Data/FSharp.Data.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<OtherFlags>$(OtherFlags) --warnon:1182 --nowarn:10001</OtherFlags>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
Expand Down
27 changes: 27 additions & 0 deletions tests/FSharp.Data.Core.Tests/TextConversions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
[<Test>]
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

[<Test>]
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

[<Test>]
let ``Guid conversions``() =
let validGuid = Guid.NewGuid()
Expand Down
Loading
Loading