• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zbraniecki / icu4x / 13958601093

19 Mar 2025 04:17PM UTC coverage: 74.164% (-1.5%) from 75.71%
13958601093

push

github

web-flow
Clean up properties docs (#6315)

58056 of 78281 relevant lines covered (74.16%)

819371.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.24
/components/pattern/src/double.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 [`DoublePlaceholder`] pattern backend.
6

7
use core::convert::Infallible;
8
use core::{cmp::Ordering, str::FromStr};
9
use either::Either;
10
use writeable::adapters::WriteableAsTryWriteableInfallible;
11
use writeable::Writeable;
12

13
use crate::common::*;
14
use crate::Error;
15

16
#[cfg(feature = "alloc")]
17
use alloc::{boxed::Box, string::String};
18

19
/// A two-value enum for the [`DoublePlaceholder`] pattern backend.
20
///
21
/// # Examples
22
///
23
/// ```
24
/// use core::cmp::Ordering;
25
/// use core::str::FromStr;
26
/// use icu_pattern::DoublePlaceholderKey;
27
/// use icu_pattern::DoublePlaceholderPattern;
28
/// use icu_pattern::PatternItem;
29
///
30
/// // Parse the string syntax
31
/// let pattern = DoublePlaceholderPattern::try_from_str(
32
///     "Hello, {0} and {1}!",
33
///     Default::default(),
34
/// )
35
/// .unwrap();
36
///
37
/// assert_eq!(
38
///     pattern.iter().cmp(
39
///         [
40
///             PatternItem::Literal("Hello, "),
41
///             PatternItem::Placeholder(DoublePlaceholderKey::Place0),
42
///             PatternItem::Literal(" and "),
43
///             PatternItem::Placeholder(DoublePlaceholderKey::Place1),
44
///             PatternItem::Literal("!")
45
///         ]
46
///         .into_iter()
47
///     ),
48
///     Ordering::Equal
49
/// );
50
/// ```
51
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
559✔
52
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
×
53
#[allow(clippy::exhaustive_enums)] // Defined to have two entries
54
pub enum DoublePlaceholderKey {
×
55
    /// The placeholder `{0}`.
56
    Place0,
×
57
    /// The placeholder `{1}`.
58
    Place1,
×
59
}
60

61
impl FromStr for DoublePlaceholderKey {
62
    type Err = Error;
63
    fn from_str(s: &str) -> Result<Self, Self::Err> {
317✔
64
        match s {
65
            "0" => Ok(Self::Place0),
317✔
66
            "1" => Ok(Self::Place1),
159✔
67
            _ => Err(Error::InvalidPlaceholder),
×
68
        }
69
    }
317✔
70
}
71

72
impl<W0, W1> PlaceholderValueProvider<DoublePlaceholderKey> for (W0, W1)
73
where
74
    W0: Writeable,
75
    W1: Writeable,
76
{
77
    type Error = Infallible;
78

79
    type W<'a>
80
        = WriteableAsTryWriteableInfallible<Either<&'a W0, &'a W1>>
81
    where
82
        Self: 'a;
83

84
    type L<'a, 'l>
85
        = &'l str
86
    where
87
        Self: 'a;
88

89
    #[inline]
90
    fn value_for(&self, key: DoublePlaceholderKey) -> Self::W<'_> {
307✔
91
        let writeable = match key {
307✔
92
            DoublePlaceholderKey::Place0 => Either::Left(&self.0),
153✔
93
            DoublePlaceholderKey::Place1 => Either::Right(&self.1),
154✔
94
        };
95
        WriteableAsTryWriteableInfallible(writeable)
307✔
96
    }
307✔
97
    #[inline]
98
    fn map_literal<'a, 'l>(&'a self, literal: &'l str) -> Self::L<'a, 'l> {
125✔
99
        literal
100
    }
125✔
101
}
102

103
impl<W> PlaceholderValueProvider<DoublePlaceholderKey> for [W; 2]
104
where
105
    W: Writeable,
106
{
107
    type Error = Infallible;
108

109
    type W<'a>
110
        = WriteableAsTryWriteableInfallible<&'a W>
111
    where
112
        Self: 'a;
113

114
    type L<'a, 'l>
115
        = &'l str
116
    where
117
        Self: 'a;
118

119
    #[inline]
120
    fn value_for(&self, key: DoublePlaceholderKey) -> Self::W<'_> {
78✔
121
        let [item0, item1] = self;
78✔
122
        let writeable = match key {
78✔
123
            DoublePlaceholderKey::Place0 => item0,
39✔
124
            DoublePlaceholderKey::Place1 => item1,
39✔
125
        };
126
        WriteableAsTryWriteableInfallible(writeable)
78✔
127
    }
