@@ -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 ) -> RegexOutput ?
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: RegexOutput ) ? {
27
+ ) throws -> ( upperBound: String . Index , output: RegexOutput ) ? {
28
28
guard index != bounds. upperBound, let res = nibble ( input [ index] ) else {
29
29
return nil
30
30
}
@@ -49,6 +49,69 @@ private struct Asciibbler: Nibbler {
49
49
}
50
50
}
51
51
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
+
52
115
enum MatchCall {
53
116
case match
54
117
case firstMatch
@@ -223,4 +286,186 @@ class CustomRegexComponentTests: XCTestCase {
223
286
224
287
225
288
}
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
+ }
226
471
}
0 commit comments