1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#[derive(Debug, PartialEq)]
pub struct Locale {
    pub language: String,
    pub country: Option<String>,
    pub encoding: Option<String>,
    pub modifier: Option<String>,
}

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("found unexpected character '{0}' in \"{2}\" at {1}")]
    UnexpectedCharacterRecieved(char, usize, String),
}
pub type Result<T> = std::result::Result<T, Error>;

impl Locale {
    fn parse<'a, T: AsRef<str>>(text: T) -> Result<Locale> {
        // lang_COUNTRY.ENCODING@MODIFIER
        let mut locale = Locale {
            language: String::new(),
            country: None,
            encoding: None,
            modifier: None,
        };
        let text = text.as_ref();
        for (idx, character) in text.chars().enumerate() {
            match character {
                '_' => {
                    if locale.country.is_some()
                        || locale.encoding.is_some()
                        || locale.modifier.is_some()
                    {
                        return Err(Error::UnexpectedCharacterRecieved(
                            character,
                            idx,
                            text.to_string(),
                        ));
                    }
                    locale.country = Some(String::new());
                }
                '.' => {
                    if locale.encoding.is_some() || locale.modifier.is_some() {
                        return Err(Error::UnexpectedCharacterRecieved(
                            character,
                            idx,
                            text.to_string(),
                        ));
                    }
                    locale.encoding = Some(String::new());
                }
                '@' => {
                    if locale.modifier.is_some() {
                        return Err(Error::UnexpectedCharacterRecieved(
                            character,
                            idx,
                            text.to_string(),
                        ));
                    }
                    locale.modifier = Some(String::new());
                }
                character => match (&locale.country, &locale.encoding, &locale.modifier) {
                    (None, None, None) => locale.language.push(character),
                    (Some(country), None, None) => {
                        locale.country = Some(format!("{country}{character}"))
                    }
                    (_, Some(encoding), None) => {
                        locale.encoding = Some(format!("{encoding}{character}"))
                    }
                    (_, _, Some(modifier)) => {
                        locale.modifier = Some(format!("{modifier}{character}"))
                    }
                },
            }
        }
        Ok(locale)
    }
}
impl TryFrom<String> for Locale {
    fn try_from(text: String) -> Result<Self> {
        Locale::parse(text)
    }
    type Error = Error;
}

#[cfg(test)]
mod tests {
    use super::{Locale, Result};
    #[test]
    fn parse_full() -> Result<()> {
        let result = super::Locale::parse("lang_COUNTRY.ENCODING@MODIFIER")?;
        assert_eq!(result.language, "lang");
        assert_eq!(result.country, Some("COUNTRY".into()));
        assert_eq!(result.encoding, Some("ENCODING".into()));
        assert_eq!(result.modifier, Some("MODIFIER".into()));
        Ok(())
    }

    #[test]
    fn parse_rest() {
        let tests = vec![
            (
                "lang_COUNTRY",
                Locale {
                    language: "lang".into(),
                    country: Some("COUNTRY".into()),
                    encoding: None,
                    modifier: None,
                },
            ),
            (
                "lang@MODIFIER",
                Locale {
                    language: "lang".into(),
                    country: None,
                    encoding: None,
                    modifier: Some("MODIFIER".into()),
                },
            ),
            (
                "justLang",
                Locale {
                    language: "justLang".into(),
                    country: None,
                    encoding: None,
                    modifier: None,
                },
            ),
        ];
        for (example, expected_result) in tests {
            assert_eq!(Locale::parse(example).unwrap(), expected_result);
        }
    }
}