use std::collections::HashMap;
use std::fmt;
use std::fmt::Display;
use std::str::FromStr;
use regex::Regex;
use crate::id::LocaleIdentifier;
use crate::{LocaleError, LocaleResult};
#[derive(Debug, PartialEq)]
pub struct LocaleString {
language_code: String,
territory: Option<String>,
code_set: Option<String>,
modifier: Option<String>,
}
#[derive(Debug, PartialEq)]
pub enum ParseError {
EmptyString,
PosixUnsupported,
RegexFailure,
InvalidLanguageCode,
InvalidTerritoryCode,
InvalidCodeSet,
InvalidModifier,
InvalidPath,
}
const SEP_TERRITORY: char = '_';
const SEP_CODE_SET: char = '.';
const SEP_MODIFIER: char = '@';
impl LocaleIdentifier for LocaleString {
fn new(language_code: String) -> LocaleResult<Self> {
if language_code.len() != 2 || !language_code.chars().all(|c| c.is_lowercase()) {
return Err(LocaleError::InvalidLanguageCode);
};
Ok(LocaleString {
language_code,
territory: None,
code_set: None,
modifier: None,
})
}
fn with_language(&self, language_code: String) -> LocaleResult<Self> {
if language_code.len() != 2 || !language_code.chars().all(|c| c.is_lowercase()) {
return Err(LocaleError::InvalidLanguageCode);
};
Ok(LocaleString {
language_code,
territory: self.territory.clone(),
code_set: self.code_set.clone(),
modifier: self.modifier.clone(),
})
}
fn with_territory(&self, territory: String) -> LocaleResult<Self> {
if territory.len() < 2
|| territory.len() > 2
|| !territory.chars().all(|c| c.is_uppercase())
{
return Err(LocaleError::InvalidTerritoryCode);
};
Ok(LocaleString {
language_code: self.language_code.clone(),
territory: Some(territory),
code_set: self.code_set.clone(),
modifier: self.modifier.clone(),
})
}
fn with_code_set(&self, code_set: String) -> LocaleResult<Self> {
if code_set.chars().all(|c| c.is_whitespace()) {
return Err(LocaleError::InvalidCodeSet);
};
Ok(LocaleString {
language_code: self.language_code.clone(),
territory: self.territory.clone(),
code_set: Some(code_set),
modifier: self.modifier.clone(),
})
}
fn with_modifier(&self, modifier: String) -> LocaleResult<Self> {
Ok(LocaleString {
language_code: self.language_code.clone(),
territory: self.territory.clone(),
code_set: self.code_set.clone(),
modifier: Some(modifier),
})
}
fn with_modifiers<K, V>(&self, modifiers: HashMap<K, V>) -> LocaleResult<Self>
where
K: Display,
V: Display,
{
let modifier_strings: Vec<String> = modifiers
.iter()
.map(|(key, value)| format!("{}={}", key, value))
.collect();
Ok(LocaleString {
language_code: self.language_code.clone(),
territory: self.territory.clone(),
code_set: self.code_set.clone(),
modifier: Some(modifier_strings.join(";")),
})
}
fn language_code(&self) -> String {
self.language_code.clone()
}
fn territory(&self) -> Option<String> {
self.territory.clone()
}
fn code_set(&self) -> Option<String> {
self.code_set.clone()
}
fn modifier(&self) -> Option<String> {
self.modifier.clone()
}
}
impl Display for LocaleString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
[
self.language_code.clone(),
match &self.territory {
Some(v) => format!("{}{}", SEP_TERRITORY, v),
None => "".to_string(),
},
match &self.code_set {
Some(v) => format!("{}{}", SEP_CODE_SET, v),
None => "".to_string(),
},
match &self.modifier {
Some(v) => format!("{}{}", SEP_MODIFIER, v),
None => "".to_string(),
},
]
.join("")
)
}
}
impl FromStr for LocaleString {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
lazy_static! {
static ref RE: Regex =
Regex::new(r"^([a-z][a-z]+)(_[A-Z][A-Z]+)?(\.[A-Z][a-zA-Z0-9\-_]+)?(@\w+)?$")
.unwrap();
}
if s.is_empty() {
return Err(ParseError::EmptyString);
}
if s == "C" || s == "POSIX" {
return Err(ParseError::PosixUnsupported);
}
match RE.captures(s) {
None => Err(ParseError::RegexFailure),
Some(groups) => {
let mut locale =
LocaleString::new(groups.get(1).unwrap().as_str().to_string()).unwrap();
if let Some(group_str) = groups.get(2) {
locale = locale
.with_territory(group_str.as_str()[1..].to_string())
.unwrap();
}
if let Some(group_str) = groups.get(3) {
locale = locale
.with_code_set(group_str.as_str()[1..].to_string())
.unwrap();
}
if let Some(group_str) = groups.get(4) {
locale = locale
.with_modifier(group_str.as_str()[1..].to_string())
.unwrap();
}
Ok(locale)
}
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::str::FromStr;
use crate::{LocaleError, LocaleIdentifier, LocaleString};
#[test]
fn test_bad_constructor_length() {
assert_eq!(
LocaleString::new("english".to_string()),
Err(LocaleError::InvalidLanguageCode)
);
}
#[test]
fn test_bad_constructor_case() {
assert_eq!(
LocaleString::new("EN".to_string()),
Err(LocaleError::InvalidLanguageCode)
);
}
#[test]
fn test_bad_territory_length() {
assert_eq!(
LocaleString::new("en".to_string())
.unwrap()
.with_territory("USA".to_string()),
Err(LocaleError::InvalidTerritoryCode)
);
}
#[test]
fn test_bad_country_case() {
assert_eq!(
LocaleString::new("en".to_string())
.unwrap()
.with_territory("us".to_string()),
Err(LocaleError::InvalidTerritoryCode)
);
}
#[test]
fn test_constructor() {
let locale = LocaleString::new("en".to_string()).unwrap();
assert_eq!(locale.language_code(), "en".to_string());
assert_eq!(locale.territory(), None);
assert_eq!(locale.modifier(), None);
}
#[test]
fn test_with_language() {
let locale = LocaleString::new("en".to_string()).unwrap();
assert_eq!(
locale
.with_language("fr".to_string())
.unwrap()
.language_code(),
"fr".to_string()
);
}
#[test]
fn test_with_country() {
let locale = LocaleString::new("en".to_string()).unwrap();
assert_eq!(
locale.with_territory("UK".to_string()).unwrap().territory(),
Some("UK".to_string())
);
}
#[test]
fn test_with_code_set() {
let locale = LocaleString::new("en".to_string()).unwrap();
assert_eq!(
locale
.with_code_set("UTF-8".to_string())
.unwrap()
.code_set(),
Some("UTF-8".to_string())
);
}
#[test]
fn test_with_modifier() {
let locale = LocaleString::new("en".to_string()).unwrap();
assert_eq!(
locale
.with_modifier("collation=pinyin;currency=CNY".to_string())
.unwrap()
.modifier(),
Some("collation=pinyin;currency=CNY".to_string())
);
}
#[test]
fn test_with_modifiers() {
let locale = LocaleString::new("en".to_string()).unwrap();
let modifiers: HashMap<&str, &str> = [("collation", "pinyin"), ("currency", "CNY")]
.iter()
.cloned()
.collect();
assert!(locale
.with_modifiers(modifiers)
.unwrap()
.modifier()
.unwrap()
.contains("collation=pinyin"));
}
#[test]
fn test_to_string() {
let locale = LocaleString::new("en".to_string())
.unwrap()
.with_territory("US".to_string())
.unwrap()
.with_code_set("UTF-8".to_string())
.unwrap()
.with_modifier("collation=pinyin;currency=CNY".to_string())
.unwrap();
assert_eq!(
locale.to_string(),
"en_US.UTF-8@collation=pinyin;currency=CNY".to_string()
);
}
#[test]
fn test_from_str_1() {
match LocaleString::from_str("en") {
Ok(locale) => assert_eq!(locale.language_code(), "en"),
_ => panic!("LocaleString::from_str failure"),
}
}
#[test]
fn test_from_str_2() {
match LocaleString::from_str("en_US") {
Ok(locale) => {
assert_eq!(locale.language_code(), "en");
assert_eq!(locale.territory(), Some("US".to_string()));
}
_ => panic!("LocaleString::from_str failure"),
}
}
#[test]
fn test_from_str_3() {
match LocaleString::from_str("en_US.UTF-8") {
Ok(locale) => {
assert_eq!(locale.language_code(), "en");
assert_eq!(locale.territory(), Some("US".to_string()));
assert_eq!(locale.code_set(), Some("UTF-8".to_string()));
}
_ => panic!("LocaleString::from_str failure"),
}
}
#[test]
fn test_from_str_4() {
match LocaleString::from_str("en_US.UTF-8@Latn") {
Ok(locale) => {
assert_eq!(locale.language_code(), "en");
assert_eq!(locale.territory(), Some("US".to_string()));
assert_eq!(locale.code_set(), Some("UTF-8".to_string()));
assert_eq!(locale.modifier(), Some("Latn".to_string()));
}
_ => panic!("LocaleString::from_str failure"),
}
}
}