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}