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

zbraniecki / icu4x / 12020603084

23 Nov 2024 08:43PM UTC coverage: 75.71% (+0.2%) from 75.477%
12020603084

push

github

sffc
Touch Cargo.lock

55589 of 73424 relevant lines covered (75.71%)

644270.14 hits per line

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

89.16
/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)]
488✔
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> {
323✔
64
        match s {
65
            "0" => Ok(Self::Place0),
323✔
66
            "1" => Ok(Self::Place1),
163✔
67
            _ => Err(Error::InvalidPlaceholder),
×
68
        }
69
    }
323✔
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
    type W<'a>
79
        = WriteableAsTryWriteableInfallible<Either<&'a W0, &'a W1>>
80
    where
81
        W0: 'a,
82
        W1: 'a;
83
    const LITERAL_PART: writeable::Part = crate::PATTERN_LITERAL_PART;
84
    #[inline]
85
    fn value_for(&self, key: DoublePlaceholderKey) -> (Self::W<'_>, writeable::Part) {
208✔
86
        let writeable = match key {
208✔
87
            DoublePlaceholderKey::Place0 => Either::Left(&self.0),
104✔
88
            DoublePlaceholderKey::Place1 => Either::Right(&self.1),
104✔
89
        };
90
        (
208✔
91
            WriteableAsTryWriteableInfallible(writeable),
208✔
92
            crate::PATTERN_PLACEHOLDER_PART,
93
        )
94
    }
208✔
95
}
96

97
impl<W> PlaceholderValueProvider<DoublePlaceholderKey> for [W; 2]
98
where
99
    W: Writeable,
100
{
101
    type Error = Infallible;
102
    type W<'a>
103
        = WriteableAsTryWriteableInfallible<&'a W>
104
    where
105
        W: 'a;
106
    const LITERAL_PART: writeable::Part = crate::PATTERN_LITERAL_PART;
107
    #[inline]
108
    fn value_for(&self, key: DoublePlaceholderKey) -> (Self::W<'_>, writeable::Part) {
78✔
109
        let [item0, item1] = self;
78✔
110
        let writeable = match key {
78✔
111
            DoublePlaceholderKey::Place0 => item0,
39✔
112
            DoublePlaceholderKey::Place1 => item1,
39✔
113
        };
114
        (
78✔
115
            WriteableAsTryWriteableInfallible(writeable),
78✔
116
            crate::PATTERN_PLACEHOLDER_PART,
117
        )
118
    }
78✔
119
}
120

121
/// Internal representation of a placeholder
122
#[derive(Debug, Copy, Clone)]
×
123
struct DoublePlaceholderInfo {
124
    /// The placeholder key: 0 or 1
125
    pub key: DoublePlaceholderKey,
×
126
    /// An offset field. This can take one of two forms:
127
    /// - Encoded form: 1 + offset from start of literals
128
    /// - Decoded form: offset from start of store
129
    pub offset: usize,
×
130
}
131

132
impl DoublePlaceholderInfo {
133
    pub fn from_char(ch: char) -> Self {
1,163✔
134
        Self {
1,163✔
135
            key: if ((ch as usize) & 0x1) == 0 {
1,163✔
136
                DoublePlaceholderKey::Place0
582✔
137
            } else {
138
                DoublePlaceholderKey::Place1
581✔
139
            },
140
            offset: (ch as usize) >> 1,
1,163✔
141
        }
142
    }
1,163✔
143
    #[cfg(feature = "alloc")]
144
    pub fn try_to_char(self) -> Result<char, Error> {
388✔
145
        let usize_val = match self.key {
776✔
146
            DoublePlaceholderKey::Place0 => 0,
195✔
147
            DoublePlaceholderKey::Place1 => 1,
193✔
148
        } | (self.offset << 1);
388✔
149
        u32::try_from(usize_val)
776✔
150
            .ok()
151
            .and_then(|x| char::try_from(x).ok())
387✔
152
            .ok_or(Error::InvalidPattern)
388✔
153
    }
388✔
154
    /// Creates a PlaceholderInfo for an empty Place0
155
    pub fn no_place0() -> Self {
6✔
156
        Self {
6✔
157
            key: DoublePlaceholderKey::Place0,
6✔
158
            offset: 0,
159
        }
160
    }
6✔
161
    /// Changes Place0 to Place1 and vice-versa
162
    pub fn swap(self) -> Self {
13✔
163
        Self {
13✔
164
            key: match self.key {
13✔
165
                DoublePlaceholderKey::Place0 => DoublePlaceholderKey::Place1,
8✔
166
                DoublePlaceholderKey::Place1 => DoublePlaceholderKey::Place0,
5✔
167
            },
168
            offset: self.offset,
13✔
169
        }
170
    }
13✔
171
    /// Sets the offset to 0 (ignored placeholder), retaining the key
172
    #[cfg(feature = "alloc")]
173
    pub fn clear(self) -> Self {
10✔
174
        Self {
175
            key: self.key,
176
            offset: 0,
177
        }
178
    }
10✔
179
}
180

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

