Skip to content

Support putting two format specifiers next to each other #620

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Jun 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e30cb05
Support putting two format specifiers next to each other
Happypig375 May 29, 2025
ee47fdc
Update Parsing.fs
Happypig375 May 29, 2025
666f5d0
Update Parsing.fs
Happypig375 May 29, 2025
c74f1b8
Update Parsing.fs
Happypig375 May 29, 2025
ce008cb
Update Parsing.fs
Happypig375 May 29, 2025
9c73bbb
Update Parsing.fs
Happypig375 May 29, 2025
64dd539
Update Parsing.fs
Happypig375 May 29, 2025
a88a7f4
Fixed the entire feature
Happypig375 Jun 10, 2025
058bb8c
More variations in tests
Happypig375 Jun 10, 2025
86b87da
inline?
Happypig375 Jun 11, 2025
10f3d37
Support consuming spaces before and after
Happypig375 Jun 11, 2025
e407fdb
Align spaces
Happypig375 Jun 11, 2025
0d4a942
This should pass
Happypig375 Jun 12, 2025
49932ac
Test Scan
Happypig375 Jun 12, 2025
35374b7
Fix test
Happypig375 Jun 12, 2025
d4f4959
Update Parsing.fs
Happypig375 Jun 12, 2025
7432c28
Update Parsing.fs
Happypig375 Jun 12, 2025
4bbb1d6
Update Parsing.fs
Happypig375 Jun 12, 2025
b46db24
Update Parsing.fs
Happypig375 Jun 12, 2025
6a94e1e
Update Parsing.fs
Happypig375 Jun 12, 2025
e3ff337
Update Parsing.fs
Happypig375 Jun 12, 2025
0868dcd
Support + specifier to consume plus in front
Happypig375 Jun 13, 2025
a5aa847
Update Parsing.fs
Happypig375 Jun 13, 2025
62378c9
Update Parsing.fs
Happypig375 Jun 13, 2025
17bf00f
Performance optimizations
Happypig375 Jun 13, 2025
9e52640
Better error message
Happypig375 Jun 13, 2025
c7272e5
Update Parsing.fs
Happypig375 Jun 13, 2025
9ca9a54
Update Parsing.fs
Happypig375 Jun 13, 2025
2edf163
Whoops
Happypig375 Jun 13, 2025
0f968bb
Merge branch 'master' into patch-4
Happypig375 Jun 13, 2025
e5a8411
Fix comment
Happypig375 Jun 13, 2025
af42dad
Scan -> Parsedf
Happypig375 Jun 13, 2025
d7902f7
Remove trySscanf active pattern
Happypig375 Jun 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 97 additions & 55 deletions src/FSharpPlus/Parsing.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,69 @@

[<AutoOpen>]
module Parsing =

open System
open System.Text.RegularExpressions
open FSharpPlus
open FSharpPlus.Internals
open FSharpPlus.Internals.Prelude

let inline private getGroups (pf: PrintfFormat<_,_,_,_,_>) s =
let formatters = [|"%A"; "%b"; "%B"; "%c"; "%d"; "%e"; "%E"; "%f"; "%F"; "%g"; "%G"; "%i"; "%M"; "%o"; "%O"; "%s"; "%u"; "%x"; "%X"|]
let formatStr = replace "%%" "%" pf.Value
let constants = split formatters formatStr
let regex = Regex ("^" + String.Join ("(.*?)", constants |> Array.map Regex.Escape) + "$")
let getGroup x =
let groups =
regex.Match(x).Groups
|> Seq.cast<Group>
|> Seq.skip 1
groups
|> Seq.map (fun g -> g.Value)
|> Seq.toArray
(getGroup s, getGroup pf.Value) ||> Array.zipShortest
open Prelude

