% swift-regex.tex — Swift Regex (5.7 / iOS 16): literal vs Regex(string) vs
% RegexBuilder, Regex<Output> typed captures, named captures, the matching
% family, TryCapture, Foundation parsers as components, NSRegularExpression
% contrast, performance, anchoring, when not to use a regex.
% Source: docs/memos/swift-regex-string-processing.md.
% NOT from the memo (added from knowledge): bare /.../ needs Swift 6 mode or
% the BareSlashRegexLiterals feature (SE-0354), #/.../# extended literal,
% prefixMatch, Regex(_:as:), Match dynamic member lookup, .reluctant, Local,
% NSNotFound for unmatched groups. Foundation component signatures
% (.date(_:locale:timeZone:), .localizedCurrency(code:locale:)) checked
% against Apple's docs 2026-09-25.
% Source fixes: Q14 .localizedCurrency needs locale:; Q6 names come from the
% compiler's literal parsing, not a macro.
% Build ONLY with: tools/print/print-sheet.py <this>.tex --dry-run
% @source: hiot monorepo, docs/school/sheets/swift/swift-regex.tex — the SOURCE OF TRUTH; a copy anywhere else (e.g. artur.gurgul.pro) is regenerated from it, never edited
% @labels: area=swift kind=api level=senior platform=apple new=no round=missing-2026-09-25 topic=language
% @tags: regex, regexbuilder, regex-literal, trycapture, typed-captures, firstmatch, wholematch, nsregularexpression, nsrange, catastrophic-backtracking, redos
\documentclass[8pt]{extarticle}
\usepackage{printup-sheet}
\usepackage{array}

