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