• 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

91.94
/components/datetime/src/provider/pattern/reference/parser.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
use super::{
6
    super::error::PatternError,
7
    super::{GenericPatternItem, PatternItem},
8
};
9
#[cfg(test)]
10
use super::{GenericPattern, Pattern};
11
use crate::provider::fields::{self, Field, FieldLength, FieldSymbol, TimeZone};
12
use alloc::string::String;
13
use alloc::vec;
14
use alloc::vec::Vec;
15
use core::mem;
16

17
#[derive(Debug, PartialEq)]
×
18
struct SegmentSymbol {
19
    symbol: FieldSymbol,
×
20
    length: u8,
×
21
}
22

23
impl SegmentSymbol {
24
    fn finish(self, result: &mut Vec<PatternItem>) -> Result<(), PatternError> {
675,466✔
25
        let length = FieldLength::from_idx(self.length)
675,466✔
26
            .map_err(|_| PatternError::FieldLengthInvalid(self.symbol))?;
4✔
27
        result.push(PatternItem::from((self.symbol, length)));
675,464✔
28
        Ok(())
675,464✔
29
    }
675,466✔
30
}
31

32
#[derive(Debug, PartialEq)]
×
33
struct SegmentSecondSymbol {
34
    integer_digits: u8,
×
35
    seen_decimal_separator: bool,
×
36
    fraction_digits: u8,
×
37
}
38

39
impl SegmentSecondSymbol {
40
    fn finish(self, result: &mut Vec<PatternItem>) -> Result<(), PatternError> {
44,942✔
41
        let second_symbol = FieldSymbol::Second(fields::Second::Second);
44,942✔
42
        let symbol = if self.fraction_digits == 0 {
44,971✔
43
            second_symbol
44,912✔
44
        } else {
45
            let decimal_second = fields::DecimalSecond::from_idx(self.fraction_digits)
30✔
46
                .map_err(|_| PatternError::FieldLengthInvalid(second_symbol))?;
2✔
47
            FieldSymbol::DecimalSecond(decimal_second)
29✔
48
        };
49
        let length = FieldLength::from_idx(self.integer_digits)
44,941✔
50
            .map_err(|_| PatternError::FieldLengthInvalid(symbol))?;
×
51
        result.push(PatternItem::Field(Field { symbol, length }));
44,941✔
52
        if self.seen_decimal_separator && self.fraction_digits == 0 {
44,941✔
53
            result.push(PatternItem::Literal('.'));
6✔
54
        }
55
        Ok(())
44,941✔
56
    }
44,942✔
57
}
58

59
#[derive(Debug, PartialEq)]
×
60
struct SegmentLiteral {
61
    literal: String,
×
62
    quoted: bool,
×
63
}
64

65
impl SegmentLiteral {
66
    fn finish(self, result: &mut Vec<PatternItem>) -> Result<(), PatternError> {
727,655✔
67
        if !self.literal.is_empty() {
727,655✔
68
            result.extend(self.literal.chars().map(PatternItem::from));
483,068✔
69
        }
70
        Ok(())
727,647✔
71
    }
727,647✔
72

73
    fn finish_generic(self, result: &mut Vec<GenericPatternItem>) -> Result<(), PatternError> {
66,344✔
74
        if !self.literal.is_empty() {
66,344✔
75
            result.extend(self.literal.chars().map(GenericPatternItem::from));
22,163✔
76
        }
77
        Ok(())
66,344✔
78
    }
66,344✔
79
}
80

81
#[derive(Debug, PartialEq)]
×
82
struct SymbolAlias {
83
    ch: char,
×
84
    length: u8,
×
85
}
86

87
impl SymbolAlias {
88
    fn try_new(ch: char) -> Option<Self> {
577,595✔
89
        matches!(ch, 'Z').then_some(Self { ch, length: 1 })
577,595✔
90
    }
577,595✔
91

92
    fn finish(self, result: &mut Vec<PatternItem>) -> Result<(), PatternError> {
11✔
93
        match (self.ch, self.length) {
25✔
94
            // Z..ZZZ => xxxx
95
            ('Z', 1..=3) => SegmentSymbol {
11✔
96
                symbol: FieldSymbol::TimeZone(TimeZone::Iso),
8✔
97
                length: 4,
98
            },
8✔
99
            // ZZZZ => OOOO
100
            ('Z', 4) => SegmentSymbol {
1✔
101
                symbol: FieldSymbol::TimeZone(TimeZone::LocalizedOffset),
1✔
102
                length: 4,
103
            },
1✔
104
            // ZZZZZ => XXXXX
105
            ('Z', 5) => SegmentSymbol {
2✔
106
                symbol: FieldSymbol::TimeZone(TimeZone::IsoWithZ),
2✔
107
                length: 5,
108
            },
2✔
109
            _ => return Err(PatternError::UnknownSubstitution(self.ch)),
×
110
        }
111
        .finish(result)
112
    }
11✔
113
}
114

