1use alloc::string::String;
8use core::fmt;
9
10use crate::rand::{RandomSource, SeededRng, below_u32, simple_seed};
11use crate::uf::State;
12use crate::util::impl_document_traits;
13
14const CEP_LEN: usize = 8;
15const FORMATTED_DIGIT_POS: [usize; 8] = [0, 1, 2, 3, 4, 6, 7, 8];
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19#[repr(u8)]
20pub enum PostalRegion {
21 GranSaoPaulo = 0,
22 InteriorSaoPaulo = 1,
23 RjEs = 2,
24 Mg = 3,
25 BaSe = 4,
26 PeAlPbRn = 5,
27 CePiMaPaAmAcApRr = 6,
28 DfGoToMtMsRo = 7,
29 PrSc = 8,
30 Rs = 9,
31}
32
33impl PostalRegion {
34 fn from_digit(d: u8) -> Self {
35 assert!(d <= 9, "digit must be 0..=9");
36 unsafe { core::mem::transmute(d) }
38 }
39}
40
41#[derive(Clone, Copy)]
42struct CepRange {
43 start: u32,
44 end: u32,
45}
46
47impl CepRange {
48 const fn new(start: u32, end: u32) -> Self {
49 Self { start, end }
50 }
51
52 const fn len(self) -> u32 {
53 self.end - self.start + 1
54 }
55
56 const fn contains(self, value: u32) -> bool {
57 value >= self.start && value <= self.end
58 }
59}
60
61static CEP_RANGES: [&[CepRange]; 27] = [
64 &[CepRange::new(69_900_000, 69_999_999)], &[CepRange::new(57_000_000, 57_999_999)], &[
67 CepRange::new(69_000_000, 69_299_999),
68 CepRange::new(69_400_000, 69_899_999),
69 ], &[CepRange::new(68_900_000, 68_999_999)], &[CepRange::new(40_000_000, 48_999_999)], &[CepRange::new(60_000_000, 63_999_999)], &[
74 CepRange::new(70_000_000, 72_799_999),
75 CepRange::new(73_000_000, 73_699_999),
76 ], &[CepRange::new(29_000_000, 29_999_999)], &[
79 CepRange::new(72_800_000, 72_999_999),
80 CepRange::new(73_700_000, 76_799_999),
81 ], &[CepRange::new(65_000_000, 65_999_999)], &[CepRange::new(30_000_000, 39_999_999)], &[CepRange::new(79_000_000, 79_999_999)], &[CepRange::new(78_000_000, 78_899_999)], &[CepRange::new(66_000_000, 68_899_999)], &[CepRange::new(58_000_000, 58_999_999)], &[CepRange::new(50_000_000, 56_999_999)], &[CepRange::new(64_000_000, 64_999_999)], &[CepRange::new(80_000_000, 87_999_999)], &[CepRange::new(20_000_000, 28_999_999)], &[CepRange::new(59_000_000, 59_999_999)], &[CepRange::new(76_800_000, 76_999_999)], &[CepRange::new(69_300_000, 69_399_999)], &[CepRange::new(90_000_000, 99_999_999)], &[CepRange::new(88_000_000, 89_999_999)], &[CepRange::new(49_000_000, 49_999_999)], &[CepRange::new(1_000_000, 19_999_999)], &[CepRange::new(77_000_000, 77_999_999)], ];
101
102fn cep_ranges(state: State) -> &'static [CepRange] {
103 CEP_RANGES[state as usize]
104}
105
106fn state_from_cep_value(value: u32) -> Option<State> {
108 crate::uf::ALL
109 .iter()
110 .find(|&&state| cep_ranges(state).iter().any(|range| range.contains(value)))
111 .copied()
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub enum CepError {
116 InvalidLength,
117 InvalidCharacter,
118 InvalidFormat,
119}
120
121impl fmt::Display for CepError {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 f.write_str(match self {
124 Self::InvalidLength => "CEP must contain exactly 8 digits",
125 Self::InvalidCharacter => "CEP contains invalid characters",
126 Self::InvalidFormat => "CEP format must be #####-### or 8 digits",
127 })
128 }
129}
130
131impl core::error::Error for CepError {}
132
133#[derive(Clone, Copy, PartialEq, Eq, Hash)]
146pub struct Cep {
147 bytes: [u8; CEP_LEN],
148}
149
150impl Cep {
151 pub fn as_str(&self) -> &str {
153 unsafe { core::str::from_utf8_unchecked(&self.bytes) }
155 }
156
157 pub fn digits(&self) -> [u8; CEP_LEN] {
159 self.bytes.map(|b| b - b'0')
160 }
161
162 pub fn postal_region(&self) -> PostalRegion {
164 PostalRegion::from_digit(self.bytes[0] - b'0')
165 }
166
167 pub fn state(&self) -> Option<State> {
169 state_from_cep_value(self.as_u32())
170 }
171
172 pub fn formatted(&self) -> String {
174 let s = self.as_str();
175 alloc::format!("{}-{}", &s[0..5], &s[5..8])
176 }
177
178 pub fn masked(&self) -> String {
180 let s = self.as_str();
181 alloc::format!("{}-***", &s[0..5])
182 }
183
184 fn as_u32(self) -> u32 {
186 let d = self.digits();
187 u32::from(d[0]) * 10_000_000
188 + u32::from(d[1]) * 1_000_000
189 + u32::from(d[2]) * 100_000
190 + u32::from(d[3]) * 10_000
191 + u32::from(d[4]) * 1_000
192 + u32::from(d[5]) * 100
193 + u32::from(d[6]) * 10
194 + u32::from(d[7])
195 }
196
197 fn from_numeric(digits: [u8; CEP_LEN]) -> Self {
198 Self {
199 bytes: digits.map(|d| d + b'0'),
200 }
201 }
202}
203
204impl fmt::Display for Cep {
205 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206 let s = self.as_str();
207 write!(f, "{}-{}", &s[0..5], &s[5..8])
208 }
209}
210
211impl_document_traits!(Cep, CepError);
212
213pub fn normalize(cep: &str) -> String {
215 cep.chars().filter(char::is_ascii_digit).collect()
216}
217
218pub fn remove_symbols(cep: &str) -> String {
220 normalize(cep)
221}
222
223pub fn is_valid_lenient(cep: &str) -> bool {
225 normalize(cep).len() == CEP_LEN
226}
227
228pub fn is_valid(cep: &str) -> bool {
232 is_valid_lenient(cep)
233}
234
235pub fn is_valid_strict(cep: &str) -> Result<(), CepError> {
237 parse_strict(cep).map(|_| ())
238}
239
240pub fn format_cep(cep: &str) -> Option<String> {
242 let d = remove_symbols(cep);
243 (d.len() == CEP_LEN).then(|| alloc::format!("{}-{}", &d[0..5], &d[5..8]))
244}
245
246pub fn generate() -> String {
250 generate_cep().as_str().into()
251}
252
253pub fn generate_cep() -> Cep {
257 let mut rng = SeededRng::new(simple_seed());
258 generate_cep_with_rng(&mut rng)
259}
260
261pub fn generate_cep_with_seed(seed: u64) -> Cep {
265 let mut rng = SeededRng::new(seed);
266 generate_cep_with_rng(&mut rng)
267}
268
269pub fn generate_cep_with_rng<R: RandomSource + ?Sized>(rng: &mut R) -> Cep {
273 cep_from_value(below_u32(rng, 100_000_000))
274}
275
276pub fn generate_for_region(region: PostalRegion) -> Cep {
280 let mut rng = SeededRng::new(simple_seed());
281 generate_for_region_with_rng(&mut rng, region)
282}
283
284pub fn generate_for_region_with_seed(seed: u64, region: PostalRegion) -> Cep {
288 let mut rng = SeededRng::new(seed);
289 generate_for_region_with_rng(&mut rng, region)
290}
291
292pub fn generate_for_region_with_rng<R: RandomSource + ?Sized>(
296 rng: &mut R,
297 region: PostalRegion,
298) -> Cep {
299 let value = u32::from(region as u8) * 10_000_000 + below_u32(rng, 10_000_000);
300 cep_from_value(value)
301}
302
303pub fn generate_for_state(state: State) -> Cep {
308 let mut rng = SeededRng::new(simple_seed());
309 generate_for_state_with_rng(&mut rng, state)
310}
311
312pub fn generate_for_state_with_seed(seed: u64, state: State) -> Cep {
317 let mut rng = SeededRng::new(seed);
318 generate_for_state_with_rng(&mut rng, state)
319}
320
321pub fn generate_for_state_with_rng<R: RandomSource + ?Sized>(rng: &mut R, state: State) -> Cep {
327 let ranges = cep_ranges(state);
328 let total: u32 = ranges.iter().map(|range| range.len()).sum();
329 let mut offset = below_u32(rng, total);
330
331 for range in ranges {
332 let len = range.len();
333 if offset < len {
334 return cep_from_value(range.start + offset);
335 }
336 offset -= len;
337 }
338
339 unreachable!("CEP range offset must select a segment")
340}
341
342fn cep_from_value(mut value: u32) -> Cep {
343 let mut digits = [0u8; CEP_LEN];
344 for i in (0..CEP_LEN).rev() {
345 digits[i] = (value % 10) as u8;
346 value /= 10;
347 }
348 Cep::from_numeric(digits)
349}
350
351fn parse_strict(s: &str) -> Result<Cep, CepError> {
352 let raw = s.as_bytes();
353
354 match raw.len() {
355 8 => {
356 if !raw.iter().all(u8::is_ascii_digit) {
357 return Err(CepError::InvalidCharacter);
358 }
359 }
360 9 => {
361 if raw[5] != b'-' {
362 return Err(CepError::InvalidFormat);
363 }
364 for &i in &FORMATTED_DIGIT_POS {
365 if !raw[i].is_ascii_digit() {
366 return Err(CepError::InvalidCharacter);
367 }
368 }
369 }
370 _ => return Err(CepError::InvalidLength),
371 }
372
373 let mut digits = [0u8; CEP_LEN];
374 for (idx, &pos) in FORMATTED_DIGIT_POS.iter().enumerate() {
375 if pos < raw.len() {
376 digits[idx] = raw[pos] - b'0';
377 }
378 }
379
380 if raw.len() == 8 {
382 for (i, &b) in raw.iter().enumerate() {
383 digits[i] = b - b'0';
384 }
385 }
386
387 Ok(Cep::from_numeric(digits))
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393 use alloc::string::ToString;
394
395 fn cep_sp() -> Cep {
396 Cep::from_numeric([0, 1, 3, 1, 0, 1, 0, 0])
398 }
399
400 fn cep_rj() -> Cep {
401 Cep::from_numeric([2, 0, 0, 4, 0, 0, 2, 0])
403 }
404
405 fn cep_rs() -> Cep {
406 Cep::from_numeric([9, 0, 0, 1, 0, 0, 0, 0])
408 }
409
410 #[test]
411 fn is_valid_accepts_valid_unformatted() {
412 assert!(is_valid(cep_sp().as_str()));
413 assert!(is_valid(cep_rj().as_str()));
414 assert!(is_valid(cep_rs().as_str()));
415 }
416
417 #[test]
418 fn is_valid_accepts_valid_formatted() {
419 assert!(is_valid(cep_sp().as_ref()));
420 assert!(is_valid(cep_rj().as_ref()));
421 }
422
423 #[test]
424 fn is_valid_lenient_strips_garbage() {
425 let cep = cep_sp();
426 let s = cep.as_str();
427 let garbage = alloc::format!("{}${}", &s[0..5], &s[5..8]);
428 assert!(is_valid(&garbage));
429
430 let padded = alloc::format!(" {} ", cep_sp());
431 assert!(is_valid(&padded));
432 }
433
434 #[test]
435 fn is_valid_rejects_wrong_length() {
436 assert!(!is_valid(""));
437 assert!(!is_valid("1234567"));
438 assert!(!is_valid("123456789"));
439 }
440
441 #[test]
442 fn is_valid_rejects_no_digits() {
443 assert!(!is_valid("abcdefgh"));
444 assert!(!is_valid("...---"));
445 }
446
447 #[test]
448 fn strict_accepts_valid_unformatted() {
449 assert!(is_valid_strict(cep_sp().as_str()).is_ok());
450 assert!(is_valid_strict(cep_rj().as_str()).is_ok());
451 }
452
453 #[test]
454 fn strict_accepts_valid_formatted() {
455 assert!(is_valid_strict(cep_sp().as_ref()).is_ok());
456 assert!(is_valid_strict(cep_rj().as_ref()).is_ok());
457 }
458
459 #[test]
460 fn strict_rejects_garbage_between_digits() {
461 let cep = cep_sp();
462 let s = cep.as_str();
463 let garbage = alloc::format!("{}${}", &s[0..5], &s[5..8]);
464 assert!(is_valid_strict(&garbage).is_err());
465 }
466
467 #[test]
468 fn strict_rejects_whitespace() {
469 let padded = alloc::format!(" {} ", cep_sp().as_str());
470 assert_eq!(is_valid_strict(&padded), Err(CepError::InvalidLength));
471 }
472
473 #[test]
474 fn strict_rejects_misplaced_separators() {
475 assert_eq!(is_valid_strict("0131-0100"), Err(CepError::InvalidFormat));
476 }
477
478 #[test]
479 fn strict_rejects_letters() {
480 assert_eq!(is_valid_strict("abcdefgh"), Err(CepError::InvalidCharacter));
481 }
482
483 #[test]
484 fn parse_roundtrip() {
485 let cep = cep_sp();
486 let parsed: Cep = cep.to_string().parse().unwrap();
487 assert_eq!(cep, parsed);
488 assert_eq!(parsed.as_str(), cep.as_str());
489 }
490
491 #[test]
492 fn parse_unformatted() {
493 let cep = cep_sp();
494 let parsed: Cep = cep.as_str().parse().unwrap();
495 assert_eq!(cep, parsed);
496 }
497
498 #[test]
499 fn parse_equality_across_formats() {
500 let from_fmt: Cep = cep_sp().to_string().parse().unwrap();
501 let from_raw: Cep = cep_sp().as_str().parse().unwrap();
502 assert_eq!(from_fmt, from_raw);
503 }
504
505 #[test]
506 fn cep_is_copy() {
507 let a = cep_sp();
508 let b = a;
509 assert_eq!(a, b);
510 }
511
512 #[test]
513 fn cep_as_ref_str() {
514 let cep = cep_sp();
515 let r: &str = cep.as_ref();
516 assert_eq!(r, cep.as_str());
517 }
518
519 #[test]
520 fn debug_format() {
521 let cep = cep_sp();
522 let dbg = alloc::format!("{cep:?}");
523 assert!(dbg.starts_with("Cep("));
524 assert!(dbg.ends_with(')'));
525 assert!(dbg.contains('-'));
526 }
527
528 #[test]
529 fn postal_region() {
530 assert_eq!(cep_sp().postal_region(), PostalRegion::GranSaoPaulo);
531 assert_eq!(cep_rj().postal_region(), PostalRegion::RjEs);
532 assert_eq!(cep_rs().postal_region(), PostalRegion::Rs);
533 }
534
535 #[test]
536 fn state_lookup() {
537 assert_eq!(cep_sp().state(), Some(State::SP));
538 assert_eq!(cep_rj().state(), Some(State::RJ));
539 assert_eq!(cep_rs().state(), Some(State::RS));
540 }
541
542 #[test]
543 fn state_abbreviation() {
544 assert_eq!(State::SP.abbreviation(), "SP");
545 assert_eq!(State::RJ.abbreviation(), "RJ");
546 assert_eq!(State::RS.abbreviation(), "RS");
547 assert_eq!(State::DF.abbreviation(), "DF");
548 }
549
550 #[test]
551 fn formatted() {
552 assert_eq!(cep_sp().formatted(), "01310-100");
553 assert_eq!(cep_rj().formatted(), "20040-020");
554 }
555
556 #[test]
557 fn masked() {
558 assert_eq!(cep_sp().masked(), "01310-***");
559 assert_eq!(cep_rj().masked(), "20040-***");
560 }
561
562 #[test]
563 fn remove_symbols_strips_formatting() {
564 let cep = cep_sp();
565 let formatted = cep.to_string();
566 assert_eq!(remove_symbols(&formatted), cep.as_str());
567 assert_eq!(remove_symbols(cep.as_str()), cep.as_str());
568 assert_eq!(remove_symbols(""), "");
569 }
570
571 #[test]
572 fn format_cep_produces_formatted_output() {
573 let cep = cep_sp();
574 let formatted = cep.to_string();
575 assert_eq!(format_cep(cep.as_str()), Some(formatted.clone()));
576 assert_eq!(format_cep(&formatted), Some(formatted));
577 }
578
579 #[test]
580 fn format_cep_returns_none_on_bad_length() {
581 assert_eq!(format_cep("1234"), None);
582 assert_eq!(format_cep(""), None);
583 }
584
585 #[test]
586 fn format_cep_preserves_leading_zeros() {
587 let cep = cep_sp();
588 let formatted = format_cep(cep.as_str()).unwrap();
589 assert!(formatted.starts_with("01"));
590 }
591
592 #[test]
593 fn generate_produces_valid_ceps() {
594 for _ in 0..100 {
595 let cep = generate();
596 assert_eq!(cep.len(), 8);
597 assert!(is_valid(&cep), "generated invalid CEP: {cep}");
598 }
599 }
600
601 #[test]
602 fn generate_cep_roundtrips() {
603 for _ in 0..100 {
604 let cep = generate_cep();
605 assert!(is_valid(cep.as_str()));
606 let parsed: Cep = cep.as_str().parse().unwrap();
607 assert_eq!(cep, parsed);
608 }
609 }
610
611 #[test]
612 fn generate_for_region_respects_first_digit() {
613 let regions = [
614 PostalRegion::GranSaoPaulo,
615 PostalRegion::InteriorSaoPaulo,
616 PostalRegion::RjEs,
617 PostalRegion::Mg,
618 PostalRegion::BaSe,
619 PostalRegion::PeAlPbRn,
620 PostalRegion::CePiMaPaAmAcApRr,
621 PostalRegion::DfGoToMtMsRo,
622 PostalRegion::PrSc,
623 PostalRegion::Rs,
624 ];
625 for region in regions {
626 let cep = generate_for_region(region);
627 assert_eq!(cep.postal_region(), region);
628 assert!(is_valid(cep.as_str()));
629 }
630 }
631
632 #[test]
633 fn generate_for_state_within_range() {
634 let states = [
635 State::SP,
636 State::RJ,
637 State::MG,
638 State::RS,
639 State::DF,
640 State::AM,
641 State::AC,
642 ];
643 for state in states {
644 for _ in 0..10 {
645 let cep = generate_for_state(state);
646 assert_eq!(
647 cep.state(),
648 Some(state),
649 "CEP {cep} should map to {state:?}"
650 );
651 }
652 }
653 }
654
655 #[test]
656 fn leading_zero_cep() {
657 let cep = cep_sp();
658 assert!(cep.as_str().starts_with('0'));
659 assert!(is_valid(cep.as_str()));
660
661 let parsed: Cep = cep.as_str().parse().unwrap();
662 assert_eq!(parsed.digits()[0], 0);
663 }
664}