lang_id/
id.rs

1use tinystr::ParseError;
2use unic_langid::subtags;
3
4use crate::LangID;
5/// RawID
6///
7/// # Example
8///
9/// ```
10/// use lang_id::{LangID, RawID};
11///
12/// const fn id_und() -> LangID {
13///   RawID::new(6581877, None, None).into_lang_id()
14/// }
15///
16/// assert_eq!(id_und().language, "und");
17/// ```
18#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
19pub struct RawID {
20  pub language: u64,
21  pub script: Option<u32>,
22  pub region: Option<u32>,
23}
24
25impl core::fmt::Display for RawID {
26  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
27    let Self {
28      language,
29      script,
30      region,
31    } = self;
32
33    match () {
34      #[cfg(not(feature = "compact_str"))]
35      () => write!(
36        f,
37        r##"RawID::new({language}, {script:?}, {region:?}).into_lang_id()"##,
38      ),
39      #[cfg(feature = "compact_str")]
40      () => write!(
41        f,
42        r##"/*{}*/ RawID::new({language}, {script:?}, {region:?}).into_lang_id()"##,
43        self.to_bcp47(),
44      ),
45    }
46  }
47}
48
49impl RawID {
50  pub const fn new(language: u64, script: Option<u32>, region: Option<u32>) -> Self {
51    Self {
52      language,
53      script,
54      region,
55    }
56  }
57  /// Attempts to construct a Raw language ID from individual BCP 47 components.
58  ///
59  /// # Example
60  ///
61  /// ```
62  /// use lang_id::RawID;
63  ///
64  /// let lzh = RawID::try_from_str("lzh", "Hant", "")?;
65  ///
66  /// #[cfg(feature = "compact_str")]
67  /// assert_eq!(
68  ///   lzh.to_string(),
69  ///   "/*lzh-Hant*/ RawID::new(6847084, Some(1953390920), None).into_lang_id()"
70  /// );
71  ///
72  /// # Ok::<(), tinystr::ParseError>(())
73  /// ```
74  pub const fn try_from_str(
75    language: &str,
76    script: &str,
77    region: &str,
78  ) -> Result<Self, ParseError> {
79    use tinystr::TinyAsciiStr as TinyStr;
80    type Language = TinyStr<8>;
81    type Tiny4 = TinyStr<4>;
82
83    let language = match language.is_empty() {
84      true => return Err(ParseError::ContainsNull),
85      _ => {
86        let tmp = match Language::try_from_str(language) {
87          Ok(s) => s,
88          Err(e) => return Err(e),
89        };
90        u64::from_le_bytes(*tmp.all_bytes())
91      }
92    };
93
94    let script = match script.is_empty() {
95      true => None,
96      _ => Some({
97        let str = match Tiny4::try_from_str(script) {
98          Ok(s) => s,
99          Err(e) => return Err(e),
100        };
101        u32::from_le_bytes(*str.all_bytes())
102      }),
103    };
104
105    let region = match region.is_empty() {
106      true => None,
107      _ => Some({
108        let str = match Tiny4::try_from_str(region) {
109          Ok(s) => s,
110          Err(e) => return Err(e),
111        };
112        u32::from_le_bytes(*str.all_bytes())
113      }),
114    };
115
116    let id = RawID::new(language, script, region);
117
118    Ok(id)
119  }
120
121  #[cfg(feature = "compact_str")]
122  /// Converts the language identifier to a BCP47-compliant string
123  /// representation.
124  ///
125  /// Constructs a string in `language[-script][-region]` format.
126  ///
127  /// ```
128  /// use lang_id::RawID;
129  ///
130  /// let id = RawID::try_from_str("es", "", "419")?;
131  /// let bcp47 = id.to_bcp47();
132  /// assert_eq!(bcp47, "es-419");
133  /// # Ok::<(), tinystr::ParseError>(())
134  /// ```
135  pub fn to_bcp47(&self) -> compact_str::CompactString {
136    use compact_str::{CompactString, format_compact};
137    let id = self.into_lang_id();
138
139    let LangID {
140      language,
141      script,
142      region,
143      ..
144    } = id;
145
146    let empty_str = || CompactString::const_new("");
147    format_compact!(
148      "{language}{}{}",
149      match script {
150        Some(s) => format_compact!("-{s}"),
151        _ => empty_str(),
152      },
153      match region {
154        Some(s) => format_compact!("-{s}"),
155        _ => empty_str(),
156      }
157    )
158  }
159
160  /// Warn: This function is unsafe.
161  ///
162  /// Since `Option<T>.map(|x| y)` cannot be used in **const fn** in rust 1.85,
163  /// use match expressions instead.
164  #[inline]
165  pub const fn into_lang_id(self) -> LangID {
166    unsafe {
167      let language = subtags::Language::from_raw_unchecked(self.language);
168
169      // self.script.map(subtags::Script::from_raw_unchecked)
170      let script = match self.script {
171        Some(s) => Some(subtags::Script::from_raw_unchecked(s)),
172        _ => None,
173      };
174
175      let region = match self.region {
176        Some(r) => Some(subtags::Region::from_raw_unchecked(r)),
177        _ => None,
178      };
179
180      LangID::from_raw_parts_unchecked(language, script, region, None)
181    }
182  }
183}
184
185#[cfg(test)]
186mod tests {
187  #[allow(unused_imports)]
188  use super::*;
189
190  #[test]
191  #[cfg(feature = "compact_str")]
192  fn test_build_lzh() -> Result<(), ParseError> {
193    let lzh = RawID::try_from_str("lzh", "Hant", "")?;
194    use compact_str::ToCompactString;
195
196    assert_eq!(
197      lzh.to_compact_string(),
198      "/*lzh-Hant*/ RawID::new(6847084, Some(1953390920), None).into_lang_id()"
199    );
200    Ok(())
201  }
202
203  #[test]
204  #[cfg(feature = "compact_str")]
205  fn test_to_bcp47() -> Result<(), ParseError> {
206    let id = RawID::try_from_str("es", "Latn", "419")?;
207    let bcp47 = id.to_bcp47();
208    assert_eq!(bcp47, "es-Latn-419");
209    // dbg!(bcp47);
210    Ok(())
211  }
212}