293
impl crate::private::Sealed for DoublePlaceholder {}
294

295
impl PatternBackend for DoublePlaceholder {
296
    type PlaceholderKey<'a> = DoublePlaceholderKey;
297
    #[cfg(feature = "alloc")]
298
    type PlaceholderKeyCow<'a> = DoublePlaceholderKey;
299
    type Error<'a> = Infallible;
300
    type Store = str;
301
    type Iter<'a> = DoublePlaceholderPatternIterator<'a>;
302

303
    fn validate_store(store: &Self::Store) -> Result<(), Error> {
274✔
304
        let mut chars = store.chars();
274✔
305
        let ph_first_char = chars.next().ok_or(Error::InvalidPattern)?;
274✔
306
        let ph_second_char = chars.next().ok_or(Error::InvalidPattern)?;
273✔
307
        let initial_offset = ph_first_char.len_utf8() + ph_second_char.len_utf8();
272✔
308
        let ph_first = DoublePlaceholderInfo::from_char(ph_first_char);
272✔
309
        let ph_second = DoublePlaceholderInfo::from_char(ph_second_char);
272✔
310
        if ph_first.key == ph_second.key {
272✔
311
            return Err(Error::InvalidPattern);
1✔
312
        }
313
        if ph_first.offset > ph_second.offset && ph_second.offset > 0 {
271✔
314
            return Err(Error::InvalidPattern);
1✔
315
        }
316
        if ph_second.offset > store.len() - initial_offset + 1 {
270✔
317
            return Err(Error::InvalidPattern);
1✔
318
        }
319
        if (ph_second_char as usize) >= 0xD800 {
269✔
320
            return Err(Error::InvalidPattern);
1✔
321
        }
322
        Ok(())
268✔
323
    }
274✔
324

325
    fn iter_items(store: &Self::Store) -> Self::Iter<'_> {
153✔
326
        let mut chars = store.chars();
153✔
327
        let (mut ph_first, ph_first_len) = match chars.next() {
153✔
328
            Some(ch) => (DoublePlaceholderInfo::from_char(ch), ch.len_utf8()),
153✔
329
            None => {
330
                debug_assert!(false);
×
331
                (DoublePlaceholderInfo::no_place0(), 0)
332
            }
333
        };
334
        let (mut ph_second, ph_second_len) = match chars.next() {
153✔
335
            Some(ch) => (DoublePlaceholderInfo::from_char(ch), ch.len_utf8()),
153✔
336
            None => {
337
                debug_assert!(false);
×
338
                (ph_first.swap(), ph_first_len)
339
            }
340
        };
341
        let initial_offset = ph_first_len + ph_second_len;
153✔
342
        ph_first.offset += initial_offset - 1;
153✔
343
        ph_second.offset += initial_offset - 1;
153✔
344
        DoublePlaceholderPatternIterator {
153✔
345
            store,
346
            ph_first,
153✔
347
            ph_second,
153✔
348
            current_offset: initial_offset,
349
        }
350
    }
153✔
351

352
    #[cfg(feature = "alloc")]
353
    fn try_from_items<
738✔
354
        'cow,
355
        'ph,
356
        I: Iterator<Item = Result<PatternItemCow<'cow, Self::PlaceholderKey<'ph>>, Error>>,
