1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5
6#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
8pub struct RegionCode {
9 value: String,
10}
11
12impl RegionCode {
13 #[must_use]
15 pub fn new(input: &str) -> Option<Self> {
16 parse_region_code(input)
17 }
18
19 #[must_use]
21 pub fn as_str(&self) -> &str {
22 &self.value
23 }
24
25 #[must_use]
27 pub fn into_string(self) -> String {
28 self.value
29 }
30
31 #[must_use]
33 pub fn is_numeric(&self) -> bool {
34 self.value.bytes().all(|byte| byte.is_ascii_digit())
35 }
36}
37
38impl AsRef<str> for RegionCode {
39 fn as_ref(&self) -> &str {
40 self.as_str()
41 }
42}
43
44impl fmt::Display for RegionCode {
45 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46 formatter.write_str(self.as_str())
47 }
48}
49
50#[must_use]
52pub fn parse_region_code(input: &str) -> Option<RegionCode> {
53 normalize_region_code(input).map(|value| RegionCode { value })
54}
55
56#[must_use]
58pub fn is_region_code(input: &str) -> bool {
59 normalize_region_code(input).is_some()
60}
61
62#[must_use]
64pub fn normalize_region_code(input: &str) -> Option<String> {
65 let trimmed = input.trim();
66 if trimmed.len() == 2 && trimmed.bytes().all(|byte| byte.is_ascii_alphabetic()) {
67 return Some(trimmed.to_ascii_uppercase());
68 }
69
70 if trimmed.len() == 3 && trimmed.bytes().all(|byte| byte.is_ascii_digit()) {
71 return Some(trimmed.to_string());
72 }
73
74 None
75}
76
77#[cfg(test)]
78mod tests {
79 use super::{RegionCode, is_region_code, normalize_region_code, parse_region_code};
80
81 #[test]
82 fn accepts_common_region_examples() {
83 for region in ["US", "GB", "CA", "JP", "DE", "419"] {
84 assert!(is_region_code(region));
85 assert_eq!(parse_region_code(region).unwrap().as_str(), region);
86 }
87 }
88
89 #[test]
90 fn normalizes_alpha_regions_to_uppercase() {
91 assert_eq!(normalize_region_code("us"), Some("US".to_string()));
92 assert_eq!(normalize_region_code(" gb "), Some("GB".to_string()));
93 assert_eq!(RegionCode::new("419").unwrap().as_str(), "419");
94 assert!(RegionCode::new("419").unwrap().is_numeric());
95 }
96
97 #[test]
98 fn rejects_invalid_region_shapes() {
99 for region in ["", "U", "USA", "U1", "41A", "1234", "US-CA", "日本"] {
100 assert!(!is_region_code(region));
101 assert!(parse_region_code(region).is_none());
102 }
103 }
104}