icu_time/
types.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
5use icu_calendar::{types::RataDie, AsCalendar, Date, Iso, RangeError};
6
7use crate::zone::UtcOffset;
8
9/// This macro defines a struct for 0-based date fields: hours, minutes, seconds
10/// and fractional seconds. Each unit is bounded by a range. The traits implemented
11/// here will return a Result on whether or not the unit is in range from the given
12/// input.
13macro_rules! dt_unit {
14    ($name:ident, $storage:ident, $value:expr, $(#[$docs:meta])+) => {
15        $(#[$docs])+
16        #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
17        pub struct $name($storage);
18
19        impl $name {
20            /// Gets the numeric value for this component.
21            pub const fn number(self) -> $storage {
22                self.0
23            }
24
25            /// Creates a new value at 0.
26            pub const fn zero() -> $name {
27                Self(0)
28            }
29
30            /// Returns whether the value is zero.
31            #[inline]
32            pub fn is_zero(self) -> bool {
33                self.0 == 0
34            }
35        }
36
37        impl TryFrom<$storage> for $name {
38            type Error = RangeError;
39
40            fn try_from(input: $storage) -> Result<Self, Self::Error> {
41                if input > $value {
42                    Err(RangeError {
43                        field: stringify!($name),
44                        min: 0,
45                        max: $value,
46                        value: input as i32,
47                    })
48                } else {
49                    Ok(Self(input))
50                }
51            }
52        }
53
54        impl TryFrom<usize> for $name {
55            type Error = RangeError;
56
57            fn try_from(input: usize) -> Result<Self, Self::Error> {
58                if input > $value {
59                    Err(RangeError {
60                        field: "$name",
61                        min: 0,
62                        max: $value,
63                        value: input as i32,
64                    })
65                } else {
66                    Ok(Self(input as $storage))
67                }
68            }
69        }
70
71        impl From<$name> for $storage {
72            fn from(input: $name) -> Self {
73                input.0
74            }
75        }
76
77        impl From<$name> for usize {
78            fn from(input: $name) -> Self {
79                input.0 as Self
80            }
81        }
82    };
83}
84
85dt_unit!(
86    Hour,
87    u8,
88    23,
89    /// An ISO-8601 hour component, for use with ISO calendars.
90    ///
91    /// Must be within inclusive bounds `[0, 23]`.
92);
93
94dt_unit!(
95    Minute,
96    u8,
97    59,
98    /// An ISO-8601 minute component, for use with ISO calendars.
99    ///
100    /// Must be within inclusive bounds `[0, 59]`.
101);
102
103dt_unit!(
104    Second,
105    u8,
106    60,
107    /// An ISO-8601 second component, for use with ISO calendars.
108    ///
109    /// Must be within inclusive bounds `[0, 60]`. `60` accommodates for leap seconds.
110);
111
112dt_unit!(
113    Nanosecond,
114    u32,
115    999_999_999,
116    /// A fractional second component, stored as nanoseconds.
117    ///
118    /// Must be within inclusive bounds `[0, 999_999_999]`."
119);
120
121/// A representation of a time in hours, minutes, seconds, and nanoseconds
122///
123/// **The primary definition of this type is in the [`icu_time`](https://docs.rs/icu_time) crate. Other ICU4X crates re-export it for convenience.**
124///
125/// This type supports the range [00:00:00.000000000, 23:59:60.999999999].
126#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
127#[allow(clippy::exhaustive_structs)] // this type is stable
128pub struct Time {
129    /// Hour
130    pub hour: Hour,
131
132    /// Minute
133    pub minute: Minute,
134
135    /// Second
136    pub second: Second,
137
138    /// Subsecond
139    pub subsecond: Nanosecond,
140}
141
142impl Time {
143    /// Construct a new [`Time`], without validating that all components are in range
144    pub const fn new(hour: Hour, minute: Minute, second: Second, subsecond: Nanosecond) -> Self {
145        Self {
146            hour,
147            minute,
148            second,
149            subsecond,
150        }
151    }
152
153    /// Construct a new [`Time`] representing the start of the day (00:00:00.000)
154    pub const fn start_of_day() -> Self {
155        Self {
156            hour: Hour(0),
157            minute: Minute(0),
158            second: Second(0),
159            subsecond: Nanosecond(0),
160        }
161    }
162
163    /// Construct a new [`Time`] representing noon (12:00:00.000)
164    pub const fn noon() -> Self {
165        Self {
166            hour: Hour(12),
167            minute: Minute(0),
168            second: Second(0),
169            subsecond: Nanosecond(0),
170        }
171    }
172
173    /// Construct a new [`Time`], whilst validating that all components are in range
174    pub fn try_new(hour: u8, minute: u8, second: u8, nanosecond: u32) -> Result<Self, RangeError> {
175        Ok(Self {
176            hour: hour.try_into()?,
177            minute: minute.try_into()?,
178            second: second.try_into()?,
179            subsecond: nanosecond.try_into()?,
180        })
181    }
182}
183
184/// A date and time for a given calendar.
185///
186/// **The primary definition of this type is in the [`icu_time`](https://docs.rs/icu_time) crate. Other ICU4X crates re-export it for convenience.**
187#[derive(Debug, PartialEq, Eq, Clone, Copy)]
188#[allow(clippy::exhaustive_structs)] // this type is stable
189pub struct DateTime<A: AsCalendar> {
190    /// The date
191    pub date: Date<A>,
192    /// The time
193    pub time: Time,
194}
195
196/// A date and time for a given calendar, local to a specified time zone.
197///
198/// **The primary definition of this type is in the [`icu_time`](https://docs.rs/icu_time) crate. Other ICU4X crates re-export it for convenience.**
199#[derive(Debug, PartialEq, Eq, Clone, Copy)]
200#[allow(clippy::exhaustive_structs)] // this type is stable
201pub struct ZonedDateTime<A: AsCalendar, Z> {
202    /// The date, local to the time zone
203    pub date: Date<A>,
204    /// The time, local to the time zone
205    pub time: Time,
206    /// The time zone
207    pub zone: Z,
208}
209
210impl ZonedDateTime<Iso, UtcOffset> {
211    /// Creates a [`ZonedDateTime`] from an absolute time, in milliseconds since the UNIX Epoch,
212    /// and a UTC offset.
213    ///
214    /// This constructor returns a [`ZonedDateTime`] that supports only the localized offset
215    /// time zone style.
216    ///
217    /// # Examples
218    ///
219    /// ```
220    /// use icu::calendar::cal::Iso;
221    /// use icu::time::zone::UtcOffset;
222    /// use icu::time::ZonedDateTime;
223    ///
224    /// let iso_str = "2025-04-30T17:45-0700";
225    /// let timestamp = 1746060300000; // milliseconds since UNIX epoch
226    /// let offset: UtcOffset = "-0700".parse().unwrap();
227    ///
228    /// let zdt_from_timestamp =
229    ///     ZonedDateTime::from_epoch_milliseconds_and_utc_offset(
230    ///         timestamp, offset,
231    ///     );
232    ///
233    /// // Check that it equals the same as the parse result:
234    /// let zdt_from_str =
235    ///     ZonedDateTime::try_offset_only_from_str(iso_str, Iso).unwrap();
236    /// assert_eq!(zdt_from_timestamp, zdt_from_str);
237    /// ```
238    ///
239    /// Negative timestamps are supported:
240    ///
241    /// ```
242    /// use icu::calendar::cal::Iso;
243    /// use icu::time::zone::UtcOffset;
244    /// use icu::time::ZonedDateTime;
245    ///
246    /// let iso_str = "1920-01-02T03:04:05.250+0600";
247    /// let timestamp = -1577847354750; // milliseconds since UNIX epoch
248    /// let offset: UtcOffset = "+0600".parse().unwrap();
249    ///
250    /// let zdt_from_timestamp =
251    ///     ZonedDateTime::from_epoch_milliseconds_and_utc_offset(
252    ///         timestamp, offset,
253    ///     );
254    ///
255    /// // Check that it equals the same as the parse result:
256    /// let zdt_from_str =
257    ///     ZonedDateTime::try_offset_only_from_str(iso_str, Iso).unwrap();
258    /// assert_eq!(zdt_from_timestamp, zdt_from_str);
259    /// ```
260    pub fn from_epoch_milliseconds_and_utc_offset(
261        epoch_milliseconds: i64,
262        utc_offset: UtcOffset,
263    ) -> Self {
264        // TODO(#6512): Handle overflow
265        let local_epoch_milliseconds = epoch_milliseconds + (1000 * utc_offset.to_seconds()) as i64;
266        let (epoch_days, time_millisecs) = (
267            local_epoch_milliseconds.div_euclid(86400000),
268            local_epoch_milliseconds.rem_euclid(86400000),
269        );
270        const UNIX_EPOCH: RataDie = calendrical_calculations::iso::const_fixed_from_iso(1970, 1, 1);
271        let rata_die = UNIX_EPOCH + epoch_days;
272        #[allow(clippy::unwrap_used)] // these values are derived via modulo operators
273        let time = Time::try_new(
274            (time_millisecs / 3600000) as u8,
275            ((time_millisecs % 3600000) / 60000) as u8,
276            ((time_millisecs % 60000) / 1000) as u8,
277            ((time_millisecs % 1000) as u32) * 1000000,
278        )
279        .unwrap();
280        ZonedDateTime {
281            date: Date::from_rata_die(rata_die, Iso),
282            time,
283            zone: utc_offset,
284        }
285    }
286}