icu_locale_core/extensions/unicode/mod.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
5//! Unicode Extensions provide information about user preferences in a given locale.
6//!
7//! The main struct for this extension is [`Unicode`] which contains [`Keywords`] and
8//! [`Attributes`].
9//!
10//!
11//! # Examples
12//!
13//! ```
14//! use icu::locale::Locale;
15//! use icu::locale::extensions::unicode::{Unicode, attribute, key, value};
16//!
17//! let loc: Locale = "en-US-u-foobar-hc-h12".parse().expect("Parsing failed.");
18//!
19//! assert_eq!(
20//! loc.extensions.unicode.keywords.get(&key!("hc")),
21//! Some(&value!("h12"))
22//! );
23//! assert!(
24//! loc.extensions
25//! .unicode
26//! .attributes
27//! .contains(&attribute!("foobar"))
28//! );
29//! ```
30mod attribute;
31mod attributes;
32mod key;
33mod keywords;
34mod subdivision;
35mod value;
36
37use core::cmp::Ordering;
38#[cfg(feature = "alloc")]
39use core::str::FromStr;
40
41#[doc(inline)]
42pub use attribute::{Attribute, attribute};
43pub use attributes::Attributes;
44#[doc(inline)]
45pub use key::{Key, key};
46pub use keywords::Keywords;
47#[doc(inline)]
48pub use subdivision::{SubdivisionId, SubdivisionSuffix, subdivision_suffix};
49#[doc(inline)]
50pub use value::{Value, value};
51
52#[cfg(feature = "alloc")]
53use super::ExtensionType;
54#[cfg(feature = "alloc")]
55use crate::parser::ParseError;
56#[cfg(feature = "alloc")]
57use crate::parser::SubtagIterator;
58
59pub(crate) const UNICODE_EXT_CHAR: char = 'u';
60pub(crate) const UNICODE_EXT_STR: &str = "u";
61
62/// Unicode Extensions provide information about user preferences in a given locale.
63///
64/// A list of [`Unicode BCP47 U Extensions`] as defined in [`Unicode Locale
65/// Identifier`] specification.
66///
67/// Unicode extensions provide subtags that specify language and/or locale-based behavior
68/// or refinements to language tags, according to work done by the Unicode Consortium.
69/// (See [`RFC 6067`] for details).
70///
71/// [`Unicode BCP47 U Extensions`]: https://unicode.org/reports/tr35/#u_Extension
72/// [`RFC 6067`]: https://www.ietf.org/rfc/rfc6067.txt
73/// [`Unicode Locale Identifier`]: https://unicode.org/reports/tr35/#Unicode_locale_identifier
74///
75/// # Examples
76///
77/// ```
78/// use icu::locale::Locale;
79/// use icu::locale::extensions::unicode::{key, value};
80///
81/// let loc: Locale =
82/// "de-u-hc-h12-ca-buddhist".parse().expect("Parsing failed.");
83///
84/// assert_eq!(
85/// loc.extensions.unicode.keywords.get(&key!("ca")),
86/// Some(&value!("buddhist"))
87/// );
88/// ```
89#[derive(Clone, PartialEq, Eq, Debug, Default, Hash)]
90#[allow(clippy::exhaustive_structs)] // spec-backed stable datastructure
91pub struct Unicode {
92 /// The key-value pairs present in this locale extension, with each extension key subtag
93 /// associated to its provided value subtag.
94 pub keywords: Keywords,
95 /// A canonically ordered sequence of single standalone subtags for this locale extension.
96 pub attributes: Attributes,
97}
98
99impl Unicode {
100 /// Returns a new empty map of Unicode extensions. Same as [`default()`](Default::default()), but is `const`.
101 ///
102 /// # Examples
103 ///
104 /// ```
105 /// use icu::locale::extensions::unicode::Unicode;
106 ///
107 /// assert_eq!(Unicode::new(), Unicode::default());
108 /// ```
109 #[inline]
110 pub const fn new() -> Self {
111 Self {
112 keywords: Keywords::new(),
113 attributes: Attributes::new(),
114 }
115 }
116
117 /// A constructor which takes a str slice, parses it and
118 /// produces a well-formed [`Unicode`].
119 ///
120 /// ✨ *Enabled with the `alloc` Cargo feature.*
121 #[inline]
122 #[cfg(feature = "alloc")]
123 pub fn try_from_str(s: &str) -> Result<Self, ParseError> {
124 Self::try_from_utf8(s.as_bytes())
125 }
126
127 /// See [`Self::try_from_str`]
128 ///
129 /// ✨ *Enabled with the `alloc` Cargo feature.*
130 #[cfg(feature = "alloc")]
131 pub fn try_from_utf8(code_units: &[u8]) -> Result<Self, ParseError> {
132 let mut iter = SubtagIterator::new(code_units);
133
134 let ext = iter.next().ok_or(ParseError::InvalidExtension)?;
135 if let ExtensionType::Unicode = ExtensionType::try_from_byte_slice(ext)? {
136 return Self::try_from_iter(&mut iter);
137 }
138
139 Err(ParseError::InvalidExtension)
140 }
141
142 /// Returns [`true`] if there list of keywords and attributes is empty.
143 ///
144 /// # Examples
145 ///
146 /// ```
147 /// use icu::locale::Locale;
148 ///
149 /// let loc: Locale = "en-US-u-foo".parse().expect("Parsing failed.");
150 ///
151 /// assert!(!loc.extensions.unicode.is_empty());
152 /// ```
153 pub fn is_empty(&self) -> bool {
154 self.keywords.is_empty() && self.attributes.is_empty()
155 }
156
157 /// Clears all Unicode extension keywords and attributes, effectively removing
158 /// the Unicode extension.
159 ///
160 /// # Example
161 ///
162 /// ```
163 /// use icu::locale::Locale;
164 ///
165 /// let mut loc: Locale =
166 /// "und-t-mul-u-hello-ca-buddhist-hc-h12".parse().unwrap();
167 /// loc.extensions.unicode.clear();
168 /// assert_eq!(loc, "und-t-mul".parse().unwrap());
169 /// ```
170 pub fn clear(&mut self) {
171 self.keywords.clear();
172 self.attributes.clear();
173 }
174
175 pub(crate) fn as_tuple(&self) -> (&Attributes, &Keywords) {
176 (&self.attributes, &self.keywords)
177 }
178
179 /// Returns an ordering suitable for use in [`BTreeSet`].
180 ///
181 /// The ordering may or may not be equivalent to string ordering, and it
182 /// may or may not be stable across ICU4X releases.
183 ///
184 /// [`BTreeSet`]: alloc::collections::BTreeSet
185 pub fn total_cmp(&self, other: &Self) -> Ordering {
186 self.as_tuple().cmp(&other.as_tuple())
187 }
188
189 #[cfg(feature = "alloc")]
190 pub(crate) fn try_from_iter(iter: &mut SubtagIterator) -> Result<Self, ParseError> {
191 let attributes = Attributes::from_iter(iter);
192 let keywords = Keywords::try_from_iter(iter)?;
193
194 // Ensure we've defined at least one attribute or keyword
195 if attributes.is_empty() && keywords.is_empty() {
196 return Err(ParseError::InvalidExtension);
197 }
198
199 Ok(Self {
200 keywords,
201 attributes,
202 })
203 }
204
205 pub(crate) fn for_each_subtag_str<E, F>(&self, f: &mut F, with_ext: bool) -> Result<(), E>
206 where
207 F: FnMut(&str) -> Result<(), E>,
208 {
209 if !self.is_empty() {
210 if with_ext {
211 f(UNICODE_EXT_STR)?;
212 }
213 self.attributes.for_each_subtag_str(f)?;
214 self.keywords.for_each_subtag_str(f)?;
215 }
216 Ok(())
217 }
218
219 /// Extends the `Unicode` with values from another `Unicode`.
220 ///
221 /// # Example
222 ///
223 /// ```
224 /// use icu::locale::extensions::unicode::Unicode;
225 ///
226 /// let mut ue: Unicode = "u-foobar-ca-buddhist".parse().unwrap();
227 /// let ue2: Unicode = "u-ca-gregory-hc-h12".parse().unwrap();
228 ///
229 /// ue.extend(ue2);
230 ///
231 /// assert_eq!(ue, "u-foobar-ca-gregory-hc-h12".parse().unwrap());
232 /// ```
233 #[cfg(feature = "alloc")]
234 pub fn extend(&mut self, other: Unicode) {
235 self.keywords.extend_from_keywords(other.keywords);
236 self.attributes.extend_from_attributes(other.attributes);
237 }
238}
239
240/// ✨ *Enabled with the `alloc` Cargo feature.*
241#[cfg(feature = "alloc")]
242impl FromStr for Unicode {
243 type Err = ParseError;
244
245 #[inline]
246 fn from_str(s: &str) -> Result<Self, Self::Err> {
247 Self::try_from_str(s)
248 }
249}
250
251writeable::impl_display_with_writeable!(Unicode, #[cfg(feature = "alloc")]);
252
253impl writeable::Writeable for Unicode {
254 fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result {
255 sink.write_char(UNICODE_EXT_CHAR)?;
256
257 if !self.attributes.is_empty() {
258 sink.write_char('-')?;
259 writeable::Writeable::write_to(&self.attributes, sink)?;
260 }
261 if !self.keywords.is_empty() {
262 sink.write_char('-')?;
263 writeable::Writeable::write_to(&self.keywords, sink)?;
264 }
265 Ok(())
266 }
267
268 fn writeable_length_hint(&self) -> writeable::LengthHint {
269 if self.is_empty() {
270 return writeable::LengthHint::exact(0);
271 }
272 let mut result = writeable::LengthHint::exact(1);
273 if !self.attributes.is_empty() {
274 result += writeable::Writeable::writeable_length_hint(&self.attributes) + 1;
275 }
276 if !self.keywords.is_empty() {
277 result += writeable::Writeable::writeable_length_hint(&self.keywords) + 1;
278 }
279 result
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn test_unicode_extension_fromstr() {
289 let ue: Unicode = "u-foo-hc-h12".parse().expect("Failed to parse Unicode");
290 assert_eq!(ue.to_string(), "u-foo-hc-h12");
291
292 let ue: Result<Unicode, _> = "u".parse();
293 assert!(ue.is_err());
294 }
295}