Skip to main content

stdbr_core/
rg.rs

1//! RG (Registro Geral) - per-UF identity card validation, formatting and generation.
2//!
3//! RG is issued independently by each Brazilian state and there is no single
4//! national algorithm. Only **SP** has a widely-adopted, documented mod-11
5//! check digit algorithm — implemented here in full. For every other UF this
6//! module performs **structural validation only** (length range + digit
7//! charset) and treats the input as opaque digits.
8//!
9//! # SP algorithm
10//!
11//! 8-digit body `d1..d8`. Weights `9,8,7,6,5,4,3,2` applied left-to-right.
12//! `sum = Σ d_i * w_i`. Check digit = `sum mod 11`; remainder
13//! `10` is rendered as the ASCII character `'X'`. Canonical formatted form is
14//! `XX.XXX.XXX-X`.
15//!
16//! # Other UFs
17//!
18//! `is_valid`/`is_valid_strict` only enforce length (5..=14 digits). Generation
19//! returns `RgError::UnsupportedUfForGeneration`. Promote a UF from structural
20//! to full validation by extending `uf_spec` once an authoritative algorithm
21//! is verified.
22
23use alloc::string::String;
24use alloc::vec::Vec;
25use core::fmt;
26
27use crate::rand::{RandomSource, SeededRng, below_u8, simple_seed};
28use crate::uf::State;
29
30const RG_MAX_LEN: usize = 14;
31const SP_BODY_LEN: u8 = 9;
32const SP_FORMATTED_LEN: u8 = 12;
33const SP_BASE_LEN: usize = 8;
34const SP_WEIGHTS: [u32; SP_BASE_LEN] = [9, 8, 7, 6, 5, 4, 3, 2];
35const SP_FORMATTED_DIGIT_POS: [usize; 9] = [0, 1, 3, 4, 5, 7, 8, 9, 11];
36
37const STRUCTURAL_MIN_LEN: u8 = 5;
38const STRUCTURAL_MAX_LEN: u8 = 14;
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum RgError {
42    InvalidLength,
43    InvalidCharacter,
44    InvalidFormat,
45    InvalidCheckDigit,
46    UnsupportedUfForGeneration,
47}
48
49impl fmt::Display for RgError {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        f.write_str(match self {
52            Self::InvalidLength => "RG length is outside the accepted range for this UF",
53            Self::InvalidCharacter => "RG contains invalid characters",
54            Self::InvalidFormat => "RG format does not match the canonical mask for this UF",
55            Self::InvalidCheckDigit => "RG check digit is invalid",
56            Self::UnsupportedUfForGeneration => {
57                "RG generation is not supported for this UF (no verified algorithm)"
58            }
59        })
60    }
61}
62
63impl core::error::Error for RgError {}
64
65/// Per-UF formatting and validation spec.
66#[derive(Clone, Copy)]
67struct UfSpec {
68    body_len: Option<u8>,
69    has_check_digit: bool,
70    allow_x_terminator: bool,
71    separators: &'static [(u8, char)],
72    formatted_len: Option<u8>,
73}
74
75const STRUCTURAL_DEFAULT: UfSpec = UfSpec {
76    body_len: None,
77    has_check_digit: false,
78    allow_x_terminator: false,
79    separators: &[],
80    formatted_len: None,
81};
82
83const SP_SPEC: UfSpec = UfSpec {
84    body_len: Some(SP_BODY_LEN),
85    has_check_digit: true,
86    allow_x_terminator: true,
87    separators: &[(2, '.'), (5, '.'), (8, '-')],
88    formatted_len: Some(SP_FORMATTED_LEN),
89};
90
91const fn uf_spec(uf: State) -> UfSpec {
92    match uf {
93        State::SP => SP_SPEC,
94        _ => STRUCTURAL_DEFAULT,
95    }
96}
97
98/// A validated RG stored as ASCII bytes (digits, plus optional trailing `'X'`
99/// for SP), tagged with its issuing UF.
100#[derive(Clone, Copy, PartialEq, Eq, Hash)]
101pub struct Rg {
102    bytes: [u8; RG_MAX_LEN],
103    len: u8,
104    uf: State,
105}
106
107impl Rg {
108    /// Unformatted body as `&str` (digits, optionally trailing `'X'`).
109    pub fn as_str(&self) -> &str {
110        // SAFETY: constructors guarantee ASCII digits/`X` only.
111        unsafe { core::str::from_utf8_unchecked(&self.bytes[..self.len as usize]) }
112    }
113
114    /// Issuing state.
115    pub fn uf(&self) -> State {
116        self.uf
117    }
118
119    /// Formatted per the UF mask. For UFs without a known mask, returns the
120    /// unformatted body.
121    pub fn formatted(&self) -> String {
122        format_with_spec(self.as_str(), uf_spec(self.uf)).unwrap_or_else(|| self.as_str().into())
123    }
124
125    /// Masked representation — shows the first 2 digits and masks the rest.
126    ///
127    /// SP: `"294653272"` → `"29.***.***-*"` (formatted with separators).
128    /// Other UFs: `"1234567"` → `"12*****"` (no separators).
129    pub fn masked(&self) -> String {
130        let s = self.as_str();
131        let spec = uf_spec(self.uf);
132        if spec.has_check_digit && s.len() == SP_BODY_LEN as usize {
133            let mut out = String::with_capacity(SP_FORMATTED_LEN as usize);
134            out.push_str(&s[..2]);
135            out.push('.');
136            out.push_str("***");
137            out.push('.');
138            out.push_str("***");
139            out.push('-');
140            out.push('*');
141            out
142        } else {
143            let mut out = String::with_capacity(s.len());
144            for (i, _) in s.chars().enumerate() {
145                if i < 2 {
146                    out.push(s.as_bytes()[i] as char);
147                } else {
148                    out.push('*');
149                }
150            }
151            out
152        }
153    }
154
155    /// Body without the check digit.
156    ///
157    /// SP: returns the 8-digit base (without DV).
158    /// Other UFs: returns `as_str()` (no DV is identifiable).
159    pub fn body(&self) -> &str {
160        let spec = uf_spec(self.uf);
161        if spec.has_check_digit && self.len as usize == SP_BODY_LEN as usize {
162            // SAFETY: constructors guarantee ASCII content.
163            unsafe { core::str::from_utf8_unchecked(&self.bytes[..SP_BASE_LEN]) }
164        } else {
165            self.as_str()
166        }
167    }
168
169    /// Check digit when the UF has a verified algorithm. `Some(0..=9)` for
170    /// digits, `Some(10)` for the SP `'X'` terminator, `None` otherwise.
171    pub fn check_digit(&self) -> Option<u8> {
172        let spec = uf_spec(self.uf);
173        if !spec.has_check_digit {
174            return None;
175        }
176        let last = self.bytes[self.len as usize - 1];
177        if last == b'X' {
178            Some(10)
179        } else {
180            Some(last - b'0')
181        }
182    }
183}
184
185impl AsRef<str> for Rg {
186    fn as_ref(&self) -> &str {
187        self.as_str()
188    }
189}
190
191impl fmt::Display for Rg {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        f.write_str(&self.formatted())
194    }
195}
196
197impl fmt::Debug for Rg {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        write!(f, "Rg({}, {})", self.uf.abbreviation(), self.formatted())
200    }
201}
202
203/// Normalizes a permissive RG input. For SP, preserves `X` case-insensitively;
204/// for other UFs, retains only ASCII digits.
205pub fn normalize(rg: &str, uf: State) -> String {
206    let spec = uf_spec(uf);
207    let mut out = String::with_capacity(rg.len());
208    for c in rg.chars() {
209        if c.is_ascii_digit() {
210            out.push(c);
211        } else if spec.allow_x_terminator && (c == 'X' || c == 'x') {
212            out.push('X');
213        }
214    }
215    out
216}
217
218/// Compatibility alias for [`normalize`].
219pub fn remove_symbols(rg: &str, uf: State) -> String {
220    normalize(rg, uf)
221}
222
223/// Lenient validation - strips symbols, then checks length and (for SP) the
224/// check digit.
225pub fn is_valid_lenient(rg: &str, uf: State) -> bool {
226    let spec = uf_spec(uf);
227    let raw = normalize(rg, uf);
228    if !validate_body_length(&raw, spec) {
229        return false;
230    }
231    if !validate_charset(&raw, spec) {
232        return false;
233    }
234    if spec.has_check_digit {
235        return sp_check_digit_ok(&raw);
236    }
237    true
238}
239
240/// Compatibility alias for [`is_valid_lenient`].
241///
242/// Use [`is_valid_strict`] when punctuation and whitespace must be rejected.
243pub fn is_valid(rg: &str, uf: State) -> bool {
244    is_valid_lenient(rg, uf)
245}
246
247/// Strict validation - input must be either the canonical formatted mask or
248/// the unformatted body. No leading/trailing whitespace, no extra symbols.
249pub fn is_valid_strict(rg: &str, uf: State) -> Result<(), RgError> {
250    parse_strict(rg, uf).map(|_| ())
251}
252
253/// Insert per-UF separators. Returns `None` if the input length doesn't match
254/// the UF body length (or, for variable-length UFs, the structural range).
255pub fn format_rg(rg: &str, uf: State) -> Option<String> {
256    let spec = uf_spec(uf);
257    let raw = remove_symbols(rg, uf);
258    if !validate_body_length(&raw, spec) {
259        return None;
260    }
261    Some(format_with_spec(&raw, spec).unwrap_or(raw))
262}
263
264/// SP-only: compute the check digit for an 8-digit base. Returns `Some(0..=9)`
265/// or `Some(10)` (caller renders as `'X'`); `None` for non-SP or wrong length.
266pub fn compute_check_digit(base: &str, uf: State) -> Option<u8> {
267    if !matches!(uf, State::SP) {
268        return None;
269    }
270    let raw: Vec<u8> = base.bytes().filter(u8::is_ascii_digit).collect();
271    if raw.len() != SP_BASE_LEN {
272        return None;
273    }
274    Some(sp_check_digit(&raw))
275}
276
277/// Parse a raw RG string into a validated [`Rg`].
278pub fn parse_strict(raw: &str, uf: State) -> Result<Rg, RgError> {
279    let spec = uf_spec(uf);
280    let bytes = raw.as_bytes();
281
282    let body = if let Some(len) = spec.formatted_len.filter(|&l| bytes.len() == l as usize) {
283        let _ = len;
284        parse_formatted_sp(bytes)?
285    } else if let Some(body_len) = spec.body_len {
286        if bytes.len() != body_len as usize {
287            return Err(RgError::InvalidLength);
288        }
289        parse_unformatted(bytes, spec)?
290    } else {
291        if bytes.len() < STRUCTURAL_MIN_LEN as usize || bytes.len() > STRUCTURAL_MAX_LEN as usize {
292            return Err(RgError::InvalidLength);
293        }
294        parse_unformatted(bytes, spec)?
295    };
296
297    if spec.has_check_digit && !sp_check_digit_ok_bytes(&body) {
298        return Err(RgError::InvalidCheckDigit);
299    }
300
301    Ok(Rg::from_body(&body, uf))
302}
303
304/// Generate a random valid RG. SP only; other UFs return
305/// `RgError::UnsupportedUfForGeneration`.
306///
307/// This generation is not cryptographically secure.
308pub fn generate(uf: State) -> Result<Rg, RgError> {
309    let mut rng = SeededRng::new(simple_seed());
310    generate_with_rng(&mut rng, uf)
311}
312
313/// Generate a deterministic valid RG from a seed.
314///
315/// SP only; other UFs return `RgError::UnsupportedUfForGeneration`. This
316/// generation is not cryptographically secure. Seed zero is accepted.
317pub fn generate_with_seed(seed: u64, uf: State) -> Result<Rg, RgError> {
318    let mut rng = SeededRng::new(seed);
319    generate_with_rng(&mut rng, uf)
320}
321
322/// Generate a valid RG using an injected random source.
323///
324/// SP only; other UFs return `RgError::UnsupportedUfForGeneration`. This
325/// generation is not cryptographically secure.
326pub fn generate_with_rng<R: RandomSource + ?Sized>(rng: &mut R, uf: State) -> Result<Rg, RgError> {
327    if !matches!(uf, State::SP) {
328        return Err(RgError::UnsupportedUfForGeneration);
329    }
330    let mut digits = [0u8; SP_BASE_LEN];
331    for d in &mut digits {
332        *d = below_u8(rng, 10);
333    }
334    let mut body = [0u8; 9];
335    for (i, &d) in digits.iter().enumerate() {
336        body[i] = d + b'0';
337    }
338    let dv = sp_check_digit(&digits);
339    body[8] = if dv == 10 { b'X' } else { b'0' + dv };
340    Ok(Rg::from_body(&body, State::SP))
341}
342
343impl Rg {
344    fn from_body(body: &[u8], uf: State) -> Self {
345        let mut bytes = [0u8; RG_MAX_LEN];
346        bytes[..body.len()].copy_from_slice(body);
347        Self {
348            bytes,
349            len: u8::try_from(body.len()).expect("RG body length must fit in u8"),
350            uf,
351        }
352    }
353}
354
355fn validate_body_length(raw: &str, spec: UfSpec) -> bool {
356    if let Some(n) = spec.body_len {
357        raw.len() == n as usize
358    } else {
359        let n = raw.len();
360        n >= STRUCTURAL_MIN_LEN as usize && n <= STRUCTURAL_MAX_LEN as usize
361    }
362}
363
364fn validate_charset(raw: &str, spec: UfSpec) -> bool {
365    let bytes = raw.as_bytes();
366    if bytes.is_empty() {
367        return false;
368    }
369    if spec.allow_x_terminator {
370        let (last_idx, rest) = (bytes.len() - 1, &bytes[..bytes.len() - 1]);
371        if !rest.iter().all(u8::is_ascii_digit) {
372            return false;
373        }
374        let last = bytes[last_idx];
375        last.is_ascii_digit() || last == b'X'
376    } else {
377        bytes.iter().all(u8::is_ascii_digit)
378    }
379}
380
381fn parse_unformatted(bytes: &[u8], spec: UfSpec) -> Result<Vec<u8>, RgError> {
382    if !validate_charset(
383        // SAFETY: caller already verified ASCII boundaries via spec body_len/range checks.
384        unsafe { core::str::from_utf8_unchecked(bytes) },
385        spec,
386    ) {
387        return Err(RgError::InvalidCharacter);
388    }
389    Ok(bytes.to_vec())
390}
391
392fn parse_formatted_sp(bytes: &[u8]) -> Result<Vec<u8>, RgError> {
393    // Format mask: `XX.XXX.XXX-X` — separators at offsets 2, 6, 10.
394    if bytes[2] != b'.' || bytes[6] != b'.' || bytes[10] != b'-' {
395        return Err(RgError::InvalidFormat);
396    }
397    let mut out = Vec::with_capacity(SP_BODY_LEN as usize);
398    for (i, &idx) in SP_FORMATTED_DIGIT_POS.iter().enumerate() {
399        let b = bytes[idx];
400        let last = i == SP_FORMATTED_DIGIT_POS.len() - 1;
401        if b.is_ascii_digit() || (last && b == b'X') {
402            out.push(b);
403        } else if last && b == b'x' {
404            out.push(b'X');
405        } else {
406            return Err(RgError::InvalidCharacter);
407        }
408    }
409    Ok(out)
410}
411
412fn format_with_spec(body: &str, spec: UfSpec) -> Option<String> {
413    if spec.separators.is_empty() {
414        return None;
415    }
416    let body_len = spec.body_len? as usize;
417    if body.len() != body_len {
418        return None;
419    }
420    let total = body_len + spec.separators.len();
421    let mut out = String::with_capacity(total);
422    let mut sep_iter = spec.separators.iter().peekable();
423    for (i, ch) in body.chars().enumerate() {
424        while let Some(&&(pos, sep_ch)) = sep_iter.peek() {
425            if pos as usize == i && i != 0 {
426                out.push(sep_ch);
427                sep_iter.next();
428            } else {
429                break;
430            }
431        }
432        out.push(ch);
433    }
434    Some(out)
435}
436
437fn sp_check_digit(digits: &[u8]) -> u8 {
438    let sum: u32 = digits
439        .iter()
440        .zip(SP_WEIGHTS.iter())
441        .map(|(&d, &w)| u32::from(d) * w)
442        .sum();
443    (sum % 11) as u8
444}
445
446fn sp_check_digit_ok(body: &str) -> bool {
447    sp_check_digit_ok_bytes(body.as_bytes())
448}
449
450fn sp_check_digit_ok_bytes(bytes: &[u8]) -> bool {
451    if bytes.len() != SP_BODY_LEN as usize {
452        return false;
453    }
454    let mut digits = [0u8; SP_BASE_LEN];
455    for (i, &b) in bytes[..SP_BASE_LEN].iter().enumerate() {
456        if !b.is_ascii_digit() {
457            return false;
458        }
459        digits[i] = b - b'0';
460    }
461    let expected = sp_check_digit(&digits);
462    let last = bytes[SP_BASE_LEN];
463    let actual = if last == b'X' {
464        10
465    } else if last.is_ascii_digit() {
466        last - b'0'
467    } else {
468        return false;
469    };
470    actual == expected
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476    use alloc::string::ToString;
477
478    #[test]
479    fn real_rg_29465327_2() {
480        // Source: bosontreinamentos.com.br
481        assert!(is_valid("294653272", State::SP));
482        assert!(is_valid("29.465.327-2", State::SP));
483        let parsed = parse_strict("294653272", State::SP).unwrap();
484        assert_eq!(parsed.as_str(), "294653272");
485        assert_eq!(parsed.check_digit(), Some(2));
486        assert_eq!(parsed.formatted(), "29.465.327-2");
487        let parsed_fmt = parse_strict("29.465.327-2", State::SP).unwrap();
488        assert_eq!(parsed_fmt, parsed);
489    }
490
491    #[test]
492    fn real_rg_39406714_9() {
493        // Source: dev.to/shadowlik
494        assert!(is_valid("394067149", State::SP));
495        assert!(is_valid("39.406.714-9", State::SP));
496        let parsed = parse_strict("394067149", State::SP).unwrap();
497        assert_eq!(parsed.as_str(), "394067149");
498        assert_eq!(parsed.check_digit(), Some(9));
499        assert_eq!(parsed.formatted(), "39.406.714-9");
500        let parsed_fmt = parse_strict("39.406.714-9", State::SP).unwrap();
501        assert_eq!(parsed_fmt, parsed);
502    }
503
504    #[test]
505    fn compute_check_digit_real_rgs() {
506        assert_eq!(compute_check_digit("29465327", State::SP), Some(2));
507        assert_eq!(compute_check_digit("39406714", State::SP), Some(9));
508    }
509
510    #[test]
511    fn format_real_rgs() {
512        assert_eq!(
513            format_rg("294653272", State::SP),
514            Some("29.465.327-2".into())
515        );
516        assert_eq!(
517            format_rg("394067149", State::SP),
518            Some("39.406.714-9".into())
519        );
520    }
521
522    #[test]
523    fn sp_check_digit_known_values() {
524        // 12345678: sum = 9+16+21+24+25+24+21+16 = 156; 156 mod 11 = 2.
525        assert_eq!(sp_check_digit(&[1, 2, 3, 4, 5, 6, 7, 8]), 2);
526        // 44444444: sum = 4*44 = 176; 176 mod 11 = 0.
527        assert_eq!(sp_check_digit(&[4, 4, 4, 4, 4, 4, 4, 4]), 0);
528        // 11111111: sum = 1*44 = 44; 44 mod 11 = 0.
529        assert_eq!(sp_check_digit(&[1, 1, 1, 1, 1, 1, 1, 1]), 0);
530        // 60000000: sum = 6*9 = 54; 54 mod 11 = 10 → 'X'.
531        assert_eq!(sp_check_digit(&[6, 0, 0, 0, 0, 0, 0, 0]), 10);
532    }
533
534    #[test]
535    fn is_valid_sp_accepts_valid() {
536        assert!(is_valid("123456782", State::SP));
537        assert!(is_valid("12.345.678-2", State::SP));
538    }
539
540    #[test]
541    fn is_valid_sp_rejects_wrong_dv() {
542        assert!(!is_valid("123456789", State::SP));
543        assert!(!is_valid("12.345.678-9", State::SP));
544        assert!(!is_valid("294653271", State::SP));
545    }
546
547    #[test]
548    fn is_valid_sp_x_terminator() {
549        assert!(is_valid("60000000X", State::SP));
550        assert!(is_valid("60.000.000-X", State::SP));
551        assert!(is_valid("60.000.000-x", State::SP));
552    }
553
554    #[test]
555    fn is_valid_sp_lenient_strips_garbage() {
556        let s = "123456782";
557        let garbage = alloc::format!("{}!{}@{}#{}", &s[0..2], &s[2..5], &s[5..8], &s[8..9]);
558        assert!(is_valid(&garbage, State::SP));
559    }
560
561    #[test]
562    fn is_valid_sp_rejects_wrong_length() {
563        assert!(!is_valid("", State::SP));
564        assert!(!is_valid("12345", State::SP));
565        assert!(!is_valid("1234567890", State::SP));
566    }
567
568    #[test]
569    fn structural_other_uf_accepts_any_digits_in_range() {
570        assert!(is_valid("12345", State::RJ));
571        assert!(is_valid("1234567890", State::MG));
572        assert!(is_valid("12345678901234", State::PR));
573    }
574
575    #[test]
576    fn structural_other_uf_rejects_too_short_or_long() {
577        assert!(!is_valid("1234", State::RJ));
578        assert!(!is_valid("123456789012345", State::RJ));
579    }
580
581    #[test]
582    fn structural_other_uf_rejects_letters() {
583        assert!(!is_valid("abcdef", State::RJ));
584        assert!(!is_valid("", State::RJ));
585    }
586
587    #[test]
588    fn strict_sp_accepts_unformatted() {
589        assert!(is_valid_strict("123456782", State::SP).is_ok());
590        assert!(is_valid_strict("294653272", State::SP).is_ok());
591    }
592
593    #[test]
594    fn strict_sp_accepts_formatted() {
595        assert!(is_valid_strict("12.345.678-2", State::SP).is_ok());
596        assert!(is_valid_strict("29.465.327-2", State::SP).is_ok());
597    }
598
599    #[test]
600    fn strict_sp_rejects_misplaced_separators() {
601        assert_eq!(
602            is_valid_strict("123.45.678-2", State::SP),
603            Err(RgError::InvalidFormat)
604        );
605        assert_eq!(
606            is_valid_strict("12.345.6782-", State::SP),
607            Err(RgError::InvalidFormat)
608        );
609    }
610
611    #[test]
612    fn strict_sp_rejects_garbage() {
613        assert_eq!(
614            is_valid_strict("12.345.678!2", State::SP),
615            Err(RgError::InvalidFormat)
616        );
617    }
618
619    #[test]
620    fn strict_sp_rejects_bad_dv() {
621        assert_eq!(
622            is_valid_strict("123456789", State::SP),
623            Err(RgError::InvalidCheckDigit)
624        );
625    }
626
627    #[test]
628    fn strict_sp_rejects_x_in_middle() {
629        assert_eq!(
630            is_valid_strict("1234X6782", State::SP),
631            Err(RgError::InvalidCharacter)
632        );
633    }
634
635    #[test]
636    fn strict_other_uf_accepts_digits_only() {
637        assert!(is_valid_strict("1234567", State::RJ).is_ok());
638    }
639
640    #[test]
641    fn strict_other_uf_rejects_separators() {
642        assert!(is_valid_strict("12.345.67", State::RJ).is_err());
643    }
644
645    #[test]
646    fn parse_sp_roundtrip() {
647        let parsed = parse_strict("123456782", State::SP).unwrap();
648        assert_eq!(parsed.as_str(), "123456782");
649        let parsed_fmt = parse_strict("12.345.678-2", State::SP).unwrap();
650        assert_eq!(parsed_fmt, parsed);
651    }
652
653    #[test]
654    fn parse_sp_x_terminator() {
655        let parsed = parse_strict("60.000.000-X", State::SP).unwrap();
656        assert_eq!(parsed.as_str(), "60000000X");
657        assert_eq!(parsed.check_digit(), Some(10));
658    }
659
660    #[test]
661    fn parse_other_uf_returns_digits() {
662        let parsed = parse_strict("1234567", State::RJ).unwrap();
663        assert_eq!(parsed.as_str(), "1234567");
664        assert_eq!(parsed.uf(), State::RJ);
665        assert_eq!(parsed.check_digit(), None);
666    }
667
668    #[test]
669    fn format_sp_inserts_separators() {
670        assert_eq!(
671            format_rg("123456782", State::SP),
672            Some("12.345.678-2".into())
673        );
674        assert_eq!(
675            format_rg("60000000X", State::SP),
676            Some("60.000.000-X".into())
677        );
678    }
679
680    #[test]
681    fn format_sp_passes_through_already_formatted() {
682        assert_eq!(
683            format_rg("12.345.678-2", State::SP),
684            Some("12.345.678-2".into())
685        );
686    }
687
688    #[test]
689    fn format_other_uf_returns_digits_unchanged() {
690        assert_eq!(format_rg("1234567", State::RJ), Some("1234567".into()));
691    }
692
693    #[test]
694    fn format_returns_none_on_bad_length() {
695        assert_eq!(format_rg("12", State::SP), None);
696        assert_eq!(format_rg("12", State::RJ), None);
697    }
698
699    #[test]
700    fn remove_symbols_sp_keeps_x() {
701        assert_eq!(remove_symbols("60.000.000-X", State::SP), "60000000X");
702        assert_eq!(remove_symbols("60.000.000-x", State::SP), "60000000X");
703    }
704
705    #[test]
706    fn remove_symbols_other_uf_drops_letters() {
707        assert_eq!(remove_symbols("12.345-67", State::RJ), "1234567");
708        assert_eq!(remove_symbols("X1234567", State::RJ), "1234567");
709    }
710
711    #[test]
712    fn compute_check_digit_sp() {
713        assert_eq!(compute_check_digit("12345678", State::SP), Some(2));
714        assert_eq!(compute_check_digit("44444444", State::SP), Some(0));
715        assert_eq!(compute_check_digit("60000000", State::SP), Some(10));
716    }
717
718    #[test]
719    fn compute_check_digit_rejects_other_uf() {
720        assert_eq!(compute_check_digit("1234567", State::RJ), None);
721    }
722
723    #[test]
724    fn compute_check_digit_rejects_bad_length() {
725        assert_eq!(compute_check_digit("1234567", State::SP), None);
726        assert_eq!(compute_check_digit("123456789", State::SP), None);
727    }
728
729    #[test]
730    fn generate_produces_valid() {
731        for _ in 0..100 {
732            let rg = generate(State::SP).unwrap();
733            assert!(is_valid(rg.as_str(), State::SP));
734            let parsed = parse_strict(rg.as_str(), State::SP).unwrap();
735            assert_eq!(parsed, rg);
736        }
737    }
738
739    #[test]
740    fn generate_format_roundtrip() {
741        for _ in 0..100 {
742            let rg = generate(State::SP).unwrap();
743            let formatted = rg.formatted();
744            let parsed = parse_strict(&formatted, State::SP).unwrap();
745            assert_eq!(parsed, rg);
746        }
747    }
748
749    #[test]
750    fn seeded_generation_accepts_zero_and_is_deterministic() {
751        let first = generate_with_seed(0, State::SP).unwrap();
752        let second = generate_with_seed(0, State::SP).unwrap();
753        assert_eq!(first, second);
754        assert!(is_valid(first.as_str(), State::SP));
755    }
756
757    #[test]
758    fn generate_ok_others_err() {
759        assert!(generate(State::SP).is_ok());
760        assert_eq!(
761            generate(State::RJ),
762            Err(RgError::UnsupportedUfForGeneration)
763        );
764    }
765
766    #[test]
767    fn masked_sp_real_rgs() {
768        let rg = parse_strict("294653272", State::SP).unwrap();
769        assert_eq!(rg.masked(), "29.***.***-*");
770        let rg = parse_strict("60000000X", State::SP).unwrap();
771        assert_eq!(rg.masked(), "60.***.***-*");
772    }
773
774    #[test]
775    fn masked_other_uf() {
776        let rg = parse_strict("1234567", State::RJ).unwrap();
777        assert_eq!(rg.masked(), "12*****");
778    }
779
780    #[test]
781    fn masked_generated_sp() {
782        let rg = generate(State::SP).unwrap();
783        let m = rg.masked();
784        assert!(m.starts_with(&rg.as_str()[..2]));
785        assert_eq!(m, alloc::format!("{}.***.***-*", &rg.as_str()[..2]));
786    }
787
788    #[test]
789    fn body_sp_real_rgs() {
790        let rg = parse_strict("294653272", State::SP).unwrap();
791        assert_eq!(rg.body(), "29465327");
792        let rg = parse_strict("60000000X", State::SP).unwrap();
793        assert_eq!(rg.body(), "60000000");
794    }
795
796    #[test]
797    fn body_other_uf_returns_full() {
798        let rg = parse_strict("1234567", State::RJ).unwrap();
799        assert_eq!(rg.body(), "1234567");
800    }
801
802    #[test]
803    fn rg_is_copy() {
804        let rg = generate(State::SP).unwrap();
805        let copy = rg;
806        assert_eq!(rg, copy);
807    }
808
809    #[test]
810    fn rg_as_ref_str() {
811        let rg = generate(State::SP).unwrap();
812        let r: &str = rg.as_ref();
813        assert_eq!(r, rg.as_str());
814    }
815
816    #[test]
817    fn debug_format_includes_uf() {
818        let rg = parse_strict("294653272", State::SP).unwrap();
819        let dbg = alloc::format!("{rg:?}");
820        assert!(dbg.starts_with("Rg(SP, "));
821        assert!(dbg.ends_with(')'));
822    }
823
824    #[test]
825    fn display_uses_formatted() {
826        let rg = parse_strict("294653272", State::SP).unwrap();
827        assert_eq!(rg.to_string(), "29.465.327-2");
828    }
829
830    #[test]
831    fn other_uf_display_passes_through() {
832        let rg = parse_strict("1234567", State::RJ).unwrap();
833        assert_eq!(rg.to_string(), "1234567");
834    }
835}