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::{subdivision_suffix, SubdivisionId},
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::{subdivision_suffix, SubdivisionId},
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::extensions::unicode::SubdivisionId;
136 /// use icu::locale::ParseError;
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 let is_alpha = code_units
168 .first()
169 .and_then(|b| {
170 b.is_ascii_alphabetic()
171 .then_some(true)
172 .or_else(|| b.is_ascii_digit().then_some(false))
173 })
174 .ok_or(ParseError::InvalidExtension)?;
175 let region_len = if is_alpha { 2 } else { 3 };
176 let (region_code_units, suffix_code_units) = code_units
177 .split_at_checked(region_len)
178 .ok_or(ParseError::InvalidExtension)?;
179 let region =
180 Region::try_from_utf8(region_code_units).map_err(|_| ParseError::InvalidExtension)?;
181 let suffix = SubdivisionSuffix::try_from_utf8(suffix_code_units)?;
182 Ok(Self { region, suffix })
183 }
184
185 /// Convert to [`Subtag`]
186 pub const fn into_subtag(self) -> Subtag {
187 let result = self
188 .region
189 .to_tinystr()
190 .to_ascii_lowercase()
191 .concat(self.suffix.to_tinystr());
192 Subtag::from_tinystr_unvalidated(result)
193 }
194}
195
196impl writeable::Writeable for SubdivisionId {
197 #[inline]
198 fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result {
199 sink.write_str(self.region.to_tinystr().to_ascii_lowercase().as_str())?;
200 sink.write_str(self.suffix.as_str())
201 }
202
203 #[inline]
204 fn writeable_length_hint(&self) -> writeable::LengthHint {
205 self.region.writeable_length_hint() + self.suffix.writeable_length_hint()
206 }
207}
208
209writeable::impl_display_with_writeable!(SubdivisionId, #[cfg(feature = "alloc")]);
210
211impl FromStr for SubdivisionId {
212 type Err = ParseError;
213
214 #[inline]
215 fn from_str(s: &str) -> Result<Self, Self::Err> {
216 Self::try_from_str(s)
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_subdivisionid_fromstr() {
226 let si: SubdivisionId = "gbzzzz".parse().expect("Failed to parse SubdivisionId");
227 assert_eq!(si.region.to_string(), "GB");
228 assert_eq!(si.suffix.to_string(), "zzzz");
229 assert_eq!(si.to_string(), "gbzzzz");
230
231 for sample in ["", "gb", "o"] {
232 let oe: Result<SubdivisionId, _> = sample.parse();
233 assert!(oe.is_err(), "Should fail: {sample}");
234 }
235 }
236}