1use alloc::string::String;
8use core::fmt;
9
10use crate::rand::{simple_seed, xorshift64};
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
41fn cep_range(state: State) -> (u32, u32) {
43 match state {
44 State::SP => (1_000_000, 19_999_999),
45 State::RJ => (20_000_000, 28_999_999),
46 State::ES => (29_000_000, 29_999_999),
47 State::MG => (30_000_000, 39_999_999),
48 State::BA => (40_000_000, 48_999_999),
49 State::SE => (49_000_000, 49_999_999),
50 State::PE => (50_000_000, 56_999_999),
51 State::AL => (57_000_000, 57_999_999),
52 State::PB => (58_000_000, 58_999_999),
53 State::RN => (59_000_000, 59_999_999),
54 State::CE => (60_000_000, 63_999_999),
55 State::PI => (64_000_000, 64_999_999),
56 State::MA => (65_000_000, 65_999_999),
57 State::PA => (66_000_000, 68_899_999),
58 State::AM => (69_000_000, 69_299_999),
59 State::AC => (69_900_000, 69_999_999),
60 State::AP => (68_900_000, 68_999_999),
61 State::RR => (69_300_000, 69_399_999),
62 State::DF => (70_000_000, 72_799_999),
63 State::GO => (72_800_000, 76_799_999),
64 State::TO => (77_000_000, 77_999_999),
65 State::MT => (78_000_000, 78_899_999),
66 State::MS => (79_000_000, 79_999_999),
67 State::RO => (76_800_000, 76_999_999),
68 State::PR => (80_000_000, 87_999_999),
69 State::SC => (88_000_000, 89_999_999),
70 State::RS => (90_000_000, 99_999_999),
71 }
72}
73
74fn state_from_cep_value(value: u32) -> Option<State> {
76 for &state in &crate::uf::ALL {
77 let (start, end) = cep_range(state);
78 if value >= start && value <= end {
79 return Some(state);
80 }
81 }
82 None
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum CepError {
87 InvalidLength,
88 InvalidCharacter,
89 InvalidFormat,
90}
91
92impl fmt::Display for CepError {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 f.write_str(match self {
95 Self::InvalidLength => "CEP must contain exactly 8 digits",
96 Self::InvalidCharacter => "CEP contains invalid characters",
97 Self::InvalidFormat => "CEP format must be #####-### or 8 digits",
98 })
99 }
100}
101
102#[derive(Clone, Copy, PartialEq, Eq, Hash)]
115pub struct Cep {
116 bytes: [u8; CEP_LEN],
117}
118
119impl Cep {
120 pub fn as_str(&self) -> &str {
122 unsafe { core::str::from_utf8_unchecked(&self.bytes) }
124 }
125
126 pub fn digits(&self) -> [u8; CEP_LEN] {
128 self.bytes.map(|b| b - b'0')
129 }
130
131 pub fn postal_region(&self) -> PostalRegion {
133 PostalRegion::from_digit(self.bytes[0] - b'0')
134 }
135
136 pub fn state(&self) -> Option<State> {
138 state_from_cep_value(self.as_u32())
139 }
140
141 pub fn formatted(&self) -> String {
143 let s = self.as_str();
144 alloc::format!("{}-{}", &s[0..5], &s[5..8])
145 }
146
147 pub fn masked(&self) -> String {
149 let s = self.as_str();
150 alloc::format!("{}-***", &s[0..5])
151 }
152
153 fn as_u32(self) -> u32 {
155 let d = self.digits();
156 u32::from(d[0]) * 10_000_000
157 + u32::from(d[1]) * 1_000_000
158 + u32::from(d[2]) * 100_000
159 + u32::from(d[3]) * 10_000
160 + u32::from(d[4]) * 1_000
161 + u32::from(d[5]) * 100
162 + u32::from(d[6]) * 10
163 + u32::from(d[7])
164 }
165
166 fn from_numeric(digits: [u8; CEP_LEN]) -> Self {
167 Self {
168 bytes: digits.map(|d| d + b'0'),
169 }
170 }
171}
172
173impl fmt::Display for Cep {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 let s = self.as_str();
176 write!(f, "{}-{}", &s[0..5], &s[5..8])
177 }
178}
179
180impl_document_traits!(Cep, CepError);
181
182pub fn remove_symbols(cep: &str) -> String {
183 cep.chars().filter(char::is_ascii_digit).collect()
184}
185
186pub fn is_valid(cep: &str) -> bool {
187 let raw = remove_symbols(cep);
188 raw.len() == CEP_LEN
189}
190
191pub fn is_valid_strict(cep: &str) -> Result<(), CepError> {
193 parse_strict(cep).map(|_| ())
194}
195
196pub fn format_cep(cep: &str) -> Option<String> {
198 let d = remove_symbols(cep);
199 (d.len() == CEP_LEN).then(|| alloc::format!("{}-{}", &d[0..5], &d[5..8]))
200}
201
202pub fn generate() -> String {
204 generate_cep().as_str().into()
205}
206
207pub fn generate_cep() -> Cep {
209 let mut seed = simple_seed();
210 let mut digits = [0u8; CEP_LEN];
211 for d in &mut digits {
212 seed = xorshift64(seed);
213 *d = (seed % 10) as u8;
214 }
215 Cep::from_numeric(digits)
216}
217
218pub fn generate_for_region(region: PostalRegion) -> Cep {
220 let mut seed = simple_seed();
221 let mut digits = [0u8; CEP_LEN];
222 digits[0] = region as u8;
223 for d in &mut digits[1..] {
224 seed = xorshift64(seed);
225 *d = (seed % 10) as u8;
226 }
227 Cep::from_numeric(digits)
228}
229
230pub fn generate_for_state(state: State) -> Cep {
232 let (start, end) = cep_range(state);
233 let mut seed = simple_seed();
234 seed = xorshift64(seed);
235 let range = end - start + 1;
236 #[allow(clippy::cast_possible_truncation)]
237 let value = start + (seed as u32 % range);
238
239 let mut digits = [0u8; CEP_LEN];
240 let mut v = value;
241 for i in (0..CEP_LEN).rev() {
242 digits[i] = (v % 10) as u8;
243 v /= 10;
244 }
245 Cep::from_numeric(digits)
246}
247
248fn parse_strict(s: &str) -> Result<Cep, CepError> {
249 let raw = s.as_bytes();
250
251 match raw.len() {
252 8 => {
253 if !raw.iter().all(u8::is_ascii_digit) {
254 return Err(CepError::InvalidCharacter);
255 }
256 }
257 9 => {
258 if raw[5] != b'-' {
259 return Err(CepError::InvalidFormat);
260 }
261 for &i in &FORMATTED_DIGIT_POS {
262 if !raw[i].is_ascii_digit() {
263 return Err(CepError::InvalidCharacter);
264 }
265 }
266 }
267 _ => return Err(CepError::InvalidLength),
268 }
269
270 let mut digits = [0u8; CEP_LEN];
271 for (idx, &pos) in FORMATTED_DIGIT_POS.iter().enumerate() {
272 if pos < raw.len() {
273 digits[idx] = raw[pos] - b'0';
274 }
275 }
276
277 if raw.len() == 8 {
279 for (i, &b) in raw.iter().enumerate() {
280 digits[i] = b - b'0';
281 }
282 }
283
284 Ok(Cep::from_numeric(digits))
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use alloc::string::ToString;
291
292 fn cep_sp() -> Cep {
293 Cep::from_numeric([0, 1, 3, 1, 0, 1, 0, 0])
295 }
296
297 fn cep_rj() -> Cep {
298 Cep::from_numeric([2, 0, 0, 4, 0, 0, 2, 0])
300 }
301
302 fn cep_rs() -> Cep {
303 Cep::from_numeric([9, 0, 0, 1, 0, 0, 0, 0])
305 }
306
307 #[test]
308 fn is_valid_accepts_valid_unformatted() {
309 assert!(is_valid(cep_sp().as_str()));
310 assert!(is_valid(cep_rj().as_str()));
311 assert!(is_valid(cep_rs().as_str()));
312 }
313
314 #[test]
315 fn is_valid_accepts_valid_formatted() {
316 assert!(is_valid(cep_sp().as_ref()));
317 assert!(is_valid(cep_rj().as_ref()));
318 }
319
320 #[test]
321 fn is_valid_lenient_strips_garbage() {
322 let cep = cep_sp();
323 let s = cep.as_str();
324 let garbage = alloc::format!("{}${}", &s[0..5], &s[5..8]);
325 assert!(is_valid(&garbage));
326
327 let padded = alloc::format!(" {} ", cep_sp());
328 assert!(is_valid(&padded));
329 }
330
331 #[test]
332 fn is_valid_rejects_wrong_length() {
333 assert!(!is_valid(""));
334 assert!(!is_valid("1234567"));
335 assert!(!is_valid("123456789"));
336 }
337
338 #[test]
339 fn is_valid_rejects_no_digits() {
340 assert!(!is_valid("abcdefgh"));
341 assert!(!is_valid("...---"));
342 }
343
344 #[test]
345 fn strict_accepts_valid_unformatted() {
346 assert!(is_valid_strict(cep_sp().as_str()).is_ok());
347 assert!(is_valid_strict(cep_rj().as_str()).is_ok());
348 }
349
350 #[test]
351 fn strict_accepts_valid_formatted() {
352 assert!(is_valid_strict(cep_sp().as_ref()).is_ok());
353 assert!(is_valid_strict(cep_rj().as_ref()).is_ok());
354 }
355
356 #[test]
357 fn strict_rejects_garbage_between_digits() {
358 let cep = cep_sp();
359 let s = cep.as_str();
360 let garbage = alloc::format!("{}${}", &s[0..5], &s[5..8]);
361 assert!(is_valid_strict(&garbage).is_err());
362 }
363
364 #[test]
365 fn strict_rejects_whitespace() {
366 let padded = alloc::format!(" {} ", cep_sp().as_str());
367 assert_eq!(is_valid_strict(&padded), Err(CepError::InvalidLength));
368 }
369
370 #[test]
371 fn strict_rejects_misplaced_separators() {
372 assert_eq!(is_valid_strict("0131-0100"), Err(CepError::InvalidFormat));
373 }
374
375 #[test]
376 fn strict_rejects_letters() {
377 assert_eq!(is_valid_strict("abcdefgh"), Err(CepError::InvalidCharacter));
378 }
379
380 #[test]
381 fn parse_roundtrip() {
382 let cep = cep_sp();
383 let parsed: Cep = cep.to_string().parse().unwrap();
384 assert_eq!(cep, parsed);
385 assert_eq!(parsed.as_str(), cep.as_str());
386 }
387
388 #[test]
389 fn parse_unformatted() {
390 let cep = cep_sp();
391 let parsed: Cep = cep.as_str().parse().unwrap();
392 assert_eq!(cep, parsed);
393 }
394
395 #[test]
396 fn parse_equality_across_formats() {
397 let from_fmt: Cep = cep_sp().to_string().parse().unwrap();
398 let from_raw: Cep = cep_sp().as_str().parse().unwrap();
399 assert_eq!(from_fmt, from_raw);
400 }
401
402 #[test]
403 fn cep_is_copy() {
404 let a = cep_sp();
405 let b = a;
406 assert_eq!(a, b);
407 }
408
409 #[test]
410 fn cep_as_ref_str() {
411 let cep = cep_sp();
412 let r: &str = cep.as_ref();
413 assert_eq!(r, cep.as_str());
414 }
415
416 #[test]
417 fn debug_format() {
418 let cep = cep_sp();
419 let dbg = alloc::format!("{cep:?}");
420 assert!(dbg.starts_with("Cep("));
421 assert!(dbg.ends_with(')'));
422 assert!(dbg.contains('-'));
423 }
424
425 #[test]
426 fn postal_region() {
427 assert_eq!(cep_sp().postal_region(), PostalRegion::GranSaoPaulo);
428 assert_eq!(cep_rj().postal_region(), PostalRegion::RjEs);
429 assert_eq!(cep_rs().postal_region(), PostalRegion::Rs);
430 }
431
432 #[test]
433 fn state_lookup() {
434 assert_eq!(cep_sp().state(), Some(State::SP));
435 assert_eq!(cep_rj().state(), Some(State::RJ));
436 assert_eq!(cep_rs().state(), Some(State::RS));
437 }
438
439 #[test]
440 fn state_abbreviation() {
441 assert_eq!(State::SP.abbreviation(), "SP");
442 assert_eq!(State::RJ.abbreviation(), "RJ");
443 assert_eq!(State::RS.abbreviation(), "RS");
444 assert_eq!(State::DF.abbreviation(), "DF");
445 }
446
447 #[test]
448 fn formatted() {
449 assert_eq!(cep_sp().formatted(), "01310-100");
450 assert_eq!(cep_rj().formatted(), "20040-020");
451 }
452
453 #[test]
454 fn masked() {
455 assert_eq!(cep_sp().masked(), "01310-***");
456 assert_eq!(cep_rj().masked(), "20040-***");
457 }
458
459 #[test]
460 fn remove_symbols_strips_formatting() {
461 let cep = cep_sp();
462 let formatted = cep.to_string();
463 assert_eq!(remove_symbols(&formatted), cep.as_str());
464 assert_eq!(remove_symbols(cep.as_str()), cep.as_str());
465 assert_eq!(remove_symbols(""), "");
466 }
467
468 #[test]
469 fn format_cep_produces_formatted_output() {
470 let cep = cep_sp();
471 let formatted = cep.to_string();
472 assert_eq!(format_cep(cep.as_str()), Some(formatted.clone()));
473 assert_eq!(format_cep(&formatted), Some(formatted));
474 }
475
476 #[test]
477 fn format_cep_returns_none_on_bad_length() {
478 assert_eq!(format_cep("1234"), None);
479 assert_eq!(format_cep(""), None);
480 }
481
482 #[test]
483 fn format_cep_preserves_leading_zeros() {
484 let cep = cep_sp();
485 let formatted = format_cep(cep.as_str()).unwrap();
486 assert!(formatted.starts_with("01"));
487 }
488
489 #[test]
490 fn generate_produces_valid_ceps() {
491 for _ in 0..100 {
492 let cep = generate();
493 assert_eq!(cep.len(), 8);
494 assert!(is_valid(&cep), "generated invalid CEP: {cep}");
495 }
496 }
497
498 #[test]
499 fn generate_cep_roundtrips() {
500 for _ in 0..100 {
501 let cep = generate_cep();
502 assert!(is_valid(cep.as_str()));
503 let parsed: Cep = cep.as_str().parse().unwrap();
504 assert_eq!(cep, parsed);
505 }
506 }
507
508 #[test]
509 fn generate_for_region_respects_first_digit() {
510 let regions = [
511 PostalRegion::GranSaoPaulo,
512 PostalRegion::InteriorSaoPaulo,
513 PostalRegion::RjEs,
514 PostalRegion::Mg,
515 PostalRegion::BaSe,
516 PostalRegion::PeAlPbRn,
517 PostalRegion::CePiMaPaAmAcApRr,
518 PostalRegion::DfGoToMtMsRo,
519 PostalRegion::PrSc,
520 PostalRegion::Rs,
521 ];
522 for region in regions {
523 let cep = generate_for_region(region);
524 assert_eq!(cep.postal_region(), region);
525 assert!(is_valid(cep.as_str()));
526 }
527 }
528
529 #[test]
530 fn generate_for_state_within_range() {
531 let states = [
532 State::SP,
533 State::RJ,
534 State::MG,
535 State::RS,
536 State::DF,
537 State::AM,
538 State::AC,
539 ];
540 for state in states {
541 for _ in 0..10 {
542 let cep = generate_for_state(state);
543 assert_eq!(
544 cep.state(),
545 Some(state),
546 "CEP {cep} should map to {state:?}"
547 );
548 }
549 }
550 }
551
552 #[test]
553 fn leading_zero_cep() {
554 let cep = cep_sp();
555 assert!(cep.as_str().starts_with('0'));
556 assert!(is_valid(cep.as_str()));
557
558 let parsed: Cep = cep.as_str().parse().unwrap();
559 assert_eq!(parsed.digits()[0], 0);
560 }
561}