Skip to content

Instantly share code, notes, and snippets.

@pauljohanneskraft
Last active April 6, 2025 15:40
Show Gist options
  • Save pauljohanneskraft/fed9b91dabaf1a0c265098cc683b8cb2 to your computer and use it in GitHub Desktop.
Save pauljohanneskraft/fed9b91dabaf1a0c265098cc683b8cb2 to your computer and use it in GitHub Desktop.

Regex Matching in Swift using switch-case like syntax

Scenario

In this short bit, we want to decide whether a string is a phone number or an email address and create a value from this enum based on a string. We further created the two regexes to detect the different types - they may not be perfect, but this isn't the focus of this bit.

enum ContactPoint {
    case phoneNumber(String)
    case emailAddress(local: String, domain: String)
}

let phoneNumberRegex = /^\+?(\d{1,4})[\s\-\.]?(?:\(?(\d{1,4})\)?[\s\-\.]?)?([\d\s\-\.]{4,})$/
let emailRegex = /^(?<local>[A-Za-z0-9._%+-]+)@(?<domain>[A-Za-z0-9.-]+\.[A-Za-z]{2,})$/

let string = "+1 (555) 123-4567"

The most elegant but only fictional solution

Unfortunately, regex pattern matching support for switch-case statements isn't available in Swift. I could imagine a syntax like this:

let result: ContactPoint? = switch string {
case phoneNumberRegex as match:
  .phoneNumber(string)
case emailRegex as match:
  .email(local: match.local, domain: match.domain)
default:
  nil
}

Something very similar to this is possible by overriding the ~= operator, but it is limited to matching and you couldn't get the output without performing the match again. With the help of the last extension in the Swift file, we can write:

let result: ContactPoint? = switch string {
case phoneNumberRegex:
  .phoneNumber(string)
case emailRegex:
  // match doesn't exist here though, so we would need to rewrite the following line
  .email(local: match.local, domain: match.domain)
default:
  nil
}

Current approach

At the moment, Swift supports if-let chaining to handle this case, which may not be the most elegant.

let result: ContactPoint? = if let match = try? phoneNumberRegex.wholeMatch(in: string) {
    .phoneNumber(string)
} else if let match = try? emailRegex.wholeMatch(in: string) {
    .emailAddress(local: String(match.local), domain: String(match.domain))
} else {
    nil
}

New solution

With the code in the Swift file, you can write code like this to solve this issue:

let result: ContactPoint? = string.wholeMatch(
  phoneNumberRegex.transform { _ in
    .phoneNumber(string)
  },
  emailRegex.transform { match in
    .emailAddress(local: String(match.local), domain: String(match.domain))
  }
)
public enum RegexMatchingStrategy {
case first
case whole
case prefix
}
public protocol RegexMatcher<MatchOutput> {
associatedtype MatchOutput
func match(input: String, strategy: RegexMatchingStrategy) -> MatchOutput?
}
extension Regex: RegexMatcher {
public typealias MatchOutput = Match
public func match(input: String, strategy: RegexMatchingStrategy) -> Match? {
do {
return try match(input, using: strategy)
} catch {
return nil
}
}
public func transform<NewOutput>(
_ transform: @escaping (Match) throws -> NewOutput,
onError: @escaping (Error) -> Void = { _ in }
) -> some RegexMatcher<NewOutput> {
AnyRegexMatcher { input, strategy in
do {
return try match(input, using: strategy)
.flatMap(transform)
} catch {
onError(error)
return nil
}
}
}
private func match(_ input: String, using strategy: RegexMatchingStrategy) throws -> Match? {
return switch strategy {
case .first: try firstMatch(in: input)
case .whole: try wholeMatch(in: input)
case .prefix: try prefixMatch(in: input)
}
}
}
fileprivate struct AnyRegexMatcher<Output>: RegexMatcher {
func match(input: String, strategy: RegexMatchingStrategy) -> Output? {
return _match(input, strategy)
}
typealias MatchOutput = Output
private let _match: (String, RegexMatchingStrategy) -> Output?
init(match: @escaping (String, RegexMatchingStrategy) -> Output?) {
self._match = match
}
}
extension String {
public func firstMatch<Output>(_ matchers: any RegexMatcher<Output>...) -> Output? {
for matcher in matchers {
if let result = matcher.match(input: self, strategy: .first) {
return result
}
}
return nil
}
public func wholeMatch<Output>(_ matchers: any RegexMatcher<Output>...) -> Output? {
for matcher in matchers {
if let result = matcher.match(input: self, strategy: .whole) {
return result
}
}
return nil
}
public func prefixMatch<Output>(_ matchers: any RegexMatcher<Output>...) -> Output? {
for matcher in matchers {
if let result = matcher.match(input: self, strategy: .prefix) {
return result
}
}
return nil
}
}
extension RegexComponent {
public static func ~= (lhs: Self, rhs: String) -> Bool {
rhs.contains(lhs)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment