@@ -14,7 +14,7 @@ import _StringProcessing
14
14
@testable import RegexBuilder
15
15
16
16
// A nibbler processes a single character from a string
17
- private protocol Nibbler : CustomRegexComponent {
17
+ private protocol Nibbler : CustomMatchingRegexComponent {
18
18
func nibble( _: Character ) -> Output ?
19
19
}
20
20
@@ -24,7 +24,7 @@ extension Nibbler {
24
24
_ input: String ,
25
25
startingAt index: String . Index ,
26
26
in bounds: Range < String . Index >
27
- ) -> ( upperBound: String . Index , output: Output ) ? {
27
+ ) throws -> ( upperBound: String . Index , output: Output ) ? {
28
28
guard index != bounds. upperBound, let res = nibble ( input [ index] ) else {
29
29
return nil
30
30
}
@@ -49,6 +49,68 @@ private struct Asciibbler: Nibbler {
49
49
}
50
50
}
51
51
52
+ private struct IntParser : CustomMatchingRegexComponent {
53
+ struct ParseError : Error , Hashable { }
54
+ typealias Output = 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
+ let r = Regex {
60
+ Capture ( OneOrMore ( . digit) ) { Int ( $0) }
61
+ }
62
+ guard let match = input [ index..< bounds. upperBound] . wholeMatch ( of: r) ,
63
+ let output = match. output. 1 else {
64
+ throw ParseError ( )
65
+ }
66
+
67
+ return ( match. range. upperBound, output)
68
+ }
69
+ }
70
+
71
+ private struct CurrencyParser : CustomMatchingRegexComponent {
72
+ enum Currency : String {
73
+ case usd = " USD "
74
+ case ntd = " NTD "
75
+ case dem = " DEM "
76
+ }
77
+
78
+ enum ParseError : Error , Hashable {
79
+ case unrecognized
80
+ case deprecated
81
+ }
82
+
83
+ typealias Output = Currency
84
+ func match( _ input: String ,
85
+ startingAt index: String . Index ,
86
+ in bounds: Range < String . Index >
87
+ ) throws -> ( upperBound: String . Index , output: Currency ) ? {
88
+
89
+ guard index != bounds. upperBound else {
90
+ return nil
91
+ }
92
+
93
+ let substr = input [ index..< bounds. upperBound]
94
+
95
+ let currencies : [ Currency ] = [ . usd, . ntd ]
96
+ let deprecated : [ Currency ] = [ . dem ]
97
+
98
+ for currency in currencies {
99
+ if let range = substr. range ( of: currency. rawValue) {
100
+ return ( range. upperBound, currency)
101
+ }
102
+ }
103
+
104
+ for dep in deprecated {
105
+ if let _ = substr. range ( of: dep. rawValue) {
106
+ throw ParseError . deprecated
107
+ }
108
+ }
109
+
110
+ throw ParseError . unrecognized
111
+ }
112
+ }
113
+
52
114
enum MatchCall {
53
115
case match
54
116
case firstMatch
@@ -223,4 +285,99 @@ class CustomRegexComponentTests: XCTestCase {
223
285
224
286
225
287
}
288
+
289
+ func testCustomRegexThrows( ) {
290
+
291
+ func customTest< Match: Equatable , E: Error & Equatable > (
292
+ _ regex: Regex < Match > ,
293
+ _ tests: ( input: String , match: Match ? , expectError: E ? ) ... ,
294
+ file: StaticString = #file,
295
+ line: UInt = #line
296
+ ) {
297
+ for (input, match, expectError) in tests {
298
+ do {
299
+ let result = try regex. wholeMatch ( in: input) ? . output
300
+ XCTAssertEqual ( result, match)
301
+ } catch let e as E {
302
+ XCTAssertEqual ( e, expectError)
303
+ } catch {
304
+ XCTFail ( )
305
+ }
306
+ }
307
+ }
308
+
309
+ customTest (
310
+ Regex {
311
+ IntParser ( )
312
+ } ,
313
+ ( " zzz " , nil , IntParser . ParseError ( ) ) ,
314
+ ( " x10x " , nil , IntParser . ParseError ( ) ) ,
315
+ ( " 30 " , 30 , nil )
316
+ )
317
+
318
+ customTest (
319
+ Regex {
320
+ CurrencyParser ( )
321
+ } ,
322
+ ( " USD " , . usd, nil ) ,
323
+ ( " NTD " , . ntd, nil ) ,
324
+ ( " NTD USD " , . usd, nil ) ,
325
+ ( " DEM " , nil , CurrencyParser . ParseError. deprecated) ,
326
+ ( " XXX " , nil , CurrencyParser . ParseError. unrecognized)
327
+ )
328
+
329
+ customTest (
330
+ Regex {
331
+ CurrencyParser ( )
332
+ IntParser ( )
333
+ } ,
334
+ ( " USD100 " , " USD100 " , nil ) ,
335
+ ( " USD100.000 " , " USD100 " , IntParser . ParseError ( ) )
336
+ )
337
+
338
+ customTest (
339
+ Regex {
340
+ CurrencyParser ( )
341
+ IntParser ( )
342
+ } ,
343
+ ( " XXX100 " , nil , CurrencyParser . ParseError. unrecognized) ,
344
+ ( " XXX100.00 " , nil , CurrencyParser . ParseError. unrecognized) ,
345
+ ( " DEM100 " , nil , CurrencyParser . ParseError. deprecated)
346
+ )
347
+
348
+ func currencyTest(
349
+ _ regex: Regex < ( Substring , CurrencyParser . Currency , Int ) > ,
350
+ _ tests: ( input: String , match: ( Substring , CurrencyParser . Currency , Int ) ? , expectError1: CurrencyParser . ParseError ? , expectError2: IntParser . ParseError ? ) ... ,
351
+ file: StaticString = #file,
352
+ line: UInt = #line
353
+ ) {
354
+ for (input, match, expectError1, expectError2) in tests {
355
+ do {
356
+ let result = try regex. wholeMatch ( in: input) ? . output
357
+ XCTAssertEqual ( result? . 0 , match? . 0 )
358
+ XCTAssertEqual ( result? . 1 , match? . 1 )
359
+ XCTAssertEqual ( result? . 2 , match? . 2 )
360
+ } catch let e as CurrencyParser . ParseError {
361
+ XCTAssertEqual ( e, expectError1)
362
+ } catch let e as IntParser . ParseError {
363
+ XCTAssertEqual ( e, expectError2)
364
+ } catch {
365
+ XCTFail ( " caught error: \( error. localizedDescription) " )
366
+ }
367
+ }
368
+ }
369
+
370
+ currencyTest (
371
+ Regex {
372
+ Capture ( CurrencyParser ( ) )
373
+ Capture ( IntParser ( ) )
374
+ } ,
375
+ ( " USD100 " , ( " USD100 " , . usd, 100 ) , nil , nil ) ,
376
+ ( " NTD500 " , ( " NTD500 " , . ntd, 500 ) , nil , nil ) ,
377
+ ( " XXX20 " , nil , . unrecognized, IntParser . ParseError ( ) ) ,
378
+ ( " DEM500 " , nil , . deprecated, nil ) ,
379
+ ( " DEM500.345 " , nil , . deprecated, IntParser . ParseError ( ) ) ,
380
+ ( " NTD100.345 " , nil , nil , IntParser . ParseError ( ) )
381
+ )
382
+ }
226
383
}
0 commit comments