jiff::zoned

Struct ZonedInner

Source
struct ZonedInner {
    timestamp: Timestamp,
    datetime: DateTime,
    offset: Offset,
    time_zone: TimeZone,
}
Expand description

The representation of a Zoned.

This uses 4 different things: a timestamp, a datetime, an offset and a time zone. This in turn makes Zoned a bit beefy (40 bytes on x86-64), but I think this is probably the right trade off. (At time of writing, 2024-07-04.)

Technically speaking, the only essential fields here are timestamp and time zone. The datetime and offset can both be unambiguously computed from the combination of a timestamp and a time zone. Indeed, just the timestamp and the time zone was my initial representation. But as I developed the API of this type, it became clearer that we should probably store the datetime and offset as well.

The main issue here is that in order to compute the datetime from a timestamp and a time zone, you need to do two things:

  1. First, compute the offset. This means doing a binary search on the TZif data for the transition (or closest transition) matching the timestamp.
  2. Second, use the offset (from UTC) to convert the timestamp into a civil datetime. This involves a “Unix time to Unix epoch days” conversion that requires some heavy arithmetic.

So if we don’t store the datetime or offset, then we need to compute them any time we need them. And the Temporal design really pushes heavily in favor of treating the “instant in time” and “civil datetime” as two sides to the same coin. That means users are very encouraged to just use whatever they need. So if we are always computing the offset and datetime whenever we need them, we’re potentially punishing users for working with civil datetimes. It just doesn’t feel like the right trade-off.

Instead, my idea here is that, ultimately, Zoned is meant to provide a one-stop shop for “doing the right thing.” Presenting that unified abstraction comes with costs. And that if we want to expose cheaper ways of performing at least some of the operations on Zoned by making fewer assumptions, then we should probably endeavor to do that by exposing a lower level API. I’m not sure what that would look like, so I think it should be driven by use cases.

Some other things I considered:

  • Use Zoned(Arc<ZonedInner>) to make Zoned pointer-sized. But I didn’t like this because it implies creating any new Zoned value requires an allocation. Since a TimeZone internally uses an Arc, all it requires today is a chunky memcpy and an atomic ref count increment.
  • Use OnceLock shenanigans for the datetime and offset fields. This would make Zoned even beefier and I wasn’t totally clear how much this would save us. And it would impose some (probably small) cost on every datetime or offset access.
  • Use a radically different design that permits a Zoned to be Copy. I personally find it deeply annoying that Zoned is both the “main” datetime type in Jiff and also the only one that doesn’t implement Copy. I explored some designs, but I couldn’t figure out how to make it work in a satisfying way. The main issue here is TimeZone. A TimeZone is a huge chunk of data and the ergonomics of the Zoned API require being able to access a TimeZone without the caller providing it explicitly. So to me, the only real alternative here is to use some kind of integer handle into a global time zone database. But now you all of a sudden need to worry about synchronization for every time zone access and plausibly also garbage collection. And this also complicates matters for using custom time zone databases. So I ultimately came down on “Zoned is not Copy” as the least awful choice. heavy sigh

Fields§

§timestamp: Timestamp§datetime: DateTime§offset: Offset§time_zone: TimeZone

Trait Implementations§

Source§

impl Clone for ZonedInner

Source§

fn clone(&self) -> ZonedInner

Returns a copy of the value. Read more
1.0.0 · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dst: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dst. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.