Skip to main content

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}