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