jiff_core/tz/mod.rs
1/*!
2Building blocks for supporting time zones.
3*/
4
5use crate::{util::SmallStr, Timestamp};
6
7mod offset;
8pub mod posix;
9pub mod tzif;
10
11pub use self::offset::{
12 AmbiguousError, AmbiguousOffset, AmbiguousTimestamp, Offset,
13};
14
15/// A limit on how much stack space we're willing to use for time zone
16/// abbreviations.
17///
18/// POSIX says this:
19///
20/// > Indicate no less than three, nor more than {TZNAME_MAX}, bytes that are
21/// > the designation for the standard (std) or the alternative (dst -such as
22/// > Daylight Savings Time) timezone.
23///
24/// But it doesn't seem worth the trouble to query `TZNAME_MAX`. Interestingly,
25/// IANA says:
26///
27/// > are 3 or more characters specifying the standard and daylight saving time
28/// > (DST) zone abbreviations
29///
30/// Which implies that IANA thinks there is no limit. But that seems unwise.
31/// Moreover, in practice, it seems like the `date` utility supports fairly
32/// long abbreviations. On my mac (so, BSD `date` as I understand it):
33///
34/// ```text
35/// $ TZ=ZZZ5YYYYYYYYYYYYYYYYYYYYY date
36/// Sun Mar 17 20:05:58 YYYYYYYYYYYYYYYYYYYYY 2024
37/// ```
38///
39/// And on my Linux machine (so, GNU `date`):
40///
41/// ```text
42/// $ TZ=ZZZ5YYYYYYYYYYYYYYYYYYYYY date
43/// Sun Mar 17 08:05:36 PM YYYYYYYYYYYYYYYYYYYYY 2024
44/// ```
45///
46/// I don't know exactly what limit these programs use, but 30 seems good
47/// enough?
48///
49/// Previously, I had been using 255 and stuffing the string in a `Box<str>`.
50/// But as part of work on [#168], I was looking to remove allocation from as
51/// many places as possible. And this was one candidate. But making room on the
52/// stack for 255 byte abbreviations seemed gratuitous. So I picked something
53/// smaller. If we come across an abbreviation bigger than this max, then we'll
54/// error.
55///
56/// In environments with dynamic memory allocation, this maximum is just the
57/// maximum number of bytes we're willing to spend on array-backed storage of
58/// a time zone abbreviation. If we hit anything bigger, we'll use the heap.
59///
60/// In core-only environments, we use a bigger limit as mentioned above.
61/// Anything bigger than this will result in a parse error.
62///
63/// [#168]: https://github.com/BurntSushi/jiff/issues/168
64const TIME_ZONE_ABBREVIATION_MAX: usize = {
65 #[cfg(feature = "alloc")]
66 {
67 // 6 + 1 byte for the length gives us a nice 7 bytes total for the
68 // array.
69 6
70 }
71 #[cfg(not(feature = "alloc"))]
72 {
73 REASONABLE_ABBREVIATION_MAX
74 }
75};
76
77/// When a time zone abbreviation is bigger than this, we give up and error.
78const REASONABLE_ABBREVIATION_MAX: usize = {
79 #[cfg(feature = "alloc")]
80 {
81 // Let this expand to a pretty unreasonable amount.
82 // We could make this even higher, but we should have some
83 // kind of limit.
84 255
85 }
86 #[cfg(not(feature = "alloc"))]
87 {
88 // We make this the same as the array capacity maximum in environments
89 // with dynamic memory allocation as a conservative choice.
90 //
91 // This seems short, but at time of writing, the maximum possible
92 // abbreviation in the tzdb is 5 bytes.
93 //
94 // Actually, we make this bigger so that an abbreviation can fit a
95 // full offset to second resolution. e.g., `+10:30:25`.
96 9
97 }
98};
99
100/// A limit on how much stack space we're willing to use for time zone
101/// identifiers.
102///
103/// As of 2026-07-03, 32 is the length of the longest IANA time zone
104/// identifier. Specifically, `America/Argentina/ComodRivadavia`. Anything
105/// bigger than this will error in core-only environments and spill
106/// over to the heap in all other environments. For environments with
107/// a heap, we set a slightly smaller array to make the total size a
108/// bit smaller (4 words on x86-64 instead of 5). This just means that
109/// `America/Argentina/ComodRivadavia` will spill to the heap, but nothing else
110/// should.
111const TIME_ZONE_ID_MAX: usize = {
112 #[cfg(feature = "alloc")]
113 {
114 30
115 }
116 #[cfg(not(feature = "alloc"))]
117 {
118 32
119 }
120};
121
122/// A type that defines the storage for an IANA time zone identifier.
123pub type TimeZoneId = SmallStr<TIME_ZONE_ID_MAX>;
124
125/// A type that defines the storage for a time zone abbreviation.
126///
127/// For an abbreviation whose length is less than or equal to a certain
128/// implementation defined number, it will be stored inline inside an array.
129/// All other cases spill out into the heap. (Unless callers do not have
130/// dynamic memory allocation, in which case, whatever tried to store the
131/// time zone abbreviation will return an error. For example, parsing a POSIX
132/// time zone transition rule will fail in that case.)
133pub type Abbreviation = SmallStr<TIME_ZONE_ABBREVIATION_MAX>;
134
135/// A representation a single time zone transition.
136#[derive(Clone, Debug)]
137pub struct Transition {
138 timestamp: Timestamp,
139 info: OffsetInfo,
140}
141
142impl Transition {
143 /// Returns the timestamp at which this transition began.
144 pub fn timestamp(&self) -> Timestamp {
145 self.timestamp
146 }
147
148 /// Returns the offset corresponding to this time zone transition. All
149 /// instants at and following this transition's timestamp (and before the
150 /// next transition's timestamp) need to apply this offset from UTC to get
151 /// the civil or "local" time in the corresponding time zone.
152 pub fn offset(&self) -> Offset {
153 self.info.offset()
154 }
155
156 /// Returns the time zone abbreviation corresponding to this time
157 /// zone transition.
158 pub fn abbreviation(&self) -> &Abbreviation {
159 &self.info.abbreviation()
160 }
161
162 /// Returns whether daylight saving time is enabled for this time zone
163 /// transition.
164 pub fn dst(&self) -> Dst {
165 self.info.dst()
166 }
167
168 /// Consumes this transition and returns the underlying `OffsetInfo`.
169 pub fn into_offset_info(self) -> OffsetInfo {
170 self.info
171 }
172}
173
174/// Information associated with an offset when doing a time zone transition
175/// lookup.
176///
177/// Callers should generally only need the offset. This exposes additional
178/// information such as the time zone abbreviation or whether a timestamp is in
179/// daylight saving time or not.
180#[derive(Clone, Debug, Eq, Hash, PartialEq)]
181pub struct OffsetInfo {
182 offset: Offset,
183 abbreviation: Abbreviation,
184 dst: Dst,
185}
186
187impl OffsetInfo {
188 /// Returns the offset corresponding to this time zone transition. All
189 /// instants at and following this transition's timestamp (and before the
190 /// next transition's timestamp) need to apply this offset from UTC to get
191 /// the civil or "local" time in the corresponding time zone.
192 pub fn offset(&self) -> Offset {
193 self.offset
194 }
195
196 /// Returns the time zone abbreviation corresponding to this time
197 /// zone transition.
198 pub fn abbreviation(&self) -> &Abbreviation {
199 &self.abbreviation
200 }
201
202 /// Consumes this offset info and returns its abbreviation.
203 pub fn into_abbreviation(self) -> Abbreviation {
204 self.abbreviation
205 }
206
207 /// Returns whether daylight saving time is enabled for this time zone
208 /// transition.
209 pub fn dst(&self) -> Dst {
210 self.dst
211 }
212}
213
214/// An enum indicating whether a particular datetime is in daylight saving time
215/// (DST) or not.
216#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
217#[cfg_attr(feature = "defmt", derive(defmt::Format))]
218pub enum Dst {
219 /// DST is not in effect. In other words, standard time is in effect.
220 No,
221 /// DST is in effect.
222 Yes,
223}
224
225impl Dst {
226 /// Returns true when this value is equal to `Dst::Yes`.
227 pub fn is_dst(self) -> bool {
228 matches!(self, Dst::Yes)
229 }
230
231 /// Returns true when this value is equal to `Dst::No`.
232 ///
233 /// `std` in this context refers to "standard time." That is, it is the
234 /// offset from UTC used when DST is not in effect.
235 pub fn is_std(self) -> bool {
236 matches!(self, Dst::No)
237 }
238}
239
240impl From<bool> for Dst {
241 fn from(is_dst: bool) -> Dst {
242 if is_dst {
243 Dst::Yes
244 } else {
245 Dst::No
246 }
247 }
248}
249
250/// Creates a new time zone offset in a `const` context from a given number
251/// of hours.
252///
253/// Negative offsets correspond to time zones west of the prime meridian,
254/// while positive offsets correspond to time zones east of the prime
255/// meridian. Equivalently, in all cases, `civil-time - offset = UTC`.
256///
257/// The fallible non-const version of this constructor is
258/// [`Offset::from_hours`].
259///
260/// This is a convenience free function for [`Offset::constant`]. It is
261/// intended to provide a terse syntax for constructing `Offset` values from
262/// a value that is known to be valid.
263///
264/// # Panics
265///
266/// This routine panics when the given number of hours is out of range.
267/// Namely, `hours` must be in the range `-25..=25`.
268///
269/// Similarly, when used in a const context, an out of bounds hour will prevent
270/// your Rust program from compiling.
271///
272/// # Example
273///
274/// ```
275/// use jiff_core::tz::offset;
276///
277/// let o = offset(-5);
278/// assert_eq!(o.seconds(), -18_000);
279/// let o = offset(5);
280/// assert_eq!(o.seconds(), 18_000);
281/// ```
282#[inline]
283pub const fn offset(hours: i8) -> Offset {
284 Offset::constant(hours)
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 // Don't bother trying to test this on non-64 bit. It's too annoying to
292 // keep this test updated.
293 #[cfg(target_pointer_width = "64")]
294 #[test]
295 fn sizes() {
296 #[cfg(feature = "alloc")]
297 assert_eq!(24, core::mem::size_of::<Abbreviation>());
298 #[cfg(not(feature = "alloc"))]
299 assert_eq!(16, core::mem::size_of::<Abbreviation>());
300
301 #[cfg(feature = "alloc")]
302 assert_eq!(32, core::mem::size_of::<TimeZoneId>());
303 #[cfg(not(feature = "alloc"))]
304 assert_eq!(40, core::mem::size_of::<TimeZoneId>());
305 }
306}