icu_pattern/single.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//! Code for the [`SinglePlaceholder`] pattern backend.
6
7use core::convert::Infallible;
8use core::{cmp::Ordering, str::FromStr};
9use writeable::adapters::WriteableAsTryWriteableInfallible;
10use writeable::Writeable;
11
12use crate::common::*;
13use crate::Error;
14
15#[cfg(feature = "alloc")]
16use alloc::{boxed::Box, string::String};
17
18/// A singleton enum for the [`SinglePlaceholder`] pattern backend.
19///
20/// # Examples
21///
22/// ```
23/// use core::cmp::Ordering;
24/// use core::str::FromStr;
25/// use icu_pattern::PatternItem;
26/// use icu_pattern::SinglePlaceholder;
27/// use icu_pattern::SinglePlaceholderKey;
28/// use icu_pattern::SinglePlaceholderPattern;
29///
30/// // Parse the string syntax and check the resulting data store:
31/// let pattern = SinglePlaceholderPattern::try_from_str(
32/// "Hello, {0}!",
33/// Default::default(),
34/// )
35/// .unwrap();
36///
37/// assert_eq!(
38/// pattern.iter().cmp(
39/// [
40/// PatternItem::Literal("Hello, "),
41/// PatternItem::Placeholder(SinglePlaceholderKey::Singleton),
42/// PatternItem::Literal("!")
43/// ]
44/// .into_iter()
45/// ),
46/// Ordering::Equal
47/// );
48/// ```
49#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
50#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
51#[allow(clippy::exhaustive_enums)] // Singleton
52pub enum SinglePlaceholderKey {
53 Singleton,
54}
55
56impl FromStr for SinglePlaceholderKey {
57 type Err = core::convert::Infallible;
58 fn from_str(_: &str) -> Result<Self, Self::Err> {
59 Ok(Self::Singleton)
60 }
61}
62
63impl<W> PlaceholderValueProvider<SinglePlaceholderKey> for (W,)
64where
65 W: Writeable,
66{
67 type Error = Infallible;
68
69 type W<'a>
70 = WriteableAsTryWriteableInfallible<&'a W>
71 where
72 Self: 'a;
73
74 type L<'a, 'l>
75 = &'l str
76 where
77 Self: 'a;
78
79 fn value_for(&self, _key: SinglePlaceholderKey) -> Self::W<'_> {
80 WriteableAsTryWriteableInfallible(&self.0)
81 }
82 #[inline]
83 fn map_literal<'a, 'l>(&'a self, literal: &'l str) -> Self::L<'a, 'l> {
84 literal
85 }
86}
87
88impl<W> PlaceholderValueProvider<SinglePlaceholderKey> for [W; 1]
89where
90 W: Writeable,
91{
92 type Error = Infallible;
93
94 type W<'a>
95 = WriteableAsTryWriteableInfallible<&'a W>
96 where
97 Self: 'a;
98
99 type L<'a, 'l>
100 = &'l str
101 where
102 Self: 'a;
103
104 fn value_for(&self, _key: SinglePlaceholderKey) -> Self::W<'_> {
105 let [value] = self;
106 WriteableAsTryWriteableInfallible(value)
107 }
108 #[inline]
109 fn map_literal<'a, 'l>(&'a self, literal: &'l str) -> Self::L<'a, 'l> {
110 literal
111 }
112}
113
114/// Backend for patterns containing zero or one placeholder.
115///
116/// This empty type is not constructible.
117///
118/// # Placeholder Keys
119///
120/// The placeholder is always [`SinglePlaceholderKey::Singleton`].
121///
122/// In [`Pattern::interpolate()`], pass a single-element array or tuple.
123///
124/// # Encoding Details
125///
126/// The first code point of the string is 1 plus the byte offset of the placeholder counting from
127/// after that initial code point. If zero, there is no placeholder.
128///
129/// # Examples
130///
131/// Parsing a pattern into the encoding:
132///
133/// ```
134/// use core::str::FromStr;
135/// use icu_pattern::Pattern;
136/// use icu_pattern::SinglePlaceholder;
137///
138/// // Parse the string syntax and check the resulting data store:
139/// let pattern = Pattern::<SinglePlaceholder>::try_from_str(
140/// "Hello, {0}!",
141/// Default::default(),
142/// )
143/// .unwrap();
144///
145/// assert_eq!("\u{8}Hello, !", &pattern.store);
146/// ```
147///
148/// Example patterns supported by this backend:
149///
150/// ```
151/// use core::str::FromStr;
152/// use icu_pattern::Pattern;
153/// use icu_pattern::QuoteMode;
154/// use icu_pattern::SinglePlaceholder;
155///
156/// // Single numeric placeholder:
157/// assert_eq!(
158/// Pattern::<SinglePlaceholder>::try_from_str(
159/// "{0} days ago",
160/// Default::default()
161/// )
162/// .unwrap()
163/// .interpolate_to_string([5]),
164/// "5 days ago",
165/// );
166///
167/// // Single named placeholder:
168/// assert_eq!(
169/// Pattern::<SinglePlaceholder>::try_from_str(
170/// "{name}",
171/// Default::default()
172/// )
173/// .unwrap()
174/// .interpolate_to_string(["Alice"]),
175/// "Alice",
176/// );
177///
178/// // No placeholder (note, the placeholder value is never accessed):
179/// assert_eq!(
180/// Pattern::<SinglePlaceholder>::try_from_str(
181/// "yesterday",
182/// Default::default()
183/// )
184/// .unwrap()
185/// .interpolate_to_string(["hi"]),
186/// "yesterday",
187/// );
188///
189/// // Escaped placeholder and a real placeholder:
190/// assert_eq!(
191/// Pattern::<SinglePlaceholder>::try_from_str(
192/// "'{0}' {1}",
193/// QuoteMode::QuotingSupported.into()
194/// )
195/// .unwrap()
196/// .interpolate_to_string(("hi",)),
197/// "{0} hi",
198/// );
199/// ```
200///
201/// [`Pattern::interpolate()`]: crate::Pattern::interpolate
202#[derive(Debug, Copy, Clone, PartialEq, Eq)]
203#[allow(clippy::exhaustive_enums)] // Empty Enum
204pub enum SinglePlaceholder {}
205
206impl crate::private::Sealed for SinglePlaceholder {}
207
208impl PatternBackend for SinglePlaceholder {
209 type PlaceholderKey<'a> = SinglePlaceholderKey;
210 #[cfg(feature = "alloc")]
211 type PlaceholderKeyCow<'a> = SinglePlaceholderKey;
212 type Error<'a> = Infallible;
213 type Store = str;
214 type Iter<'a> = SinglePlaceholderPatternIterator<'a>;
215
216 fn validate_store(store: &Self::Store) -> Result<(), Error> {
217 let placeholder_offset_char = store.chars().next().ok_or(Error::InvalidPattern)?;
218 let initial_offset = placeholder_offset_char.len_utf8();
219 let placeholder_offset = placeholder_offset_char as usize;
220 if placeholder_offset > store.len() - initial_offset + 1 {
221 return Err(Error::InvalidPattern);
222 }
223 if placeholder_offset >= 0xD800 {
224 return Err(Error::InvalidPattern);
225 }
226 Ok(())
227 }
228
229 fn iter_items(store: &Self::Store) -> Self::Iter<'_> {
230 let placeholder_offset_char = match store.chars().next() {
231 Some(i) => i,
232 None => {
233 debug_assert!(false);
234 '\0'
235 }
236 };
237 let initial_offset = placeholder_offset_char.len_utf8();
238 SinglePlaceholderPatternIterator {
239 store,
240 placeholder_offset: placeholder_offset_char as usize + initial_offset - 1,
241 current_offset: initial_offset,
242 }
243 }
244
245 #[cfg(feature = "alloc")]
246 fn try_from_items<
247 'cow,
248 'ph,
249 I: Iterator<Item = Result<PatternItemCow<'cow, Self::PlaceholderKey<'ph>>, Error>>,
250 >(
251 items: I,
252 ) -> Result<Box<str>, Error> {
253 let mut result = String::new();
254 let mut seen_placeholder = false;
255 for item in items {
256 match item? {
257 PatternItemCow::Literal(s) => result.push_str(&s),
258 PatternItemCow::Placeholder(_) if !seen_placeholder => {
259 seen_placeholder = true;
260 let placeholder_offset =
261 u32::try_from(result.len() + 1).map_err(|_| Error::InvalidPattern)?;
262 if placeholder_offset >= 0xD800 {
263 return Err(Error::InvalidPattern);
264 }
265 let placeholder_offset_char =
266 char::try_from(placeholder_offset).map_err(|_| Error::InvalidPattern)?;
267 result.insert(0, placeholder_offset_char);
268 }
269 PatternItemCow::Placeholder(_) => {
270 return Err(Error::InvalidPattern);
271 }
272 }
273 }
274 if !seen_placeholder {
275 result.insert(0, '\0');
276 }
277 Ok(result.into_boxed_str())
278 }
279
280 fn empty() -> &'static Self::Store {
281 "\0"
282 }
283}
284
285#[doc(hidden)] // TODO(#4467): Should be internal
286#[derive(Debug)]
287pub struct SinglePlaceholderPatternIterator<'a> {
288 store: &'a str,
289 placeholder_offset: usize,
290 current_offset: usize,
291}
292
293// Note: This impl is not exported via public bounds, but it might be in the
294// future, and the compiler might be able to find it. The code is also
295// reachable from `Iterator::size_hint`.
296impl ExactSizeIterator for SinglePlaceholderPatternIterator<'_> {
297 fn len(&self) -> usize {
298 let placeholder_offset_char = match self.store.chars().next() {
299 Some(i) => i,
300 None => {
301 debug_assert!(false);
302 '\0'
303 }
304 };
305 let initial_offset = placeholder_offset_char.len_utf8();
306 let placeholder_offset = placeholder_offset_char as usize + initial_offset - 1;
307 let store_len = self.store.len();
308 if placeholder_offset < initial_offset {
309 // No placeholder
310 if initial_offset < store_len {
311 // No placeholder, non-empty literal
312 1
313 } else {
314 // No placeholder, empty literal
315 0
316 }
317 } else if placeholder_offset == initial_offset {
318 // Has placeholder, empty prefix
319 if initial_offset < store_len {
320 // Has placeholder, empty prefix, non-empty suffix
321 2
322 } else {
323 // Has placeholder, empty prefix, empty suffix
324 1
325 }
326 } else if placeholder_offset < store_len {
327 // Has placeholder, non-empty prefix, non-empty suffix
328 3
329 } else {
330 // Has placeholder, non-empty prefix, empty suffix
331 2
332 }
333 }
334}
335
336impl<'a> Iterator for SinglePlaceholderPatternIterator<'a> {
337 type Item = PatternItem<'a, SinglePlaceholderKey>;
338 fn next(&mut self) -> Option<Self::Item> {
339 match self.current_offset.cmp(&self.placeholder_offset) {
340 Ordering::Less => {
341 // Prefix
342 let literal_str = match self.store.get(self.current_offset..self.placeholder_offset)
343 {
344 Some(s) => s,
345 None => {
346 debug_assert!(false, "offsets are in range");
347 ""
348 }
349 };
350 self.current_offset = self.placeholder_offset;
351 Some(PatternItem::Literal(literal_str))
352 }
353 Ordering::Equal => {
354 // Placeholder
355 self.placeholder_offset = 0;
356 Some(PatternItem::Placeholder(SinglePlaceholderKey::Singleton))
357 }
358 Ordering::Greater => {
359 // Suffix or end of string
360 let literal_str = match self.store.get(self.current_offset..) {
361 Some(s) => s,
362 None => {
363 debug_assert!(false, "offsets are in range");
364 ""
365 }
366 };
367 if literal_str.is_empty() {
368 // End of string
369 None
370 } else {
371 // Suffix
372 self.current_offset = self.store.len();
373 Some(PatternItem::Literal(literal_str))
374 }
375 }
376 }
377 }
378
379 fn size_hint(&self) -> (usize, Option<usize>) {
380 let len = self.len();
381 (len, Some(len))
382 }
383}
384
385// TODO(#1668): Add more tests