Bug 1861709 replace AudioCallbackDriver::ThreadRunning() assertions that mean to...
[gecko.git] / third_party / rust / icu_locid / src / locale.rs
blob4412da86e030f72102beffb68426e38fdeac3662
1 // This file is part of ICU4X. For terms of use, please see the file
2 // called LICENSE at the top level of the ICU4X source tree
3 // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
5 use crate::ordering::SubtagOrderingResult;
6 use crate::parser::{
7     parse_locale, parse_locale_with_single_variant_single_keyword_unicode_keyword_extension,
8     ParserError, ParserMode, SubtagIterator,
9 };
10 use crate::{extensions, subtags, LanguageIdentifier};
11 use alloc::string::String;
12 use core::cmp::Ordering;
13 use core::str::FromStr;
14 use tinystr::TinyAsciiStr;
15 use writeable::Writeable;
17 /// A core struct representing a [`Unicode Locale Identifier`].
18 ///
19 /// A locale is made of two parts:
20 ///  * Unicode Language Identifier
21 ///  * A set of Unicode Extensions
22 ///
23 /// [`Locale`] exposes all of the same fields and methods as [`LanguageIdentifier`], and
24 /// on top of that is able to parse, manipulate and serialize unicode extension fields.
25 ///
26 ///
27 /// # Examples
28 ///
29 /// ```
30 /// use icu_locid::{
31 ///     extensions_unicode_key as key, extensions_unicode_value as value,
32 ///     locale, subtags_language as language, subtags_region as region,
33 /// };
34 ///
35 /// let loc = locale!("en-US-u-ca-buddhist");
36 ///
37 /// assert_eq!(loc.id.language, language!("en"));
38 /// assert_eq!(loc.id.script, None);
39 /// assert_eq!(loc.id.region, Some(region!("US")));
40 /// assert_eq!(loc.id.variants.len(), 0);
41 /// assert_eq!(
42 ///     loc.extensions.unicode.keywords.get(&key!("ca")),
43 ///     Some(&value!("buddhist"))
44 /// );
45 /// ```
46 ///
47 /// # Parsing
48 ///
49 /// Unicode recognizes three levels of standard conformance for a locale:
50 ///
51 ///  * *well-formed* - syntactically correct
52 ///  * *valid* - well-formed and only uses registered language subtags, extensions, keywords, types...
53 ///  * *canonical* - valid and no deprecated codes or structure.
54 ///
55 /// At the moment parsing normalizes a well-formed locale identifier converting
56 /// `_` separators to `-` and adjusting casing to conform to the Unicode standard.
57 ///
58 /// Any bogus subtags will cause the parsing to fail with an error.
59 /// No subtag validation or canonicalization is performed.
60 ///
61 /// # Examples
62 ///
63 /// ```
64 /// use icu::locid::{subtags::*, Locale};
65 ///
66 /// let loc: Locale = "eN_latn_Us-Valencia_u-hC-H12"
67 ///     .parse()
68 ///     .expect("Failed to parse.");
69 ///
70 /// assert_eq!(loc.id.language, "en".parse::<Language>().unwrap());
71 /// assert_eq!(loc.id.script, "Latn".parse::<Script>().ok());
72 /// assert_eq!(loc.id.region, "US".parse::<Region>().ok());
73 /// assert_eq!(
74 ///     loc.id.variants.get(0),
75 ///     "valencia".parse::<Variant>().ok().as_ref()
76 /// );
77 /// ```
78 /// [`Unicode Locale Identifier`]: https://unicode.org/reports/tr35/tr35.html#Unicode_locale_identifier
79 #[derive(Default, PartialEq, Eq, Clone, Hash)]
80 #[allow(clippy::exhaustive_structs)] // This struct is stable (and invoked by a macro)
81 pub struct Locale {
82     /// The basic language/script/region components in the locale identifier along with any variants.
83     pub id: LanguageIdentifier,
84     /// Any extensions present in the locale identifier.
85     pub extensions: extensions::Extensions,
88 #[test]
89 fn test_sizes() {
90     assert_eq!(core::mem::size_of::<subtags::Language>(), 3);
91     assert_eq!(core::mem::size_of::<subtags::Script>(), 4);
92     assert_eq!(core::mem::size_of::<subtags::Region>(), 3);
93     assert_eq!(core::mem::size_of::<subtags::Variant>(), 8);
94     assert_eq!(core::mem::size_of::<subtags::Variants>(), 16);
95     assert_eq!(core::mem::size_of::<LanguageIdentifier>(), 32);
97     assert_eq!(core::mem::size_of::<extensions::transform::Transform>(), 56);
98     assert_eq!(core::mem::size_of::<Option<LanguageIdentifier>>(), 32);
99     assert_eq!(core::mem::size_of::<extensions::transform::Fields>(), 24);
101     assert_eq!(core::mem::size_of::<extensions::unicode::Attributes>(), 24);
102     assert_eq!(core::mem::size_of::<extensions::unicode::Keywords>(), 24);
103     assert_eq!(core::mem::size_of::<Vec<extensions::other::Other>>(), 24);
104     assert_eq!(core::mem::size_of::<extensions::private::Private>(), 24);
105     assert_eq!(core::mem::size_of::<extensions::Extensions>(), 152);
107     assert_eq!(core::mem::size_of::<Locale>(), 184);
110 impl Locale {
111     /// A constructor which takes a utf8 slice, parses it and
112     /// produces a well-formed [`Locale`].
113     ///
114     /// # Examples
115     ///
116     /// ```
117     /// use icu::locid::Locale;
118     ///
119     /// Locale::try_from_bytes(b"en-US-u-hc-h12").unwrap();
120     /// ```
121     pub fn try_from_bytes(v: &[u8]) -> Result<Self, ParserError> {
122         parse_locale(v)
123     }
125     /// The default undefined locale "und". Same as [`default()`](Default::default()).
126     ///
127     /// # Examples
128     ///
129     /// ```
130     /// use icu::locid::Locale;
131     ///
132     /// assert_eq!(Locale::default(), Locale::UND);
133     /// ```
134     pub const UND: Self = Self {
135         id: LanguageIdentifier::UND,
136         extensions: extensions::Extensions::new(),
137     };
139     /// This is a best-effort operation that performs all available levels of canonicalization.
140     ///
141     /// At the moment the operation will normalize casing and the separator, but in the future
142     /// it may also validate and update from deprecated subtags to canonical ones.
143     ///
144     /// # Examples
145     ///
146     /// ```
147     /// use icu::locid::Locale;
148     ///
149     /// assert_eq!(
150     ///     Locale::canonicalize("pL_latn_pl-U-HC-H12").as_deref(),
151     ///     Ok("pl-Latn-PL-u-hc-h12")
152     /// );
153     /// ```
154     pub fn canonicalize<S: AsRef<[u8]>>(input: S) -> Result<String, ParserError> {
155         let locale = Self::try_from_bytes(input.as_ref())?;
156         Ok(locale.write_to_string().into_owned())
157     }
159     /// Compare this [`Locale`] with BCP-47 bytes.
160     ///
161     /// The return value is equivalent to what would happen if you first converted this
162     /// [`Locale`] to a BCP-47 string and then performed a byte comparison.
163     ///
164     /// This function is case-sensitive and results in a *total order*, so it is appropriate for
165     /// binary search. The only argument producing [`Ordering::Equal`] is `self.to_string()`.
166     ///
167     /// # Examples
168     ///
169     /// ```
170     /// use icu::locid::Locale;
171     /// use std::cmp::Ordering;
172     ///
173     /// let bcp47_strings: &[&str] = &[
174     ///     "pl-Latn-PL",
175     ///     "und",
176     ///     "und-fonipa",
177     ///     "und-t-m0-true",
178     ///     "und-u-ca-hebrew",
179     ///     "und-u-ca-japanese",
180     ///     "zh",
181     /// ];
182     ///
183     /// for ab in bcp47_strings.windows(2) {
184     ///     let a = ab[0];
185     ///     let b = ab[1];
186     ///     assert!(a.cmp(b) == Ordering::Less);
187     ///     let a_loc = a.parse::<Locale>().unwrap();
188     ///     assert!(a_loc.strict_cmp(a.as_bytes()) == Ordering::Equal);
189     ///     assert!(a_loc.strict_cmp(b.as_bytes()) == Ordering::Less);
190     /// }
191     /// ```
192     pub fn strict_cmp(&self, other: &[u8]) -> Ordering {
193         self.strict_cmp_iter(other.split(|b| *b == b'-')).end()
194     }
196     /// Compare this [`Locale`] with an iterator of BCP-47 subtags.
197     ///
198     /// This function has the same equality semantics as [`Locale::strict_cmp`]. It is intended as
199     /// a more modular version that allows multiple subtag iterators to be chained together.
200     ///
201     /// For an additional example, see [`SubtagOrderingResult`].
202     ///
203     /// # Examples
204     ///
205     /// ```
206     /// use icu::locid::locale;
207     /// use std::cmp::Ordering;
208     ///
209     /// let subtags: &[&[u8]] =
210     ///     &[b"ca", b"ES", b"valencia", b"u", b"ca", b"hebrew"];
211     ///
212     /// let loc = locale!("ca-ES-valencia-u-ca-hebrew");
213     /// assert_eq!(
214     ///     Ordering::Equal,
215     ///     loc.strict_cmp_iter(subtags.iter().copied()).end()
216     /// );
217     ///
218     /// let loc = locale!("ca-ES-valencia");
219     /// assert_eq!(
220     ///     Ordering::Less,
221     ///     loc.strict_cmp_iter(subtags.iter().copied()).end()
222     /// );
223     ///
224     /// let loc = locale!("ca-ES-valencia-u-nu-arab");
225     /// assert_eq!(
226     ///     Ordering::Greater,
227     ///     loc.strict_cmp_iter(subtags.iter().copied()).end()
228     /// );
229     /// ```
230     pub fn strict_cmp_iter<'l, I>(&self, mut subtags: I) -> SubtagOrderingResult<I>
231     where
232         I: Iterator<Item = &'l [u8]>,
233     {
234         let r = self.for_each_subtag_str(&mut |subtag| {
235             if let Some(other) = subtags.next() {
236                 match subtag.as_bytes().cmp(other) {
237                     Ordering::Equal => Ok(()),
238                     not_equal => Err(not_equal),
239                 }
240             } else {
241                 Err(Ordering::Greater)
242             }
243         });
244         match r {
245             Ok(_) => SubtagOrderingResult::Subtags(subtags),
246             Err(o) => SubtagOrderingResult::Ordering(o),
247         }
248     }
250     /// Compare this `Locale` with a potentially unnormalized BCP-47 string.
251     ///
252     /// The return value is equivalent to what would happen if you first parsed the
253     /// BCP-47 string to a `Locale` and then performed a structucal comparison.
254     ///
255     /// # Examples
256     ///
257     /// ```
258     /// use icu::locid::Locale;
259     /// use std::cmp::Ordering;
260     ///
261     /// let bcp47_strings: &[&str] = &[
262     ///     "pl-LaTn-pL",
263     ///     "uNd",
264     ///     "UND-FONIPA",
265     ///     "UnD-t-m0-TrUe",
266     ///     "uNd-u-CA-Japanese",
267     ///     "ZH",
268     /// ];
269     ///
270     /// for a in bcp47_strings {
271     ///     assert!(a.parse::<Locale>().unwrap().normalizing_eq(a));
272     /// }
273     /// ```
274     pub fn normalizing_eq(&self, other: &str) -> bool {
275         macro_rules! subtag_matches {
276             ($T:ty, $iter:ident, $expected:expr) => {
277                 $iter
278                     .next()
279                     .map(|b| <$T>::try_from_bytes(b) == Ok($expected))
280                     .unwrap_or(false)
281             };
282         }
284         let mut iter = SubtagIterator::new(other.as_bytes());
285         if !subtag_matches!(subtags::Language, iter, self.id.language) {
286             return false;
287         }
288         if let Some(ref script) = self.id.script {
289             if !subtag_matches!(subtags::Script, iter, *script) {
290                 return false;
291             }
292         }
293         if let Some(ref region) = self.id.region {
294             if !subtag_matches!(subtags::Region, iter, *region) {
295                 return false;
296             }
297         }
298         for variant in self.id.variants.iter() {
299             if !subtag_matches!(subtags::Variant, iter, *variant) {
300                 return false;
301             }
302         }
303         if !self.extensions.is_empty() {
304             match extensions::Extensions::try_from_iter(&mut iter) {
305                 Ok(exts) => {
306                     if self.extensions != exts {
307                         return false;
308                     }
309                 }
310                 Err(_) => {
311                     return false;
312                 }
313             }
314         }
315         iter.next().is_none()
316     }
318     #[doc(hidden)]
319     #[allow(clippy::type_complexity)]
320     pub const fn try_from_bytes_with_single_variant_single_keyword_unicode_extension(
321         v: &[u8],
322     ) -> Result<
323         (
324             subtags::Language,
325             Option<subtags::Script>,
326             Option<subtags::Region>,
327             Option<subtags::Variant>,
328             Option<(extensions::unicode::Key, Option<TinyAsciiStr<8>>)>,
329         ),
330         ParserError,
331     > {
332         parse_locale_with_single_variant_single_keyword_unicode_keyword_extension(
333             v,
334             ParserMode::Locale,
335         )
336     }
338     pub(crate) fn for_each_subtag_str<E, F>(&self, f: &mut F) -> Result<(), E>
339     where
340         F: FnMut(&str) -> Result<(), E>,
341     {
342         self.id.for_each_subtag_str(f)?;
343         self.extensions.for_each_subtag_str(f)?;
344         Ok(())
345     }
348 impl FromStr for Locale {
349     type Err = ParserError;
351     fn from_str(source: &str) -> Result<Self, Self::Err> {
352         Self::try_from_bytes(source.as_bytes())
353     }
356 impl From<LanguageIdentifier> for Locale {
357     fn from(id: LanguageIdentifier) -> Self {
358         Self {
359             id,
360             extensions: extensions::Extensions::default(),
361         }
362     }
365 impl From<Locale> for LanguageIdentifier {
366     fn from(loc: Locale) -> Self {
367         loc.id
368     }
371 impl AsRef<LanguageIdentifier> for Locale {
372     fn as_ref(&self) -> &LanguageIdentifier {
373         &self.id
374     }
377 impl AsMut<LanguageIdentifier> for Locale {
378     fn as_mut(&mut self) -> &mut LanguageIdentifier {
379         &mut self.id
380     }
383 impl core::fmt::Debug for Locale {
384     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
385         writeable::Writeable::write_to(self, f)
386     }
389 impl_writeable_for_each_subtag_str_no_test!(Locale, selff, selff.extensions.is_empty() => selff.id.write_to_string());
391 #[test]
392 fn test_writeable() {
393     use writeable::assert_writeable_eq;
394     assert_writeable_eq!(Locale::UND, "und");
395     assert_writeable_eq!("und-001".parse::<Locale>().unwrap(), "und-001");
396     assert_writeable_eq!("und-Mymr".parse::<Locale>().unwrap(), "und-Mymr");
397     assert_writeable_eq!("my-Mymr-MM".parse::<Locale>().unwrap(), "my-Mymr-MM");
398     assert_writeable_eq!(
399         "my-Mymr-MM-posix".parse::<Locale>().unwrap(),
400         "my-Mymr-MM-posix",
401     );
402     assert_writeable_eq!(
403         "zh-macos-posix".parse::<Locale>().unwrap(),
404         "zh-macos-posix",
405     );
406     assert_writeable_eq!(
407         "my-t-my-d0-zawgyi".parse::<Locale>().unwrap(),
408         "my-t-my-d0-zawgyi",
409     );
410     assert_writeable_eq!(
411         "ar-SA-u-ca-islamic-civil".parse::<Locale>().unwrap(),
412         "ar-SA-u-ca-islamic-civil",
413     );
414     assert_writeable_eq!(
415         "en-001-x-foo-bar".parse::<Locale>().unwrap(),
416         "en-001-x-foo-bar",
417     );
418     assert_writeable_eq!("und-t-m0-true".parse::<Locale>().unwrap(), "und-t-m0-true",);
421 /// # Examples
423 /// ```
424 /// use icu::locid::Locale;
425 /// use icu::locid::{locale, subtags_language as language};
427 /// assert_eq!(Locale::from(language!("en")), locale!("en"));
428 /// ```
429 impl From<subtags::Language> for Locale {
430     fn from(language: subtags::Language) -> Self {
431         Self {
432             id: language.into(),
433             ..Default::default()
434         }
435     }
438 /// # Examples
440 /// ```
441 /// use icu::locid::Locale;
442 /// use icu::locid::{locale, subtags_script as script};
444 /// assert_eq!(Locale::from(Some(script!("latn"))), locale!("und-Latn"));
445 /// ```
446 impl From<Option<subtags::Script>> for Locale {
447     fn from(script: Option<subtags::Script>) -> Self {
448         Self {
449             id: script.into(),
450             ..Default::default()
451         }
452     }
455 /// # Examples
457 /// ```
458 /// use icu::locid::Locale;
459 /// use icu::locid::{locale, subtags_region as region};
461 /// assert_eq!(Locale::from(Some(region!("US"))), locale!("und-US"));
462 /// ```
463 impl From<Option<subtags::Region>> for Locale {
464     fn from(region: Option<subtags::Region>) -> Self {
465         Self {
466             id: region.into(),
467             ..Default::default()
468         }
469     }
472 /// # Examples
474 /// ```
475 /// use icu::locid::Locale;
476 /// use icu::locid::{
477 ///     locale, subtags_language as language, subtags_region as region,
478 ///     subtags_script as script,
479 /// };
481 /// assert_eq!(
482 ///     Locale::from((
483 ///         language!("en"),
484 ///         Some(script!("Latn")),
485 ///         Some(region!("US"))
486 ///     )),
487 ///     locale!("en-Latn-US")
488 /// );
489 /// ```
490 impl
491     From<(
492         subtags::Language,
493         Option<subtags::Script>,
494         Option<subtags::Region>,
495     )> for Locale
497     fn from(
498         lsr: (
499             subtags::Language,
500             Option<subtags::Script>,
501             Option<subtags::Region>,
502         ),
503     ) -> Self {
504         Self {
505             id: lsr.into(),
506             ..Default::default()
507         }
508     }