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