78✔
128
    #[inline]
129
    fn map_literal<'a, 'l>(&'a self, literal: &'l str) -> Self::L<'a, 'l> {
53✔
130
        literal
131
    }
53✔
132
}
133

134
/// Internal representation of a placeholder
135
#[derive(Debug, Copy, Clone)]
×
136
struct DoublePlaceholderInfo {
137
    /// The placeholder key: 0 or 1
138
    pub key: DoublePlaceholderKey,
×
139
    /// An offset field. This can take one of two forms:
140
    /// - Encoded form: 1 + offset from start of literals
141
    /// - Decoded form: offset from start of store
142
    pub offset: usize,
×
143
}
144

145
impl DoublePlaceholderInfo {
146
    pub fn from_char(ch: char) -> Self {
1,475✔
147
        Self {
1,475✔
148
            key: if ((ch as usize) & 0x1) == 0 {
1,475✔
149
                DoublePlaceholderKey::Place0
738✔
150
            } else {
151
                DoublePlaceholderKey::Place1
737✔
152
            },
153
            offset: (ch as usize) >> 1,
1,475✔
154
        }
155
    }
1,475✔
156
    #[cfg(feature = "alloc")]
157
    pub fn try_to_char(self) -> Result<char, Error> {
413✔
158
        let usize_val = match self.key {
826✔
159
            DoublePlaceholderKey::Place0 => 0,
206✔
160
            DoublePlaceholderKey::Place1 => 1,
207✔
161
        } | (self.offset << 1);
413✔
162
        u32::try_from(usize_val)
826✔
163
            .ok()
164
            .and_then(|x| char::try_from(x).ok())
411✔
165
            .ok_or(Error::InvalidPattern)
413✔
166
    }
413✔
167
    /// Creates a PlaceholderInfo for an empty Place0
168
    pub fn no_place0() -> Self {
6✔
169
        Self {
6✔
170
            key: DoublePlaceholderKey::Place0,
6✔
171
            offset: 0,
172
        }
173
    }
6✔
174
    /// Changes Place0 to Place1 and vice-versa
175
    pub fn swap(self) -> Self {
13✔
176
        Self {
13✔
177
            key: match self.key {
13✔
178
                DoublePlaceholderKey::Place0 => DoublePlaceholderKey::Place1,
8✔
179
                DoublePlaceholderKey::Place1 => DoublePlaceholderKey::Place0,
5✔
180
            },
181
            offset: self.offset,
13✔
182
        }
183
    }
13✔
184
    /// Sets the offset to 0 (ignored placeholder), retaining the key
185
    #[cfg(feature = "alloc")]
186
    pub fn clear(self) -> Self {
10✔
187
        Self {
188
            key: self.key,
189
            offset: 0,
190
        }
191
    }
10✔
192
}
193

