diff --git a/Sources/_StringProcessing/Regex/AnyRegexOutput.swift b/Sources/_StringProcessing/Regex/AnyRegexOutput.swift index 23222da00..00fc2e952 100644 --- a/Sources/_StringProcessing/Regex/AnyRegexOutput.swift +++ b/Sources/_StringProcessing/Regex/AnyRegexOutput.swift @@ -62,6 +62,7 @@ public struct AnyRegexOutput { /// The depth of `Optioals`s wrapping the underlying value. For example, /// `Substring` has optional depth `0`, and `Int??` has optional depth `2`. let optionalDepth: Int + /// The bounds of the output element. let bounds: Range? } @@ -90,7 +91,7 @@ extension AnyRegexOutput { /// - Parameter type: The expected output type. /// - Returns: The output, if the underlying value can be converted to the /// output type; otherwise `nil`. - public func `as`(_ type: Output.Type) -> Output? { + public func `as`(_ type: Output.Type = Output.self) -> Output? { let elements = _elements.map { StructuredCapture( optionalCount: $0.optionalDepth, @@ -206,23 +207,30 @@ extension Regex.Match where Output == AnyRegexOutput { /// - Parameter type: The expected output type. /// - Returns: A match generic over the output type, if the underlying values /// can be converted to the output type; otherwise, `nil`. - public func `as`(_ type: Output.Type) -> Regex.Match? { + public func `as`( + _ type: Output.Type = Output.self + ) -> Regex.Match? { fatalError("FIXME: Not implemented") } } @available(SwiftStdlib 5.7, *) -extension Regex where Output == AnyRegexOutput { +extension Regex { /// Returns whether a named-capture with `name` exists public func contains(captureNamed name: String) -> Bool { - fatalError("FIXME: not implemented") + program.tree.root._captureList.captures.contains(where: { + $0.name == name + }) } +} +@available(SwiftStdlib 5.7, *) +extension Regex where Output == AnyRegexOutput { /// Creates a type-erased regex from an existing regex. /// /// Use this initializer to fit a regex with strongly typed captures into the /// use site of a dynamic regex, i.e. one that was created from a string. - public init(_ match: Regex) { + public init(_ regex: Regex) { fatalError("FIXME: Not implemented") } @@ -231,7 +239,9 @@ extension Regex where Output == AnyRegexOutput { /// - Parameter type: The expected output type. /// - Returns: A regex generic over the output type if the underlying types can be converted. /// Returns `nil` otherwise. - public func `as`(_ type: Output.Type) -> Regex? { + public func `as`( + _ type: Output.Type = Output.self + ) -> Regex? { fatalError("FIXME: Not implemented") } } diff --git a/Tests/RegexBuilderTests/RegexDSLTests.swift b/Tests/RegexBuilderTests/RegexDSLTests.swift index 4e08ea103..8271b61f2 100644 --- a/Tests/RegexBuilderTests/RegexDSLTests.swift +++ b/Tests/RegexBuilderTests/RegexDSLTests.swift @@ -742,43 +742,6 @@ class RegexDSLTests: XCTestCase { } } - func testDynamicCaptures() throws { - do { - let regex = try Regex("aabcc.") - let line = "aabccd" - let match = try XCTUnwrap(line.wholeMatch(of: regex)) - XCTAssertEqual(match.0, line[...]) - let output = match.output - XCTAssertEqual(output[0].substring, line[...]) - } - do { - let regex = try Regex( - #""" - (?[0-9A-F]+)(?:\.\.(?[0-9A-F]+))?\s+;\s+(?\w+).* - """#) - let line = """ - A6F0..A6F1 ; Extend # Mn [2] BAMUM COMBINING MARK KOQNDON..BAMUM \ - COMBINING MARK TUKWENTIS - """ - let match = try XCTUnwrap(line.wholeMatch(of: regex)) - XCTAssertEqual(match.0, line[...]) - let output = match.output - XCTAssertEqual(output[0].substring, line[...]) - XCTAssertTrue(output[1].substring == "A6F0") - XCTAssertTrue(output["lower"]?.substring == "A6F0") - XCTAssertTrue(output[2].substring == "A6F1") - XCTAssertTrue(output["upper"]?.substring == "A6F1") - XCTAssertTrue(output[3].substring == "Extend") - XCTAssertTrue(output["desc"]?.substring == "Extend") - let typedOutput = try XCTUnwrap(output.as( - (Substring, lower: Substring, upper: Substring?, Substring).self)) - XCTAssertEqual(typedOutput.0, line[...]) - XCTAssertTrue(typedOutput.lower == "A6F0") - XCTAssertTrue(typedOutput.upper == "A6F1") - XCTAssertTrue(typedOutput.3 == "Extend") - } - } - func testBackreference() throws { try _testDSLCaptures( ("abc#41#42abcabcabc", ("abc#41#42abcabcabc", "abc", 42, "abc", nil)), diff --git a/Tests/RegexTests/AnyRegexOutputTests.swift b/Tests/RegexTests/AnyRegexOutputTests.swift new file mode 100644 index 000000000..8d91c0ec8 --- /dev/null +++ b/Tests/RegexTests/AnyRegexOutputTests.swift @@ -0,0 +1,157 @@ + +import _StringProcessing +import XCTest + +// Test that our existential capture and concrete captures are +// the same +private func checkSame( + _ aro: AnyRegexOutput, + _ concrete: (Substring, fieldA: Substring, fieldB: Substring) +) { + XCTAssertEqual(aro[0].substring, concrete.0) + + XCTAssertEqual(aro["fieldA"]!.substring, concrete.1) + XCTAssertEqual(aro["fieldA"]!.substring, concrete.fieldA) + + XCTAssertEqual(aro[1].substring, concrete.1) + + XCTAssertEqual(aro["fieldB"]!.substring, concrete.2) + XCTAssertEqual(aro["fieldB"]!.substring, concrete.fieldB) + + XCTAssertEqual(aro[2].substring, concrete.2) + +} +private func checkSame( + _ aro: Regex.Match, + _ concrete: Regex<(Substring, fieldA: Substring, fieldB: Substring)>.Match +) { + checkSame(aro.output, concrete.output) + + XCTAssertEqual(aro.0, concrete.0) + XCTAssertEqual(aro[0].substring, concrete.0) + + XCTAssertEqual(aro["fieldA"]!.substring, concrete.1) + XCTAssertEqual(aro["fieldA"]!.substring, concrete.fieldA) + XCTAssertEqual(aro[1].substring, concrete.1) + + XCTAssertEqual(aro["fieldB"]!.substring, concrete.2) + XCTAssertEqual(aro["fieldB"]!.substring, concrete.fieldB) + XCTAssertEqual(aro[2].substring, concrete.2) +} +private func checkSame( + _ aro: Regex, + _ concrete: Regex<(Substring, fieldA: Substring, fieldB: Substring)> +) { + XCTAssertEqual( + aro.contains(captureNamed: "fieldA"), + concrete.contains(captureNamed: "fieldA")) + XCTAssertEqual( + aro.contains(captureNamed: "fieldB"), + concrete.contains(captureNamed: "fieldB")) + XCTAssertEqual( + aro.contains(captureNamed: "notAField"), + concrete.contains(captureNamed: "notAField")) +} + +extension RegexTests { + func testAnyRegexOutput() { + let regex = try! Regex(#""" + (?x) + (? [^,]*) + , + (? [^,]*) + """#) + + let match = "abc,def".wholeMatch(of: regex)! + XCTAssertEqual(match.0, "abc,def") + XCTAssertEqual(match[0].substring, "abc,def") + + XCTAssertEqual(match["fieldA"]!.substring, "abc") + XCTAssertEqual(match.output["fieldA"]!.substring, "abc") + XCTAssertEqual(match[1].substring, "abc") + + XCTAssertEqual(match["fieldB"]!.substring, "def") + XCTAssertEqual(match.output["fieldB"]!.substring, "def") + XCTAssertEqual(match[2].substring, "def") + + XCTAssertNil(match["notACapture"]) + XCTAssertNil(match.output["notACapture"]) + XCTAssertEqual(match.count, 3) + + XCTAssert(regex.contains(captureNamed: "fieldA")) + XCTAssert(regex.contains(captureNamed: "fieldB")) + XCTAssertFalse(regex.contains(captureNamed: "notAField")) + + // MARK: Check equivalence with concrete + + let regexConcrete: + Regex<(Substring, fieldA: Substring, fieldB: Substring)> + = try! Regex(#""" + (?x) + (? [^,]*) + , + (? [^,]*) + """#) + checkSame(regex, regexConcrete) + + let matchConcrete = "abc,def".wholeMatch(of: regexConcrete)! + checkSame(match, matchConcrete) + + let output = match.output + let concreteOutput = matchConcrete.output + checkSame(output, concreteOutput) + + // TODO: ARO init from concrete match tuple + + let concreteOutputCasted = output.as( + (Substring, fieldA: Substring, fieldB: Substring).self + )! + checkSame(output, concreteOutputCasted) + + var concreteOutputCopy = concreteOutput + concreteOutputCopy = output.as()! + checkSame(output, concreteOutputCopy) + + // TODO: Regex.Match: init from tuple match and as to tuple match + + // TODO: Regex: init from tuple regex and as cast to tuple regex + + } + + func testDynamicCaptures() throws { + do { + let regex = try Regex("aabcc.") + let line = "aabccd" + let match = try XCTUnwrap(line.wholeMatch(of: regex)) + XCTAssertEqual(match.0, line[...]) + let output = match.output + XCTAssertEqual(output[0].substring, line[...]) + } + do { + let regex = try Regex( + #""" + (?[0-9A-F]+)(?:\.\.(?[0-9A-F]+))?\s+;\s+(?\w+).* + """#) + let line = """ + A6F0..A6F1 ; Extend # Mn [2] BAMUM COMBINING MARK KOQNDON..BAMUM \ + COMBINING MARK TUKWENTIS + """ + let match = try XCTUnwrap(line.wholeMatch(of: regex)) + XCTAssertEqual(match.0, line[...]) + let output = match.output + XCTAssertEqual(output[0].substring, line[...]) + XCTAssertTrue(output[1].substring == "A6F0") + XCTAssertTrue(output["lower"]?.substring == "A6F0") + XCTAssertTrue(output[2].substring == "A6F1") + XCTAssertTrue(output["upper"]?.substring == "A6F1") + XCTAssertTrue(output[3].substring == "Extend") + XCTAssertTrue(output["desc"]?.substring == "Extend") + let typedOutput = try XCTUnwrap(output.as( + (Substring, lower: Substring, upper: Substring?, Substring).self)) + XCTAssertEqual(typedOutput.0, line[...]) + XCTAssertTrue(typedOutput.lower == "A6F0") + XCTAssertTrue(typedOutput.upper == "A6F1") + XCTAssertTrue(typedOutput.3 == "Extend") + } + } +}