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}