194
/// Backend for patterns containing zero, one, or two placeholders.
195
///
196
/// This empty type is not constructible.
197
///
198
/// # Placeholder Keys
199
///
200
/// The placeholder is either [`DoublePlaceholderKey::Place0`] or [`DoublePlaceholderKey::Place1`].
201
///
202
/// In [`Pattern::interpolate()`], pass a two-element array or tuple.
203
///
204
/// # Encoding Details
205
///
206
/// The first two code points contain the indices of the first and second placeholders with
207
/// the following encoding:
208
///
209
/// 1. First bit: 0 for [`DoublePlaceholderKey::Place0`], 1 for [`DoublePlaceholderKey::Place1`].
210
/// 2. Second and higher bits: 1 plus the byte offset of the placeholder counting from after
211
///    the placeholder index code points. If zero, skip this placeholder.
212
///
213
/// # Examples
214
///
215
/// Parsing a pattern into the encoding:
216
///
217
/// ```
218
/// use core::str::FromStr;
219
/// use icu_pattern::DoublePlaceholder;
220
/// use icu_pattern::Pattern;
221
///
222
/// // Parse the string syntax and check the resulting data store:
223
/// let pattern = Pattern::<DoublePlaceholder>::try_from_str(
224
///     "Hello, {0} and {1}!",
225
///     Default::default(),
226
/// )
227
/// .unwrap();
228
///
229
/// assert_eq!("\x10\x1BHello,  and !", &pattern.store);
230
/// ```
231
///
232
/// Explanation of the lead code points:
233
///
234
/// 1. `\x10` is placeholder 0 at index 7: ((7 + 1) << 1) | 0
235
/// 2. `\x1B` is placeholder 1 at index 12: ((12 + 1) << 1) | 1
236
///
237
/// Example patterns supported by this backend:
238
///
239
/// ```
240
/// use core::str::FromStr;
241
/// use icu_pattern::DoublePlaceholder;
242
/// use icu_pattern::Pattern;
243
/// use icu_pattern::QuoteMode;
244
///
245
/// // Single numeric placeholder (note, "5" is used):
246
/// assert_eq!(
247
///     Pattern::<DoublePlaceholder>::try_from_str(
248
///         "{0} days ago",
249
///         Default::default()
250
///     )
251
///     .unwrap()
252
///     .interpolate_to_string([5, 7]),
253
///     "5 days ago",
254
/// );
255
///
256
/// // No placeholder (note, the placeholder value is never accessed):
257
/// assert_eq!(
258
///     Pattern::<DoublePlaceholder>::try_from_str(
259
///         "yesterday",
260
///         Default::default()
261
///     )
262
///     .unwrap()
263
///     .interpolate_to_string(["foo", "bar"]),
264
///     "yesterday",
265
/// );
266
///
267
/// // Escaped placeholder and a placeholder value 1 (note, "bar" is used):
268
/// assert_eq!(
269
///     Pattern::<DoublePlaceholder>::try_from_str(
270
///         "'{0}' {1}",
271
///         QuoteMode::QuotingSupported.into()
272
///     )
273
///     .unwrap()
274
///     .interpolate_to_string(("foo", "bar")),
275
///     "{0} bar",
276
/// );
277
///
278
/// // Pattern with the placeholders in the opposite order:
279
/// assert_eq!(
280
///     Pattern::<DoublePlaceholder>::try_from_str(
281
///         "A {1} B {0} C",
282
///         Default::default()
283
///     )
284
///     .unwrap()
285
///     .interpolate_to_string(("D", "E")),
286
///     "A E B D C",
287
/// );
288
///
289
/// // No literals, only placeholders:
290
/// assert_eq!(
291
///     Pattern::<DoublePlaceholder>::try_from_str(
292
///         "{1}{0}",
293
///         Default::default()
294
///     )
295
///     .unwrap()
296
///     .interpolate_to_string((1, "A")),
297
///     "A1",
298
/// );
299
/// ```
300
///
301
/// [`Pattern::interpolate()`]: crate::Pattern::interpolate
302
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
×
303
#[allow(clippy::exhaustive_enums)] // Empty Enum
304
pub enum DoublePlaceholder {}
305

306
impl crate::private::Sealed for DoublePlaceholder {}
307

