lang_id/
id.rs

1pub use 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 ID {
20  pub language: u64,
21  pub script: Option<u32>,
22  pub region: Option<u32>,
23}
24
25impl core::fmt::Display for ID {
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 ID {
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  /// assert_eq!(
67  ///   lzh.to_string(),
68  ///   "RawID::new(6847084, Some(1953390920), None).into_lang_id()"
69  /// );
70  ///
71  /// # Ok::<(), tinystr::ParseError>(())
72  /// ```
73  pub const fn try_from_str(
74    language: &str,
75    script: &str,
76    region: &str,
77  ) -> Result<Self, ParseError> {
78    use tinystr::TinyAsciiStr as TinyStr;
79    type Language = TinyStr<8>;
80    type Tiny4 = TinyStr<4>;
81
82    let language = match language.is_empty() {
83      true => return Err(ParseError::ContainsNull),
84      _ => {
85        let tmp = match Language::try_from_str(language) {
86          Ok(s) => s,
87          Err(e) => return Err(e),
88        };
89        u64::from_le_bytes(*tmp.all_bytes())
90      }
91    };
92
93    let script = match script.is_empty() {
94      true => None,
95      _ => Some({
96        let str = match Tiny4::try_from_str(script) {
97          Ok(s) => s,
98          Err(e) => return Err(e),
99        };
100        u32::from_le_bytes(*str.all_bytes())
101      }),
102    };
103
104    let region = match region.is_empty() {
105      true => None,
106      _ => Some({
107        let str = match Tiny4::try_from_str(region) {
108          Ok(s) => s,
109          Err(e) => return Err(e),
110        };
111        u32::from_le_bytes(*str.all_bytes())
112      }),
113    };
114
115    let id = ID::new(language, script, region);
116
117    Ok(id)
118  }
119
120  #[cfg(feature = "compact_str")]
121  /// Converts the language identifier to a BCP47-compliant string
122  /// representation.
123  ///
124  /// Constructs a string in `language[-script][-region]` format.
125  ///
126  /// ```
127  /// use lang_id::RawID;
128  ///
129  /// let id = RawID::try_from_str("es", "Latn", "419")?;
130  /// let bcp47 = id.to_bcp47();
131  /// assert_eq!(bcp47, "es-Latn-419");
132  /// # Ok::<(), tinystr::ParseError>(())
133  /// ```
134  pub fn to_bcp47(&self) -> compact_str::CompactString {
135    use compact_str::{CompactString, format_compact};
136    let id = self.into_lang_id();
137
138    let LangID {
139      language,
140      script,
141      region,
142      ..
143    } = id;
144
145    let empty_str = || CompactString::const_new("");
146    format_compact!(
147      "{language}{}{}",
148      match script {
149        Some(s) => format_compact!("-{s}"),
150        _ => empty_str(),
151      },
152      match region {
153        Some(s) => format_compact!("-{s}"),
154        _ => empty_str(),
155      }
156    )
157  }
158
159  /// Warn: This function is unsafe.
160  ///
161  /// Since `Option<T>.map(|x| y)` cannot be used in **const fn** in rust 1.85,
162  /// use match expressions instead.
163  #[inline]
164  pub const fn into_lang_id(self) -> LangID {
165    unsafe {
166      let language = subtags::Language::from_raw_unchecked(self.language);
167
168      // self.script.map(subtags::Script::from_raw_unchecked)
169      let script = match self.script {
170        Some(s) => Some(subtags::Script::from_raw_unchecked(s)),
171        _ => None,
172      };
173
174      let region = match self.region {
175        Some(r) => Some(subtags::Region::from_raw_unchecked(r)),
176        _ => None,
177      };
178
179      LangID::from_raw_parts_unchecked(language, script, region, None)
180    }
181  }
182}
183
184#[cfg(test)]
185mod tests {
186  use super::*;
187
188  #[test]
189  fn test_build_lzh() -> Result<(), ParseError> {
190    let lzh = ID::try_from_str("lzh", "Hant", "")?;
191    assert_eq!(
192      lzh.to_string(),
193      "RawID::new(6847084, Some(1953390920), None).into_lang_id()"
194    );
195    Ok(())
196  }
197
198  #[test]
199  #[cfg(feature = "compact_str")]
200  fn test_to_bcp47() -> Result<(), ParseError> {
201    let id = ID::try_from_str("es", "Latn", "419")?;
202    let bcp47 = id.to_bcp47();
203    assert_eq!(bcp47, "es-Latn-419");
204    // dbg!(bcp47);
205    Ok(())
206  }
207}