Skip to main content

ec_validator/
cedula.rs

1use crate::ValidationError;
2
3/// Validates an Ecuadorian Cédula de Identidad (national ID).
4///
5/// # Arguments
6///
7/// * `input` - A string containing the Cédula number (10 digits).
8///
9/// # Errors
10///
11/// Returns [`ValidationError`] on validation failure:
12/// - [`ValidationError::InvalidLength`] - Not exactly 10 digits
13/// - [`ValidationError::InvalidFormat`] - Contains non-numeric characters
14/// - [`ValidationError::InvalidProvinceCode`] - Province code not in 01-24
15/// - [`ValidationError::InvalidCheckDigit`] - Mod-10 check digit invalid
16///
17/// # Examples
18///
19/// ```
20/// use ec_validator::cedula;
21///
22/// // Valid Ecuadorian Cédula
23/// let result = cedula::validate("1713175071");
24/// assert!(result.is_ok());
25///
26/// // Invalid Cédula (wrong check digit)
27/// let result = cedula::validate("1713175072");
28/// assert!(result.is_err());
29/// ```
30pub fn validate(input: &str) -> Result<(), ValidationError> {
31    let input = input.trim();
32
33    if input.len() != 10 {
34        return Err(ValidationError::InvalidLength);
35    }
36
37    if !input.chars().all(|c| c.is_ascii_digit()) {
38        return Err(ValidationError::InvalidFormat);
39    }
40
41    let province: u32 = input[..2].parse().unwrap();
42    if province == 0 || province > 24 {
43        return Err(ValidationError::InvalidProvinceCode);
44    }
45
46    let third_digit = input.chars().nth(2).unwrap().to_digit(10).unwrap();
47    if third_digit > 6 {
48        return Err(ValidationError::InvalidFormat);
49    }
50
51    let digits: Vec<u32> = input.chars().map(|c| c.to_digit(10).unwrap()).collect();
52
53    let mut sum = 0u32;
54    for (i, &digit) in digits.iter().enumerate().take(9) {
55        let product = if i % 2 == 0 { digit * 2 } else { digit };
56        if product >= 10 {
57            sum += product - 9;
58        } else {
59            sum += product;
60        }
61    }
62
63    let remainder = if sum.is_multiple_of(10) {
64        0
65    } else {
66        10 - (sum % 10)
67    };
68
69    if remainder != digits[9] {
70        return Err(ValidationError::InvalidCheckDigit);
71    }
72
73    Ok(())
74}
75
76/// Convenience function that returns `true` if the Cédula is valid, `false` otherwise.
77///
78/// # Arguments
79///
80/// * `input` - A string containing the Cédula number.
81///
82/// # Examples
83///
84/// ```
85/// use ec_validator::cedula;
86///
87/// assert!(cedula::is_valid("1713175071"));
88/// assert!(!cedula::is_valid("0000000000"));
89/// ```
90pub fn is_valid(input: &str) -> bool {
91    validate(input).is_ok()
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use proptest::prelude::*;
98
99    #[test]
100    fn valid_cedulas() {
101        assert!(validate("1713175071").is_ok());
102        assert!(validate("0910009000").is_ok());
103        assert!(validate("1710034065").is_ok());
104    }
105
106    #[test]
107    fn invalid_length() {
108        assert_eq!(validate("171317507"), Err(ValidationError::InvalidLength));
109    }
110
111    #[test]
112    fn invalid_province() {
113        assert_eq!(
114            validate("9913175071"),
115            Err(ValidationError::InvalidProvinceCode)
116        );
117    }
118
119    #[test]
120    fn invalid_check_digit() {
121        assert_eq!(
122            validate("1713175072"),
123            Err(ValidationError::InvalidCheckDigit)
124        );
125    }
126
127    #[test]
128    fn non_numeric() {
129        assert!(validate("171317507a").is_err());
130    }
131
132    proptest! {
133        #[test]
134        fn no_panic(s in "\\d{10}") {
135            let _ = validate(&s);
136        }
137    }
138}