308
impl PatternBackend for DoublePlaceholder {
309
    type PlaceholderKey<'a> = DoublePlaceholderKey;
310
    #[cfg(feature = "alloc")]
311
    type PlaceholderKeyCow<'a> = DoublePlaceholderKey;
312
    type Error<'a> = Infallible;
313
    type Store = str;
314
    type Iter<'a> = DoublePlaceholderPatternIterator<'a>;
315

316
    fn validate_store(store: &Self::Store) -> Result<(), Error> {
318✔
317
        let mut chars = store.chars();
318✔
318
        let ph_first_char = chars.next().ok_or(Error::InvalidPattern)?;
318✔
319
        let ph_second_char = chars.next().ok_or(Error::InvalidPattern)?;
317✔
320
        let initial_offset = ph_first_char.len_utf8() + ph_second_char.len_utf8();
316✔
321
        let ph_first = DoublePlaceholderInfo::from_char(ph_first_char);
316✔
322
        let ph_second = DoublePlaceholderInfo::from_char(ph_second_char);
316✔
323
        if ph_first.key == ph_second.key {
316✔
324
            return Err(Error::InvalidPattern);
1✔
325
        }
326
        if ph_first.offset > ph_second.offset && ph_second.offset > 0 {
315✔
327
            return Err(Error::InvalidPattern);
1✔
328
        }
329
        if ph_second.offset > store.len() - initial_offset + 1 {
314✔
330
            return Err(Error::InvalidPattern);
1✔
331
        }
332
        if (ph_second_char as usize) >= 0xD800 {
313✔
333
            return Err(Error::InvalidPattern);
1✔
334
        }
335
        Ok(())
312✔
336
    }
318✔
337

338
    fn iter_items(store: &Self::Store) -> Self::Iter<'_> {
209✔
339
        let mut chars = store.chars();
209✔
340
        let (mut ph_first, ph_first_len) = match chars.next() {
209✔
341
            Some(ch) => (DoublePlaceholderInfo::from_char(ch), ch.len_utf8()),
209✔
342
            None => {
343
                debug_assert!(false);
×
344
                (DoublePlaceholderInfo::no_place0(), 0)
345
            }
346
        };
347
        let (mut ph_second, ph_second_len) = match chars.next() {
209✔
348
            Some(ch) => (DoublePlaceholderInfo::from_char(ch), ch.len_utf8()),
209✔
349
            None => {
350
                debug_assert!(false);
×
351
                (ph_first.swap(), ph_first_len)
352
            }
353
        };
354
        let initial_offset = ph_first_len + ph_second_len;
209✔
355
        ph_first.offset += initial_offset - 1;
209✔
356
        ph_second.offset += initial_offset - 1;
209✔
357
        DoublePlaceholderPatternIterator {
209✔
358
            store,
359
            ph_first,
209✔
360
            ph_second,
209✔
361
            current_offset: initial_offset,
362
        }
363
    }
209✔
364

365
    #[cfg(feature = "alloc")]
366
    fn try_from_items<
821✔
367
        'cow,
368
        'ph,
369
        I: Iterator<Item = Result<PatternItemCow<'cow, Self::PlaceholderKey<'ph>>, Error>>,
370
    >(
371
        items: I,
372
    ) -> Result<Box<str>, Error> {
373
        let mut result = String::new();
821✔
374
        let mut first_ph = None;
1,061✔
375
        let mut second_ph = None;
1,061✔
376
        for item in items {
962✔
377
            match item? {
686✔
378
                PatternItemCow::Literal(s) => result.push_str(&s),
303✔
379
                PatternItemCow::Placeholder(ph_key) => {
385✔
380
                    if second_ph.is_some() {
381✔
381
                        return Err(Error::InvalidPattern);
×
382
                    }
383
                    let placeholder_offset = result.len() + 1;
383✔
384
                    if placeholder_offset >= 0xD800 {
381✔
385
                        return Err(Error::InvalidPattern);
×
386
                    }
387
                    let ph_info = DoublePlaceholderInfo {
381✔
388
                        key: ph_key,
389
                        offset: placeholder_offset,
390
                    };
391
                    if first_ph.is_none() {
381✔
392
                        first_ph.replace(ph_info);
194✔
393
                    } else {
394
                        second_ph.replace(ph_info);
187✔
395
                    }
396
                }
397
            }
398
        }
197✔
399
        let (first_ph, second_ph) = match (first_ph, second_ph) {
394✔
400
            (Some(a), Some(b)) => (a, b),
187✔
401
            (Some(a), None) => (a, a.swap().clear()),
8✔
402
            (None, None) => (
2✔
403
                DoublePlaceholderInfo::no_place0(),
2✔
404
                DoublePlaceholderInfo::no_place0().swap(),
2✔
405
            ),
2✔
406
            (None, Some(_)) => unreachable!("first_ph always populated before second_ph"),
×
407
        };
408
        if first_ph.key == second_ph.key {
197✔
409
            return Err(Error::InvalidPattern);
×
410
        }
411

412
        result.insert(0, second_ph.try_to_char()?);
197✔
413
        result.insert(0, first_ph.try_to_char()?);
197✔
414

415
        Ok(result.into_boxed_str())
199✔
416
    }
437✔
417

418
    fn empty() -> &'static Self::Store {
57✔
419
        "\0\u{1}"
420
    }
57✔
421
}
422

423
#[doc(hidden)] // TODO(#4467): Should be internal
424
#[derive(Debug)]
×
425
pub struct DoublePlaceholderPatternIterator<'a> {
426
    store: &'a str,
427
    ph_first: DoublePlaceholderInfo,
×
428
    ph_second: DoublePlaceholderInfo,
×
429
    current_offset: usize,
×
430
}
431

