@@ -14,7 +14,7 @@ import _StringProcessing
1414@testable import RegexBuilder
1515
1616// A nibbler processes a single character from a string
17- private protocol Nibbler : CustomRegexComponent {
17+ private protocol Nibbler : CustomMatchingRegexComponent {
1818 func nibble( _: Character ) -> RegexOutput ?
1919}
2020
@@ -24,7 +24,7 @@ extension Nibbler {
2424 _ input: String ,
2525 startingAt index: String . Index ,
2626 in bounds: Range < String . Index >
27- ) -> ( upperBound: String . Index , output: RegexOutput ) ? {
27+ ) throws -> ( upperBound: String . Index , output: RegexOutput ) ? {
2828 guard index != bounds. upperBound, let res = nibble ( input [ index] ) else {
2929 return nil
3030 }
@@ -49,6 +49,69 @@ private struct Asciibbler: Nibbler {
4949 }
5050}
5151
52+ private struct IntParser : CustomMatchingRegexComponent {
53+ struct ParseError : Error , Hashable { }
54+ typealias RegexOutput = Int
55+ func match( _ input: String ,
56+ startingAt index: String . Index ,
57+ in bounds: Range < String . Index >
58+ ) throws -> ( upperBound: String . Index , output: Int ) ? {
59+ guard index != bounds. upperBound else { return nil }
60+
61+ let r = Regex {
62+ Capture ( OneOrMore ( . digit) ) { Int ( $0) }
63+ }
64+
65+ guard let match = input [ index..< bounds. upperBound] . prefixMatch ( of: r) ,
66+ let output = match. 1 else {
67+ throw ParseError ( )
68+ }
69+
70+ return ( match. range. upperBound, output)
71+ }
72+ }
73+
74+ private struct CurrencyParser : CustomMatchingRegexComponent {
75+ enum Currency : String , Hashable {
76+ case usd = " USD "
77+ case ntd = " NTD "
78+ case dem = " DEM "
79+ }
80+
81+ enum ParseError : Error , Hashable {
82+ case unrecognized
83+ case deprecated
84+ }
85+
86+ typealias RegexOutput = Currency
87+ func match( _ input: String ,
88+ startingAt index: String . Index ,
89+ in bounds: Range < String . Index >
90+ ) throws -> ( upperBound: String . Index , output: Currency ) ? {
91+
92+ guard index != bounds. upperBound else { return nil }
93+
94+ let substr = input [ index..< bounds. upperBound]
95+ guard !substr. isEmpty else { return nil }
96+
97+ let currencies : [ Currency ] = [ . usd, . ntd ]
98+ let deprecated : [ Currency ] = [ . dem ]
99+
100+ for currency in currencies {
101+ if substr. hasPrefix ( currency. rawValue) {
102+ return ( input. range ( of: currency. rawValue) !. upperBound, currency)
103+ }
104+ }
105+
106+ for dep in deprecated {
107+ if substr. hasPrefix ( dep. rawValue) {
108+ throw ParseError . deprecated
109+ }
110+ }
111+ throw ParseError . unrecognized
112+ }
113+ }
114+
52115enum MatchCall {
53116 case match
54117 case firstMatch
@@ -223,4 +286,186 @@ class CustomRegexComponentTests: XCTestCase {
223286
224287
225288 }
289+
290+ func testCustomRegexThrows( ) {
291+
292+ func customTest< Match: Equatable , E: Error & Equatable > (
293+ _ regex: Regex < Match > ,
294+ _ tests: ( input: String , match: Match ? , expectError: E ? ) ... ,
295+ file: StaticString = #file,
296+ line: UInt = #line
297+ ) {
298+ for (input, match, expectError) in tests {
299+ do {
300+ let result = try regex. wholeMatch ( in: input) ? . output
301+ XCTAssertEqual ( result, match)
302+ } catch let e as E {
303+ XCTAssertEqual ( e, expectError)
304+ } catch {
305+ XCTFail ( )
306+ }
307+ }
308+ }
309+
310+ func customTest< Match: Equatable , Error1: Error & Equatable , Error2: Error & Equatable > (
311+ _ regex: Regex < Match > ,
312+ _ tests: ( input: String , match: Match ? , expectError1: Error1 ? , expectError2: Error2 ? ) ... ,
313+ file: StaticString = #file,
314+ line: UInt = #line
315+ ) {
316+ for (input, match, expectError1, expectError2) in tests {
317+ do {
318+ let result = try regex. wholeMatch ( in: input) ? . output
319+ XCTAssertEqual ( result, match)
320+ } catch let e as Error1 {
321+ XCTAssertEqual ( e, expectError1, input, file: file, line: line)
322+ } catch let e as Error2 {
323+ XCTAssertEqual ( e, expectError2, input, file: file, line: line)
324+ } catch {
325+ XCTFail ( " caught error: \( error. localizedDescription) " )
326+ }
327+ }
328+ }
329+
330+ func customTest< Capture: Equatable , Error1: Error & Equatable , Error2: Error & Equatable > (
331+ _ regex: Regex < ( Substring , Capture ) > ,
332+ _ tests: ( input: String , match: ( Substring , Capture ) ? , expectError1: Error1 ? , expectError2: Error2 ? ) ... ,
333+ file: StaticString = #file,
334+ line: UInt = #line
335+ ) {
336+ for (input, match, expectError1, expectError2) in tests {
337+ do {
338+ let result = try regex. wholeMatch ( in: input) ? . output
339+ XCTAssertEqual ( result? . 0 , match? . 0 , file: file, line: line)
340+ XCTAssertEqual ( result? . 1 , match? . 1 , file: file, line: line)
341+ } catch let e as Error1 {
342+ XCTAssertEqual ( e, expectError1, input, file: file, line: line)
343+ } catch let e as Error2 {
344+ XCTAssertEqual ( e, expectError2, input, file: file, line: line)
345+ } catch {
346+ XCTFail ( " caught error: \( error. localizedDescription) " )
347+ }
348+ }
349+ }
350+
351+ func customTest< Capture1: Equatable , Capture2: Equatable , Error1: Error & Equatable , Error2: Error & Equatable > (
352+ _ regex: Regex < ( Substring , Capture1 , Capture2 ) > ,
353+ _ tests: ( input: String , match: ( Substring , Capture1 , Capture2 ) ? , expectError1: Error1 ? , expectError2: Error2 ? ) ... ,
354+ file: StaticString = #file,
355+ line: UInt = #line
356+ ) {
357+ for (input, match, expectError1, expectError2) in tests {
358+ do {
359+ let result = try regex. wholeMatch ( in: input) ? . output
360+ XCTAssertEqual ( result? . 0 , match? . 0 , file: file, line: line)
361+ XCTAssertEqual ( result? . 1 , match? . 1 , file: file, line: line)
362+ XCTAssertEqual ( result? . 2 , match? . 2 , file: file, line: line)
363+ } catch let e as Error1 {
364+ XCTAssertEqual ( e, expectError1, input, file: file, line: line)
365+ } catch let e as Error2 {
366+ XCTAssertEqual ( e, expectError2, input, file: file, line: line)
367+ } catch {
368+ XCTFail ( " caught error: \( error. localizedDescription) " )
369+ }
370+ }
371+ }
372+
373+ // No capture, one error
374+ customTest (
375+ Regex {
376+ IntParser ( )
377+ } ,
378+ ( " zzz " , nil , IntParser . ParseError ( ) ) ,
379+ ( " x10x " , nil , IntParser . ParseError ( ) ) ,
380+ ( " 30 " , 30 , nil )
381+ )
382+
383+ customTest (
384+ Regex {
385+ CurrencyParser ( )
386+ } ,
387+ ( " USD " , . usd, nil ) ,
388+ ( " NTD " , . ntd, nil ) ,
389+ ( " NTD USD " , nil , nil ) ,
390+ ( " DEM " , nil , CurrencyParser . ParseError. deprecated) ,
391+ ( " XXX " , nil , CurrencyParser . ParseError. unrecognized)
392+ )
393+
394+ // No capture, two errors
395+ customTest (
396+ Regex {
397+ IntParser ( )
398+ " "
399+ IntParser ( )
400+ } ,
401+ ( " 20304 100 " , " 20304 100 " , nil , nil ) ,
402+ ( " 20304.445 200 " , nil , IntParser . ParseError ( ) , nil ) ,
403+ ( " 20304 200.123 " , nil , nil , IntParser . ParseError ( ) ) ,
404+ ( " 20304.445 200.123 " , nil , IntParser . ParseError ( ) , IntParser . ParseError ( ) )
405+ )
406+
407+ customTest (
408+ Regex {
409+ CurrencyParser ( )
410+ IntParser ( )
411+ } ,
412+ ( " USD100 " , " USD100 " , nil , nil ) ,
413+ ( " XXX100 " , nil , CurrencyParser . ParseError. unrecognized, nil ) ,
414+ ( " USD100.000 " , nil , nil , IntParser . ParseError ( ) ) ,
415+ ( " XXX100.0000 " , nil , CurrencyParser . ParseError. unrecognized, IntParser . ParseError ( ) )
416+ )
417+
418+ // One capture, two errors: One error is thrown from inside a capture,
419+ // while the other one is thrown from outside
420+ customTest (
421+ Regex {
422+ Capture { CurrencyParser ( ) }
423+ IntParser ( )
424+ } ,
425+ ( " USD100 " , ( " USD100 " , . usd) , nil , nil ) ,
426+ ( " NTD305.5 " , nil , nil , IntParser . ParseError ( ) ) ,
427+ ( " DEM200 " , ( " DEM200 " , . dem) , CurrencyParser . ParseError. deprecated, nil ) ,
428+ ( " XXX " , nil , CurrencyParser . ParseError. unrecognized, IntParser . ParseError ( ) )
429+ )
430+
431+ customTest (
432+ Regex {
433+ CurrencyParser ( )
434+ Capture { IntParser ( ) }
435+ } ,
436+ ( " USD100 " , ( " USD100 " , 100 ) , nil , nil ) ,
437+ ( " NTD305.5 " , nil , nil , IntParser . ParseError ( ) ) ,
438+ ( " DEM200 " , ( " DEM200 " , 200 ) , CurrencyParser . ParseError. deprecated, nil ) ,
439+ ( " XXX " , nil , CurrencyParser . ParseError. unrecognized, IntParser . ParseError ( ) )
440+ )
441+
442+ // One capture, two errors: Both errors are thrown from inside the capture
443+ customTest (
444+ Regex {
445+ Capture {
446+ CurrencyParser ( )
447+ IntParser ( )
448+ }
449+ } ,
450+ ( " USD100 " , ( " USD100 " , " USD100 " ) , nil , nil ) ,
451+ ( " NTD305.5 " , nil , nil , IntParser . ParseError ( ) ) ,
452+ ( " DEM200 " , ( " DEM200 " , " DEM200 " ) , CurrencyParser . ParseError. deprecated, nil ) ,
453+ ( " XXX " , nil , CurrencyParser . ParseError. unrecognized, IntParser . ParseError ( ) )
454+ )
455+
456+ // Two captures, two errors: Different erros are thrown from inside captures
457+ customTest (
458+ Regex {
459+ Capture ( CurrencyParser ( ) )
460+ Capture ( IntParser ( ) )
461+ } ,
462+ ( " USD100 " , ( " USD100 " , . usd, 100 ) , nil , nil ) ,
463+ ( " NTD500 " , ( " NTD500 " , . ntd, 500 ) , nil , nil ) ,
464+ ( " XXX20 " , nil , CurrencyParser . ParseError. unrecognized, IntParser . ParseError ( ) ) ,
465+ ( " DEM500 " , nil , CurrencyParser . ParseError. deprecated, nil ) ,
466+ ( " DEM500.345 " , nil , CurrencyParser . ParseError. deprecated, IntParser . ParseError ( ) ) ,
467+ ( " NTD100.345 " , nil , nil , IntParser . ParseError ( ) )
468+ )
469+
470+ }
226471}
0 commit comments