115
#[derive(Debug, PartialEq)]
×
116
enum Segment {
117
    Symbol(SegmentSymbol),
×
118
    SecondSymbol(SegmentSecondSymbol),
×
119
    Literal(SegmentLiteral),
×
120
    SymbolAlias(SymbolAlias),
×
121
}
122

123
impl Segment {
124
    fn finish(self, result: &mut Vec<PatternItem>) -> Result<(), PatternError> {
1,448,065✔
125
        match self {
1,448,065✔
126
            Self::Symbol(v) => v.finish(result),
675,457✔
127
            Self::SecondSymbol(v) => v.finish(result),
44,942✔
128
            Self::Literal(v) => v.finish(result),
727,655✔
129
            Self::SymbolAlias(v) => v.finish(result),
11✔
130
        }
131
    }
1,448,065✔
132

133
    fn finish_generic(self, result: &mut Vec<GenericPatternItem>) -> Result<(), PatternError> {
66,344✔
134
        match self {
66,344✔
135
            Self::Symbol(_) => unreachable!("no symbols in generic pattern"),
×
136
            Self::SecondSymbol(_) => unreachable!("no symbols in generic pattern"),
×
137
            Self::Literal(v) => v.finish_generic(result),
66,344✔
138
            Self::SymbolAlias(_) => unreachable!("no symbols in generic pattern"),
×
139
        }
140
    }
66,344✔
141
}
142

143
#[derive(Debug)]
×
144
pub struct Parser<'p> {
145
    source: &'p str,
×
146
    state: Segment,
×
147
}
148

