validator_rs/
currency.rs

1//! Currency validation functions
2//!
3//! This module provides validation for currency strings with extensive
4//! customization options for different currency formats worldwide.
5
6use regex::Regex;
7
8/// Options for currency validation
9///
10/// This struct provides configuration for validating currency strings
11/// with support for various formats, symbols, and regional conventions.
12#[derive(Debug, Clone)]
13pub struct CurrencyOptions {
14    /// Currency symbol (e.g., "$", "€", "¥")
15    pub symbol: String,
16    /// Require the currency symbol to be present
17    pub require_symbol: bool,
18    /// Allow space after the symbol
19    pub allow_space_after_symbol: bool,
20    /// Symbol appears after the digits instead of before
21    pub symbol_after_digits: bool,
22    /// Allow negative values
23    pub allow_negatives: bool,
24    /// Use parentheses for negative values instead of minus sign
25    pub parens_for_negatives: bool,
26    /// Negative sign appears before the digits
27    pub negative_sign_before_digits: bool,
28    /// Negative sign appears after the digits
29    pub negative_sign_after_digits: bool,
30    /// Allow space placeholder for negative sign (e.g., "R 123" and "R-123")
31    pub allow_negative_sign_placeholder: bool,
32    /// Thousands separator character
33    pub thousands_separator: char,
34    /// Decimal separator character
35    pub decimal_separator: char,
36    /// Allow decimal portion
37    pub allow_decimal: bool,
38    /// Require decimal portion
39    pub require_decimal: bool,
40    /// Allowed number of digits after decimal (e.g., vec![2] for exactly 2 digits)
41    pub digits_after_decimal: Vec<usize>,
42    /// Allow space after digits
43    pub allow_space_after_digits: bool,
44}
45
46impl Default for CurrencyOptions {
47    fn default() -> Self {
48        Self {
49            symbol: "$".to_string(),
50            require_symbol: false,
51            allow_space_after_symbol: false,
52            symbol_after_digits: false,
53            allow_negatives: true,
54            parens_for_negatives: false,
55            negative_sign_before_digits: false,
56            negative_sign_after_digits: false,
57            allow_negative_sign_placeholder: false,
58            thousands_separator: ',',
59            decimal_separator: '.',
60            allow_decimal: true,
61            require_decimal: false,
62            digits_after_decimal: vec![2],
63            allow_space_after_digits: false,
64        }
65    }
66}
67
68impl CurrencyOptions {
69    /// Create a new CurrencyOptions with default values
70    pub fn new() -> Self {
71        Self::default()
72    }
73
74    /// Set the currency symbol
75    pub fn symbol(mut self, symbol: impl Into<String>) -> Self {
76        self.symbol = symbol.into();
77        self
78    }
79
80    /// Set whether symbol is required
81    pub fn require_symbol(mut self, require: bool) -> Self {
82        self.require_symbol = require;
83        self
84    }
85
86    /// Set whether to allow space after symbol
87    pub fn allow_space_after_symbol(mut self, allow: bool) -> Self {
88        self.allow_space_after_symbol = allow;
89        self
90    }
91
92    /// Set whether symbol appears after digits
93    pub fn symbol_after_digits(mut self, after: bool) -> Self {
94        self.symbol_after_digits = after;
95        self
96    }
97
98    /// Set whether negatives are allowed
99    pub fn allow_negatives(mut self, allow: bool) -> Self {
100        self.allow_negatives = allow;
101        self
102    }
103
104    /// Set whether to use parentheses for negatives
105    pub fn parens_for_negatives(mut self, use_parens: bool) -> Self {
106        self.parens_for_negatives = use_parens;
107        self
108    }
109
110    /// Set thousands separator character
111    pub fn thousands_separator(mut self, sep: char) -> Self {
112        self.thousands_separator = sep;
113        self
114    }
115
116    /// Set decimal separator character
117    pub fn decimal_separator(mut self, sep: char) -> Self {
118        self.decimal_separator = sep;
119        self
120    }
121
122    /// Set whether decimals are allowed
123    pub fn allow_decimal(mut self, allow: bool) -> Self {
124        self.allow_decimal = allow;
125        self
126    }
127
128    /// Set whether decimal is required
129    pub fn require_decimal(mut self, require: bool) -> Self {
130        self.require_decimal = require;
131        self
132    }
133
134    /// Set allowed digits after decimal
135    pub fn digits_after_decimal(mut self, digits: Vec<usize>) -> Self {
136        self.digits_after_decimal = digits;
137        self
138    }
139
140    /// Set whether negative sign appears before digits
141    pub fn negative_sign_before_digits(mut self, before: bool) -> Self {
142        self.negative_sign_before_digits = before;
143        self
144    }
145
146    /// Set whether negative sign appears after digits
147    pub fn negative_sign_after_digits(mut self, after: bool) -> Self {
148        self.negative_sign_after_digits = after;
149        self
150    }
151
152    /// Set whether to allow negative sign placeholder
153    pub fn allow_negative_sign_placeholder(mut self, allow: bool) -> Self {
154        self.allow_negative_sign_placeholder = allow;
155        self
156    }
157
158    /// Set whether to allow space after digits
159    pub fn allow_space_after_digits(mut self, allow: bool) -> Self {
160        self.allow_space_after_digits = allow;
161        self
162    }
163}
164
165/// Build a regex pattern for currency validation based on options
166/// Note: Rust regex doesn't support lookahead/lookbehind, so we validate differently
167fn build_currency_regex(options: &CurrencyOptions) -> Result<Regex, regex::Error> {
168    // Build decimal digits pattern
169    let mut decimal_digits = format!(r"\d{{{}}}", options.digits_after_decimal[0]);
170    for digit in options.digits_after_decimal.iter().skip(1) {
171        decimal_digits.push_str(&format!(r"|\d{{{}}}", digit));
172    }
173
174    // Escape non-word characters in symbol (matching JS /\W/ regex)
175    // Use regex::escape which handles all special regex characters
176    let escaped_symbol = regex::escape(&options.symbol);
177    
178    let symbol = format!(
179        "({}){}", 
180        escaped_symbol,
181        if options.require_symbol { "" } else { "?" }
182    );
183
184    let negative = r"-?";
185    let whole_dollar_amount_without_sep = r"[1-9]\d*";
186    
187    // Escape thousands separator
188    let escaped_thousands_sep = if options.thousands_separator.is_alphanumeric() || options.thousands_separator == '_' {
189        options.thousands_separator.to_string()
190    } else {
191        format!(r"\{}", options.thousands_separator)
192    };
193    
194    let whole_dollar_amount_with_sep = format!(
195        r"[1-9]\d{{0,2}}({}\d{{3}})*",
196        escaped_thousands_sep
197    );
198    
199    let valid_whole_dollar_amounts = vec![
200        "0",
201        whole_dollar_amount_without_sep,
202        &whole_dollar_amount_with_sep,
203    ];
204    
205    let whole_dollar_amount = format!("({})?", valid_whole_dollar_amounts.join("|"));
206    
207    // Escape decimal separator
208    let escaped_decimal_sep = if options.decimal_separator.is_alphanumeric() || options.decimal_separator == '_' {
209        options.decimal_separator.to_string()
210    } else {
211        format!(r"\{}", options.decimal_separator)
212    };
213    
214    let decimal_amount = format!(
215        r"({}({})){}",
216        escaped_decimal_sep,
217        decimal_digits,
218        if options.require_decimal { "" } else { "?" }
219    );
220
221    let mut pattern = whole_dollar_amount.clone();
222    if options.allow_decimal || options.require_decimal {
223        pattern.push_str(&decimal_amount);
224    }
225
226    // Handle negative sign placement (default is negative sign before symbol)
227    if options.allow_negatives && !options.parens_for_negatives {
228        if options.negative_sign_after_digits {
229            pattern.push_str(negative);
230        } else if options.negative_sign_before_digits {
231            pattern = format!("{}{}", negative, pattern);
232        }
233    }
234
235    // Handle spacing - simplified without lookahead
236    if options.allow_negative_sign_placeholder {
237        // South African Rand: allows "R 123" or "R-123"
238        pattern = format!(r"( ?-?)?{}", pattern);
239    } else if options.allow_space_after_symbol {
240        pattern = format!(r" ?{}", pattern);
241    } else if options.allow_space_after_digits {
242        pattern.push_str(r" ?");
243    }
244
245    // Add symbol before or after
246    if options.symbol_after_digits {
247        pattern.push_str(&symbol);
248    } else {
249        pattern = format!("{}{}", symbol, pattern);
250    }
251
252    // Handle parentheses for negatives or default negative placement
253    if options.allow_negatives {
254        if options.parens_for_negatives {
255            // Pattern: ( \( PATTERN \) | PATTERN )
256            pattern = format!(r"(\({}\)|{})", pattern, pattern);
257        } else if !options.negative_sign_before_digits && !options.negative_sign_after_digits {
258            pattern = format!("{}{}", negative, pattern);
259        }
260    }
261
262    // Build final pattern - simplified without lookahead
263    let final_pattern = format!(r"^{}$", pattern);
264    
265    Regex::new(&final_pattern)
266}
267
268/// Additional validation without using lookahead (manual checks)
269fn validate_currency_manual(value: &str, options: &CurrencyOptions) -> bool {
270    // Empty string is invalid
271    if value.is_empty() {
272        return false;
273    }
274    
275    // Don't allow leading or trailing whitespace
276    if value.starts_with(' ') || value.ends_with(' ') {
277        return false;
278    }
279    
280    // Check for "- " pattern (negative sign followed by space)
281    if value.starts_with("- ") {
282        return false;
283    }
284    
285    // Must contain at least one digit
286    if !value.chars().any(|c| c.is_ascii_digit()) {
287        return false;
288    }
289    
290    // Check for invalid patterns with spaces
291    // "$ " (symbol followed by space when not allowed)
292    if !options.allow_space_after_symbol && !options.allow_negative_sign_placeholder {
293        if value.contains(&format!("{} ", options.symbol)) {
294            return false;
295        }
296    }
297    
298    // Check for "SYMBOL -" pattern (space between symbol and negative)
299    // This is invalid with allow_negative_sign_placeholder but valid with allow_space_after_symbol
300    if options.allow_negative_sign_placeholder && !options.allow_space_after_symbol {
301        if value.contains(&format!("{} -", options.symbol)) {
302            return false;
303        }
304    }
305    
306    // Check specific invalid patterns
307    // Don't allow digit followed by space followed by end of string
308    // unless allow_space_after_digits is true
309    if !options.allow_space_after_digits && !options.allow_negative_sign_placeholder {
310        let trimmed = value.trim_end_matches(&options.symbol);
311        let trimmed = trimmed.trim_end_matches(')');
312        if trimmed.ends_with(' ') {
313            return false;
314        }
315    }
316    
317    true
318}
319
320/// Validates if a string is a valid currency format
321///
322/// # Examples
323///
324/// ```
325/// use validator_rs::currency::{is_currency, CurrencyOptions};
326///
327/// // Default options (US format)
328/// assert!(is_currency("$10,123.45", None));
329/// assert!(is_currency("10123.45", None));
330/// assert!(!is_currency("$ 32.50", None)); // no space after symbol
331///
332/// // Custom options
333/// let euro_options = CurrencyOptions::new()
334///     .symbol("€")
335///     .thousands_separator('.')
336///     .decimal_separator(',');
337/// assert!(is_currency("€1.234,56", Some(euro_options)));
338/// ```
339pub fn is_currency(value: &str, options: Option<CurrencyOptions>) -> bool {
340    let opts = options.unwrap_or_default();
341    
342    // Manual validation first (replaces lookahead assertions)
343    if !validate_currency_manual(value, &opts) {
344        return false;
345    }
346    
347    match build_currency_regex(&opts) {
348        Ok(regex) => regex.is_match(value),
349        Err(_) => false,
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    // Test 1: Default format -$##,###.## (en-US, en-CA, en-AU, en-NZ, en-HK)
358    #[test]
359    fn test_default_currency() {
360        let valid = vec![
361            "-$10,123.45",
362            "$10,123.45",
363            "$10123.45",
364            "10,123.45",
365            "10123.45",
366            "10,123",
367            "1,123,456",
368            "1123456",
369            "1.39",
370            ".03",
371            "0.10",
372            "$0.10",
373            "-$0.01",
374            "-$.99",
375            "$100,234,567.89",
376            "$10,123",
377            "10,123",
378            "-10123",
379        ];
380
381        let invalid = vec![
382            "1.234",
383            "$1.1",
384            "$ 32.50",
385            "500$",
386            ".0001",
387            "$.001",
388            "$0.001",
389            "12,34.56",
390            "123456,123,123456",
391            "123,4",
392            ",123",
393            "$-,123",
394            "$",
395            ".",
396            ",",
397            "00",
398            "$-",
399            "$-,.",
400            "-",
401            "-$",
402            "",
403            "- $",
404        ];
405
406        for val in valid {
407            assert!(is_currency(val, None), "Expected '{}' to be valid", val);
408        }
409
410        for val in invalid {
411            assert!(!is_currency(val, None), "Expected '{}' to be invalid", val);
412        }
413    }
414
415    // Test 2: No decimal allowed
416    #[test]
417    fn test_no_decimal() {
418        let options = CurrencyOptions::new().allow_decimal(false);
419
420        let valid = vec![
421            "-$10,123",
422            "$10,123",
423            "$10123",
424            "10,123",
425            "10123",
426            "1,123,456",
427            "1123456",
428            "1",
429            "0",
430            "$0",
431            "-$0",
432            "$100,234,567",
433            "$10,123",
434            "10,123",
435            "-10123",
436        ];
437
438        let invalid = vec![
439            "-$10,123.45",
440            "$10,123.45",
441            "$10123.45",
442            "10,123.45",
443            "10123.45",
444            "1.39",
445            ".03",
446            "0.10",
447            "$0.10",
448            "-$0.01",
449            "-$.99",
450            "$100,234,567.89",
451            "1.234",
452            "$1.1",
453            "$ 32.50",
454            ".0001",
455            "$.001",
456            "$0.001",
457            "12,34.56",
458            "123,4",
459            ",123",
460            "$-,123",
461            "$",
462            ".",
463            ",",
464            "00",
465            "$-",
466            "$-,.",
467            "-",
468            "-$",
469            "",
470            "- $",
471        ];
472
473        for val in valid {
474            assert!(is_currency(val, Some(options.clone())), "Expected '{}' to be valid", val);
475        }
476
477        for val in invalid {
478            assert!(!is_currency(val, Some(options.clone())), "Expected '{}' to be invalid", val);
479        }
480    }
481
482    // Test 3: Require decimal
483    #[test]
484    fn test_require_decimal() {
485        let options = CurrencyOptions::new().require_decimal(true);
486
487        let valid = vec![
488            "-$10,123.45",
489            "$10,123.45",
490            "$10123.45",
491            "10,123.45",
492            "10123.45",
493            "10,123.00",
494            "1.39",
495            ".03",
496            "0.10",
497            "$0.10",
498            "-$0.01",
499            "-$.99",
500            "$100,234,567.89",
501        ];
502
503        let invalid = vec![
504            "$10,123",
505            "10,123",
506            "-10123",
507            "1,123,456",
508            "1123456",
509            "1.234",
510            "$1.1",
511            "$ 32.50",
512            "500$",
513            ".0001",
514            "$.001",
515            "$0.001",
516            "12,34.56",
517            "123456,123,123456",
518            "123,4",
519            ",123",
520            "$-,123",
521            "$",
522            ".",
523            ",",
524            "00",
525            "$-",
526            "$-,.",
527            "-",
528            "-$",
529            "",
530            "- $",
531        ];
532
533        for val in valid {
534            assert!(is_currency(val, Some(options.clone())), "Expected '{}' to be valid", val);
535        }
536
537        for val in invalid {
538            assert!(!is_currency(val, Some(options.clone())), "Expected '{}' to be invalid", val);
539        }
540    }
541
542    // Test 4: Custom digits after decimal
543    #[test]
544    fn test_digits_after_decimal() {
545        let options = CurrencyOptions::new()
546            .digits_after_decimal(vec![1, 3]);
547
548        let valid = vec![
549            "-$10,123.4",
550            "$10,123.454",
551            "$10123.452",
552            "10,123.453",
553            "10123.450",
554            "10,123",
555            "1,123,456",
556            "1123456",
557            "1.3",
558            ".030",
559            "0.100",
560            "$0.1",
561            "-$0.0",
562            "-$.9",
563            "$100,234,567.893",
564            "$10,123",
565            "10,123.123",
566            "-10123.1",
567        ];
568
569        let invalid = vec![
570            "1.23",
571            "$1.13322",
572            "$ 32.50",
573            "500$",
574            ".0001",
575            "$.01",
576            "$0.01",
577            "12,34.56",
578            "123456,123,123456",
579            "123,4",
580            ",123",
581            "$-,123",
582            "$",
583            ".",
584            ",",
585            "00",
586            "$-",
587            "$-,.",
588            "-",
589            "-$",
590            "",
591            "- $",
592        ];
593
594        for val in valid {
595            assert!(is_currency(val, Some(options.clone())), "Expected '{}' to be valid", val);
596        }
597
598        for val in invalid {
599            assert!(!is_currency(val, Some(options.clone())), "Expected '{}' to be invalid", val);
600        }
601    }
602
603    // Test 5: Require symbol
604    #[test]
605    fn test_require_symbol() {
606        let options = CurrencyOptions::new().require_symbol(true);
607
608        let valid = vec![
609            "-$10,123.45",
610            "$10,123.45",
611            "$10123.45",
612            "$10,123.45",
613            "$10,123",
614            "$1,123,456",
615            "$1123456",
616            "$1.39",
617            "$.03",
618            "$0.10",
619            "$0.10",
620            "-$0.01",
621            "-$.99",
622            "$100,234,567.89",
623            "$10,123",
624            "-$10123",
625        ];
626
627        let invalid = vec![
628            "1.234",
629            "$1.234",
630            "1.1",
631            "$1.1",
632            "$ 32.50",
633            " 32.50",
634            "500",
635            "10,123,456",
636            ".0001",
637            "$.001",
638            "$0.001",
639            "1,234.56",
640            "123456,123,123456",
641            "$123456,123,123456",
642            "123.4",
643            "$123.4",
644            ",123",
645            "$,123",
646            "$-,123",
647            "$",
648            ".",
649            "$.",
650            ",",
651            "$,",
652            "00",
653            "$00",
654            "$-",
655            "$-,.",
656            "-",
657            "-$",
658            "",
659            "$ ",
660            "- $",
661        ];
662
663        for val in valid {
664            assert!(is_currency(val, Some(options.clone())), "Expected '{}' to be valid", val);
665        }
666
667        for val in invalid {
668            assert!(!is_currency(val, Some(options.clone())), "Expected '{}' to be invalid", val);
669        }
670    }
671
672    // Test 6: Chinese Yuan format with negative_sign_before_digits
673    #[test]
674    fn test_yuan_format() {
675        let mut options = CurrencyOptions::new();
676        options.symbol = "¥".to_string();
677        options.negative_sign_before_digits = true;
678
679        let valid = vec![
680            "123,456.78",
681            "-123,456.78",
682            "¥6,954,231",
683            "¥-6,954,231",
684            "¥10.03",
685            "¥-10.03",
686            "10.03",
687            "1.39",
688            ".03",
689            "0.10",
690            "¥-10567.01",
691            "¥0.01",
692            "¥1,234,567.89",
693            "¥10,123",
694            "¥-10,123",
695            "¥-10,123.45",
696            "10,123",
697            "10123",
698            "¥-100",
699        ];
700
701        let invalid = vec![
702            "1.234",
703            "¥1.1",
704            "5,00",
705            ".0001",
706            "¥.001",
707            "¥0.001",
708            "12,34.56",
709            "123456,123,123456",
710            "123 456",
711            ",123",
712            "¥-,123",
713            "",
714            " ",
715            "¥",
716            "¥-",
717            "¥-,.",
718            "-",
719            "- ¥",
720            "-¥",
721        ];
722
723        for val in valid {
724            assert!(is_currency(val, Some(options.clone())), "Expected '{}' to be valid", val);
725        }
726
727        for val in invalid {
728            assert!(!is_currency(val, Some(options.clone())), "Expected '{}' to be invalid", val);
729        }
730    }
731
732    // Test 7: Negative sign after digits
733    #[test]
734    fn test_negative_sign_after_digits() {
735        let mut options = CurrencyOptions::new();
736        options.negative_sign_after_digits = true;
737
738        let valid = vec![
739            "$10,123.45-",
740            "$10,123.45",
741            "$10123.45",
742            "10,123.45",
743            "10123.45",
744            "10,123",
745            "1,123,456",
746            "1123456",
747            "1.39",
748            ".03",
749            "0.10",
750            "$0.10",
751            "$0.01-",
752            "$.99-",
753            "$100,234,567.89",
754            "$10,123",
755            "10,123",
756            "10123-",
757        ];
758
759        let invalid = vec![
760            "-123",
761            "1.234",
762            "$1.1",
763            "$ 32.50",
764            "500$",
765            ".0001",
766            "$.001",
767            "$0.001",
768            "12,34.56",
769            "123456,123,123456",
770            "123,4",
771            ",123",
772            "$-,123",
773            "$",
774            ".",
775            ",",
776            "00",
777            "$-",
778            "$-,.",
779            "-",
780            "-$",
781            "",
782            "- $",
783        ];
784
785        for val in valid {
786            assert!(is_currency(val, Some(options.clone())), "Expected '{}' to be valid", val);
787        }
788
789        for val in invalid {
790            assert!(!is_currency(val, Some(options.clone())), "Expected '{}' to be invalid", val);
791        }
792    }
793
794    // Test 8: No negatives allowed (Chinese Yuan)
795    #[test]
796    fn test_no_negatives_yuan() {
797        let mut options = CurrencyOptions::new();
798        options.symbol = "¥".to_string();
799        options.allow_negatives = false;
800
801        let valid = vec![
802            "123,456.78",
803            "¥6,954,231",
804            "¥10.03",
805            "10.03",
806            "1.39",
807            ".03",
808            "0.10",
809            "¥0.01",
810            "¥1,234,567.89",
811            "¥10,123",
812            "10,123",
813            "10123",
814            "¥100",
815        ];
816
817        let invalid = vec![
818            "1.234",
819            "-123,456.78",
820            "¥-6,954,231",
821            "¥-10.03",
822            "¥-10567.01",
823            "¥1.1",
824            "¥-10,123",
825            "¥-10,123.45",
826            "5,00",
827            "¥-100",
828            ".0001",
829            "¥.001",
830            "¥-.001",
831            "¥0.001",
832            "12,34.56",
833            "123456,123,123456",
834            "123 456",
835            ",123",
836            "¥-,123",
837            "",
838            " ",
839            "¥",
840            "¥-",
841            "¥-,.",
842            "-",
843            "- ¥",
844            "-¥",
845        ];
846
847        for val in valid {
848            assert!(is_currency(val, Some(options.clone())), "Expected '{}' to be valid", val);
849        }
850
851        for val in invalid {
852            assert!(!is_currency(val, Some(options.clone())), "Expected '{}' to be invalid", val);
853        }
854    }
855
856    // Test 9: South African Rand format with negative_sign_placeholder
857    #[test]
858    fn test_south_african_rand() {
859        let mut options = CurrencyOptions::new();
860        options.symbol = "R".to_string();
861        options.negative_sign_before_digits = true;
862        options.thousands_separator = ' ';
863        options.decimal_separator = ',';
864        options.allow_negative_sign_placeholder = true;
865
866        let valid = vec![
867            "123 456,78",
868            "-10 123",
869            "R-10 123",
870            "R 6 954 231",
871            "R10,03",
872            "10,03",
873            "1,39",
874            ",03",
875            "0,10",
876            "R10567,01",
877            "R0,01",
878            "R1 234 567,89",
879            "R10 123",
880            "R 10 123",
881            "R 10123",
882            "R-10123",
883            "10 123",
884            "10123",
885        ];
886
887        let invalid = vec![
888            "1,234",
889            "R -10123",
890            "R- 10123",
891            "R,1",
892            ",0001",
893            "R,001",
894            "R0,001",
895            "12 34,56",
896            "123456 123 123456",
897            " 123",
898            "- 123",
899            "123 ",
900            "",
901            " ",
902            "R",
903            "R- .1",
904            "R-",
905            "-",
906            "-R 10123",
907            "R00",
908            "R -",
909            "-R",
910        ];
911
912        for val in valid {
913            assert!(is_currency(val, Some(options.clone())), "Expected '{}' to be valid", val);
914        }
915
916        for val in invalid {
917            assert!(!is_currency(val, Some(options.clone())), "Expected '{}' to be invalid", val);
918        }
919    }
920
921    // Test 10: Italian Euro format with space after symbol
922    #[test]
923    fn test_euro_italian() {
924        let mut options = CurrencyOptions::new();
925        options.symbol = "€".to_string();
926        options.thousands_separator = '.';
927        options.decimal_separator = ',';
928        options.allow_space_after_symbol = true;
929
930        let valid = vec![
931            "123.456,78",
932            "-123.456,78",
933            "€6.954.231",
934            "-€6.954.231",
935            "€ 896.954.231",
936            "-€ 896.954.231",
937            "16.954.231",
938            "-16.954.231",
939            "€10,03",
940            "-€10,03",
941            "10,03",
942            "-10,03",
943            "-1,39",
944            ",03",
945            "0,10",
946            "-€10567,01",
947            "-€ 10567,01",
948            "€ 0,01",
949            "€1.234.567,89",
950            "€10.123",
951            "10.123",
952            "-€10.123",
953            "€ 10.123",
954            "€10.123",
955            "€ 10123",
956            "10.123",
957            "-10123",
958        ];
959
960        let invalid = vec![
961            "1,234",
962            "€ 1,1",
963            "50#,50",
964            "123,@€ ",
965            "€€500",
966            ",0001",
967            "€ ,001",
968            "€0,001",
969            "12.34,56",
970            "123456.123.123456",
971            "€123€",
972            "",
973            " ",
974            "€",
975            " €",
976            "€ ",
977            "€€",
978            " 123",
979            "- 123",
980            ".123",
981            "-€.123",
982            "123 ",
983            "€-",
984            "- €",
985            "€ - ",
986            "-",
987            "- ",
988            "-€",
989        ];
990
991        for val in valid {
992            assert!(is_currency(val, Some(options.clone())), "Expected '{}' to be valid", val);
993        }
994
995        for val in invalid {
996            assert!(!is_currency(val, Some(options.clone())), "Expected '{}' to be invalid", val);
997        }
998    }
999
1000    // Test 11: Greek Euro format with symbol after digits
1001    #[test]
1002    fn test_euro_greek() {
1003        let mut options = CurrencyOptions::new();
1004        options.symbol = "€".to_string();
1005        options.thousands_separator = '.';
1006        options.symbol_after_digits = true;
1007        options.decimal_separator = ',';
1008        options.allow_space_after_digits = true;
1009
1010        let valid = vec![
1011            "123.456,78",
1012            "-123.456,78",
1013            "6.954.231 €",
1014            "-6.954.231 €",
1015            "896.954.231",
1016            "-896.954.231",
1017            "16.954.231",
1018            "-16.954.231",
1019            "10,03€",
1020            "-10,03€",
1021            "10,03",
1022            "-10,03",
1023            "1,39",
1024            ",03",
1025            "-,03",
1026            "-,03 €",
1027            "-,03€",
1028            "0,10",
1029            "10567,01€",
1030            "0,01 €",
1031            "1.234.567,89€",
1032            "10.123€",
1033            "10.123",
1034            "10.123€",
1035            "10.123 €",
1036            "10123 €",
1037            "10.123",
1038            "10123",
1039        ];
1040
1041        let invalid = vec![
1042            "1,234",
1043            "1,1 €",
1044            ",0001",
1045            ",001 €",
1046            "0,001€",
1047            "12.34,56",
1048            "123456.123.123456",
1049            "€123€",
1050            "",
1051            " ",
1052            "€",
1053            " €",
1054            "€ ",
1055            " 123",
1056            "- 123",
1057            ".123",
1058            "-.123€",
1059            "-.123 €",
1060            "123 ",
1061            "-€",
1062            "- €",
1063            "-",
1064            "- ",
1065        ];
1066
1067        for val in valid {
1068            assert!(is_currency(val, Some(options.clone())), "Expected '{}' to be valid", val);
1069        }
1070
1071        for val in invalid {
1072            assert!(!is_currency(val, Some(options.clone())), "Expected '{}' to be invalid", val);
1073        }
1074    }
1075
1076    // Test 12: Danish Krone with space after symbol
1077    #[test]
1078    fn test_danish_krone() {
1079        let mut options = CurrencyOptions::new();
1080        options.symbol = "kr.".to_string();
1081        options.negative_sign_before_digits = true;
1082        options.thousands_separator = '.';
1083        options.decimal_separator = ',';
1084        options.allow_space_after_symbol = true;
1085
1086        let valid = vec![
1087            "123.456,78",
1088            "-10.123",
1089            "kr. -10.123",
1090            "kr.-10.123",
1091            "kr. 6.954.231",
1092            "kr.10,03",
1093            "kr. -10,03",
1094            "10,03",
1095            "1,39",
1096            ",03",
1097            "0,10",
1098            "kr. 10567,01",
1099            "kr. 0,01",
1100            "kr. 1.234.567,89",
1101            "kr. -1.234.567,89",
1102            "10.123",
1103            "kr. 10.123",
1104            "kr.10.123",
1105            "10123",
1106            "10.123",
1107            "kr.-10123",
1108        ];
1109
1110        let invalid = vec![
1111            "1,234",
1112            "kr.  -10123",
1113            "kr.,1",
1114            ",0001",
1115            "kr. ,001",
1116            "kr.0,001",
1117            "12.34,56",
1118            "123456.123.123456",
1119            ".123",
1120            "kr.-.123",
1121            "kr. -.123",
1122            "- 123",
1123            "123 ",
1124            "",
1125            " ",
1126            "kr.",
1127            " kr.",
1128            "kr. ",
1129            "kr.-",
1130            "kr. -",
1131            "kr. - ",
1132            " - ",
1133            "-",
1134            "- kr.",
1135            "-kr.",
1136        ];
1137
1138        for val in valid {
1139            assert!(is_currency(val, Some(options.clone())), "Expected '{}' to be valid", val);
1140        }
1141
1142        for val in invalid {
1143            assert!(!is_currency(val, Some(options.clone())), "Expected '{}' to be invalid", val);
1144        }
1145    }
1146
1147    // Test 13: Danish Krone with no negatives
1148    #[test]
1149    fn test_danish_krone_no_negatives() {
1150        let mut options = CurrencyOptions::new();
1151        options.symbol = "kr.".to_string();
1152        options.allow_negatives = false;
1153        options.negative_sign_before_digits = true;
1154        options.thousands_separator = '.';
1155        options.decimal_separator = ',';
1156        options.allow_space_after_symbol = true;
1157
1158        let valid = vec![
1159            "123.456,78",
1160            "10.123",
1161            "kr. 10.123",
1162            "kr.10.123",
1163            "kr. 6.954.231",
1164            "kr.10,03",
1165            "kr. 10,03",
1166            "10,03",
1167            "1,39",
1168            ",03",
1169            "0,10",
1170            "kr. 10567,01",
1171            "kr. 0,01",
1172            "kr. 1.234.567,89",
1173            "kr.1.234.567,89",
1174            "10.123",
1175            "kr. 10.123",
1176            "kr.10.123",
1177            "10123",
1178            "10.123",
1179            "kr.10123",
1180        ];
1181
1182        let invalid = vec![
1183            "1,234",
1184            "-10.123",
1185            "kr. -10.123",
1186            "kr. -1.234.567,89",
1187            "kr.-10123",
1188            "kr.  -10123",
1189            "kr.-10.123",
1190            "kr. -10,03",
1191            "kr.,1",
1192            ",0001",
1193            "kr. ,001",
1194            "kr.0,001",
1195            "12.34,56",
1196            "123456.123.123456",
1197            ".123",
1198            "kr.-.123",
1199            "kr. -.123",
1200            "- 123",
1201            "123 ",
1202            "",
1203            " ",
1204            "kr.",
1205            " kr.",
1206            "kr. ",
1207            "kr.-",
1208            "kr. -",
1209            "kr. - ",
1210            " - ",
1211            "-",
1212            "- kr.",
1213            "-kr.",
1214        ];
1215
1216        for val in valid {
1217            assert!(is_currency(val, Some(options.clone())), "Expected '{}' to be valid", val);
1218        }
1219
1220        for val in invalid {
1221            assert!(!is_currency(val, Some(options.clone())), "Expected '{}' to be invalid", val);
1222        }
1223    }
1224
1225    // Test 14: Parentheses for negatives
1226    #[test]
1227    fn test_parens_for_negatives() {
1228        let mut options = CurrencyOptions::new();
1229        options.parens_for_negatives = true;
1230
1231        let valid = vec![
1232            "1,234",
1233            "(1,234)",
1234            "($6,954,231)",
1235            "$10.03",
1236            "(10.03)",
1237            "($10.03)",
1238            "1.39",
1239            ".03",
1240            "(.03)",
1241            "($.03)",
1242            "0.10",
1243            "$10567.01",
1244            "($0.01)",
1245            "$1,234,567.89",
1246            "$10,123",
1247            "(10,123)",
1248            "10123",
1249        ];
1250
1251        let invalid = vec![
1252            "1.234",
1253            "($1.1)",
1254            "-$1.10",
1255            "$ 32.50",
1256            "500$",
1257            ".0001",
1258            "$.001",
1259            "($0.001)",
1260            "12,34.56",
1261            "123456,123,123456",
1262            "( 123)",
1263            ",123",
1264            "$-,123",
1265            "",
1266            " ",
1267            "  ",
1268            "   ",
1269            "$",
1270            "$ ",
1271            " $",
1272            " 123",
1273            "(123) ",
1274            ".",
1275            ",",
1276            "00",
1277            "$-",
1278            "$ - ",
1279            "$- ",
1280            " - ",
1281            "-",
1282            "- $",
1283            "-$",
1284            "()",
1285            "( )",
1286            "(  -)",
1287            "(  - )",
1288            "(  -  )",
1289            "(-)",
1290            "(-$)",
1291        ];
1292
1293        for val in valid {
1294            assert!(is_currency(val, Some(options.clone())), "Expected '{}' to be valid", val);
1295        }
1296
1297        for val in invalid {
1298            assert!(!is_currency(val, Some(options.clone())), "Expected '{}' to be invalid", val);
1299        }
1300    }
1301
1302    // Test 15: No negatives allowed (standard USD)
1303    #[test]
1304    fn test_no_negatives_usd() {
1305        let mut options = CurrencyOptions::new();
1306        options.allow_negatives = false;
1307
1308        let valid = vec![
1309            "$10,123.45",
1310            "$10123.45",
1311            "10,123.45",
1312            "10123.45",
1313            "10,123",
1314            "1,123,456",
1315            "1123456",
1316            "1.39",
1317            ".03",
1318            "0.10",
1319            "$0.10",
1320            "$100,234,567.89",
1321            "$10,123",
1322            "10,123",
1323        ];
1324
1325        let invalid = vec![
1326            "1.234",
1327            "-1.234",
1328            "-10123",
1329            "-$0.01",
1330            "-$.99",
1331            "$1.1",
1332            "-$1.1",
1333            "$ 32.50",
1334            "500$",
1335            ".0001",
1336            "$.001",
1337            "$0.001",
1338            "12,34.56",
1339            "123456,123,123456",
1340            "-123456,123,123456",
1341            "123,4",
1342            ",123",
1343            "$-,123",
1344            "$",
1345            ".",
1346            ",",
1347            "00",
1348            "$-",
1349            "$-,.",
1350            "-",
1351            "-$",
1352            "",
1353            "- $",
1354            "-$10,123.45",
1355        ];
1356
1357        for val in valid {
1358            assert!(is_currency(val, Some(options.clone())), "Expected '{}' to be valid", val);
1359        }
1360
1361        for val in invalid {
1362            assert!(!is_currency(val, Some(options.clone())), "Expected '{}' to be invalid", val);
1363        }
1364    }
1365
1366    // Test 16: Brazilian Real format
1367    #[test]
1368    fn test_brazilian_real() {
1369        let mut options = CurrencyOptions::new();
1370        options.symbol = "R$".to_string();
1371        options.require_symbol = true;
1372        options.allow_space_after_symbol = true;
1373        options.symbol_after_digits = false;
1374        options.thousands_separator = '.';
1375        options.decimal_separator = ',';
1376
1377        let valid = vec![
1378            "R$ 1.400,00",
1379            "R$ 400,00",
1380        ];
1381
1382        let invalid = vec![
1383            "$ 1.400,00",
1384            "$R 1.400,00",
1385        ];
1386
1387        for val in valid {
1388            assert!(is_currency(val, Some(options.clone())), "Expected '{}' to be valid", val);
1389        }
1390
1391        for val in invalid {
1392            assert!(!is_currency(val, Some(options.clone())), "Expected '{}' to be invalid", val);
1393        }
1394    }
1395}
1396