swift-parsing
swift-parsing copied to clipboard
Take and/or TakeUpTo parsers
This is a pitch for adding one or possibly both parsers to the core library. There is nothing in the library that currently lets you do what these do. They are both achieving the same basic goal with a slightly different approach.
They were inspired by this thread in Slack, and I figured the solution might be worth sharing.
For example, the goal from the thread was parsing episode names. This is a simpler example, with a standard format of "
This is actually quite hard to parse right now, since name could contain anything (including "-", digits, etc). The main rule is that it always ends with " -
struct EpisodeTitle {
let name: String
let number: Int
}
Take
The Take
parser is provided with two parsers: a 'taker' and a 'terminator'. The results for both are returned as a tuple. Here's how it would parse EpisodeTitle
:
let parser: some Parser<Substring, EpisodeTitle> = Parse(EpisodeTitle.init) {
Take {
Prefix(1...).map(.string)
} upTo: {
" - "
Int.parser()
End()
}
}
let episodeTitle = try parser.parse("Testing, 1, 2, 3 - a Brief History - Ep 2")
Because Take
's output is a tuple, it's signature looks like this in our example:
let takeParser: some Parser<Substring, (String, Int)> = Take {
Prefix(1...).map(.string)
} upTo: {
" - "
Int.parser()
End()
}
This is a bit odd, but does generally boil down into single-level tuples thanks to how parsers work in the library.
TakeUpTo
This variant just lets you specify the terminator
Parser
, and anything before that is returned unmodified.
This is a somewhat simpler implementation, but with the key limitation that it doesn't let you have any complex parsing before the terminator. Also, it because it doesn't return the value parsed by the terminator, it can't return that value either, so it has to be parsed again.
For example:
let parser: some Parser<Substring, EpisodeTitle> = Parse(EpisodeTitle.init) {
TakeUpTo {
" - "
Int.parser()
End()
}.map(.string)
" - "
Int.parser()
End()
}
let episodeTitle = try parser.parse("Testing, 1, 2, 3 - a Brief History - Ep 2")
Alternate approaches
I was unable to find anything which would allow parsing in situations like this, but I might have missed something. I attempted to use:
Parser(Episode.init) {
Many {
Prefix(1)
} terminator: {
" - "
Int.parser()
End()
}.map(.string)
" - "
Int.parser()
End()
}
However, Many
doesn't actively check the terminator - it just checks if the next input matches the terminator once the main parser stops matching. As such, just eats all the text, including the episode details.
Anyway, would like to hear your thoughts. Of the two, Take
is more flexible, but also more complicated, and still has some rough edges I'd like to sand off.