357
    >(
358
        items: I,
359
    ) -> Result<Box<str>, Error> {
360
        let mut result = String::new();
738✔
361
        let mut first_ph = None;
888✔
362
        let mut second_ph = None;
888✔
363
        for item in items {
949✔
364
            match item? {
622✔
365
                PatternItemCow::Literal(s) => result.push_str(&s),
261✔
366
                PatternItemCow::Placeholder(ph_key) => {
362✔
367
                    if second_ph.is_some() {
362✔
368
                        return Err(Error::InvalidPattern);
×
369
                    }
370
                    let placeholder_offset = result.len() + 1;
362✔
371
                    if placeholder_offset >= 0xD800 {
361✔
372
                        return Err(Error::InvalidPattern);
×
373
                    }
374
                    let ph_info = DoublePlaceholderInfo {
363✔
375
                        key: ph_key,
376
                        offset: placeholder_offset,
377
                    };
378
                    if first_ph.is_none() {
361✔
379
                        first_ph.replace(ph_info);
184✔
380
                    } else {
381
                        second_ph.replace(ph_info);
178✔
382
                    }
383
                }
384
            }
385
        }
188✔
386
        let (first_ph, second_ph) = match (first_ph, second_ph) {
376✔
387
            (Some(a), Some(b)) => (a, b),
178✔
388
            (Some(a), None) => (a, a.swap().clear()),
8✔
389
            (None, None) => (
2✔
390
                DoublePlaceholderInfo::no_place0(),
2✔
391
                DoublePlaceholderInfo::no_place0().swap(),
2✔
392
            ),
2✔
393
            (None, Some(_)) => unreachable!("first_ph always populated before second_ph"),
×
394
        };
395
        if first_ph.key == second_ph.key {
188✔
396
            return Err(Error::InvalidPattern);
×
397
        }
398

399
        result.insert(0, second_ph.try_to_char()?);
187✔
400
        result.insert(0, first_ph.try_to_char()?);
189✔
401

402
        Ok(result.into_boxed_str())
189✔
403
    }
334✔
404

405
    fn empty() -> &'static Self::Store {
19✔
406
        "\0\u{1}"
407
    }
19✔
408
}
409

410
#[doc(hidden)] // TODO(#4467): Should be internal
411
#[derive(Debug)]
×
412
pub struct DoublePlaceholderPatternIterator<'a> {
413
    store: &'a str,
414
    ph_first: DoublePlaceholderInfo,
×
415
    ph_second: DoublePlaceholderInfo,
×
416
    current_offset: usize,
×
417
}
418

419
// Note: This impl is not exported via public bounds, but it might be in the
420
// future, and the compiler might be able to find it. The code is also
421
// reachable from `Iterator::size_hint`.
422
impl ExactSizeIterator for DoublePlaceholderPatternIterator<'_> {
423
    fn len(&self) -> usize {
156✔
424
        let mut chars = self.store.chars();
156✔
425
        let (mut ph_first, ph_first_len) = match chars.next() {
156✔
426
            Some(ch) => (DoublePlaceholderInfo::from_char(ch), ch.len_utf8()),
156✔
427
            None => {
428
                debug_assert!(false);
×
429
                (DoublePlaceholderInfo::no_place0(), 0)
430
            }
431
        };
432
        let (mut ph_second, ph_second_len) = match chars.next() {
156✔
433
            Some(ch) => (DoublePlaceholderInfo::from_char(ch), ch.len_utf8()),
156✔
434
            None => {
435
                debug_assert!(false);
×
436
                (ph_first.swap(), ph_first_len)
437
            }
438
        };
439
        let initial_offset = ph_first_len + ph_second_len;
156✔
440
        ph_first.offset += initial_offset - 1;
156✔
441
        ph_second.offset += initial_offset - 1;
156✔
442
        let store_len = self.store.len();
156✔
443

444
        #[allow(clippy::comparison_chain)]
445
        if ph_first.offset < initial_offset {
156✔
446
            // No placeholders
447
            if initial_offset < store_len {
2✔
448
                // No placeholder, non-empty literal
449
                1
1✔
450
            } else {
451
                // No placeholder, empty literal
452
                0
1✔
453
            }
454
        } else if ph_first.offset == initial_offset {
154✔
455
            // At least 1 placeholder, empty prefix
456
            if ph_second.offset < initial_offset {
109✔
457
                // Single placeholder
458
                if ph_first.offset < store_len {
5✔
459
                    // Single placeholder, empty prefix, non-empty suffix
460
                    2
3✔
461
                } else {
462
                    // Single placeholder, empty prefix, empty suffix
463
                    1
2✔
464
                }
465
            } else if ph_second.offset == ph_first.offset {
104✔
466
                // Two placeholders, empty infix
467
                if ph_first.offset < store_len {
39✔
468
                    // Two placeholders, empty prefix, empty infix, non-empty suffix
469
                    3
17✔
470
                } else {
471
                    // Two placeholders, empty prefix, empty infix, empty suffix
472
                    2
22✔
473
                }
474
            } else if ph_second.offset < store_len {
65✔
475
                // Two placeholders, empty prefix, non-empty infix, non-empty suffix
476
                4
2✔
477
            } else {
478
                // Two placeholders, empty prefix, non-empty infix, empty suffix
479
                3
63✔
480
            }
481
        } else {
482
            // At least 1 placeholder, non-empty prefix
483
            if ph_second.offset < initial_offset {
45✔
484
                // Single placeholder
485
                if ph_first.offset < store_len {
5✔
486
                    // Single placeholder, non-empty prefix, non-empty suffix
487
                    3
2✔
488
                } else {
489
                    // Single placeholder, non-empty prefix, empty suffix
490
                    2
3✔
491
                }
492
            } else if ph_second.offset == ph_first.offset {
40✔
493
                // Two placeholders, empty infix
494
                if ph_first.offset < store_len {
22✔
495
                    // Two placeholders, non-empty prefix, empty infix, non-empty suffix
496
                    4
2✔
497
                } else {
498
                    // Two placeholders, non-empty prefix, empty infix, empty suffix
499
                    3
20✔
500
                }
501
            } else if ph_second.offset < store_len {
18✔
502
                // Two placeholders, non-empty prefix, non-empty infix, non-empty suffix
503
                5
7✔
504
            } else {
505
                // Two placeholders, non-empty prefix, non-empty infix, empty suffix
506
                4
11✔
507
            }
508
        }
509
    }
