Skip to main content

stdbr_core/
cpf.rs

1//! CPF (Cadastro de Pessoas Físicas) - validation, formatting and generation.
2//!
3//! Uses modulo-11 weighted sums as specified by Receita Federal do Brasil.
4
5use alloc::string::String;
6use alloc::vec::Vec;
7use core::fmt;
8
9use crate::rand::{RandomSource, SeededRng, below_u8, simple_seed};
10use crate::util::{self, impl_document_traits};
11
12const CPF_LEN: usize = 11;
13const WEIGHTS_D1: [u32; 9] = [10, 9, 8, 7, 6, 5, 4, 3, 2];
14const WEIGHTS_D2: [u32; 10] = [11, 10, 9, 8, 7, 6, 5, 4, 3, 2];
15const FORMATTED_DIGIT_POS: [usize; 11] = [0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13];
16
17/// Fiscal region mapped by the 9th digit of a CPF.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19#[repr(u8)]
20pub enum FiscalRegion {
21    Rs = 0,
22    DfGoMsMtTo = 1,
23    AcAmApPaRoRr = 2,
24    CeMaPi = 3,
25    AlPbPeRn = 4,
26    BaSe = 5,
27    Mg = 6,
28    EsRj = 7,
29    Sp = 8,
30    PrSc = 9,
31}
32
33impl FiscalRegion {
34    fn from_digit(d: u8) -> Self {
35        assert!(d <= 9, "digit must be 0..=9");
36        // SAFETY: repr(u8) with discriminants 0..=9, d validated above.
37        unsafe { core::mem::transmute(d) }
38    }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum CpfError {
43    InvalidLength,
44    InvalidCharacter,
45    InvalidFormat,
46    AllDigitsEqual,
47    InvalidCheckDigits,
48}
49
50impl fmt::Display for CpfError {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        f.write_str(match self {
53            Self::InvalidLength => "CPF must contain exactly 11 digits",
54            Self::InvalidCharacter => "CPF contains invalid characters",
55            Self::InvalidFormat => "CPF format must be ###.###.###-## or 11 digits",
56            Self::AllDigitsEqual => "CPF with all equal digits is invalid",
57            Self::InvalidCheckDigits => "CPF check digits are invalid",
58        })
59    }
60}
61
62impl core::error::Error for CpfError {}
63
64/// A validated CPF stored as 11 ASCII bytes.
65///
66/// ```
67/// use stdbr_core::cpf::{Cpf, generate_cpf};
68///
69/// let cpf = generate_cpf();
70/// assert_eq!(cpf.as_str().len(), 11);
71/// assert_eq!(cpf.to_string().len(), 14); // ###.###.###-##
72///
73/// let parsed: Cpf = cpf.to_string().parse().unwrap();
74/// assert_eq!(cpf, parsed);
75/// ```
76#[derive(Clone, Copy, PartialEq, Eq, Hash)]
77pub struct Cpf {
78    bytes: [u8; CPF_LEN],
79}
80
81impl Cpf {
82    /// Unformatted 11-digit `&str`.
83    pub fn as_str(&self) -> &str {
84        // SAFETY: constructors guarantee ASCII digits only.
85        unsafe { core::str::from_utf8_unchecked(&self.bytes) }
86    }
87
88    /// The 11 numeric digits (0–9).
89    pub fn digits(&self) -> [u8; CPF_LEN] {
90        self.bytes.map(|b| b - b'0')
91    }
92
93    /// Fiscal region derived from the 9th digit.
94    pub fn fiscal_region(&self) -> FiscalRegion {
95        FiscalRegion::from_digit(self.bytes[8] - b'0')
96    }
97
98    /// Masked: `XXX.***.***-XX`.
99    pub fn masked(&self) -> String {
100        let s = self.as_str();
101        alloc::format!("{}.***.***-{}", &s[0..3], &s[9..11])
102    }
103
104    /// The two check digits `(d1, d2)`.
105    pub fn check_digits(&self) -> (u8, u8) {
106        (self.bytes[9] - b'0', self.bytes[10] - b'0')
107    }
108
109    fn from_numeric(digits: [u8; CPF_LEN]) -> Self {
110        Self {
111            bytes: digits.map(|d| d + b'0'),
112        }
113    }
114}
115
116impl fmt::Display for Cpf {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        let s = self.as_str();
119        write!(f, "{}.{}.{}-{}", &s[0..3], &s[3..6], &s[6..9], &s[9..11])
120    }
121}
122
123impl_document_traits!(Cpf, CpfError);
124
125/// Normalizes a permissive CPF input by retaining only ASCII digits.
126pub fn normalize(cpf: &str) -> String {
127    cpf.chars().filter(char::is_ascii_digit).collect()
128}
129
130/// Compatibility alias for [`normalize`].
131pub fn remove_symbols(cpf: &str) -> String {
132    normalize(cpf)
133}
134
135/// Lenient validation that ignores every non-ASCII-digit character.
136pub fn is_valid_lenient(cpf: &str) -> bool {
137    let raw = normalize(cpf);
138    if raw.len() != CPF_LEN {
139        return false;
140    }
141    let d: Vec<u8> = raw.bytes().map(|b| b - b'0').collect();
142    validate_digits(&d)
143}
144
145/// Compatibility alias for [`is_valid_lenient`].
146///
147/// Use [`is_valid_strict`] when punctuation and whitespace must be rejected.
148pub fn is_valid(cpf: &str) -> bool {
149    is_valid_lenient(cpf)
150}
151
152/// Strict validation - accepts `###.###.###-##` or `###########` only.
153pub fn is_valid_strict(cpf: &str) -> Result<(), CpfError> {
154    parse_strict(cpf).map(|_| ())
155}
156
157/// Formats as `###.###.###-##`, or `None` if not 11 digits.
158pub fn format_cpf(cpf: &str) -> Option<String> {
159    let d = remove_symbols(cpf);
160    (d.len() == CPF_LEN)
161        .then(|| alloc::format!("{}.{}.{}-{}", &d[0..3], &d[3..6], &d[6..9], &d[9..11]))
162}
163
164/// Generates a random valid CPF as an 11-digit string.
165///
166/// This generation is not cryptographically secure.
167pub fn generate() -> String {
168    generate_cpf().as_str().into()
169}
170
171/// Generates a random valid [`Cpf`].
172///
173/// This generation is not cryptographically secure.
174pub fn generate_cpf() -> Cpf {
175    let mut rng = SeededRng::new(simple_seed());
176    generate_cpf_with_rng(&mut rng)
177}
178
179/// Generates a valid [`Cpf`] using an injected random source.
180///
181/// This generation is not cryptographically secure.
182pub fn generate_cpf_with_rng<R: RandomSource + ?Sized>(rng: &mut R) -> Cpf {
183    generate_with_rng(rng)
184}
185
186/// Generates a random valid [`Cpf`] for a given fiscal region.
187///
188/// This generation is not cryptographically secure.
189pub fn generate_for_region(region: FiscalRegion) -> Cpf {
190    let mut rng = SeededRng::new(simple_seed());
191    generate_for_region_with_rng(&mut rng, region)
192}
193
194/// Generates a deterministic [`Cpf`] for a fiscal region from a seed.
195///
196/// This generation is not cryptographically secure. Seed zero is accepted.
197pub fn generate_for_region_with_seed(seed: u64, region: FiscalRegion) -> Cpf {
198    let mut rng = SeededRng::new(seed);
199    generate_for_region_with_rng(&mut rng, region)
200}
201
202/// Generates a valid [`Cpf`] for a fiscal region using an injected source.
203///
204/// This generation is not cryptographically secure.
205pub fn generate_for_region_with_rng<R: RandomSource + ?Sized>(
206    rng: &mut R,
207    region: FiscalRegion,
208) -> Cpf {
209    let mut digits = [0u8; CPF_LEN];
210
211    loop {
212        for d in &mut digits[..8] {
213            *d = below_u8(rng, 10);
214        }
215        digits[8] = region as u8;
216        if !all_equal(&digits[..9]) {
217            break;
218        }
219    }
220
221    append_check_digits(&mut digits);
222    Cpf::from_numeric(digits)
223}
224
225pub fn compute_check_digits(base: &str) -> Option<(u8, u8)> {
226    let raw = remove_symbols(base);
227    if raw.len() != 9 {
228        return None;
229    }
230
231    let d: Vec<u8> = raw.bytes().map(|b| b - b'0').collect();
232    if all_equal(&d) {
233        return None;
234    }
235
236    let d1 = check_digit(&d, &WEIGHTS_D1);
237    let mut full = [0u8; 10];
238    full[..9].copy_from_slice(&d);
239    full[9] = d1;
240    let d2 = check_digit(&full, &WEIGHTS_D2);
241
242    Some((d1, d2))
243}
244
245fn all_equal(digits: &[u8]) -> bool {
246    util::all_equal(digits)
247}
248
249fn validate_digits(d: &[u8]) -> bool {
250    !all_equal(d)
251        && d[9] == check_digit(&d[..9], &WEIGHTS_D1)
252        && d[10] == check_digit(&d[..10], &WEIGHTS_D2)
253}
254
255fn check_digit(digits: &[u8], weights: &[u32]) -> u8 {
256    let sum: u32 = digits
257        .iter()
258        .zip(weights)
259        .map(|(&d, &w)| u32::from(d) * w)
260        .sum();
261    let rem = (sum * 10) % 11;
262    if rem == 10 { 0 } else { rem as u8 }
263}
264
265fn append_check_digits(digits: &mut [u8; CPF_LEN]) {
266    digits[9] = check_digit(&digits[..9], &WEIGHTS_D1);
267    digits[10] = check_digit(&digits[..10], &WEIGHTS_D2);
268}
269
270fn parse_strict(s: &str) -> Result<Cpf, CpfError> {
271    let raw = s.as_bytes();
272
273    let numeric: Vec<u8> = match raw.len() {
274        11 => {
275            if !raw.iter().all(u8::is_ascii_digit) {
276                return Err(CpfError::InvalidCharacter);
277            }
278            raw.iter().map(|b| b - b'0').collect()
279        }
280        14 => {
281            if raw[3] != b'.' || raw[7] != b'.' || raw[11] != b'-' {
282                return Err(CpfError::InvalidFormat);
283            }
284            for &i in &FORMATTED_DIGIT_POS {
285                if !raw[i].is_ascii_digit() {
286                    return Err(CpfError::InvalidCharacter);
287                }
288            }
289            FORMATTED_DIGIT_POS.iter().map(|&i| raw[i] - b'0').collect()
290        }
291        _ => return Err(CpfError::InvalidLength),
292    };
293
294    if all_equal(&numeric) {
295        return Err(CpfError::AllDigitsEqual);
296    }
297
298    let d1 = check_digit(&numeric[..9], &WEIGHTS_D1);
299    let d2 = check_digit(&numeric[..10], &WEIGHTS_D2);
300    if numeric[9] != d1 || numeric[10] != d2 {
301        return Err(CpfError::InvalidCheckDigits);
302    }
303
304    let mut digits = [0u8; CPF_LEN];
305    digits.copy_from_slice(&numeric);
306    Ok(Cpf::from_numeric(digits))
307}
308
309/// Generates a deterministic valid [`Cpf`] from a seed.
310///
311/// This generation is not cryptographically secure. Seed zero is accepted.
312pub fn generate_with_seed(seed: u64) -> Cpf {
313    let mut rng = SeededRng::new(seed);
314    generate_with_rng(&mut rng)
315}
316
317/// Generates a valid [`Cpf`] using an injected random source.
318///
319/// This generation is not cryptographically secure.
320pub fn generate_with_rng<R: RandomSource + ?Sized>(rng: &mut R) -> Cpf {
321    let mut digits = [0u8; CPF_LEN];
322
323    loop {
324        for d in &mut digits[..9] {
325            *d = below_u8(rng, 10);
326        }
327        if !all_equal(&digits[..9]) {
328            break;
329        }
330    }
331
332    append_check_digits(&mut digits);
333    Cpf::from_numeric(digits)
334}
335
336#[cfg(test)]
337fn make_cpf(base: [u8; 9]) -> Cpf {
338    let mut digits = [0u8; CPF_LEN];
339    digits[..9].copy_from_slice(&base);
340    append_check_digits(&mut digits);
341    Cpf::from_numeric(digits)
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347    use alloc::string::ToString;
348
349    fn cpf_a() -> Cpf {
350        make_cpf([5, 2, 9, 9, 8, 2, 2, 4, 7])
351    }
352    fn cpf_b() -> Cpf {
353        make_cpf([3, 4, 7, 0, 6, 6, 1, 2, 0])
354    }
355    fn cpf_c() -> Cpf {
356        make_cpf([0, 0, 1, 2, 3, 4, 5, 6, 7])
357    }
358
359    #[test]
360    fn is_valid_accepts_valid_unformatted() {
361        assert!(is_valid(cpf_a().as_str()));
362        assert!(is_valid(cpf_b().as_str()));
363        assert!(is_valid(cpf_c().as_str()));
364    }
365
366    #[test]
367    fn is_valid_accepts_valid_formatted() {
368        let a = cpf_a().to_string();
369        let b = cpf_b().to_string();
370        assert!(is_valid(&a));
371        assert!(is_valid(&b));
372    }
373
374    #[test]
375    fn is_valid_accepts_leading_zero_cpf() {
376        let cpf = cpf_c();
377        assert!(cpf.as_str().starts_with("00"));
378        assert!(is_valid(cpf.as_str()));
379    }
380
381    #[test]
382    fn is_valid_lenient_strips_garbage() {
383        let cpf = cpf_a();
384        let s = cpf.as_str();
385        let garbage = alloc::format!("{}${}#{}!{}", &s[0..3], &s[3..6], &s[6..9], &s[9..11]);
386        assert!(is_valid(&garbage));
387
388        let padded = alloc::format!("  {}  ", cpf_a());
389        assert!(is_valid(&padded));
390
391        let mut spaced = String::new();
392        for c in s.bytes() {
393            use core::fmt::Write;
394            write!(spaced, "{} ", c as char).unwrap();
395        }
396        assert!(is_valid(spaced.trim()));
397    }
398
399    #[test]
400    fn is_valid_rejects_wrong_check_digits() {
401        let mut bad = cpf_a().digits();
402        bad[10] = (bad[10] + 1) % 10;
403        let s: String = bad.iter().map(|&d| (b'0' + d) as char).collect();
404        assert!(!is_valid(&s));
405    }
406
407    #[test]
408    fn is_valid_rejects_all_same_digits() {
409        for d in 0..=9u8 {
410            let cpf: String = core::iter::repeat_n(char::from(b'0' + d), 11).collect();
411            assert!(!is_valid(&cpf), "should reject {cpf}");
412        }
413    }
414
415    #[test]
416    fn is_valid_rejects_wrong_length() {
417        assert!(!is_valid(""));
418        assert!(!is_valid("1234567890"));
419        assert!(!is_valid("123456789012"));
420    }
421
422    #[test]
423    fn is_valid_rejects_no_digits() {
424        assert!(!is_valid("abc.def.ghi-jk"));
425        assert!(!is_valid("...---"));
426    }
427
428    #[test]
429    fn is_valid_rejects_embedded_digits_in_long_string() {
430        assert!(!is_valid("abc1234567890123def"));
431    }
432
433    #[test]
434    fn strict_accepts_valid_unformatted() {
435        assert!(is_valid_strict(cpf_a().as_str()).is_ok());
436        assert!(is_valid_strict(cpf_b().as_str()).is_ok());
437    }
438
439    #[test]
440    fn strict_accepts_valid_formatted() {
441        let a = cpf_a().to_string();
442        let b = cpf_b().to_string();
443        assert!(is_valid_strict(&a).is_ok());
444        assert!(is_valid_strict(&b).is_ok());
445    }
446
447    #[test]
448    fn strict_rejects_garbage_between_digits() {
449        let cpf = cpf_a();
450        let s = cpf.as_str();
451        let garbage = alloc::format!("{}${}#{}!{}", &s[0..3], &s[3..6], &s[6..9], &s[9..11]);
452        assert!(is_valid_strict(&garbage).is_err());
453    }
454
455    #[test]
456    fn strict_rejects_whitespace() {
457        let padded = alloc::format!("  {}  ", cpf_a().as_str());
458        assert_eq!(is_valid_strict(&padded), Err(CpfError::InvalidLength));
459
460        let padded_fmt = alloc::format!(" {} ", cpf_a());
461        assert_eq!(is_valid_strict(&padded_fmt), Err(CpfError::InvalidLength));
462    }
463
464    #[test]
465    fn strict_rejects_misplaced_separators() {
466        let cpf = cpf_a();
467        let s = cpf.as_str();
468        let bad_fmt = alloc::format!("{}.{}.{}.{}", &s[0..4], &s[4..6], &s[6..9], &s[9..11]);
469        assert!(is_valid_strict(&bad_fmt).is_err());
470    }
471
472    #[test]
473    fn strict_rejects_letters() {
474        assert_eq!(
475            is_valid_strict("abcdefghijk"),
476            Err(CpfError::InvalidCharacter)
477        );
478    }
479
480    #[test]
481    fn strict_rejects_all_same_digits() {
482        assert_eq!(
483            is_valid_strict("11111111111"),
484            Err(CpfError::AllDigitsEqual)
485        );
486        assert_eq!(
487            is_valid_strict("000.000.000-00"),
488            Err(CpfError::AllDigitsEqual)
489        );
490    }
491
492    #[test]
493    fn strict_rejects_invalid_check_digits() {
494        let mut bad = cpf_a().digits();
495        bad[10] = (bad[10] + 1) % 10;
496        let s: String = bad.iter().map(|&d| (b'0' + d) as char).collect();
497        assert_eq!(is_valid_strict(&s), Err(CpfError::InvalidCheckDigits));
498    }
499
500    #[test]
501    fn parse_roundtrip() {
502        let cpf = cpf_a();
503        let parsed: Cpf = cpf.to_string().parse().unwrap();
504        assert_eq!(cpf, parsed);
505        assert_eq!(parsed.as_str(), cpf.as_str());
506    }
507
508    #[test]
509    fn parse_unformatted() {
510        let cpf = cpf_a();
511        let parsed: Cpf = cpf.as_str().parse().unwrap();
512        assert_eq!(cpf, parsed);
513    }
514
515    #[test]
516    fn parse_equality_across_formats() {
517        let from_fmt: Cpf = cpf_a().to_string().parse().unwrap();
518        let from_raw: Cpf = cpf_a().as_str().parse().unwrap();
519        assert_eq!(from_fmt, from_raw);
520    }
521
522    #[test]
523    fn cpf_is_copy() {
524        let a = cpf_a();
525        let b = a;
526        assert_eq!(a, b);
527    }
528
529    #[test]
530    fn cpf_as_ref_str() {
531        let cpf = cpf_a();
532        let r: &str = cpf.as_ref();
533        assert_eq!(r, cpf.as_str());
534    }
535
536    #[test]
537    fn debug_format() {
538        let cpf = cpf_a();
539        let dbg = alloc::format!("{cpf:?}");
540        assert!(dbg.starts_with("Cpf("));
541        assert!(dbg.ends_with(')'));
542        assert!(dbg.contains('.'));
543        assert!(dbg.contains('-'));
544    }
545
546    #[test]
547    fn fiscal_region() {
548        let cpf = cpf_a();
549        let d = cpf.digits();
550        assert_eq!(cpf.fiscal_region(), FiscalRegion::from_digit(d[8]));
551
552        let cpf = cpf_b();
553        assert_eq!(cpf.digits()[8], 0);
554        assert_eq!(cpf.fiscal_region(), FiscalRegion::Rs);
555    }
556
557    #[test]
558    fn digits_array() {
559        let cpf = cpf_a();
560        assert_eq!(cpf.digits()[..9], [5, 2, 9, 9, 8, 2, 2, 4, 7]);
561    }
562
563    #[test]
564    fn parse_rejects_invalid() {
565        let mut bad = cpf_a().digits();
566        bad[10] = (bad[10] + 1) % 10;
567        let s: String = bad.iter().map(|&d| (b'0' + d) as char).collect();
568        assert!(s.parse::<Cpf>().is_err());
569        assert!("abc".parse::<Cpf>().is_err());
570        assert!("".parse::<Cpf>().is_err());
571    }
572
573    #[test]
574    fn masked() {
575        let cpf = cpf_a();
576        let s = cpf.as_str();
577        let expected = alloc::format!("{}.***.***-{}", &s[0..3], &s[9..11]);
578        assert_eq!(cpf.masked(), expected);
579        assert_eq!(cpf.masked().len(), 14);
580    }
581
582    #[test]
583    fn check_digits() {
584        let cpf = cpf_a();
585        let (d1, d2) = cpf.check_digits();
586        assert_eq!(d1, cpf.digits()[9]);
587        assert_eq!(d2, cpf.digits()[10]);
588    }
589
590    #[test]
591    fn remove_symbols_strips_formatting() {
592        let cpf = cpf_a();
593        let formatted = cpf.to_string();
594        assert_eq!(remove_symbols(&formatted), cpf.as_str());
595        assert_eq!(remove_symbols(cpf.as_str()), cpf.as_str());
596        assert_eq!(remove_symbols(""), "");
597    }
598
599    #[test]
600    fn remove_symbols_strips_arbitrary_chars() {
601        assert_eq!(remove_symbols("abc123def456ghi78901"), "12345678901");
602    }
603
604    #[test]
605    fn format_cpf_produces_formatted_output() {
606        let cpf = cpf_a();
607        let formatted = cpf.to_string();
608        assert_eq!(format_cpf(cpf.as_str()), Some(formatted.clone()));
609        assert_eq!(format_cpf(&formatted), Some(formatted));
610    }
611
612    #[test]
613    fn format_cpf_returns_none_on_bad_length() {
614        assert_eq!(format_cpf("1234"), None);
615        assert_eq!(format_cpf(""), None);
616    }
617
618    #[test]
619    fn format_cpf_preserves_leading_zeros() {
620        let cpf = cpf_c();
621        let formatted = format_cpf(cpf.as_str()).unwrap();
622        assert!(formatted.starts_with("001."));
623    }
624
625    #[test]
626    fn generate_produces_valid_cpfs() {
627        for _ in 0..100 {
628            let cpf = generate();
629            assert_eq!(cpf.len(), 11);
630            assert!(is_valid(&cpf), "generated invalid CPF: {cpf}");
631        }
632    }
633
634    #[test]
635    fn generate_cpf_roundtrips() {
636        for _ in 0..100 {
637            let cpf = generate_cpf();
638            assert!(is_valid(cpf.as_str()));
639            let parsed: Cpf = cpf.as_str().parse().unwrap();
640            assert_eq!(cpf, parsed);
641        }
642    }
643
644    #[test]
645    fn seeded_generation_accepts_zero_and_is_deterministic() {
646        assert_eq!(generate_with_seed(0), generate_with_seed(0));
647        assert!(is_valid(generate_with_seed(0).as_str()));
648    }
649
650    #[test]
651    fn generate_for_region_respects_region_digit() {
652        let regions = [
653            FiscalRegion::Rs,
654            FiscalRegion::DfGoMsMtTo,
655            FiscalRegion::AcAmApPaRoRr,
656            FiscalRegion::CeMaPi,
657            FiscalRegion::AlPbPeRn,
658            FiscalRegion::BaSe,
659            FiscalRegion::Mg,
660            FiscalRegion::EsRj,
661            FiscalRegion::Sp,
662            FiscalRegion::PrSc,
663        ];
664        for region in regions {
665            let cpf = generate_for_region(region);
666            assert_eq!(cpf.fiscal_region(), region);
667            assert!(is_valid(cpf.as_str()));
668        }
669    }
670
671    #[test]
672    fn compute_check_digits_known_base() {
673        let cpf = cpf_a();
674        let base = &cpf.as_str()[..9];
675        let (d1, d2) = compute_check_digits(base).unwrap();
676        assert_eq!(d1, cpf.digits()[9]);
677        assert_eq!(d2, cpf.digits()[10]);
678    }
679
680    #[test]
681    fn compute_check_digits_rejects_bad_input() {
682        assert_eq!(compute_check_digits("12345678"), None);
683        assert_eq!(compute_check_digits("1234567890"), None);
684        assert_eq!(compute_check_digits("000000000"), None);
685    }
686
687    #[test]
688    fn leading_zero_cpf() {
689        let cpf = cpf_c();
690        assert!(cpf.as_str().starts_with("00"));
691        assert!(is_valid(cpf.as_str()));
692
693        let parsed: Cpf = cpf.as_str().parse().unwrap();
694        assert_eq!(parsed.digits()[0], 0);
695        assert_eq!(parsed.digits()[1], 0);
696    }
697}