diff --git a/Tests/RegexBuilderTests/MotivationTests.swift b/Tests/RegexBuilderTests/MotivationTests.swift index 22e790e2d..7dd4c77e4 100644 --- a/Tests/RegexBuilderTests/MotivationTests.swift +++ b/Tests/RegexBuilderTests/MotivationTests.swift @@ -9,18 +9,14 @@ // //===----------------------------------------------------------------------===// -// FIXME: macOS CI seems to be busted and Linux doesn't have FormatStyle -// So, we disable this file for now - -#if false - -import _MatchingEngine - import XCTest import _StringProcessing - import RegexBuilder +// FIXME: macOS CI seems to be busted and Linux doesn't have FormatStyle +// So, we disable this larger test for now. +#if false + private struct Transaction: Hashable { enum Kind: Hashable { case credit @@ -140,17 +136,19 @@ private func processWithRuntimeDynamicRegex( ) -> Transaction? { // FIXME: Shouldn't this init throw? let regex = try! Regex(pattern) + let dateStrat = Date.FormatStyle(date: .numeric).parseStrategy + + guard let result = line.wholeMatch(of: regex)?.output, + let kind = Transaction.Kind(result[1].substring!), + let date = try? Date(String(result[2].substring!), strategy: dateStrat), + let account = result[3].substring.map(String.init), + let amount = try? Decimal( + String(result[4].substring!), format: .currency(code: "USD")) else { + return nil + } -// guard let result = line.match(regex) else { return nil } -// -// // TODO: We should have Regex or somesuch and `.1` -// // should be the same as `\1`. -// let dynCaps = result.1 -// -// -// let kind = Transaction.Kind(result.1.first!.capture as Substring) - - return nil + return Transaction( + kind: kind, date: date, account: account, amount: amount) } @available(macOS 12.0, *) @@ -239,7 +237,8 @@ extension RegexDSLTests { XCTAssertEqual( referenceOutput, processWithNSRegularExpression(line)) - _ = processWithRuntimeDynamicRegex(line) + XCTAssertEqual( + referenceOutput, processWithRuntimeDynamicRegex(line)) // Static run-time regex XCTAssertEqual( @@ -256,12 +255,104 @@ extension RegexDSLTests { XCTFail() continue } - } - } - } #endif +extension RegexDSLTests { + func testProposalExample() { + let statement = """ + CREDIT 04062020 PayPal transfer $4.99 + CREDIT 04032020 Payroll $69.73 + DEBIT 04022020 ACH transfer $38.25 + DEBIT 03242020 IRS tax payment $52249.98 + """ + let expectation: [(TransactionKind, Date, Substring, Double)] = [ + (.credit, Date(mmddyyyy: "04062020")!, "PayPal transfer", 4.99), + (.credit, Date(mmddyyyy: "04032020")!, "Payroll", 69.73), + (.debit, Date(mmddyyyy: "04022020")!, "ACH transfer", 38.25), + (.debit, Date(mmddyyyy: "03242020")!, "IRS tax payment", 52249.98), + ] + + enum TransactionKind: String { + case credit = "CREDIT" + case debit = "DEBIT" + } + + struct Date: Hashable { + var month: Int + var day: Int + var year: Int + + init?(mmddyyyy: String) { + guard let (_, m, d, y) = mmddyyyy.wholeMatch(of: Regex { + Capture(Repeat(.digit, count: 2), transform: { Int($0)! }) + Capture(Repeat(.digit, count: 2), transform: { Int($0)! }) + Capture(Repeat(.digit, count: 4), transform: { Int($0)! }) + })?.output else { + return nil + } + + self.month = m + self.day = d + self.year = y + } + } + + let statementRegex = Regex { + // First, lets capture the transaction kind by wrapping our ChoiceOf in a + // TryCapture because we want + TryCapture { + ChoiceOf { + "CREDIT" + "DEBIT" + } + } transform: { + TransactionKind(rawValue: String($0)) + } + + OneOrMore(.whitespace) + + // Next, lets represent our date as 3 separate repeat quantifiers. The first + // two will require 2 digit characters, and the last will require 4. Then + // we'll take the entire substring and try to parse a date out. + TryCapture { + Repeat(.digit, count: 2) + Repeat(.digit, count: 2) + Repeat(.digit, count: 4) + } transform: { + Date(mmddyyyy: String($0)) + } + + OneOrMore(.whitespace) + + // Next, grab the description which can be any combination of word characters, + // digits, etc. + Capture { + OneOrMore(.any, .reluctant) + } + + OneOrMore(.whitespace) + + "$" + + // Finally, we'll grab one or more digits which will represent the whole + // dollars, match the decimal point, and finally get 2 digits which will be + // our cents. + TryCapture { + OneOrMore(.digit) + "." + Repeat(.digit, count: 2) + } transform: { + Double($0) + } + } + + for (i, match) in statement.matches(of: statementRegex).enumerated() { + let (_, kind, date, description, amount) = match.output + XCTAssert((kind, date, description, amount) == expectation[i]) + } + } +} diff --git a/Tests/RegexBuilderTests/RegexDSLTests.swift b/Tests/RegexBuilderTests/RegexDSLTests.swift index 42cb9c52e..b646f16f7 100644 --- a/Tests/RegexBuilderTests/RegexDSLTests.swift +++ b/Tests/RegexBuilderTests/RegexDSLTests.swift @@ -830,6 +830,38 @@ class RegexDSLTests: XCTestCase { XCTAssertEqual(result[b], 42) } + do { + let key = Reference(Substring.self) + let value = Reference(Int.self) + let input = " " + let regex = Regex { + Capture(as: key) { + Optionally { + OneOrMore(.word) + } + } + ":" + Optionally { + Capture(as: value) { + OneOrMore(.digit) + } transform: { Int($0)! } + } + } + + let result1 = try XCTUnwrap("age:123".wholeMatch(of: regex)) + XCTAssertEqual(result1[key], "age") + XCTAssertEqual(result1[value], 123) + + let result2 = try XCTUnwrap(":567".wholeMatch(of: regex)) + XCTAssertEqual(result2[key], "") + XCTAssertEqual(result2[value], 567) + + let result3 = try XCTUnwrap("status:".wholeMatch(of: regex)) + XCTAssertEqual(result3[key], "status") + // Traps: + // XCTAssertEqual(result3[value], nil) + } + // Post-hoc captured references // #"(?:\w\1|:(\w):)+"# try _testDSLCaptures(