432
// Note: This impl is not exported via public bounds, but it might be in the
433
// future, and the compiler might be able to find it. The code is also
434
// reachable from `Iterator::size_hint`.
435
impl ExactSizeIterator for DoublePlaceholderPatternIterator<'_> {
436
    fn len(&self) -> usize {
205✔
437
        let mut chars = self.store.chars();
205✔
438
        let (mut ph_first, ph_first_len) = match chars.next() {
205✔
439
            Some(ch) => (DoublePlaceholderInfo::from_char(ch), ch.len_utf8()),
205✔
440
            None => {
441
                debug_assert!(false);
×
442
                (DoublePlaceholderInfo::no_place0(), 0)
443
            }
444
        };
445
        let (mut ph_second, ph_second_len) = match chars.next() {
205✔
446
            Some(ch) => (DoublePlaceholderInfo::from_char(ch), ch.len_utf8()),
205✔
447
            None => {
448
                debug_assert!(false);
×
449
                (ph_first.swap(), ph_first_len)
450
            }
451
        };
452
        let initial_offset = ph_first_len + ph_second_len;
205✔
453
        ph_first.offset += initial_offset - 1;
205✔
454
        ph_second.offset += initial_offset - 1;
205✔
455
        let store_len = self.store.len();
205✔
456

457
        #[allow(clippy::comparison_chain)]
458
        if ph_first.offset < initial_offset {
205✔
459
            // No placeholders
460
            if initial_offset < store_len {
2✔
461
                // No placeholder, non-empty literal
462
                1
1✔
463
            } else {
464
                // No placeholder, empty literal
465
                0
1✔
466
            }
467
        } else if ph_first.offset == initial_offset {
203✔
468
            // At least 1 placeholder, empty prefix
469
            if ph_second.offset < initial_offset {
161✔
470
                // Single placeholder
471
                if ph_first.offset < store_len {
5✔
472
                    // Single placeholder, empty prefix, non-empty suffix
473
                    2
3✔
474
                } else {
475
                    // Single placeholder, empty prefix, empty suffix
476
                    1
2✔
477
                }
478
            } else if ph_second.offset == ph_first.offset {
156✔
479
                // Two placeholders, empty infix
480
                if ph_first.offset < store_len {
52✔
481
                    // Two placeholders, empty prefix, empty infix, non-empty suffix
482
                    3
17✔
483
                } else {
484
                    // Two placeholders, empty prefix, empty infix, empty suffix
485
                    2
35✔
486
                }
487
            } else if ph_second.offset < store_len {
104✔
488
                // Two placeholders, empty prefix, non-empty infix, non-empty suffix
489
                4
2✔
490
            } else {
491
                // Two placeholders, empty prefix, non-empty infix, empty suffix
492
                3
102✔
493
            }
494
        } else {
495
            // At least 1 placeholder, non-empty prefix
496
            if ph_second.offset < initial_offset {
42✔
497
                // Single placeholder
498
                if ph_first.offset < store_len {
5✔
499
                    // Single placeholder, non-empty prefix, non-empty suffix
500
                    3
2✔
501
                } else {
502
                    // Single placeholder, non-empty prefix, empty suffix
503
                    2
3✔
504
                }
505
            } else if ph_second.offset == ph_first.offset {
37✔
506
                // Two placeholders, empty infix
507
                if ph_first.offset < store_len {
22✔
508
                    // Two placeholders, non-empty prefix, empty infix, non-empty suffix
509
                    4
2✔
510
                } else {
511
                    // Two placeholders, non-empty prefix, empty infix, empty suffix
512
                    3
20✔
513
                }
514
            } else if ph_second.offset < store_len {
15✔
515
                // Two placeholders, non-empty prefix, non-empty infix, non-empty suffix
516
                5
10✔
517
            } else {
518
                // Two placeholders, non-empty prefix, non-empty infix, empty suffix
519
                4
5✔
520
            }
521
        }
522
    }
205✔
523
}
524

