it’s not as hard as it sounds, if you start with the basic features and work your way up to the more advanced functionality :)
let’s say you want to parse an tv episode descriptor like your last example. to make things interesting, you might want to allow a user to specify both a single episode and a range of episodes. but if a show only has one season, a user might only specify the episode number. so a string-based regex for that might look like this:
/\s*(season\s+(\d+)\s*,\s*)?episode\s+(\d+)\s*(-\s*(\d+)\s*)?/
^~~~~~~~~~~~~~~~~~~~~ ^~~~~ ^~~~~~~~~~~~~~~
^~~~~ episode start ^~~~~
season episode end
optional capture group 0 optional capture group 2
which would produce
((season:Substring)?, episodeStart:Substring, (episodeEnd:Substring)?)
let episodeStart = capture.episodeStart
guard let season = capture.0?.season, let episodeEnd = capture.2?.episodeEnd
...
and accept all of the following strings:
"season 1, episode 1"
" season 1 ,episode 1"
" episode 20 "
" episode 9- 2"
"season 5 , episode 2-4"
"season 5, episode 2 - 4"
but it would be a lot easier to use if we could express this regex pattern using swift, instead of a string literal using its own niche syntax. what if we could do the following?
struct Digit:Parseable
{
// regex for [\d]
}
struct Space:Parseable
{
// regex for [\s]
}
struct Season:Parseable.Terminal
{
static
let token:String = "season"
}
struct Episode:Parseable.Terminal
{
static
let token:String = "episode"
}
struct Comma:Parseable.Terminal
{
static
let token:String = ","
}
struct Hyphen:Parseable.Terminal
{
static
let token:String = "-"
}
// parser for /\d+/
struct Integer:Parseable
{
let value:Int
static
func parse(_ context:ParsingInput) throws -> Self
{
let head:Digit = try .parse(&context),
body:[Digit] = .parse(&context)
// pretend we have an init that takes a digit sequence
return .init([head] + body)
}
}
// parser for /\s+/
struct Whitespace:Parseable
{
static
func parse(_ context:ParsingInput) throws -> Self
{
let _:Space = try .parse(&context),
_:[Space] = .parse(&context)
return .init()
}
}
struct Title:Parseable
{
let season:Int,
episodes:Range<Int>
static
func parse(_ context:ParsingInput) throws -> Self
{
let _:Whitespace? = .parse(&context),
season:
List<Season,
List<Whitespace,
List<Integer,
List<Whitespace?,
List<Comma, Whitespace?>>>>>? =
.parse(&context),
_:Episode = try .parse(&context),
_:Whitespace = try .parse(&context),
start:Integer = try .parse(&context),
_:Whitespace? = .parse(&context),
end:
List<Hyphen,
List<Whitespace?,
List<Integer, Whitespace?>>>? =
.parse(&context)
return .init(season: (season?.body.body.head.value ?? 1) - 1,
episodes: start.value - 1 ..<
end?.body.body.head.value ?? start.value)
}
}
of course, it would be the job of your library to define the protocols Parseable and Parseable.Terminal, implement List<T, U>, and to conform Optional<T> and Array<T> to Parseable. (hint, you can use do-catch to implement the requirement for Optional<T>, and use your Optional<T> implementation to implement Array<T>)