let inline private getGroups (pf: PrintfFormat<_,_,_,_,_>) =
let format = pf.Value
let regex = System.Text.StringBuilder "^"
let groups = ResizeArray<char>(format.Length / 2) // worst case, there are only format specifiers
let mutable i = 0
while i < String.length format do
match format[i] with
| '%' ->
i <- i + 1
let mutable consumeSpacesAfter = false // consume spaces after if '-' specified
let mutable consumeNumericPlus = false // consume plus before numeric values if '+' specified
while
match format[i] with
| ' ' -> regex.Append @"\s*" |> ignore; true // consume spaces before if ' ' specified
| '-' -> consumeSpacesAfter <- true; true
| '+' -> consumeNumericPlus <- true; true
| '*' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' -> true
| _ -> false
do i <- i + 1
if format[i] <> '%' then groups.Add format[i] // %% does not capture a group
match format[i] with
| 'A' | 'O' -> "(.*?)"
| 'b' -> "([Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee])"
| 'B' ->
if consumeNumericPlus then regex.Append @"\+?" |> ignore
"([01]+)"
| 'c' -> "(.)"
| 'd' | 'i' ->
regex.Append (if consumeNumericPlus then "([+-]?" else "(-?") |> ignore
"[0-9]+)"
| 'e' | 'E' | 'f' | 'F' | 'g' | 'G' | 'M' ->
regex.Append (if consumeNumericPlus then "([+-]?" else "(-?") |> ignore
@"(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][+-]?[0-9]+)?)"
| 'o' ->
if consumeNumericPlus then regex.Append @"\+?" |> ignore
"([0-7]+)"
| 'u' ->
if consumeNumericPlus then regex.Append @"\+?" |> ignore
"([0-9]+)"
| 's' -> "(.*?)"
| 'x' | 'X' ->
if consumeNumericPlus then regex.Append @"\+?" |> ignore
"([0-9a-fA-F]+)"
| '%' -> "%"
| x -> failwith $"Unknown format specifier: {x}"
|> regex.Append |> ignore
if consumeSpacesAfter then regex.Append @"\s*" else regex
| '\\' | '*' | '+' | '?' | '|' | '{' | '[' | '(' | ')' | '^' | '$' | '.' | '#' | ' ' as escape ->
regex.Append('\\').Append escape
| c -> regex.Append c
|> ignore
i <- i + 1
let regex = regex.Append '$' |> string
fun str ->
let m = Regex.Match(str, regex)
if not m.Success then [||] else
Array.init (m.Groups.Count - 1) <| fun i -> struct(m.Groups[i + 1].Value, groups[i])

let inline private conv (destType: System.Type) (b: int) (s: string) =
match destType with
Expand All @@ -38,29 +80,29 @@ module Parsing =
| t when t = typeof<int64> -> Convert.ToInt64 (s, b) |> box
| _ -> invalidOp (sprintf "Type conversion from string to type %A with base %i is not supported" destType b)

let inline private parse (s: string, f: string) : 'r =
let inline private parse struct(s: string, f: char) : 'r =
match f with
| "%B" -> conv typeof<'r> 2 s |> string |> parse
| "%o" -> conv typeof<'r> 8 s |> string |> parse
| "%x" | "%X" -> conv typeof<'r> 16 s |> string |> parse
| 'B' -> conv typeof<'r> 2 s |> string |> parse
| 'o' -> conv typeof<'r> 8 s |> string |> parse
| 'x' | 'X' -> conv typeof<'r> 16 s |> string |> parse
| _ -> parse s

