icu_datetime/options/
mod.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//! Options types for date/time formatting.
6
7use icu_time::scaffold::IntoOption;
8
9/// The length of a formatted date/time string.
10///
11/// Length settings are always a hint, not a guarantee. For example, certain locales and
12/// calendar systems do not define numeric names, so spelled-out names could occur even if a
13/// short length was requested, and likewise with numeric names with a medium or long length.
14///
15/// # Examples
16///
17/// ```
18/// use icu::calendar::Gregorian;
19/// use icu::datetime::fieldsets::YMD;
20/// use icu::datetime::input::Date;
21/// use icu::datetime::FixedCalendarDateTimeFormatter;
22/// use icu::locale::locale;
23/// use writeable::assert_writeable_eq;
24///
25/// let short_formatter = FixedCalendarDateTimeFormatter::try_new(
26///     locale!("en-US").into(),
27///     YMD::short(),
28/// )
29/// .unwrap();
30///
31/// let medium_formatter = FixedCalendarDateTimeFormatter::try_new(
32///     locale!("en-US").into(),
33///     YMD::medium(),
34/// )
35/// .unwrap();
36///
37/// let long_formatter = FixedCalendarDateTimeFormatter::try_new(
38///     locale!("en-US").into(),
39///     YMD::long(),
40/// )
41/// .unwrap();
42///
43/// assert_writeable_eq!(
44///     short_formatter.format(&Date::try_new_gregorian(2000, 1, 1).unwrap()),
45///     "1/1/00"
46/// );
47///
48/// assert_writeable_eq!(
49///     medium_formatter.format(&Date::try_new_gregorian(2000, 1, 1).unwrap()),
50///     "Jan 1, 2000"
51/// );
52///
53/// assert_writeable_eq!(
54///     long_formatter.format(&Date::try_new_gregorian(2000, 1, 1).unwrap()),
55///     "January 1, 2000"
56/// );
57/// ```
58#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
59#[cfg_attr(
60    all(feature = "serde", feature = "experimental"),
61    derive(serde::Serialize, serde::Deserialize)
62)]
63#[cfg_attr(
64    all(feature = "serde", feature = "experimental"),
65    serde(rename_all = "lowercase")
66)]
67#[non_exhaustive]
68pub enum Length {
69    /// A long date; typically spelled-out, as in “January 1, 2000”.
70    Long,
71    /// A medium-sized date; typically abbreviated, as in “Jan. 1, 2000”.
72    ///
73    /// This is the default.
74    #[default]
75    Medium,
76    /// A short date; typically numeric, as in “1/1/2000”.
77    Short,
78}
79
80impl IntoOption<Length> for Length {
81    #[inline]
82    fn into_option(self) -> Option<Self> {
83        Some(self)
84    }
85}
86
87/// The alignment context of the formatted string.
88///
89/// By default, datetimes are formatted for a variable-width context. You can
90/// give a hint that the strings will be displayed in a column-like context,
91/// which will coerce numerics to be padded with zeros.
92///
93/// # Examples
94///
95/// ```
96/// use icu::calendar::Gregorian;
97/// use icu::datetime::fieldsets::YMD;
98/// use icu::datetime::input::Date;
99/// use icu::datetime::options::Alignment;
100/// use icu::datetime::FixedCalendarDateTimeFormatter;
101/// use icu::locale::locale;
102/// use writeable::assert_writeable_eq;
103///
104/// let plain_formatter =
105///     FixedCalendarDateTimeFormatter::<Gregorian, _>::try_new(
106///         locale!("en-US").into(),
107///         YMD::short(),
108///     )
109///     .unwrap();
110///
111/// let column_formatter =
112///     FixedCalendarDateTimeFormatter::<Gregorian, _>::try_new(
113///         locale!("en-US").into(),
114///         YMD::short().with_alignment(Alignment::Column),
115///     )
116///     .unwrap();
117///
118/// // By default, en-US does not pad the month and day with zeros.
119/// assert_writeable_eq!(
120///     plain_formatter.format(&Date::try_new_gregorian(2025, 1, 1).unwrap()),
121///     "1/1/25"
122/// );
123///
124/// // The column alignment option hints that they should be padded.
125/// assert_writeable_eq!(
126///     column_formatter.format(&Date::try_new_gregorian(2025, 1, 1).unwrap()),
127///     "01/01/25"
128/// );
129/// ```
130#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
131#[cfg_attr(
132    all(feature = "serde", feature = "experimental"),
133    derive(serde::Serialize, serde::Deserialize)
134)]
135#[cfg_attr(
136    all(feature = "serde", feature = "experimental"),
137    serde(rename_all = "lowercase")
138)]
139#[non_exhaustive]
140pub enum Alignment {
141    /// Align fields as the locale specifies them to be aligned.
142    ///
143    /// This is the default option.
144    #[default]
145    Auto,
146    /// Align fields as appropriate for a column layout. For example:
147    ///
148    /// | US Holiday   | Date       |
149    /// |--------------|------------|
150    /// | Memorial Day | 05/26/2025 |
151    /// | Labor Day    | 09/01/2025 |
152    /// | Veterans Day | 11/11/2025 |
153    ///
154    /// This option causes numeric fields to be padded when necessary. It does
155    /// not impact whether a numeric or spelled-out field is chosen.
156    Column,
157}
158
159impl IntoOption<Alignment> for Alignment {
160    #[inline]
161    fn into_option(self) -> Option<Self> {
162        Some(self)
163    }
164}
165
166/// A specification of how to render the year and the era.
167///
168/// The choices may grow over time; to follow along and offer feedback, see
169/// <https://github.com/unicode-org/icu4x/issues/6010>.
170///
171/// # Examples
172///
173/// ```
174/// use icu::calendar::Gregorian;
175/// use icu::datetime::fieldsets::YMD;
176/// use icu::datetime::input::Date;
177/// use icu::datetime::options::YearStyle;
178/// use icu::datetime::FixedCalendarDateTimeFormatter;
179/// use icu::locale::locale;
180/// use writeable::assert_writeable_eq;
181///
182/// let formatter = FixedCalendarDateTimeFormatter::<Gregorian, _>::try_new(
183///     locale!("en-US").into(),
184///     YMD::short().with_year_style(YearStyle::Auto),
185/// )
186/// .unwrap();
187///
188/// // Era displayed when needed for disambiguation,
189/// // such as years before year 0 and small year numbers:
190/// assert_writeable_eq!(
191///     formatter.format(&Date::try_new_gregorian(-1000, 1, 1).unwrap()),
192///     "1/1/1001 BC"
193/// );
194/// assert_writeable_eq!(
195///     formatter.format(&Date::try_new_gregorian(77, 1, 1).unwrap()),
196///     "1/1/77 AD"
197/// );
198/// // Era elided for modern years:
199/// assert_writeable_eq!(
200///     formatter.format(&Date::try_new_gregorian(1900, 1, 1).unwrap()),
201///     "1/1/1900"
202/// );
203/// // Era and century both elided for nearby years:
204/// assert_writeable_eq!(
205///     formatter.format(&Date::try_new_gregorian(2025, 1, 1).unwrap()),
206///     "1/1/25"
207/// );
208///
209/// let formatter = FixedCalendarDateTimeFormatter::<Gregorian, _>::try_new(
210///     locale!("en-US").into(),
211///     YMD::short().with_year_style(YearStyle::Full),
212/// )
213/// .unwrap();
214///
215/// // Era still displayed in cases with ambiguity:
216/// assert_writeable_eq!(
217///     formatter.format(&Date::try_new_gregorian(-1000, 1, 1).unwrap()),
218///     "1/1/1001 BC"
219/// );
220/// assert_writeable_eq!(
221///     formatter.format(&Date::try_new_gregorian(77, 1, 1).unwrap()),
222///     "1/1/77 AD"
223/// );
224/// // Era elided for modern years:
225/// assert_writeable_eq!(
226///     formatter.format(&Date::try_new_gregorian(1900, 1, 1).unwrap()),
227///     "1/1/1900"
228/// );
229/// // But now we always get a full-precision year:
230/// assert_writeable_eq!(
231///     formatter.format(&Date::try_new_gregorian(2025, 1, 1).unwrap()),
232///     "1/1/2025"
233/// );
234///
235/// let formatter = FixedCalendarDateTimeFormatter::<Gregorian, _>::try_new(
236///     locale!("en-US").into(),
237///     YMD::short().with_year_style(YearStyle::WithEra),
238/// )
239/// .unwrap();
240///
241/// // Era still displayed in cases with ambiguity:
242/// assert_writeable_eq!(
243///     formatter.format(&Date::try_new_gregorian(-1000, 1, 1).unwrap()),
244///     "1/1/1001 BC"
245/// );
246/// assert_writeable_eq!(
247///     formatter.format(&Date::try_new_gregorian(77, 1, 1).unwrap()),
248///     "1/1/77 AD"
249/// );
250/// // But now it is shown even on modern years:
251/// assert_writeable_eq!(
252///     formatter.format(&Date::try_new_gregorian(1900, 1, 1).unwrap()),
253///     "1/1/1900 AD"
254/// );
255/// assert_writeable_eq!(
256///     formatter.format(&Date::try_new_gregorian(2025, 1, 1).unwrap()),
257///     "1/1/2025 AD"
258/// );
259/// ```
260#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
261#[cfg_attr(
262    all(feature = "serde", feature = "experimental"),
263    derive(serde::Serialize, serde::Deserialize)
264)]
265#[cfg_attr(
266    all(feature = "serde", feature = "experimental"),
267    serde(rename_all = "camelCase")
268)]
269#[non_exhaustive]
270pub enum YearStyle {
271    /// Display the century and/or era when needed to disambiguate the year,
272    /// based on locale preferences.
273    ///
274    /// This is the default option.
275    ///
276    /// Examples:
277    ///
278    /// - `1000 BC`
279    /// - `77 AD`
280    /// - `1900`
281    /// - `24`
282    #[default]
283    Auto,
284    /// Always display the century, and display the era when needed to
285    /// disambiguate the year, based on locale preferences.
286    ///
287    /// Examples:
288    ///
289    /// - `1000 BC`
290    /// - `77 AD`
291    /// - `1900`
292    /// - `2024`
293    Full,
294    /// Always display the century and era.
295    ///
296    /// Examples:
297    ///
298    /// - `1000 BC`
299    /// - `77 AD`
300    /// - `1900 AD`
301    /// - `2024 AD`
302    WithEra,
303}
304
305impl IntoOption<YearStyle> for YearStyle {
306    #[inline]
307    fn into_option(self) -> Option<Self> {
308        Some(self)
309    }
310}
311
312/// A specification for how precisely to display the time of day.
313///
314/// The time can be displayed with hour, minute, or second precision, and
315/// zero-valued fields can be automatically hidden.
316///
317/// The examples in the discriminants are based on the following inputs and hour cycles:
318///
319/// 1. 11 o'clock with 12-hour time
320/// 2. 16:20 (4:20 pm) with 24-hour time
321/// 3. 7:15:01.85 with 24-hour time
322///
323/// Fractional second digits can be displayed with a fixed precision. If you would like
324/// additional options for fractional second digit display, please leave a comment in
325/// <https://github.com/unicode-org/icu4x/issues/6008>.
326///
327/// # Examples
328///
329/// Comparison of all time precision options:
330///
331/// ```
332/// use icu::datetime::input::Time;
333/// use icu::datetime::fieldsets::T;
334/// use icu::datetime::options::SubsecondDigits;
335/// use icu::datetime::options::TimePrecision;
336/// use icu::datetime::FixedCalendarDateTimeFormatter;
337/// use icu::locale::locale;
338/// use writeable::assert_writeable_eq;
339///
340/// let formatters = [
341///     TimePrecision::Hour,
342///     TimePrecision::Minute,
343///     TimePrecision::Second,
344///     TimePrecision::Subsecond(SubsecondDigits::S2),
345///     TimePrecision::MinuteOptional,
346/// ]
347/// .map(|time_precision| {
348///     FixedCalendarDateTimeFormatter::<(), _>::try_new(
349///         locale!("en-US").into(),
350///         T::short().with_time_precision(time_precision),
351///     )
352///     .unwrap()
353/// });
354///
355/// let times = [
356///     Time::try_new(7, 0, 0, 0).unwrap(),
357///     Time::try_new(7, 0, 10, 0).unwrap(),
358///     Time::try_new(7, 12, 20, 543_200_000).unwrap(),
359/// ];
360///
361/// let expected_value_table = [
362///     // 7:00:00, 7:00:10, 7:12:20.5432
363///     ["7 AM", "7 AM", "7 AM"],                            // Hour
364///     ["7:00 AM", "7:00 AM", "7:12 AM"],                   // Minute
365///     ["7:00:00 AM", "7:00:10 AM", "7:12:20 AM"],          // Second
366///     ["7:00:00.00 AM", "7:00:10.00 AM", "7:12:20.54 AM"], // Subsecond(F2)
367///     ["7 AM", "7 AM", "7:12 AM"],                         // MinuteOptional
368/// ];
369///
370/// for (expected_value_row, formatter) in
371///     expected_value_table.iter().zip(formatters.iter())
372/// {
373///     for (expected_value, time) in
374///         expected_value_row.iter().zip(times.iter())
375///     {
376///         assert_writeable_eq!(
377///             formatter.format(time),
378///             *expected_value,
379///             "{formatter:?} @ {time:?}"
380///         );
381///     }
382/// }
383/// ```
384#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
385#[cfg_attr(
386    all(feature = "serde", feature = "experimental"),
387    derive(serde::Serialize, serde::Deserialize)
388)]
389#[cfg_attr(
390    all(feature = "serde", feature = "experimental"),
391    serde(from = "TimePrecisionSerde", into = "TimePrecisionSerde")
392)]
393#[non_exhaustive]
394pub enum TimePrecision {
395    /// Display the hour. Hide all other time fields.
396    ///
397    /// Examples:
398    ///
399    /// 1. `11 am`
400    /// 2. `16h`
401    /// 3. `07h`
402    Hour,
403    /// Display the hour and minute. Hide the second.
404    ///
405    /// Examples:
406    ///
407    /// 1. `11:00 am`
408    /// 2. `16:20`
409    /// 3. `07:15`
410    Minute,
411    /// Display the hour, minute, and second. Hide fractional seconds.
412    ///
413    /// This is currently the default, but the default is subject to change.
414    ///
415    /// Examples:
416    ///
417    /// 1. `11:00:00 am`
418    /// 2. `16:20:00`
419    /// 3. `07:15:01`
420    #[default]
421    Second,
422    /// Display the hour, minute, and second with the given number of
423    /// fractional second digits.
424    ///
425    /// Examples with [`SubsecondDigits::S1`]:
426    ///
427    /// 1. `11:00:00.0 am`
428    /// 2. `16:20:00.0`
429    /// 3. `07:15:01.8`
430    Subsecond(SubsecondDigits),
431    /// Display the hour; display the minute if nonzero. Hide the second.
432    ///
433    /// Examples:
434    ///
435    /// 1. `11 am`
436    /// 2. `16:20`
437    /// 3. `07:15`
438    MinuteOptional,
439}
440
441impl IntoOption<TimePrecision> for TimePrecision {
442    #[inline]
443    fn into_option(self) -> Option<Self> {
444        Some(self)
445    }
446}
447
448#[cfg(all(feature = "serde", feature = "experimental"))]
449#[derive(Copy, Clone, serde::Serialize, serde::Deserialize)]
450#[serde(rename_all = "camelCase")]
451enum TimePrecisionSerde {
452    Hour,
453    Minute,
454    Second,
455    Subsecond1,
456    Subsecond2,
457    Subsecond3,
458    Subsecond4,
459    Subsecond5,
460    Subsecond6,
461    Subsecond7,
462    Subsecond8,
463    Subsecond9,
464    MinuteOptional,
465}
466
467#[cfg(all(feature = "serde", feature = "experimental"))]
468impl From<TimePrecision> for TimePrecisionSerde {
469    fn from(value: TimePrecision) -> Self {
470        match value {
471            TimePrecision::Hour => TimePrecisionSerde::Hour,
472            TimePrecision::Minute => TimePrecisionSerde::Minute,
473            TimePrecision::Second => TimePrecisionSerde::Second,
474            TimePrecision::Subsecond(SubsecondDigits::S1) => TimePrecisionSerde::Subsecond1,
475            TimePrecision::Subsecond(SubsecondDigits::S2) => TimePrecisionSerde::Subsecond2,
476            TimePrecision::Subsecond(SubsecondDigits::S3) => TimePrecisionSerde::Subsecond3,
477            TimePrecision::Subsecond(SubsecondDigits::S4) => TimePrecisionSerde::Subsecond4,
478            TimePrecision::Subsecond(SubsecondDigits::S5) => TimePrecisionSerde::Subsecond5,
479            TimePrecision::Subsecond(SubsecondDigits::S6) => TimePrecisionSerde::Subsecond6,
480            TimePrecision::Subsecond(SubsecondDigits::S7) => TimePrecisionSerde::Subsecond7,
481            TimePrecision::Subsecond(SubsecondDigits::S8) => TimePrecisionSerde::Subsecond8,
482            TimePrecision::Subsecond(SubsecondDigits::S9) => TimePrecisionSerde::Subsecond9,
483            TimePrecision::MinuteOptional => TimePrecisionSerde::MinuteOptional,
484        }
485    }
486}
487
488#[cfg(all(feature = "serde", feature = "experimental"))]
489impl From<TimePrecisionSerde> for TimePrecision {
490    fn from(value: TimePrecisionSerde) -> Self {
491        match value {
492            TimePrecisionSerde::Hour => TimePrecision::Hour,
493            TimePrecisionSerde::Minute => TimePrecision::Minute,
494            TimePrecisionSerde::Second => TimePrecision::Second,
495            TimePrecisionSerde::Subsecond1 => TimePrecision::Subsecond(SubsecondDigits::S1),
496            TimePrecisionSerde::Subsecond2 => TimePrecision::Subsecond(SubsecondDigits::S2),
497            TimePrecisionSerde::Subsecond3 => TimePrecision::Subsecond(SubsecondDigits::S3),
498            TimePrecisionSerde::Subsecond4 => TimePrecision::Subsecond(SubsecondDigits::S4),
499            TimePrecisionSerde::Subsecond5 => TimePrecision::Subsecond(SubsecondDigits::S5),
500            TimePrecisionSerde::Subsecond6 => TimePrecision::Subsecond(SubsecondDigits::S6),
501            TimePrecisionSerde::Subsecond7 => TimePrecision::Subsecond(SubsecondDigits::S7),
502            TimePrecisionSerde::Subsecond8 => TimePrecision::Subsecond(SubsecondDigits::S8),
503            TimePrecisionSerde::Subsecond9 => TimePrecision::Subsecond(SubsecondDigits::S9),
504            TimePrecisionSerde::MinuteOptional => TimePrecision::MinuteOptional,
505        }
506    }
507}
508
509/// A specification for how many fractional second digits to display.
510///
511/// For example, to display the time with millisecond precision, use
512/// [`SubsecondDigits::S3`].
513///
514/// Lower-precision digits will be truncated.
515///
516/// # Examples
517///
518/// Times can be displayed with a custom number of fractional digits from 0-9:
519///
520/// ```
521/// use icu::calendar::Gregorian;
522/// use icu::datetime::fieldsets::T;
523/// use icu::datetime::input::Time;
524/// use icu::datetime::options::SubsecondDigits;
525/// use icu::datetime::options::TimePrecision;
526/// use icu::datetime::FixedCalendarDateTimeFormatter;
527/// use icu::locale::locale;
528/// use writeable::assert_writeable_eq;
529///
530/// let formatter = FixedCalendarDateTimeFormatter::<(), _>::try_new(
531///     locale!("en-US").into(),
532///     T::short()
533///         .with_time_precision(TimePrecision::Subsecond(SubsecondDigits::S2)),
534/// )
535/// .unwrap();
536///
537/// assert_writeable_eq!(
538///     formatter.format(&Time::try_new(16, 12, 20, 543200000).unwrap()),
539///     "4:12:20.54 PM"
540/// );
541/// ```
542#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
543#[non_exhaustive]
544pub enum SubsecondDigits {
545    /// One fractional digit (tenths of a second).
546    S1 = 1,
547    /// Two fractional digits (hundredths of a second).
548    S2 = 2,
549    /// Three fractional digits (milliseconds).
550    S3 = 3,
551    /// Four fractional digits.
552    S4 = 4,
553    /// Five fractional digits.
554    S5 = 5,
555    /// Six fractional digits (microseconds).
556    S6 = 6,
557    /// Seven fractional digits.
558    S7 = 7,
559    /// Eight fractional digits.
560    S8 = 8,
561    /// Nine fractional digits (nanoseconds)
562    S9 = 9,
563}
564
565impl SubsecondDigits {
566    /// Constructs a [`SubsecondDigits`] from an integer number of digits.
567    pub fn try_from_int(value: u8) -> Option<Self> {
568        use SubsecondDigits::*;
569        match value {
570            1 => Some(S1),
571            2 => Some(S2),
572            3 => Some(S3),
573            4 => Some(S4),
574            5 => Some(S5),
575            6 => Some(S6),
576            7 => Some(S7),
577            8 => Some(S8),
578            9 => Some(S9),
579            _ => None,
580        }
581    }
582}
583
584impl From<SubsecondDigits> for u8 {
585    fn from(value: SubsecondDigits) -> u8 {
586        use SubsecondDigits::*;
587        match value {
588            S1 => 1,
589            S2 => 2,
590            S3 => 3,
591            S4 => 4,
592            S5 => 5,
593            S6 => 6,
594            S7 => 7,
595            S8 => 8,
596            S9 => 9,
597        }
598    }
599}