\lstdefinelanguage{SwiftSheet}{
  morekeywords={protocol,class,final,struct,enum,func,var,let,init,import,
    if,else,return,guard,self,nil,try,true,false,in,for,Int,Date,Substring,
    Regex,Reference,Capture,TryCapture,OneOrMore,ZeroOrMore,Anchor,Local,
    ChoiceOf},
  sensitive=true, morecomment=[l]{//}, morestring=[b]"}

\tikzset{
  sb/.style={box, font=\scriptsize, inner sep=1.5pt, minimum height=4.6mm},
  ch/.style={draw=sheetGrey, minimum width=3.6mm, minimum height=4.6mm,
             font=\ttfamily\scriptsize, inner sep=0pt},
  hit/.style={fill=sheetGreen!35},
  lbl/.style={font=\scriptsize, text=black!80, inner sep=1pt, align=left},
  ttl/.style={font=\bfseries\small, anchor=west},
}
\newcolumntype{L}[1]{>{\raggedright\arraybackslash}p{#1}}

\begin{document}

\sheettitle{Swift Regex — literal, runtime, RegexBuilder}{swift · memo}

\oneliner{\texttt{Regex<Output>} (Swift 5.7, iOS 16): \textbf{Output} is the
\emph{typed} shape of a match, \texttt{(Substring, captures…)}. Three builds:
\textbf{literal} (checked + typed at compile time), \texttt{Regex(string)}
(runtime, throws, \texttt{AnyRegexOutput}), \textbf{RegexBuilder} (typed,
composable, embeds Foundation parsers). Matches \textbf{Characters}, returns
\texttt{String.Index} ranges.}

\vspace{2pt}
\noindent\begin{tikzpicture}[sheet]
  % ── three ways ──
  \node[ttl] at (-0.1,2.5) {three ways in $\to$ one type};
  \node[sb, text width=27mm] (lit) at (1.25,1.85) {literal \texttt{/(\textbackslash d+)/} · \texttt{\#/…/\#}\\checked at compile time};
  \node[sb, text width=27mm, draw=sheetOrange, fill=sheetOrange!8] (rt) at (1.25,1.05) {\texttt{try Regex(pattern)}\\throws at run time};
  \node[sb, text width=27mm, draw=sheetGreen, fill=sheetGreen!8] (dsl) at (1.25,0.25) {\texttt{Regex \{ Capture \{…\} \}}\\typed at compile time};
  \node[box, font=\scriptsize, minimum height=13mm, text width=20mm] (R) at (4.75,1.05) {\texttt{Regex<Output>}\\[2pt]typed tuple, or\\\texttt{AnyRegexOutput}};
  \draw[flow] (lit.east) -- (R.north west);
  \draw[flow, draw=sheetOrange] (rt) -- (R);
  \draw[flow, draw=sheetGreen] (dsl.east) -- (R.south west);
  % ── matching family over one string ──
  \draw[sheetGrey!40] (6.1,2.65) -- (6.1,-0.3);
  \node[ttl] at (6.2,2.5) {one pattern \texttt{/\textbackslash d+/}, four questions, input \texttt{"ab12c345"}};
  \newcommand\strip[4]{% y, name, result, hits (i/c list)
    \node[lbl, anchor=east, font=\ttfamily\scriptsize] at (9.3,#1) {#2};
    \foreach \i/\c in {0/a,1/b,2/1,3/2,4/c,5/3,6/4,7/5}
      \node[ch] at (9.55+\i*0.36,#1) {\c};
    \foreach \i/\c in {#4}
      \node[ch, hit] at (9.55+\i*0.36,#1) {\c};
    \node[lbl, anchor=west, font=\ttfamily\scriptsize] at (12.5,#1) {#3};}
  \strip{1.95}{firstMatch(of:)}{"12"}{2/1,3/2}
  \strip{1.4}{matches(of:)}{["12", "345"]}{2/1,3/2,5/3,6/4,7/5}
  \strip{0.85}{wholeMatch(of:)}{nil — must consume ALL}{}
  \strip{0.3}{prefixMatch(of:)}{nil — must start at index 0}{}
  \node[lbl, anchor=west, text=sheetBrown] at (6.2,-0.2) {\texttt{contains(\_:)} · \texttt{ranges(of:)} · \texttt{replacing(\_:with:)} · \texttt{split(separator:)} · \texttt{trimmingPrefix(\_:)} all take a regex};
\end{tikzpicture}

\begin{multicols}{2}

\section{How it works}
\begin{itemize}
  \item \textbf{Literal}: parsed by the compiler — bad pattern = compile
        error; captures become tuple elements, \texttt{(?<year>…)} a
        \emph{labelled} one (\texttt{m.year}). Bare \texttt{/…/} needs Swift 6
        mode or \texttt{BareSlashRegexLiterals} (SE-0354); \texttt{\#/…/\#}
        always works.
  \item \textbf{Runtime} \texttt{try Regex(str)} for user/config patterns:
        \texttt{m.output[1].substring}, \texttt{m.output["y"]?.substring}.
  \item \textbf{RegexBuilder} (\texttt{import RegexBuilder}):
        \texttt{OneOrMore}, \texttt{ZeroOrMore}, \texttt{Optionally},
        \texttt{Repeat(2...4)}, \texttt{ChoiceOf}, \texttt{Anchor},
        \texttt{Lookahead}/\texttt{NegativeLookahead}, \texttt{CharacterClass}
        (\texttt{.digit}, \texttt{.anyOf("+-")}). A literal can sit inside.
  \item \texttt{Capture} adds a tuple element (transform can map it);
        \texttt{TryCapture}'s transform returns \texttt{T?} — \texttt{nil}
        \textbf{fails that attempt and backtracks}: parse + validate in one
        step. \texttt{Reference(T.self)} names a capture: \texttt{m[ref]}.
  \item \textbf{Foundation parsers are components} (SE-0357
        \texttt{CustomConsumingRegexComponent}):
        \texttt{.date(.numeric, locale:, timeZone:)},
        \texttt{.localizedCurrency(code:locale:)} $\to$ \texttt{Decimal},
        \texttt{.iso8601(…)} — typed \texttt{Date}/\texttt{Decimal} captures,
        no second parse. Always pass locale + time zone.
  \item Options return a \emph{new} regex: \texttt{.ignoresCase()},
        \texttt{.dotMatchesNewlines()}; \texttt{.matchingSemantics} switches
        graphemes (default: e+U+0301 is \emph{one} \texttt{.}) to scalars. \texttt{.reluctant} = lazy \texttt{+?};
        \texttt{Local \{…\}} = atomic group. The engine \textbf{backtracks}.
\end{itemize}

\section{Example — typed literal + builder}
\begin{lstlisting}[language=SwiftSheet]
let ym = /(?<y>\d{4})-(?<mo>\d{2})/  // (Substring, y:, mo:)
if let m = "on 2026-09".firstMatch(of: ym) { print(m.y, m.mo) }

import RegexBuilder
let cents = Reference(Int.self)
let line = Regex {
  Capture { /CREDIT|DEBIT/ }                       // Substring
  OneOrMore(.whitespace)
  Capture(.date(.numeric, locale: Locale(identifier: "en_US"),
                timeZone: .gmt))                   // Date
  OneOrMore(.whitespace)
  TryCapture(as: cents) { OneOrMore(.digit) }
    transform: { Int($0) }            // overflow: nil, no match
}
if let m = "DEBIT 3/5/2026 1250".wholeMatch(of: line) {
  let (_, kind, date, _) = m.output; print(kind, date, m[cents]) }
\end{lstlisting}

\columnbreak

\section{vs \texttt{NSRegularExpression}}
{\footnotesize
\begin{tabular}{@{}L{11mm}L{27mm}L{35mm}@{}}
\toprule
 & \textbf{Swift \texttt{Regex}} & \textbf{\texttt{NSRegularExpression}} \\
\midrule
units & \texttt{Character} (graphemes) & UTF-16 code units (ICU) \\
ranges & \texttt{Range<String.Index>} & \texttt{NSRange} → \texttt{Range(r, in: s)} \\
captures & typed tuple & \texttt{range(at:)}; unmatched \texttt{NSNotFound} \\
OS & iOS 16+ & all; runtime errors; \texttt{\$1} templates \\
\bottomrule
\end{tabular}}
\begin{lstlisting}[language=SwiftSheet]
let re = try NSRegularExpression(pattern: #"(\d{4})"#)
let all = NSRange(s.startIndex..., in: s)       // NOT s.count
if let m = re.firstMatch(in: s, range: all),
   let r = Range(m.range(at: 1), in: s) { print(s[r]) }
\end{lstlisting}

\section{Performance \& anchoring}
\begin{itemize}
  \item Build once (a stored \texttt{let}), never in a loop.
        \texttt{firstMatch} retries at every index — anchor (\texttt{\^{}},
        \texttt{Anchor.startOfSubject}) or use \texttt{whole}/\texttt{prefixMatch}.
  \item \textbf{Catastrophic backtracking}: \texttt{/(a+)+\$/} on
        \texttt{"aaaa…b"} is exponential — no nested quantifiers,
        \texttt{Local}. \textbf{No timeout API}: untrusted pattern = ReDoS.
\end{itemize}

\section{When NOT to use a regex}
Fixed text (\texttt{contains}, \texttt{hasPrefix}, \texttt{split}) · JSON
(\texttt{Codable}) · URLs (\texttt{URLComponents}) · ISO dates
(\texttt{ISO8601FormatStyle}) · nested/balanced input — regex can't count.

\section{Interview traps}
\begin{itemize}
  \trap{\texttt{"abc123".wholeMatch(of: /\textbackslash d+/)} is \texttt{nil}:
        whole = implicitly anchored both ends.}
  \trap{\texttt{NSRange(location: 0, length: s.count)} — \texttt{count} is
        Characters, NSRange wants UTF-16: emoji break it. Use
        \texttt{NSRange(s.startIndex..., in: s)}.}
  \trap{\texttt{Regex(str)} for a fixed pattern forfeits compile-time checks
        and typed captures. \texttt{/a*/} ``finds'' an empty range at 0.}
\end{itemize}

\section{Remember}
\textbf{``Literal = typed, String = throws, Builder = composes.''} Whole
anchors, first scans.

\section{Likely questions}
\begin{enumerate}
  \item Type of \texttt{/(\textbackslash d+)-(\textbackslash w+)/}? — \texttt{Regex<(Substring, Substring, Substring)>}.
  \item \texttt{Capture} vs \texttt{TryCapture}? — \texttt{nil} from the transform fails the match.
  \item Why not \texttt{NSRegularExpression}? — UTF-16 \texttt{NSRange}s, untyped, runtime errors.
\end{enumerate}

\end{multicols}

\noindent{\footnotesize\color{sheetGrey}\textit{Related:} \texttt{strings-and-collections}
(graphemes, \texttt{String.Index}) · \texttt{macros-and-result-builders}
(RegexBuilder is a result builder) · \texttt{swift-numerics-overflow}
(\texttt{Decimal}) · FormatStyle / ParseStrategy}

\end{document}
