icu_locale_core/extensions/unicode/
subdivision.rs

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 ).
4
5use core::str::FromStr;
6
7use crate::parser::ParseError;
8use crate::subtags::{Region, Subtag};
9
10impl_tinystr_subtag!(
11    /// A subdivision suffix used in [`SubdivisionId`].
12    ///
13    /// This suffix represents a specific subdivision code under a given [`Region`].
14    /// For example the value of [`SubdivisionId`] may be `gbsct`, where the [`SubdivisionSuffix`]
15    /// is `sct` for Scotland.
16    ///
17    /// Such a value associated with a key `rg` means that the locale should use Unit Preferences
18    /// (default calendar, currency, week data, time cycle, measurement system) for Scotland, even if the
19    /// [`LanguageIdentifier`](crate::LanguageIdentifier) is `en-US`.
20    ///
21    /// A subdivision suffix has to be a sequence of alphanumerical characters no
22    /// shorter than one and no longer than four characters.
23    ///
24    ///
25    /// # Examples
26    ///
27    /// ```
28    /// use icu::locale::extensions::unicode::{subdivision_suffix, SubdivisionSuffix};
29    ///
30    /// let ss: SubdivisionSuffix =
31    ///     "sct".parse().expect("Failed to parse a SubdivisionSuffix.");
32    ///
33    /// assert_eq!(ss, subdivision_suffix!("sct"));
34    /// ```
35    SubdivisionSuffix,
36    extensions::unicode,
37    subdivision_suffix,
38    extensions_unicode_subdivision_suffix,
39    1..=4,
40    s,
41    s.is_ascii_alphanumeric(),
42    s.to_ascii_lowercase(),
43    s.is_ascii_alphanumeric() && s.is_ascii_lowercase(),
44    InvalidExtension,
45    ["sct"],
46    ["toolooong"],
47);
48
49/// A Subivision Id as defined in [`Unicode Locale Identifier`].
50///
51/// Subdivision Id is used in [`Unicode`] extensions:
52///  * `rg` - Regional Override
53///  * `sd` - Regional Subdivision
54///
55/// In both cases the subdivision is composed of a [`Region`] and a [`SubdivisionSuffix`] which represents
56/// different meaning depending on the key.
57///
58/// [`Unicode Locale Identifier`]: https://unicode.org/reports/tr35/tr35.html#unicode_subdivision_id
59/// [`Unicode`]: crate::extensions::unicode::Unicode
60///
61/// # Examples
62///
63/// ```
64/// use icu::locale::{
65///     extensions::unicode::{subdivision_suffix, SubdivisionId},
66///     subtags::region,
67/// };
68///
69/// let ss = subdivision_suffix!("zzzz");
70/// let region = region!("gb");
71///
72/// let si = SubdivisionId::new(region, ss);
73///
74/// assert_eq!(si.to_string(), "gbzzzz");
75/// ```
76#[derive(Debug, PartialEq, Eq, Clone, Hash, PartialOrd, Ord, Copy)]
77#[non_exhaustive]
78pub struct SubdivisionId {
79    /// A region field of a Subdivision Id.
80    pub region: Region,
81    /// A subdivision suffix field of a Subdivision Id.
82    pub suffix: SubdivisionSuffix,
83}
84
85impl SubdivisionId {
86    /// Returns a new [`SubdivisionId`].
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// use icu::locale::{
92    ///     extensions::unicode::{subdivision_suffix, SubdivisionId},
93    ///     subtags::region,
94    /// };
95    ///
96    /// let ss = subdivision_suffix!("zzzz");
97    /// let region = region!("gb");
98    ///
99    /// let si = SubdivisionId::new(region, ss);
100    ///
101    /// assert_eq!(si.to_string(), "gbzzzz");
102    /// ```
103    pub const fn new(region: Region, suffix: SubdivisionSuffix) -> Self {
104        Self { region, suffix }
105    }
106
107    /// A constructor which takes a str slice, parses it and
108    /// produces a well-formed [`SubdivisionId`].
109    #[inline]
110    pub fn try_from_str(s: &str) -> Result<Self, ParseError> {
111        Self::try_from_utf8(s.as_bytes())
112    }
113
114    /// See [`Self::try_from_str`]
115    pub fn try_from_utf8(code_units: &[u8]) -> Result<Self, ParseError> {
116        let is_alpha = code_units
117            .first()
118            .and_then(|b| {
119                b.is_ascii_alphabetic()
120                    .then_some(true)
121                    .or_else(|| b.is_ascii_digit().then_some(false))
122            })
123            .ok_or(ParseError::InvalidExtension)?;
124        let region_len = if is_alpha { 2 } else { 3 };
125        let (region_code_units, suffix_code_units) = code_units
126            .split_at_checked(region_len)
127            .ok_or(ParseError::InvalidExtension)?;
128        let region =
129            Region::try_from_utf8(region_code_units).map_err(|_| ParseError::InvalidExtension)?;
130        let suffix = SubdivisionSuffix::try_from_utf8(suffix_code_units)?;
131        Ok(Self { region, suffix })
132    }
133
134    /// Convert to [`Subtag`]
135    pub fn into_subtag(self) -> Subtag {
136        let result = self.region.to_tinystr().concat(self.suffix.to_tinystr());
137        Subtag::from_tinystr_unvalidated(result)
138    }
139}
140
141impl writeable::Writeable for SubdivisionId {
142    #[inline]
143    fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result {
144        sink.write_str(self.region.to_tinystr().to_ascii_lowercase().as_str())?;
145        sink.write_str(self.suffix.as_str())
146    }
147
148    #[inline]
149    fn writeable_length_hint(&self) -> writeable::LengthHint {
150        self.region.writeable_length_hint() + self.suffix.writeable_length_hint()
151    }
152}
153
154writeable::impl_display_with_writeable!(SubdivisionId);
155
156impl FromStr for SubdivisionId {
157    type Err = ParseError;
158
159    #[inline]
160    fn from_str(s: &str) -> Result<Self, Self::Err> {
161        Self::try_from_str(s)
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_subdivisionid_fromstr() {
171        let si: SubdivisionId = "gbzzzz".parse().expect("Failed to parse SubdivisionId");
172        assert_eq!(si.region.to_string(), "GB");
173        assert_eq!(si.suffix.to_string(), "zzzz");
174        assert_eq!(si.to_string(), "gbzzzz");
175
176        for sample in ["", "gb", "o"] {
177            let oe: Result<SubdivisionId, _> = sample.parse();
178            assert!(oe.is_err(), "Should fail: {}", sample);
179        }
180    }
181}