149
impl<'p> Parser<'p> {
150
    pub fn new(source: &'p str) -> Self {
267,449✔
151
        Self {
267,449✔
152
            source,
153
            state: Segment::Literal(SegmentLiteral {
267,449✔
154
                literal: String::new(),
267,449✔
155
                quoted: false,
156
            }),
157
        }
158
    }
267,449✔
159

160
    fn handle_quoted_literal(
1,809,438✔
161
        &mut self,
162
        ch: char,
163
        chars: &mut core::iter::Peekable<core::str::Chars>,
164
        result: &mut Vec<PatternItem>,
165
    ) -> Result<bool, PatternError> {
166
        if ch == '\'' {
1,836,808✔
167
            match (&mut self.state, chars.peek() == Some(&'\'')) {
27,370✔
168
                (Segment::Literal(ref mut literal), true) => {
10✔
169
                    literal.literal.push('\'');
10✔
170
                    chars.next();
10✔
171
                }
172
                (Segment::Literal(ref mut literal), false) => {
27,298✔
173
                    literal.quoted = !literal.quoted;
27,298✔
174
                }
27,298✔
175
                (state, true) => {
4✔
176
                    mem::replace(
4✔
177
                        state,
178
                        Segment::Literal(SegmentLiteral {
4✔
179
                            literal: String::from(ch),
4✔
180
                            quoted: false,
181
                        }),
182
                    )
183
                    .finish(result)?;
×
184
                    chars.next();
4✔
185
                }
186
                (state, false) => {
58✔
187
                    mem::replace(
1,809,496✔
188
                        state,
189
                        Segment::Literal(SegmentLiteral {
58✔
190
                            literal: String::new(),
58✔
191
                            quoted: true,
192
                        }),
193
                    )
194
                    .finish(result)?;
×
195
                }
196
            }
197
            Ok(true)
27,370✔
198
        } else if let Segment::Literal(SegmentLiteral {
1,782,068✔
199
            ref mut literal,
25,883✔
200
            quoted: true,
201
        }) = self.state
1,782,068✔
202
        {
203
            literal.push(ch);
25,883✔
204
            Ok(true)
25,883✔
205
        } else {
206
            Ok(false)
1,756,185✔
207
        }
208
    }
1,809,438✔
209

210
    fn handle_generic_quoted_literal(
76,496✔
211
        &mut self,
212
        ch: char,
213
        chars: &mut core::iter::Peekable<core::str::Chars>,
214
    ) -> Result<bool, PatternError> {
215
        if ch == '\'' {
76,537✔
216
            match (&mut self.state, chars.peek() == Some(&'\'')) {
41✔
217
                (Segment::Literal(literal), true) => {
8✔
218
                    literal.literal.push('\'');
8✔
219
                    chars.next();
8✔
220
                }
221
                (Segment::Literal(literal), false) => {
33✔
222
                    literal.quoted = !literal.quoted;
33✔
223
                }
33✔
224
                _ => unreachable!("Generic pattern has no symbols."),
×
225
            }
226
            Ok(true)
41✔
227
        } else if let Segment::Literal(SegmentLiteral {
76,455✔
228
            ref mut literal,
42✔
229
            quoted: true,
230
        }) = self.state
76,455✔
231
        {
232
            literal.push(ch);
42✔
233
            Ok(true)
42✔
234
        } else {
235
            Ok(false)
76,413✔
236
        }
237
    }
76,496✔
238

239
    pub fn parse(mut self) -> Result<Vec<PatternItem>, PatternError> {
245,356✔
240
        let mut chars = self.source.chars().peekable();
245,356✔
241
        let mut result = vec![];
245,338✔
242

243
        while let Some(ch) = chars.next() {
2,054,798✔
244
            if !self.handle_quoted_literal(ch, &mut chars, &mut result)? {
1,809,440✔
245
                if let Ok(new_symbol) = FieldSymbol::try_from(ch) {
1,756,192✔
246
                    match &mut self.state {
1,178,596✔
247
                        Segment::Symbol(SegmentSymbol {
248
                            ref symbol,
421,104✔
249
                            ref mut length,
421,104✔
250
                        }) if new_symbol == *symbol => {
421,104✔
251
                            *length += 1;
413,289✔
252
                        }
413,289✔
253
                        Segment::SecondSymbol(SegmentSecondSymbol {
254
                            ref mut integer_digits,
44,908✔
255
                            seen_decimal_separator: false,
256
                            ..
257
                        }) if matches!(new_symbol, FieldSymbol::Second(fields::Second::Second)) => {
44,908✔
258
                            *integer_digits += 1;
44,908✔
259
                        }
44,908✔
260
                        state => {
261
                            mem::replace(
720,399✔
262
                                state,
263
                                if matches!(new_symbol, FieldSymbol::Second(fields::Second::Second))
720,399✔
264
                                {
265
                                    Segment::SecondSymbol(SegmentSecondSymbol {
44,942✔
266
                                        integer_digits: 1,
267
                                        seen_decimal_separator: false,
268
                                        fraction_digits: 0,
269
                                    })
270
                                } else {
271
                                    Segment::Symbol(SegmentSymbol {
675,457✔
272
                                        symbol: new_symbol,
273
                                        length: 1,
274
                                    })
275
                                },
276
                            )
277
                            .finish(&mut result)?;
×
278
                        }
279
                    }
280
                } else if let Some(alias) = SymbolAlias::try_new(ch) {
577,596✔
281
                    match &mut self.state {
25✔
282
                        Segment::SymbolAlias(SymbolAlias { ch: ch2, length }) if *ch2 == ch => {
14✔
283
                            *length += 1;
14✔
284
                        }
14✔
285
                        state => {
286
                            mem::replace(state, Segment::SymbolAlias(alias)).finish(&mut result)?;
11✔
287
                        }
288
                    }
289
                } else {
290
                    match &mut self.state {
577,571✔
291
                        Segment::SecondSymbol(
292
                            second_symbol @ SegmentSecondSymbol {
25,079✔
293
                                seen_decimal_separator: false,
294
                                ..
295
                            },
296
                        ) if ch == '.' => second_symbol.seen_decimal_separator = true,
25,079✔
297
                        Segment::SecondSymbol(second_symbol) if ch == 'S' => {
25,305✔
298
                            // Note: this accepts both "ssSSS" and "ss.SSS"
299
                            // We say we've seen the separator to switch to fraction mode
300
                            second_symbol.seen_decimal_separator = true;
240✔
301
                            second_symbol.fraction_digits += 1;
240✔
302
                        }
240✔
303
                        Segment::Literal(literal) => literal.literal.push(ch),
95,042✔
304
                        state => {
305
                            mem::replace(
482,258✔
306
                                state,
307
                                Segment::Literal(SegmentLiteral {
482,258✔
308
                                    literal: String::from(ch),
482,258✔
309
                                    quoted: false,
310
                                }),
311
                            )
312
                            .finish(&mut result)?;
×
313
                        }
314
                    }
315
                }
316
            }
317
        }
318

319
        if matches!(
245,337✔
320
            self.state,
245,337✔
321
            Segment::Literal(SegmentLiteral { quoted: true, .. })
322
        ) {
323
            return Err(PatternError::UnclosedLiteral);
2✔
324
        }
325

326
        self.state.finish(&mut result)?;
245,335✔
327

328
        Ok(result)
245,333✔
329
    }
245,338✔
330

331
    pub fn parse_generic(mut self) -> Result<Vec<GenericPatternItem>, PatternError> {
22,111✔
332
        let mut chars = self.source.chars().peekable();
22,111✔
333
        let mut result = vec![];
22,111✔
334

335
        while let Some(ch) = chars.next() {
98,602✔
336
            if !self.handle_generic_quoted_literal(ch, &mut chars)? {
76,496✔
337
                if ch == '{' {
76,413✔
338
                    mem::replace(
44,239✔
339
                        &mut self.state,
340
                        Segment::Literal(SegmentLiteral {
44,239✔
341
                            literal: String::new(),
44,239✔
342
                            quoted: false,
343
                        }),
344
                    )
345
                    .finish_generic(&mut result)?;
×
346

347
                    let ch = chars.next().ok_or(PatternError::UnclosedPlaceholder)?;
44,239✔
348
                    let idx = ch
44,238✔
349
                        .to_digit(10)
350
                        .ok_or(PatternError::UnknownSubstitution(ch))?
44,239✔
351
                        as u8;
352
                    result.push(GenericPatternItem::Placeholder(idx));
44,237✔
353
                    let ch = chars.next().ok_or(PatternError::UnclosedPlaceholder)?;
44,237✔
354
                    if ch != '}' {
44,236✔
355
                        return Err(PatternError::UnclosedPlaceholder);
2✔
356
                    }
357
                } else if let Segment::Literal(SegmentLiteral {
32,174✔
358
                    ref mut literal, ..
32,174✔
359
                }) = self.state
32,174✔
360
                {
361
                    literal.push(ch);
32,174✔
362
                } else {
363
                    unreachable!()
×
364
                }
365
            }
366
        }
367

368
        if matches!(
22,106✔
369
            self.state,
22,106✔
370
            Segment::Literal(SegmentLiteral { quoted: true, .. })
371
        ) {
372
            return Err(PatternError::UnclosedLiteral);
1✔
373
        }
374

375
        self.state.finish_generic(&mut result)?;
22,105✔
376

377
        Ok(result)
22,105✔
378
    }
22,111✔
379

380
    #[cfg(test)]
381
    pub fn parse_placeholders(
25✔
382
        self,
383
        replacements: Vec<Pattern>,
384
    ) -> Result<Vec<PatternItem>, PatternError> {
385
        let generic_items = self.parse_generic()?;
25✔
386

387
        let gp = GenericPattern::from(generic_items);
19✔
388
        Ok(gp.combined(replacements)?.items.to_vec())
19✔
389
    }
25✔
390
}
391

