1use std::fmt;
2use std::ops::Range;
3use std::str::FromStr;
4
5use Segment::{Area, District, Lga, State, Unit};
6
7const LEN: usize = 11;
8
9#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
14pub struct Postcode([u8; LEN]);
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub enum Segment {
19 State,
21 Lga,
23 District,
25 Area,
27 Unit,
29}
30
31#[derive(Clone, Debug, PartialEq, Eq)]
33#[non_exhaustive]
34pub enum ParseError {
35 Length { found: usize },
37 PrefixLength { found: usize },
39 InvalidCharacter { ch: char, index: usize },
41 Segment(Segment),
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47#[non_exhaustive]
48pub struct Corrected {
49 pub postcode: Postcode,
50 pub corrections: usize,
52}
53
54impl Postcode {
55 pub fn parse(input: &str) -> Result<Self, ParseError> {
57 collect(input).and_then(validate).map(Self)
58 }
59
60 pub fn parse_lenient(input: &str) -> Result<Corrected, ParseError> {
67 let raw = collect(input)?;
68 let fixed: [u8; LEN] =
69 std::array::from_fn(|i| Segment::at(i).map_or(raw[i], |s| s.unconfuse(raw[i])));
70 let corrections = raw.iter().zip(&fixed).filter(|(a, b)| a != b).count();
71 validate(fixed).map(|bytes| Corrected {
72 postcode: Self(bytes),
73 corrections,
74 })
75 }
76
77 pub fn from_segments(
80 state: &str,
81 lga: &str,
82 district: &str,
83 area: &str,
84 unit: &str,
85 ) -> Result<Self, ParseError> {
86 let parts = [
87 State.padded(state)?,
88 Lga.padded(lga)?,
89 District.padded(district)?,
90 Area.padded(area)?,
91 Unit.padded(unit)?,
92 ];
93 Self::parse(&parts.concat())
94 }
95
96 pub fn as_str(&self) -> &str {
98 std::str::from_utf8(&self.0).expect("postcode bytes are ASCII")
99 }
100
101 pub fn to_spaced(&self) -> String {
103 self.joined(Unit, " ")
104 }
105
106 pub fn prefix(&self, through: Segment) -> String {
109 self.joined(through, "-")
110 }
111
112 pub fn truncate(&self, through: Segment) -> Prefix {
115 let mut bytes = self.0;
116 bytes[through.range().end..].fill(0);
117 Prefix { bytes, through }
118 }
119
120 pub fn segment(&self, segment: Segment) -> &str {
121 &self.as_str()[segment.range()]
122 }
123
124 pub fn state(&self) -> &str {
125 self.segment(State)
126 }
127
128 pub fn lga(&self) -> &str {
129 self.segment(Lga)
130 }
131
132 pub fn district(&self) -> &str {
133 self.segment(District)
134 }
135
136 pub fn area(&self) -> &str {
137 self.segment(Area)
138 }
139
140 pub fn unit(&self) -> &str {
141 self.segment(Unit)
142 }
143
144 fn joined(&self, through: Segment, separator: &str) -> String {
145 Segment::ALL
146 .into_iter()
147 .take_while(|&s| s <= through)
148 .map(|s| self.segment(s))
149 .collect::<Vec<_>>()
150 .join(separator)
151 }
152}
153
154#[derive(Clone, Copy, PartialEq, Eq, Hash)]
159pub struct Prefix {
160 bytes: [u8; LEN],
162 through: Segment,
163}
164
165impl Prefix {
166 pub fn parse(input: &str) -> Result<Self, ParseError> {
168 let (bytes, found) = scan(input)?;
169 let through = Segment::ALL
170 .into_iter()
171 .find(|s| s.range().end == found)
172 .ok_or(ParseError::PrefixLength { found })?;
173 match (Segment::ALL.into_iter())
174 .take_while(|&s| s <= through)
175 .find(|s| !s.accepts(&bytes[s.range()]))
176 {
177 Some(segment) => Err(ParseError::Segment(segment)),
178 None => Ok(Self { bytes, through }),
179 }
180 }
181
182 pub fn through(&self) -> Segment {
184 self.through
185 }
186
187 pub fn as_str(&self) -> &str {
189 std::str::from_utf8(&self.bytes[..self.through.range().end])
190 .expect("prefix bytes are ASCII")
191 }
192
193 pub fn contains(&self, code: &Postcode) -> bool {
195 code.truncate(self.through) == *self
196 }
197
198 pub fn parent(&self) -> Option<Self> {
200 let through = (Segment::ALL.into_iter()).rfind(|&s| s < self.through)?;
201 Some(Postcode(self.bytes).truncate(through))
202 }
203}
204
205pub fn is_valid(input: &str) -> bool {
207 Postcode::parse(input).is_ok()
208}
209
210impl Segment {
211 const ALL: [Self; 5] = [State, Lga, District, Area, Unit];
212
213 const fn range(self) -> Range<usize> {
214 match self {
215 State => 0..2,
216 Lga => 2..4,
217 District => 4..7,
218 Area => 7..9,
219 Unit => 9..11,
220 }
221 }
222
223 fn at(index: usize) -> Option<Self> {
224 Self::ALL.into_iter().find(|s| s.range().contains(&index))
225 }
226
227 fn accepts(self, bytes: &[u8]) -> bool {
228 match self {
229 State | Area => bytes.iter().all(u8::is_ascii_uppercase),
230 Lga | Unit => bytes.iter().all(u8::is_ascii_digit) && bytes != b"00",
232 District => bytes.iter().all(u8::is_ascii_alphanumeric),
233 }
234 }
235
236 fn unconfuse(self, byte: u8) -> u8 {
237 match (self, byte) {
238 (State | Area, b'0') => b'O',
239 (State | Area, b'1') => b'I',
240 (State | Area, b'5') => b'S',
241 (State | Area, b'8') => b'B',
242 (Lga | Unit, b'O') => b'0',
243 (Lga | Unit, b'I' | b'L') => b'1',
244 (Lga | Unit, b'S') => b'5',
245 (Lga | Unit, b'B') => b'8',
246 _ => byte,
247 }
248 }
249
250 fn padded(self, value: &str) -> Result<String, ParseError> {
251 let (value, width) = (value.trim(), self.range().len());
252 let shortest = if matches!(self, Lga | Unit) { 1 } else { width };
253 let fits = (shortest..=width).contains(&value.len())
254 && value.bytes().all(|b| b.is_ascii_alphanumeric());
255 if fits {
256 Ok(format!("{value:0>width$}"))
257 } else {
258 Err(ParseError::Segment(self))
259 }
260 }
261}
262
263fn collect(input: &str) -> Result<[u8; LEN], ParseError> {
264 match scan(input)? {
265 (bytes, LEN) => Ok(bytes),
266 (_, found) => Err(ParseError::Length { found }),
267 }
268}
269
270fn scan(input: &str) -> Result<([u8; LEN], usize), ParseError> {
272 let mut bytes = [0; LEN];
274 let mut found = 0;
275 for (index, ch) in input.char_indices() {
276 if ch == ' ' || ch == '-' {
277 continue;
278 }
279 if !ch.is_ascii_alphanumeric() {
280 return Err(ParseError::InvalidCharacter { ch, index });
281 }
282 if let Some(slot) = bytes.get_mut(found) {
283 *slot = ch.to_ascii_uppercase() as u8;
284 }
285 found += 1;
286 }
287 Ok((bytes, found))
288}
289
290fn validate(bytes: [u8; LEN]) -> Result<[u8; LEN], ParseError> {
291 match Segment::ALL
292 .into_iter()
293 .find(|s| !s.accepts(&bytes[s.range()]))
294 {
295 Some(segment) => Err(ParseError::Segment(segment)),
296 None => Ok(bytes),
297 }
298}
299
300impl FromStr for Postcode {
301 type Err = ParseError;
302
303 fn from_str(s: &str) -> Result<Self, Self::Err> {
304 Self::parse(s)
305 }
306}
307
308impl fmt::Display for Postcode {
310 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311 f.pad(&self.prefix(Unit))
312 }
313}
314
315impl fmt::Debug for Postcode {
316 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317 write!(f, "Postcode({self})")
318 }
319}
320
321impl FromStr for Prefix {
322 type Err = ParseError;
323
324 fn from_str(s: &str) -> Result<Self, Self::Err> {
325 Self::parse(s)
326 }
327}
328
329impl From<Postcode> for Prefix {
330 fn from(code: Postcode) -> Self {
331 code.truncate(Unit)
332 }
333}
334
335impl fmt::Display for Prefix {
337 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338 f.pad(&Postcode(self.bytes).prefix(self.through))
339 }
340}
341
342impl fmt::Debug for Prefix {
343 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344 write!(f, "Prefix({self})")
345 }
346}
347
348impl Ord for Prefix {
350 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
351 self.as_str().cmp(other.as_str())
352 }
353}
354
355impl PartialOrd for Prefix {
356 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
357 Some(self.cmp(other))
358 }
359}
360
361impl fmt::Display for Segment {
362 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363 f.pad(match self {
364 State => "state",
365 Lga => "lga",
366 District => "district",
367 Area => "area",
368 Unit => "unit",
369 })
370 }
371}
372
373impl fmt::Display for ParseError {
374 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
375 match self {
376 Self::Length { found } => write!(f, "expected {LEN} letters and digits, found {found}"),
377 Self::PrefixLength { found } => {
378 write!(
379 f,
380 "expected 2, 4, 7, 9 or 11 letters and digits, found {found}"
381 )
382 }
383 Self::InvalidCharacter { ch, index } => {
384 write!(f, "invalid character '{ch}' at index {index}")
385 }
386 Self::Segment(segment) => write!(f, "invalid {segment} segment"),
387 }
388 }
389}
390
391impl std::error::Error for ParseError {}
392
393#[cfg(feature = "serde")]
394impl serde::Serialize for Postcode {
395 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
396 serializer.collect_str(self)
397 }
398}
399
400#[cfg(feature = "serde")]
401impl<'de> serde::Deserialize<'de> for Postcode {
402 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
403 let text = <std::borrow::Cow<'de, str>>::deserialize(deserializer)?;
404 text.parse().map_err(serde::de::Error::custom)
405 }
406}
407
408#[cfg(feature = "serde")]
409impl serde::Serialize for Prefix {
410 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
411 serializer.collect_str(self)
412 }
413}
414
415#[cfg(feature = "serde")]
416impl<'de> serde::Deserialize<'de> for Prefix {
417 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
418 let text = <std::borrow::Cow<'de, str>>::deserialize(deserializer)?;
419 text.parse().map_err(serde::de::Error::custom)
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426
427 const PUBLISHED: &str = "\
429 EK-01-A03-FK-01 AK-11-I61-ZF-12 AK-11-H40-WD-11 BA-02-M67-BL-69 BA-02-E99-NE-30 \
430 EB-13-G95-FR-90 EB-13-I97-AB-30 EN-05-V19-CD-22 EN-05-V19-FT-20 FC-03-B06-AG-12 \
431 FC-02-B19-RT-30 JI-24-O18-JP-23 JI-24-N11-VM-58 KN-31-F82-WJ-80 KN-31-D78-IQ-38 \
432 LA-11-W06-TC-10 LA-11-U34-ZR-63 NI-09-J67-QC-65 NI-09-A75-DA-10 OG-14-T18-BN-16 \
433 OG-14-M82-QA-09";
434
435 fn parsed(code: &str) -> Postcode {
436 Postcode::parse(code).unwrap()
437 }
438
439 #[test]
440 fn published_codes_round_trip_unchanged() {
441 assert_eq!(PUBLISHED.split_whitespace().count(), 21);
442 for code in PUBLISHED.split_whitespace() {
443 assert_eq!(parsed(code).to_string(), code);
444 let lenient = Postcode::parse_lenient(code).unwrap();
445 assert_eq!((lenient.postcode, lenient.corrections), (parsed(code), 0));
446 }
447 }
448
449 #[test]
450 fn accepts_every_input_style() {
451 let code = parsed("EK-01-A03-FK-01");
452 assert_eq!(parsed("EK 01 A03 FK 01"), code);
453 assert_eq!(parsed("ek01a03fk01"), code);
454 }
455
456 #[test]
457 fn formats() {
458 let code = parsed("ek01a03fk01");
459 assert_eq!(code.as_str(), "EK01A03FK01");
460 assert_eq!(code.to_string(), "EK-01-A03-FK-01");
461 assert_eq!(format!("[{code:>17}]"), "[ EK-01-A03-FK-01]");
462 assert_eq!(format!("[{:<6}]", Area), "[area ]");
463 assert_eq!(code.to_spaced(), "EK 01 A03 FK 01");
464 assert_eq!(format!("{code:?}"), "Postcode(EK-01-A03-FK-01)");
465 }
466
467 #[test]
468 fn exposes_segments_and_prefixes() {
469 let code = parsed("LA-11-W06-TC-10");
470 let segments = [
471 code.state(),
472 code.lga(),
473 code.district(),
474 code.area(),
475 code.unit(),
476 ];
477 assert_eq!(segments, ["LA", "11", "W06", "TC", "10"]);
478 assert_eq!(code.prefix(Segment::State), "LA");
479 assert_eq!(code.prefix(Segment::District), "LA-11-W06");
480 assert_eq!(code.prefix(Segment::Area), "LA-11-W06-TC");
481 }
482
483 #[test]
484 fn from_segments_zero_fills_numbers_only() {
485 let built = Postcode::from_segments("ek", " 1", "a03", "fk", "1");
486 assert_eq!(built, Ok(parsed("EK-01-A03-FK-01")));
487
488 let rejects = |segments: [&str; 5], segment| {
489 let [state, lga, district, area, unit] = segments;
490 let built = Postcode::from_segments(state, lga, district, area, unit);
491 assert_eq!(built, Err(ParseError::Segment(segment)));
492 };
493 rejects(["E", "1", "A03", "FK", "1"], Segment::State);
494 rejects(["EK", "001", "A03", "FK", "1"], Segment::Lga);
495 rejects(["EK", "1", "A3", "FK", "1"], Segment::District);
496 rejects(["EK", "1", "A03", "F-", "1"], Segment::Area);
497 rejects(["EK", "1", "A03", "FK", ""], Segment::Unit);
498 }
499
500 #[test]
501 fn rejects_malformed_input() {
502 let rejects = |input, error| assert_eq!(Postcode::parse(input), Err(error));
503 rejects("", ParseError::Length { found: 0 });
504 rejects("EK-01-A03-FK", ParseError::Length { found: 9 });
505 rejects("EK-01-A03-FK-011", ParseError::Length { found: 12 });
506 rejects(
507 "EK_01-A03-FK-01",
508 ParseError::InvalidCharacter { ch: '_', index: 2 },
509 );
510 rejects(
511 "ÉK-01-A03-FK-01",
512 ParseError::InvalidCharacter { ch: 'É', index: 0 },
513 );
514 rejects("E1-01-A03-FK-01", ParseError::Segment(Segment::State));
515 rejects("EK-0A-A03-FK-01", ParseError::Segment(Segment::Lga));
516 rejects("EK-00-A03-FK-01", ParseError::Segment(Segment::Lga));
517 rejects("EK-01-A03-F7-01", ParseError::Segment(Segment::Area));
518 rejects("EK-01-A03-FK-00", ParseError::Segment(Segment::Unit));
519 }
520
521 #[test]
522 fn lenient_swaps_lookalikes_by_position() {
523 let fixes = |input, code, corrections| {
524 let expected = Corrected {
525 postcode: parsed(code),
526 corrections,
527 };
528 assert_eq!(Postcode::parse_lenient(input), Ok(expected));
529 };
530 fixes("EK-O1-A03-FK-0I", "EK-01-A03-FK-01", 2);
531 fixes("0G-14-T18-8N-l6", "OG-14-T18-BN-16", 3);
532 fixes("JI-24-O18-JP-23", "JI-24-O18-JP-23", 0);
534 }
535
536 #[test]
537 fn lenient_rejects_what_it_cannot_fix() {
538 let rejects = |input| {
539 let error = ParseError::Segment(Segment::Lga);
540 assert_eq!(Postcode::parse_lenient(input), Err(error));
541 };
542 rejects("EK-0X-A03-FK-01");
543 rejects("EK-OO-A03-FK-01");
544 }
545
546 #[test]
547 fn orders_by_hierarchy() {
548 let sorted = ["EK-01-A03-FK-01", "EK-01-A03-FK-02", "LA-11-W06-TC-10"].map(parsed);
549 let mut shuffled = [sorted[2], sorted[0], sorted[1]];
550 shuffled.sort();
551 assert_eq!(shuffled, sorted);
552 }
553
554 #[test]
555 fn is_valid_agrees_with_parse() {
556 assert!(is_valid("ek 01 a03 fk 01"));
557 assert!(!is_valid("EK-01-A03"));
558 }
559
560 #[cfg(feature = "serde")]
561 #[test]
562 fn serde_round_trips_through_the_canonical_form() {
563 let code: Postcode = serde_json::from_str("\"ek01a03fk01\"").unwrap();
564 assert_eq!(serde_json::to_string(&code).unwrap(), "\"EK-01-A03-FK-01\"");
565 assert!(serde_json::from_str::<Postcode>("\"EK-01\"").is_err());
566 }
567}