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}