1use std::collections::HashMap;
42use std::fmt;
43use std::fmt::Display;
44use std::str::FromStr;
45
46use regex::Regex;
47
48use crate::id::LocaleIdentifier;
49use crate::{LocaleError, LocaleResult};
50
51#[derive(Debug, PartialEq)]
59pub struct LocaleString {
60 language_code: String,
61 territory: Option<String>,
62 code_set: Option<String>,
63 modifier: Option<String>,
64}
65
66#[derive(Debug, PartialEq)]
68pub enum ParseError {
69 EmptyString,
71 PosixUnsupported,
73 RegexFailure,
75 InvalidLanguageCode,
77 InvalidTerritoryCode,
79 InvalidCodeSet,
81 InvalidModifier,
83 InvalidPath,
85}
86
87const SEP_TERRITORY: char = '_';
92const SEP_CODE_SET: char = '.';
93const SEP_MODIFIER: char = '@';
94
95impl LocaleIdentifier for LocaleString {
96 fn new(language_code: String) -> LocaleResult<Self> {
97 if language_code.len() != 2 || !language_code.chars().all(|c| c.is_lowercase()) {
98 return Err(LocaleError::InvalidLanguageCode);
99 };
100
101 Ok(LocaleString {
102 language_code,
103 territory: None,
104 code_set: None,
105 modifier: None,
106 })
107 }
108
109 fn with_language(&self, language_code: String) -> LocaleResult<Self> {
110 if language_code.len() != 2 || !language_code.chars().all(|c| c.is_lowercase()) {
111 return Err(LocaleError::InvalidLanguageCode);
112 };
113
114 Ok(LocaleString {
115 language_code,
116 territory: self.territory.clone(),
117 code_set: self.code_set.clone(),
118 modifier: self.modifier.clone(),
119 })
120 }
121
122 fn with_territory(&self, territory: String) -> LocaleResult<Self> {
123 if territory.len() < 2
124 || territory.len() > 2
125 || !territory.chars().all(|c| c.is_uppercase())
126 {
127 return Err(LocaleError::InvalidTerritoryCode);
128 };
129
130 Ok(LocaleString {
131 language_code: self.language_code.clone(),
132 territory: Some(territory),
133 code_set: self.code_set.clone(),
134 modifier: self.modifier.clone(),
135 })
136 }
137
138 fn with_code_set(&self, code_set: String) -> LocaleResult<Self> {
139 if code_set.chars().all(|c| c.is_whitespace()) {
140 return Err(LocaleError::InvalidCodeSet);
141 };
142 Ok(LocaleString {
143 language_code: self.language_code.clone(),
144 territory: self.territory.clone(),
145 code_set: Some(code_set),
146 modifier: self.modifier.clone(),
147 })
148 }
149
150 fn with_modifier(&self, modifier: String) -> LocaleResult<Self> {
151 Ok(LocaleString {
152 language_code: self.language_code.clone(),
153 territory: self.territory.clone(),
154 code_set: self.code_set.clone(),
155 modifier: Some(modifier),
156 })
157 }
158
159 fn with_modifiers<K, V>(&self, modifiers: HashMap<K, V>) -> LocaleResult<Self>
160 where
161 K: Display,
162 V: Display,
163 {
164 let modifier_strings: Vec<String> = modifiers
165 .iter()
166 .map(|(key, value)| format!("{}={}", key, value))
167 .collect();
168
169 Ok(LocaleString {
170 language_code: self.language_code.clone(),
171 territory: self.territory.clone(),
172 code_set: self.code_set.clone(),
173 modifier: Some(modifier_strings.join(";")),
174 })
175 }
176
177 fn language_code(&self) -> String {
178 self.language_code.clone()
179 }
180
181 fn territory(&self) -> Option<String> {
182 self.territory.clone()
183 }
184
185 fn code_set(&self) -> Option<String> {
186 self.code_set.clone()
187 }
188
189 fn modifier(&self) -> Option<String> {
190 self.modifier.clone()
191 }
192}
193
194impl Display for LocaleString {
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 write!(
197 f,
198 "{}",
199 [
200 self.language_code.clone(),
201 match &self.territory {
202 Some(v) => format!("{}{}", SEP_TERRITORY, v),
203 None => "".to_string(),
204 },
205 match &self.code_set {
206 Some(v) => format!("{}{}", SEP_CODE_SET, v),
207 None => "".to_string(),
208 },
209 match &self.modifier {
210 Some(v) => format!("{}{}", SEP_MODIFIER, v),
211 None => "".to_string(),
212 },
213 ]
214 .join("")
215 )
216 }
217}
218
219impl FromStr for LocaleString {
220 type Err = ParseError;
221
222 fn from_str(s: &str) -> Result<Self, Self::Err> {
223 lazy_static! {
224 static ref RE: Regex =
225 Regex::new(r"^([a-z][a-z]+)(_[A-Z][A-Z]+)?(\.[A-Z][a-zA-Z0-9\-_]+)?(@\w+)?$")
226 .unwrap();
227 }
228
229 if s.is_empty() {
230 return Err(ParseError::EmptyString);
231 }
232
233 if s == "C" || s == "POSIX" {
234 return Err(ParseError::PosixUnsupported);
235 }
236
237 match RE.captures(s) {
238 None => Err(ParseError::RegexFailure),
239 Some(groups) => {
240 let mut locale =
241 LocaleString::new(groups.get(1).unwrap().as_str().to_string()).unwrap();
242 if let Some(group_str) = groups.get(2) {
243 locale = locale
244 .with_territory(group_str.as_str()[1..].to_string())
245 .unwrap();
246 }
247 if let Some(group_str) = groups.get(3) {
248 locale = locale
249 .with_code_set(group_str.as_str()[1..].to_string())
250 .unwrap();
251 }
252 if let Some(group_str) = groups.get(4) {
253 locale = locale
254 .with_modifier(group_str.as_str()[1..].to_string())
255 .unwrap();
256 }
257 Ok(locale)
258 }
259 }
260 }
261}
262
263#[cfg(test)]
268mod tests {
269 use std::collections::HashMap;
270 use std::str::FromStr;
271
272 use crate::{LocaleError, LocaleIdentifier, LocaleString};
273
274 #[test]
276 fn test_bad_constructor_length() {
277 assert_eq!(
278 LocaleString::new("english".to_string()),
279 Err(LocaleError::InvalidLanguageCode)
280 );
281 }
282
283 #[test]
284 fn test_bad_constructor_case() {
285 assert_eq!(
286 LocaleString::new("EN".to_string()),
287 Err(LocaleError::InvalidLanguageCode)
288 );
289 }
290
291 #[test]
292 fn test_bad_territory_length() {
293 assert_eq!(
294 LocaleString::new("en".to_string())
295 .unwrap()
296 .with_territory("USA".to_string()),
297 Err(LocaleError::InvalidTerritoryCode)
298 );
299 }
300
301 #[test]
302 fn test_bad_country_case() {
303 assert_eq!(
304 LocaleString::new("en".to_string())
305 .unwrap()
306 .with_territory("us".to_string()),
307 Err(LocaleError::InvalidTerritoryCode)
308 );
309 }
310
311 #[test]
313 fn test_constructor() {
314 let locale = LocaleString::new("en".to_string()).unwrap();
315 assert_eq!(locale.language_code(), "en".to_string());
316 assert_eq!(locale.territory(), None);
317 assert_eq!(locale.modifier(), None);
318 }
319
320 #[test]
321 fn test_with_language() {
322 let locale = LocaleString::new("en".to_string()).unwrap();
323 assert_eq!(
324 locale
325 .with_language("fr".to_string())
326 .unwrap()
327 .language_code(),
328 "fr".to_string()
329 );
330 }
331
332 #[test]
333 fn test_with_country() {
334 let locale = LocaleString::new("en".to_string()).unwrap();
335 assert_eq!(
336 locale.with_territory("UK".to_string()).unwrap().territory(),
337 Some("UK".to_string())
338 );
339 }
340
341 #[test]
342 fn test_with_code_set() {
343 let locale = LocaleString::new("en".to_string()).unwrap();
344 assert_eq!(
345 locale
346 .with_code_set("UTF-8".to_string())
347 .unwrap()
348 .code_set(),
349 Some("UTF-8".to_string())
350 );
351 }
352
353 #[test]
354 fn test_with_modifier() {
355 let locale = LocaleString::new("en".to_string()).unwrap();
356 assert_eq!(
357 locale
358 .with_modifier("collation=pinyin;currency=CNY".to_string())
359 .unwrap()
360 .modifier(),
361 Some("collation=pinyin;currency=CNY".to_string())
362 );
363 }
364
365 #[test]
366 fn test_with_modifiers() {
367 let locale = LocaleString::new("en".to_string()).unwrap();
368 let modifiers: HashMap<&str, &str> = [("collation", "pinyin"), ("currency", "CNY")]
369 .iter()
370 .cloned()
371 .collect();
372 assert!(locale
373 .with_modifiers(modifiers)
374 .unwrap()
375 .modifier()
376 .unwrap()
377 .contains("collation=pinyin"));
378 }
382
383 #[test]
385 fn test_to_string() {
386 let locale = LocaleString::new("en".to_string())
387 .unwrap()
388 .with_territory("US".to_string())
389 .unwrap()
390 .with_code_set("UTF-8".to_string())
391 .unwrap()
392 .with_modifier("collation=pinyin;currency=CNY".to_string())
393 .unwrap();
394 assert_eq!(
395 locale.to_string(),
396 "en_US.UTF-8@collation=pinyin;currency=CNY".to_string()
397 );
398 }
399
400 #[test]
402 fn test_from_str_1() {
403 match LocaleString::from_str("en") {
404 Ok(locale) => assert_eq!(locale.language_code(), "en"),
405 _ => panic!("LocaleString::from_str failure"),
406 }
407 }
408
409 #[test]
410 fn test_from_str_2() {
411 match LocaleString::from_str("en_US") {
412 Ok(locale) => {
413 assert_eq!(locale.language_code(), "en");
414 assert_eq!(locale.territory(), Some("US".to_string()));
415 }
416 _ => panic!("LocaleString::from_str failure"),
417 }
418 }
419
420 #[test]
421 fn test_from_str_3() {
422 match LocaleString::from_str("en_US.UTF-8") {
423 Ok(locale) => {
424 assert_eq!(locale.language_code(), "en");
425 assert_eq!(locale.territory(), Some("US".to_string()));
426 assert_eq!(locale.code_set(), Some("UTF-8".to_string()));
427 }
428 _ => panic!("LocaleString::from_str failure"),
429 }
430 }
431
432 #[test]
433 fn test_from_str_4() {
434 match LocaleString::from_str("en_US.UTF-8@Latn") {
435 Ok(locale) => {
436 assert_eq!(locale.language_code(), "en");
437 assert_eq!(locale.territory(), Some("US".to_string()));
438 assert_eq!(locale.code_set(), Some("UTF-8".to_string()));
439 assert_eq!(locale.modifier(), Some("Latn".to_string()));
440 }
441 _ => panic!("LocaleString::from_str failure"),
442 }
443 }
444}