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

zbraniecki / icu4x / 9457158389

10 Jun 2024 11:45PM UTC coverage: 75.174% (+0.05%) from 75.121%
9457158389

push

github

web-flow
Add constructing TinyAsciiStr from utf16 (#5025)

Introduces TinyAsciiStr constructors from utf16 and converges on the
consensus from #4931.

---------

Co-authored-by: Robert Bastian <4706271+robertbastian@users.noreply.github.com>

65 of 82 new or added lines in 14 files covered. (79.27%)

3441 existing lines in 141 files now uncovered.

52850 of 70304 relevant lines covered (75.17%)

563298.06 hits per line

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

77.42
/components/datetime/src/format/datetime.rs
1
// This file is part of ICU4X. For terms of use, please see the file
1✔
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 crate::fields::{self, Field, FieldLength, FieldSymbol, Second, TimeZone, Week, Year};
6
use crate::input::{DateInput, ExtractedDateTimeInput, IsoTimeInput};
7
use crate::pattern::runtime::{PatternBorrowed, PatternMetadata};
8
use crate::pattern::{
9
    runtime::{Pattern, PatternPlurals},
10
    PatternItem,
11
};
12
use crate::provider;
13
use crate::provider::calendar::patterns::PatternPluralsFromPatternsV1Marker;
14
#[cfg(feature = "experimental")]
15
use crate::provider::date_time::GetSymbolForDayPeriodError;
16
use crate::provider::date_time::{
17
    DateSymbols, GetSymbolForEraError, GetSymbolForMonthError, GetSymbolForTimeZoneError,
18
    GetSymbolForWeekdayError, MonthPlaceholderValue, TimeSymbols, ZoneSymbols,
19
};
20

21
use core::fmt::{self, Write};
22
use core::iter::Peekable;
23
use fixed_decimal::FixedDecimal;
24
use icu_calendar::types::{
25
    Era, {DayOfWeekInMonth, IsoWeekday, MonthCode},
26
};
27
use icu_calendar::week::WeekCalculator;
28
use icu_calendar::AnyCalendarKind;
29
use icu_decimal::FixedDecimalFormatter;
30
use icu_plurals::PluralRules;
31
use icu_provider::DataPayload;
32
use icu_timezone::{CustomTimeZone, GmtOffset};
33
use writeable::{Part, Writeable};
34

35
/// [`FormattedDateTime`] is a intermediate structure which can be retrieved as
36
/// an output from [`TypedDateTimeFormatter`](crate::TypedDateTimeFormatter).
37
///
38
/// The structure contains all the information needed to display formatted value,
39
/// and it will also contain additional methods allowing the user to introspect
40
/// and even manipulate the formatted data.
41
///
42
/// # Examples
43
///
44
/// ```no_run
45
/// use icu::calendar::{DateTime, Gregorian};
46
/// use icu::datetime::TypedDateTimeFormatter;
47
/// use icu::locale::locale;
48
/// let dtf = TypedDateTimeFormatter::<Gregorian>::try_new(
49
///     &locale!("en").into(),
50
///     Default::default(),
51
/// )
52
/// .expect("Failed to create TypedDateTimeFormatter instance.");
53
///
54
/// let datetime = DateTime::try_new_gregorian_datetime(2020, 9, 1, 12, 34, 28)
55
///     .expect("Failed to construct DateTime.");
56
///
57
/// let formatted_date = dtf.format(&datetime);
58
///
59
/// let _ = format!("Date: {}", formatted_date);
60
/// ```
UNCOV
61
#[derive(Debug, Copy, Clone)]
×
62
pub struct FormattedDateTime<'l> {
63
    pub(crate) datetime: ExtractedDateTimeInput,
UNCOV
64
    pub(crate) patterns: &'l DataPayload<PatternPluralsFromPatternsV1Marker>,
×
65
    pub(crate) date_symbols: Option<&'l provider::calendar::DateSymbolsV1<'l>>,
×
66
    pub(crate) time_symbols: Option<&'l provider::calendar::TimeSymbolsV1<'l>>,
×
67
    pub(crate) week_data: Option<&'l WeekCalculator>,
×
68
    pub(crate) ordinal_rules: Option<&'l PluralRules>,
×
69
    pub(crate) fixed_decimal_format: &'l FixedDecimalFormatter,
×
70
}
71

72
impl<'l> FormattedDateTime<'l> {
73
    pub(crate) fn select_pattern_lossy<'a>(
5,291✔
74
        &'a self,
75
    ) -> (&'l Pattern<'l>, Result<(), DateTimeWriteError>) {
76
        let mut r = Ok(());
5,291✔
77
        let pattern = match self.patterns.get().0 {
5,291✔
78
            PatternPlurals::SinglePattern(ref pattern) => pattern,
5,231✔
79
            PatternPlurals::MultipleVariants(ref plural_pattern) => {
60✔
80
                let week_number = match plural_pattern.pivot_field() {
60✔
UNCOV
81
                    Week::WeekOfMonth => self
×
82
                        .week_data
UNCOV
83
                        .ok_or(DateTimeWriteError::MissingWeekCalculator)
×
84
                        .and_then(|w| {
×
85
                            self.datetime
×
86
                                .week_of_month(w)
87
                                .map_err(DateTimeWriteError::MissingInputField)
88
                        })
×
UNCOV
89
                        .map(|w| w.0)
×
90
                        .unwrap_or_else(|e| {
×
91
                            r = r.and(Err(e));
×
92
                            0
93
                        }),
×
94
                    Week::WeekOfYear => self
240✔
95
                        .week_data
96
                        .ok_or(DateTimeWriteError::MissingWeekCalculator)
60✔
97
                        .and_then(|w| {
120✔
98
                            self.datetime
60✔
99
                                .week_of_year(w)
100
                                .map_err(DateTimeWriteError::MissingInputField)
101
                        })
60✔
102
                        .map(|w| w.1 .0)
60✔
103
                        .unwrap_or_else(|e| {
60✔
UNCOV
104
                            r = r.and(Err(e));
×
105
                            0
UNCOV
106
                        }),
×
107
                };
108
                let category = self
180✔
109
                    .ordinal_rules
110
                    .map(|p| p.category_for(week_number))
120✔
111
                    .unwrap_or_else(|| {
60✔
UNCOV
112
                        r = r.and(Err(DateTimeWriteError::MissingOrdinalRules));
×
UNCOV
113
                        icu_plurals::PluralCategory::One
×
UNCOV
114
                    });
×
115
                plural_pattern.variant(category)
60✔
116
            }
60✔
117
        };
118
        (pattern, r)
5,291✔
119
    }
