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

zbraniecki / icu4x / 11904027177

19 Nov 2024 12:33AM UTC coverage: 75.477% (+0.3%) from 75.174%
11904027177

push

github

web-flow
Move DateTimePattern into pattern module (#5834)

#1317

Also removes `NeoNeverMarker` and fixes #5689

258 of 319 new or added lines in 6 files covered. (80.88%)

6967 existing lines in 278 files now uncovered.

54522 of 72237 relevant lines covered (75.48%)

655305.49 hits per line

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

77.82
/components/datetime/src/format/datetime.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::time_zone::{FormatTimeZone, FormatTimeZoneError, Iso8601Format, TimeZoneFormatterUnit};
6
use crate::error::DateTimeWriteError;
7
use crate::fields::{self, FieldLength, FieldSymbol, Second, Year};
8
use crate::input::ExtractedInput;
9
use crate::pattern::*;
10
use crate::provider::pattern::runtime::PatternMetadata;
11
use crate::provider::pattern::PatternItem;
12

13
use core::fmt::{self, Write};
14
use fixed_decimal::FixedDecimal;
15
use icu_calendar::types::{DayOfWeekInMonth, IsoWeekday};
16
use icu_decimal::FixedDecimalFormatter;
17
use writeable::{Part, Writeable};
18

19
/// Apply length to input number and write to result using fixed_decimal_format.
20
fn try_write_number<W>(
1,568✔
21
    result: &mut W,
22
    fixed_decimal_format: Option<&FixedDecimalFormatter>,
23
    mut num: FixedDecimal,
24
    length: FieldLength,
25
) -> Result<Result<(), DateTimeWriteError>, fmt::Error>
26
where
27
    W: writeable::PartsWrite + ?Sized,
28
{
29
    num.pad_start(length.to_len() as i16);
1,568✔
30

31
    if let Some(fdf) = fixed_decimal_format {
3,136✔
32
        fdf.format(&num).write_to(result)?;
1,565✔
33
        Ok(Ok(()))
1,565✔
34
    } else {
35
        result.with_part(writeable::Part::ERROR, |r| num.write_to(r))?;
1,574✔
36
        Ok(Err(DateTimeWriteError::FixedDecimalFormatterNotLoaded))
3✔
37
    }
38
}
1,568✔
39

40
#[allow(clippy::too_many_arguments)]
41
pub(crate) fn try_write_pattern_items<W>(
936✔
42
    pattern_metadata: PatternMetadata,
43
    pattern_items: impl Iterator<Item = PatternItem>,
44
    input: &ExtractedInput,
45
    datetime_names: &RawDateTimeNamesBorrowed,
46
    fixed_decimal_format: Option<&FixedDecimalFormatter>,
47
    w: &mut W,
48
) -> Result<Result<(), DateTimeWriteError>, fmt::Error>
49
where
50
    W: writeable::PartsWrite + ?Sized,
51
{
52
    let mut r = Ok(());
936✔
53
    for item in pattern_items {
6,456✔
54
        match item {
5,523✔
55
            PatternItem::Literal(ch) => w.write_char(ch)?,
2,794✔
56
            PatternItem::Field(field) => {
2,729✔
57
                r = r.and(try_write_field(
2,729✔
58
                    field,
59
                    pattern_metadata,
60
                    input,
61
                    datetime_names,
62
                    fixed_decimal_format,
63
                    w,
64
                )?);
936✔
65
            }
2,727✔
66
        }
UNCOV
67
    }
×
68
    Ok(r)
928✔
69
}
928✔
70

71
// This function assumes that the correct decision has been
72
// made regarding availability of symbols in the caller.
73
//
74
// When modifying the list of fields using symbols,
75
// update the matching query in `analyze_pattern` function.
76
fn try_write_field<W>(
2,729✔
77
    field: fields::Field,
78
    pattern_metadata: PatternMetadata,
79
    input: &ExtractedInput,
80
    datetime_names: &RawDateTimeNamesBorrowed,
81
    fdf: Option<&FixedDecimalFormatter>,
82
    w: &mut W,
83
) -> Result<Result<(), DateTimeWriteError>, fmt::Error>
84
where
85
    W: writeable::PartsWrite + ?Sized,
86
{
87
    macro_rules! input {
88
        ($name:ident = $input:expr) => {
89
            let Some($name) = $input else {
2,484✔
90
                write_value_missing(w, field)?;
9✔
91
                return Ok(Err(DateTimeWriteError::MissingInputField(stringify!(
9✔
92
                    $name
93
                ))));
94
            };
95
        };
96
    }
97

98
    Ok(match (field.symbol, field.length) {
5,449✔
99
        (FieldSymbol::Era, l) => {
163✔
100
            input!(year = input.year);
163✔
101
            input!(era = year.formatting_era());
162✔
102
            let era_symbol = datetime_names
162✔
103
                .get_name_for_era(l, era)
104
                .map_err(|e| match e {
1✔
UNCOV
105
                    GetSymbolForEraError::Invalid => DateTimeWriteError::InvalidEra(era),
×
106
                    GetSymbolForEraError::NotLoaded => DateTimeWriteError::NamesNotLoaded(field),
1✔
107
                });
1✔
108
            match era_symbol {
162✔
109
                Err(e) => {
1✔
110
                    w.with_part(Part::ERROR, |w| w.write_str(&era.fallback_name()))?;
2✔
111
                    Err(e)
1✔
112
                }
1✔
113
                Ok(era) => Ok(w.write_str(era)?),
161✔
114
            }
115
        }
116
        (FieldSymbol::Year(Year::Calendar), l) => {
303✔
117
            input!(year = input.year);
303✔
118
            let mut year = FixedDecimal::from(year.era_year_or_extended());
302✔
119
            if matches!(l, FieldLength::Two) {
302✔
120
                // 'yy' and 'YY' truncate
121
                year.set_max_position(2);
24✔
122
            }
123
            try_write_number(w, fdf, year, l)?
302✔
124
        }
302✔
125
        (FieldSymbol::Year(Year::Cyclic), l) => {
21✔
126
            input!(year = input.year);
21✔
127
            input!(cyclic = year.cyclic());
21✔
128

129
            match datetime_names.get_name_for_cyclic(l, cyclic) {
21✔
130
                Ok(name) => Ok(w.write_str(name)?),
21✔
UNCOV
131
                Err(e) => {
×
UNCOV
132
                    w.with_part(Part::ERROR, |w| {
×
UNCOV
133
                        try_write_number(
×
134
                            w,
UNCOV
135
                            fdf,
×
UNCOV
136
                            year.era_year_or_extended().into(),
×
UNCOV
137
                            FieldLength::One,
×
138
                        )
UNCOV
139
                        .map(|_| ())
×
UNCOV
140
                    })?;
×
UNCOV
141
                    return Ok(Err(match e {
×
UNCOV
142
                        GetSymbolForCyclicYearError::Invalid { max } => {
×
UNCOV
143
                            DateTimeWriteError::InvalidCyclicYear {
×
UNCOV
144
                                value: cyclic.get() as usize,
×
145
                                max,
146
                            }
UNCOV
147
                        }
×
148
                        GetSymbolForCyclicYearError::NotLoaded => {
UNCOV
149
                            DateTimeWriteError::NamesNotLoaded(field)
×
150
                        }
151
                    }));
152
                }
153
            }
154
        }
21✔
155
        (FieldSymbol::Year(Year::RelatedIso), l) => {
21✔
156
            input!(year = input.year);
21✔
157
            input!(related_iso = year.related_iso());
21✔
158

159
            // Always in latin digits according to spec
160
            FixedDecimal::from(related_iso)
21✔
161
                .padded_start(l.to_len() as i16)
21✔
162
                .write_to(w)?;
21✔
163
            Ok(())
21✔
164
        }
21✔
165
        (FieldSymbol::Month(_), l @ (FieldLength::One | FieldLength::Two)) => {
97✔
166
            input!(month = input.month);
97✔
167
            try_write_number(w, fdf, month.ordinal.into(), l)?
97✔
168
        }
97✔
169
        (FieldSymbol::Month(symbol), l) => {
244✔
170
            input!(month = input.month);
244✔
171
            match datetime_names.get_name_for_month(symbol, l, month.formatting_code) {
243✔
172
                Ok(MonthPlaceholderValue::PlainString(symbol)) => {
242✔
173
                    w.write_str(symbol)?;
242✔
174
                    Ok(())
242✔
175
                }
242✔
176
                Ok(MonthPlaceholderValue::Numeric) => {
177
                    debug_assert!(l == FieldLength::One);
×
UNCOV
178
                    try_write_number(w, fdf, month.ordinal.into(), l)?
×
UNCOV
179
                }
×
UNCOV
180
                Ok(MonthPlaceholderValue::NumericPattern(substitution_pattern)) => {
×
UNCOV
181
                    debug_assert!(l == FieldLength::One);
×
UNCOV
182
                    if let Some(fdf) = fdf {
×
UNCOV
183
                        substitution_pattern
×
UNCOV
184
                            .interpolate([fdf.format(
×
185
                                &FixedDecimal::from(month.ordinal).padded_start(l.to_len() as i16),
×
186
                            )])
UNCOV
187
                            .write_to(w)?;
×
UNCOV
188
                        Ok(())
×
189
                    } else {
UNCOV
190
                        w.with_part(Part::ERROR, |w| {
×
UNCOV
191
                            substitution_pattern
×
UNCOV
192
                                .interpolate([FixedDecimal::from(month.ordinal)
×
UNCOV
193
                                    .padded_start(l.to_len() as i16)])
×
194
                                .write_to(w)
UNCOV
195
                        })?;
×
UNCOV
196
                        Err(DateTimeWriteError::FixedDecimalFormatterNotLoaded)
×
197
                    }
198
                }
199
                Err(e) => {
1✔
200
                    w.with_part(Part::ERROR, |w| w.write_str(&month.formatting_code.0))?;
2✔
201
                    Err(match e {
2✔
202
                        GetNameForMonthError::Invalid => {
UNCOV
203
                            DateTimeWriteError::InvalidMonthCode(month.formatting_code)
×
204
                        }
205
                        GetNameForMonthError::NotLoaded => {
206
                            DateTimeWriteError::NamesNotLoaded(field)
1✔
207
                        }
208
                    })
209
                }
1✔
210
            }
211
        }
212
        (FieldSymbol::Week(w), _) => match w {},
213
        (FieldSymbol::Weekday(weekday), l) => {
214✔
214
            input!(iso_weekday = input.iso_weekday);
214✔
215
            match datetime_names
213✔
216
                .get_name_for_weekday(weekday, l, iso_weekday)
217
                .map_err(|e| match e {
1✔
218
                    GetNameForWeekdayError::NotLoaded => DateTimeWriteError::NamesNotLoaded(field),
1✔
219
                }) {
1✔
220
                Err(e) => {
1✔
221
                    w.with_part(Part::ERROR, |w| {
2✔
222
                        w.write_str(match iso_weekday {
2✔
223
                            IsoWeekday::Monday => "mon",
1✔
UNCOV
224
                            IsoWeekday::Tuesday => "tue",
×
UNCOV
225
                            IsoWeekday::Wednesday => "wed",
×
UNCOV
226
                            IsoWeekday::Thursday => "thu",
×
UNCOV
227
                            IsoWeekday::Friday => "fri",
×
UNCOV
228
                            IsoWeekday::Saturday => "sat",
×
UNCOV
229
                            IsoWeekday::Sunday => "sun",
×
230
                        })
231
                    })?;
1✔
232
                    Err(e)
1✔
233
                }
1✔
234
                Ok(s) => Ok(w.write_str(s)?),
212✔
235
            }
236
        }
237
        (FieldSymbol::Day(fields::Day::DayOfMonth), l) => {
324✔
238
            input!(day_of_month = input.day_of_month);
324✔
239
            try_write_number(w, fdf, day_of_month.0.into(), l)?
323✔
240
        }
323✔
UNCOV
241
        (FieldSymbol::Day(fields::Day::DayOfWeekInMonth), l) => {
×
UNCOV
242
            input!(day_of_month = input.day_of_month);
×
UNCOV
243
            try_write_number(w, fdf, DayOfWeekInMonth::from(day_of_month).0.into(), l)?
×
UNCOV
244
        }
×
UNCOV
245
        (FieldSymbol::Day(fields::Day::DayOfYear), l) => {
×
UNCOV
246
            input!(day_of_year = input.day_of_year);
×
UNCOV
247
            try_write_number(w, fdf, day_of_year.day_of_year.into(), l)?
×
UNCOV
248
        }
×
249
        (FieldSymbol::Hour(symbol), l) => {
334✔
250
            input!(hour = input.hour);
334✔
251
            let h = hour.number();
333✔
252
            let h = match symbol {
333✔
253
                fields::Hour::H11 => h % 12,
11✔
254
                fields::Hour::H12 => {
255
                    let v = h % 12;
146✔
256
                    if v == 0 {
146✔
257
                        12
47✔
258
                    } else {
259
                        v
99✔
260
                    }
261
                }
262
                fields::Hour::H23 => h,
166✔
263
                fields::Hour::H24 => {
264
                    if h == 0 {
10✔
265
                        24
10✔
266
                    } else {
UNCOV
267
                        h
×
268
                    }
269
                }
270
            };
271
            try_write_number(w, fdf, h.into(), l)?
333✔
272
        }
333✔
273
        (FieldSymbol::Minute, l) => {
296✔
274
            input!(minute = input.minute);
296✔
275
            try_write_number(w, fdf, minute.number().into(), l)?
295✔
276
        }
295✔
277
        (FieldSymbol::Second(Second::Second), l) => {
193✔
278
            input!(second = input.second);
193✔
279
            try_write_number(w, fdf, second.number().into(), l)?
193✔
280
        }
193✔
UNCOV
281
        (FieldSymbol::Second(Second::MillisInDay), l) => {
×
UNCOV
282
            input!(hour = input.hour);
×
UNCOV
283
            input!(minute = input.minute);
×
UNCOV
284
            input!(second = input.second);
×
UNCOV
285
            input!(nanosecond = input.nanosecond);
×
286

287
            let milliseconds = (((hour.number() as u32 * 60) + minute.number() as u32) * 60
×
UNCOV
288
                + second.number() as u32)
×
289
                * 1000
UNCOV
290
                + nanosecond.number() / 1_000_000;
×
UNCOV
291
            try_write_number(w, fdf, milliseconds.into(), l)?
×
UNCOV
292
        }
×
293
        (FieldSymbol::DecimalSecond(decimal_second), l) => {
6✔
294
            input!(second = input.second);
6✔
295
            input!(nanosecond = input.nanosecond);
5✔
296

297
            // Formatting with fractional seconds
298
            let mut s = FixedDecimal::from(second.number());
5✔
299
            let _infallible =
300
                s.concatenate_end(FixedDecimal::from(nanosecond.number()).multiplied_pow10(-9));
5✔
301
            debug_assert!(_infallible.is_ok());
5✔
302
            let position = -(decimal_second as i16);
5✔
303
            s.trunc(position);
5✔
304
            s.pad_end(position);
5✔
305
            try_write_number(w, fdf, s, l)?
5✔
306
        }
5✔
307
        (FieldSymbol::DayPeriod(period), l) => {
268✔
308
            input!(hour = input.hour);
268✔
309

310
            match datetime_names.get_name_for_day_period(
267✔
311
                period,
312
                l,
313
                hour,
314
                pattern_metadata.time_granularity().is_top_of_hour(
534✔
315
                    input.minute.unwrap_or_default().number(),
267✔
316
                    input.second.unwrap_or_default().number(),
267✔
317
                    input.nanosecond.unwrap_or_default().number(),
267✔
318
                ),
319
            ) {
320
                Err(e) => {
321
                    w.with_part(Part::ERROR, |w| {
2✔
322
                        w.write_str(if hour.number() < 12 { "AM" } else { "PM" })
1✔
323
                    })?;
1✔
324
                    Err(match e {
1✔
325
                        GetNameForDayPeriodError::NotLoaded => {
326
                            DateTimeWriteError::NamesNotLoaded(field)
1✔
327
                        }
328
                    })
329
                }
1✔
330
                Ok(s) => Ok(w.write_str(s)?),
266✔
331
            }
332
        }
333
        (FieldSymbol::TimeZone(fields::TimeZone::SpecificNonLocation), FieldLength::Four) => {
334
            perform_timezone_fallback(
30✔
335
                w,
336
                input,
337
                datetime_names,
338
                fdf,
339
                field,
340
                &[
341
                    TimeZoneFormatterUnit::SpecificNonLocation(FieldLength::Four),
342
                    TimeZoneFormatterUnit::SpecificLocation,
343
                    TimeZoneFormatterUnit::LocalizedOffset(FieldLength::Four),
344
                ],
345
            )?
346
        }
30✔
347
        (FieldSymbol::TimeZone(fields::TimeZone::SpecificNonLocation), l) => {
33✔
348
            perform_timezone_fallback(
33✔
349
                w,
350
                input,
351
                datetime_names,
352
                fdf,
353
                field,
354
                &[
33✔
355
                    TimeZoneFormatterUnit::SpecificNonLocation(l),
33✔
356
                    TimeZoneFormatterUnit::LocalizedOffset(l),
33✔
357
                ],
358
            )?
359
        }
33✔
360
        (FieldSymbol::TimeZone(fields::TimeZone::GenericNonLocation), l) => {
62✔
361
            perform_timezone_fallback(
62✔
362
                w,
363
                input,
364
                datetime_names,
365
                fdf,
366
                field,
367
                &[
62✔
368
                    TimeZoneFormatterUnit::GenericNonLocation(l),
62✔
369
                    TimeZoneFormatterUnit::GenericLocation,
62✔
370
                    TimeZoneFormatterUnit::LocalizedOffset(l),
62✔
371
                ],
372
            )?
373
        }
62✔
374
        (FieldSymbol::TimeZone(fields::TimeZone::Location), FieldLength::Four) => {
375
            perform_timezone_fallback(
28✔
376
                w,
377
                input,
378
                datetime_names,
379
                fdf,
380
                field,
381
                &[
382
                    TimeZoneFormatterUnit::GenericLocation,
383
                    TimeZoneFormatterUnit::LocalizedOffset(FieldLength::Four),
384
                ],
385
            )?
386
        }
28✔
387
        (FieldSymbol::TimeZone(fields::TimeZone::LocalizedOffset), l) => perform_timezone_fallback(
128✔
388
            w,
389
            input,
390
            datetime_names,
391
            fdf,
392
            field,
393
            &[TimeZoneFormatterUnit::LocalizedOffset(l)],
64✔
394
        )?,
64✔
395
        (FieldSymbol::TimeZone(fields::TimeZone::Location), _) => perform_timezone_fallback(
1✔
396
            w,
397
            input,
398
            datetime_names,
399
            fdf,
400
            field,
401
            &[TimeZoneFormatterUnit::Bcp47Id],
402
        )?,
1✔
403
        (FieldSymbol::TimeZone(fields::TimeZone::IsoWithZ), l) => perform_timezone_fallback(
28✔
404
            w,
405
            input,
406
            datetime_names,
407
            fdf,
408
            field,
409
            &[TimeZoneFormatterUnit::Iso8601(Iso8601Format::with_z(l))],
14✔
410
        )?,
14✔
411
        (FieldSymbol::TimeZone(fields::TimeZone::Iso), l) => perform_timezone_fallback(
26✔
412
            w,
413
            input,
414
            datetime_names,
415
            fdf,
416
            field,
417
            &[TimeZoneFormatterUnit::Iso8601(Iso8601Format::without_z(l))],
13✔
418
        )?,
2,742✔
419
    })
420
}
2,729✔
421

422
// Writes an error string for the given symbol
423
fn write_value_missing(
10✔
424
    w: &mut (impl writeable::PartsWrite + ?Sized),
425
    field: fields::Field,
426
) -> Result<(), fmt::Error> {
427
    w.with_part(Part::ERROR, |w| {
20✔
428
        "{".write_to(w)?;
10✔
429
        char::from(field.symbol).write_to(w)?;
10✔
430
        "}".write_to(w)
10✔
431
    })
10✔
432
}
10✔
433

434
fn perform_timezone_fallback(
245✔
435
    w: &mut (impl writeable::PartsWrite + ?Sized),
436
    input: &ExtractedInput,
437
    datetime_names: &RawDateTimeNamesBorrowed,
438
    fdf: Option<&FixedDecimalFormatter>,
439
    field: fields::Field,
440
    units: &[TimeZoneFormatterUnit],
441
) -> Result<Result<(), DateTimeWriteError>, core::fmt::Error> {
442
    let payloads = datetime_names.get_payloads();
245✔
443
    let mut r = Err(FormatTimeZoneError::Fallback);
245✔
444
    for unit in units {
328✔
445
        match unit.format(w, input, payloads, fdf)? {
328✔
446
            Err(FormatTimeZoneError::Fallback) => {
447
                // Expected, try the next unit
448
                continue;
449
            }
450
            r2 => {
451
                r = r2;
245✔
452
                break;
453
            }
454
        }
455
    }
456

457
    Ok(match r {
490✔
458
        Ok(()) => Ok(()),
243✔
459
        Err(e) => {
2✔
460
            if let Some(offset) = input.offset {
2✔
461
                w.with_part(Part::ERROR, |w| {
2✔
462
                    Iso8601Format::without_z(field.length).format_infallible(w, offset)
1✔
463
                })?;
1✔
464
            } else {
465
                write_value_missing(w, field)?;
246✔
466
            }
467
            match e {
2✔
468
                FormatTimeZoneError::FixedDecimalFormatterNotLoaded => {
469
                    Err(DateTimeWriteError::FixedDecimalFormatterNotLoaded)
×
470
                }
471
                FormatTimeZoneError::NamesNotLoaded => {
472
                    Err(DateTimeWriteError::NamesNotLoaded(field))
1✔
473
                }
474
                FormatTimeZoneError::MissingInputField(f) => {
1✔
475
                    Err(DateTimeWriteError::MissingInputField(f))
1✔
476
                }
1✔
477
                FormatTimeZoneError::Fallback => {
UNCOV
478
                    debug_assert!(false, "timezone fallback chain fell through {input:?}");
×
479
                    Ok(())
480
                }
481
            }
482
        }
483
    })
484
}
245✔
485

486
#[cfg(test)]
487
#[allow(unused_imports)]
488
#[cfg(feature = "compiled_data")]
489
mod tests {
490
    use super::*;
491
    use icu_decimal::options::{FixedDecimalFormatterOptions, GroupingStrategy};
492

493
    #[test]
494
    fn test_format_number() {
2✔
495
        let values = &[2, 20, 201, 2017, 20173];
1✔
496
        let samples = &[
1✔
497
            (FieldLength::One, ["2", "20", "201", "2017", "20173"]),
498
            (FieldLength::Two, ["02", "20", "201", "2017", "20173"]),
499
            (FieldLength::Three, ["002", "020", "201", "2017", "20173"]),
500
            (FieldLength::Four, ["0002", "0020", "0201", "2017", "20173"]),
501
        ];
502

503
        let mut fixed_decimal_format_options = FixedDecimalFormatterOptions::default();
1✔
504
        fixed_decimal_format_options.grouping_strategy = GroupingStrategy::Never;
1✔
505
        let fixed_decimal_format = FixedDecimalFormatter::try_new(
1✔
506
            icu_locale_core::locale!("en").into(),
1✔
507
            fixed_decimal_format_options,
1✔
508
        )
509
        .unwrap();
510

511
        for (length, expected) in samples {
5✔
512
            for (value, expected) in values.iter().zip(expected) {
4✔
513
                let mut s = String::new();
20✔
514
                try_write_number(
20✔
515
                    &mut writeable::adapters::CoreWriteAsPartsWrite(&mut s),
20✔
516
                    Some(&fixed_decimal_format),
20✔
517
                    FixedDecimal::from(*value),
20✔
518
                    *length,
20✔
519
                )
520
                .unwrap()
521
                .unwrap();
522
                assert_eq!(s, *expected);
20✔
523
            }
20✔
524
        }
525
    }
32✔
526
}
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