jiff/tz/db/mod.rs
1use crate::{
2 error::{tz::db::Error as E, Error},
3 tz::TimeZone,
4 util::{sync::Arc, utf8},
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(any(not(feature = "std"), miri))]
33 #[cfg(not(feature = "std"))]
34 {
35 // Without `std` there's no lazily initialized global state. But when
36 // the tzdb is bundled into the binary, we can still hand out a usable
37 // database: the bundled database needs no allocation, so it can be
38 // constructed in a `const`. (Without `std`, the zoneinfo and
39 // concatenated databases are both unavailable, so there's nothing to
40 // prefer over the bundle.)
41 //
42 // Ref: https://github.com/BurntSushi/jiff/issues/533
43 #[cfg(any(
44 feature = "tzdb-bundle-always",
45 all(
46 feature = "tzdb-bundle-platform",
47 any(windows, target_family = "wasm"),
48 ),
49 ))]
50 static DB: TimeZoneDatabase = TimeZoneDatabase {
51 inner: Repr::Bundled(bundled::Database::new()),
52 };
53 #[cfg(not(any(
54 feature = "tzdb-bundle-always",
55 all(
56 feature = "tzdb-bundle-platform",
57 any(windows, target_family = "wasm"),
58 ),
59 )))]
60 static DB: TimeZoneDatabase = TimeZoneDatabase::none();
61 &DB
62 }
63 // #[cfg(all(feature = "std", not(miri)))]
64 #[cfg(feature = "std")]
65 {
66 use std::sync::OnceLock;
67
68 static DB: OnceLock<TimeZoneDatabase> = OnceLock::new();
69 DB.get_or_init(|| {
70 let db = TimeZoneDatabase::from_env();
71 debug!("initialized global time zone database: {db:?}");
72 db
73 })
74 }
75}
76
77/// A handle to a [IANA Time Zone Database].
78///
79/// A `TimeZoneDatabase` provides a way to lookup [`TimeZone`]s by their
80/// human readable identifiers, such as `America/Los_Angeles` and
81/// `Europe/Warsaw`.
82///
83/// It is rare to need to create or use this type directly. Routines
84/// like zoned datetime parsing and time zone conversion provide
85/// convenience routines for using an implicit global time zone database
86/// by default. This global time zone database is available via
87/// [`jiff::tz::db`](crate::tz::db()`). But lower level parsing routines
88/// such as
89/// [`fmt::temporal::DateTimeParser::parse_zoned_with`](crate::fmt::temporal::DateTimeParser::parse_zoned_with)
90/// and
91/// [`civil::DateTime::to_zoned`](crate::civil::DateTime::to_zoned) provide a
92/// means to use a custom copy of a `TimeZoneDatabase`.
93///
94/// # Platform behavior
95///
96/// This behavior is subject to change.
97///
98/// On Unix systems, and when the `tzdb-zoneinfo` crate feature is enabled
99/// (which it is by default), Jiff will read the `/usr/share/zoneinfo`
100/// directory for time zone data.
101///
102/// On Windows systems and when the `tzdb-bundle-platform` crate feature is
103/// enabled (which it is by default), _or_ when the `tzdb-bundle-always` crate
104/// feature is enabled, then the `jiff-tzdb` crate will be used to embed the
105/// entire Time Zone Database into the compiled artifact.
106///
107/// On Android systems, and when the `tzdb-concatenated` crate feature is
108/// enabled (which it is by default), Jiff will attempt to read a concatenated
109/// zoneinfo database using the `ANDROID_DATA` or `ANDROID_ROOT` environment
110/// variables.
111///
112/// In general, using `/usr/share/zoneinfo` (or an equivalent) is heavily
113/// preferred in lieu of embedding the database into your compiled artifact.
114/// The reason is because your system copy of the Time Zone Database may be
115/// updated, perhaps a few times a year, and it is better to get seamless
116/// updates through your system rather than needing to wait on a Rust crate
117/// to update and then rebuild your software. The bundling approach should
118/// only be used when there is no plausible alternative. For example, Windows
119/// has no canonical location for a copy of the Time Zone Database. Indeed,
120/// this is why the Cargo configuration of Jiff specifically does not enabled
121/// bundling by default on Unix systems, but does enable it by default on
122/// Windows systems. Of course, if you really do need a copy of the database
123/// bundled, then you can enable the `tzdb-bundle-always` crate feature.
124///
125/// # Cloning
126///
127/// A `TimeZoneDatabase` can be cheaply cloned. It will share a thread safe
128/// cache with other copies of the same `TimeZoneDatabase`.
129///
130/// # Caching
131///
132/// Because looking up a time zone on disk, reading the file into memory
133/// and parsing the time zone transitions out of that file requires
134/// a fair amount of work, a `TimeZoneDatabase` does a fair bit of
135/// caching. This means that the vast majority of calls to, for example,
136/// [`Timestamp::in_tz`](crate::Timestamp::in_tz) don't actually need to hit
137/// disk. It will just find a cached copy of a [`TimeZone`] and return that.
138///
139/// Of course, with caching comes problems of cache invalidation. Invariably,
140/// there are parameters that Jiff uses to manage when the cache should be
141/// invalidated. Jiff tries to emit log messages about this when it happens. If
142/// you find the caching behavior of Jiff to be sub-optimal for your use case,
143/// please create an issue. (The plan is likely to expose some options for
144/// configuring the behavior of a `TimeZoneDatabase`, but I wanted to collect
145/// user feedback first.)
146///
147/// [IANA Time Zone Database]: https://en.wikipedia.org/wiki/Tz_database
148///
149/// # Example: list all available time zones
150///
151/// ```no_run
152/// use jiff::tz;
153///
154/// for tzid in tz::db().available() {
155/// println!("{tzid}");
156/// }
157/// ```
158///
159/// # Example: using multiple time zone databases
160///
161/// Jiff supports opening and using multiple time zone databases by default.
162/// All you need to do is point [`TimeZoneDatabase::from_dir`] to your own
163/// copy of the Time Zone Database, and it will handle the rest.
164///
165/// This example shows how to utilize multiple databases by parsing a datetime
166/// using an older copy of the IANA Time Zone Database. This example leverages
167/// the fact that the 2018 copy of the database preceded Brazil's announcement
168/// that daylight saving time would be abolished. This meant that datetimes
169/// in the future, when parsed with the older copy of the Time Zone Database,
170/// would still follow the old daylight saving time rules. But a mere update of
171/// the database would otherwise change the meaning of the datetime.
172///
173/// This scenario can come up if one stores datetimes in the future. This is
174/// also why the default offset conflict resolution strategy when parsing zoned
175/// datetimes is [`OffsetConflict::Reject`](crate::tz::OffsetConflict::Reject),
176/// which prevents one from silently re-interpreting datetimes to a different
177/// timestamp.
178///
179/// ```no_run
180/// use jiff::{fmt::temporal::DateTimeParser, tz::{self, TimeZoneDatabase}};
181///
182/// static PARSER: DateTimeParser = DateTimeParser::new();
183///
184/// // Open a version of tzdb from before Brazil announced its abolition
185/// // of daylight saving time.
186/// let tzdb2018 = TimeZoneDatabase::from_dir("path/to/tzdb-2018b")?;
187/// // Open the system tzdb.
188/// let tzdb = tz::db();
189///
190/// // Parse the same datetime string with the same parser, but using two
191/// // different versions of tzdb.
192/// let dt = "2020-01-15T12:00[America/Sao_Paulo]";
193/// let zdt2018 = PARSER.parse_zoned_with(&tzdb2018, dt)?;
194/// let zdt = PARSER.parse_zoned_with(tzdb, dt)?;
195///
196/// // Before DST was abolished, 2020-01-15 was in DST, which corresponded
197/// // to UTC offset -02. Since DST rules applied to datetimes in the
198/// // future, the 2018 version of tzdb would lead one to interpret
199/// // 2020-01-15 as being in DST.
200/// assert_eq!(zdt2018.offset(), tz::offset(-2));
201/// // But DST was abolished in 2019, which means that 2020-01-15 was no
202/// // no longer in DST. So after a tzdb update, the same datetime as above
203/// // now has a different offset.
204/// assert_eq!(zdt.offset(), tz::offset(-3));
205///
206/// // So if you try to parse a datetime serialized from an older copy of
207/// // tzdb, you'll get an error under the default configuration because
208/// // of `OffsetConflict::Reject`. This would succeed if you parsed it
209/// // using tzdb2018!
210/// assert!(PARSER.parse_zoned_with(tzdb, zdt2018.to_string()).is_err());
211///
212/// # Ok::<(), Box<dyn std::error::Error>>(())
213/// ```
214#[derive(Clone)]
215pub struct TimeZoneDatabase {
216 inner: Repr,
217}
218
219/// The internal representation of a `TimeZoneDatabase`.
220///
221/// The bundled database is kept out of the `Arc` because it carries no data
222/// of its own (the tzdb is compiled into the binary and any parsed zones are
223/// cached in a global). This lets it be constructed in a `const`, which is
224/// what makes it possible for `db()` to return the bundled database even when
225/// `std` is unavailable (and so there is no lazily initialized global state).
226///
227/// Ref: https://github.com/BurntSushi/jiff/issues/533
228#[derive(Clone)]
229enum Repr {
230 /// A database for which all lookups fail.
231 Empty,
232 /// The bundled database. Needs no allocation, so no `Arc`.
233 Bundled(bundled::Database),
234 /// A database backed by an `Arc` so that clones share its cache.
235 Arc(Arc<Kind>),
236}
237
238#[derive(Debug)]
239// Needed for core-only "dumb" `Arc`.
240#[cfg_attr(not(feature = "alloc"), derive(Clone))]
241enum Kind {
242 ZoneInfo(zoneinfo::Database),
243 Concatenated(concatenated::Database),
244}
245
246impl TimeZoneDatabase {
247 /// Returns a database for which all time zone lookups fail.
248 ///
249 /// # Example
250 ///
251 /// ```
252 /// use jiff::tz::TimeZoneDatabase;
253 ///
254 /// let db = TimeZoneDatabase::none();
255 /// assert_eq!(db.available().count(), 0);
256 /// ```
257 pub const fn none() -> TimeZoneDatabase {
258 TimeZoneDatabase { inner: Repr::Empty }
259 }
260
261 /// Returns a time zone database initialized from the current environment.
262 ///
263 /// This routine never fails, but it may not be able to find a copy of
264 /// your Time Zone Database. When this happens, log messages (with some
265 /// at least at the `WARN` level) will be emitted. They can be viewed by
266 /// installing a [`log`] compatible logger such as [`env_logger`].
267 ///
268 /// Typically, one does not need to call this routine directly. Instead,
269 /// it's done for you as part of [`jiff::tz::db`](crate::tz::db()).
270 /// This does require Jiff's `std` feature to be enabled though. So for
271 /// example, you might use this constructor when the features `alloc`
272 /// and `tzdb-bundle-always` are enabled to get access to a bundled
273 /// copy of the IANA time zone database. (Accessing the system copy at
274 /// `/usr/share/zoneinfo` requires `std`.)
275 ///
276 /// Beware that calling this constructor will create a new _distinct_
277 /// handle from the one returned by `jiff::tz::db` with its own cache.
278 ///
279 /// [`log`]: https://docs.rs/log
280 /// [`env_logger`]: https://docs.rs/env_logger
281 ///
282 /// # Platform behavior
283 ///
284 /// When the `TZDIR` environment variable is set, this will attempt to
285 /// open the Time Zone Database at the directory specified. Otherwise,
286 /// this will search a list of predefined directories for a system
287 /// installation of the Time Zone Database. Typically, it's found at
288 /// `/usr/share/zoneinfo`.
289 ///
290 /// On Windows systems, under the default crate configuration, this will
291 /// return an embedded copy of the Time Zone Database since Windows does
292 /// not have a canonical installation of the Time Zone Database.
293 pub fn from_env() -> TimeZoneDatabase {
294 // On Android, try the concatenated database first, since that's
295 // typically what is used.
296 //
297 // Overall this logic might be sub-optimal. Like, does it really make
298 // sense to check for the zoneinfo or concatenated database on non-Unix
299 // platforms? Probably not to be honest. But these should only be
300 // executed ~once generally, so it doesn't seem like a big deal to try.
301 // And trying makes things a little more flexible I think.
302 #[cfg(not(miri))]
303 {
304 if cfg!(target_os = "android") {
305 let db = concatenated::Database::from_env();
306 if !db.is_definitively_empty() {
307 return TimeZoneDatabase::new(Kind::Concatenated(db));
308 }
309
310 let db = zoneinfo::Database::from_env();
311 if !db.is_definitively_empty() {
312 return TimeZoneDatabase::new(Kind::ZoneInfo(db));
313 }
314 } else {
315 let db = zoneinfo::Database::from_env();
316 if !db.is_definitively_empty() {
317 return TimeZoneDatabase::new(Kind::ZoneInfo(db));
318 }
319
320 let db = concatenated::Database::from_env();
321 if !db.is_definitively_empty() {
322 return TimeZoneDatabase::new(Kind::Concatenated(db));
323 }
324 }
325 }
326
327 let db = bundled::Database::new();
328 if !db.is_definitively_empty() {
329 return TimeZoneDatabase { inner: Repr::Bundled(db) };
330 }
331
332 warn!(
333 "could not find zoneinfo, concatenated tzdata or \
334 bundled time zone database",
335 );
336 TimeZoneDatabase::none()
337 }
338
339 /// Returns a time zone database initialized from the given directory.
340 ///
341 /// Unlike [`TimeZoneDatabase::from_env`], this always attempts to look for
342 /// a copy of the Time Zone Database at the directory given. And if it
343 /// fails to find one at that directory, then an error is returned.
344 ///
345 /// Basically, you should use this when you need to use a _specific_
346 /// copy of the Time Zone Database, and use `TimeZoneDatabase::from_env`
347 /// when you just want Jiff to try and "do the right thing for you."
348 ///
349 /// # Errors
350 ///
351 /// This returns an error if the given directory does not contain a valid
352 /// copy of the Time Zone Database. Generally, this means a directory with
353 /// at least one valid TZif file.
354 #[cfg(feature = "std")]
355 pub fn from_dir<P: AsRef<std::path::Path>>(
356 path: P,
357 ) -> Result<TimeZoneDatabase, Error> {
358 let path = path.as_ref();
359 let db = zoneinfo::Database::from_dir(path)?;
360 if db.is_definitively_empty() {
361 warn!(
362 "could not find zoneinfo data at directory {path}",
363 path = path.display(),
364 );
365 }
366 Ok(TimeZoneDatabase::new(Kind::ZoneInfo(db)))
367 }
368
369 /// Returns a time zone database initialized from a path pointing to a
370 /// concatenated `tzdata` file. This type of format is only known to be
371 /// found on Android environments. The specific format for this file isn't
372 /// defined formally anywhere, but Jiff parses the same format supported
373 /// by the [Android Platform].
374 ///
375 /// Unlike [`TimeZoneDatabase::from_env`], this always attempts to look for
376 /// a copy of the Time Zone Database at the path given. And if it
377 /// fails to find one at that path, then an error is returned.
378 ///
379 /// Basically, you should use this when you need to use a _specific_
380 /// copy of the Time Zone Database in its concatenated format, and use
381 /// `TimeZoneDatabase::from_env` when you just want Jiff to try and "do the
382 /// right thing for you." (`TimeZoneDatabase::from_env` will attempt to
383 /// automatically detect the presence of a system concatenated `tzdata`
384 /// file on Android.)
385 ///
386 /// # Errors
387 ///
388 /// This returns an error if the given path does not contain a valid
389 /// copy of the concatenated Time Zone Database.
390 ///
391 /// [Android Platform]: https://android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/util/ZoneInfoDB.java
392 #[cfg(feature = "std")]
393 pub fn from_concatenated_path<P: AsRef<std::path::Path>>(
394 path: P,
395 ) -> Result<TimeZoneDatabase, Error> {
396 let path = path.as_ref();
397 let db = concatenated::Database::from_path(path)?;
398 if db.is_definitively_empty() {
399 warn!(
400 "could not find concatenated tzdata in file {path}",
401 path = path.display(),
402 );
403 }
404 Ok(TimeZoneDatabase::new(Kind::Concatenated(db)))
405 }
406
407 /// Returns a time zone database initialized from the bundled copy of
408 /// the [IANA Time Zone Database].
409 ///
410 /// While this API is always available, in order to get a non-empty
411 /// database back, this requires that one of the crate features
412 /// `tzdb-bundle-always` or `tzdb-bundle-platform` is enabled. In the
413 /// latter case, the bundled database is only available on platforms known
414 /// to lack a system copy of the IANA Time Zone Database (i.e., non-Unix
415 /// systems).
416 ///
417 /// This routine is infallible, but it may return a database
418 /// that is definitively empty if the bundled data is not
419 /// available. To query whether the data is empty or not, use
420 /// [`TimeZoneDatabase::is_definitively_empty`].
421 ///
422 /// # Data generation
423 ///
424 /// The data in this crate comes from the [IANA Time Zone Database] "data
425 /// only" distribution. [`jiff-cli`] is used to first compile the release
426 /// into binary TZif data using the `zic` compiler, and secondly, converts
427 /// the binary data into a flattened and de-duplicated representation that
428 /// is embedded into this crate's source code.
429 ///
430 /// The conversion into the TZif binary data uses the following settings:
431 ///
432 /// * The "rearguard" data is used (see below).
433 /// * The binary data itself is compiled using the "slim" format. Which
434 /// effectively means that the TZif data primarily only uses explicit
435 /// time zone transitions for historical data and POSIX time zones for
436 /// current time zone transition rules. This doesn't have any impact
437 /// on the actual results. The reason that there are "slim" and "fat"
438 /// formats is to support legacy applications that can't deal with
439 /// POSIX time zones. For example, `/usr/share/zoneinfo` on my modern
440 /// Archlinux installation (2025-02-27) is in the "fat" format.
441 ///
442 /// The reason that rearguard data is used is a bit more subtle and has
443 /// to do with a difference in how the IANA Time Zone Database treats its
444 /// internal "daylight saving time" flag and what people in the "real
445 /// world" consider "daylight saving time." For example, in the standard
446 /// distribution of the IANA Time Zone Database, `Europe/Dublin` has its
447 /// daylight saving time flag set to _true_ during Winter and set to
448 /// _false_ during Summer. The actual time shifts are the same as, e.g.,
449 /// `Europe/London`, but which one is actually labeled "daylight saving
450 /// time" is not.
451 ///
452 /// The IANA Time Zone Database does this for `Europe/Dublin`, presumably,
453 /// because _legally_, time during the Summer in Ireland is called `Irish
454 /// Standard Time`, and time during the Winter is called `Greenwich Mean
455 /// Time`. These legal names are reversed from what is typically the case,
456 /// where "standard" time is during the Winter and daylight saving time is
457 /// during the Summer. The IANA Time Zone Database implements this tweak in
458 /// legal language via a "negative daylight saving time offset." This is
459 /// somewhat odd, and some consumers of the IANA Time Zone Database cannot
460 /// handle it. Thus, the rearguard format was born for, seemingly, legacy
461 /// programs.
462 ///
463 /// Jiff can handle negative daylight saving time offsets just fine,
464 /// but we use the rearguard format anyway so that the underlying data
465 /// more accurately reflects on-the-ground reality for humans living in
466 /// `Europe/Dublin`. In particular, using the rearguard data enables
467 /// [localization of time zone names] to be done correctly.
468 ///
469 /// [IANA Time Zone Database]: https://en.wikipedia.org/wiki/Tz_database
470 /// [`jiff-cli`]: https://github.com/BurntSushi/jiff/tree/master/crates/jiff-cli
471 /// [localization of time zone names]: https://github.com/BurntSushi/jiff/issues/258
472 pub fn bundled() -> TimeZoneDatabase {
473 let db = bundled::Database::new();
474 if db.is_definitively_empty() {
475 warn!("could not find embedded/bundled zoneinfo");
476 }
477 TimeZoneDatabase { inner: Repr::Bundled(db) }
478 }
479
480 /// Creates a new DB from the internal kind.
481 fn new(kind: Kind) -> TimeZoneDatabase {
482 TimeZoneDatabase { inner: Repr::Arc(Arc::new(kind)) }
483 }
484
485 /// Returns a [`TimeZone`] corresponding to the IANA time zone identifier
486 /// given.
487 ///
488 /// The lookup is performed without regard to ASCII case.
489 ///
490 /// To see a list of all available time zone identifiers for this database,
491 /// use [`TimeZoneDatabase::available`].
492 ///
493 /// It is guaranteed that if the given time zone name is case insensitively
494 /// equivalent to `UTC`, then the time zone returned will be equivalent to
495 /// `TimeZone::UTC`. Similarly for `Etc/Unknown` and `TimeZone::unknown()`.
496 ///
497 /// # Example
498 ///
499 /// ```
500 /// use jiff::tz;
501 ///
502 /// let tz = tz::db().get("america/NEW_YORK")?;
503 /// assert_eq!(tz.iana_name(), Some("America/New_York"));
504 ///
505 /// # Ok::<(), Box<dyn std::error::Error>>(())
506 /// ```
507 pub fn get(&self, name: &str) -> Result<TimeZone, Error> {
508 let found = match self.inner {
509 Repr::Empty => {
510 return Err(Error::from(
511 E::failed_time_zone_no_database_configured(name),
512 ));
513 }
514 Repr::Bundled(ref db) => db.get(name),
515 Repr::Arc(ref kind) => match **kind {
516 Kind::ZoneInfo(ref db) => db.get(name),
517 Kind::Concatenated(ref db) => db.get(name),
518 },
519 };
520 if let Some(tz) = found {
521 trace!("found time zone `{name}` in {db:?}", db = self);
522 return Ok(tz);
523 }
524 Err(Error::from(E::failed_time_zone(name)))
525 }
526
527 /// Returns a list of all available time zone identifiers from this
528 /// database.
529 ///
530 /// Note that time zone identifiers are more of a machine readable
531 /// abstraction and not an end user level abstraction. Still, users
532 /// comfortable with configuring their system's default time zone through
533 /// IANA time zone identifiers are probably comfortable interacting with
534 /// the identifiers returned here.
535 ///
536 /// # Example
537 ///
538 /// ```no_run
539 /// use jiff::tz;
540 ///
541 /// for tzid in tz::db().available() {
542 /// println!("{tzid}");
543 /// }
544 /// ```
545 pub fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
546 match self.inner {
547 Repr::Empty => TimeZoneNameIter::empty(),
548 Repr::Bundled(ref db) => db.available(),
549 Repr::Arc(ref kind) => match **kind {
550 Kind::ZoneInfo(ref db) => db.available(),
551 Kind::Concatenated(ref db) => db.available(),
552 },
553 }
554 }
555
556 /// Resets the internal cache of this database.
557 ///
558 /// Subsequent interactions with this database will need to re-read time
559 /// zone data from disk.
560 ///
561 /// It might be useful to call this if you know the time zone database
562 /// has changed on disk and want to force Jiff to re-load it immediately
563 /// without spawning a new process or waiting for Jiff's internal cache
564 /// invalidation heuristics to kick in.
565 pub fn reset(&self) {
566 match self.inner {
567 Repr::Empty => {}
568 Repr::Bundled(ref db) => db.reset(),
569 Repr::Arc(ref kind) => match **kind {
570 Kind::ZoneInfo(ref db) => db.reset(),
571 Kind::Concatenated(ref db) => db.reset(),
572 },
573 }
574 }
575
576 /// Returns true if it is known that this time zone database is empty.
577 ///
578 /// When this returns true, it is guaranteed that all
579 /// [`TimeZoneDatabase::get`] calls will fail, and that
580 /// [`TimeZoneDatabase::available`] will always return an empty iterator.
581 ///
582 /// Note that if this returns false, it is still possible for this database
583 /// to be empty.
584 ///
585 /// # Example
586 ///
587 /// ```
588 /// use jiff::tz::TimeZoneDatabase;
589 ///
590 /// let db = TimeZoneDatabase::none();
591 /// assert!(db.is_definitively_empty());
592 /// ```
593 pub fn is_definitively_empty(&self) -> bool {
594 match self.inner {
595 Repr::Empty => true,
596 Repr::Bundled(ref db) => db.is_definitively_empty(),
597 Repr::Arc(ref kind) => match **kind {
598 Kind::ZoneInfo(ref db) => db.is_definitively_empty(),
599 Kind::Concatenated(ref db) => db.is_definitively_empty(),
600 },
601 }
602 }
603}
604
605impl core::fmt::Debug for TimeZoneDatabase {
606 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
607 f.write_str("TimeZoneDatabase(")?;
608 match self.inner {
609 Repr::Empty => return f.write_str("unavailable)"),
610 Repr::Bundled(ref db) => core::fmt::Debug::fmt(db, f)?,
611 Repr::Arc(ref kind) => match **kind {
612 Kind::ZoneInfo(ref db) => core::fmt::Debug::fmt(db, f)?,
613 Kind::Concatenated(ref db) => core::fmt::Debug::fmt(db, f)?,
614 },
615 }
616 f.write_str(")")
617 }
618}
619
620#[cfg(feature = "defmt")]
621impl defmt::Format for TimeZoneDatabase {
622 fn format(&self, f: defmt::Formatter) {
623 // `Kind` doesn't implement `defmt::Format`, so we only emit the type
624 // name. On embedded targets the database is usually bundled or unused,
625 // so the internal backend probably isn't meaningful to log either way.
626 defmt::write!(f, "TimeZoneDatabase(unavailable)");
627 }
628}
629
630/// An iterator over the time zone identifiers in a [`TimeZoneDatabase`].
631///
632/// This iterator is created by [`TimeZoneDatabase::available`].
633///
634/// There are no guarantees about the order in which this iterator yields
635/// time zone identifiers.
636///
637/// The lifetime parameter corresponds to the lifetime of the
638/// `TimeZoneDatabase` from which this iterator was created.
639#[derive(Clone, Debug)]
640#[cfg_attr(feature = "defmt", derive(defmt::Format))]
641pub struct TimeZoneNameIter<'d> {
642 #[cfg(feature = "alloc")]
643 it: alloc::vec::IntoIter<TimeZoneName<'d>>,
644 #[cfg(not(feature = "alloc"))]
645 it: core::iter::Empty<TimeZoneName<'d>>,
646}
647
648impl<'d> TimeZoneNameIter<'d> {
649 /// Creates a time zone name iterator that never yields any elements.
650 fn empty() -> TimeZoneNameIter<'d> {
651 #[cfg(feature = "alloc")]
652 {
653 TimeZoneNameIter { it: alloc::vec::Vec::new().into_iter() }
654 }
655 #[cfg(not(feature = "alloc"))]
656 {
657 TimeZoneNameIter { it: core::iter::empty() }
658 }
659 }
660
661 /// Creates a time zone name iterator that yields the elements from the
662 /// iterator given. (They are collected into a `Vec`.)
663 #[cfg(feature = "alloc")]
664 fn from_iter(
665 it: impl Iterator<Item = impl Into<alloc::string::String>>,
666 ) -> TimeZoneNameIter<'d> {
667 let names: alloc::vec::Vec<TimeZoneName<'d>> =
668 it.map(|name| TimeZoneName::new(name.into())).collect();
669 TimeZoneNameIter { it: names.into_iter() }
670 }
671}
672
673impl<'d> Iterator for TimeZoneNameIter<'d> {
674 type Item = TimeZoneName<'d>;
675
676 fn next(&mut self) -> Option<TimeZoneName<'d>> {
677 self.it.next()
678 }
679}
680
681/// A name for a time zone yield by the [`TimeZoneNameIter`] iterator.
682///
683/// The iterator is created by [`TimeZoneDatabase::available`].
684///
685/// The lifetime parameter corresponds to the lifetime of the
686/// `TimeZoneDatabase` from which this name was created.
687#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
688#[cfg_attr(feature = "defmt", derive(defmt::Format))]
689pub struct TimeZoneName<'d> {
690 /// The lifetime of the tzdb.
691 ///
692 /// We don't currently use this, but it could be quite useful if we ever
693 /// adopt a "compile time" tzdb like what `chrono-tz` has. Then we could
694 /// return strings directly from the embedded data. Or perhaps a "compile
695 /// time" TZif or some such.
696 lifetime: core::marker::PhantomData<&'d str>,
697 #[cfg(feature = "alloc")]
698 name: alloc::string::String,
699 #[cfg(not(feature = "alloc"))]
700 name: core::convert::Infallible,
701}
702
703impl<'d> TimeZoneName<'d> {
704 /// Returns a new time zone name from the string given.
705 ///
706 /// The lifetime returned is inferred according to the caller's context.
707 #[cfg(feature = "alloc")]
708 fn new(name: alloc::string::String) -> TimeZoneName<'d> {
709 TimeZoneName { lifetime: core::marker::PhantomData, name }
710 }
711
712 /// Returns this time zone name as a borrowed string.
713 ///
714 /// Note that the lifetime of the string returned is tied to `self`,
715 /// which may be shorter than the lifetime `'d` of the originating
716 /// `TimeZoneDatabase`.
717 #[inline]
718 pub fn as_str<'a>(&'a self) -> &'a str {
719 #[cfg(feature = "alloc")]
720 {
721 self.name.as_str()
722 }
723 #[cfg(not(feature = "alloc"))]
724 {
725 // Can never be reached because `TimeZoneName` cannot currently
726 // be constructed in core-only environments.
727 unreachable!()
728 }
729 }
730}
731
732impl<'d> core::fmt::Display for TimeZoneName<'d> {
733 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
734 f.write_str(self.as_str())
735 }
736}
737
738/// Checks if `name` is a "special" time zone and returns one if so.
739///
740/// This is limited to special constants that should have consistent values
741/// across time zone database implementations. For example, `UTC`.
742fn special_time_zone(name: &str) -> Option<TimeZone> {
743 if utf8::cmp_ignore_ascii_case("utc", name).is_eq() {
744 return Some(TimeZone::UTC);
745 }
746 if utf8::cmp_ignore_ascii_case("etc/unknown", name).is_eq() {
747 return Some(TimeZone::unknown());
748 }
749 None
750}
751
752#[cfg(test)]
753mod tests {
754 use super::*;
755
756 /// Tests that the global database returns the bundled tzdb when the bundle
757 /// is enabled but neither the zoneinfo nor concatenated databases are.
758 ///
759 /// This configuration doesn't have `std` (since both `tzdb-zoneinfo` and
760 /// `tzdb-concatenated` imply it), and so without special handling `db()`
761 /// used to return an empty database despite the tzdb being compiled in.
762 ///
763 /// Regression test for: https://github.com/BurntSushi/jiff/issues/533
764 #[cfg(all(
765 feature = "tzdb-bundle-always",
766 not(feature = "tzdb-zoneinfo"),
767 not(feature = "tzdb-concatenated"),
768 ))]
769 #[test]
770 fn bundled_db_when_only_bundle_enabled() {
771 let db = db();
772 assert!(!db.is_definitively_empty());
773 assert!(db.get("America/New_York").is_ok());
774 // The convenience APIs route through the global `db()`, so this is
775 // the symptom originally reported in #533.
776 assert!(crate::civil::date(2024, 7, 4)
777 .at(12, 0, 0, 0)
778 .in_tz("America/New_York")
779 .is_ok());
780 }
781
782 /// This tests that the size of a time zone database is kept small.
783 ///
784 /// It's two words because the bundled database is stored outside the
785 /// `Arc` (so it can be constructed in a `const`, see `db()` and #533),
786 /// which means the representation needs a tag to distinguish it from the
787 /// empty and `Arc`-backed variants.
788 ///
789 /// I think it would probably be okay to make this bigger if we had a
790 /// good reason to, but it seems sensible to put a road-block to avoid
791 /// accidentally increasing its size.
792 #[test]
793 fn time_zone_database_size() {
794 #[cfg(feature = "alloc")]
795 {
796 let word = core::mem::size_of::<usize>();
797 assert_eq!(2 * word, core::mem::size_of::<TimeZoneDatabase>());
798 }
799 // A `TimeZoneDatabase` in core-only is vapid.
800 #[cfg(not(feature = "alloc"))]
801 {
802 assert_eq!(1, core::mem::size_of::<TimeZoneDatabase>());
803 }
804 }
805
806 /// Time zone databases should always return `TimeZone::UTC` if the time
807 /// zone is known to be UTC.
808 ///
809 /// Regression test for: https://github.com/BurntSushi/jiff/issues/346
810 #[test]
811 fn bundled_returns_utc_constant() {
812 let db = TimeZoneDatabase::bundled();
813 if db.is_definitively_empty() {
814 return;
815 }
816 assert_eq!(db.get("UTC").unwrap(), TimeZone::UTC);
817 assert_eq!(db.get("utc").unwrap(), TimeZone::UTC);
818 assert_eq!(db.get("uTc").unwrap(), TimeZone::UTC);
819 assert_eq!(db.get("UtC").unwrap(), TimeZone::UTC);
820
821 // Also, similarly, for `Etc/Unknown`.
822 assert_eq!(db.get("Etc/Unknown").unwrap(), TimeZone::unknown());
823 assert_eq!(db.get("etc/UNKNOWN").unwrap(), TimeZone::unknown());
824 }
825
826 /// Time zone databases should always return `TimeZone::UTC` if the time
827 /// zone is known to be UTC.
828 ///
829 /// Regression test for: https://github.com/BurntSushi/jiff/issues/346
830 #[cfg(all(feature = "std", not(miri)))]
831 #[test]
832 fn zoneinfo_returns_utc_constant() {
833 let Ok(db) = TimeZoneDatabase::from_dir("/usr/share/zoneinfo") else {
834 return;
835 };
836 if db.is_definitively_empty() {
837 return;
838 }
839 assert_eq!(db.get("UTC").unwrap(), TimeZone::UTC);
840 assert_eq!(db.get("utc").unwrap(), TimeZone::UTC);
841 assert_eq!(db.get("uTc").unwrap(), TimeZone::UTC);
842 assert_eq!(db.get("UtC").unwrap(), TimeZone::UTC);
843
844 // Also, similarly, for `Etc/Unknown`.
845 assert_eq!(db.get("Etc/Unknown").unwrap(), TimeZone::unknown());
846 assert_eq!(db.get("etc/UNKNOWN").unwrap(), TimeZone::unknown());
847 }
848
849 /// This checks that our zoneinfo database never returns a time zone
850 /// identifier that isn't presumed to correspond to a real and valid
851 /// TZif file in the tzdb.
852 ///
853 /// This test was added when I optimized the initialized of Jiff's zoneinfo
854 /// database. Originally, it did a directory traversal along with a 4-byte
855 /// read of every file in the directory to check if the file was TZif or
856 /// something else. This turned out to be quite slow on slow file systems.
857 /// I rejiggered it so that the reads of every file were removed. But this
858 /// meant we could have loaded a name from a file that wasn't TZif into
859 /// our in-memory cache.
860 ///
861 /// For doing a single time zone lookup, this isn't a problem, since we
862 /// have to read the TZif data anyway. If it's invalid, then we just
863 /// return `None` and log a warning. No big deal.
864 ///
865 /// But for the `TimeZoneDatabase::available()` API, we were previously
866 /// just returning a list of names under the presumption that every such
867 /// name corresponds to a valid TZif file. This test checks that we don't
868 /// emit junk. (Which was in practice accomplished to moving the 4-byte
869 /// read to when we call `TimeZoneDatabase::available()`.)
870 ///
871 /// Ref: https://github.com/BurntSushi/jiff/issues/366
872 #[cfg(all(feature = "std", not(miri)))]
873 #[test]
874 fn zoneinfo_available_returns_only_tzif() {
875 use alloc::{
876 collections::BTreeSet,
877 string::{String, ToString},
878 };
879
880 let Ok(db) = TimeZoneDatabase::from_dir("/usr/share/zoneinfo") else {
881 return;
882 };
883 if db.is_definitively_empty() {
884 return;
885 }
886 let names: BTreeSet<String> =
887 db.available().map(|n| n.as_str().to_string()).collect();
888 // Not all zoneinfo directories are created equal. Some have more or
889 // less junk than others. So just try a few things.
890 let should_be_absent = [
891 "leapseconds",
892 "tzdata.zi",
893 "leap-seconds.list",
894 "SECURITY",
895 "zone1970.tab",
896 "iso3166.tab",
897 "zonenow.tab",
898 "zone.tab",
899 ];
900 for name in should_be_absent {
901 assert!(
902 !names.contains(name),
903 "found `{name}` in time zone list, but it shouldn't be there",
904 );
905 }
906 }
907}