icu_locale_core/extensions/other/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//! Other Use Extensions is a list of extensions other than unicode,
6//! transform or private.
7//!
8//! Those extensions are treated as a pass-through, and no Unicode related
9//! behavior depends on them.
10//!
11//! The main struct for this extension is [`Other`] which is a list of [`Subtag`]s.
12//!
13//! # Examples
14//!
15//! ```
16//! use icu::locale::extensions::other::Other;
17//! use icu::locale::Locale;
18//!
19//! let mut loc: Locale = "en-US-a-foo-faa".parse().expect("Parsing failed.");
20//! ```
21
22#[cfg(feature = "alloc")]
23use core::str::FromStr;
24
25#[cfg(feature = "alloc")]
26use super::ExtensionType;
27#[cfg(feature = "alloc")]
28use crate::parser::ParseError;
29#[cfg(feature = "alloc")]
30use crate::parser::SubtagIterator;
31use crate::shortvec::ShortBoxSlice;
32use crate::subtags::Subtag;
33#[cfg(feature = "alloc")]
34use alloc::vec::Vec;
35
36/// A list of [`Other Use Extensions`] as defined in [`Unicode Locale
37/// Identifier`] specification.
38///
39/// Those extensions are treated as a pass-through, and no Unicode related
40/// behavior depends on them.
41///
42/// # Examples
43///
44/// ```
45/// use icu::locale::extensions::other::Other;
46/// use icu::locale::subtags::Subtag;
47///
48/// let subtag1: Subtag = "foo".parse().expect("Failed to parse a Subtag.");
49/// let subtag2: Subtag = "bar".parse().expect("Failed to parse a Subtag.");
50///
51/// let other = Other::from_vec_unchecked(b'a', vec![subtag1, subtag2]);
52/// assert_eq!(&other.to_string(), "a-foo-bar");
53/// ```
54///
55/// [`Other Use Extensions`]: https://unicode.org/reports/tr35/#other_extensions
56/// [`Unicode Locale Identifier`]: https://unicode.org/reports/tr35/#Unicode_locale_identifier
57#[derive(Clone, PartialEq, Eq, Debug, Default, Hash, PartialOrd, Ord)]
58pub struct Other {
59 // Safety invariant: must be ASCII
60 ext: u8,
61 keys: ShortBoxSlice<Subtag>,
62}
63
64impl Other {
65 /// A constructor which takes a str slice, parses it and
66 /// produces a well-formed [`Other`].
67 ///
68 /// ✨ *Enabled with the `alloc` Cargo feature.*
69 #[inline]
70 #[cfg(feature = "alloc")]
71 pub fn try_from_str(s: &str) -> Result<Self, ParseError> {
72 Self::try_from_utf8(s.as_bytes())
73 }
74
75 /// See [`Self::try_from_str`]
76 ///
77 /// ✨ *Enabled with the `alloc` Cargo feature.*
78 #[cfg(feature = "alloc")]
79 pub fn try_from_utf8(code_units: &[u8]) -> Result<Self, ParseError> {
80 let mut iter = SubtagIterator::new(code_units);
81
82 let ext = iter.next().ok_or(ParseError::InvalidExtension)?;
83 if let ExtensionType::Other(b) = ExtensionType::try_from_byte_slice(ext)? {
84 return Self::try_from_iter(b, &mut iter);
85 }
86
87 Err(ParseError::InvalidExtension)
88 }
89
90 /// A constructor which takes a pre-sorted list of [`Subtag`].
91 ///
92 /// ✨ *Enabled with the `alloc` Cargo feature.*
93 ///
94 /// # Panics
95 ///
96 /// Panics if `ext` is not ASCII alphabetic.
97 ///
98 /// # Examples
99 ///
100 /// ```
101 /// use icu::locale::extensions::other::Other;
102 /// use icu::locale::subtags::Subtag;
103 ///
104 /// let subtag1: Subtag = "foo".parse().expect("Failed to parse a Subtag.");
105 /// let subtag2: Subtag = "bar".parse().expect("Failed to parse a Subtag.");
106 ///
107 /// let other = Other::from_vec_unchecked(b'a', vec![subtag1, subtag2]);
108 /// assert_eq!(&other.to_string(), "a-foo-bar");
109 /// ```
110 #[cfg(feature = "alloc")]
111 pub fn from_vec_unchecked(ext: u8, keys: Vec<Subtag>) -> Self {
112 Self::from_short_slice_unchecked(ext, keys.into())
113 }
114
115 #[allow(dead_code)]
116 pub(crate) fn from_short_slice_unchecked(ext: u8, keys: ShortBoxSlice<Subtag>) -> Self {
117 assert!(ext.is_ascii_alphabetic());
118 // Safety invariant upheld here: ext checked as ASCII above
119 Self { ext, keys }
120 }
121
122 #[cfg(feature = "alloc")]
123 pub(crate) fn try_from_iter(ext: u8, iter: &mut SubtagIterator) -> Result<Self, ParseError> {
124 debug_assert!(matches!(
125 ExtensionType::try_from_byte(ext),
126 Ok(ExtensionType::Other(_)),
127 ));
128
129 let mut keys = ShortBoxSlice::new();
130 while let Some(subtag) = iter.peek() {
131 if !Subtag::valid_key(subtag) {
132 break;
133 }
134 if let Ok(key) = Subtag::try_from_utf8(subtag) {
135 keys.push(key);
136 }
137 iter.next();
138 }
139
140 if keys.is_empty() {
141 Err(ParseError::InvalidExtension)
142 } else {
143 Ok(Self::from_short_slice_unchecked(ext, keys))
144 }
145 }
146
147 /// Gets the tag character for this extension as a &str.
148 ///
149 /// # Examples
150 ///
151 /// ```
152 /// use icu::locale::Locale;
153 ///
154 /// let loc: Locale = "und-a-hello-world".parse().unwrap();
155 /// let other_ext = &loc.extensions.other[0];
156 /// assert_eq!(other_ext.get_ext_str(), "a");
157 /// ```
158 pub fn get_ext_str(&self) -> &str {
159 debug_assert!(self.ext.is_ascii_alphabetic());
160 // Safety: from safety invariant on self.ext (that it is ASCII)
161 unsafe { core::str::from_utf8_unchecked(core::slice::from_ref(&self.ext)) }
162 }
163
164 /// Gets the tag character for this extension as a char.
165 ///
166 /// # Examples
167 ///
168 /// ```
169 /// use icu::locale::Locale;
170 ///
171 /// let loc: Locale = "und-a-hello-world".parse().unwrap();
172 /// let other_ext = &loc.extensions.other[0];
173 /// assert_eq!(other_ext.get_ext(), 'a');
174 /// ```
175 pub fn get_ext(&self) -> char {
176 self.ext as char
177 }
178
179 /// Gets the tag character for this extension as a byte.
180 ///
181 /// # Examples
182 ///
183 /// ```
184 /// use icu::locale::Locale;
185 ///
186 /// let loc: Locale = "und-a-hello-world".parse().unwrap();
187 /// let other_ext = &loc.extensions.other[0];
188 /// assert_eq!(other_ext.get_ext_byte(), b'a');
189 /// ```
190 pub fn get_ext_byte(&self) -> u8 {
191 self.ext
192 }
193
194 pub(crate) fn for_each_subtag_str<E, F>(&self, f: &mut F, with_ext: bool) -> Result<(), E>
195 where
196 F: FnMut(&str) -> Result<(), E>,
197 {
198 if self.keys.is_empty() {
199 return Ok(());
200 }
201
202 if with_ext {
203 f(self.get_ext_str())?;
204 }
205 self.keys.iter().map(|t| t.as_str()).try_for_each(f)
206 }
207}
208
209/// ✨ *Enabled with the `alloc` Cargo feature.*
210#[cfg(feature = "alloc")]
211impl FromStr for Other {
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
220writeable::impl_display_with_writeable!(Other, #[cfg(feature = "alloc")]);
221
222impl writeable::Writeable for Other {
223 fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result {
224 if self.keys.is_empty() {
225 return Ok(());
226 }
227 sink.write_str(self.get_ext_str())?;
228 for key in self.keys.iter() {
229 sink.write_char('-')?;
230 writeable::Writeable::write_to(key, sink)?;
231 }
232
233 Ok(())
234 }
235
236 fn writeable_length_hint(&self) -> writeable::LengthHint {
237 if self.keys.is_empty() {
238 return writeable::LengthHint::exact(0);
239 };
240 let mut result = writeable::LengthHint::exact(1);
241 for key in self.keys.iter() {
242 result += writeable::Writeable::writeable_length_hint(key) + 1;
243 }
244 result
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn test_other_extension_fromstr() {
254 let oe: Other = "o-foo-bar".parse().expect("Failed to parse Other");
255 assert_eq!(oe.to_string(), "o-foo-bar");
256
257 let oe: Result<Other, _> = "o".parse();
258 assert!(oe.is_err());
259 }
260}