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}