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
87 lines
3.0 KiB
Swift
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("") }
|
|
}
|
|
}
|