525
impl<'a> Iterator for DoublePlaceholderPatternIterator<'a> {
526
    type Item = PatternItem<'a, DoublePlaceholderKey>;
527
    fn next(&mut self) -> Option<Self::Item> {
817✔
528
        match self.current_offset.cmp(&self.ph_first.offset) {
817✔
529
            Ordering::Less => {
530
                // Prefix
531
                let literal_str = match self.store.get(self.current_offset..self.ph_first.offset) {
43✔
532
                    Some(s) => s,
43✔
533
                    None => {
534
                        debug_assert!(false, "offsets are in range");
×
535
                        ""
536
                    }
537
                };
538
                self.current_offset = self.ph_first.offset;
43✔
539
                Some(PatternItem::Literal(literal_str))
43✔
540
            }
43✔
541
            Ordering::Equal => {
542
                // Placeholder
543
                self.ph_first.offset = 0;
204✔
544
                Some(PatternItem::Placeholder(self.ph_first.key))
204✔
545
            }
546
            Ordering::Greater => match self.current_offset.cmp(&self.ph_second.offset) {
570✔
547
                Ordering::Less => {
548
                    // Infix
549
                    let literal_str =
550
                        match self.store.get(self.current_offset..self.ph_second.offset) {
124✔
551
                            Some(s) => s,
124✔
552
                            None => {
553
                                debug_assert!(false, "offsets are in range");
×
554
                                ""
555
                            }
556
                        };
557
                    self.current_offset = self.ph_second.offset;
124✔
558
                    Some(PatternItem::Literal(literal_str))
124✔
559
                }
124✔
560
                Ordering::Equal => {
561
                    // Placeholder
562
                    self.ph_second.offset = 0;
198✔
563
                    Some(PatternItem::Placeholder(self.ph_second.key))
198✔
564
                }
565
                Ordering::Greater => {
566
                    // Suffix or end of string
567
                    let literal_str = match self.store.get(self.current_offset..) {
248✔
568
                        Some(s) => s,
248✔
569
                        None => {
570
                            debug_assert!(false, "offsets are in range");
×
571
                            ""
572
                        }
573
                    };
574
                    if literal_str.is_empty() {
248✔
575
                        // End of string
576
                        None
210✔
577
                    } else {
578
                        // Suffix
579
                        self.current_offset = self.store.len();
38✔
580
                        Some(PatternItem::Literal(literal_str))
38✔
581
                    }
582
                }
583
            },
584
        }
585
    }
817✔
586

587
    fn size_hint(&self) -> (usize, Option<usize>) {
207✔
588
        let len = self.len();
207✔
589
        (len, Some(len))
207✔
590
    }
207✔
591
}
592

