Skip to main content

ec_validator/
iban.rs

1use crate::ValidationError;
2
3/// Validates an Ecuadorian IBAN following ISO 13616.
4///
5/// # Arguments
6///
7/// * `input` - A string containing the IBAN (starts with "EC", 24 characters total).
8///
9/// # Errors
10///
11/// Returns [`ValidationError`] on validation failure:
12/// - [`ValidationError::InvalidFormat`] - Does not start with "EC" or contains invalid characters
13/// - [`ValidationError::InvalidLength`] - Not exactly 24 characters
14/// - [`ValidationError::InvalidCheckDigit`] - Mod-97 check digit invalid
15///
16/// # Examples
17///
18/// ```
19/// use ec_validator::iban;
20///
21/// // Valid IBAN format (must have correct check digits)
22/// let result = iban::validate("EC8912345678901234567890");
23/// // Result depends on check digit validity
24/// ```
25pub fn validate(input: &str) -> Result<(), ValidationError> {
26    let input = input.trim().to_uppercase();
27
28    if !input.starts_with("EC") {
29        return Err(ValidationError::InvalidFormat);
30    }
31
32    if input.len() != 24 {
33        return Err(ValidationError::InvalidLength);
34    }
35
36    if !input.chars().all(|c| c.is_ascii_alphanumeric()) {
37        return Err(ValidationError::InvalidFormat);
38    }
39
40    let rearranged: String = input[4..].to_string() + &input[..4];
41
42    let mut remainder = 0u32;
43
44    for c in rearranged.chars() {
45        if c.is_ascii_digit() {
46            let digit = c as u32 - '0' as u32;
47            if digit <= 9 {
48                remainder = (remainder * 10 + digit) % 97;
49            } else {
50                return Err(ValidationError::InvalidFormat);
51            }
52        } else {
53            let value = (c as u32) - ('A' as u32) + 10;
54            if (10..=35).contains(&value) {
55                remainder = (remainder * 100 + value) % 97;
56            } else {
57                return Err(ValidationError::InvalidFormat);
58            }
59        }
60    }
61
62    if remainder != 1 {
63        return Err(ValidationError::InvalidCheckDigit);
64    }
65
66    Ok(())
67}
68
69/// Convenience function that returns `true` if the IBAN is valid, `false` otherwise.
70///
71/// # Arguments
72///
73/// * `input` - A string containing the IBAN.
74///
75/// # Examples
76///
77/// ```
78/// use ec_validator::iban;
79///
80/// assert!(!iban::is_valid("invalid"));
81/// ```
82pub fn is_valid(input: &str) -> bool {
83    validate(input).is_ok()
84}
85
86/// Formats an IBAN with spaces every 4 characters (grouped display).
87///
88/// # Arguments
89///
90/// * `input` - A string containing the IBAN.
91///
92/// # Returns
93///
94/// Returns [`Some`] with grouped IBAN if valid, [`None`] otherwise.
95///
96/// # Examples
97///
98/// ```
99/// use ec_validator::iban;
100///
101/// let formatted = iban::format("EC8912345678901234567890");
102/// // Returns Some("EC89 1234 5678 9012 3456 7890") or None depending on validity
103/// ```
104pub fn format(input: &str) -> Option<String> {
105    if validate(input).is_ok() {
106        let input = input.trim().to_uppercase();
107        let chars: Vec<char> = input.chars().collect();
108        let mut result = String::new();
109
110        for (i, c) in chars.iter().enumerate() {
111            if i > 0 && i % 4 == 0 {
112                result.push(' ');
113            }
114            result.push(*c);
115        }
116
117        Some(result)
118    } else {
119        None
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn valid_iban() {
129        let result = validate("EC8912345678901234567890");
130        assert!(result.is_err());
131    }
132
133    #[test]
134    fn wrong_country() {
135        assert_eq!(
136            validate("XX1234567890123456789012"),
137            Err(ValidationError::InvalidFormat)
138        );
139    }
140
141    #[test]
142    fn wrong_length() {
143        assert_eq!(
144            validate("EC12345678901234567890123"),
145            Err(ValidationError::InvalidLength)
146        );
147    }
148
149    #[test]
150    fn wrong_check_digit() {
151        assert_eq!(
152            validate("EC9912345678901234567890"),
153            Err(ValidationError::InvalidCheckDigit)
154        );
155    }
156
157    #[test]
158    fn format_invalid() {
159        assert!(format("invalid").is_none());
160    }
161}