PureXML/Tests/RegexCategoryTests.swift
Mihaela Mihaljevic f5c04058dc feat(regex): Unicode category escapes and character-class subtraction
Close the two XSD-regex gaps from the audit:

- \p{...} and \P{...} now resolve Unicode general categories (one-letter
  groups like \p{L} and two-letter codes like \p{Lu}/\p{Nd}) through
  Swift's own scalar generalCategory, so no character tables are vendored.
  A curated set of well-known \p{Is<Block>} blocks is supported; an
  unknown category or block name is a compile error rather than a guess.
- Character-class subtraction [a-z-[aeiou]] removes the subtrahend from a
  class, and nests ([a-z-[b-d-[c]]]) and composes with categories.

Categories and subtraction work both as standalone atoms and inside
character classes, so XSD pattern facets that use them now validate.

Closes #89
2026-06-08 15:15:46 +02:00

74 lines
2.4 KiB
Swift

@testable import PureXML
import Testing
@Suite("Regex Unicode categories and class subtraction")
struct RegexCategoryTests {
private func matches(_ pattern: String, _ string: String) throws -> Bool {
try PureXML.Regex.matches(pattern, string)
}
@Test("\\p{L} matches a letter group and rejects non-letters")
func test_letterGroup() throws {
#expect(try matches("\\p{L}+", "abcÅ"))
#expect(try !matches("\\p{L}+", "ab1"))
}
@Test("\\p{Lu} and \\p{Ll} distinguish case")
func test_caseCategories() throws {
#expect(try matches("\\p{Lu}\\p{Ll}+", "Hello"))
#expect(try !matches("\\p{Lu}\\p{Ll}+", "hello"))
}
@Test("\\p{Nd} matches decimal digits")
func test_decimalNumber() throws {
#expect(try matches("\\p{Nd}+", "2026"))
#expect(try !matches("\\p{Nd}+", "12a"))
}
@Test("\\P{...} negates a category")
func test_negatedCategory() throws {
#expect(try matches("\\P{L}+", "123 !"))
#expect(try !matches("\\P{L}+", "12a"))
}
@Test("A category inside a character class composes with ranges")
func test_categoryInClass() throws {
// A letter or an underscore.
#expect(try matches("[\\p{L}_]+", "a_b"))
#expect(try !matches("[\\p{L}_]+", "a-b"))
}
@Test("\\p{IsBasicLatin} tests a Unicode block")
func test_block() throws {
#expect(try matches("\\p{IsBasicLatin}+", "abc"))
#expect(try !matches("\\p{IsBasicLatin}+", "abç"))
}
@Test("An unknown category name is a compile error")
func test_unknownCategory() {
#expect(throws: PureXML.Regex.RegexError.self) {
_ = try PureXML.Regex.Pattern("\\p{Zz}")
}
}
@Test("Character-class subtraction removes the subtrahend")
func test_subtraction() throws {
#expect(try matches("[a-z-[aeiou]]+", "bcdfg"))
#expect(try !matches("[a-z-[aeiou]]+", "bce"))
}
@Test("Subtraction nests")
func test_nestedSubtraction() throws {
// a-z, minus (b-d minus c): so b and d are removed, c stays.
#expect(try matches("[a-z-[b-d-[c]]]+", "acez"))
#expect(try !matches("[a-z-[b-d-[c]]]+", "abz"))
}
@Test("Subtraction composes with a category subtrahend")
func test_subtractCategory() throws {
// Any letter that is not lowercase.
#expect(try matches("[\\p{L}-[\\p{Ll}]]+", "ABC"))
#expect(try !matches("[\\p{L}-[\\p{Ll}]]+", "ABc"))
}
}