From c35f1111ce1d7b97569ef4e1911d26f6122608a6 Mon Sep 17 00:00:00 2001 From: gusty <1261319+gusty@users.noreply.github.com> Date: Mon, 2 Oct 2023 10:14:41 +0200 Subject: [PATCH 1/5] + Failing tests --- tests/FSharpPlus.Tests/Parsing.fs | 35 ++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/tests/FSharpPlus.Tests/Parsing.fs b/tests/FSharpPlus.Tests/Parsing.fs index 34e604dba..864cb9b6d 100644 --- a/tests/FSharpPlus.Tests/Parsing.fs +++ b/tests/FSharpPlus.Tests/Parsing.fs @@ -20,18 +20,33 @@ module Parsing = [] let parseDateTime () = -#if MONO - let v1 : DateTime = parse "2011-03-04T15:42:19+03:00" - Assert.IsTrue((v1 = DateTime(2011,3,4,12,42,19))) -#else - Assert.Ignore ("Depends on how it's executed...") -#endif + + let t = DateTime(2011,3,14,12,42,19) + let u = DateTimeOffset(2011,3,14,15,42,19, TimeSpan.FromHours 3.) + let u0 = DateTimeOffset(2011,3,14,15,42,19, TimeSpan.FromHours 0.) + + let t1 = parse "2011-03-14T15:42:19+03:00" in Assert.AreEqual( t, t1, nameof t1) + let t2 = tryParse "2011-03-14T15:42:19+03:00" in Assert.AreEqual(Some t, t2, nameof t2) + + let u1 = parse "2011-03-14T15:42:19+03:00" in Assert.AreEqual( u, u1, nameof u1) + let u2 = tryParse "2011-03-14T15:42:19+03:00" in Assert.AreEqual(Some u, u2, nameof u2) + + let t3 = parse "Mon, 14 Mar 2011 12:42:19 GMT" in Assert.AreEqual( t, t3, nameof t3) + let t4 = tryParse "Mon, 14 Mar 2011 12:42:19 GMT" in Assert.AreEqual(Some t, t4, nameof t4) + + let u3 = parse "Mon, 14 Mar 2011 12:42:19 GMT" in Assert.AreEqual( u0, u3, nameof u3) + let u4 = tryParse "Mon, 14 Mar 2011 12:42:19 GMT" in Assert.AreEqual(Some u0, u4, nameof u4) + + let u5 = parse "2011-03-14T15:42:19" in Assert.AreEqual( u0, u5, nameof u5) + let u6 = tryParse "2011-03-14T15:42:19" in Assert.AreEqual(Some u0, u6, nameof u6) + + let u7 = parse "2011-03-14T15:42:19Z" in Assert.AreEqual( u0, u7, nameof u7) + let u8 = tryParse "2011-03-14T15:42:19Z" in Assert.AreEqual(Some u0, u8, nameof u8) + + [] let parse () = - let v2 : DateTimeOffset = parse "2011-03-04T15:42:19+03:00" - - Assert.IsTrue((v2 = DateTimeOffset(2011,3,4,15,42,19, TimeSpan.FromHours 3.))) let _101 = tryParse "10.1.0.1" : Net.IPAddress option let _102 = tryParse "102" : string option @@ -50,7 +65,7 @@ module Parsing = areStEqual r66 (Some 66.0) let r123: WrappedListA option = tryParse "[1;2;3]" - areStEqual r123 (Some (WrappedListA [1; 2; 3])) + areStEqual r123 (Some (WrappedListA [1; 2; 3])) [] let parseCustomType () = From b036b9da474732afac4d8bb22f7401ea5e8af88e Mon Sep 17 00:00:00 2001 From: gusty <1261319+gusty@users.noreply.github.com> Date: Mon, 2 Oct 2023 11:12:50 +0200 Subject: [PATCH 2/5] Fix --- src/FSharpPlus/Control/Converter.fs | 90 ++++++++++++++++++++++++++--- tests/FSharpPlus.Tests/Parsing.fs | 4 +- 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/src/FSharpPlus/Control/Converter.fs b/src/FSharpPlus/Control/Converter.fs index c3398f45a..1c63517ee 100644 --- a/src/FSharpPlus/Control/Converter.fs +++ b/src/FSharpPlus/Control/Converter.fs @@ -111,8 +111,25 @@ type TryParse = static member TryParse (_: string , _: TryParse) = fun x -> Some x : option static member TryParse (_: StringBuilder , _: TryParse) = fun x -> Some (new StringBuilder (x: string)) : option #if !FABLE_COMPILER - static member TryParse (_: DateTime , _: TryParse) = fun (x:string) -> DateTime.TryParseExact (x, [|"yyyy-MM-ddTHH:mm:ss.fffZ"; "yyyy-MM-ddTHH:mm:ssZ"|], null, DateTimeStyles.RoundtripKind) |> tupleToOption : option - static member TryParse (_: DateTimeOffset, _: TryParse) = fun (x:string) -> DateTimeOffset.TryParseExact (x, [|"yyyy-MM-ddTHH:mm:ss.fffK"; "yyyy-MM-ddTHH:mm:ssK"|], null, DateTimeStyles.RoundtripKind) |> tupleToOption : option + + static member TryParse (_: DateTime , _: TryParse) = fun (x:string) -> + match DateTime.TryParseExact (x, [|"yyyy-MM-ddTHH:mm:ss.fffZ"; "yyyy-MM-ddTHH:mm:ssZ"|], null, DateTimeStyles.RoundtripKind) with + | true, x -> Some x + | _ -> + match DateTime.TryParse (x, CultureInfo.InvariantCulture, DateTimeStyles.None) with + | true, x -> Some x + | _ -> None + + static member TryParse (_: DateTimeOffset, _: TryParse) = fun (x:string) -> + match DateTimeOffset.TryParseExact (x, [|"yyyy-MM-ddTHH:mm:ss.fffK"; "yyyy-MM-ddTHH:mm:ssK"|], null, DateTimeStyles.AssumeUniversal) with + | true, x -> Some x + | _ -> + match DateTimeOffset.TryParseExact (x, [|"yyyy-MM-ddTHH:mm:ss.fffK"; "yyyy-MM-ddTHH:mm:ssK"|], null, DateTimeStyles.RoundtripKind) with + | true, x -> Some x + | _ -> + match DateTimeOffset.TryParse (x, CultureInfo.InvariantCulture, DateTimeStyles.None) with + | true, x -> Some x + | _ -> None #endif static member inline Invoke (value: string) = @@ -120,17 +137,24 @@ type TryParse = let inline call (a: 'a) = fun (x: 'x) -> call_2 (a, Unchecked.defaultof<'r>) x : 'r option call Unchecked.defaultof value -type TryParse with - static member inline TryParse (_: 'R, _: Default2) = fun x -> + /// The F# signature + static member inline InvokeOnInstance (value: string) = (^R: (static member TryParse : string -> 'R option) value) + + /// The .Net signature + static member inline InvokeOnConvention (value: string) = let mutable r = Unchecked.defaultof< ^R> - if (^R: (static member TryParse : _ * _ -> _) (x, &r)) then Some r else None + if (^R: (static member TryParse : _ * _ -> _) (value, &r)) then Some r else None + + #if NET7_0 + /// IParsable<'T> + static member InvokeOnInterface<'T when 'T :> IParsable<'T>> (value: string) = + let mutable r = Unchecked.defaultof<'T> + if ('T.TryParse(value, CultureInfo.InvariantCulture, &r)) then Some r else None + #endif - static member inline TryParse (_: ^t when ^t: null and ^t: struct, _: Default1) = id - static member inline TryParse (_: 'R, _: Default1) = fun x -> (^R: (static member TryParse : string -> 'R option) x) type Parse = inherit Default1 - static member inline Parse (_: ^R , _: Default1) = fun (x:string) -> (^R: (static member Parse : _ -> ^R) x) static member inline Parse (_: ^R , _: Parse ) = fun (x:string) -> (^R: (static member Parse : _ * _ -> ^R) (x, CultureInfo.InvariantCulture)) static member inline Parse (_: 'T when 'T : enum<_>, _: Parse ) = fun (x:string) -> @@ -139,6 +163,15 @@ type Parse = | _ -> invalidArg "value" ("Requested value '" + x + "' was not found.") ) : 'enum + #if !FABLE_COMPILER + static member Parse (_: DateTime , _: Parse) = fun (x:string) -> + try DateTime.ParseExact (x, [|"yyyy-MM-ddTHH:mm:ss.fffZ"; "yyyy-MM-ddTHH:mm:ssZ"|], null, DateTimeStyles.RoundtripKind) + with _ -> DateTime.Parse (x, CultureInfo.InvariantCulture) + + static member Parse (_: DateTimeOffset, _: Parse) = fun (x:string) -> + try DateTimeOffset.ParseExact (x, [|"yyyy-MM-ddTHH:mm:ss.fffK"; "yyyy-MM-ddTHH:mm:ssK"|], null, DateTimeStyles.AssumeUniversal) + with _ -> DateTimeOffset.Parse (x, CultureInfo.InvariantCulture) + #endif static member Parse (_: bool , _: Parse) = fun (x:string) -> Boolean.Parse (x) @@ -151,4 +184,45 @@ type Parse = let inline call (a: 'a) = fun (x: 'x) -> call_2 (a, Unchecked.defaultof<'r>) x : 'r call Unchecked.defaultof value + static member inline InvokeOnInstance (value: string) = (^R: (static member Parse : _ -> ^R) value) + + +type Parse with + + static member inline Parse (_: ^R , _: Default4) = fun (value: string) -> + match TryParse.InvokeOnConvention value with + | Some x -> x : ^R + | None -> invalidArg "value" ("Error parsing value '" + value + "'.") + + static member inline Parse (_: ^R , _: Default3) = fun (value: string) -> + match TryParse.InvokeOnInstance value with + | Some x -> x : ^R + | None -> invalidArg "value" ("Error parsing value '" + value + "'.") + + static member inline Parse (_: ^R , _: Default2) : string -> ^R = Parse.InvokeOnInstance + + #if NET7_0 + static member Parse<'T when 'T :> IParsable<'T>> (_: 'T, _: Default1) = fun (x: string) -> 'T.Parse (x, CultureInfo.InvariantCulture) + static member inline Parse (_: ^t when ^t: null and ^t: struct, _: Default1) = id + #else + static member inline Parse (_: ^t when ^t: null and ^t: struct, _: Default2) = id + #endif + +type TryParse with + + static member inline TryParse (_: 'R, _: Default4) : string -> 'R option = fun (value: string) -> + try Parse.InvokeOnInstance value |> Some + with _ -> None // todo, maybe match on invalidArg only + + static member inline TryParse (_: 'R, _: Default3) : string -> 'R option = TryParse.InvokeOnConvention + + static member inline TryParse (_: 'R, _: Default2) : string -> 'R option = TryParse.InvokeOnInstance + + #if NET7_0 + static member inline TryParse (_: 'R, _: Default1) : string -> 'R option = TryParse.InvokeOnInterface + static member inline TryParse (_: ^t when ^t: null and ^t: struct, _: Default1) = id + #else + static member inline TryParse (_: ^t when ^t: null and ^t: struct, _: Default2) = id + #endif + #endif diff --git a/tests/FSharpPlus.Tests/Parsing.fs b/tests/FSharpPlus.Tests/Parsing.fs index 864cb9b6d..45a26aead 100644 --- a/tests/FSharpPlus.Tests/Parsing.fs +++ b/tests/FSharpPlus.Tests/Parsing.fs @@ -34,8 +34,8 @@ module Parsing = let t3 = parse "Mon, 14 Mar 2011 12:42:19 GMT" in Assert.AreEqual( t, t3, nameof t3) let t4 = tryParse "Mon, 14 Mar 2011 12:42:19 GMT" in Assert.AreEqual(Some t, t4, nameof t4) - let u3 = parse "Mon, 14 Mar 2011 12:42:19 GMT" in Assert.AreEqual( u0, u3, nameof u3) - let u4 = tryParse "Mon, 14 Mar 2011 12:42:19 GMT" in Assert.AreEqual(Some u0, u4, nameof u4) + let u3 = parse "Mon, 14 Mar 2011 15:42:19 GMT" in Assert.AreEqual( u0, u3, nameof u3) + let u4 = tryParse "Mon, 14 Mar 2011 15:42:19 GMT" in Assert.AreEqual(Some u0, u4, nameof u4) let u5 = parse "2011-03-14T15:42:19" in Assert.AreEqual( u0, u5, nameof u5) let u6 = tryParse "2011-03-14T15:42:19" in Assert.AreEqual(Some u0, u6, nameof u6) From fbd007218df6acc47e688a91fe4bba6f1efed562 Mon Sep 17 00:00:00 2001 From: gusty <1261319+gusty@users.noreply.github.com> Date: Tue, 3 Oct 2023 08:43:30 +0200 Subject: [PATCH 3/5] Use match instead of try --- src/FSharpPlus/Control/Converter.fs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/FSharpPlus/Control/Converter.fs b/src/FSharpPlus/Control/Converter.fs index 1c63517ee..87da45cd6 100644 --- a/src/FSharpPlus/Control/Converter.fs +++ b/src/FSharpPlus/Control/Converter.fs @@ -165,8 +165,9 @@ type Parse = #if !FABLE_COMPILER static member Parse (_: DateTime , _: Parse) = fun (x:string) -> - try DateTime.ParseExact (x, [|"yyyy-MM-ddTHH:mm:ss.fffZ"; "yyyy-MM-ddTHH:mm:ssZ"|], null, DateTimeStyles.RoundtripKind) - with _ -> DateTime.Parse (x, CultureInfo.InvariantCulture) + match DateTime.TryParseExact (x, [|"yyyy-MM-ddTHH:mm:ss.fffZ"; "yyyy-MM-ddTHH:mm:ssZ"|], null, DateTimeStyles.RoundtripKind) with + | true, x -> x + | _ -> DateTime.Parse (x, CultureInfo.InvariantCulture) static member Parse (_: DateTimeOffset, _: Parse) = fun (x:string) -> try DateTimeOffset.ParseExact (x, [|"yyyy-MM-ddTHH:mm:ss.fffK"; "yyyy-MM-ddTHH:mm:ssK"|], null, DateTimeStyles.AssumeUniversal) @@ -210,7 +211,7 @@ type Parse with type TryParse with - static member inline TryParse (_: 'R, _: Default4) : string -> 'R option = fun (value: string) -> + static member inline TryParse (_: 'R, _: Default4) : string -> 'R option = fun (value: string) -> try Parse.InvokeOnInstance value |> Some with _ -> None // todo, maybe match on invalidArg only From 4bf12a547ffd4d3a79b73ea36ebf6977deb651e0 Mon Sep 17 00:00:00 2001 From: gusty <1261319+gusty@users.noreply.github.com> Date: Tue, 3 Oct 2023 08:59:46 +0200 Subject: [PATCH 4/5] Use same behavior as for Parse --- src/FSharpPlus/Control/Converter.fs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/FSharpPlus/Control/Converter.fs b/src/FSharpPlus/Control/Converter.fs index 87da45cd6..f1beea6cb 100644 --- a/src/FSharpPlus/Control/Converter.fs +++ b/src/FSharpPlus/Control/Converter.fs @@ -124,9 +124,6 @@ type TryParse = match DateTimeOffset.TryParseExact (x, [|"yyyy-MM-ddTHH:mm:ss.fffK"; "yyyy-MM-ddTHH:mm:ssK"|], null, DateTimeStyles.AssumeUniversal) with | true, x -> Some x | _ -> - match DateTimeOffset.TryParseExact (x, [|"yyyy-MM-ddTHH:mm:ss.fffK"; "yyyy-MM-ddTHH:mm:ssK"|], null, DateTimeStyles.RoundtripKind) with - | true, x -> Some x - | _ -> match DateTimeOffset.TryParse (x, CultureInfo.InvariantCulture, DateTimeStyles.None) with | true, x -> Some x | _ -> None From 61959f297fd42a283b558b4a006838dd3b0d82b3 Mon Sep 17 00:00:00 2001 From: gusty <1261319+gusty@users.noreply.github.com> Date: Tue, 3 Oct 2023 09:00:52 +0200 Subject: [PATCH 5/5] Be safe on parsing errors only --- src/FSharpPlus/Control/Converter.fs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/FSharpPlus/Control/Converter.fs b/src/FSharpPlus/Control/Converter.fs index f1beea6cb..5cf8fa41c 100644 --- a/src/FSharpPlus/Control/Converter.fs +++ b/src/FSharpPlus/Control/Converter.fs @@ -124,9 +124,9 @@ type TryParse = match DateTimeOffset.TryParseExact (x, [|"yyyy-MM-ddTHH:mm:ss.fffK"; "yyyy-MM-ddTHH:mm:ssK"|], null, DateTimeStyles.AssumeUniversal) with | true, x -> Some x | _ -> - match DateTimeOffset.TryParse (x, CultureInfo.InvariantCulture, DateTimeStyles.None) with - | true, x -> Some x - | _ -> None + match DateTimeOffset.TryParse (x, CultureInfo.InvariantCulture, DateTimeStyles.None) with + | true, x -> Some x + | _ -> None #endif static member inline Invoke (value: string) = @@ -208,9 +208,10 @@ type Parse with type TryParse with - static member inline TryParse (_: 'R, _: Default4) : string -> 'R option = fun (value: string) -> - try Parse.InvokeOnInstance value |> Some - with _ -> None // todo, maybe match on invalidArg only + static member inline TryParse (_: 'R, _: Default4) : string -> 'R option = fun (value: string) -> + try Some (Parse.InvokeOnInstance value) with + | :? ArgumentNullException | :? FormatException -> None + | _ -> reraise () static member inline TryParse (_: 'R, _: Default3) : string -> 'R option = TryParse.InvokeOnConvention