jiff/tz/db/
mod.rs

1use crate::{
2    error::{err, Error},
3    tz::TimeZone,
4    util::sync::Arc,
5};
6
7mod bundled;
8mod concatenated;
9mod zoneinfo;
10
11/// Returns a copy of the global [`TimeZoneDatabase`].
12///
13/// This is the same database used for convenience routines like
14/// [`Timestamp::in_tz`](crate::Timestamp::in_tz) and parsing routines
15/// for [`Zoned`](crate::Zoned) that need to do IANA time zone identifier
16/// lookups. Basically, whenever an implicit time zone database is needed,
17/// it is *this* copy of the time zone database that is used.
18///
19/// In feature configurations where a time zone database cannot interact with
20/// the file system (like when `std` is not enabled), this returns a database
21/// where every lookup will fail.
22///
23/// # Example
24///
25/// ```
26/// use jiff::tz;
27///
28/// assert!(tz::db().get("Antarctica/Troll").is_ok());
29/// assert!(tz::db().get("does-not-exist").is_err());
30/// ```
31pub fn db() -> &'static TimeZoneDatabase {
32    #[cfg(not(feature = "std"))]
33    {
34        static NONE: TimeZoneDatabase = TimeZoneDatabase::none();
35        &NONE
36    }
37    #[cfg(feature = "std")]
38    {
39        use std::sync::OnceLock;
40
41        static DB: OnceLock<TimeZoneDatabase> = OnceLock::new();
42        DB.get_or_init(|| {
43            let db = TimeZoneDatabase::from_env();
44            debug!("initialized global time zone database: {db:?}");
45            db
46        })
47    }
48}
49
50/// A handle to a [IANA Time Zone Database].
51///
52/// A `TimeZoneDatabase` provides a way to lookup [`TimeZone`]s by their
53/// human readable identifiers, such as `America/Los_Angeles` and
54/// `Europe/Warsaw`.
55///
56/// It is rare to need to create or use this type directly. Routines
57/// like zoned datetime parsing and time zone conversion provide
58/// convenience routines for using an implicit global time zone database
59/// by default. This global time zone database is available via
60/// [`jiff::tz::db`](crate::tz::db()`). But lower level parsing routines
61/// such as
62/// [`fmt::temporal::DateTimeParser::parse_zoned_with`](crate::fmt::temporal::DateTimeParser::parse_zoned_with)
63/// and
64/// [`civil::DateTime::to_zoned`](crate::civil::DateTime::to_zoned) provide a
65/// means to use a custom copy of a `TimeZoneDatabase`.
66///
67/// # Platform behavior
68///
69/// This behavior is subject to change.
70///
71/// On Unix systems, and when the `tzdb-zoneinfo` crate feature is enabled
72/// (which it is by default), Jiff will read the `/usr/share/zoneinfo`
73/// directory for time zone data.
74///
75/// On Windows systems and when the `tzdb-bundle-platform` crate feature is
76/// enabled (which it is by default), _or_ when the `tzdb-bundle-always` crate
77/// feature is enabled, then the `jiff-tzdb` crate will be used to embed the
78/// entire Time Zone Database into the compiled artifact.
79///
80/// On Android systems, and when the `tzdb-concatenated` crate feature is
81/// enabled (which it is by default), Jiff will attempt to read a concatenated
82/// zoneinfo database using the `ANDROID_DATA` or `ANDROID_ROOT` environment
83/// variables.
84///
85/// In general, using `/usr/share/zoneinfo` (or an equivalent) is heavily
86/// preferred in lieu of embedding the database into your compiled artifact.
87/// The reason is because your system copy of the Time Zone Database may be
88/// updated, perhaps a few times a year, and it is better to get seamless
89/// updates through your system rather than needing to wait on a Rust crate
90/// to update and then rebuild your software. The bundling approach should
91/// only be used when there is no plausible alternative. For example, Windows
92/// has no canonical location for a copy of the Time Zone Database. Indeed,
93/// this is why the Cargo configuration of Jiff specifically does not enabled
94/// bundling by default on Unix systems, but does enable it by default on
95/// Windows systems. Of course, if you really do need a copy of the database
96/// bundled, then you can enable the `tzdb-bundle-always` crate feature.
97///
98/// # Cloning
99///
100/// A `TimeZoneDatabase` can be cheaply cloned. It will share a thread safe
101/// cache with other copies of the same `TimeZoneDatabase`.
102///
103/// # Caching
104///
105/// Because looking up a time zone on disk, reading the file into memory
106/// and parsing the time zone transitions out of that file requires
107/// a fair amount of work, a `TimeZoneDatabase` does a fair bit of
108/// caching. This means that the vast majority of calls to, for example,
109/// [`Timestamp::in_tz`](crate::Timestamp::in_tz) don't actually need to hit
110/// disk. It will just find a cached copy of a [`TimeZone`] and return that.
111///
112/// Of course, with caching comes problems of cache invalidation. Invariably,
113/// there are parameters that Jiff uses to manage when the cache should be
114/// invalidated. Jiff tries to emit log messages about this when it happens. If
115/// you find the caching behavior of Jiff to be sub-optimal for your use case,
116/// please create an issue. (The plan is likely to expose some options for
117/// configuring the behavior of a `TimeZoneDatabase`, but I wanted to collect
118/// user feedback first.)
119///
120/// [IANA Time Zone Database]: https://en.wikipedia.org/wiki/Tz_database
121///
122/// # Example: list all available time zones
123///
124/// ```no_run
125/// use jiff::tz;
126///
127/// for tzid in tz::db().available() {
128///     println!("{tzid}");
129/// }
130/// ```
131///
132/// # Example: using multiple time zone databases
133///
134/// Jiff supports opening and using multiple time zone databases by default.
135/// All you need to do is point [`TimeZoneDatabase::from_dir`] to your own
136/// copy of the Time Zone Database, and it will handle the rest.
137///
138/// This example shows how to utilize multiple databases by parsing a datetime
139/// using an older copy of the IANA Time Zone Database. This example leverages
140/// the fact that the 2018 copy of the database preceded Brazil's announcement
141/// that daylight saving time would be abolished. This meant that datetimes
142/// in the future, when parsed with the older copy of the Time Zone Database,
143/// would still follow the old daylight saving time rules. But a mere update of
144/// the database would otherwise change the meaning of the datetime.
145///
146/// This scenario can come up if one stores datetimes in the future. This is
147/// also why the default offset conflict resolution strategy when parsing zoned
148/// datetimes is [`OffsetConflict::Reject`](crate::tz::OffsetConflict::Reject),
149/// which prevents one from silently re-interpreting datetimes to a different
150/// timestamp.
151///
152/// ```no_run
153/// use jiff::{fmt::temporal::DateTimeParser, tz::{self, TimeZoneDatabase}};
154///
155/// static PARSER: DateTimeParser = DateTimeParser::new();
156///
157/// // Open a version of tzdb from before Brazil announced its abolition
158/// // of daylight saving time.
159/// let tzdb2018 = TimeZoneDatabase::from_dir("path/to/tzdb-2018b")?;
160/// // Open the system tzdb.
161/// let tzdb = tz::db();
162///
163/// // Parse the same datetime string with the same parser, but using two
164/// // different versions of tzdb.
165/// let dt = "2020-01-15T12:00[America/Sao_Paulo]";
166/// let zdt2018 = PARSER.parse_zoned_with(&tzdb2018, dt)?;
167/// let zdt = PARSER.parse_zoned_with(tzdb, dt)?;
168///
169/// // Before DST was abolished, 2020-01-15 was in DST, which corresponded
170/// // to UTC offset -02. Since DST rules applied to datetimes in the
171/// // future, the 2018 version of tzdb would lead one to interpret
172/// // 2020-01-15 as being in DST.
173/// assert_eq!(zdt2018.offset(), tz::offset(-2));
174/// // But DST was abolished in 2019, which means that 2020-01-15 was no
175/// // no longer in DST. So after a tzdb update, the same datetime as above
176/// // now has a different offset.
177/// assert_eq!(zdt.offset(), tz::offset(-3));
178///
179/// // So if you try to parse a datetime serialized from an older copy of
180/// // tzdb, you'll get an error under the default configuration because
181/// // of `OffsetConflict::Reject`. This would succeed if you parsed it
182/// // using tzdb2018!
183/// assert!(PARSER.parse_zoned_with(tzdb, zdt2018.to_string()).is_err());
184///
185/// # Ok::<(), Box<dyn std::error::Error>>(())
186/// ```
187#[derive(Clone)]
188pub struct TimeZoneDatabase {
189    inner: Option<Arc<Kind>>,
190}
191
192#[derive(Debug)]
193// Needed for core-only "dumb" `Arc`.
194#[cfg_attr(not(feature = "alloc"), derive(Clone))]
195enum Kind {
196    ZoneInfo(zoneinfo::Database),
197    Concatenated(concatenated::Database),
198    Bundled(bundled::Database),
199}
200
201impl TimeZoneDatabase {
202    /// Returns a database for which all time zone lookups fail.
203    ///
204    /// # Example
205    ///
206    /// ```
207    /// use jiff::tz::TimeZoneDatabase;
208    ///
209    /// let db = TimeZoneDatabase::none();
210    /// assert_eq!(db.available().count(), 0);
211    /// ```
212    pub const fn none() -> TimeZoneDatabase {
213        TimeZoneDatabase { inner: None }
214    }
215
216    /// Returns a time zone database initialized from the current environment.
217    ///
218    /// This routine never fails, but it may not be able to find a copy of
219    /// your Time Zone Database. When this happens, log messages (with some
220    /// at least at the `WARN` level) will be emitted. They can be viewed by
221    /// installing a [`log`] compatible logger such as [`env_logger`].
222    ///
223    /// Typically, one does not need to call this routine directly. Instead,
224    /// it's done for you as part of [`jiff::tz::db`](crate::tz::db()).
225    /// This does require Jiff's `std` feature to be enabled though. So for
226    /// example, you might use this constructor when the features `alloc`
227    /// and `tzdb-bundle-always` are enabled to get access to a bundled
228    /// copy of the IANA time zone database. (Accessing the system copy at
229    /// `/usr/share/zoneinfo` requires `std`.)
230    ///
231    /// Beware that calling this constructor will create a new _distinct_
232    /// handle from the one returned by `jiff::tz::db` with its own cache.
233    ///
234    /// [`log`]: https://docs.rs/log
235    /// [`env_logger`]: https://docs.rs/env_logger
236    ///
237    /// # Platform behavior
238    ///
239    /// When the `TZDIR` environment variable is set, this will attempt to
240    /// open the Time Zone Database at the directory specified. Otherwise,
241    /// this will search a list of predefined directories for a system
242    /// installation of the Time Zone Database. Typically, it's found at
243    /// `/usr/share/zoneinfo`.
244    ///
245    /// On Windows systems, under the default crate configuration, this will
246    /// return an embedded copy of the Time Zone Database since Windows does
247    /// not have a canonical installation of the Time Zone Database.
248    pub fn from_env() -> TimeZoneDatabase {
249        // On Android, try the concatenated database first, since that's
250        // typically what is used.
251        //
252        // Overall this logic might be sub-optimal. Like, does it really make
253        // sense to check for the zoneinfo or concatenated database on non-Unix
254        // platforms? Probably not to be honest. But these should only be
255        // executed ~once generally, so it doesn't seem like a big deal to try.
256        // And trying makes things a little more flexible I think.
257        if cfg!(target_os = "android") {
258            let db = concatenated::Database::from_env();
259            if !db.is_definitively_empty() {
260                return TimeZoneDatabase::new(Kind::Concatenated(db));
261            }
262
263            let db = zoneinfo::Database::from_env();
264            if !db.is_definitively_empty() {
265                return TimeZoneDatabase::new(Kind::ZoneInfo(db));
266            }
267        } else {
268            let db = zoneinfo::Database::from_env();
269            if !db.is_definitively_empty() {
270                return TimeZoneDatabase::new(Kind::ZoneInfo(db));
271            }
272
273            let db = concatenated::Database::from_env();
274            if !db.is_definitively_empty() {
275                return TimeZoneDatabase::new(Kind::Concatenated(db));
276            }
277        }
278
279        let db = bundled::Database::new();
280        if !db.is_definitively_empty() {
281            return TimeZoneDatabase::new(Kind::Bundled(db));
282        }
283
284        warn!(
285            "could not find zoneinfo, concatenated tzdata or \
286             bundled time zone database",
287        );
288        TimeZoneDatabase::none()
289    }
290
291    /// Returns a time zone database initialized from the given directory.
292    ///
293    /// Unlike [`TimeZoneDatabase::from_env`], this always attempts to look for
294    /// a copy of the Time Zone Database at the directory given. And if it
295    /// fails to find one at that directory, then an error is returned.
296    ///
297    /// Basically, you should use this when you need to use a _specific_
298    /// copy of the Time Zone Database, and use `TimeZoneDatabase::from_env`
299    /// when you just want Jiff to try and "do the right thing for you."
300    ///
301    /// # Errors
302    ///
303    /// This returns an error if the given directory does not contain a valid
304    /// copy of the Time Zone Database. Generally, this means a directory with
305    /// at least one valid TZif file.
306    #[cfg(feature = "std")]
307    pub fn from_dir<P: AsRef<std::path::Path>>(
308        path: P,
309    ) -> Result<TimeZoneDatabase, Error> {
310        let path = path.as_ref();
311        let db = zoneinfo::Database::from_dir(path)?;
312        if db.is_definitively_empty() {
313            warn!(
314                "could not find zoneinfo data at directory {path}",
315                path = path.display(),
316            );
317        }
318        Ok(TimeZoneDatabase::new(Kind::ZoneInfo(db)))
319    }
320
321    /// Returns a time zone database initialized from a path pointing to a
322    /// concatenated `tzdata` file. This type of format is only known to be
323    /// found on Android environments. The specific format for this file isn't
324    /// defined formally anywhere, but Jiff parses the same format supported
325    /// by the [Android Platform].
326    ///
327    /// Unlike [`TimeZoneDatabase::from_env`], this always attempts to look for
328    /// a copy of the Time Zone Database at the path given. And if it
329    /// fails to find one at that path, then an error is returned.
330    ///
331    /// Basically, you should use this when you need to use a _specific_
332    /// copy of the Time Zone Database in its concatenated format, and use
333    /// `TimeZoneDatabase::from_env` when you just want Jiff to try and "do the
334    /// right thing for you." (`TimeZoneDatabase::from_env` will attempt to
335    /// automatically detect the presence of a system concatenated `tzdata`
336    /// file on Android.)
337    ///
338    /// # Errors
339    ///
340    /// This returns an error if the given path does not contain a valid
341    /// copy of the concatenated Time Zone Database.
342    ///
343    /// [Android Platform]: https://android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/util/ZoneInfoDB.java
344    #[cfg(feature = "std")]
345    pub fn from_concatenated_path<P: AsRef<std::path::Path>>(
346        path: P,
347    ) -> Result<TimeZoneDatabase, Error> {
348        let path = path.as_ref();
349        let db = concatenated::Database::from_path(path)?;
350        if db.is_definitively_empty() {
351            warn!(
352                "could not find concatenated tzdata in file {path}",
353                path = path.display(),
354            );
355        }
356        Ok(TimeZoneDatabase::new(Kind::Concatenated(db)))
357    }
358
359    /// Returns a time zone database initialized from the bundled copy of
360    /// the [IANA Time Zone Database].
361    ///
362    /// While this API is always available, in order to get a non-empty
363    /// database back, this requires that one of the crate features
364    /// `tzdb-bundle-always` or `tzdb-bundle-platform` is enabled. In the
365    /// latter case, the bundled database is only available on platforms known
366    /// to lack a system copy of the IANA Time Zone Database (i.e., non-Unix
367    /// systems).
368    ///
369    /// This routine is infallible, but it may return a database
370    /// that is definitively empty if the bundled data is not
371    /// available. To query whether the data is empty or not, use
372    /// [`TimeZoneDatabase::is_definitively_empty`].
373    ///
374    /// [IANA Time Zone Database]: https://en.wikipedia.org/wiki/Tz_database
375    pub fn bundled() -> TimeZoneDatabase {
376        let db = bundled::Database::new();
377        if db.is_definitively_empty() {
378            warn!("could not find embedded/bundled zoneinfo");
379        }
380        TimeZoneDatabase::new(Kind::Bundled(db))
381    }
382
383    /// Creates a new DB from the internal kind.
384    fn new(kind: Kind) -> TimeZoneDatabase {
385        TimeZoneDatabase { inner: Some(Arc::new(kind)) }
386    }
387
388    /// Returns a [`TimeZone`] corresponding to the IANA time zone identifier
389    /// given.
390    ///
391    /// The lookup is performed without regard to ASCII case.
392    ///
393    /// To see a list of all available time zone identifiers for this database,
394    /// use [`TimeZoneDatabase::available`].
395    ///
396    /// # Example
397    ///
398    /// ```
399    /// use jiff::tz;
400    ///
401    /// let tz = tz::db().get("america/NEW_YORK")?;
402    /// assert_eq!(tz.iana_name(), Some("America/New_York"));
403    ///
404    /// # Ok::<(), Box<dyn std::error::Error>>(())
405    /// ```
406    pub fn get(&self, name: &str) -> Result<TimeZone, Error> {
407        let inner = self.inner.as_deref().ok_or_else(|| {
408            if cfg!(feature = "std") {
409                err!(
410                    "failed to find time zone `{name}` since there is no \
411                     time zone database configured",
412                )
413            } else {
414                err!(
415                    "failed to find time zone `{name}`, there is no \
416                     global time zone database configured (and is currently \
417                     impossible to do so without Jiff's `std` feature \
418                     enabled, if you need this functionality, please file \
419                     an issue on Jiff's tracker with your use case)",
420                )
421            }
422        })?;
423        match *inner {
424            Kind::ZoneInfo(ref db) => {
425                if let Some(tz) = db.get(name) {
426                    trace!("found time zone `{name}` in {db:?}", db = self);
427                    return Ok(tz);
428                }
429            }
430            Kind::Concatenated(ref db) => {
431                if let Some(tz) = db.get(name) {
432                    trace!("found time zone `{name}` in {db:?}", db = self);
433                    return Ok(tz);
434                }
435            }
436            Kind::Bundled(ref db) => {
437                if let Some(tz) = db.get(name) {
438                    trace!("found time zone `{name}` in {db:?}", db = self);
439                    return Ok(tz);
440                }
441            }
442        }
443        Err(err!("failed to find time zone `{name}` in time zone database"))
444    }
445
446    /// Returns a list of all available time zone identifiers from this
447    /// database.
448    ///
449    /// Note that time zone identifiers are more of a machine readable
450    /// abstraction and not an end user level abstraction. Still, users
451    /// comfortable with configuring their system's default time zone through
452    /// IANA time zone identifiers are probably comfortable interacting with
453    /// the identifiers returned here.
454    ///
455    /// # Example
456    ///
457    /// ```no_run
458    /// use jiff::tz;
459    ///
460    /// for tzid in tz::db().available() {
461    ///     println!("{tzid}");
462    /// }
463    /// ```
464    pub fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
465        let Some(inner) = self.inner.as_deref() else {
466            return TimeZoneNameIter::empty();
467        };
468        match *inner {
469            Kind::ZoneInfo(ref db) => db.available(),
470            Kind::Concatenated(ref db) => db.available(),
471            Kind::Bundled(ref db) => db.available(),
472        }
473    }
474
475    /// Resets the internal cache of this database.
476    ///
477    /// Subsequent interactions with this database will need to re-read time
478    /// zone data from disk.
479    ///
480    /// It might be useful to call this if you know the time zone database
481    /// has changed on disk and want to force Jiff to re-load it immediately
482    /// without spawning a new process or waiting for Jiff's internal cache
483    /// invalidation heuristics to kick in.
484    pub fn reset(&self) {
485        let Some(inner) = self.inner.as_deref() else { return };
486        match *inner {
487            Kind::ZoneInfo(ref db) => db.reset(),
488            Kind::Concatenated(ref db) => db.reset(),
489            Kind::Bundled(ref db) => db.reset(),
490        }
491    }
492
493    /// Returns true if it is known that this time zone database is empty.
494    ///
495    /// When this returns true, it is guaranteed that all
496    /// [`TimeZoneDatabase::get`] calls will fail, and that
497    /// [`TimeZoneDatabase::available`] will always return an empty iterator.
498    ///
499    /// Note that if this returns false, it is still possible for this database
500    /// to be empty.
501    ///
502    /// # Example
503    ///
504    /// ```
505    /// use jiff::tz::TimeZoneDatabase;
506    ///
507    /// let db = TimeZoneDatabase::none();
508    /// assert!(db.is_definitively_empty());
509    /// ```
510    pub fn is_definitively_empty(&self) -> bool {
511        let Some(inner) = self.inner.as_deref() else { return true };
512        match *inner {
513            Kind::ZoneInfo(ref db) => db.is_definitively_empty(),
514            Kind::Concatenated(ref db) => db.is_definitively_empty(),
515            Kind::Bundled(ref db) => db.is_definitively_empty(),
516        }
517    }
518}
519
520impl core::fmt::Debug for TimeZoneDatabase {
521    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
522        write!(f, "TimeZoneDatabase(")?;
523        let Some(inner) = self.inner.as_deref() else {
524            return write!(f, "unavailable)");
525        };
526        match *inner {
527            Kind::ZoneInfo(ref db) => write!(f, "{db:?}")?,
528            Kind::Concatenated(ref db) => write!(f, "{db:?}")?,
529            Kind::Bundled(ref db) => write!(f, "{db:?}")?,
530        }
531        write!(f, ")")
532    }
533}
534
535/// An iterator over the time zone identifiers in a [`TimeZoneDatabase`].
536///
537/// This iterator is created by [`TimeZoneDatabase::available`].
538///
539/// There are no guarantees about the order in which this iterator yields
540/// time zone identifiers.
541///
542/// The lifetime parameter corresponds to the lifetime of the
543/// `TimeZoneDatabase` from which this iterator was created.
544#[derive(Clone, Debug)]
545pub struct TimeZoneNameIter<'d> {
546    #[cfg(feature = "alloc")]
547    it: alloc::vec::IntoIter<TimeZoneName<'d>>,
548    #[cfg(not(feature = "alloc"))]
549    it: core::iter::Empty<TimeZoneName<'d>>,
550}
551
552impl<'d> TimeZoneNameIter<'d> {
553    /// Creates a time zone name iterator that never yields any elements.
554    fn empty() -> TimeZoneNameIter<'d> {
555        #[cfg(feature = "alloc")]
556        {
557            TimeZoneNameIter { it: alloc::vec::Vec::new().into_iter() }
558        }
559        #[cfg(not(feature = "alloc"))]
560        {
561            TimeZoneNameIter { it: core::iter::empty() }
562        }
563    }
564
565    /// Creates a time zone name iterator that yields the elements from the
566    /// iterator given. (They are collected into a `Vec`.)
567    #[cfg(feature = "alloc")]
568    fn from_iter(
569        it: impl Iterator<Item = impl Into<alloc::string::String>>,
570    ) -> TimeZoneNameIter<'d> {
571        let names: alloc::vec::Vec<TimeZoneName<'d>> =
572            it.map(|name| TimeZoneName::new(name.into())).collect();
573        TimeZoneNameIter { it: names.into_iter() }
574    }
575}
576
577impl<'d> Iterator for TimeZoneNameIter<'d> {
578    type Item = TimeZoneName<'d>;
579
580    fn next(&mut self) -> Option<TimeZoneName<'d>> {
581        self.it.next()
582    }
583}
584
585/// A name for a time zone yield by the [`TimeZoneNameIter`] iterator.
586///
587/// The iterator is created by [`TimeZoneDatabase::available`].
588///
589/// The lifetime parameter corresponds to the lifetime of the
590/// `TimeZoneDatabase` from which this name was created.
591#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
592pub struct TimeZoneName<'d> {
593    /// The lifetime of the tzdb.
594    ///
595    /// We don't currently use this, but it could be quite useful if we ever
596    /// adopt a "compile time" tzdb like what `chrono-tz` has. Then we could
597    /// return strings directly from the embedded data. Or perhaps a "compile
598    /// time" TZif or some such.
599    lifetime: core::marker::PhantomData<&'d str>,
600    #[cfg(feature = "alloc")]
601    name: alloc::string::String,
602    #[cfg(not(feature = "alloc"))]
603    name: core::convert::Infallible,
604}
605
606impl<'d> TimeZoneName<'d> {
607    /// Returns a new time zone name from the string given.
608    ///
609    /// The lifetime returned is inferred according to the caller's context.
610    #[cfg(feature = "alloc")]
611    fn new(name: alloc::string::String) -> TimeZoneName<'d> {
612        TimeZoneName { lifetime: core::marker::PhantomData, name }
613    }
614
615    /// Returns this time zone name as a borrowed string.
616    ///
617    /// Note that the lifetime of the string returned is tied to `self`,
618    /// which may be shorter than the lifetime `'d` of the originating
619    /// `TimeZoneDatabase`.
620    #[inline]
621    pub fn as_str<'a>(&'a self) -> &'a str {
622        #[cfg(feature = "alloc")]
623        {
624            self.name.as_str()
625        }
626        #[cfg(not(feature = "alloc"))]
627        {
628            // Can never be reached because `TimeZoneName` cannot currently
629            // be constructed in core-only environments.
630            unreachable!()
631        }
632    }
633}
634
635impl<'d> core::fmt::Display for TimeZoneName<'d> {
636    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
637        write!(f, "{}", self.as_str())
638    }
639}
640
641#[cfg(test)]
642mod tests {
643    use super::*;
644
645    /// This tests that the size of a time zone database is kept at a single
646    /// word.
647    ///
648    /// I think it would probably be okay to make this bigger if we had a
649    /// good reason to, but it seems sensible to put a road-block to avoid
650    /// accidentally increasing its size.
651    #[test]
652    fn time_zone_database_size() {
653        #[cfg(feature = "alloc")]
654        {
655            let word = core::mem::size_of::<usize>();
656            assert_eq!(word, core::mem::size_of::<TimeZoneDatabase>());
657        }
658        // A `TimeZoneDatabase` in core-only is vapid.
659        #[cfg(not(feature = "alloc"))]
660        {
661            assert_eq!(1, core::mem::size_of::<TimeZoneDatabase>());
662        }
663    }
664}