Skip to main content

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
49impl SubdivisionSuffix {
50    pub(crate) const UNKNOWN: Self = subdivision_suffix!("zzzz");
51
52    pub(crate) fn is_unknown(self) -> bool {
53        self == Self::UNKNOWN
54    }
55}
56
57/// A Subivision Id as defined in [`Unicode Locale Identifier`].
58///
59/// Subdivision Id is used in [`Unicode`] extensions:
60///  * `rg` - Regional Override
61///  * `sd` - Regional Subdivision
62///
63/// In both cases the subdivision is composed of a [`Region`] and a [`SubdivisionSuffix`] which represents
64/// different meaning depending on the key.
65///
66/// [`Unicode Locale Identifier`]: https://unicode.org/reports/tr35/tr35.html#unicode_subdivision_id
67/// [`Unicode`]: crate::extensions::unicode::Unicode
68///
69/// # Examples
70///
71/// ```
72/// use icu::locale::{
73///     extensions::unicode::{SubdivisionId, subdivision_suffix},
74///     subtags::region,
75/// };
76///
77/// // "zzzz" means "unknown subdivision"
78/// let ss = subdivision_suffix!("zzzz");
79/// let region = region!("gb");
80///
81/// let si = SubdivisionId::new(region, ss);
82///
83/// assert_eq!(si.to_string(), "gbzzzz");
84/// ```
85#[derive(Debug, PartialEq, Eq, Clone, Hash, PartialOrd, Ord, Copy)]
86#[non_exhaustive]
87pub struct SubdivisionId {
88    /// A region field of a Subdivision Id.
89    pub region: Region,
90    /// A subdivision suffix field of a Subdivision Id.
91    pub suffix: SubdivisionSuffix,
92}
93
94impl SubdivisionId {
95    /// Returns a new [`SubdivisionId`].
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// use icu::locale::{
101    ///     extensions::unicode::{SubdivisionId, subdivision_suffix},
102    ///     subtags::region,
103    /// };
104    ///
105    /// let ss = subdivision_suffix!("zzzz");
106    /// let region = region!("gb");
107    ///
108    /// let si = SubdivisionId::new(region, ss);
109    ///
110    /// assert_eq!(si.to_string(), "gbzzzz");
111    /// ```
112    pub const fn new(region: Region, suffix: SubdivisionSuffix) -> Self {
113        Self { region, suffix }
114    }
115
116    /// A constructor which takes a str slice, parses it and
117    /// produces a well-formed [`SubdivisionId`].
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// use icu::locale::extensions::unicode::SubdivisionId;
123    /// use writeable::assert_writeable_eq;
124    ///
125    /// let subdivision = SubdivisionId::try_from_str("gbeng").unwrap();
126    ///
127    /// assert_writeable_eq!(subdivision, "gbeng");
128    /// assert_writeable_eq!(subdivision.region, "GB");
129    /// assert_writeable_eq!(subdivision.suffix, "eng");
130    /// ```
131    ///
132    /// When the value can't be parsed:
133    ///
134    /// ```
135    /// use icu::locale::ParseError;
136    /// use icu::locale::extensions::unicode::SubdivisionId;
137    ///
138    /// // Value is too short
139    /// assert!(matches!(
140    ///     SubdivisionId::try_from_str("zz"),
141    ///     Err(ParseError::InvalidExtension),
142    /// ));
143    ///
144    /// // Value is too long
145    /// assert!(matches!(
146    ///     SubdivisionId::try_from_str("abcdefg"),
147    ///     Err(ParseError::InvalidExtension),
148    /// ));
149    ///
150    /// // Value does not start with a valid region code
151    /// assert!(matches!(
152    ///     SubdivisionId::try_from_str("a0zzzz"),
153    ///     Err(ParseError::InvalidExtension),
154    /// ));
155    /// assert!(matches!(
156    ///     SubdivisionId::try_from_str("0azzzz"),
157    ///     Err(ParseError::InvalidExtension),
158    /// ));
159    /// ```
160    #[inline]
161    pub fn try_from_str(s: &str) -> Result<Self, ParseError> {
162        Self::try_from_utf8(s.as_bytes())
163    }
164
165    /// See [`Self::try_from_str`]
166    pub fn try_from_utf8(code_units: &[u8]) -> Result<Self, ParseError> {
167        Self::try_from_subtag(Subtag::try_from_utf8(code_units)?)
168    }
169
170    pub(crate) const fn try_from_subtag(subtag: Subtag) -> Result<Self, ParseError> {
171        let code_units = subtag.as_str().as_bytes();
172        let Some(first) = code_units.first() else {
173            return Err(ParseError::InvalidExtension);
174        };
175        let region_len = if first.is_ascii_alphabetic() {
176            2
177        } else if first.is_ascii_digit() {
178            3
179        } else {
180            return Err(ParseError::InvalidExtension);
181        };
182        let Some((region_code_units, suffix_code_units)) = code_units.split_at_checked(region_len)
183        else {
184            return Err(ParseError::InvalidExtension);
185        };
186        let Ok(region) = Region::try_from_utf8(region_code_units) else {
187            return Err(ParseError::InvalidExtension);
188        };
189        let Ok(suffix) = SubdivisionSuffix::try_from_utf8(suffix_code_units) else {
190            return Err(ParseError::InvalidExtension);
191        };
192        Ok(Self { region, suffix })
193    }
194
195    /// Convert to [`Subtag`]
196    pub const fn into_subtag(self) -> Subtag {
197        let result = self
198            .region
199            .to_tinystr()
200            .to_ascii_lowercase()
201            .concat(self.suffix.to_tinystr());
202        Subtag::from_tinystr_unvalidated(result)
203    }
204}
205
206impl writeable::Writeable for SubdivisionId {
207    #[inline]
208    fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result {
209        sink.write_str(self.region.to_tinystr().to_ascii_lowercase().as_str())?;
210        sink.write_str(self.suffix.as_str())
211    }
212
213    #[inline]
214    fn writeable_length_hint(&self) -> writeable::LengthHint {
215        self.region.writeable_length_hint() + self.suffix.writeable_length_hint()
216    }
217}
218
219writeable::impl_display_with_writeable!(SubdivisionId, #[cfg(feature = "alloc")]);
220
221impl FromStr for SubdivisionId {
222    type Err = ParseError;
223
224    #[inline]
225    fn from_str(s: &str) -> Result<Self, Self::Err> {
226        Self::try_from_str(s)
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_subdivisionid_fromstr() {
236        let si: SubdivisionId = "gbzzzz".parse().expect("Failed to parse SubdivisionId");
237        assert_eq!(si.region.to_string(), "GB");
238        assert_eq!(si.suffix.to_string(), "zzzz");
239        assert_eq!(si.to_string(), "gbzzzz");
240
241        for sample in ["", "gb", "o"] {
242            let oe: Result<SubdivisionId, _> = sample.parse();
243            assert!(oe.is_err(), "Should fail: {sample}");
244        }
245    }
246}