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}