PureXML/Tests/RegexTests.swift
Mihaela Mihaljevic 951b808f02 feat(regex): XML Schema regular-expression engine
Add the PureXML.Regex namespace: a recursive-descent parser for the XSD
regex grammar (literals, ., character classes with ranges and negation, the
\d \w \s family and negations, single-char escapes, grouping, alternation,
and the ?, *, +, {n}, {n,}, {n,m} quantifiers) compiled to a Thompson NFA.
matches(_:) is whole-string as xs:pattern requires, and runs linear in the
input with no backtracking blow-up. \p{...} category escapes are not yet
supported.

Closes #59
Closes #60
Closes #30
2026-06-08 06:18:50 +02:00

87 lines
3.0 KiB
Swift

@testable import PureXML
import Testing
@Suite("Regex")
struct RegexTests {
private func matches(_ pattern: String, _ string: String) throws -> Bool {
try PureXML.Regex.matches(pattern, string)
}
@Test("Literal patterns match the whole string")
func test_literal() throws {
#expect(try matches("abc", "abc"))
#expect(try !matches("abc", "abcd"))
#expect(try !matches("abc", "ab"))
}
@Test("The dot matches any character except line breaks")
func test_dot() throws {
#expect(try matches("a.c", "axc"))
#expect(try !matches("a.c", "a\nc"))
}
@Test("Quantifiers ? * + bound repetition")
func test_quantifiers() throws {
#expect(try matches("ab?c", "ac"))
#expect(try matches("ab?c", "abc"))
#expect(try matches("ab*c", "abbbc"))
#expect(try matches("ab+c", "abc"))
#expect(try !matches("ab+c", "ac"))
}
@Test("Bounded quantifiers {n}, {n,}, {n,m}")
func test_boundedQuantifiers() throws {
#expect(try matches("a{3}", "aaa"))
#expect(try !matches("a{3}", "aa"))
#expect(try matches("a{2,}", "aaaa"))
#expect(try matches("a{2,4}", "aaa"))
#expect(try !matches("a{2,4}", "aaaaa"))
}
@Test("Alternation and grouping")
func test_alternationGrouping() throws {
#expect(try matches("cat|dog", "dog"))
#expect(try matches("(ab)+", "ababab"))
#expect(try matches("gr(a|e)y", "grey"))
#expect(try !matches("gr(a|e)y", "groy"))
}
@Test("Character classes with ranges and negation")
func test_charClasses() throws {
#expect(try matches("[a-z]+", "hello"))
#expect(try !matches("[a-z]+", "Hello"))
#expect(try matches("[^0-9]+", "abc"))
#expect(try matches("[abc]{2}", "ca"))
}
@Test("Class escapes \\d \\w \\s and their negations")
func test_classEscapes() throws {
#expect(try matches("\\d{4}", "2026"))
#expect(try !matches("\\d{4}", "20x6"))
#expect(try matches("\\w+", "a_1"))
#expect(try matches("a\\sb", "a b"))
#expect(try matches("\\D+", "abc"))
}
@Test("Escaped metacharacters are literal")
func test_escapes() throws {
#expect(try matches("a\\.b", "a.b"))
#expect(try !matches("a\\.b", "axb"))
#expect(try matches("\\(\\)", "()"))
}
@Test("A realistic ISBN-like pattern")
func test_realistic() throws {
let pattern = "\\d{3}-\\d{1,5}-\\d{1,7}-\\d{1,6}-[0-9X]"
#expect(try matches(pattern, "978-0-13-468599-1"))
#expect(try !matches(pattern, "abc-0-13-468599-1"))
}
@Test("Malformed patterns are rejected")
func test_errors() {
#expect(throws: PureXML.Regex.RegexError.self) { _ = try PureXML.Regex.Pattern("a(b") }
#expect(throws: PureXML.Regex.RegexError.self) { _ = try PureXML.Regex.Pattern("*ab") }
#expect(throws: PureXML.Regex.RegexError.self) { _ = try PureXML.Regex.Pattern("") }
}
}