let inline private tryParse (s: string, f: string) : 'r option =
let inline private tryParse struct(s: string, f: char) : 'r option =
match f with
| "%B" -> Option.protect (conv typeof<'r> 2) s |> Option.map string |> Option.bind tryParse
| "%o" -> Option.protect (conv typeof<'r> 8) s |> Option.map string |> Option.bind tryParse
| "%x" | "%X" -> Option.protect (conv typeof<'r> 16) s |> Option.map string |> Option.bind tryParse
| 'B' -> Option.protect (conv typeof<'r> 2) s |> Option.map string |> Option.bind tryParse
| 'o' -> Option.protect (conv typeof<'r> 8) s |> Option.map string |> Option.bind tryParse
| 'x' | 'X' -> Option.protect (conv typeof<'r> 16) s |> Option.map string |> Option.bind tryParse
| _ -> tryParse s

type ParseArray =
static member inline ParseArray (_: 't , _: obj) = fun (g: (string * string) []) -> (parse (g.[0])) : 't
static member inline ParseArray (_: 't , _: obj) = fun (g: struct(string * char) []) -> (parse (g.[0])) : 't

static member inline Invoke (g: (string * string) []) =
static member inline Invoke (g: struct(string * char) []) =
let inline call_2 (a: ^a, b: ^b) = ((^a or ^b) : (static member ParseArray: _*_ -> _) b, a) g
let inline call (a: 'a) = call_2 (a, Unchecked.defaultof<'r>) : 'r
call Unchecked.defaultof<ParseArray>

static member inline ParseArray (t: 't, _: ParseArray) = fun (g: (string * string) []) ->
static member inline ParseArray (t: 't, _: ParseArray) = fun (g: struct(string * char) []) ->
let _f _ = Constraints.whenNestedTuple t : ('t1*'t2*'t3*'t4*'t5*'t6*'t7*'tr)
let (t1: 't1) = parse (g.[0])
let (t2: 't2) = parse (g.[1])
Expand All @@ -72,29 +114,29 @@ module Parsing =
let (tr: 'tr) = ParseArray.Invoke (g.[7..])
Tuple<_,_,_,_,_,_,_,_> (t1, t2, t3, t4, t5, t6, t7, tr) |> retype : 't

static member inline ParseArray (_: unit , _: ParseArray) = fun (_: (string * string) []) -> ()
static member inline ParseArray (_: Tuple<'t1> , _: ParseArray) = fun (g: (string * string) []) -> Tuple<_> (parse g.[0]) : Tuple<'t1>
static member inline ParseArray (_: Id<'t1> , _: ParseArray) = fun (g: (string * string) []) -> Id<_> (parse g.[0])
static member inline ParseArray (_: 't1*'t2 , _: ParseArray) = fun (g: (string * string) []) -> parse g.[0], parse g.[1]
static member inline ParseArray (_: 't1*'t2'*'t3 , _: ParseArray) = fun (g: (string * string) []) -> parse g.[0], parse g.[1], parse g.[2]
static member inline ParseArray (_: 't1*'t2'*'t3*'t4 , _: ParseArray) = fun (g: (string * string) []) -> parse g.[0], parse g.[1], parse g.[2], parse g.[3]
static member inline ParseArray (_: 't1*'t2'*'t3*'t4*'t5 , _: ParseArray) = fun (g: (string * string) []) -> parse g.[0], parse g.[1], parse g.[2], parse g.[3], parse g.[4]
static member inline ParseArray (_: 't1*'t2'*'t3*'t4*'t5*'t6 , _: ParseArray) = fun (g: (string * string) []) -> parse g.[0], parse g.[1], parse g.[2], parse g.[3], parse g.[4], parse g.[5]
static member inline ParseArray (_: 't1*'t2'*'t3*'t4*'t5*'t6*'t7, _: ParseArray) = fun (g: (string * string) []) -> parse g.[0], parse g.[1], parse g.[2], parse g.[3], parse g.[4], parse g.[5], parse g.[6]

let inline private tryParseElemAt i (g: (string * string) []) =
static member inline ParseArray (_: unit , _: ParseArray) = fun (_: struct(string * char) []) -> ()
static member inline ParseArray (_: Tuple<'t1> , _: ParseArray) = fun (g: struct(string * char) []) -> Tuple<_> (parse g.[0]) : Tuple<'t1>
static member inline ParseArray (_: Id<'t1> , _: ParseArray) = fun (g: struct(string * char) []) -> Id<_> (parse g.[0])
static member inline ParseArray (_: 't1*'t2 , _: ParseArray) = fun (g: struct(string * char) []) -> parse g.[0], parse g.[1]
static member inline ParseArray (_: 't1*'t2'*'t3 , _: ParseArray) = fun (g: struct(string * char) []) -> parse g.[0], parse g.[1], parse g.[2]
static member inline ParseArray (_: 't1*'t2'*'t3*'t4 , _: ParseArray) = fun (g: struct(string * char) []) -> parse g.[0], parse g.[1], parse g.[2], parse g.[3]
static member inline ParseArray (_: 't1*'t2'*'t3*'t4*'t5 , _: ParseArray) = fun (g: struct(string * char) []) -> parse g.[0], parse g.[1], parse g.[2], parse g.[3], parse g.[4]
static member inline ParseArray (_: 't1*'t2'*'t3*'t4*'t5*'t6 , _: ParseArray) = fun (g: struct(string * char) []) -> parse g.[0], parse g.[1], parse g.[2], parse g.[3], parse g.[4], parse g.[5]
static member inline ParseArray (_: 't1*'t2'*'t3*'t4*'t5*'t6*'t7, _: ParseArray) = fun (g: struct(string * char) []) -> parse g.[0], parse g.[1], parse g.[2], parse g.[3], parse g.[4], parse g.[5], parse g.[6]

let inline private tryParseElemAt i (g: struct(string * char) []) =
if i < Array.length g then tryParse (g.[i])
else None

type TryParseArray =
static member inline TryParseArray (_:'t, _:obj) = fun (g: (string * string) []) -> tryParseElemAt 0 g : 't option
static member inline TryParseArray (_:'t, _:obj) = fun (g: struct(string * char) []) -> tryParseElemAt 0 g : 't option

static member inline Invoke (g: (string * string) []) =
static member inline Invoke (g: struct(string * char) []) =
let inline call_2 (a: ^a, b: ^b) = ((^a or ^b) : (static member TryParseArray: _*_ -> _) b, a) g
let inline call (a: 'a) = call_2 (a, Unchecked.defaultof<'r>) : 'r option
call Unchecked.defaultof<TryParseArray>

static member inline TryParseArray (t: 't, _: TryParseArray) = fun (g: (string * string) []) ->
static member inline TryParseArray (t: 't, _: TryParseArray) = fun (g: struct(string * char) []) ->
let _f _ = Constraints.whenNestedTuple t : ('t1*'t2*'t3*'t4*'t5*'t6*'t7*'tr)
let (t1: 't1 option) = tryParseElemAt 0 g
let (t2: 't2 option) = tryParseElemAt 1 g
Expand All @@ -108,31 +150,31 @@ module Parsing =
| Some t1, Some t2, Some t3, Some t4, Some t5, Some t6, Some t7, Some tr -> Some (Tuple<_,_,_,_,_,_,_,_> (t1, t2, t3, t4, t5, t6, t7, tr) |> retype : 't)
| _ -> None

static member inline TryParseArray (_: unit , _: TryParseArray) = fun (_: (string * string) []) -> ()
static member inline TryParseArray (_: Tuple<'t1> , _: TryParseArray) = fun (g: (string * string) []) -> Tuple<_> <!> tryParseElemAt 0 g : Tuple<'t1> option
static member inline TryParseArray (_: Id<'t1> , _: TryParseArray) = fun (g: (string * string) []) -> Id<_> <!> tryParseElemAt 0 g
static member inline TryParseArray (_: 't1*'t2 , _: TryParseArray) = fun (g: (string * string) []) -> tuple2 <!> tryParseElemAt 0 g <*> tryParseElemAt 1 g
static member inline TryParseArray (_: 't1*'t2'*'t3 , _: TryParseArray) = fun (g: (string * string) []) -> tuple3 <!> tryParseElemAt 0 g <*> tryParseElemAt 1 g <*> tryParseElemAt 2 g
static member inline TryParseArray (_: 't1*'t2'*'t3*'t4 , _: TryParseArray) = fun (g: (string * string) []) -> tuple4 <!> tryParseElemAt 0 g <*> tryParseElemAt 1 g <*> tryParseElemAt 2 g <*> tryParseElemAt 3 g
static member inline TryParseArray (_: 't1*'t2'*'t3*'t4*'t5 , _: TryParseArray) = fun (g: (string * string) []) -> tuple5 <!> tryParseElemAt 0 g <*> tryParseElemAt 1 g <*> tryParseElemAt 2 g <*> tryParseElemAt 3 g <*> tryParseElemAt 4 g
static member inline TryParseArray (_: 't1*'t2'*'t3*'t4*'t5*'t6 , _: TryParseArray) = fun (g: (string * string) []) -> tuple6 <!> tryParseElemAt 0 g <*> tryParseElemAt 1 g <*> tryParseElemAt 2 g <*> tryParseElemAt 3 g <*> tryParseElemAt 4 g <*> tryParseElemAt 5 g
static member inline TryParseArray (_: 't1*'t2'*'t3*'t4*'t5*'t6*'t7, _: TryParseArray) = fun (g: (string * string) []) -> tuple7 <!> tryParseElemAt 0 g <*> tryParseElemAt 1 g <*> tryParseElemAt 2 g <*> tryParseElemAt 3 g <*> tryParseElemAt 4 g <*> tryParseElemAt 5 g <*> tryParseElemAt 6 g
static member inline TryParseArray (_: unit , _: TryParseArray) = fun (_: struct(string * char) []) -> ()
static member inline TryParseArray (_: Tuple<'t1> , _: TryParseArray) = fun (g: struct(string * char) []) -> Tuple<_> <!> tryParseElemAt 0 g : Tuple<'t1> option
static member inline TryParseArray (_: Id<'t1> , _: TryParseArray) = fun (g: struct(string * char) []) -> Id<_> <!> tryParseElemAt 0 g
static member inline TryParseArray (_: 't1*'t2 , _: TryParseArray) = fun (g: struct(string * char) []) -> tuple2 <!> tryParseElemAt 0 g <*> tryParseElemAt 1 g
static member inline TryParseArray (_: 't1*'t2'*'t3 , _: TryParseArray) = fun (g: struct(string * char) []) -> tuple3 <!> tryParseElemAt 0 g <*> tryParseElemAt 1 g <*> tryParseElemAt 2 g
static member inline TryParseArray (_: 't1*'t2'*'t3*'t4 , _: TryParseArray) = fun (g: struct(string * char) []) -> tuple4 <!> tryParseElemAt 0 g <*> tryParseElemAt 1 g <*> tryParseElemAt 2 g <*> tryParseElemAt 3 g
static member inline TryParseArray (_: 't1*'t2'*'t3*'t4*'t5 , _: TryParseArray) = fun (g: struct(string * char) []) -> tuple5 <!> tryParseElemAt 0 g <*> tryParseElemAt 1 g <*> tryParseElemAt 2 g <*> tryParseElemAt 3 g <*> tryParseElemAt 4 g
static member inline TryParseArray (_: 't1*'t2'*'t3*'t4*'t5*'t6 , _: TryParseArray) = fun (g: struct(string * char) []) -> tuple6 <!> tryParseElemAt 0 g <*> tryParseElemAt 1 g <*> tryParseElemAt 2 g <*> tryParseElemAt 3 g <*> tryParseElemAt 4 g <*> tryParseElemAt 5 g
static member inline TryParseArray (_: 't1*'t2'*'t3*'t4*'t5*'t6*'t7, _: TryParseArray) = fun (g: struct(string * char) []) -> tuple7 <!> tryParseElemAt 0 g <*> tryParseElemAt 1 g <*> tryParseElemAt 2 g <*> tryParseElemAt 3 g <*> tryParseElemAt 4 g <*> tryParseElemAt 5 g <*> tryParseElemAt 6 g


/// Gets a tuple with the result of parsing each element of a string array.
let inline parseArray (source: string []) : '``(T1 * T2 * ... * Tn)`` = ParseArray.Invoke (Array.map (fun x -> (x, "")) source)
let inline parseArray (source: string []) : '``(T1 * T2 * ... * Tn)`` = ParseArray.Invoke (Array.map (fun x -> (x, '\000')) source)

/// Gets a tuple with the result of parsing each element of a formatted text.
let inline sscanf (pf: PrintfFormat<_,_,_,_,'``(T1 * T2 * ... * Tn)``>) s : '``(T1 * T2 * ... * Tn)`` = getGroups pf s |> ParseArray.Invoke
let inline sscanf (pf: PrintfFormat<_,_,_,_,'``(T1 * T2 * ... * Tn)``>) : string -> '``(T1 * T2 * ... * Tn)`` = getGroups pf >> ParseArray.Invoke

/// Gets a tuple with the result of parsing each element of a formatted text from the Console.
let inline scanfn pf : '``(T1 * T2 * ... * Tn)`` = sscanf pf (Console.ReadLine ())

/// Gets a tuple with the result of parsing each element of a string array. Returns None in case of failure.
let inline tryParseArray (source: string []) : '``(T1 * T2 * ... * Tn)`` option = TryParseArray.Invoke (Array.map (fun x -> (x, "")) source)
let inline tryParseArray (source: string []) : '``(T1 * T2 * ... * Tn)`` option = TryParseArray.Invoke (Array.map (fun x -> x, '\000') source)

/// Gets a tuple with the result of parsing each element of a formatted text. Returns None in case of failure.
let inline trySscanf (pf: PrintfFormat<_,_,_,_,'``(T1 * T2 * ... * Tn)``>) s : '``(T1 * T2 * ... * Tn)`` option = getGroups pf s |> TryParseArray.Invoke
let inline trySscanf (pf: PrintfFormat<_,_,_,_,'``(T1 * T2 * ... * Tn)``>) : string -> '``(T1 * T2 * ... * Tn)`` option = getGroups pf >> TryParseArray.Invoke

/// Gets a tuple with the result of parsing each element of a formatted text from the Console. Returns None in case of failure.
let inline tryScanfn pf : '``(T1 * T2 * ... * Tn)`` option = trySscanf pf (Console.ReadLine ())
Expand Down
Loading
Loading