icu_datetime/
unchecked.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//! Datetime formatting without static checking of invariants.
6
7use crate::format::datetime::try_write_pattern_items;
8pub use crate::format::DateTimeInputUnchecked;
9use crate::pattern::*;
10use crate::raw::neo::*;
11use crate::scaffold::*;
12use crate::DateTimeFormatter;
13use crate::FixedCalendarDateTimeFormatter;
14use core::fmt;
15use icu_calendar::types::MonthCode;
16use tinystr::TinyStr16;
17use writeable::TryWriteable;
18
19#[cfg(doc)]
20use crate::fieldsets::enums::CompositeFieldSet;
21#[cfg(doc)]
22use crate::FormattedDateTime;
23#[cfg(doc)]
24use icu_calendar::types::CyclicYear;
25#[cfg(doc)]
26use icu_decimal::DecimalFormatter;
27
28impl<C: CldrCalendar, FSet: DateTimeNamesMarker> FixedCalendarDateTimeFormatter<C, FSet> {
29    /// Formats a datetime without enforcing either the field set or the calendar.
30    ///
31    /// This function is useful when the caller knows something about the field set that the
32    /// type system is unaware of. For example, if the formatter is represented with a
33    /// [dynamic field set](crate::fieldsets::enums), the caller may be able to provide a
34    /// narrower type for formatting.
35    ///
36    /// ❗ The caller must ensure that:
37    ///
38    /// 1. The calendar of the input matches the calendar of the formatter
39    /// 2. The fields of the input are a superset of the fields of the formatter
40    ///
41    /// Returns a [`FormattedDateTimeUnchecked`] to surface errors when they occur,
42    /// but not every invariant will result in an error. Use with caution!
43    ///
44    /// # Examples
45    ///
46    /// In the following example, we know that the formatter's field set is [`YMD`], but the
47    /// type system thinks we are a [`CompositeFieldSet`], which requires a [`ZonedDateTime`]
48    /// as input. However, since [`Date`] contains all the fields required by [`YMD`], we can
49    /// successfully pass it into [`format_unchecked`].
50    ///
51    /// ```
52    /// use icu::calendar::cal::Buddhist;
53    /// use icu::datetime::fieldsets::enums::CompositeFieldSet;
54    /// use icu::datetime::fieldsets::{T, YMD};
55    /// use icu::datetime::input::{Date, Time};
56    /// use icu::datetime::unchecked::DateTimeInputUnchecked;
57    /// use icu::datetime::unchecked::FormattedDateTimeUncheckedError;
58    /// use icu::datetime::unchecked::MissingInputFieldKind;
59    /// use icu::datetime::FixedCalendarDateTimeFormatter;
60    /// use icu::locale::locale;
61    /// use writeable::assert_try_writeable_eq;
62    ///
63    /// let formatter = FixedCalendarDateTimeFormatter::<Buddhist, _>::try_new(
64    ///     locale!("th").into(),
65    ///     YMD::long(),
66    /// )
67    /// .unwrap()
68    /// .cast_into_fset::<CompositeFieldSet>();
69    ///
70    /// // Create a date and convert it to the correct calendar:
71    /// let mut input = DateTimeInputUnchecked::default();
72    /// let date = Date::try_new_iso(2025, 3, 7).unwrap().to_calendar(Buddhist);
73    ///
74    /// // Safe because the calendar matches the formatter:
75    /// input.set_date_fields_unchecked(date);
76    ///
77    /// // Safe because YMD needs only date fields, which are in the input:
78    /// let result = formatter.format_unchecked(input);
79    ///
80    /// assert_try_writeable_eq!(result, "7 มีนาคม 2568");
81    ///
82    /// // If we don't give all needed fields, we will get an error!
83    /// let mut input = DateTimeInputUnchecked::default();
84    /// let result = formatter.format_unchecked(input);
85    /// assert_try_writeable_eq!(
86    ///     result,
87    ///     "{d} {M} {G} {y}",
88    ///     Err(FormattedDateTimeUncheckedError::MissingInputField(
89    ///         MissingInputFieldKind::DayOfMonth
90    ///     ))
91    /// );
92    /// ```
93    ///
94    /// [`Date`]: crate::input::Date
95    /// [`ZonedDateTime`]: crate::input::ZonedDateTime
96    /// [`YMD`]: crate::fieldsets::YMD
97    /// [`format_unchecked`]: Self::format_unchecked
98    pub fn format_unchecked(&self, datetime: DateTimeInputUnchecked) -> FormattedDateTimeUnchecked {
99        FormattedDateTimeUnchecked {
100            pattern: self.selection.select(&datetime),
101            input: datetime,
102            names: self.names.as_borrowed(),
103        }
104    }
105}
106
107impl<FSet: DateTimeNamesMarker> DateTimeFormatter<FSet> {
108    /// Formats a datetime without enforcing either the field set or the calendar.
109    ///
110    /// This function is useful when the caller knows something about the field set that the
111    /// type system is unaware of. For example, if the formatter is represented with a
112    /// [dynamic field set](crate::fieldsets::enums), the caller may be able to provide a
113    /// narrower type for formatting.
114    ///
115    /// ❗ The caller must ensure that:
116    ///
117    /// 1. The calendar of the input matches the calendar of the formatter
118    /// 2. The fields of the input are a superset of the fields of the formatter
119    ///
120    /// Returns a [`FormattedDateTimeUnchecked`] to surface errors when they occur,
121    /// but not every invariant will result in an error. Use with caution!
122    ///
123    /// # Examples
124    ///
125    /// In the following example, we know that the formatter's field set is [`YMD`], but the
126    /// type system thinks we are a [`CompositeFieldSet`], which requires a [`ZonedDateTime`]
127    /// as input. However, since [`Date`] contains all the fields required by [`YMD`], we can
128    /// successfully pass it into [`format_unchecked`].
129    ///
130    /// ```
131    /// use icu::datetime::fieldsets::enums::CompositeFieldSet;
132    /// use icu::datetime::fieldsets::{T, YMD};
133    /// use icu::datetime::input::{Date, Time};
134    /// use icu::datetime::unchecked::DateTimeInputUnchecked;
135    /// use icu::datetime::unchecked::FormattedDateTimeUncheckedError;
136    /// use icu::datetime::unchecked::MissingInputFieldKind;
137    /// use icu::datetime::DateTimeFormatter;
138    /// use icu::locale::locale;
139    /// use writeable::assert_try_writeable_eq;
140    ///
141    /// let formatter =
142    ///     DateTimeFormatter::try_new(locale!("th-TH").into(), YMD::long())
143    ///         .unwrap()
144    ///         .cast_into_fset::<CompositeFieldSet>();
145    ///
146    /// // Create a date and convert it to the correct calendar:
147    /// let mut input = DateTimeInputUnchecked::default();
148    /// let date = Date::try_new_iso(2025, 3, 7)
149    ///     .unwrap()
150    ///     .to_calendar(formatter.calendar());
151    ///
152    /// // Safe because the calendar matches the formatter:
153    /// input.set_date_fields_unchecked(date);
154    ///
155    /// // Safe because YMD needs only date fields, which are in the input:
156    /// let result = formatter.format_unchecked(input);
157    ///
158    /// assert_try_writeable_eq!(result, "7 มีนาคม 2568");
159    ///
160    /// // If we don't give all needed fields, we will get an error!
161    /// let mut input = DateTimeInputUnchecked::default();
162    /// let result = formatter.format_unchecked(input);
163    /// assert_try_writeable_eq!(
164    ///     result,
165    ///     "{d} {M} {G} {y}",
166    ///     Err(FormattedDateTimeUncheckedError::MissingInputField(
167    ///         MissingInputFieldKind::DayOfMonth
168    ///     ))
169    /// );
170    /// ```
171    ///
172    /// [`Date`]: crate::input::Date
173    /// [`ZonedDateTime`]: crate::input::ZonedDateTime
174    /// [`YMD`]: crate::fieldsets::YMD
175    /// [`format_unchecked`]: Self::format_unchecked
176    pub fn format_unchecked(&self, datetime: DateTimeInputUnchecked) -> FormattedDateTimeUnchecked {
177        FormattedDateTimeUnchecked {
178            pattern: self.selection.select(&datetime),
179            input: datetime,
180            names: self.names.as_borrowed(),
181        }
182    }
183}
184
185/// An intermediate type during a datetime formatting operation with dynamic input.
186///
187/// Unlike [`FormattedDateTime`], converting this to a string could fail.
188///
189/// Not intended to be stored: convert to a string first.
190#[derive(Debug)]
191pub struct FormattedDateTimeUnchecked<'a> {
192    pattern: DateTimeZonePatternDataBorrowed<'a>,
193    input: DateTimeInputUnchecked,
194    names: RawDateTimeNamesBorrowed<'a>,
195}
196
197impl TryWriteable for FormattedDateTimeUnchecked<'_> {
198    type Error = FormattedDateTimeUncheckedError;
199    fn try_write_to_parts<S: writeable::PartsWrite + ?Sized>(
200        &self,
201        sink: &mut S,
202    ) -> Result<Result<(), Self::Error>, fmt::Error> {
203        let err = match try_write_pattern_items(
204            self.pattern.metadata(),
205            self.pattern.iter_items(),
206            &self.input,
207            &self.names,
208            self.names.decimal_formatter,
209            sink,
210        ) {
211            Ok(Ok(())) => return Ok(Ok(())),
212            Err(fmt::Error) => return Err(fmt::Error),
213            Ok(Err(err)) => err,
214        };
215        Ok(Err(match err {
216            FormattedDateTimePatternError::InvalidMonthCode(month_code) => {
217                Self::Error::InvalidMonthCode(month_code)
218            }
219            FormattedDateTimePatternError::InvalidEra(tiny_ascii_str) => {
220                Self::Error::InvalidEra(tiny_ascii_str)
221            }
222            FormattedDateTimePatternError::InvalidCyclicYear { value, max } => {
223                Self::Error::InvalidCyclicYear { value, max }
224            }
225            FormattedDateTimePatternError::DecimalFormatterNotLoaded => {
226                Self::Error::DecimalFormatterNotLoaded
227            }
228            FormattedDateTimePatternError::NamesNotLoaded(error_field) => {
229                Self::Error::NamesNotLoaded(error_field)
230            }
231            FormattedDateTimePatternError::MissingInputField(name) => {
232                Self::Error::MissingInputField(name)
233            }
234            FormattedDateTimePatternError::UnsupportedLength(error_field) => {
235                Self::Error::UnsupportedLength(error_field)
236            }
237            FormattedDateTimePatternError::UnsupportedField(error_field) => {
238                Self::Error::UnsupportedField(error_field)
239            }
240        }))
241    }
242
243    // TODO(#489): Implement writeable_length_hint
244}
245
246impl FormattedDateTimeUnchecked<'_> {
247    /// Gets the pattern used in this formatted value.
248    ///
249    /// From the pattern, one can check the properties of the included components, such as
250    /// the hour cycle being used for formatting. See [`DateTimePattern`].
251    pub fn pattern(&self) -> DateTimePattern {
252        self.pattern.to_pattern()
253    }
254}
255
256/// The kind of a missing datetime input field.
257#[non_exhaustive]
258#[derive(Debug, PartialEq, Copy, Clone, displaydoc::Display)]
259pub enum MissingInputFieldKind {
260    /// Day of month
261    DayOfMonth,
262    /// Day of year
263    DayOfYear,
264    /// Hour
265    Hour,
266    /// Minute
267    Minute,
268    /// Month
269    Month,
270    /// Second
271    Second,
272    /// Subsecond
273    Subsecond,
274    /// Weekday
275    Weekday,
276    /// Year
277    Year,
278    /// Cyclic year
279    YearCyclic,
280    /// Era year
281    YearEra,
282    /// Time zone identifier
283    TimeZoneId,
284    /// Time zone name timestamp
285    TimeZoneNameTimestamp,
286    /// Time zone variant
287    TimeZoneVariant,
288}
289
290#[non_exhaustive]
291#[derive(Debug, PartialEq, Copy, Clone, displaydoc::Display)]
292/// Error for the [`TryWriteable`] implementation of [`FormattedDateTimeUnchecked`].
293///
294/// There are 3 general conditions for these errors to occur:
295///
296/// 1. Invariants of **unchecked functions** are not upheld
297/// 2. Invariants of **locale data** are not upheld
298/// 3. Invariants of **trait impls** are not upheld (including [scaffolding traits])
299///
300/// It is not always possible to distinguish the source of the errors. Each variant is documented
301/// with rules of thumb for when they might occur.
302///
303/// [unstable scaffolding traits]: crate::scaffold
304pub enum FormattedDateTimeUncheckedError {
305    /// The [`MonthCode`] of the input is not valid for this calendar.
306    ///
307    /// Error conditions:
308    ///
309    /// - **Unchecked functions:** for example, wrong calendar passed to [`set_date_fields_unchecked`]
310    /// - **Locale data:** for example, datetime names don't match the formatter's calendar
311    /// - **Trait impls:** for example, the date returns fields for the wrong calendar
312    ///
313    /// The output will contain the raw [`MonthCode`] as a fallback value.
314    ///
315    /// [`set_date_fields_unchecked`]: DateTimeInputUnchecked::set_date_fields_unchecked
316    #[displaydoc("Invalid month {0:?}")]
317    InvalidMonthCode(MonthCode),
318    /// The era code of the input is not valid for this calendar.
319    ///
320    /// Same error conditions as [`FormattedDateTimeUncheckedError::InvalidMonthCode`].
321    ///
322    /// The output will contain the era code as the fallback.
323    #[displaydoc("Invalid era {0:?}")]
324    InvalidEra(TinyStr16),
325    /// The [`CyclicYear::year`] of the input is not valid for this calendar.
326    ///
327    /// Same error conditions as [`FormattedDateTimeUncheckedError::InvalidMonthCode`].
328    ///
329    /// The output will contain [`CyclicYear::related_iso`] as a fallback value.
330    #[displaydoc("Invalid cyclic year {value} (maximum {max})")]
331    InvalidCyclicYear {
332        /// Value
333        value: u8,
334        /// Max
335        max: u8,
336    },
337
338    /// The localized names for a field have not been loaded.
339    ///
340    /// Error conditions:
341    ///
342    /// - **Locale data:** for example, year style patterns contain inconsistant field lengths
343    /// - **Trait impls:** for example, a custom field set does not include the correct names data
344    ///
345    /// The output will contain fallback values using field identifiers (such as `tue` for `Weekday::Tuesday`,
346    /// `M02` for month 2, etc.).
347    #[displaydoc("Names for {0:?} not loaded")]
348    NamesNotLoaded(ErrorField),
349    /// The [`DecimalFormatter`] has not been loaded.
350    ///
351    /// Same error conditions as [`FormattedDateTimeUncheckedError::NamesNotLoaded`].
352    ///
353    /// The output will contain fallback values using Latin numerals.
354    #[displaydoc("DecimalFormatter not loaded")]
355    DecimalFormatterNotLoaded,
356
357    /// An input field (such as "hour" or "month") is missing.
358    ///
359    /// Error conditions:
360    ///
361    /// - **Unchecked functions:** for example, insufficient fields in [`DateTimeInputUnchecked`]
362    /// - **Locale data:** for example, the pattern contains a field not in the fieldset
363    /// - **Trait impls:** for example, a custom field set does not require the correct fields
364    ///
365    /// The output will contain the string `{X}` instead, where `X` is the symbol for which the input is missing.
366    #[displaydoc("Incomplete input, missing value for {0:?}")]
367    MissingInputField(MissingInputFieldKind),
368
369    /// The pattern contains a field symbol for which formatting is unsupported.
370    ///
371    /// Error conditions:
372    ///
373    /// - **Locale data:** the pattern contains an unsupported field
374    ///
375    /// The output will contain the string `{unsupported:X}`, where `X` is the symbol of the unsupported field.
376    #[displaydoc("Unsupported field {0:?}")]
377    UnsupportedField(ErrorField),
378    /// The pattern contains a field that has a valid symbol but invalid length.
379    ///
380    /// Same error conditions as [`FormattedDateTimeUncheckedError::UnsupportedField`].
381    ///
382    /// The output will contain fallback values similar to [`FormattedDateTimeUncheckedError::NamesNotLoaded`].
383    #[displaydoc("Field length for {0:?} is invalid")]
384    UnsupportedLength(ErrorField),
385}