156✔
510
}
511

512
impl<'a> Iterator for DoublePlaceholderPatternIterator<'a> {
513
    type Item = PatternItem<'a, DoublePlaceholderKey>;
514
    fn next(&mut self) -> Option<Self::Item> {
621✔
515
        match self.current_offset.cmp(&self.ph_first.offset) {
621✔
516
            Ordering::Less => {
517
                // Prefix
518
                let literal_str = match self.store.get(self.current_offset..self.ph_first.offset) {
46✔
519
                    Some(s) => s,
46✔
520
                    None => {
521
                        debug_assert!(false, "offsets are in range");
×
522
                        ""
523
                    }
524
                };
525
                self.current_offset = self.ph_first.offset;
46✔
526
                Some(PatternItem::Literal(literal_str))
46✔
527
            }
46✔
528
            Ordering::Equal => {
529
                // Placeholder
530
                self.ph_first.offset = 0;
155✔
531
                Some(PatternItem::Placeholder(self.ph_first.key))
155✔
532
            }
533
            Ordering::Greater => match self.current_offset.cmp(&self.ph_second.offset) {
420✔
534
                Ordering::Less => {
535
                    // Infix
536
                    let literal_str =
537
                        match self.store.get(self.current_offset..self.ph_second.offset) {
84✔
538
                            Some(s) => s,
84✔
539
                            None => {
540
                                debug_assert!(false, "offsets are in range");
×
541
                                ""
542
                            }
543
                        };
544
                    self.current_offset = self.ph_second.offset;
84✔
545
                    Some(PatternItem::Literal(literal_str))
84✔
546
                }
84✔
547
                Ordering::Equal => {
548
                    // Placeholder
549
                    self.ph_second.offset = 0;
145✔
550
                    Some(PatternItem::Placeholder(self.ph_second.key))
145✔
551
                }
552
                Ordering::Greater => {
553
                    // Suffix or end of string
554
                    let literal_str = match self.store.get(self.current_offset..) {
191✔
555
                        Some(s) => s,
191✔
556
                        None => {
557
                            debug_assert!(false, "offsets are in range");
×
558
                            ""
559
                        }
560
                    };
561
                    if literal_str.is_empty() {
191✔
562
                        // End of string
563
                        None
156✔
564
                    } else {
565
                        // Suffix
566
                        self.current_offset = self.store.len();
35✔
567
                        Some(PatternItem::Literal(literal_str))
35✔
568
                    }
569
                }
570
            },
571
        }
572
    }
621✔
573

574
    fn size_hint(&self) -> (usize, Option<usize>) {
155✔
575
        let len = self.len();
155✔
576
        (len, Some(len))
155✔
577
    }
155✔
578
}
579

580
#[cfg(test)]
581
mod tests {
582
    use super::*;
583
    use crate::DoublePlaceholderPattern;
584

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

743
    #[test]
744
    fn test_invalid() {
2✔
745
        let cases = [
1✔
746
            "",               // too short
747
            "\x00",           // too short
748
            "\x00\x00",       // duplicate placeholders
749
            "\x04\x03",       // first offset is after second offset
750
            "\x04\x05",       // second offset out of range (also first offset)
751
            "\x04\u{10001}@", // second offset too large for UTF-8
752
        ];
753
        let long_str = "0123456789".repeat(1000000);
1✔
754
        for cas in cases {
1✔
755
            let cas = cas.replace('@', &long_str);
6✔
756
            assert!(DoublePlaceholder::validate_store(&cas).is_err(), "{cas:?}");
6✔
757
        }
7✔
758
    }
2✔
759
}
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