5,291✔
120
}
121

122
impl<'l> Writeable for FormattedDateTime<'l> {
123
    fn write_to<W: fmt::Write + ?Sized>(&self, sink: &mut W) -> fmt::Result {
3,752✔
124
        let (pattern, mut r) = self.select_pattern_lossy();
3,752✔
125

126
        r = r.and(try_write_pattern(
7,504✔
127
            pattern.as_borrowed(),
3,752✔
128
            &self.datetime,
129
            self.date_symbols,
3,752✔
130
            self.time_symbols,
3,752✔
131
            None::<()>.as_ref(),
3,752✔
132
            self.week_data,
3,752✔
133
            Some(self.fixed_decimal_format),
3,752✔
134
            &mut writeable::adapters::CoreWriteAsPartsWrite(sink),
3,752✔
135
        )?);
3,752✔
136

137
        debug_assert!(r.is_ok(), "{r:?}");
3,752✔
138
        Ok(())
3,752✔
139
    }
3,752✔
140

141
    // TODO(#489): Implement writeable_length_hint
142
}
143

144
impl<'l> fmt::Display for FormattedDateTime<'l> {
145
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
749✔
146
        self.write_to(f)
749✔
147
    }
749✔
148
}
149

150
// Apply length to input number and write to result using fixed_decimal_format.
151
fn try_write_number<W>(
11,273✔
152
    result: &mut W,
153
    fixed_decimal_format: Option<&FixedDecimalFormatter>,
154
    mut num: FixedDecimal,
155
    length: FieldLength,
156
) -> Result<Result<(), DateTimeWriteError>, fmt::Error>
157
where
158
    W: writeable::PartsWrite + ?Sized,
159
{
160
    if let Some(fdf) = fixed_decimal_format {
22,536✔
161
        match length {
11,273✔
162
            FieldLength::One | FieldLength::NumericOverride(_) => {}
163
            FieldLength::TwoDigit => {
164
                num.pad_start(2);
5,117✔
165
                num.set_max_position(2);
5,116✔
166
            }
167
            FieldLength::Abbreviated => {
168
                num.pad_start(3);
5✔
169
            }
170
            FieldLength::Wide => {
171
                num.pad_start(4);
5✔
172
            }
173
            FieldLength::Narrow => {
UNCOV
174
                num.pad_start(5);
×
175
            }
176
            FieldLength::Six => {
UNCOV
177
                num.pad_start(6);
×
178
            }
UNCOV
179
            FieldLength::Fixed(p) => {
×
UNCOV
180
                num.pad_start(p as i16);
×
181
                num.set_max_position(p as i16);
×
182
            }
183
        }
184

185
        fdf.format(&num).write_to(result)?;
11,273✔
186
        Ok(Ok(()))
11,263✔
187
    } else {
188
        result.with_part(writeable::Part::ERROR, |r| num.write_to(r))?;
11,273✔
UNCOV
189
        Ok(Err(DateTimeWriteError::MissingFixedDecimalFormatter))
×
190
    }
191
}
11,263✔
192

193
#[allow(clippy::too_many_arguments)]
194
pub(crate) fn try_write_pattern<'data, W, DS, TS, ZS>(
3,981✔
195
    pattern: PatternBorrowed<'data>,
3,981✔
196
    datetime: &ExtractedDateTimeInput,
197
    date_symbols: Option<&DS>,
198
    time_symbols: Option<&TS>,
199
    zone_symbols: Option<&ZS>,
200
    week_data: Option<&'data WeekCalculator>,
201
    fixed_decimal_format: Option<&FixedDecimalFormatter>,
202
    w: &mut W,
203
) -> Result<Result<(), DateTimeWriteError>, fmt::Error>
204
where
205
    W: writeable::PartsWrite + ?Sized,
206
    DS: DateSymbols<'data>,
207
    TS: TimeSymbols,
208
    ZS: ZoneSymbols,
209
{
210
    try_write_pattern_items(
3,981✔
211
        pattern.metadata,
3,981✔
212
        pattern.items.iter(),
3,981✔
213
        datetime,
214
        date_symbols,
215
        time_symbols,
216
        zone_symbols,
217
        week_data,
218
        fixed_decimal_format,
219
        w,
220
    )
221
}
3,981✔
222

223
#[allow(clippy::too_many_arguments)]
224
pub(crate) fn try_write_pattern_items<'data, W, DS, TS, ZS>(
4,278✔
225
    pattern_metadata: PatternMetadata,
226
    pattern_items: impl Iterator<Item = PatternItem>,
227
    datetime: &ExtractedDateTimeInput,
228
    date_symbols: Option<&DS>,
229
    time_symbols: Option<&TS>,
230
    zone_symbols: Option<&ZS>,
231
    week_data: Option<&'data WeekCalculator>,
232
    fixed_decimal_format: Option<&FixedDecimalFormatter>,
233
    w: &mut W,
234
) -> Result<Result<(), DateTimeWriteError>, fmt::Error>
235
where
236
    W: writeable::PartsWrite + ?Sized,
237
    DS: DateSymbols<'data>,
238
    TS: TimeSymbols,
239
    ZS: ZoneSymbols,
240
{
241
    let mut r = Ok(());
4,278✔
242
    let mut iter = pattern_items.peekable();
4,278✔
243
    while let Some(item) = iter.next() {
36,547✔
244
        match item {
32,272✔
245
            PatternItem::Literal(ch) => w.write_char(ch)?,
16,371✔
246
            PatternItem::Field(field) => {
15,901✔
247
                r = r.and(try_write_field(
15,901✔
248
                    field,
249
                    &mut iter,
250
                    pattern_metadata,
251
                    datetime,
252
                    date_symbols,
253
                    time_symbols,
254
                    zone_symbols,
255
                    week_data,
256
                    fixed_decimal_format,
257
                    w,
258
                )?)
4,278✔
259
            }
15,899✔
260
        }
261
    }
262
    Ok(r)
4,270✔
263
}
4,270✔
264

265
#[non_exhaustive]
266
#[derive(Debug, PartialEq, Copy, Clone, displaydoc::Display)]
4✔
267
/// Error for `TryWriteable` implementations
268
pub enum DateTimeWriteError {
269
    // Data not loaded
270
    /// Missing FixedDecimalFormatter
271
    #[displaydoc("FixedDecimalFormatter not loaded")]
272
    MissingFixedDecimalFormatter,
273
    // TODO: Remove Missing*Symbols and use exclusively MissingNames
274
    /// Missing DateSymbols
275
    #[displaydoc("DateSymbols not loaded")]
276
    MissingDateSymbols,
277
    /// Missing ZoneSymbols
278
    #[displaydoc("ZoneSymbols not loaded")]
279
    MissingZoneSymbols,
280
    /// Missing TimeSymbols
281
    #[displaydoc("TimeSymbols not loaded")]
282
    MissingTimeSymbols,
283
    /// Missing OrdinalRules
284
    #[displaydoc("OrdinalRules not loaded")]
285
    MissingOrdinalRules,
286
    /// Missing WeekCalculator
287
    #[displaydoc("WeekCalculator not loaded")]
288
    MissingWeekCalculator,
289
    /// TODO
290
    #[displaydoc("Names for {0:?} not loaded")]
UNCOV
291
    MissingNames(Field),
×
292

293
    // Something not found in data
294
    // TODO: Are these actionable? Can clients even invent their own months and days?
295
    /// Missing month symbol
296
    #[displaydoc("Cannot find symbol for month {0:?}")]
UNCOV
297
    MissingMonthSymbol(MonthCode),
×
298
    /// Missing era symbol
299
    #[displaydoc("Cannot find symbol for era {0:?}")]
300
    MissingEraSymbol(Era),
2✔
301
    /// Missing weekday symbol
302
    #[displaydoc("Cannot find symbol for weekday {0:?}")]
303
    MissingWeekdaySymbol(IsoWeekday),
×
304

305
    // Invalid input
306
    /// Incomplete input
307
    #[displaydoc("Incomplete input, missing value for {0:?}")]
308
    MissingInputField(&'static str),
×
309
    /// Cyclic year overflow
310
    #[displaydoc("Cyclic year overflow, found {value}, maximum {max}")]
×
311
    CyclicYearOverflow {
312
        /// Value
UNCOV
313
        value: usize,
×
314
        /// Max
UNCOV
315
        max: usize,
×
316
    },
317
    /// Unsupported field
318
    #[displaydoc("Unsupported field {0:?}")]
UNCOV
319
    UnsupportedField(Field),
×
320
}
321

322
// This function assumes that the correct decision has been
323
// made regarding availability of symbols in the caller.
324
//
325
// When modifying the list of fields using symbols,
326
// update the matching query in `analyze_pattern` function.
327
#[allow(clippy::too_many_arguments)]
328
pub(crate) fn try_write_field<'data, W, DS, TS, ZS>(
16,924✔
329
    field: fields::Field,
330
    iter: &mut Peekable<impl Iterator<Item = PatternItem>>,
331
    pattern_metadata: PatternMetadata,
332
    datetime: &ExtractedDateTimeInput,
333
    date_symbols: Option<&DS>,
334
    time_symbols: Option<&TS>,
335
    zone_symbols: Option<&ZS>,
336
    week_data: Option<&WeekCalculator>,
337
    fdf: Option<&FixedDecimalFormatter>,
338
    w: &mut W,
339
) -> Result<Result<(), DateTimeWriteError>, fmt::Error>
340
where
341
    W: writeable::PartsWrite + ?Sized,
342
    DS: DateSymbols<'data>,
343
    TS: TimeSymbols,
344
    ZS: ZoneSymbols,
345
{
346
    // Writes an error string for the given symbol
347
    fn write_value_missing(
40✔
348
        w: &mut (impl writeable::PartsWrite + ?Sized),
349
        field: fields::Field,
350
    ) -> Result<(), fmt::Error> {
351
        w.with_part(Part::ERROR, |w| {
80✔
352
            "{".write_to(w)?;
40✔
353
            char::from(field.symbol).write_to(w)?;
40✔
354
            "}".write_to(w)
40✔
355
        })
40✔
356
    }
40✔
357

358
    fn try_write_time_zone_gmt(
4✔
359
        w: &mut (impl writeable::PartsWrite + ?Sized),
360
        _gmt_offset: Option<GmtOffset>,
361
        zone_symbols: Option<&impl ZoneSymbols>,
362
        _graceful: bool,
363
    ) -> Result<Result<(), DateTimeWriteError>, fmt::Error> {
364
        #[allow(clippy::bind_instead_of_map)] // TODO: Use proper formatting logic here
365
        Ok(
4✔
366
            match zone_symbols
4✔
367
                .ok_or(DateTimeWriteError::MissingZoneSymbols)
4✔
368
                .and_then(|_zs| {
4✔
369
                    // TODO: Use proper formatting logic here
370
                    Ok("{todo}")
4✔
371
                }) {
4✔
UNCOV
372
                Err(e) => Err(e),
×
373
                Ok(s) => Ok(s.write_to(w)?),
4✔
374
            },
375
        )
376
    }
4✔
377

UNCOV
378
    fn write_time_zone_missing(
×
379
        w: &mut (impl writeable::PartsWrite + ?Sized),
380
    ) -> Result<(), fmt::Error> {
UNCOV
381
        w.with_part(Part::ERROR, |w| "GMT+?".write_to(w))
×
UNCOV
382
    }
×
383

384
    Ok(match (field.symbol, field.length) {
33,848✔
385
        (FieldSymbol::Era, l) => match datetime.year() {
1,147✔
386
            None => {
387
                write_value_missing(w, field)?;
4✔
388
                Err(DateTimeWriteError::MissingInputField("year"))
4✔
389
            }
390
            Some(year) => match date_symbols
3,429✔
391
                .ok_or(DateTimeWriteError::MissingDateSymbols)
1,143✔
392
                .and_then(|ds| {
2,286✔
393
                    ds.get_symbol_for_era(l, &year.era).map_err(|e| match e {
1,152✔
394
                        GetSymbolForEraError::Missing => {
395
                            DateTimeWriteError::MissingEraSymbol(year.era)
5✔
396
                        }
397
                        #[cfg(feature = "experimental")]
398
                        GetSymbolForEraError::MissingNames(f) => {
4✔
399
                            DateTimeWriteError::MissingNames(f)
4✔
400
                        }
4✔
401
                    })
9✔
402
                }) {
1,143✔
403
                Err(e) => {
9✔
404
                    w.with_part(Part::ERROR, |w| w.write_str(&year.era.0))?;
18✔
405
                    Err(e)
9✔
406
                }
9✔
407
                Ok(era) => Ok(w.write_str(era)?),
1,134✔
408
            },
409
        },
410
        (FieldSymbol::Year(Year::Calendar), l) => match datetime.year() {
2,399✔
411
            None => {
412
                write_value_missing(w, field)?;
4✔
413
                Err(DateTimeWriteError::MissingInputField("year"))
4✔
414
            }
415
            Some(year) => try_write_number(w, fdf, year.number.into(), l)?,
2,395✔
416
        },
417
        (FieldSymbol::Year(Year::WeekOf), l) => match week_data
549✔
418
            .ok_or(DateTimeWriteError::MissingWeekCalculator)
183✔
419
            .and_then(|w| {
366✔
420
                datetime
183✔
421
                    .week_of_year(w)
422
                    .map_err(DateTimeWriteError::MissingInputField)
423
            }) {
183✔
UNCOV
424
            Err(e) => {
×
UNCOV
425
                write_value_missing(w, field)?;
×
UNCOV
426
                Err(e)
×
UNCOV
427
            }
×
428
            Ok((year, _)) => try_write_number(w, fdf, year.number.into(), l)?,
183✔
429
        },
430
        (FieldSymbol::Year(Year::Cyclic), l) => match datetime.year() {
105✔
431
            None => {
UNCOV
432
                write_value_missing(w, field)?;
×
UNCOV
433
                Err(DateTimeWriteError::MissingInputField("year"))
×
434
            }
435
            Some(year) => {
105✔
436
                let r = year
315✔
437
                    .cyclic
438
                    .ok_or(DateTimeWriteError::MissingInputField("cyclic"))
105✔
439
                    .and_then(|cyclic| {
210✔
440
                        // TODO(#3761): This is a hack, we should use actual data for cyclic years
441
                        let cyclics: &[&str; 60] = match datetime.any_calendar_kind() {
210✔
442
                            Some(AnyCalendarKind::Dangi) => &[
45✔
443
                                "갑자", "을축", "병인", "정묘", "무진", "기사", "경오", "신미",
444
                                "임신", "계유", "갑술", "을해", "병자", "정축", "무인", "기묘",
445
                                "경진", "신사", "임오", "계미", "갑신", "을유", "병술", "정해",
446
                                "무자", "기축", "경인", "신묘", "임진", "계사", "갑오", "을미",
447
                                "병신", "정유", "무술", "기해", "경자", "신축", "임인", "계묘",
448
                                "갑진", "을사", "병오", "정미", "무신", "기유", "경술", "신해",
449
                                "임자", "계축", "갑인", "을묘", "병진", "정사", "무오", "기미",
450
                                "경신", "신유", "임술", "계해",
451
                            ],
45✔
452
                            // for now assume all other calendars use the stem-branch model
453
                            _ => &[
60✔
454
                                "甲子", "乙丑", "丙寅", "丁卯", "戊辰", "己巳", "庚午", "辛未",
455
                                "壬申", "癸酉", "甲戌", "乙亥", "丙子", "丁丑", "戊寅", "己卯",
456
                                "庚辰", "辛巳", "壬午", "癸未", "甲申", "乙酉", "丙戌", "丁亥",
457
                                "戊子", "己丑", "庚寅", "辛卯", "壬辰", "癸巳", "甲午", "乙未",
458
                                "丙申", "丁酉", "戊戌", "己亥", "庚子", "辛丑", "壬寅", "癸卯",
459
                                "甲辰", "乙巳", "丙午", "丁未", "戊申", "己酉", "庚戌", "辛亥",
460
                                "壬子", "癸丑", "甲寅", "乙卯", "丙辰", "丁巳", "戊午", "己未",
461
                                "庚申", "辛酉", "壬戌", "癸亥",
462
                            ],
60✔
463
                        };
464
                        let value: usize = cyclic.get() as usize;
105✔
465
                        cyclics
210✔
466
                            .get(value - 1)
105✔
467
                            .ok_or(DateTimeWriteError::CyclicYearOverflow {
105✔
468
                                value,
469
                                max: cyclics.len() + 1,
470
                            })
471
                    });
105✔
472
                match r {
105✔
UNCOV
473
                    Err(e) => {
×
UNCOV
474
                        w.with_part(Part::ERROR, |w| {
×
UNCOV
475
                            try_write_number(w, fdf, year.number.into(), l).map(|_| ())
×
UNCOV
476
                        })?;
×
UNCOV
477
                        Err(e)
×
UNCOV
478
                    }
×
479
                    Ok(cyclic_str) => Ok(w.write_str(cyclic_str)?),
105✔
480
                }
481
            }
482
        },
483
        (FieldSymbol::Year(Year::RelatedIso), l) => {
105✔
484
            match datetime
210✔
485
                .year()
486
                .ok_or(DateTimeWriteError::MissingInputField("year"))
105✔
487
                .and_then(|year| {
105✔
488
                    year.related_iso
210✔
489
                        .ok_or(DateTimeWriteError::MissingInputField("related_iso"))
105✔
490
                }) {
105✔
UNCOV
491
                Err(e) => {
×
UNCOV
492
                    write_value_missing(w, field)?;
×
UNCOV
493
                    Err(e)
×
UNCOV
494
                }
×
495
                Ok(iso) => try_write_number(w, fdf, iso.into(), l)?,
105✔
496
            }
497
        }
498
        (FieldSymbol::Month(_), l @ (FieldLength::One | FieldLength::TwoDigit)) => {
631✔
499
            match datetime.month() {
631✔
500
                None => {
UNCOV
501
                    write_value_missing(w, field)?;
×
UNCOV
502
                    Err(DateTimeWriteError::MissingInputField("month"))
×
503
                }
504
                Some(month) => try_write_number(w, fdf, month.ordinal.into(), l)?,
631✔
505
            }
506
        }
507
        (FieldSymbol::Month(month), l) => match datetime.month() {
1,758✔
508
            None => {
509
                write_value_missing(w, field)?;
4✔
510
                Err(DateTimeWriteError::MissingInputField("month"))
4✔
511
            }
512
            Some(formattable_month) => match date_symbols
5,262✔
513
                .ok_or(DateTimeWriteError::MissingDateSymbols)
1,754✔
514
                .and_then(|ds| {
3,508✔
515
                    ds.get_symbol_for_month(month, l, formattable_month.code)
3,508✔
516
                        .map_err(|e| match e {
1,758✔
517
                            GetSymbolForMonthError::Missing => {
UNCOV
518
                                DateTimeWriteError::MissingMonthSymbol(formattable_month.code)
×
519
                            }
520
                            #[cfg(feature = "experimental")]
521
                            GetSymbolForMonthError::MissingNames(f) => {
4✔
522
                                DateTimeWriteError::MissingNames(f)
4✔
523
                            }
4✔
524
                        })
4✔
525
                }) {
1,754✔
526
                Err(e) => {
4✔
527
                    w.with_part(Part::ERROR, |w| w.write_str(&formattable_month.code.0))?;
8✔
528
                    Err(e)
4✔
529
                }
4✔
530
                Ok(MonthPlaceholderValue::PlainString(symbol)) => {
1,720✔
531
                    w.write_str(symbol)?;
1,720✔
532
                    Ok(())
1,720✔
533
                }
1,720✔
534
                Ok(MonthPlaceholderValue::StringNeedingLeapPrefix(symbol)) => {
30✔
535
                    // FIXME (#3766) this should be using actual data for leap months
536
                    let leap_str = match datetime.any_calendar_kind() {
30✔
537
                        Some(AnyCalendarKind::Chinese) => "閏",
30✔
538
                        Some(AnyCalendarKind::Dangi) => "윤",
×
539
                        _ => "(leap)",
×
540
                    };
541
                    w.write_str(leap_str)?;
30✔
542
                    w.write_str(symbol)?;
30✔
543
                    Ok(())
30✔
544
                }
30✔
545
                #[cfg(feature = "experimental")]
546
                Ok(MonthPlaceholderValue::Numeric) => {
547
                    try_write_number(w, fdf, formattable_month.ordinal.into(), l)?
×
548
                }
×
549
                #[cfg(feature = "experimental")]
550
                Ok(MonthPlaceholderValue::NumericPattern(substitution_pattern)) => {
×
UNCOV
551
                    w.write_str(substitution_pattern.get_prefix())?;
×
552
                    let r = try_write_number(w, fdf, formattable_month.ordinal.into(), l)?;
×
553
                    w.write_str(substitution_pattern.get_suffix())?;
×
554
                    r
×
555
                }
×
556
            },
557
        },
558
        (FieldSymbol::Week(week), l) => match week {
213✔
559
            Week::WeekOfYear => match week_data
366✔
560
                .ok_or(DateTimeWriteError::MissingWeekCalculator)
183✔
561
                .and_then(|w| {
366✔
562
                    datetime
183✔
563
                        .week_of_year(w)
564
                        .map_err(DateTimeWriteError::MissingInputField)
565
                }) {
183✔
UNCOV
566
                Err(e) => {
×
UNCOV
567
                    write_value_missing(w, field)?;
×
UNCOV
568
                    Err(e)
×
UNCOV
569
                }
×
570
                Ok((_, week_of_year)) => try_write_number(w, fdf, week_of_year.0.into(), l)?,
183✔
571
            },
572
            Week::WeekOfMonth => match week_data
60✔
573
                .ok_or(DateTimeWriteError::MissingWeekCalculator)
30✔
574
                .and_then(|w| {
60✔
575
                    datetime
30✔
576
                        .week_of_month(w)
577
                        .map_err(DateTimeWriteError::MissingInputField)
578
                }) {
30✔
UNCOV
579
                Err(e) => {
×
UNCOV
580
                    write_value_missing(w, field)?;
×
UNCOV
581
                    Err(e)
×
582
                }
×
583
                Ok(week_of_month) => try_write_number(w, fdf, week_of_month.0.into(), l)?,
30✔
584
            },
585
        },
586
        (FieldSymbol::Weekday(weekday), l) => match datetime.iso_weekday() {
1,072✔
587
            None => {
588
                write_value_missing(w, field)?;
4✔
589
                Err(DateTimeWriteError::MissingInputField("iso_weekday"))
4✔
590
            }
591
            Some(wd) => match date_symbols
3,204✔
592
                .ok_or(DateTimeWriteError::MissingDateSymbols)
1,068✔
593
                .and_then(|ds| {
2,136✔
594
                    ds.get_symbol_for_weekday(weekday, l, wd)
2,136✔
595
                        .map_err(|e| match e {
1,072✔
596
                            GetSymbolForWeekdayError::Missing => {
UNCOV
597
                                DateTimeWriteError::MissingWeekdaySymbol(wd)
×
598
                            }
599
                            #[cfg(feature = "experimental")]
600
                            GetSymbolForWeekdayError::MissingNames(f) => {
4✔
601
                                DateTimeWriteError::MissingNames(f)
4✔
602
                            }
4✔
603
                        })
4✔
604
                }) {
1,068✔
605
                Err(e) => {
4✔
606
                    w.with_part(Part::ERROR, |w| {
8✔
607
                        w.write_str(match wd {
8✔
608
                            IsoWeekday::Monday => "mon",
4✔
UNCOV
609
                            IsoWeekday::Tuesday => "tue",
×
UNCOV
610
                            IsoWeekday::Wednesday => "wed",
×
UNCOV
611
                            IsoWeekday::Thursday => "thu",
×
UNCOV
612
                            IsoWeekday::Friday => "fri",
×
UNCOV
613
                            IsoWeekday::Saturday => "sat",
×
UNCOV
614
                            IsoWeekday::Sunday => "sun",
×
615
                        })
616
                    })?;
4✔
617
                    Err(e)
4✔
618
                }
4✔
619
                Ok(s) => Ok(w.write_str(s)?),
1,064✔
620
            },
621
        },
622
        (FieldSymbol::Day(fields::Day::DayOfMonth), l) => match datetime.day_of_month() {
2,246✔
623
            None => {
624
                write_value_missing(w, field)?;
4✔
625
                Err(DateTimeWriteError::MissingInputField("day_of_month"))
4✔
626
            }
627
            Some(d) => try_write_number(w, fdf, d.0.into(), l)?,
2,242✔
628
        },
629
        (FieldSymbol::Day(fields::Day::DayOfWeekInMonth), l) => {
15✔
630
            match datetime.day_of_month().map(DayOfWeekInMonth::from) {
15✔
631
                None => {
UNCOV
632
                    write_value_missing(w, field)?;
×
633
                    Err(DateTimeWriteError::MissingInputField("day_of_month"))
×
634
                }
635
                Some(d) => try_write_number(w, fdf, d.0.into(), l)?,
15✔
636
            }
637
        }
638
        (FieldSymbol::Hour(hour), l) => match datetime.hour() {
2,143✔
639
            None => {
640
                write_value_missing(w, field)?;
4✔
641
                Err(DateTimeWriteError::MissingInputField("hour"))
4✔
642
            }
643
            Some(h) => {
2,139✔
644
                let h = usize::from(h) as isize;
2,139✔
645
                let h = match hour {
2,139✔
646
                    fields::Hour::H11 => h % 12,
85✔
647
                    fields::Hour::H12 => {
648
                        let v = h % 12;
881✔
649
                        if v == 0 {
881✔
650
                            12
424✔
651
                        } else {
652
                            v
457✔
653
                        }
654
                    }
655
                    fields::Hour::H23 => h,
1,088✔
656
                    fields::Hour::H24 => {
657
                        if h == 0 {
85✔
658
                            24
85✔
659
                        } else {
UNCOV
660
                            h
×
661
                        }
662
                    }
663
                };
664
                try_write_number(w, fdf, h.into(), l)?
2,139✔
665
            }
2,139✔
666
        },
667
        (FieldSymbol::Minute, l) => match datetime.minute() {
2,010✔
668
            None => {
669
                write_value_missing(w, field)?;
4✔
670
                Err(DateTimeWriteError::MissingInputField("minute"))
4✔
671
            }
672
            Some(iso_minute) => try_write_number(w, fdf, usize::from(iso_minute).into(), l)?,
2,006✔
673
        },
674
        (FieldSymbol::Second(Second::Second), l) => match (datetime.second(), iter.peek()) {
1,325✔
675
            (
676
                None,
677
                Some(&PatternItem::Field(
678
                    next_field @ Field {
4✔
679
                        symbol: FieldSymbol::Second(Second::FractionalSecond),
680
                        ..
681
                    },
682
                )),
683
            ) => {
684
                iter.next(); // Advance over nanosecond symbol
4✔
685
                write_value_missing(w, field)?;
4✔
686
                // Write error value for nanos even if we have them
687
                write_value_missing(w, next_field)?;
4✔
688
                Err(DateTimeWriteError::MissingInputField("second"))
4✔
689
            }
4✔
690
            (None, _) => {
691
                write_value_missing(w, field)?;
×
UNCOV
692
                Err(DateTimeWriteError::MissingInputField("second"))
×
693
            }
694
            (
695
                Some(second),
49✔
696
                Some(&PatternItem::Field(
697
                    next_field @ Field {
49✔
698
                        symbol: FieldSymbol::Second(Second::FractionalSecond),
699
                        length,
49✔
700
                    },
701
                )),
702
            ) => {
703
                iter.next(); // Advance over nanosecond symbol
49✔
704
                let r = datetime
147✔
705
                    .nanosecond()
706
                    .ok_or(DateTimeWriteError::MissingInputField("nanosecond"))
49✔
707
                    .and_then(|ns| {
98✔
708
                        // We only support fixed field length for fractional seconds.
709
                        let FieldLength::Fixed(p) = length else {
49✔
710
                            return Err(DateTimeWriteError::UnsupportedField(next_field));
×
711
                        };
712
                        Ok((ns, p))
49✔
713
                    });
49✔
714
                match r {
49✔
UNCOV
715
                    Err(e) => {
×
716
                        let seconds_result =
UNCOV
717
                            try_write_number(w, fdf, usize::from(second).into(), l)?;
×
UNCOV
718
                        write_value_missing(w, next_field)?;
×
719
                        // Return the earlier error
UNCOV
720
                        seconds_result.and(Err(e))
×
721
                    }
722
                    Ok((ns, p)) => {
49✔
723
                        let mut s = FixedDecimal::from(usize::from(second));
49✔
724
                        let _infallible = s.concatenate_end(
49✔
725
                            FixedDecimal::from(usize::from(ns)).multiplied_pow10(-9),
49✔
726
                        );
727
                        debug_assert!(_infallible.is_ok());
49✔
728
                        s.pad_end(-(p as i16));
49✔
729
                        try_write_number(w, fdf, s, l)?
49✔
730
                    }
49✔
731
                }
732
            }
733
            (Some(second), _) => try_write_number(w, fdf, usize::from(second).into(), l)?,
1,272✔
734
        },
735
        (FieldSymbol::Second(Second::FractionalSecond), _) => {
736
            // Fractional second not following second
UNCOV
737
            write_value_missing(w, field)?;
×
UNCOV
738
            Err(DateTimeWriteError::UnsupportedField(field))
×
739
        }
740
        (FieldSymbol::DayPeriod(period), l) => match datetime.hour() {
1,560✔
741
            None => {
742
                write_value_missing(w, field)?;
4✔
743
                Err(DateTimeWriteError::MissingInputField("hour"))
4✔
744
            }
745
            Some(hour) => {
1,556✔
746
                match time_symbols
3,112✔
747
                    .ok_or(DateTimeWriteError::MissingTimeSymbols)
1,556✔
748
                    .and_then(|ts| {
3,112✔
749
                        ts.get_symbol_for_day_period(
1,556✔
750
                            period,
1,556✔
751
                            l,
1,556✔
752
                            hour,
1,556✔
753
                            pattern_metadata.time_granularity().is_top_of_hour(
3,112✔
754
                                datetime.minute().map(u8::from).unwrap_or(0),
1,556✔
755
                                datetime.second().map(u8::from).unwrap_or(0),
1,556✔
756
                                datetime.nanosecond().map(u32::from).unwrap_or(0),
1,556✔
757
                            ),
758
                        )
759
                        .map_err(|e| match e {
4✔
760
                            #[cfg(feature = "experimental")]
761
                            GetSymbolForDayPeriodError::MissingNames(f) => {
4✔
762
                                DateTimeWriteError::MissingNames(f)
4✔
763
                            }
764
                        })
4✔
765
                    }) {
1,556✔
766
                    Err(e) => {
4✔
767
                        w.with_part(Part::ERROR, |w| {
8✔
768
                            w.write_str(if usize::from(hour) < 12 { "AM" } else { "PM" })
4✔
769
                        })?;
4✔
770
                        Err(e)
4✔
771
                    }
4✔
772
                    Ok(s) => Ok(w.write_str(s)?),
1,552✔
773
                }
774
            }
775
        },
776
        (FieldSymbol::TimeZone(TimeZone::LowerV), _l) => match datetime.time_zone() {
12✔
777
            None => {
UNCOV
778
                write_value_missing(w, field)?;
×
UNCOV
779
                Err(DateTimeWriteError::MissingInputField("time_zone"))
×
780
            }
781
            Some(CustomTimeZone {
782
                #[cfg(feature = "experimental")]
783
                gmt_offset,
12✔
784
                metazone_id: Some(metazone_id),
12✔
785
                time_zone_id,
12✔
786
                ..
787
            }) => match zone_symbols
24✔
788
                .ok_or(GetSymbolForTimeZoneError::MissingNames(field))
12✔
789
                .and_then(|zs| zs.get_generic_short_for_zone(metazone_id, time_zone_id))
24✔
790
            {
791
                Err(e) => match e {
4✔
792
                    GetSymbolForTimeZoneError::TypeTooNarrow => {
UNCOV
793
                        write_time_zone_missing(w)?;
×
UNCOV
794
                        Err(DateTimeWriteError::MissingNames(field))
×
795
                    }
796
                    #[cfg(feature = "experimental")]
797
                    GetSymbolForTimeZoneError::Missing => {
798
                        try_write_time_zone_gmt(w, gmt_offset, zone_symbols, true)?
4✔
799
                            .map_err(|_| DateTimeWriteError::MissingNames(field))
4✔
800
                    }
UNCOV
801
                    GetSymbolForTimeZoneError::MissingNames(f) => {
×
UNCOV
802
                        write_time_zone_missing(w)?;
×
UNCOV
803
                        Err(DateTimeWriteError::MissingNames(f))
×
UNCOV
804
                    }
×
805
                },
806
                Ok(s) => Ok(w.write_str(s)?),
8✔
807
            },
UNCOV
808
            Some(CustomTimeZone { gmt_offset, .. }) => {
×
809
                // Required time zone fields not present in input
UNCOV
810
                try_write_time_zone_gmt(w, gmt_offset, zone_symbols, false)?
×
UNCOV
811
                    .map_err(|_| DateTimeWriteError::MissingInputField("metazone_id"))
×
812
            }
813
        },
814
        (
815
            FieldSymbol::TimeZone(_)
816
            | FieldSymbol::Day(_)
817
            | FieldSymbol::Second(Second::Millisecond),
818
            _,
819
        ) => {
820
            w.with_part(Part::ERROR, |w| {
16,924✔
UNCOV
821
                w.write_str("{unsupported:")?;
×
UNCOV
822
                w.write_char(char::from(field.symbol))?;
×
UNCOV
823
                w.write_str("}")
×
UNCOV
824
            })?;
×
UNCOV
825
            Err(DateTimeWriteError::UnsupportedField(field))
×
826
        }
827
    })
828
}
16,924✔
829

830
/// What data is required to format a given pattern.
831
#[derive(Default)]
1,502✔
832
pub struct RequiredData {
833
    // DateSymbolsV1 is required.
834
    pub date_symbols_data: bool,
751✔
835
    // TimeSymbolsV1 is required.
836
    pub time_symbols_data: bool,
751✔
837
    // WeekDataV1 is required.
838
    pub week_data: bool,
751✔
839
}
840

841
impl RequiredData {
842
    // Checks if formatting `pattern` would require us to load data & if so adds
843
    // them to this struct. Returns true if requirements are saturated and would
844
    // not change by any further calls.
845
    // Keep it in sync with the `write_field` use of symbols.
846
    fn add_requirements_from_pattern(
758✔
847
        &mut self,
848
        pattern: &Pattern,
849
        supports_time_zones: bool,
850
    ) -> Result<bool, Field> {
851
        let fields = pattern.items.iter().filter_map(|p| match p {
5,849✔
852
            PatternItem::Field(field) => Some(field),
2,572✔
853
            _ => None,
2,519✔
854
        });
5,091✔
855

856
        for field in fields {
3,330✔
857
            if !self.date_symbols_data {
3,943✔
858
                self.date_symbols_data = match field.symbol {
2,740✔
859
                    FieldSymbol::Era => true,
60✔
860
                    FieldSymbol::Month(_) => {
861
                        !matches!(field.length, FieldLength::One | FieldLength::TwoDigit)
223✔
862
                    }
863
                    FieldSymbol::Weekday(_) => true,
122✔
864
                    _ => false,
965✔
865
                }
866
            }
867
            if !self.time_symbols_data {
2,573✔
868
                self.time_symbols_data = matches!(field.symbol, FieldSymbol::DayPeriod(_));
2,541✔
869
            }
870

871
            if !self.week_data {
5,079✔
872
                self.week_data = matches!(
2,506✔
873
                    field.symbol,
2,506✔
874
                    FieldSymbol::Year(Year::WeekOf) | FieldSymbol::Week(_)
875
                )
876
            }
877

878
            if supports_time_zones {
2,573✔
879
                if self.date_symbols_data && self.time_symbols_data && self.week_data {
346✔
880
                    // If we support time zones, and require everything else, we
881
                    // know all we need to return already.
UNCOV
882
                    return Ok(true);
×
883
                }
884
            } else if matches!(field.symbol, FieldSymbol::TimeZone(_)) {
2,227✔
885
                // If we don't support time zones, and encountered a time zone
886
                // field, error out.
887
                return Err(field);
1✔
888
            }
889
        }
890

891
        Ok(false)
757✔
892
    }
758✔
893
}
894

895
// Determines what optional data needs to be loaded to format `patterns`.
896
pub fn analyze_patterns(
751✔
897
    patterns: &PatternPlurals,
898
    supports_time_zones: bool,
899
) -> Result<RequiredData, Field> {
900
    let mut required = RequiredData::default();
751✔
901
    for pattern in patterns.patterns_iter() {
1,508✔
902
        if required.add_requirements_from_pattern(pattern, supports_time_zones)? {
758✔
903
            // We can bail early if everything is required & we don't need to
904
            // validate the absence of TimeZones.
905
            break;
906
        }
907
    }
908
    Ok(required)
750✔
909
}
751✔
910

911
#[cfg(test)]
912
#[allow(unused_imports)]
913
#[cfg(feature = "compiled_data")]
914
mod tests {
915
    use super::*;
916
    use crate::{neo_marker::NeoAutoDateMarker, neo_skeleton::NeoSkeletonLength, pattern::runtime};
917
    use icu_decimal::options::{FixedDecimalFormatterOptions, GroupingStrategy};
918
    use tinystr::tinystr;
919

920
    #[test]
921
    fn test_mixed_calendar_eras() {
2✔
922
        use crate::neo::NeoFormatter;
923
        use crate::options::length;
924
        use icu_calendar::japanese::JapaneseExtended;
925
        use icu_calendar::Date;
926

927
        let locale = "en-u-ca-japanese".parse().unwrap();
1✔
928
        let dtf = NeoFormatter::<NeoAutoDateMarker>::try_new(&locale, NeoSkeletonLength::Medium)
1✔
929
            .expect("DateTimeFormat construction succeeds");
930

931
        let date = Date::try_new_gregorian_date(1800, 9, 1).expect("Failed to construct Date.");
3✔
932
        let date = date
1✔
933
            .to_calendar(JapaneseExtended::new())
1✔
934
            .into_japanese_date()
935
            .to_any();
3✔
936

937
        writeable::assert_try_writeable_eq!(
1✔
938
            dtf.strict_format(&date).unwrap(),
1✔
939
            "Sep 1, 12 kansei-1789",
940
            Err(DateTimeWriteError::MissingEraSymbol(Era(tinystr!(
1✔
941
                16,
942
                "kansei-1789"
943
            ))))
944
        );
945
    }
2✔
946

947
    #[test]
948
    #[cfg(feature = "serde")]
949
    fn test_basic() {
2✔
950
        use crate::provider::calendar::{GregorianDateSymbolsV1Marker, TimeSymbolsV1Marker};
951
        use icu_calendar::DateTime;
952
        use icu_provider::prelude::*;
953

954
        let locale = "en-u-ca-gregory".parse().unwrap();
1✔
955
        let req = DataRequest {
1✔
956
            locale: &locale,
957
            ..Default::default()
1✔
958
        };
959
        let date_data: DataPayload<GregorianDateSymbolsV1Marker> = crate::provider::Baked
1✔
960
            .load(req)
961
            .unwrap()
962
            .take_payload()
963
            .unwrap();
964
        let time_data: DataPayload<TimeSymbolsV1Marker> = crate::provider::Baked
1✔
965
            .load(req)
966
            .unwrap()
967
            .take_payload()
968
            .unwrap();
969
        let pattern: runtime::Pattern = "MMM".parse().unwrap();
1✔
970
        let datetime = DateTime::try_new_gregorian_datetime(2020, 8, 1, 12, 34, 28).unwrap();
1✔
971
        let fixed_decimal_format =
972
            FixedDecimalFormatter::try_new(&locale, Default::default()).unwrap();
1✔
973

974
        let mut sink = String::new();
1✔
975
        try_write_pattern(
1✔
976
            pattern.as_borrowed(),
1✔
977
            &ExtractedDateTimeInput::extract_from(&datetime),
1✔
978
            Some(date_data.get()),
1✔
979
            Some(time_data.get()),
1✔
980
            None::<()>.as_ref(),
1✔
981
            None,
1✔
982
            Some(&fixed_decimal_format),
1✔
983
            &mut writeable::adapters::CoreWriteAsPartsWrite(&mut sink),
1✔
984
        )
985
        .unwrap()
986
        .unwrap();
987
        println!("{sink}");
1✔
988
    }
2✔
989

990
    #[test]
991
    fn test_format_number() {
42✔
992
        let values = &[2, 20, 201, 2017, 20173];
41✔
993
        let samples = &[
41✔
994
            (FieldLength::One, ["2", "20", "201", "2017", "20173"]),
995
            (FieldLength::TwoDigit, ["02", "20", "01", "17", "73"]),
996
            (
997
                FieldLength::Abbreviated,
998
                ["002", "020", "201", "2017", "20173"],
999
            ),
1000
            (FieldLength::Wide, ["0002", "0020", "0201", "2017", "20173"]),
1001
        ];
1002

1003
        let mut fixed_decimal_format_options = FixedDecimalFormatterOptions::default();
41✔
1004
        fixed_decimal_format_options.grouping_strategy = GroupingStrategy::Never;
41✔
1005
        let fixed_decimal_format = FixedDecimalFormatter::try_new(
41✔
1006
            &icu_locale_core::locale!("en").into(),
41✔
1007
            fixed_decimal_format_options,
41✔
1008
        )
1009
        .unwrap();
41✔
1010

1011
        for (length, expected) in samples {
5✔
1012
            for (value, expected) in values.iter().zip(expected) {
4✔
1013
                let mut s = String::new();
20✔
1014
                try_write_number(
20✔
1015
                    &mut writeable::adapters::CoreWriteAsPartsWrite(&mut s),
20✔
1016
                    Some(&fixed_decimal_format),
20✔
1017
                    FixedDecimal::from(*value),
20✔
1018
                    *length,
20✔
1019
                )
1020
                .unwrap()
1021
                .unwrap();
1022
                assert_eq!(s, *expected);
20✔
1023
            }
20✔
1024
        }
1025
    }
2✔
1026
}
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