593
#[cfg(test)]
594
mod tests {
595
    use super::*;
596
    use crate::DoublePlaceholderPattern;
597

598
    #[test]
599
    fn test_permutations() {
2✔
600
        #[derive(Debug)]
×
601
        struct TestCase<'a> {
602
            pattern: &'a str,
603
            store: &'a str,
×
604
            /// Interpolation with 0=apple, 1=orange
605
            interpolated: &'a str,
×
606
        }
607
        let cases = [
1✔
608
            TestCase {
1✔
609
                pattern: "",
610
                store: "\x00\x01",
611
                interpolated: "",
612
            },
613
            TestCase {
1✔
614
                pattern: "{0}",
615
                store: "\x02\x01",
616
                interpolated: "apple",
617
            },
618
            TestCase {
1✔
619
                pattern: "X{0}",
620
                store: "\x04\x01X",
621
                interpolated: "Xapple",
622
            },
623
            TestCase {
1✔
624
                pattern: "{0}Y",
625
                store: "\x02\x01Y",
626
                interpolated: "appleY",
627
            },
628
            TestCase {
1✔
629
                pattern: "X{0}Y",
630
                store: "\x04\x01XY",
631
                interpolated: "XappleY",
632
            },
633
            TestCase {
1✔
634
                pattern: "{1}",
635
                store: "\x03\x00",
636
                interpolated: "orange",
637
            },
638
            TestCase {
1✔
639
                pattern: "X{1}",
640
                store: "\x05\x00X",
641
                interpolated: "Xorange",
642
            },
643
            TestCase {
1✔
644
                pattern: "{1}Y",
645
                store: "\x03\x00Y",
646
                interpolated: "orangeY",
647
            },
648
            TestCase {
1✔
649
                pattern: "X{1}Y",
650
                store: "\x05\x00XY",
651
                interpolated: "XorangeY",
652
            },
653
            TestCase {
1✔
654
                pattern: "{0}{1}",
655
                store: "\x02\x03",
656
                interpolated: "appleorange",
657
            },
658
            TestCase {
1✔
659
                pattern: "X{0}{1}",
660
                store: "\x04\x05X",
661
                interpolated: "Xappleorange",
662
            },
663
            TestCase {
1✔
664
                pattern: "{0}Y{1}",
665
                store: "\x02\x05Y",
666
                interpolated: "appleYorange",
667
            },
668
            TestCase {
1✔
669
                pattern: "{0}{1}Z",
670
                store: "\x02\x03Z",
671
                interpolated: "appleorangeZ",
672
            },
673
            TestCase {
1✔
674
                pattern: "X{0}Y{1}",
675
                store: "\x04\x07XY",
676
                interpolated: "XappleYorange",
677
            },
678
            TestCase {
1✔
679
                pattern: "X{0}{1}Z",
680
                store: "\x04\x05XZ",
681
                interpolated: "XappleorangeZ",
682
            },
683
            TestCase {
1✔
684
                pattern: "{0}Y{1}Z",
685
                store: "\x02\x05YZ",
686
                interpolated: "appleYorangeZ",
687
            },
688
            TestCase {
1✔
689
                pattern: "X{0}Y{1}Z",
690
                store: "\x04\x07XYZ",
691
                interpolated: "XappleYorangeZ",
692
            },
693
            TestCase {
1✔
694
                pattern: "{1}{0}",
695
                store: "\x03\x02",
696
                interpolated: "orangeapple",
697
            },
698
            TestCase {
1✔
699
                pattern: "X{1}{0}",
700
                store: "\x05\x04X",
701
                interpolated: "Xorangeapple",
702
            },
703
            TestCase {
1✔
704
                pattern: "{1}Y{0}",
705
                store: "\x03\x04Y",
706
                interpolated: "orangeYapple",
707
            },
708
            TestCase {
1✔
709
                pattern: "{1}{0}Z",
710
                store: "\x03\x02Z",
711
                interpolated: "orangeappleZ",
712
            },
713
            TestCase {
1✔
714
                pattern: "X{1}Y{0}",
715
                store: "\x05\x06XY",
716
                interpolated: "XorangeYapple",
717
            },
718
            TestCase {
1✔
719
                pattern: "X{1}{0}Z",
720
                store: "\x05\x04XZ",
721
                interpolated: "XorangeappleZ",
722
            },
723
            TestCase {
1✔
724
                pattern: "{1}Y{0}Z",
725
                store: "\x03\x04YZ",
726
                interpolated: "orangeYappleZ",
727
            },
728
            TestCase {
1✔
729
                pattern: "X{1}Y{0}Z",
730
                store: "\x05\x06XYZ",
731
                interpolated: "XorangeYappleZ",
732
            },
733
            TestCase {
1✔
734
                pattern: "01234567890123456789012345678901234567890123456789012345678901234567890123456789{0}Y{1}Z",
735
                store: "\u{A2}\u{A5}01234567890123456789012345678901234567890123456789012345678901234567890123456789YZ",
736
                interpolated: "01234567890123456789012345678901234567890123456789012345678901234567890123456789appleYorangeZ",
737
            },
738
        ];
739
        for cas in cases {
1✔
740
            let TestCase {
741
                pattern,
26✔
742
                store,
26✔
743
                interpolated,
26✔
744
            } = cas;
745
            let parsed =
746
                DoublePlaceholderPattern::try_from_str(pattern, Default::default()).unwrap();
26✔
747
            let actual = parsed
52✔
748
                .interpolate(["apple", "orange"])
26✔
749
                .write_to_string()
750
                .into_owned();
751
            assert_eq!(&parsed.store, store, "{cas:?}");
26✔
752
            assert_eq!(actual, interpolated, "{cas:?}");
26✔
753
        }
27✔
754
    }
2✔
755

756
    #[test]
757
    fn test_invalid() {
2✔
758
        let cases = [
1✔
759
            "",               // too short
760
            "\x00",           // too short
761
            "\x00\x00",       // duplicate placeholders
762
            "\x04\x03",       // first offset is after second offset
763
            "\x04\x05",       // second offset out of range (also first offset)
764
            "\x04\u{10001}@", // second offset too large for UTF-8
765
        ];
766
        let long_str = "0123456789".repeat(1000000);
1✔
767
        for cas in cases {
1✔
768
            let cas = cas.replace('@', &long_str);
6✔
769
            assert!(DoublePlaceholder::validate_store(&cas).is_err(), "{cas:?}");
6✔
770
        }
7✔
771
    }
2✔
772
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc