import Testing @testable import PureXML /// `contains`, `substring-before`, and `substring-after` locate the needle /// through a linear-time (KMP) search rather than matching at every position /// (which is quadratic on a repetitive string). These pin the search semantics: /// found at the start/middle/end, not found, overlapping repeats, and the empty /// needle, plus a repetitive case that the naive search would have made slow. @Suite("substring search functions") struct SubstringSearchTests { private func string(_ expression: String) throws -> String { try PureXML.XPath.Query(expression).value(at: PureXML.parseTree("")).string } private func boolean(_ expression: String) throws -> Bool { try PureXML.XPath.Query(expression).value(at: PureXML.parseTree("")).boolean } @Test("contains finds the needle anywhere, or reports its absence") func test_contains() throws { #expect(try boolean("contains('hello world', 'world')")) #expect(try boolean("contains('hello world', 'hello')")) #expect(try boolean("contains('hello world', 'o w')")) #expect(try !boolean("contains('hello', 'xyz')")) #expect(try boolean("contains('hello', '')")) // Overlapping repeats: the match starts at the first valid position. #expect(try boolean("contains('aaaa', 'aaa')")) #expect(try !boolean("contains('aaa', 'aaaa')")) } @Test("substring-before and substring-after split at the first occurrence") func test_split() throws { #expect(try string("substring-before('1999/04/01', '/')") == "1999") #expect(try string("substring-after('1999/04/01', '/')") == "04/01") #expect(try string("substring-before('a-b-c', '-')") == "a") #expect(try string("substring-after('a-b-c', '-')") == "b-c") // Needle absent: both return the empty string (XPath 1.0). #expect(try string("substring-before('abc', 'x')") == "") #expect(try string("substring-after('abc', 'x')") == "") } @Test("the search is correct on a repetitive string the naive scan made slow") func test_repetitive() throws { let haystack = String(repeating: "a", count: 500) + "b" // 'a...ab' (the whole string) is present once, at the start. #expect(try boolean("contains('\(haystack)', '\(haystack)')")) // 'a...a' + 'c' is absent; the search must report false without rescanning. let absent = String(repeating: "a", count: 400) + "c" #expect(try !boolean("contains('\(haystack)', '\(absent)')")) #expect(try string("substring-after('\(haystack)', 'b')") == "") } }