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
210const UNIX_EPOCH: RataDie = calendrical_calculations::gregorian::fixed_from_gregorian(1970, 1, 1);
211
212impl Ord for ZonedDateTime<Iso, UtcOffset> {
213    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
214        self.to_epoch_milliseconds_utc()
215            .cmp(&other.to_epoch_milliseconds_utc())
216    }
217}
218impl PartialOrd for ZonedDateTime<Iso, UtcOffset> {
219    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
220        Some(self.cmp(other))
221    }
222}
223
224impl ZonedDateTime<Iso, UtcOffset> {
225    /// Creates a [`ZonedDateTime`] from an absolute time, in milliseconds since the UNIX Epoch,
226    /// and a UTC offset.
227    ///
228    /// This constructor returns a [`ZonedDateTime`] that supports only the localized offset
229    /// time zone style.
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// use icu::calendar::cal::Iso;
235    /// use icu::time::zone::UtcOffset;
236    /// use icu::time::ZonedDateTime;
237    ///
238    /// let iso_str = "2025-04-30T17:45-0700";
239    /// let timestamp = 1746060300000; // milliseconds since UNIX epoch
240    /// let offset: UtcOffset = "-0700".parse().unwrap();
241    ///
242    /// let zdt_from_timestamp =
243    ///     ZonedDateTime::from_epoch_milliseconds_and_utc_offset(
244    ///         timestamp, offset,
245    ///     );
246    ///
247    /// // Check that it equals the same as the parse result:
248    /// let zdt_from_str =
249    ///     ZonedDateTime::try_offset_only_from_str(iso_str, Iso).unwrap();
250    /// assert_eq!(zdt_from_timestamp, zdt_from_str);
251    /// ```
252    ///
253    /// Negative timestamps are supported:
254    ///
255    /// ```
256    /// use icu::calendar::cal::Iso;
257    /// use icu::time::zone::UtcOffset;
258    /// use icu::time::ZonedDateTime;
259    ///
260    /// let iso_str = "1920-01-02T03:04:05.250+0600";
261    /// let timestamp = -1577847354750; // milliseconds since UNIX epoch
262    /// let offset: UtcOffset = "+0600".parse().unwrap();
263    ///
264    /// let zdt_from_timestamp =
265    ///     ZonedDateTime::from_epoch_milliseconds_and_utc_offset(
266    ///         timestamp, offset,
267    ///     );
268    ///
269    /// // Check that it equals the same as the parse result:
270    /// let zdt_from_str =
271    ///     ZonedDateTime::try_offset_only_from_str(iso_str, Iso).unwrap();
272    /// assert_eq!(zdt_from_timestamp, zdt_from_str);
273    /// ```
274    pub fn from_epoch_milliseconds_and_utc_offset(
275        epoch_milliseconds: i64,
276        utc_offset: UtcOffset,
277    ) -> Self {
278        // TODO(#6512): Handle overflow
279        let local_epoch_milliseconds = epoch_milliseconds + (1000 * utc_offset.to_seconds()) as i64;
280        let (epoch_days, time_millisecs) = (
281            local_epoch_milliseconds.div_euclid(86400000),
282            local_epoch_milliseconds.rem_euclid(86400000),
283        );
284        let rata_die = UNIX_EPOCH + epoch_days;
285        #[expect(clippy::unwrap_used)] // these values are derived via modulo operators
286        let time = Time::try_new(
287            (time_millisecs / 3600000) as u8,
288            ((time_millisecs % 3600000) / 60000) as u8,
289            ((time_millisecs % 60000) / 1000) as u8,
290            ((time_millisecs % 1000) as u32) * 1000000,
291        )
292        .unwrap();
293        ZonedDateTime {
294            date: Date::from_rata_die(rata_die, Iso),
295            time,
296            zone: utc_offset,
297        }
298    }
299
300    pub(crate) fn to_epoch_milliseconds_utc(self) -> i64 {
301        let ZonedDateTime { date, time, zone } = self;
302        let days = date.to_rata_die() - UNIX_EPOCH;
303        let hours = time.hour.number() as i64;
304        let minutes = time.minute.number() as i64;
305        let seconds = time.second.number() as i64;
306        let nanos = time.subsecond.number() as i64;
307        let offset_seconds = zone.to_seconds() as i64;
308        (((days * 24 + hours) * 60 + minutes) * 60 + seconds - offset_seconds) * 1000
309            + nanos / 1_000_000
310    }
311}