1use super::error::{ExtractionError, Result};
7use super::types::Language;
8use regex::Regex;
9
10#[derive(Debug, Clone)]
12pub struct FieldPattern {
13 pub field_type: InvoiceFieldType,
15
16 pub regex: Regex,
18
19 pub confidence_base: f64,
21
22 pub language: Option<Language>,
24
25 pub context_hints: Vec<String>,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum InvoiceFieldType {
32 InvoiceNumber,
33 InvoiceDate,
34 DueDate,
35 TotalAmount,
36 TaxAmount,
37 NetAmount,
38 VatNumber,
39 SupplierName,
40 CustomerName,
41 Currency,
42 ArticleNumber,
43 LineItemDescription,
44 LineItemQuantity,
45 LineItemUnitPrice,
46}
47
48impl FieldPattern {
49 pub fn new(
51 field_type: InvoiceFieldType,
52 pattern: &str,
53 confidence_base: f64,
54 language: Option<Language>,
55 ) -> Result<Self> {
56 let regex = Regex::new(pattern)
57 .map_err(|e| ExtractionError::RegexError(format!("{}: {}", pattern, e)))?;
58
59 Ok(Self {
60 field_type,
61 regex,
62 confidence_base,
63 language,
64 context_hints: Vec::new(),
65 })
66 }
67
68 pub fn with_hints(mut self, hints: Vec<String>) -> Self {
70 self.context_hints = hints;
71 self
72 }
73
74 pub fn matches(&self, text: &str) -> Option<String> {
76 self.regex
77 .captures(text)
78 .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
79 }
80}
81
82pub struct PatternLibrary {
84 patterns: Vec<FieldPattern>,
85}
86
87impl PatternLibrary {
88 pub fn new() -> Self {
90 Self {
91 patterns: Vec::new(),
92 }
93 }
94
95 pub fn with_language(lang: Language) -> Self {
97 let mut lib = Self::new();
98 lib.load_patterns_for_language(lang);
99 lib
100 }
101
102 pub fn add_pattern(&mut self, pattern: FieldPattern) {
104 self.patterns.push(pattern);
105 }
106
107 pub fn default_spanish() -> Self {
131 Self::with_language(Language::Spanish)
132 }
133
134 pub fn default_english() -> Self {
146 Self::with_language(Language::English)
147 }
148
149 pub fn default_german() -> Self {
161 Self::with_language(Language::German)
162 }
163
164 pub fn default_italian() -> Self {
176 Self::with_language(Language::Italian)
177 }
178
179 pub fn merge(&mut self, other: PatternLibrary) {
207 self.patterns.extend(other.patterns);
208 }
209
210 pub fn match_text(&self, text: &str) -> Vec<(InvoiceFieldType, String, f64)> {
212 let mut matches = Vec::new();
213
214 for pattern in &self.patterns {
215 if let Some(matched_value) = pattern.matches(text) {
216 matches.push((pattern.field_type, matched_value, pattern.confidence_base));
217 }
218 }
219
220 matches
221 }
222
223 fn load_patterns_for_language(&mut self, lang: Language) {
225 match lang {
226 Language::Spanish => self.load_spanish_patterns(),
227 Language::English => self.load_english_patterns(),
228 Language::German => self.load_german_patterns(),
229 Language::Italian => self.load_italian_patterns(),
230 }
231 }
232
233 fn load_spanish_patterns(&mut self) {
235 if let Ok(pattern) = FieldPattern::new(
238 InvoiceFieldType::InvoiceNumber,
239 r"(?:Factura|FACTURA|Fac\.?)\s+(?:N[úuº°]?\.?|Número)\s*:?\s*([A-Z0-9][A-Z0-9\-/]*)",
240 0.9,
241 Some(Language::Spanish),
242 ) {
243 self.add_pattern(pattern.with_hints(vec![
244 "factura".to_string(),
245 "número".to_string(),
246 "nº".to_string(),
247 ]));
248 }
249
250 if let Ok(pattern) = FieldPattern::new(
253 InvoiceFieldType::InvoiceDate,
254 r"(?:Fecha(?:\s+de\s+emisión)?|FECHA):?\s*(\d{1,2}[-/]\d{1,2}[-/]\d{2,4})",
255 0.85,
256 Some(Language::Spanish),
257 ) {
258 self.add_pattern(pattern.with_hints(vec!["fecha".to_string(), "emisión".to_string()]));
259 }
260
261 if let Ok(pattern) = FieldPattern::new(
264 InvoiceFieldType::DueDate,
265 r"(?:Vencimiento|Fecha\s+de\s+vencimiento|VENCIMIENTO):?\s*(\d{1,2}[-/]\d{1,2}[-/]\d{2,4})",
266 0.85,
267 Some(Language::Spanish),
268 ) {
269 self.add_pattern(pattern.with_hints(vec!["vencimiento".to_string()]));
270 }
271
272 if let Ok(pattern) = FieldPattern::new(
275 InvoiceFieldType::TotalAmount,
276 r"(?:Total|TOTAL|Importe\s+Total):?\s*€?\s*([0-9]{1,3}(?:[.,][0-9]{3})*[.,][0-9]{2})\s*€?",
277 0.9,
278 Some(Language::Spanish),
279 ) {
280 self.add_pattern(pattern.with_hints(vec!["total".to_string(), "importe".to_string()]));
281 }
282
283 if let Ok(pattern) = FieldPattern::new(
286 InvoiceFieldType::TaxAmount,
287 r"(?:IVA|I\.V\.A\.|Impuesto).*?:?\s*€?\s*([0-9]{1,3}(?:[.,][0-9]{3})*[.,][0-9]{2})\s*€?",
288 0.85,
289 Some(Language::Spanish),
290 ) {
291 self.add_pattern(pattern.with_hints(vec!["iva".to_string(), "impuesto".to_string()]));
292 }
293
294 if let Ok(pattern) = FieldPattern::new(
297 InvoiceFieldType::NetAmount,
298 r"(?:Base\s+Imponible|Base):?\s*€?\s*([0-9]{1,3}(?:[.,][0-9]{3})*[.,][0-9]{2})\s*€?",
299 0.85,
300 Some(Language::Spanish),
301 ) {
302 self.add_pattern(pattern.with_hints(vec!["base".to_string(), "imponible".to_string()]));
303 }
304
305 if let Ok(pattern) = FieldPattern::new(
308 InvoiceFieldType::NetAmount,
309 r"(?:Neto|NETO|Subtotal|SUBTOTAL|Suma\s+Neta):?\s*€?\s*([0-9]{1,3}(?:[.,][0-9]{3})*[.,][0-9]{2})\s*€?",
310 0.80,
311 Some(Language::Spanish),
312 ) {
313 self.add_pattern(pattern.with_hints(vec!["neto".to_string(), "subtotal".to_string()]));
314 }
315
316 if let Ok(pattern) = FieldPattern::new(
319 InvoiceFieldType::VatNumber,
320 r"(?:CIF|NIF|N\.I\.F\.|C\.I\.F\.):?\s*([A-Z]?[0-9]{8}[A-Z0-9])",
321 0.9,
322 Some(Language::Spanish),
323 ) {
324 self.add_pattern(pattern.with_hints(vec!["cif".to_string(), "nif".to_string()]));
325 }
326
327 if let Ok(pattern) = FieldPattern::new(
330 InvoiceFieldType::CustomerName,
331 r"(?:Cliente|CLIENTE|Facturar\s+a|FACTURAR\s+A|Destinatario|DESTINATARIO|A\s+la\s+atención\s+de|Att\.?|Attn\.?):?\s*([A-ZÀ-ÿa-z][A-ZÀ-ÿa-z\s\.,&\-]{2,80})",
332 0.75,
333 Some(Language::Spanish),
334 ) {
335 self.add_pattern(pattern.with_hints(vec![
336 "cliente".to_string(),
337 "facturar".to_string(),
338 "destinatario".to_string(),
339 "atención".to_string(),
340 ]));
341 }
342
343 if let Ok(pattern) = FieldPattern::new(
346 InvoiceFieldType::Currency,
347 r"(?:Moneda:?\s+)?(€|EUR|USD|GBP|CHF)",
348 0.7,
349 Some(Language::Spanish),
350 ) {
351 self.add_pattern(pattern);
352 }
353 }
354
355 fn load_english_patterns(&mut self) {
357 if let Ok(pattern) = FieldPattern::new(
360 InvoiceFieldType::InvoiceNumber,
361 r"(?:Invoice|INVOICE)\s+(?:#|No\.?|Number)\s*:?\s*([A-Z0-9][A-Z0-9\-/]*)",
362 0.9,
363 Some(Language::English),
364 ) {
365 self.add_pattern(pattern.with_hints(vec![
366 "invoice".to_string(),
367 "number".to_string(),
368 "no".to_string(),
369 ]));
370 }
371
372 if let Ok(pattern) = FieldPattern::new(
375 InvoiceFieldType::InvoiceDate,
376 r"(?:(?:Invoice\s+)?Date|DATE):?\s*(\d{1,2}[-/]\d{1,2}[-/]\d{2,4})",
377 0.85,
378 Some(Language::English),
379 ) {
380 self.add_pattern(pattern.with_hints(vec!["date".to_string(), "invoice".to_string()]));
381 }
382
383 if let Ok(pattern) = FieldPattern::new(
386 InvoiceFieldType::DueDate,
387 r"(?:Due\s+Date|Payment\s+Due|DUE\s+DATE):?\s*(\d{1,2}[-/]\d{1,2}[-/]\d{2,4})",
388 0.85,
389 Some(Language::English),
390 ) {
391 self.add_pattern(pattern.with_hints(vec!["due".to_string(), "payment".to_string()]));
392 }
393
394 if let Ok(pattern) = FieldPattern::new(
397 InvoiceFieldType::TotalAmount,
398 r"(?:Total|TOTAL|Amount\s+Due):?\s*[$£]?\s*([0-9]{1,3}(?:,[0-9]{3})*\.[0-9]{2})\s*[$£]?",
399 0.9,
400 Some(Language::English),
401 ) {
402 self.add_pattern(pattern.with_hints(vec![
403 "total".to_string(),
404 "amount".to_string(),
405 "due".to_string(),
406 ]));
407 }
408
409 if let Ok(pattern) = FieldPattern::new(
412 InvoiceFieldType::TaxAmount,
413 r"(?:VAT|Tax|V\.A\.T\.).*?:?\s*[$£]?\s*([0-9]{1,3}(?:,[0-9]{3})*\.[0-9]{2})\s*[$£]?",
414 0.85,
415 Some(Language::English),
416 ) {
417 self.add_pattern(pattern.with_hints(vec!["vat".to_string(), "tax".to_string()]));
418 }
419
420 if let Ok(pattern) = FieldPattern::new(
423 InvoiceFieldType::NetAmount,
424 r"(?:Subtotal|Net\s+Amount|SUBTOTAL):?\s*[$£]?\s*([0-9]{1,3}(?:,[0-9]{3})*\.[0-9]{2})\s*[$£]?",
425 0.85,
426 Some(Language::English),
427 ) {
428 self.add_pattern(pattern.with_hints(vec!["subtotal".to_string(), "net".to_string()]));
429 }
430
431 if let Ok(pattern) = FieldPattern::new(
434 InvoiceFieldType::NetAmount,
435 r"(?:Net|NET|Sub-total|SUB-TOTAL|Net\s+Sum):?\s*[$£]?\s*([0-9]{1,3}(?:,[0-9]{3})*\.[0-9]{2})\s*[$£]?",
436 0.80,
437 Some(Language::English),
438 ) {
439 self.add_pattern(pattern.with_hints(vec!["net".to_string(), "sub".to_string()]));
440 }
441
442 if let Ok(pattern) = FieldPattern::new(
445 InvoiceFieldType::NetAmount,
446 r"(?:Total\s+excl(?:uding)?\.?\s+VAT|Total\s+ex\.?\s+VAT|Subtotal\s+ex\.?\s+VAT)\s*\n?\s*[$£]?\s*([0-9]{1,3}(?:,[0-9]{3})*\.[0-9]{2})",
447 0.85,
448 Some(Language::English),
449 ) {
450 self.add_pattern(pattern.with_hints(vec![
451 "total".to_string(),
452 "excl".to_string(),
453 "vat".to_string(),
454 ]));
455 }
456
457 if let Ok(pattern) = FieldPattern::new(
460 InvoiceFieldType::VatNumber,
461 r"(?:VAT\s+(?:No\.?|Reg\.?|Registration)|V\.A\.T\.\s+No\.?):?\s*([A-Z]{2}[0-9]{9,12}|[0-9]{9,12})",
462 0.9,
463 Some(Language::English),
464 ) {
465 self.add_pattern(
466 pattern.with_hints(vec!["vat".to_string(), "registration".to_string()]),
467 );
468 }
469
470 if let Ok(pattern) = FieldPattern::new(
475 InvoiceFieldType::CustomerName,
476 r"(?:Bill\s+to|BILL\s+TO|Sold\s+to|SOLD\s+TO|Client|CLIENT):[\s:]*([A-Za-z]{3,}\s+[A-Za-z][A-Za-z\s\.,&\-]{2,77})",
477 0.75,
478 Some(Language::English),
479 ) {
480 self.add_pattern(pattern.with_hints(vec![
481 "bill".to_string(),
482 "sold".to_string(),
483 "client".to_string(),
484 ]));
485 }
486
487 if let Ok(pattern) = FieldPattern::new(
490 InvoiceFieldType::Currency,
491 r"(?:Currency:?\s+)?([$£]|USD|GBP|EUR|CHF)",
492 0.7,
493 Some(Language::English),
494 ) {
495 self.add_pattern(pattern);
496 }
497 }
498
499 fn load_german_patterns(&mut self) {
501 if let Ok(pattern) = FieldPattern::new(
504 InvoiceFieldType::InvoiceNumber,
505 r"(?:Rechnungsnummer|Rechnung\s+Nr\.?|Re\.-Nr\.?):?\s*([A-Z0-9][A-Z0-9\-/]*)",
506 0.9,
507 Some(Language::German),
508 ) {
509 self.add_pattern(pattern.with_hints(vec![
510 "rechnung".to_string(),
511 "rechnungsnummer".to_string(),
512 "nummer".to_string(),
513 ]));
514 }
515
516 if let Ok(pattern) = FieldPattern::new(
519 InvoiceFieldType::InvoiceDate,
520 r"(?:(?:Rechnungs)?datum|DATUM):?\s*(\d{1,2}[.\-]\d{1,2}[.\-]\d{2,4})",
521 0.85,
522 Some(Language::German),
523 ) {
524 self.add_pattern(
525 pattern.with_hints(vec!["datum".to_string(), "rechnungsdatum".to_string()]),
526 );
527 }
528
529 if let Ok(pattern) = FieldPattern::new(
532 InvoiceFieldType::DueDate,
533 r"(?:Fälligkeitsdatum|Zahlbar\s+bis|FÄLLIGKEITSDATUM):?\s*(\d{1,2}[.\-]\d{1,2}[.\-]\d{2,4})",
534 0.85,
535 Some(Language::German),
536 ) {
537 self.add_pattern(
538 pattern.with_hints(vec!["fälligkeitsdatum".to_string(), "zahlbar".to_string()]),
539 );
540 }
541
542 if let Ok(pattern) = FieldPattern::new(
545 InvoiceFieldType::TotalAmount,
546 r"(?:Gesamtbetrag|Betrag|Summe|GESAMTBETRAG):?\s*€?\s*([0-9]{1,3}(?:\.[0-9]{3})*,[0-9]{2})\s*€?",
547 0.9,
548 Some(Language::German),
549 ) {
550 self.add_pattern(pattern.with_hints(vec![
551 "gesamtbetrag".to_string(),
552 "betrag".to_string(),
553 "summe".to_string(),
554 ]));
555 }
556
557 if let Ok(pattern) = FieldPattern::new(
560 InvoiceFieldType::TaxAmount,
561 r"(?:MwSt\.?|Umsatzsteuer|USt\.?).*?:?\s*€?\s*([0-9]{1,3}(?:\.[0-9]{3})*,[0-9]{2})\s*€?",
562 0.85,
563 Some(Language::German),
564 ) {
565 self.add_pattern(pattern.with_hints(vec![
566 "mwst".to_string(),
567 "umsatzsteuer".to_string(),
568 "ust".to_string(),
569 ]));
570 }
571
572 if let Ok(pattern) = FieldPattern::new(
575 InvoiceFieldType::NetAmount,
576 r"(?:Nettobetrag|Zwischensumme|NETTOBETRAG):?\s*€?\s*([0-9]{1,3}(?:\.[0-9]{3})*,[0-9]{2})\s*€?",
577 0.85,
578 Some(Language::German),
579 ) {
580 self.add_pattern(
581 pattern.with_hints(vec!["nettobetrag".to_string(), "zwischensumme".to_string()]),
582 );
583 }
584
585 if let Ok(pattern) = FieldPattern::new(
588 InvoiceFieldType::NetAmount,
589 r"(?:Netto|NETTO|Summe\s+Netto|Teilsumme|TEILSUMME):?\s*€?\s*([0-9]{1,3}(?:\.[0-9]{3})*,[0-9]{2})\s*€?",
590 0.80,
591 Some(Language::German),
592 ) {
593 self.add_pattern(
594 pattern.with_hints(vec!["netto".to_string(), "teilsumme".to_string()]),
595 );
596 }
597
598 if let Ok(pattern) = FieldPattern::new(
601 InvoiceFieldType::VatNumber,
602 r"(?:USt-IdNr\.?|Steuernummer):?\s*(DE[0-9]{9}|[0-9]{2,3}/[0-9]{3}/[0-9]{4,5})",
603 0.9,
604 Some(Language::German),
605 ) {
606 self.add_pattern(
607 pattern.with_hints(vec!["ust-idnr".to_string(), "steuernummer".to_string()]),
608 );
609 }
610
611 if let Ok(pattern) = FieldPattern::new(
614 InvoiceFieldType::CustomerName,
615 r"(?:Kunde|KUNDE|Rechnungsempfänger|RECHNUNGSEMPFÄNGER|An|z\.Hd\.|z\.\s*Hd\.):?\s*([A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß\s\.,&\-]{2,80})",
616 0.75,
617 Some(Language::German),
618 ) {
619 self.add_pattern(pattern.with_hints(vec![
620 "kunde".to_string(),
621 "rechnungsempfänger".to_string(),
622 "z.hd".to_string(),
623 ]));
624 }
625
626 if let Ok(pattern) = FieldPattern::new(
629 InvoiceFieldType::Currency,
630 r"(?:Währung:?\s+)?(€|EUR|USD|GBP|CHF)",
631 0.7,
632 Some(Language::German),
633 ) {
634 self.add_pattern(pattern);
635 }
636 }
637
638 fn load_italian_patterns(&mut self) {
640 if let Ok(pattern) = FieldPattern::new(
643 InvoiceFieldType::InvoiceNumber,
644 r"(?:Fattura\s+N\.?|Numero\s+Fattura|N\.\s+Fatt\.?|FATTURA\s+N\.?):?\s*([A-Z0-9][A-Z0-9\-/]*)",
645 0.9,
646 Some(Language::Italian),
647 ) {
648 self.add_pattern(pattern.with_hints(vec!["fattura".to_string(), "numero".to_string()]));
649 }
650
651 if let Ok(pattern) = FieldPattern::new(
654 InvoiceFieldType::InvoiceDate,
655 r"(?:(?:Data\s+)?Fattura|Data|DATA):?\s*(\d{1,2}[-/]\d{1,2}[-/]\d{2,4})",
656 0.85,
657 Some(Language::Italian),
658 ) {
659 self.add_pattern(pattern.with_hints(vec!["data".to_string(), "fattura".to_string()]));
660 }
661
662 if let Ok(pattern) = FieldPattern::new(
665 InvoiceFieldType::DueDate,
666 r"(?:(?:Data\s+)?Scadenza|SCADENZA):?\s*(\d{1,2}[-/]\d{1,2}[-/]\d{2,4})",
667 0.85,
668 Some(Language::Italian),
669 ) {
670 self.add_pattern(pattern.with_hints(vec!["scadenza".to_string()]));
671 }
672
673 if let Ok(pattern) = FieldPattern::new(
676 InvoiceFieldType::TotalAmount,
677 r"(?:Totale(?:\s+Fattura)?|Importo\s+Totale|TOTALE):?\s*€?\s*([0-9]{1,3}(?:\.[0-9]{3})*,[0-9]{2})\s*€?",
678 0.9,
679 Some(Language::Italian),
680 ) {
681 self.add_pattern(pattern.with_hints(vec!["totale".to_string(), "importo".to_string()]));
682 }
683
684 if let Ok(pattern) = FieldPattern::new(
687 InvoiceFieldType::TaxAmount,
688 r"(?:IVA|I\.V\.A\.|Imposta).*?:?\s*€?\s*([0-9]{1,3}(?:\.[0-9]{3})*,[0-9]{2})\s*€?",
689 0.85,
690 Some(Language::Italian),
691 ) {
692 self.add_pattern(pattern.with_hints(vec!["iva".to_string(), "imposta".to_string()]));
693 }
694
695 if let Ok(pattern) = FieldPattern::new(
698 InvoiceFieldType::NetAmount,
699 r"(?:Imponibile|Subtotale|IMPONIBILE):?\s*€?\s*([0-9]{1,3}(?:\.[0-9]{3})*,[0-9]{2})\s*€?",
700 0.85,
701 Some(Language::Italian),
702 ) {
703 self.add_pattern(
704 pattern.with_hints(vec!["imponibile".to_string(), "subtotale".to_string()]),
705 );
706 }
707
708 if let Ok(pattern) = FieldPattern::new(
711 InvoiceFieldType::NetAmount,
712 r"(?:Netto|NETTO|Somma\s+Netta|Importo\s+Netto):?\s*€?\s*([0-9]{1,3}(?:\.[0-9]{3})*,[0-9]{2})\s*€?",
713 0.80,
714 Some(Language::Italian),
715 ) {
716 self.add_pattern(pattern.with_hints(vec!["netto".to_string(), "somma".to_string()]));
717 }
718
719 if let Ok(pattern) = FieldPattern::new(
722 InvoiceFieldType::VatNumber,
723 r"(?:P\.IVA|P\.\s*IVA|Partita\s+IVA):?\s*(IT[0-9]{11}|[0-9]{11})",
724 0.9,
725 Some(Language::Italian),
726 ) {
727 self.add_pattern(pattern.with_hints(vec!["p.iva".to_string(), "partita".to_string()]));
728 }
729
730 if let Ok(pattern) = FieldPattern::new(
733 InvoiceFieldType::CustomerName,
734 r"(?:Cliente|CLIENTE|Fatturare\s+a|FATTURARE\s+A|Spett\.le|Spettabile|SPETTABILE|Destinatario|DESTINATARIO):?\s*([A-Za-zÀ-ÿ][A-Za-zÀ-ÿ\s\.,&\-]{2,80})",
735 0.75,
736 Some(Language::Italian),
737 ) {
738 self.add_pattern(pattern.with_hints(vec![
739 "cliente".to_string(),
740 "fatturare".to_string(),
741 "spett".to_string(),
742 "destinatario".to_string(),
743 ]));
744 }
745
746 if let Ok(pattern) = FieldPattern::new(
749 InvoiceFieldType::Currency,
750 r"(?:Valuta:?\s+)?(€|EUR|USD|GBP|CHF)",
751 0.7,
752 Some(Language::Italian),
753 ) {
754 self.add_pattern(pattern);
755 }
756 }
757}
758
759impl Default for PatternLibrary {
760 fn default() -> Self {
761 Self::new()
762 }
763}
764
765#[cfg(test)]
766mod tests {
767 use super::*;
768
769 #[test]
770 fn test_pattern_library_new() {
771 let lib = PatternLibrary::new();
772 assert_eq!(lib.patterns.len(), 0);
773 }
774
775 #[test]
776 fn test_field_pattern_creation() {
777 let pattern = FieldPattern::new(InvoiceFieldType::InvoiceNumber, r"INV-(\d+)", 0.9, None);
778 assert!(pattern.is_ok());
779 }
780
781 #[test]
782 fn test_field_pattern_invalid_regex() {
783 let pattern = FieldPattern::new(InvoiceFieldType::InvoiceNumber, r"[invalid(", 0.9, None);
784 assert!(pattern.is_err());
785 }
786
787 #[test]
788 fn test_pattern_matches() {
789 let pattern = FieldPattern::new(InvoiceFieldType::InvoiceNumber, r"INV-(\d+)", 0.9, None)
790 .expect("Hardcoded regex pattern should be valid");
791
792 assert_eq!(pattern.matches("INV-12345"), Some("12345".to_string()));
793 assert_eq!(pattern.matches("Invoice INV-999"), Some("999".to_string()));
794 assert_eq!(pattern.matches("No match here"), None);
795 }
796}