The big ones here are:
1. Using Neri-Schneider to convert to-and-from Unix epoch days.
2. Add a bit set to `Span` to make it cheap to determine which units
are non-zero. We then use this bit set to enable fast paths in
routines that do arithmetic with `Span`.
There's a host of other junk here too. For example, `Timestamp::series`
now converts its `Span` to a `SignedDuration` and uses that to do
arithmetic instead of `Span`. And adding a `SignedDuration` to a
`Timestamp` is now faster because we avoid doing redundant checks by
skipping `Timestamp`'s constructor.
... and a lot more of that in a similar vein.
This overall results in better performance than both `chrono` and `time`
in *most* cases.
Fixes#235, Fixes#255
I think someone asked for this when `jiff 0.1` was released, and I
thought it was a good idea then. But I hadn't gotten around to it. With
the number of formats Jiff supports having expanded a bit since then, I
think now is a good time to get this documented.
Basically, anywhere we linked to the `icu` crate, we now also include a
mention of `jiff-icu` to facilitate conversions. And we update our
comparison example to include use of this crate.
Basically, instead of just logging a warning and falling back to `UTC`,
we now still log a warning and fall back to a time zone that _behaves_
as `UTC`, but is identified as `Etc/Unknown`. This avoids unrecoverable
failure while still also surfacing the fact that "something" has gone
wrong somewhere.
While doing this, I also fixed a bug where Jiff would perform offset
conflict resolution for `Z` offsets. But it shouldn't do that, since `Z`
reflects an unknown offset. In that case, we always respect the `Z`
offset (as numerically equivalent to `+00:00`) in order to resolve the
instant, and then use the time zone to compute the offset for that
instant unambiguously.
Ref https://github.com/tc39/proposal-canonical-tz/pull/25Closes#230
Basically, if callers opt into days being invariant---and thus no
relative date is required to resolve its length---then weeks are also
treated as invariant.
Temporal doesn't do this. As I understand it, I think the reasoning is
that they might some day support calendars that don't use 7 day weeks.
However, I think it's still pretty unlikely for Jiff to support
non-Gregorian calendars (other than things like the ISO 8601 week date
calendar). And moreover, I believe the only calendars to use weeks that
aren't 7 days are ancient calendars. I believe all actively used
calendars use 7 day weeks. So for this assumption to be wrong, Jiff
would not only need to support non-Gregorian calendars but _ancient_
non-Gregorian calendars. I think that's probably never going to happen
and is best left to specialty crates.
Because of that, I think it's say to support invariant weeks *when* the
caller opts into invariant days.
Closes#136
This makes the `Span` APIs more consistent with the rest of Jiff in that
days are never silently assumed to be 24 hours. Instead, callers are
forced to either provide a relative date or a special
`SpanRelativeTo::days_are_24_hours()` marker to opt into invariant
24-hour days.
This is meant to make the API more uniform and reduce the possibility of
bugs. In particular, in #48, it was brought up that it is very easy to
assume that Jiff APIs will never let you silently assume that days are
always 24 hours. This is actually quite easy to do if you're just
dealing with `Span` APIs.
Fixes#48
This started with wanting to get rid of the `(offset, dst, abbrev)`
triple in the `to_offset` API. Specifically, I thought that returning
a `&str` tied to the time zone for the abbreviation was somewhat
limiting from an API evolution perspective, particularly in core-only
environments. Specifically, in core-only environments, we only support
fixed offset time zones, and in order to provide the `&str` API, we had
to pre-compute the string representation of that offset and store it on
the `TimeZone`. Since there's no indirection in `TimeZone` in core-only
environments, this bloated the size of it, and consequently `Zoned` as
well.
By changing this to an opaque type with a more restrictive lifetime, we
can compute the string "later."
But... we really don't want to be doing this every single time we want
the offset for an instant. And moreover, even in non-core environments,
this code path was doing more work than is necessary (albeit not much)
to return the DST status and time zone abbreviation strings. It's also
just more data to shuffle around.
So I split `to_offset` into two APIs: one for common use cases and one
that returns the data that `to_offset` returned previously. This
required a fair bit of rejiggering, but nothing substantial changed.
Also, I changed the lifetime of the abbreviation returned by
`TimeZoneTransition::abbreviation` to be tied to the transition. This
also affords us some API flexibility in the future for similar reasons
as above.
Fixes#222
This should be more friendly to future API evolution here. I previously
did the simplest thing possible and wasn't really thinking about
core-only environments.
We still don't support any tzdb in core only environments, but we
theoretically could in the future. (Although there are a number of
issues to figure out. This merely fixes one of them.)
Fixes#221
Jiff has three different kinds of databases that it can draw time zone
transitions from: the standard Unix zoneinfo database, a bundled or
embedded zoneinfo database (compiled into the binary), or the special
Android concatenated zoneinfo database. Previously, we would try to
create as many of these databases as possible, and then look all time
zones up in each.
I think that this type of semantic is very messy, because you can wind
up drawing from one db for one time zone and another db for another
(although in theory this shouldn't happen if they're all in sync). It
also requires that you always look for all three, which feels wrong.
Instead, we now just look for one and stop when we find it. Effectively,
we changed the internals from a product to a sum.
On Android, we check for a concatenated tzdb first, since that's likely
what we'll find.
Fixes#213
This makes the naming with APIs like `Zoned::duration_since` a bit more
consistent. And solidifies `SignedDuration` as Jiff's primary
non-calendar duration type.
I'm open to adding `Span::to_unsigned_duration` in the future, but let's
see how far we can get with what we have. Note that
`TryFrom<Span> for std::time::Duration` still exists, although that of
course only works for non-calendar spans since there is no way to
provide a relative reference time.
Fixes#220
This will cause Jiff to reject some datetime strings that it
previously accepted. This also brings Jiff into line with how
Temporal behaves in these case. It is overall safe because
it avoids misinterpreting datetimes in the future that are
serialized before a change to DST. For example, imagine serializing
`2006-04-02T02:30-05[America/Indiana/Vevay]` before you knew that
Indiana/Vevay was going to switch to DST (they hadn't previously used
DST since 1972). Before that switch, it was perfectly reasonable to
think that the datetime was valid. But after the switch, it corresponded
to a gap. Jiff will now reject this.
Users can still of course change the offset conflict resolution strategy
depending on what they want to do (reject, keep exact time or keep civil
time).
Fixes#212
This officially drops the deprecated behavior of `%V`, which
corresponded to an IANA time zone identifier. This was done to improve
compatibility with other `strftime` and `strptime` implementations, such
as the one found in GNU libc.
Fixes#147
Previously, it was an API bug for this routine to be infallible since we
specifically disallow adding calendar spans on `Timestamp`. To paper
over this, we made the API panic in such cases. But now we properly
return an error.
It is lamentable that an API like this is fallible. And the generics
means that even adding a `SignedDuration`, which ought to be infallible,
callers still need to deal with the possible error path.
Fixes#36
I meant to include this in #234, but it slipped my notice.
Basically, if two offsets compare equal as-is, then it should always
return true. Before this, if our parsed offset was `-00:44:30` and the
time zone offset was `-00:44:30`, we were returning them as not equal
because we skipped to rounding the latter to the nearest minute.
I fixed this in the parser implementation but forgot to fix it in the
doc example. That's what this commit does.
This new routine is like `resolve`, but permits callers to control
how and when two offsets are considered equal. This is then in turn
used by our Temporal datetime parser to permit parsing of offsets
that have been rounded to the nearest minute without rejecting them
for being unequal.
Fixes#231
This is to help support `Offset` equality that rounds to the nearest
minute. It's also pretty easy to add since we can just reuse
`SignedDuration::round`.
We also add `TryFrom<SignedDuration> for Offset` and
`From<Offset> for SignedDuration`.
Closes#233
And, also, add lower level routines to `jiff::fmt::temporal` for
printing and parsing a `TimeZone` without going through Serde.
For now, we don't implement `Serialize` or `Deserialize` on `TimeZone`
directly. Instead, we add `with` helpers to `jiff::fmt::serde::tz`. This
is because it's not clear if this is an appropriate "universal"
serialization for a `TimeZone`, and especially since for some (very
rare) cases, a `TimeZone` cannot be serialized succinctly. I also think
it's somewhat _odd_ that a `Deserialize` routine will do an implicit
global tzdb lookup. But it's not really clear that that is an issue in
practice, especially since we're already doing that for parsing `Zoned`
values.
Closes#89
This PR adds the following methods to `jiff::civil::ISOWeekDate`:
* `ISOWeekDate::first_of_week`
* `ISOWeekDate::last_of_week`
* `ISOWeekDate::first_of_year`
* `ISOWeekDate::last_of_year`
* `ISOWeekDate::days_in_year`
* `ISOWeekDate::weeks_in_year`
* `ISOWeekDate::tomorrow`
* `ISOWeekDate::yesterday`
This makes it a bit easier to navigate around the calendar without
having to do this manually.
In #227, I originally proposed adding `ISOWeekDate::series` as well, but
as I implemented it, I think it turned out to be a bad idea. In
particular, `Span` is really a _Gregorian_ period of time, and applying
it to `ISOWeekDate` can be awkward. It works fine for units of weeks or
lower, but above it's weird. For months, one might excuse it since it's
"obvious" that a month will be applied on the Gregorian interpretation
of a week date since the ISO 8601 week calendar has no concept of
months. But for years... things get weird. If you ask for a series of 1
year intervals starting at `2026-W01-Monday`, then 1 year later is
actually `2026-W53-Tuesday` instead of the expected `2027-W01-Monday`.
This is because the year arithmetic is done on the Gregorian date and
`2026-W01-Monday` is `2025-12-29`. Thus, one year later is `2026-12-29`,
which is in turn `2026-W53-Tuesday`.
This is just overall begging for bugs. The only way I can see to offer a
series-like API for `ISOWeekDate` is to specifically implement ISO week
arithmetic. So, I guess we'd return an error for non-zero units of
months (thus making the `ISOWeekDate::series` API fallible) and use the
ISO week interpretation of "years" instead of the Gregorian
interpretation. But this is a fair bit of infrastructure that I'm not
sure is worth having. So I've left it out.
On the upside, it is very easy to map back and forth between `Date` and
`ISOWeekDate`. And you can still use `Date::series` for this, and things
will generally make sense as long as you stick to units of weeks or
lower. But this does at least make it clear that the series is
calculated on the Gregorian calendar and not the ISO 8601 week calendar.
Closes#227
... and replace it with serde-yaml. This is non-ideal since serde-yaml
is deprecated, but I'll take that over the shenanigans going on with
serde-yml (that I was unaware of, since this was a dev-dependency).
Ref https://x.com/davidtolnay/status/1883906113428676938
When doing timestamp arithmetic, we were special casing a path where if
there weren't any fractional seconds, could skip a fair bit of math.
However, we were only checking whether the span given lacked fractional
seconds. We also need to check if the timestamp does. If either have
non-zero fractional seconds, then we can take the optimized path. This
was resulting in apparent precision loss.
Fixes#223
I had always intended to expose the ability to parse a prefix of some
string via Jiff's `strptime`-like API, but I wanted a use case first to
make sure the API was well motivated. So this is mostly just exposing
what was already there and documenting it.
Closes#209
This PR tweaks how the "prefer offset" conflict resolution works.
Previously, it could produce some unintuitive results when dealing with
DST gaps. (See #211 for an example.) And in particular, Jiff's behavior
differed from Temporal. We change the implementation to match Temporal's
behavior in the case presented in #211.
We also add a test demonstrating a case where Jiff differs from
Temporal, and this perhaps might be correct. See
https://github.com/tc39/proposal-temporal/issues/3078 for more
discussion on this point.
Fixes#211
I think this helps make the naming a little more consistent.
We still have `Date::to_zoned` and `Date::to_datetime` (instead of just
`Date::zoned` and `Date::datetime`), but I think those makes sense. At
least, it feels wrong to drop the `to_` prefix there.
I honestly kind of feel like `Date::to_iso_week_date` and
`ISOWeekDate::to_date` are also right, but for `Zoned`,
`to_iso_week_date` makes less sense? And I want `Zoned` to use the same
names of methods that are available on `DateTime` and `Date`.
This adds a whole bunch of conversion specifiers from reading
`man strftime` on my system (GNU libc).
I originally didn't add most of these because they bloat the size of
`BrokenDownTime` and many of them seem a little dubious. But a few folks
have filed issues about compatibility. Given the `strtime` API is
already a complete clusterfuck, it probably makes sense to just match
existing practice as much as we can.
One conversion specifier I intend to add but haven't yet is `%V` for the
ISO 8601 week number. It is conspicuously absent here since `%V` is
currently used for IANA time zone identifiers. That will change in
`jiff 0.2`, which will make room for `%V` to be the ISO 8601 week
number.
Ref #139, Ref #147
And similarly, deprecate `%:V` in favor of `%:Q`.
This was regretably done to improve compatibility with other strtime
implementations. In particular, `%V` will, in `jiff 0.2`, correspond to
an ISO 8601 week number. This matches other `strftime` and `strptime`
implementations, such as GNU's.
Ref #147
This does make _some_ use cases a bit more annoying, but I think it
overall makes the API more consistent. Essentially, unless
`SpanRelativeTo::days_are_24_hours` are provided, then Jiff will always
consider days to be a variable unit like weeks, months and years.
Previously, this would only happen _some_ of the time.
For now, when that assumption is silently made, Jiff will emit a
deprecation warning. This will turn into a hard error in `jiff 0.2`.
Ref #48
Pretty much what it says on the tin. The idea here is that two different
`Span` values in memory, which compare not equal, might still be the
same duration. Obviously I knew this when I added the trait impl
originally, and realized it could be a footgun, but I thought its
convenience outweighed the footgun. However, I think #32 pretty
convincingly argues that it's the wrong default.
If this ends up being the wrong decision, we can always add the trait
impl back. In particular, I am worried about this making `Span` a very
annoying-to-use type. Not implementing basic equality is just super
annoying because it's common to want it in tests and other various
things where the footgun isn't really relevant. But at least this way,
we can fix the mistake in the future.
Ref #32
In most cases we just squash the warning instead of cfg'ing out the
code. It is just super painful to correctly annotate everything piece of
code otherwise.
As ISO-Week is Monday == day 1 based system, it would be nice to
give an example that matches the ISO 8601 standard. So instead
of only mentioning `Weekday::to_sunday_zero_offset`, we mention
`Weekday::to_monday_one_offset` as well.
Closes#196
This is just forwarding to `Date::to_iso_week_date`. It matches the
pattern already established. I think this didn't exist already either
because of an oversight or because I was unsure if it was worth
including. But since basically everything else is included, it ends up
being weird. And at least one user had trouble finding the ISO week date
functionality because of this.
Closes#197
I guess some platforms use `*const u8` for C strings while most
seemingly use `*const i8`. And indeed, `CStr::as_ptr` returns a
`*const u8` or a `*const i8` depending on the platform. So do the right
thing here and use std's `c_char` alias.
Ref #140Fixes#200
Android support has two prongs:
* The special Android concatenated time zone database will now be read
by Jiff automatically.
* The `persist.sys.timezone` Android property is read to determine the
system's configured IANA time zone identifier.
Closes#140
This commit adds a new type to the `jiff::fmt::temporal` module that
exposes the raw components of a parsed Temporal ISO 8601 datetime
string.
This is meant to address use cases that need something a bit more
flexible than what is provided by the higher level parsing routines.
Namely, the higher level routines go out of their way to stop you from
shooting yourself in the foot. For example, parsing into a `Zoned`
requires a time zone annotation.
But parsing into a `Pieces` doesn't require any of that. It just has
to match the Temporal ISO 8601 grammar. Then you can mix and match
the pieces in whatever way you desire. And indeed, you can pretty
easily shoot yourself in the foot with this API. I feel okay about this
because it's tucked into a corner of Jiff that you specifically have to
seek out to use. I've also included examples in the docs of _how_ you
can easily shoot yourself in the foot.
I've included a couple case studies in the docs reflecting some real
world examples I've come across in the issue tracker.
Ref #112, Ref #181, Closes#188,
Ref https://github.com/tc39/proposal-temporal/issues/2930
This somewhat revives #22, but makes it possible to restore the previous
behavior by enabling `jiff::fmt::temporal::SpanPrinter::lowercase`.
The main motivation here is also detailed in #22, and it came up again
in #188. I was previously reluctant to do this because I find
`P1Y2M3DT4H5M6S` hideously difficult to read and `P1y2m3dT4h5m6s`
somewhat less difficult to read. But now that `jiff::fmt::friendly` is a
thing and users have easy access to a more readable duration display
format, I feel less bad about this. It's still a shame that it's the
default via `span.to_string()`, but I tried to sprinkle a few
`format!("{span:#}")` in places to nudge users toward the friendly
format.
It's a shame more systems don't accept lowercase unit designator labels,
but since Jiff uses the ISO 8601 by default specifically for its
interoperability, it makes sense to be as interoperable as we can by
default.
Fixes#188
This adds a new `SignedDuration::round` method that behaves like `Span`,
but for time (non-calendar) units only.
For the most part, we just copy `Span::round` as-is, but without the
`largest` unit knob (which doesn't make sense for something like
`SignedDuration`) or the `relative` datetime knob (which isn't needed
for a duration with all invariant units, as is the case for
`SignedDuration`). We can even reuse the same implementation for `Span`
rounding: we convert the duration to a 128-bit signed integer of
nanoseconds, do the rounding and then convert back (and check for
overflow).
Note that we specifically don't both with adding rounding functionality
built into APIs like `Zoned::duration_since` or
`civil::DateTime::duration_until`, unlike what we do for `Span`, since
composition works better for `SignedDuration` than it does for `Span`.
Namely, in order to round a `Span` with calendar units, you need to
provide a relative datetime. By building rounding into the datetime
arithmetic routines, we absolve the caller of needing to manage this
carefully across two distinct operations. Since `SignedDuration` only
supports invariant units, there is no purpose to providing a relative
datetime and rounding is thus fully orthogonal. Moreover, if we
overloaded methods like `Zoned::duration_since`, then they would go from
infallible to fallible, which is kind of a bummer.
Closes#187