392
#[cfg(test)]
393
mod tests {
394
    use super::super::super::reference::Pattern;
395
    use super::*;
396
    use crate::provider::fields::{self, FieldLength};
397

398
    #[test]
399
    fn pattern_parse_simple() {
2✔
400
        let samples = [
2✔
401
            (
1✔
402
                "dd/MM/y",
403
                vec![
2✔
404
                    (fields::Day::DayOfMonth.into(), FieldLength::Two).into(),
1✔
405
                    '/'.into(),
1✔
406
                    (fields::Month::Format.into(), FieldLength::Two).into(),
1✔
407
                    '/'.into(),
1✔
408
                    (fields::Year::Calendar.into(), FieldLength::One).into(),
1✔
409
                ],
410
            ),
411
            (
1✔
412
                "HH:mm:ss",
413
                vec![
2✔
414
                    (fields::Hour::H23.into(), FieldLength::Two).into(),
1✔
415
                    ':'.into(),
1✔
416
                    (FieldSymbol::Minute, FieldLength::Two).into(),
1✔
417
                    ':'.into(),
1✔
418
                    (fields::Second::Second.into(), FieldLength::Two).into(),
1✔
419
                ],
420
            ),
421
            (
1✔
422
                "y年M月d日",
423
                vec![
2✔
424
                    (fields::Year::Calendar.into(), FieldLength::One).into(),
1✔
425
                    'å¹´'.into(),
1✔
426
                    (fields::Month::Format.into(), FieldLength::One).into(),
1✔
427
                    '月'.into(),
1✔
428
                    (fields::Day::DayOfMonth.into(), FieldLength::One).into(),
1✔
429
                    'æ—¥'.into(),
1✔
430
                ],
431
            ),
432
            (
1✔
433
                "HH:mm:ss.SS",
434
                vec![
2✔
435
                    (fields::Hour::H23.into(), FieldLength::Two).into(),
1✔
436
                    ':'.into(),
1✔
437
                    (FieldSymbol::Minute, FieldLength::Two).into(),
1✔
438
                    ':'.into(),
1✔
439
                    (fields::DecimalSecond::Subsecond2.into(), FieldLength::Two).into(),
1✔
440
                ],
441
            ),
442
        ];
×
443

444
        for (string, items) in samples {
5✔
445
            assert_eq!(
8✔
446
                string.parse::<Pattern>().expect("Parsing pattern failed."),
4✔
447
                Pattern::from(items)
4✔
448
            );
449
        }
5✔
450
    }
2✔
451

452
    fn str2pis(input: &str) -> Vec<PatternItem> {
31✔
453
        input.chars().map(Into::into).collect()
31✔
454
    }
31✔
455

456
    #[test]
457
    fn pattern_parse_literals() {
2✔
458
        let samples = [
2✔
459
            ("", ""),
1✔
460
            (" ", " "),
1✔
461
            ("  ", "  "),
1✔
462
            (" żółć ", " żółć "),
1✔
463
            ("''", "'"),
1✔
464
            (" ''", " '"),
1✔
465
            (" '' ", " ' "),
1✔
466
            ("''''", "''"),
1✔
467
            (" '' '' ", " ' ' "),
1✔
468
            ("ż'ół'ć", "żółć"),
1✔
469
            ("ż'ó''ł'ć", "żó'łć"),
1✔
470
            (" 'Ymd' ", " Ymd "),
1✔
471
            ("الأسبوع", "الأسبوع"),
1✔
472
        ];
473

474
        for (string, pattern) in samples {
1✔
475
            assert_eq!(
26✔
476
                Parser::new(string)
13✔
477
                    .parse()
478
                    .expect("Parsing pattern failed."),
479
                str2pis(pattern),
13✔
480
            );
481

482
            assert_eq!(
26✔
483
                Parser::new(string)
13✔
484
                    .parse_placeholders(vec![])
26✔
485
                    .expect("Parsing pattern failed."),
486
                str2pis(pattern),
13✔
487
            );
488
        }
1✔
489

490
        let broken = [(" 'foo ", PatternError::UnclosedLiteral)];
1✔
491

492
        for (string, error) in broken {
1✔
493
            assert_eq!(Parser::new(string).parse(), Err(error),);
2✔
494
        }
1✔
495
    }
2✔
496

497
    #[test]
498
    fn pattern_parse_symbols() {
2✔
499
        let samples = [
1✔
500
            (
1✔
501
                "y",
502
                vec![(fields::Year::Calendar.into(), FieldLength::One).into()],
1✔
503
            ),
504
            (
1✔
505
                "yy",
506
                vec![(fields::Year::Calendar.into(), FieldLength::Two).into()],
1✔
507
            ),
508
            (
1✔
509
                "yyy",
510
                vec![(fields::Year::Calendar.into(), FieldLength::Three).into()],
1✔
511
            ),
512
            (
1✔
513
                "yyyy",
514
                vec![(fields::Year::Calendar.into(), FieldLength::Four).into()],
1✔
515
            ),
516
            (
1✔
517
                "yyyyy",
518
                vec![(fields::Year::Calendar.into(), FieldLength::Five).into()],
1✔
519
            ),
520
            (
1✔
521
                "yyyyyy",
522
                vec![(fields::Year::Calendar.into(), FieldLength::Six).into()],
1✔
523
            ),
524
            (
1✔
525
                "yM",
526
                vec![
2✔
527
                    (fields::Year::Calendar.into(), FieldLength::One).into(),
1✔
528
                    (fields::Month::Format.into(), FieldLength::One).into(),
1✔
529
                ],
530
            ),
531
            (
1✔
532
                "y ",
533
                vec![
2✔
534
                    (fields::Year::Calendar.into(), FieldLength::One).into(),
1✔
535
                    ' '.into(),
1✔
536
                ],
537
            ),
538
            (
1✔
539
                "y M",
540
                vec![
2✔
541
                    (fields::Year::Calendar.into(), FieldLength::One).into(),
1✔
542
                    ' '.into(),
1✔
543
                    (fields::Month::Format.into(), FieldLength::One).into(),
1✔
544
                ],
545
            ),
546
            (
1✔
547
                "hh''a",
548
                vec![
2✔
549
                    (fields::Hour::H12.into(), FieldLength::Two).into(),
1✔
550
                    '\''.into(),
1✔
551
                    (fields::DayPeriod::AmPm.into(), FieldLength::One).into(),
1✔
552
                ],
553
            ),
554
            (
1✔
555
                "hh''b",
556
                vec![
2✔
557
                    (fields::Hour::H12.into(), FieldLength::Two).into(),
1✔
558
                    '\''.into(),
1✔
559
                    (fields::DayPeriod::NoonMidnight.into(), FieldLength::One).into(),
1✔
560
                ],
561
            ),
562
            (
1✔
563
                "y'My'M",
564
                vec![
2✔
565
                    (fields::Year::Calendar.into(), FieldLength::One).into(),
1✔
566
                    'M'.into(),
1✔
567
                    'y'.into(),
1✔
568
                    (fields::Month::Format.into(), FieldLength::One).into(),
1✔
569
                ],
570
            ),
571
            (
1✔
572
                "y 'My' M",
573
                vec![
2✔
574
                    (fields::Year::Calendar.into(), FieldLength::One).into(),
1✔
575
                    ' '.into(),
1✔
576
                    'M'.into(),
1✔
577
                    'y'.into(),
1✔
578
                    ' '.into(),
1✔
579
                    (fields::Month::Format.into(), FieldLength::One).into(),
1✔
580
                ],
581
            ),
582
            (
1✔
583
                " 'r'. 'y'. ",
584
                vec![
2✔
585
                    ' '.into(),
1✔
586
                    'r'.into(),
1✔
587
                    '.'.into(),
1✔
588
                    ' '.into(),
1✔
589
                    'y'.into(),
1✔
590
                    '.'.into(),
1✔
591
                    ' '.into(),
1✔
592
                ],
593
            ),
594
            (
1✔
595
                "hh 'o''clock' a",
596
                vec![
2✔
597
                    (fields::Hour::H12.into(), FieldLength::Two).into(),
1✔
598
                    ' '.into(),
1✔
599
                    'o'.into(),
1✔
600
                    '\''.into(),
1✔
601
                    'c'.into(),
1✔
602
                    'l'.into(),
1✔
603
                    'o'.into(),
1✔
604
                    'c'.into(),
1✔
605
                    'k'.into(),
1✔
606
                    ' '.into(),
1✔
607
                    (fields::DayPeriod::AmPm.into(), FieldLength::One).into(),
1✔
608
                ],
609
            ),
610
            (
1✔
611
                "hh 'o''clock' b",
612
                vec![
2✔
613
                    (fields::Hour::H12.into(), FieldLength::Two).into(),
1✔
614
                    ' '.into(),
1✔
615
                    'o'.into(),
1✔
616
                    '\''.into(),
1✔
617
                    'c'.into(),
1✔
618
                    'l'.into(),
1✔
619
                    'o'.into(),
1✔
620
                    'c'.into(),
1✔
621
                    'k'.into(),
1✔
622
                    ' '.into(),
1✔
623
                    (fields::DayPeriod::NoonMidnight.into(), FieldLength::One).into(),
1✔
624
                ],
625
            ),
626
            (
1✔
627
                "hh''a",
628
                vec![
2✔
629
                    (fields::Hour::H12.into(), FieldLength::Two).into(),
1✔
630
                    '\''.into(),
1✔
631
                    (fields::DayPeriod::AmPm.into(), FieldLength::One).into(),
1✔
632
                ],
633
            ),
634
            (
1✔
635
                "hh''b",
636
                vec![
2✔
637
                    (fields::Hour::H12.into(), FieldLength::Two).into(),
1✔
638
                    '\''.into(),
1✔
639
                    (fields::DayPeriod::NoonMidnight.into(), FieldLength::One).into(),
1✔
640
                ],
641
            ),
642
            (
1✔
643
                "s.SS",
644
                vec![(fields::DecimalSecond::Subsecond2.into(), FieldLength::One).into()],
1✔
645
            ),
646
            (
1✔
647
                "sSS",
648
                vec![(fields::DecimalSecond::Subsecond2.into(), FieldLength::One).into()],
1✔
649
            ),
650
            (
1✔
651
                "s.. z",
652
                vec![
2✔
653
                    (fields::Second::Second.into(), FieldLength::One).into(),
1✔
654
                    '.'.into(),
1✔
655
                    '.'.into(),
1✔
656
                    ' '.into(),
1✔
657
                    (
1✔
658
                        fields::TimeZone::SpecificNonLocation.into(),
1✔
659
                        FieldLength::One,
1✔
660
                    )
661
                        .into(),
662
                ],
663
            ),
664
            (
1✔
665
                "s.SSz",
666
                vec![
2✔
667
                    (fields::DecimalSecond::Subsecond2.into(), FieldLength::One).into(),
1✔
668
                    (
1✔
669
                        fields::TimeZone::SpecificNonLocation.into(),
1✔
670
                        FieldLength::One,
1✔
671
                    )
672
                        .into(),
673
                ],
674
            ),
675
            (
1✔
676
                "sSSz",
677
                vec![
2✔
678
                    (fields::DecimalSecond::Subsecond2.into(), FieldLength::One).into(),
1✔
679
                    (
1✔
680
                        fields::TimeZone::SpecificNonLocation.into(),
1✔
681
                        FieldLength::One,
1✔
682
                    )
683
                        .into(),
684
                ],
685
            ),
686
            (
1✔
687
                "s.SSss",
688
                vec![
2✔
689
                    (fields::DecimalSecond::Subsecond2.into(), FieldLength::One).into(),
1✔
690
                    (fields::Second::Second.into(), FieldLength::Two).into(),
1✔
691
                ],
692
            ),
693
            (
1✔
694
                "sSSss",
695
                vec![
2✔
696
                    (fields::DecimalSecond::Subsecond2.into(), FieldLength::One).into(),
1✔
697
                    (fields::Second::Second.into(), FieldLength::Two).into(),
1✔
698
                ],
699
            ),
700
            (
1✔
701
                "s.z",
702
                vec![
2✔
703
                    (fields::Second::Second.into(), FieldLength::One).into(),
1✔
704
                    '.'.into(),
1✔
705
                    (
1✔
706
                        fields::TimeZone::SpecificNonLocation.into(),
1✔
707
                        FieldLength::One,
1✔
708
                    )
709
                        .into(),
710
                ],
711
            ),
712
            (
1✔
713
                "s.ss",
714
                vec![
2✔
715
                    (fields::Second::Second.into(), FieldLength::One).into(),
1✔
716
                    '.'.into(),
1✔
717
                    (fields::Second::Second.into(), FieldLength::Two).into(),
1✔
718
                ],
719
            ),
720
            (
1✔
721
                "z",
722
                vec![(
1✔
723
                    fields::TimeZone::SpecificNonLocation.into(),
1✔
724
                    FieldLength::One,
1✔
725
                )
726
                    .into()],
727
            ),
728
            (
1✔
729
                "Z",
730
                vec![(fields::TimeZone::Iso.into(), FieldLength::Four).into()],
1✔
731
            ),
732
            (
1✔
733
                "ZZ",
734
                vec![(fields::TimeZone::Iso.into(), FieldLength::Four).into()],
1✔
735
            ),
736
            (
1✔
737
                "ZZZ",
738
                vec![(fields::TimeZone::Iso.into(), FieldLength::Four).into()],
1✔
739
            ),
740
            (
1✔
741
                "ZZZZ",
742
                vec![(fields::TimeZone::LocalizedOffset.into(), FieldLength::Four).into()],
1✔
743
            ),
744
            (
1✔
745
                "ZZZZZ",
746
                vec![(fields::TimeZone::IsoWithZ.into(), FieldLength::Five).into()],
1✔
747
            ),
748
            (
1✔
749
                "O",
750
                vec![(fields::TimeZone::LocalizedOffset.into(), FieldLength::One).into()],
1✔
751
            ),
752
            (
1✔
753
                "v",
754
                vec![(
1✔
755
                    fields::TimeZone::GenericNonLocation.into(),
1✔
756
                    FieldLength::One,
1✔
757
                )
758
                    .into()],
759
            ),
760
            (
1✔
761
                "V",
762
                vec![(fields::TimeZone::Location.into(), FieldLength::One).into()],
1✔
763
            ),
764
            (
1✔
765
                "x",
766
                vec![(fields::TimeZone::Iso.into(), FieldLength::One).into()],
1✔
767
            ),
768
            (
1✔
769
                "X",
770
                vec![(fields::TimeZone::IsoWithZ.into(), FieldLength::One).into()],
1✔
771
            ),
772
        ];
×
773

774
        for (string, pattern) in samples {
1✔
775
            assert_eq!(
76✔
776
                Parser::new(string)
38✔
777
                    .parse()
778
                    .expect("Parsing pattern failed."),
779
                pattern,
780
                "{string}",
781
            );
782
        }
39✔
783

784
        let broken = [
1✔
785
            (
1✔
786
                "yyyyyyy",
787
                PatternError::FieldLengthInvalid(FieldSymbol::Year(fields::Year::Calendar)),
1✔
788
            ),
789
            (
1✔
790
                "hh:mm:ss.SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS",
791
                PatternError::FieldLengthInvalid(FieldSymbol::Second(fields::Second::Second)),
1✔
792
            ),
793
        ];
794

795
        for (string, error) in broken {
1✔
796
            assert_eq!(Parser::new(string).parse(), Err(error),);
4✔
797
        }
1✔
798
    }
2✔
799

800
    #[test]
801
    fn pattern_parse_placeholders() {
2✔
802
        let samples = [
2✔
803
            ("{0}", vec![Pattern::from("ONE")], str2pis("ONE")),
1✔
804
            (
1✔
805
                "{0}{1}",
806
                vec![Pattern::from("ONE"), Pattern::from("TWO")],
1✔
807
                str2pis("ONETWO"),
1✔
808
            ),
×
809
            (
1✔
810
                "{0} 'at' {1}",
811
                vec![Pattern::from("ONE"), Pattern::from("TWO")],
1✔
812
                str2pis("ONE at TWO"),
1✔
813
            ),
×
814
            (
1✔
815
                "{0}'at'{1}",
816
                vec![Pattern::from("ONE"), Pattern::from("TWO")],
1✔
817
                str2pis("ONEatTWO"),
1✔
818
            ),
×
819
            (
1✔
820
                "'{0}' 'at' '{1}'",
821
                vec![Pattern::from("ONE"), Pattern::from("TWO")],
1✔
822
                str2pis("{0} at {1}"),
1✔
823
            ),
×
824
        ];
×
825

826
        for (string, replacements, pattern) in samples {
6✔
827
            assert_eq!(
10✔
828
                Parser::new(string)
5✔
829
                    .parse_placeholders(replacements)
5✔
830
                    .expect("Parsing pattern failed."),
831
                pattern,
832
            );
833
        }
6✔
834

835
        let broken = [
1✔
836
            ("{0}", vec![], PatternError::UnknownSubstitution('0')),
1✔
837
            ("{a}", vec![], PatternError::UnknownSubstitution('a')),
1✔
838
            ("{", vec![], PatternError::UnclosedPlaceholder),
1✔
839
            (
1✔
840
                "{0",
841
                vec![Pattern::from(vec![])],
1✔
842
                PatternError::UnclosedPlaceholder,
1✔
843
            ),
844
            (
1✔
845
                "{01",
846
                vec![Pattern::from(vec![])],
1✔
847
                PatternError::UnclosedPlaceholder,
1✔
848
            ),
849
            (
1✔
850
                "{00}",
851
                vec![Pattern::from(vec![])],
1✔
852
                PatternError::UnclosedPlaceholder,
1✔
853
            ),
854
            (
1✔
855
                "'{00}",
856
                vec![Pattern::from(vec![])],
1✔
857
                PatternError::UnclosedLiteral,
1✔
858
            ),
859
        ];
×
860

861
        for (string, replacements, error) in broken {
8✔
862
            assert_eq!(
14✔
863
                Parser::new(string).parse_placeholders(replacements),
7✔
864
                Err(error),
7✔
865
            );
866
        }
8✔
867
    }
2✔
868
}
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