Compare commits

...

30 commits

Author SHA1 Message Date
Andrew Gallant
61dc9bd8aa fmt: rename Decimal to Integer
Some checks failed
ci / win-msvc (push) Has been cancelled
ci / win-gnu (push) Has been cancelled
ci / msrv (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / test-all (macos, macos-latest, nightly) (push) Has been cancelled
ci / test-default (beta, ubuntu-latest, beta) (push) Has been cancelled
ci / test-default (macos, macos-latest, stable) (push) Has been cancelled
ci / test-default (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-default (stable, ubuntu-latest, stable) (push) Has been cancelled
ci / test-all (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-only-bundle (macos, macos-latest, nightly) (push) Has been cancelled
ci / test-only-bundle (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-core (macos, macos-latest, nightly) (push) Has been cancelled
ci / test-core (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-various-feature-combos (macos, macos-latest, nightly) (push) Has been cancelled
ci / test-various-feature-combos (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / cross (aarch64-linux-android) (push) Has been cancelled
ci / cross (aarch64-unknown-linux-gnu) (push) Has been cancelled
ci / cross (i686-unknown-linux-gnu) (push) Has been cancelled
ci / cross (powerpc-unknown-linux-gnu) (push) Has been cancelled
ci / cross (powerpc64-unknown-linux-gnu) (push) Has been cancelled
ci / cross (s390x-unknown-linux-gnu) (push) Has been cancelled
ci / cross (x86_64-linux-android) (push) Has been cancelled
ci / riscv32imc-unknown-none-elf (push) Has been cancelled
ci / wasm32-wasip1 (push) Has been cancelled
ci / wasm32-unknown-emscripten (push) Has been cancelled
ci / wasm32-unknown-uknown (push) Has been cancelled
ci / docsrs (push) Has been cancelled
ci / rustfmt (push) Has been cancelled
ci / generated (push) Has been cancelled
And similarly for `DecimalFormatter`.

I believe I originally called this `Decimal` because it was also used to
do fractional formatting.
2025-12-22 21:02:40 -05:00
Andrew Gallant
1467f47e36 fmt: get rid of Decimal::end
It's not used. I believe it was being used when `Decimal` was also
responsible for formatting the fractional part of a floating point
number. But it isn't needed any more.
2025-12-22 21:02:40 -05:00
Andrew Gallant
5b27b22096 fmt: simplify decimal formatter
I don't think we need to keep track of maximum digits separately for
signed and unsigned integer formatting. I think this might mean that
signed integer formatting uses an extra byte of stack space, but, it can
also have a sign. Unlike unsigned integer formatting. So no space is
wasted here.

We also remove the `force_sign` option, which Jiff does not use. And
overall simplify the signed integer formatting to just reuse the
unsigned integer formatting. And also get rid of the dumb
`checked_abs()` usage. Not sure what I was thinking.
2025-12-22 21:02:40 -05:00
Andrew Gallant
4d2041567c binary-size: remove many uses of write!
Some checks are pending
ci / msrv (push) Waiting to run
ci / examples (push) Waiting to run
ci / test-various-feature-combos (macos, macos-latest, nightly) (push) Waiting to run
ci / integrations (push) Waiting to run
ci / time-zone-init (macos-latest) (push) Waiting to run
ci / test-various-feature-combos (nightly, ubuntu-latest, nightly) (push) Waiting to run
ci / time-zone-init (ubuntu-24.04-arm) (push) Waiting to run
ci / test-default (beta, ubuntu-latest, beta) (push) Waiting to run
ci / test-default (macos, macos-latest, stable) (push) Waiting to run
ci / test-default (nightly, ubuntu-latest, nightly) (push) Waiting to run
ci / test-default (stable, ubuntu-latest, stable) (push) Waiting to run
ci / test-all (macos, macos-latest, nightly) (push) Waiting to run
ci / test-all (nightly, ubuntu-latest, nightly) (push) Waiting to run
ci / test-only-bundle (macos, macos-latest, nightly) (push) Waiting to run
ci / test-only-bundle (nightly, ubuntu-latest, nightly) (push) Waiting to run
ci / test-core (macos, macos-latest, nightly) (push) Waiting to run
ci / cross (aarch64-linux-android) (push) Waiting to run
ci / cross (aarch64-unknown-linux-gnu) (push) Waiting to run
ci / cross (i686-unknown-linux-gnu) (push) Waiting to run
ci / cross (powerpc-unknown-linux-gnu) (push) Waiting to run
ci / cross (powerpc64-unknown-linux-gnu) (push) Waiting to run
ci / cross (s390x-unknown-linux-gnu) (push) Waiting to run
ci / cross (x86_64-linux-android) (push) Waiting to run
ci / riscv32imc-unknown-none-elf (push) Waiting to run
ci / wasm32-wasip1 (push) Waiting to run
ci / wasm32-unknown-emscripten (push) Waiting to run
ci / wasm32-unknown-uknown (push) Waiting to run
ci / docsrs (push) Waiting to run
ci / rustfmt (push) Waiting to run
ci / generated (push) Waiting to run
Using `write!` unnecessarily when a simple `f.write_str` would do ends
up generating more LLVM lines on my Biff `cargo llvm-lines` benchmark.
This PR replaces those uses of `write!` with `Formatter::write_str`.
2025-12-22 15:12:28 -05:00
Andrew Gallant
21f69521cd shared: remove escaping and UTF-8 routines from shared module
With the error refactor, these are no longer used. Namely, while
switching to structured errors, I took that opportunity to slim down
errors so that we are not repeating parts of the input as often.
2025-12-22 14:28:18 -05:00
Andrew Gallant
a50f6797ce logging: small tweak to formatting
Some checks are pending
ci / msrv (push) Waiting to run
ci / examples (push) Waiting to run
ci / integrations (push) Waiting to run
ci / test-default (beta, ubuntu-latest, beta) (push) Waiting to run
ci / test-default (macos, macos-latest, stable) (push) Waiting to run
ci / test-default (nightly, ubuntu-latest, nightly) (push) Waiting to run
ci / test-default (stable, ubuntu-latest, stable) (push) Waiting to run
ci / test-all (macos, macos-latest, nightly) (push) Waiting to run
ci / test-core (macos, macos-latest, nightly) (push) Waiting to run
ci / test-core (nightly, ubuntu-latest, nightly) (push) Waiting to run
ci / test-various-feature-combos (nightly, ubuntu-latest, nightly) (push) Waiting to run
ci / test-release (push) Waiting to run
ci / cross (i686-unknown-linux-gnu) (push) Waiting to run
ci / cross (powerpc-unknown-linux-gnu) (push) Waiting to run
ci / cross (powerpc64-unknown-linux-gnu) (push) Waiting to run
ci / test-all (nightly, ubuntu-latest, nightly) (push) Waiting to run
ci / test-only-bundle (macos, macos-latest, nightly) (push) Waiting to run
ci / test-only-bundle (nightly, ubuntu-latest, nightly) (push) Waiting to run
ci / test-various-feature-combos (macos, macos-latest, nightly) (push) Waiting to run
ci / cross (aarch64-linux-android) (push) Waiting to run
ci / cross (aarch64-unknown-linux-gnu) (push) Waiting to run
ci / cross (s390x-unknown-linux-gnu) (push) Waiting to run
ci / cross (x86_64-linux-android) (push) Waiting to run
ci / riscv32imc-unknown-none-elf (push) Waiting to run
ci / wasm32-wasip1 (push) Waiting to run
ci / wasm32-unknown-emscripten (push) Waiting to run
ci / wasm32-unknown-uknown (push) Waiting to run
ci / docsrs (push) Waiting to run
ci / rustfmt (push) Waiting to run
ci / generated (push) Waiting to run
2025-12-22 10:08:33 -05:00
Andrew Gallant
0392d43064 error: add note about introspection 2025-12-22 10:08:33 -05:00
Andrew Gallant
523b55bc1a error: add Error::is_invalid_parameter predicate 2025-12-22 10:08:33 -05:00
Andrew Gallant
3cb0b6aefa error: add Error::is_crate_feature predicate
There's only a few of these, but they seem like a distinct category
from other error types. It's also nice to do this and standarize on the
specific error message.

The other reason I wanted to do this was to distinguish it from a
possible "is configuration error" category. (e.g., Trying to round a
span to the nearest year without a relative datatime.) These could also
be seen as configuration errors, but I want them to be in a separate
category I think.
2025-12-22 10:08:33 -05:00
Andrew Gallant
8e8033a29d error: add Error::is_range predicate
I'm somewhat concerned that this doesn't cover all cases, but I think
this should be a good start.
2025-12-22 10:08:33 -05:00
Andrew Gallant
259e8134ef error: implement an error chaining iterator
We previously only used this in the `core::fmt::Display` trait
implementation, so we just inlined it there. But we'll want to use it to
get the root of the chain for error predicates, so this commit does some
refactoring to provide a standalone internal iterator for the error
chain.
2025-12-22 10:08:33 -05:00
Andrew Gallant
831d3efb4d shared: move itime range error into itime module
Some checks failed
ci / test-miri (push) Has been cancelled
ci / test-various-feature-combos (macos, macos-latest, nightly) (push) Has been cancelled
ci / test-various-feature-combos (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-release (push) Has been cancelled
ci / test-doc (push) Has been cancelled
ci / test-bench (push) Has been cancelled
ci / test-fuzz (push) Has been cancelled
ci / win-msvc (push) Has been cancelled
ci / win-gnu (push) Has been cancelled
ci / msrv (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / integrations (push) Has been cancelled
ci / time-zone-init (macos-latest) (push) Has been cancelled
ci / time-zone-init (ubuntu-24.04-arm) (push) Has been cancelled
ci / time-zone-init (ubuntu-latest) (push) Has been cancelled
ci / time-zone-init (windows-latest) (push) Has been cancelled
ci / cross (aarch64-linux-android) (push) Has been cancelled
ci / cross (aarch64-unknown-linux-gnu) (push) Has been cancelled
ci / cross (i686-unknown-linux-gnu) (push) Has been cancelled
ci / cross (powerpc-unknown-linux-gnu) (push) Has been cancelled
ci / cross (powerpc64-unknown-linux-gnu) (push) Has been cancelled
ci / cross (s390x-unknown-linux-gnu) (push) Has been cancelled
ci / cross (x86_64-linux-android) (push) Has been cancelled
ci / riscv32imc-unknown-none-elf (push) Has been cancelled
ci / wasm32-wasip1 (push) Has been cancelled
ci / wasm32-unknown-emscripten (push) Has been cancelled
ci / wasm32-unknown-uknown (push) Has been cancelled
ci / docsrs (push) Has been cancelled
ci / rustfmt (push) Has been cancelled
ci / generated (push) Has been cancelled
This follows the pattern I used for TZif and POSIX time zone parsing.
2025-12-20 14:38:20 -05:00
Andrew Gallant
26202e5d4d shared: switch POSIX time zone parsing over to structured errors 2025-12-20 14:38:20 -05:00
Andrew Gallant
ac0054c72f shared: replace some uses of write!
I believe this reduces code size.
2025-12-20 14:38:20 -05:00
Andrew Gallant
f275164239 shared: remove alloc cfgs
I guess parsing a POSIX time zone doesn't actually require `alloc` any
more. That's cool.
2025-12-20 14:38:20 -05:00
Andrew Gallant
d024322b27 shared: use structured errors for TZif parsing
For this one, I defined the error type inline with it. This just felt
right and it does stay reasonably encapsulated.

I'll probably swing back and do the same for the time range error type?
2025-12-20 14:38:20 -05:00
Andrew Gallant
973876a9f1 shared: use structured errors for shared/itime 2025-12-20 14:38:20 -05:00
Andrew Gallant
3a832162be
fmt: some minor perf improvements to strptime
Some checks are pending
ci / test-various-feature-combos (macos, macos-latest, nightly) (push) Waiting to run
ci / test-various-feature-combos (nightly, ubuntu-latest, nightly) (push) Waiting to run
ci / test-release (push) Waiting to run
ci / test-doc (push) Waiting to run
ci / test-bench (push) Waiting to run
ci / test-fuzz (push) Waiting to run
ci / test-miri (push) Waiting to run
ci / win-msvc (push) Waiting to run
ci / win-gnu (push) Waiting to run
ci / msrv (push) Waiting to run
ci / examples (push) Waiting to run
ci / integrations (push) Waiting to run
ci / time-zone-init (macos-latest) (push) Waiting to run
ci / time-zone-init (ubuntu-24.04-arm) (push) Waiting to run
ci / time-zone-init (ubuntu-latest) (push) Waiting to run
ci / time-zone-init (windows-latest) (push) Waiting to run
ci / cross (aarch64-linux-android) (push) Waiting to run
ci / cross (aarch64-unknown-linux-gnu) (push) Waiting to run
ci / cross (i686-unknown-linux-gnu) (push) Waiting to run
ci / cross (powerpc-unknown-linux-gnu) (push) Waiting to run
ci / cross (powerpc64-unknown-linux-gnu) (push) Waiting to run
ci / cross (s390x-unknown-linux-gnu) (push) Waiting to run
ci / cross (x86_64-linux-android) (push) Waiting to run
ci / wasm32-unknown-emscripten (push) Waiting to run
ci / wasm32-unknown-uknown (push) Waiting to run
ci / riscv32imc-unknown-none-elf (push) Waiting to run
ci / wasm32-wasip1 (push) Waiting to run
ci / docsrs (push) Waiting to run
ci / rustfmt (push) Waiting to run
ci / generated (push) Waiting to run
This adds a little inlining back to help reclaim a small performance
regression in `strptime`. This improves the relevant benchmark from
70ns to 64ns on my machine.
2025-12-19 13:11:17 -05:00
Andrew Gallant
b8757deba8 error: switch everything over to structured errors
Some checks failed
ci / test-various-feature-combos (macos, macos-latest, nightly) (push) Has been cancelled
ci / test-various-feature-combos (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-release (push) Has been cancelled
ci / test-doc (push) Has been cancelled
ci / test-bench (push) Has been cancelled
ci / test-fuzz (push) Has been cancelled
ci / test-miri (push) Has been cancelled
ci / win-msvc (push) Has been cancelled
ci / win-gnu (push) Has been cancelled
ci / msrv (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / integrations (push) Has been cancelled
ci / time-zone-init (macos-latest) (push) Has been cancelled
ci / time-zone-init (ubuntu-24.04-arm) (push) Has been cancelled
ci / time-zone-init (ubuntu-latest) (push) Has been cancelled
ci / time-zone-init (windows-latest) (push) Has been cancelled
ci / cross (aarch64-linux-android) (push) Has been cancelled
ci / cross (aarch64-unknown-linux-gnu) (push) Has been cancelled
ci / cross (i686-unknown-linux-gnu) (push) Has been cancelled
ci / cross (powerpc-unknown-linux-gnu) (push) Has been cancelled
ci / cross (powerpc64-unknown-linux-gnu) (push) Has been cancelled
ci / cross (s390x-unknown-linux-gnu) (push) Has been cancelled
ci / cross (x86_64-linux-android) (push) Has been cancelled
ci / riscv32imc-unknown-none-elf (push) Has been cancelled
ci / wasm32-wasip1 (push) Has been cancelled
ci / wasm32-unknown-emscripten (push) Has been cancelled
ci / wasm32-unknown-uknown (push) Has been cancelled
ci / docsrs (push) Has been cancelled
ci / rustfmt (push) Has been cancelled
ci / generated (push) Has been cancelled
This was an incredibly tedious and tortuous refactor. But this removes
almost all of the "create ad hoc stringly-typed errors everywhere."

This partially makes progress toward #418, but my initial impetus for
doing this was to see if I could reduce binary size and improve
compilation times. My general target was to see if I could reduce total
LLVM lines. I tested this with [Biff] using this command in the root of
the Biff repo:

```
cargo llvm-lines --profile release-lto
```

Before this change, Biff had 768,596 LLVM lines. With this change, it
has 757,331 lines. So... an improvement, but a very modest one.

What about compilation times? This does seem to translate to---also a
modest---improvement. For compiling release builds of Biff. Before:

```
$ hyperfine -w1 --prepare 'cargo clean' 'cargo b -r'
Benchmark 1: cargo b -r
  Time (mean ± σ):      7.776 s ±  0.052 s    [User: 65.876 s, System: 2.621 s]
  Range (min … max):    7.690 s …  7.862 s    10 runs
```

After:

```
$ hyperfine -w1 --prepare 'cargo clean' 'cargo b -r'
Benchmark 1: cargo b -r
  Time (mean ± σ):      7.591 s ±  0.067 s    [User: 65.686 s, System: 2.564 s]
  Range (min … max):    7.504 s …  7.689 s    10 runs
```

What about dev builds? Before:

```
$ hyperfine -w1 --prepare 'cargo clean' 'cargo b'
Benchmark 1: cargo b
  Time (mean ± σ):      4.074 s ±  0.022 s    [User: 14.493 s, System: 1.818 s]
  Range (min … max):    4.037 s …  4.099 s    10 runs
```

After:

```
$ hyperfine -w1 --prepare 'cargo clean' 'cargo b'
Benchmark 1: cargo b
  Time (mean ± σ):      4.541 s ±  0.027 s    [User: 15.385 s, System: 2.081 s]
  Range (min … max):    4.503 s …  4.591 s    10 runs
```

Well... that's disappointing. A modest improvement to release builds,
but a fairly large regression in dev builds. Maybe it's because of the
additional hand-written impls for new structured error types? Bah.

And binary size? Normal release builds (not LTO) of Biff that were
stripped were 4,431,456 bytes before this change and 4,392,064 after.

Hopefully this will unlock other improvements to justify doing this.
Note also that this slims down a number of error messages.

[Biff]: https://github.com/BurntSushi/biff
2025-12-16 16:51:01 -05:00
Andrew Gallant
3765a52b8d fmt: add &mut dyn Write impl for jiff::fmt::Write
I was playing with using this to avoid parametric polymorphism
costs, but I couldn't find my way to a concrete benefit.
Either way, this impl should exist for people that want to
use it.
2025-12-16 16:51:01 -05:00
Andrew Gallant
42080c4e71 fmt/strtime: avoid inlining the or_else cases
These all generally call `BrokenDownTime::to_date`, which
adds a fair bit of code. Since these are the uncommon case,
let's not inline that goop.
2025-12-16 16:51:01 -05:00
Andrew Gallant
40941a5188 fmt/strtime: tighten up to_timestamp and to_date
Both of these functions are marked as `inline`, but they can
be quite beefy. Instead, set the inlineable implementation
as the common case, and put the rest behind a non-inlineable
function.
2025-12-16 16:51:01 -05:00
Andrew Gallant
3780351543 rangeint: don't try to inline error constructor 2025-12-16 16:51:01 -05:00
Andrew Gallant
54c1b2d25b error: make map_err closures as cold and non-inlineable
This doesn't seem to have much impact as shown via a
`cargo llvm-lines`. But this still seems like good sense
to have.
2025-12-16 16:51:01 -05:00
Andrew Gallant
aa361bbfc2 fmt: un-generic a function
This reduces code size somewhat.

There are probably more things like this, but I picked
this one to test its effects. It got rid of a few LLVM
lines from an LTO-compiled binary that uses Jiff. But
not much.
2025-12-16 16:51:01 -05:00
Andrew Gallant
34359896a4
fuzz: update dependencies
Some checks failed
ci / test-various-feature-combos (macos, macos-latest, nightly) (push) Has been cancelled
ci / test-various-feature-combos (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-release (push) Has been cancelled
ci / test-doc (push) Has been cancelled
ci / test-bench (push) Has been cancelled
ci / test-fuzz (push) Has been cancelled
ci / test-miri (push) Has been cancelled
ci / win-msvc (push) Has been cancelled
ci / win-gnu (push) Has been cancelled
ci / msrv (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / integrations (push) Has been cancelled
ci / time-zone-init (macos-latest) (push) Has been cancelled
ci / time-zone-init (ubuntu-24.04-arm) (push) Has been cancelled
ci / time-zone-init (ubuntu-latest) (push) Has been cancelled
ci / time-zone-init (windows-latest) (push) Has been cancelled
ci / cross (aarch64-linux-android) (push) Has been cancelled
ci / cross (aarch64-unknown-linux-gnu) (push) Has been cancelled
ci / cross (i686-unknown-linux-gnu) (push) Has been cancelled
ci / cross (powerpc-unknown-linux-gnu) (push) Has been cancelled
ci / cross (powerpc64-unknown-linux-gnu) (push) Has been cancelled
ci / cross (s390x-unknown-linux-gnu) (push) Has been cancelled
ci / cross (x86_64-linux-android) (push) Has been cancelled
ci / riscv32imc-unknown-none-elf (push) Has been cancelled
ci / wasm32-wasip1 (push) Has been cancelled
ci / wasm32-unknown-emscripten (push) Has been cancelled
ci / wasm32-unknown-uknown (push) Has been cancelled
ci / docsrs (push) Has been cancelled
ci / rustfmt (push) Has been cancelled
ci / generated (push) Has been cancelled
2025-12-12 17:35:45 -05:00
Andrew Gallant
f889e5b40a
jiff-tzdb-0.1.5
Some checks failed
ci / test-doc (push) Has been cancelled
ci / test-bench (push) Has been cancelled
ci / test-various-feature-combos (macos, macos-latest, nightly) (push) Has been cancelled
ci / test-various-feature-combos (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-release (push) Has been cancelled
ci / test-fuzz (push) Has been cancelled
ci / test-miri (push) Has been cancelled
ci / win-msvc (push) Has been cancelled
ci / win-gnu (push) Has been cancelled
ci / msrv (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / integrations (push) Has been cancelled
ci / time-zone-init (macos-latest) (push) Has been cancelled
ci / time-zone-init (ubuntu-24.04-arm) (push) Has been cancelled
ci / time-zone-init (ubuntu-latest) (push) Has been cancelled
ci / time-zone-init (windows-latest) (push) Has been cancelled
ci / cross (aarch64-linux-android) (push) Has been cancelled
ci / cross (aarch64-unknown-linux-gnu) (push) Has been cancelled
ci / cross (i686-unknown-linux-gnu) (push) Has been cancelled
ci / cross (powerpc-unknown-linux-gnu) (push) Has been cancelled
ci / cross (powerpc64-unknown-linux-gnu) (push) Has been cancelled
ci / cross (s390x-unknown-linux-gnu) (push) Has been cancelled
ci / cross (x86_64-linux-android) (push) Has been cancelled
ci / riscv32imc-unknown-none-elf (push) Has been cancelled
ci / wasm32-wasip1 (push) Has been cancelled
ci / wasm32-unknown-emscripten (push) Has been cancelled
ci / wasm32-unknown-uknown (push) Has been cancelled
ci / docsrs (push) Has been cancelled
ci / rustfmt (push) Has been cancelled
ci / generated (push) Has been cancelled
2025-12-10 20:44:39 -05:00
Andrew Gallant
4526cd2663 jiff-tzdb: update to tzdb 2025c
Ref: https://lists.iana.org/hyperkitty/list/tz-announce@iana.org/thread/TAGXKYLMAQRZRFTERQ33CEKOW7KRJVAK/
2025-12-10 20:44:22 -05:00
Addison Crump
9d7e099a7a fuzz: add initial set of fuzzer targets
Some checks failed
ci / test-various-feature-combos (macos, macos-latest, nightly) (push) Has been cancelled
ci / test-various-feature-combos (nightly, ubuntu-latest, nightly) (push) Has been cancelled
ci / test-release (push) Has been cancelled
ci / test-doc (push) Has been cancelled
ci / test-bench (push) Has been cancelled
ci / test-fuzz (push) Has been cancelled
ci / test-miri (push) Has been cancelled
ci / win-msvc (push) Has been cancelled
ci / win-gnu (push) Has been cancelled
ci / msrv (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / integrations (push) Has been cancelled
ci / time-zone-init (macos-latest) (push) Has been cancelled
ci / time-zone-init (ubuntu-24.04-arm) (push) Has been cancelled
ci / time-zone-init (ubuntu-latest) (push) Has been cancelled
ci / time-zone-init (windows-latest) (push) Has been cancelled
ci / cross (aarch64-linux-android) (push) Has been cancelled
ci / cross (aarch64-unknown-linux-gnu) (push) Has been cancelled
ci / cross (s390x-unknown-linux-gnu) (push) Has been cancelled
ci / cross (i686-unknown-linux-gnu) (push) Has been cancelled
ci / cross (powerpc-unknown-linux-gnu) (push) Has been cancelled
ci / cross (powerpc64-unknown-linux-gnu) (push) Has been cancelled
ci / cross (x86_64-linux-android) (push) Has been cancelled
ci / riscv32imc-unknown-none-elf (push) Has been cancelled
ci / wasm32-wasip1 (push) Has been cancelled
ci / wasm32-unknown-emscripten (push) Has been cancelled
ci / wasm32-unknown-uknown (push) Has been cancelled
ci / docsrs (push) Has been cancelled
ci / rustfmt (push) Has been cancelled
ci / generated (push) Has been cancelled
There's a lot more that we could do, but this should be a good start.
2025-11-08 10:33:22 -05:00
Azan Ali
552b9d1fef civil: added FromStr and Display impls for ISOWeekDate
Some checks are pending
ci / test-various-feature-combos (macos, macos-latest, nightly) (push) Waiting to run
ci / test-various-feature-combos (nightly, ubuntu-latest, nightly) (push) Waiting to run
ci / test-release (push) Waiting to run
ci / test-doc (push) Waiting to run
ci / test-bench (push) Waiting to run
ci / test-miri (push) Waiting to run
ci / win-msvc (push) Waiting to run
ci / win-gnu (push) Waiting to run
ci / msrv (push) Waiting to run
ci / examples (push) Waiting to run
ci / integrations (push) Waiting to run
ci / time-zone-init (macos-latest) (push) Waiting to run
ci / time-zone-init (ubuntu-24.04-arm) (push) Waiting to run
ci / time-zone-init (ubuntu-latest) (push) Waiting to run
ci / time-zone-init (windows-latest) (push) Waiting to run
ci / test-default (beta, ubuntu-latest, beta) (push) Waiting to run
ci / cross (aarch64-linux-android) (push) Waiting to run
ci / cross (aarch64-unknown-linux-gnu) (push) Waiting to run
ci / cross (i686-unknown-linux-gnu) (push) Waiting to run
ci / cross (powerpc-unknown-linux-gnu) (push) Waiting to run
ci / cross (powerpc64-unknown-linux-gnu) (push) Waiting to run
ci / cross (s390x-unknown-linux-gnu) (push) Waiting to run
ci / cross (x86_64-linux-android) (push) Waiting to run
ci / riscv32imc-unknown-none-elf (push) Waiting to run
ci / wasm32-wasip1 (push) Waiting to run
ci / wasm32-unknown-emscripten (push) Waiting to run
ci / wasm32-unknown-uknown (push) Waiting to run
ci / docsrs (push) Waiting to run
ci / rustfmt (push) Waiting to run
ci / generated (push) Waiting to run
This also includes `Deserialize` and `Serialize` Serde impls, deferring
to `FromStr` and `Display`, respectively.

This brings `ISOWeekDate` into parity with the other datetime types in
terms of parsing/printing.

N.B. In this commit, we start trying to make error messages a little
more consistent. So there are some changes here that impact the messages
other than ISO 8601 week date parsing.

Closes #412
2025-11-07 21:38:38 -05:00
108 changed files with 10811 additions and 5768 deletions

View file

@ -229,6 +229,23 @@ jobs:
run: |
cargo bench --manifest-path bench/Cargo.toml -- --test
# Test that we can build the fuzzer targets.
#
# It's not necessary to check the fuzzers on all platforms, as this is pretty
# strictly a testing utility.
test-fuzz:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Build fuzzer targets
working-directory: ./fuzz
run: cargo build --verbose
# Runs miri on a subset of Jiff's test suite. This doesn't quite cover
# everything. In particular, `miri` and `insta` cannot play nice together,
# and `insta` is used a lot among Jiff's tests. However, the primary reason

View file

@ -2,6 +2,7 @@
"rust-analyzer.linkedProjects": [
"bench/Cargo.toml",
"crates/jiff-icu/Cargo.toml",
"fuzz/Cargo.toml",
"Cargo.toml"
]
}

View file

@ -1,5 +1,18 @@
# CHANGELOG
0.2.17 (TBD)
============
TODO
Enhancements:
* [#412](https://github.com/BurntSushi/jiff/issues/412):
Add `Display`, `FromStr`, `Serialize` and `Deserialize` trait implementations
for `jiff::civil::ISOWeekDate`. These all use the ISO 8601 week date format.
* [#418](https://github.com/BurntSushi/jiff/issues/418):
Add some basic predicates to `jiff::Error` for basic error introspection.
0.2.16 (2025-11-07)
===================
This release contains a number of enhancements and bug fixes that have accrued

View file

@ -79,7 +79,7 @@ fn copy(srcdir: &Path, dstdir: &Path, dir: &Path) -> anyhow::Result<()> {
fn copy_rust_source_file(src: &Path, dst: &Path) -> anyhow::Result<()> {
let code = fs::read_to_string(src)
.with_context(|| format!("failed to read {}", src.display()))?;
let code = remove_only_jiffs(&remove_cfg_alloc(&code));
let code = remove_only_jiffs(&remove_cfg_alloc_or_std(&code));
let mut out = String::new();
writeln!(out, "// auto-generated by: jiff-cli generate shared")?;
@ -105,13 +105,13 @@ fn remove_only_jiffs(code: &str) -> String {
RE.replace_all(code, "").into_owned()
}
/// Removes all `#[cfg(feature = "alloc")]` gates.
/// Removes all `#[cfg(feature = "alloc|std")]` gates.
///
/// This is because the proc-macro always runs in a context where `alloc`
/// (and `std`) are enabled.
fn remove_cfg_alloc(code: &str) -> String {
fn remove_cfg_alloc_or_std(code: &str) -> String {
static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r###"#\[cfg\(feature = "alloc"\)\]\n"###).unwrap()
Regex::new(r###"#\[cfg\(feature = "(alloc|std)"\)\]\n"###).unwrap()
});
RE.replace_all(code, "").into_owned()
}

View file

@ -0,0 +1,100 @@
// auto-generated by: jiff-cli generate shared
use crate::shared::{
error,
util::itime::{days_in_month, days_in_year, IEpochDay},
};
// N.B. Every variant in this error type is a range error.
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum Error {
DateInvalidDayOfYear { year: i16 },
DateInvalidDayOfYearNoLeap,
DateInvalidDays { year: i16, month: i8 },
DateTimeSeconds,
// TODO: I believe this can never happen.
DayOfYear,
EpochDayDays,
EpochDayI32,
NthWeekdayOfMonth,
Tomorrow,
YearNext,
YearPrevious,
Yesterday,
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::Time(err).into()
}
}
// impl error::IntoError for Error {
// fn into_error(self) -> error::Error {
// self.into()
// }
// }
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
DateInvalidDayOfYear { year } => write!(
f,
"number of days for `{year:04}` is invalid, \
must be in range `1..={max_day}`",
max_day = days_in_year(year),
),
DateInvalidDayOfYearNoLeap => f.write_str(
"number of days is invalid, must be in range `1..=365`",
),
DateInvalidDays { year, month } => write!(
f,
"number of days for `{year:04}-{month:02}` is invalid, \
must be in range `1..={max_day}`",
max_day = days_in_month(year, month),
),
DateTimeSeconds => {
f.write_str("adding seconds to datetime overflowed")
}
DayOfYear => f.write_str("day of year is invalid"),
EpochDayDays => write!(
f,
"adding to epoch day resulted in a value outside \
the allowed range of `{min}..={max}`",
min = IEpochDay::MIN.epoch_day,
max = IEpochDay::MAX.epoch_day,
),
EpochDayI32 => f.write_str(
"adding to epoch day overflowed 32-bit signed integer",
),
NthWeekdayOfMonth => f.write_str(
"invalid nth weekday of month, \
must be non-zero and in range `-5..=5`",
),
Tomorrow => f.write_str(
"returning tomorrow for `9999-12-31` is not \
possible because it is greater than Jiff's supported
maximum date",
),
YearNext => f.write_str(
"creating a date for a year following `9999` is \
not possible because it is greater than Jiff's supported \
maximum date",
),
YearPrevious => f.write_str(
"creating a date for a year preceding `-9999` is \
not possible because it is less than Jiff's supported \
minimum date",
),
Yesterday => f.write_str(
"returning yesterday for `-9999-01-01` is not \
possible because it is less than Jiff's supported
minimum date",
),
}
}
}

View file

@ -0,0 +1,57 @@
// auto-generated by: jiff-cli generate shared
pub(crate) mod itime;
/// An error scoped to Jiff's `shared` module.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Error {
kind: ErrorKind,
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
self.kind.fmt(f)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum ErrorKind {
Time(self::itime::Error),
}
impl From<ErrorKind> for Error {
fn from(kind: ErrorKind) -> Error {
Error { kind }
}
}
impl core::fmt::Display for ErrorKind {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match *self {
ErrorKind::Time(ref err) => err.fmt(f),
}
}
}
/*
/// A slim error that occurs when an input value is out of bounds.
#[derive(Clone, Debug)]
struct SlimRangeError {
what: &'static str,
}
impl SlimRangeError {
fn new(what: &'static str) -> SlimRangeError {
SlimRangeError { what }
}
}
impl std::error::Error for SlimRangeError {}
impl core::fmt::Display for SlimRangeError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let SlimRangeError { what } = *self;
write!(f, "parameter '{what}' is not in the required range")
}
}
*/

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@ use super::utf8;
pub(crate) struct Byte(pub u8);
impl core::fmt::Display for Byte {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
if self.0 == b' ' {
return write!(f, " ");
@ -37,6 +38,7 @@ impl core::fmt::Display for Byte {
}
impl core::fmt::Debug for Byte {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "\"")?;
core::fmt::Display::fmt(self, f)?;
@ -54,15 +56,16 @@ impl core::fmt::Debug for Byte {
pub(crate) struct Bytes<'a>(pub &'a [u8]);
impl<'a> core::fmt::Display for Bytes<'a> {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
// This is a sad re-implementation of a similar impl found in bstr.
let mut bytes = self.0;
while let Some(result) = utf8::decode(bytes) {
let ch = match result {
Ok(ch) => ch,
Err(errant_bytes) => {
Err(err) => {
// The decode API guarantees `errant_bytes` is non-empty.
write!(f, r"\x{:02x}", errant_bytes[0])?;
write!(f, r"\x{:02x}", err.as_slice()[0])?;
bytes = &bytes[1..];
continue;
}
@ -81,6 +84,7 @@ impl<'a> core::fmt::Display for Bytes<'a> {
}
impl<'a> core::fmt::Debug for Bytes<'a> {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "\"")?;
core::fmt::Display::fmt(self, f)?;
@ -88,3 +92,34 @@ impl<'a> core::fmt::Debug for Bytes<'a> {
Ok(())
}
}
/// A helper for repeating a single byte utilizing `Byte`.
///
/// This is limited to repeating a byte up to `u8::MAX` times in order
/// to reduce its size overhead. And in practice, Jiff just doesn't
/// need more than this (at time of writing, 2025-11-29).
pub(crate) struct RepeatByte {
pub(crate) byte: u8,
pub(crate) count: u8,
}
impl core::fmt::Display for RepeatByte {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
for _ in 0..self.count {
write!(f, "{}", Byte(self.byte))?;
}
Ok(())
}
}
impl core::fmt::Debug for RepeatByte {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "\"")?;
core::fmt::Display::fmt(self, f)?;
write!(f, "\"")?;
Ok(())
}
}

View file

@ -24,8 +24,6 @@ they are internal types. Specifically, to distinguish them from Jiff's public
types. For example, `Date` versus `IDate`.
*/
use super::error::{err, Error};
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub(crate) struct ITimestamp {
pub(crate) second: i64,
@ -143,11 +141,13 @@ impl IDateTime {
pub(crate) fn checked_add_seconds(
&self,
seconds: i32,
) -> Result<IDateTime, Error> {
let day_second =
self.time.to_second().second.checked_add(seconds).ok_or_else(
|| err!("adding `{seconds}s` to datetime overflowed"),
)?;
) -> Result<IDateTime, RangeError> {
let day_second = self
.time
.to_second()
.second
.checked_add(seconds)
.ok_or_else(|| RangeError::DateTimeSeconds)?;
let days = day_second.div_euclid(86400);
let second = day_second.rem_euclid(86400);
let date = self.date.checked_add_days(days)?;
@ -162,8 +162,8 @@ pub(crate) struct IEpochDay {
}
impl IEpochDay {
const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 };
const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 };
pub(crate) const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 };
pub(crate) const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 };
/// Converts days since the Unix epoch to a Gregorian date.
///
@ -219,20 +219,17 @@ impl IEpochDay {
/// If this would overflow an `i32` or result in an out-of-bounds epoch
/// day, then this returns an error.
#[inline]
pub(crate) fn checked_add(&self, amount: i32) -> Result<IEpochDay, Error> {
pub(crate) fn checked_add(
&self,
amount: i32,
) -> Result<IEpochDay, RangeError> {
let epoch_day = self.epoch_day;
let sum = epoch_day.checked_add(amount).ok_or_else(|| {
err!("adding `{amount}` to epoch day `{epoch_day}` overflowed i32")
})?;
let sum = epoch_day
.checked_add(amount)
.ok_or_else(|| RangeError::EpochDayI32)?;
let ret = IEpochDay { epoch_day: sum };
if !(IEpochDay::MIN <= ret && ret <= IEpochDay::MAX) {
return Err(err!(
"adding `{amount}` to epoch day `{epoch_day}` \
resulted in `{sum}`, which is not in the required \
epoch day range of `{min}..={max}`",
min = IEpochDay::MIN.epoch_day,
max = IEpochDay::MAX.epoch_day,
));
return Err(RangeError::EpochDayDays);
}
Ok(ret)
}
@ -260,14 +257,11 @@ impl IDate {
year: i16,
month: i8,
day: i8,
) -> Result<IDate, Error> {
) -> Result<IDate, RangeError> {
if day > 28 {
let max_day = days_in_month(year, month);
if day > max_day {
return Err(err!(
"day={day} is out of range for year={year} \
and month={month}, must be in range 1..={max_day}",
));
return Err(RangeError::DateInvalidDays { year, month });
}
}
Ok(IDate { year, month, day })
@ -283,37 +277,22 @@ impl IDate {
pub(crate) fn from_day_of_year(
year: i16,
day: i16,
) -> Result<IDate, Error> {
) -> Result<IDate, RangeError> {
if !(1 <= day && day <= 366) {
return Err(err!(
"day-of-year={day} is out of range for year={year}, \
must be in range 1..={max_day}",
max_day = days_in_year(year),
));
return Err(RangeError::DateInvalidDayOfYear { year });
}
let start = IDate { year, month: 1, day: 1 }.to_epoch_day();
let end = start
.checked_add(i32::from(day) - 1)
.map_err(|_| {
err!(
"failed to find date for \
year={year} and day-of-year={day}: \
adding `{day}` to `{start}` overflows \
Jiff's range",
start = start.epoch_day,
)
})?
// This can only happen when `year=9999` and `day=366`.
.map_err(|_| RangeError::DayOfYear)?
.to_date();
// If we overflowed into the next year, then `day` is too big.
if year != end.year {
// Can only happen given day=366 and this is a leap year.
debug_assert_eq!(day, 366);
debug_assert!(!is_leap_year(year));
return Err(err!(
"day-of-year={day} is out of range for year={year}, \
must be in range 1..={max_day}",
max_day = days_in_year(year),
));
return Err(RangeError::DateInvalidDayOfYear { year });
}
Ok(end)
}
@ -329,12 +308,9 @@ impl IDate {
pub(crate) fn from_day_of_year_no_leap(
year: i16,
mut day: i16,
) -> Result<IDate, Error> {
) -> Result<IDate, RangeError> {
if !(1 <= day && day <= 365) {
return Err(err!(
"day-of-year={day} is out of range for year={year}, \
must be in range 1..=365",
));
return Err(RangeError::DateInvalidDayOfYearNoLeap);
}
if day >= 60 && is_leap_year(year) {
day += 1;
@ -392,12 +368,9 @@ impl IDate {
&self,
nth: i8,
weekday: IWeekday,
) -> Result<IDate, Error> {
) -> Result<IDate, RangeError> {
if nth == 0 || !(-5 <= nth && nth <= 5) {
return Err(err!(
"got nth weekday of `{nth}`, but \
must be non-zero and in range `-5..=5`",
));
return Err(RangeError::NthWeekdayOfMonth);
}
if nth > 0 {
let first_weekday = self.first_of_month().weekday();
@ -414,13 +387,10 @@ impl IDate {
// of `Day`, we can't let this boundary condition escape. So we
// check it here.
if day < 1 {
return Err(err!(
"day={day} is out of range for year={year} \
and month={month}, must be in range 1..={max_day}",
year = self.year,
month = self.month,
max_day = days_in_month(self.year, self.month),
));
return Err(RangeError::DateInvalidDays {
year: self.year,
month: self.month,
});
}
IDate::try_new(self.year, self.month, day)
}
@ -428,16 +398,12 @@ impl IDate {
/// Returns the day before this date.
#[inline]
pub(crate) fn yesterday(self) -> Result<IDate, Error> {
pub(crate) fn yesterday(self) -> Result<IDate, RangeError> {
if self.day == 1 {
if self.month == 1 {
let year = self.year - 1;
if year <= -10000 {
return Err(err!(
"returning yesterday for -9999-01-01 is not \
possible because it is less than Jiff's supported
minimum date",
));
return Err(RangeError::Yesterday);
}
return Ok(IDate { year, month: 12, day: 31 });
}
@ -450,16 +416,12 @@ impl IDate {
/// Returns the day after this date.
#[inline]
pub(crate) fn tomorrow(self) -> Result<IDate, Error> {
pub(crate) fn tomorrow(self) -> Result<IDate, RangeError> {
if self.day >= 28 && self.day == days_in_month(self.year, self.month) {
if self.month == 12 {
let year = self.year + 1;
if year >= 10000 {
return Err(err!(
"returning tomorrow for 9999-12-31 is not \
possible because it is greater than Jiff's supported
maximum date",
));
return Err(RangeError::Tomorrow);
}
return Ok(IDate { year, month: 1, day: 1 });
}
@ -471,34 +433,20 @@ impl IDate {
/// Returns the year one year before this date.
#[inline]
pub(crate) fn prev_year(self) -> Result<i16, Error> {
pub(crate) fn prev_year(self) -> Result<i16, RangeError> {
let year = self.year - 1;
if year <= -10_000 {
return Err(err!(
"returning previous year for {year:04}-{month:02}-{day:02} is \
not possible because it is less than Jiff's supported \
minimum date",
year = self.year,
month = self.month,
day = self.day,
));
return Err(RangeError::YearPrevious);
}
Ok(year)
}
/// Returns the year one year from this date.
#[inline]
pub(crate) fn next_year(self) -> Result<i16, Error> {
pub(crate) fn next_year(self) -> Result<i16, RangeError> {
let year = self.year + 1;
if year >= 10_000 {
return Err(err!(
"returning next year for {year:04}-{month:02}-{day:02} is \
not possible because it is greater than Jiff's supported \
maximum date",
year = self.year,
month = self.month,
day = self.day,
));
return Err(RangeError::YearNext);
}
Ok(year)
}
@ -508,7 +456,7 @@ impl IDate {
pub(crate) fn checked_add_days(
&self,
amount: i32,
) -> Result<IDate, Error> {
) -> Result<IDate, RangeError> {
match amount {
0 => Ok(*self),
-1 => self.yesterday(),
@ -720,6 +668,84 @@ pub(crate) enum IAmbiguousOffset {
Fold { before: IOffset, after: IOffset },
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum RangeError {
DateInvalidDayOfYear { year: i16 },
DateInvalidDayOfYearNoLeap,
DateInvalidDays { year: i16, month: i8 },
DateTimeSeconds,
DayOfYear,
EpochDayDays,
EpochDayI32,
NthWeekdayOfMonth,
Tomorrow,
YearNext,
YearPrevious,
Yesterday,
}
impl core::fmt::Display for RangeError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::RangeError::*;
match *self {
DateInvalidDayOfYear { year } => write!(
f,
"number of days for `{year:04}` is invalid, \
must be in range `1..={max_day}`",
max_day = days_in_year(year),
),
DateInvalidDayOfYearNoLeap => f.write_str(
"number of days is invalid, must be in range `1..=365`",
),
DateInvalidDays { year, month } => write!(
f,
"number of days for `{year:04}-{month:02}` is invalid, \
must be in range `1..={max_day}`",
max_day = days_in_month(year, month),
),
DateTimeSeconds => {
f.write_str("adding seconds to datetime overflowed")
}
DayOfYear => f.write_str("day of year is invalid"),
EpochDayDays => write!(
f,
"adding to epoch day resulted in a value outside \
the allowed range of `{min}..={max}`",
min = IEpochDay::MIN.epoch_day,
max = IEpochDay::MAX.epoch_day,
),
EpochDayI32 => f.write_str(
"adding to epoch day overflowed 32-bit signed integer",
),
NthWeekdayOfMonth => f.write_str(
"invalid nth weekday of month, \
must be non-zero and in range `-5..=5`",
),
Tomorrow => f.write_str(
"returning tomorrow for `9999-12-31` is not \
possible because it is greater than Jiff's supported
maximum date",
),
YearNext => f.write_str(
"creating a date for a year following `9999` is \
not possible because it is greater than Jiff's supported \
maximum date",
),
YearPrevious => f.write_str(
"creating a date for a year preceding `-9999` is \
not possible because it is less than Jiff's supported \
minimum date",
),
Yesterday => f.write_str(
"returning yesterday for `-9999-01-01` is not \
possible because it is less than Jiff's supported
minimum date",
),
}
}
}
/// Returns true if and only if the given year is a leap year.
///
/// A leap year is a year with 366 days. Typical years have 365 days.
@ -922,4 +948,20 @@ mod tests {
let d1 = IDate { year: 9999, month: 12, day: 31 };
assert_eq!(d1.tomorrow().ok(), None);
}
#[test]
fn from_day_of_year() {
assert_eq!(
IDate::from_day_of_year(9999, 365),
Ok(IDate { year: 9999, month: 12, day: 31 }),
);
assert_eq!(
IDate::from_day_of_year(9998, 366),
Err(RangeError::DateInvalidDayOfYear { year: 9998 }),
);
assert_eq!(
IDate::from_day_of_year(9999, 366),
Err(RangeError::DayOfYear),
);
}
}

View file

@ -1,7 +1,4 @@
// auto-generated by: jiff-cli generate shared
pub(crate) mod array_str;
pub(crate) mod error;
pub(crate) mod escape;
pub(crate) mod itime;
pub(crate) mod utf8;

View file

@ -1,5 +1,59 @@
// auto-generated by: jiff-cli generate shared
/// Represents an invalid UTF-8 sequence.
///
/// This is an error returned by `decode`. It is guaranteed to
/// contain 1, 2 or 3 bytes.
pub(crate) struct Utf8Error {
bytes: [u8; 3],
len: u8,
}
impl Utf8Error {
#[cold]
#[inline(never)]
fn new(original_bytes: &[u8], err: core::str::Utf8Error) -> Utf8Error {
let len = err.error_len().unwrap_or_else(|| original_bytes.len());
// OK because the biggest invalid UTF-8
// sequence possible is 3.
debug_assert!(1 <= len && len <= 3);
let mut bytes = [0; 3];
bytes[..len].copy_from_slice(&original_bytes[..len]);
Utf8Error {
bytes,
// OK because the biggest invalid UTF-8
// sequence possible is 3.
len: u8::try_from(len).unwrap(),
}
}
/// Returns the slice of invalid UTF-8 bytes.
///
/// The slice returned is guaranteed to have length equivalent
/// to `Utf8Error::len`.
pub(crate) fn as_slice(&self) -> &[u8] {
&self.bytes[..self.len()]
}
/// Returns the length of the invalid UTF-8 sequence found.
///
/// This is guaranteed to be 1, 2 or 3.
pub(crate) fn len(&self) -> usize {
usize::from(self.len)
}
}
impl core::fmt::Display for Utf8Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(
f,
"found invalid UTF-8 byte {errant_bytes:?} in format \
string (format strings must be valid UTF-8)",
errant_bytes = crate::shared::util::escape::Bytes(self.as_slice()),
)
}
}
/// Decodes the next UTF-8 encoded codepoint from the given byte slice.
///
/// If no valid encoding of a codepoint exists at the beginning of the
@ -15,25 +69,24 @@
/// *WARNING*: This is not designed for performance. If you're looking for
/// a fast UTF-8 decoder, this is not it. If you feel like you need one in
/// this crate, then please file an issue and discuss your use case.
pub(crate) fn decode(bytes: &[u8]) -> Option<Result<char, &[u8]>> {
pub(crate) fn decode(bytes: &[u8]) -> Option<Result<char, Utf8Error>> {
if bytes.is_empty() {
return None;
}
let string = match core::str::from_utf8(&bytes[..bytes.len().min(4)]) {
Ok(s) => s,
Err(ref err) if err.valid_up_to() > 0 => {
// OK because we just verified we have at least some
// valid UTF-8.
core::str::from_utf8(&bytes[..err.valid_up_to()]).unwrap()
}
// In this case, we want to return 1-3 bytes that make up a prefix of
// a potentially valid codepoint.
Err(err) => {
return Some(Err(
&bytes[..err.error_len().unwrap_or_else(|| bytes.len())]
))
}
Err(err) => return Some(Err(Utf8Error::new(bytes, err))),
};
// OK because we guaranteed above that `string`
// must be non-empty. And thus, `str::chars` must
// yield at least one Unicode scalar value.
Some(Ok(string.chars().next().unwrap()))
}

View file

@ -1,6 +1,6 @@
[package]
name = "jiff-tzdb"
version = "0.1.4" #:version
version = "0.1.5" #:version
authors = ["Andrew Gallant <jamslam@gmail.com>"]
license = "Unlicense OR MIT"
homepage = "https://github.com/BurntSushi/jiff/tree/master/crates/jiff-tzdb"

View file

@ -1,4 +1,4 @@
pub(super) static VERSION: Option<&str> = Some(r"2025b");
pub(super) static VERSION: Option<&str> = Some(r"2025c");
pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Africa/Abidjan", 3982..4112),
@ -14,15 +14,15 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Africa/Blantyre", 3851..3982),
(r"Africa/Brazzaville", 10414..10594),
(r"Africa/Bujumbura", 3851..3982),
(r"Africa/Cairo", 163461..164770),
(r"Africa/Casablanca", 189897..191818),
(r"Africa/Cairo", 162382..163691),
(r"Africa/Casablanca", 190185..192106),
(r"Africa/Ceuta", 41827..42389),
(r"Africa/Conakry", 3982..4112),
(r"Africa/Dakar", 3982..4112),
(r"Africa/Dar_es_Salaam", 12063..12254),
(r"Africa/Djibouti", 12063..12254),
(r"Africa/Douala", 10414..10594),
(r"Africa/El_Aaiun", 186193..188019),
(r"Africa/El_Aaiun", 186481..188307),
(r"Africa/Freetown", 3982..4112),
(r"Africa/Gaborone", 3851..3982),
(r"Africa/Harare", 3851..3982),
@ -74,7 +74,7 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"America/Argentina/Tucuman", 77548..78274),
(r"America/Argentina/Ushuaia", 71856..72564),
(r"America/Aruba", 9872..10049),
(r"America/Asuncion", 152557..153642),
(r"America/Asuncion", 151478..152563),
(r"America/Atikokan", 5647..5796),
(r"America/Atka", 122687..123656),
(r"America/Bahia", 65501..66183),
@ -94,13 +94,13 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"America/Catamarca", 71148..71856),
(r"America/Cayenne", 6101..6252),
(r"America/Cayman", 5647..5796),
(r"America/Chicago", 184439..186193),
(r"America/Chicago", 184727..186481),
(r"America/Chihuahua", 64055..64746),
(r"America/Ciudad_Juarez", 66873..67591),
(r"America/Coral_Harbour", 5647..5796),
(r"America/Cordoba", 70440..71148),
(r"America/Costa_Rica", 16497..16729),
(r"America/Coyhaique", 167472..168834),
(r"America/Coyhaique", 167760..169122),
(r"America/Creston", 16966..17206),
(r"America/Cuiaba", 132233..133167),
(r"America/Curacao", 9872..10049),
@ -113,21 +113,21 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"America/Edmonton", 133167..134137),
(r"America/Eirunepe", 30431..30867),
(r"America/El_Salvador", 11514..11690),
(r"America/Ensenada", 149270..150349),
(r"America/Fort_Nelson", 171698..173146),
(r"America/Ensenada", 166393..167760),
(r"America/Fort_Nelson", 171986..173434),
(r"America/Fort_Wayne", 36060..36591),
(r"America/Fortaleza", 37559..38043),
(r"America/Glace_Bay", 110783..111663),
(r"America/Godthab", 191818..192783),
(r"America/Goose_Bay", 176127..177707),
(r"America/Godthab", 192106..193071),
(r"America/Goose_Bay", 176415..177995),
(r"America/Grand_Turk", 108139..108992),
(r"America/Grenada", 9872..10049),
(r"America/Guadeloupe", 9872..10049),
(r"America/Guatemala", 15573..15785),
(r"America/Guayaquil", 9693..9872),
(r"America/Guyana", 10594..10775),
(r"America/Halifax", 179306..180978),
(r"America/Havana", 153642..154759),
(r"America/Halifax", 179594..181266),
(r"America/Havana", 152563..153680),
(r"America/Hermosillo", 17472..17730),
(r"America/Indiana/Indianapolis", 36060..36591),
(r"America/Indiana/Knox", 139996..141012),
@ -143,14 +143,14 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"America/Jamaica", 21737..22076),
(r"America/Jujuy", 61970..62660),
(r"America/Juneau", 120800..121766),
(r"America/Kentucky/Louisville", 158397..159639),
(r"America/Kentucky/Louisville", 157318..158560),
(r"America/Kentucky/Monticello", 131261..132233),
(r"America/Knox_IN", 139996..141012),
(r"America/Kralendijk", 9872..10049),
(r"America/La_Paz", 7964..8134),
(r"America/Lima", 18282..18565),
(r"America/Los_Angeles", 160873..162167),
(r"America/Louisville", 158397..159639),
(r"America/Los_Angeles", 159794..161088),
(r"America/Louisville", 157318..158560),
(r"America/Lower_Princes", 9872..10049),
(r"America/Maceio", 38601..39103),
(r"America/Managua", 18565..18860),
@ -165,20 +165,20 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"America/Metlakatla", 42389..42975),
(r"America/Mexico_City", 98432..99205),
(r"America/Miquelon", 40739..41289),
(r"America/Moncton", 174634..176127),
(r"America/Moncton", 174922..176415),
(r"America/Monterrey", 74688..75397),
(r"America/Montevideo", 128417..129386),
(r"America/Montreal", 180978..182695),
(r"America/Montreal", 181266..182983),
(r"America/Montserrat", 9872..10049),
(r"America/Nassau", 180978..182695),
(r"America/New_York", 182695..184439),
(r"America/Nipigon", 180978..182695),
(r"America/Nassau", 181266..182983),
(r"America/New_York", 182983..184727),
(r"America/Nipigon", 181266..182983),
(r"America/Nome", 123656..124631),
(r"America/Noronha", 36591..37075),
(r"America/North_Dakota/Beulah", 146140..147183),
(r"America/North_Dakota/Center", 134137..135127),
(r"America/North_Dakota/New_Salem", 135127..136117),
(r"America/Nuuk", 191818..192783),
(r"America/Nuuk", 192106..193071),
(r"America/Ojinaga", 67591..68309),
(r"America/Panama", 5647..5796),
(r"America/Pangnirtung", 106419..107274),
@ -189,24 +189,24 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"America/Porto_Acre", 28274..28692),
(r"America/Porto_Velho", 26248..26642),
(r"America/Puerto_Rico", 9872..10049),
(r"America/Punta_Arenas", 157179..158397),
(r"America/Rainy_River", 162167..163461),
(r"America/Punta_Arenas", 156100..157318),
(r"America/Rainy_River", 161088..162382),
(r"America/Rankin_Inlet", 104795..105602),
(r"America/Recife", 37075..37559),
(r"America/Regina", 54642..55280),
(r"America/Resolute", 103988..104795),
(r"America/Rio_Branco", 28274..28692),
(r"America/Rosario", 70440..71148),
(r"America/Santa_Isabel", 149270..150349),
(r"America/Santa_Isabel", 166393..167760),
(r"America/Santarem", 27453..27862),
(r"America/Santiago", 196015..197369),
(r"America/Santiago", 196303..197657),
(r"America/Santo_Domingo", 19436..19753),
(r"America/Sao_Paulo", 137116..138068),
(r"America/Scoresbysund", 192783..193767),
(r"America/Scoresbysund", 193071..194055),
(r"America/Shiprock", 147183..148225),
(r"America/Sitka", 119844..120800),
(r"America/St_Barthelemy", 9872..10049),
(r"America/St_Johns", 188019..189897),
(r"America/St_Johns", 188307..190185),
(r"America/St_Kitts", 9872..10049),
(r"America/St_Lucia", 9872..10049),
(r"America/St_Thomas", 9872..10049),
@ -214,14 +214,14 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"America/Swift_Current", 22418..22786),
(r"America/Tegucigalpa", 13299..13493),
(r"America/Thule", 30867..31322),
(r"America/Thunder_Bay", 180978..182695),
(r"America/Tijuana", 149270..150349),
(r"America/Toronto", 180978..182695),
(r"America/Thunder_Bay", 181266..182983),
(r"America/Tijuana", 166393..167760),
(r"America/Toronto", 181266..182983),
(r"America/Tortola", 9872..10049),
(r"America/Vancouver", 164770..166100),
(r"America/Vancouver", 163691..165021),
(r"America/Virgin", 9872..10049),
(r"America/Whitehorse", 141012..142041),
(r"America/Winnipeg", 162167..163461),
(r"America/Winnipeg", 161088..162382),
(r"America/Yakutat", 118898..119844),
(r"America/Yellowknife", 133167..134137),
(r"Antarctica/Casey", 18860..19147),
@ -261,23 +261,23 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Asia/Chungking", 25461..25854),
(r"Asia/Colombo", 14362..14609),
(r"Asia/Dacca", 14131..14362),
(r"Asia/Damascus", 159639..160873),
(r"Asia/Damascus", 158560..159794),
(r"Asia/Dhaka", 14131..14362),
(r"Asia/Dili", 8651..8821),
(r"Asia/Dubai", 4511..4644),
(r"Asia/Dushanbe", 23152..23518),
(r"Asia/Famagusta", 127477..128417),
(r"Asia/Gaza", 197369..200319),
(r"Asia/Gaza", 197657..200607),
(r"Asia/Harbin", 25461..25854),
(r"Asia/Hebron", 200319..203287),
(r"Asia/Hebron", 200607..203575),
(r"Asia/Ho_Chi_Minh", 15785..16021),
(r"Asia/Hong_Kong", 100821..101596),
(r"Asia/Hovd", 46530..47124),
(r"Asia/Irkutsk", 91581..92341),
(r"Asia/Istanbul", 154759..155959),
(r"Asia/Istanbul", 153680..154880),
(r"Asia/Jakarta", 14856..15104),
(r"Asia/Jayapura", 8480..8651),
(r"Asia/Jerusalem", 193767..194841),
(r"Asia/Jerusalem", 194055..195129),
(r"Asia/Kabul", 6859..7018),
(r"Asia/Kamchatka", 80467..81194),
(r"Asia/Karachi", 17206..17472),
@ -320,7 +320,7 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Asia/Tashkent", 22786..23152),
(r"Asia/Tbilisi", 53338..53967),
(r"Asia/Tehran", 103176..103988),
(r"Asia/Tel_Aviv", 193767..194841),
(r"Asia/Tel_Aviv", 194055..195129),
(r"Asia/Thimbu", 7171..7325),
(r"Asia/Thimphu", 7171..7325),
(r"Asia/Tokyo", 15360..15573),
@ -336,14 +336,14 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Asia/Yangon", 10227..10414),
(r"Asia/Yekaterinburg", 92341..93101),
(r"Asia/Yerevan", 73980..74688),
(r"Atlantic/Azores", 168834..170235),
(r"Atlantic/Azores", 169122..170523),
(r"Atlantic/Bermuda", 144073..145097),
(r"Atlantic/Canary", 33169..33647),
(r"Atlantic/Cape_Verde", 9164..9339),
(r"Atlantic/Faeroe", 29129..29570),
(r"Atlantic/Faroe", 29129..29570),
(r"Atlantic/Jan_Mayen", 63350..64055),
(r"Atlantic/Madeira", 166100..167472),
(r"Atlantic/Madeira", 165021..166393),
(r"Atlantic/Reykjavik", 3982..4112),
(r"Atlantic/South_Georgia", 3585..3717),
(r"Atlantic/St_Helena", 3982..4112),
@ -375,24 +375,24 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Brazil/DeNoronha", 36591..37075),
(r"Brazil/East", 137116..138068),
(r"Brazil/West", 27862..28274),
(r"Canada/Atlantic", 179306..180978),
(r"Canada/Central", 162167..163461),
(r"Canada/Eastern", 180978..182695),
(r"Canada/Atlantic", 179594..181266),
(r"Canada/Central", 161088..162382),
(r"Canada/Eastern", 181266..182983),
(r"Canada/Mountain", 133167..134137),
(r"Canada/Newfoundland", 188019..189897),
(r"Canada/Pacific", 164770..166100),
(r"Canada/Newfoundland", 188307..190185),
(r"Canada/Pacific", 163691..165021),
(r"Canada/Saskatchewan", 54642..55280),
(r"Canada/Yukon", 141012..142041),
(r"CET", 151454..152557),
(r"Chile/Continental", 196015..197369),
(r"Chile/EasterIsland", 194841..196015),
(r"CST6CDT", 184439..186193),
(r"Cuba", 153642..154759),
(r"CET", 150375..151478),
(r"Chile/Continental", 196303..197657),
(r"Chile/EasterIsland", 195129..196303),
(r"CST6CDT", 184727..186481),
(r"Cuba", 152563..153680),
(r"EET", 57918..58600),
(r"Egypt", 163461..164770),
(r"Eire", 173146..174634),
(r"Egypt", 162382..163691),
(r"Eire", 173434..174922),
(r"EST", 5647..5796),
(r"EST5EDT", 182695..184439),
(r"EST5EDT", 182983..184727),
(r"Etc/GMT", 113..224),
(r"Etc/GMT+0", 113..224),
(r"Etc/GMT+1", 3182..3295),
@ -428,44 +428,44 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Etc/Universal", 224..335),
(r"Etc/UTC", 224..335),
(r"Etc/Zulu", 224..335),
(r"Europe/Amsterdam", 151454..152557),
(r"Europe/Amsterdam", 150375..151478),
(r"Europe/Andorra", 23884..24273),
(r"Europe/Astrakhan", 81920..82646),
(r"Europe/Athens", 57918..58600),
(r"Europe/Belfast", 177707..179306),
(r"Europe/Belfast", 177995..179594),
(r"Europe/Belgrade", 34563..35041),
(r"Europe/Berlin", 63350..64055),
(r"Europe/Bratislava", 69009..69732),
(r"Europe/Brussels", 151454..152557),
(r"Europe/Brussels", 150375..151478),
(r"Europe/Bucharest", 57257..57918),
(r"Europe/Budapest", 97666..98432),
(r"Europe/Busingen", 35041..35538),
(r"Europe/Chisinau", 64746..65501),
(r"Europe/Copenhagen", 63350..64055),
(r"Europe/Dublin", 173146..174634),
(r"Europe/Gibraltar", 155959..157179),
(r"Europe/Guernsey", 177707..179306),
(r"Europe/Dublin", 173434..174922),
(r"Europe/Gibraltar", 154880..156100),
(r"Europe/Guernsey", 177995..179594),
(r"Europe/Helsinki", 32688..33169),
(r"Europe/Isle_of_Man", 177707..179306),
(r"Europe/Istanbul", 154759..155959),
(r"Europe/Jersey", 177707..179306),
(r"Europe/Isle_of_Man", 177995..179594),
(r"Europe/Istanbul", 153680..154880),
(r"Europe/Jersey", 177995..179594),
(r"Europe/Kaliningrad", 113459..114363),
(r"Europe/Kiev", 38043..38601),
(r"Europe/Kirov", 78274..79009),
(r"Europe/Kyiv", 38043..38601),
(r"Europe/Lisbon", 170235..171698),
(r"Europe/Lisbon", 170523..171986),
(r"Europe/Ljubljana", 34563..35041),
(r"Europe/London", 177707..179306),
(r"Europe/Luxembourg", 151454..152557),
(r"Europe/London", 177995..179594),
(r"Europe/Luxembourg", 150375..151478),
(r"Europe/Madrid", 111663..112560),
(r"Europe/Malta", 126549..127477),
(r"Europe/Mariehamn", 32688..33169),
(r"Europe/Minsk", 99205..100013),
(r"Europe/Monaco", 150349..151454),
(r"Europe/Monaco", 149270..150375),
(r"Europe/Moscow", 109875..110783),
(r"Europe/Nicosia", 44735..45332),
(r"Europe/Oslo", 63350..64055),
(r"Europe/Paris", 150349..151454),
(r"Europe/Paris", 149270..150375),
(r"Europe/Podgorica", 34563..35041),
(r"Europe/Prague", 69009..69732),
(r"Europe/Riga", 55280..55974),
@ -493,8 +493,8 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Europe/Zaporozhye", 38043..38601),
(r"Europe/Zurich", 35041..35538),
(r"Factory", 0..113),
(r"GB", 177707..179306),
(r"GB-Eire", 177707..179306),
(r"GB", 177995..179594),
(r"GB-Eire", 177995..179594),
(r"GMT", 113..224),
(r"GMT+0", 113..224),
(r"GMT-0", 113..224),
@ -515,13 +515,13 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Indian/Mayotte", 12063..12254),
(r"Indian/Reunion", 4511..4644),
(r"Iran", 103176..103988),
(r"Israel", 193767..194841),
(r"Israel", 194055..195129),
(r"Jamaica", 21737..22076),
(r"Japan", 15360..15573),
(r"Kwajalein", 12882..13101),
(r"Libya", 29570..30001),
(r"MET", 151454..152557),
(r"Mexico/BajaNorte", 149270..150349),
(r"MET", 150375..151478),
(r"Mexico/BajaNorte", 166393..167760),
(r"Mexico/BajaSur", 66183..66873),
(r"Mexico/General", 98432..99205),
(r"MST", 16966..17206),
@ -534,7 +534,7 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Pacific/Bougainville", 12254..12455),
(r"Pacific/Chatham", 100013..100821),
(r"Pacific/Chuuk", 6705..6859),
(r"Pacific/Easter", 194841..196015),
(r"Pacific/Easter", 195129..196303),
(r"Pacific/Efate", 22076..22418),
(r"Pacific/Enderbury", 8134..8306),
(r"Pacific/Fakaofo", 5796..5949),
@ -574,29 +574,29 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range<usize>)] = &[
(r"Pacific/Wallis", 3717..3851),
(r"Pacific/Yap", 6705..6859),
(r"Poland", 116167..117090),
(r"Portugal", 170235..171698),
(r"Portugal", 170523..171986),
(r"PRC", 25461..25854),
(r"PST8PDT", 160873..162167),
(r"PST8PDT", 159794..161088),
(r"ROC", 39103..39614),
(r"ROK", 27038..27453),
(r"Singapore", 15104..15360),
(r"Turkey", 154759..155959),
(r"Turkey", 153680..154880),
(r"UCT", 224..335),
(r"Universal", 224..335),
(r"US/Alaska", 124631..125608),
(r"US/Aleutian", 122687..123656),
(r"US/Arizona", 16966..17206),
(r"US/Central", 184439..186193),
(r"US/Central", 184727..186481),
(r"US/East-Indiana", 36060..36591),
(r"US/Eastern", 182695..184439),
(r"US/Eastern", 182983..184727),
(r"US/Hawaii", 13910..14131),
(r"US/Indiana-Starke", 139996..141012),
(r"US/Michigan", 112560..113459),
(r"US/Mountain", 147183..148225),
(r"US/Pacific", 160873..162167),
(r"US/Pacific", 159794..161088),
(r"US/Samoa", 5197..5343),
(r"UTC", 224..335),
(r"W-SU", 109875..110783),
(r"WET", 170235..171698),
(r"WET", 170523..171986),
(r"Zulu", 224..335),
];

4
fuzz/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
target
corpus
artifacts
coverage

244
fuzz/Cargo.lock generated Normal file
View file

@ -0,0 +1,244 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "cc"
version = "1.2.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "jiff"
version = "0.2.16"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
"log",
"portable-atomic",
"portable-atomic-util",
"serde_core",
"windows-sys",
]
[[package]]
name = "jiff-fuzz"
version = "0.0.0"
dependencies = [
"jiff",
"libfuzzer-sys",
]
[[package]]
name = "jiff-static"
version = "0.2.16"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "jiff-tzdb"
version = "0.1.5"
[[package]]
name = "jiff-tzdb-platform"
version = "0.1.3"
dependencies = [
"jiff-tzdb",
]
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom",
"libc",
]
[[package]]
name = "libc"
version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "libfuzzer-sys"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404"
dependencies = [
"arbitrary",
"cc",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"

44
fuzz/Cargo.toml Normal file
View file

@ -0,0 +1,44 @@
[package]
name = "jiff-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[features]
relaxed = []
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] }
[dependencies.jiff]
path = ".."
[workspace]
members = ["."]
[[bin]]
name = "rfc2822_parse"
path = "fuzz_targets/rfc2822_parse.rs"
test = false
doc = false
bench = false
[[bin]]
name = "strtime_parse"
path = "fuzz_targets/strtime_parse.rs"
test = false
doc = false
bench = false
[[bin]]
name = "temporal_parse"
path = "fuzz_targets/temporal_parse.rs"
test = false
doc = false
bench = false
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }

View file

@ -0,0 +1,50 @@
#![cfg_attr(fuzzing, no_main)]
use std::borrow::Cow;
use libfuzzer_sys::fuzz_target;
use jiff::fmt::rfc2822;
mod shim;
fn do_fuzz(data: &[u8]) {
const RFC2822_PARSER: rfc2822::DateTimeParser =
rfc2822::DateTimeParser::new();
const RFC2822_PRINTER: rfc2822::DateTimePrinter =
rfc2822::DateTimePrinter::new();
let Ok(first) = RFC2822_PARSER.parse_zoned(data) else { return };
let mut unparsed = Vec::with_capacity(data.len());
RFC2822_PRINTER
.print_zoned(&first, &mut unparsed)
.expect("We parsed it, so we should be able to print it");
match RFC2822_PARSER.parse_zoned(&unparsed) {
Ok(second) => {
assert_eq!(
first, second,
"expected the initially parsed value \
to be equal to the value after printing and re-parsing",
);
}
Err(e) if cfg!(not(feature = "relaxed")) => {
let unparsed_str = String::from_utf8_lossy(&unparsed);
panic!(
"should be able to parse a printed value; \
failed with `{e}` at: `{unparsed_str}`{}, \
corresponding to {first:?}",
if matches!(unparsed_str, Cow::Owned(_)) {
Cow::from(format!(" (lossy; actual bytes: {unparsed:?})"))
} else {
Cow::from("")
}
);
}
Err(_) => {}
}
}
fuzz_target!(|data: &[u8]| do_fuzz(data));
maybe_define_main!();

48
fuzz/fuzz_targets/shim.rs Normal file
View file

@ -0,0 +1,48 @@
use std::{
error::Error,
ffi::c_int,
{env, fs, ptr},
};
extern "C" {
// Initializer provided by libfuzzer-sys for creating an
// appropriate panic hook.
fn LLVMFuzzerInitialize(
argc: *const isize,
argv: *const *const *const u8,
) -> c_int;
// This is a magic function defined by libfuzzer-sys; use for replay.
#[allow(improper_ctypes)]
fn rust_fuzzer_test_input(input: &[u8]) -> i32;
}
#[allow(unused)]
pub fn main() -> Result<(), Box<dyn Error>> {
let mut count = 0usize;
unsafe {
let _ = LLVMFuzzerInitialize(ptr::null(), ptr::null());
}
for testcase in env::args_os().skip(1) {
let content = fs::read(testcase)?;
unsafe {
let _ = rust_fuzzer_test_input(&content);
}
count += 1;
}
println!("Executed {count} testcases successfully!");
if count == 0 {
println!("Did you mean to specify a testcase?");
}
Ok(())
}
#[macro_export]
macro_rules! maybe_define_main {
() => {
#[cfg(not(fuzzing))]
fn main() {
let _ = $crate::shim::main();
}
};
}

View file

@ -0,0 +1,103 @@
#![cfg_attr(fuzzing, no_main)]
use std::borrow::Cow;
use libfuzzer_sys::{
arbitrary,
arbitrary::{Arbitrary, Unstructured},
fuzz_target,
};
use jiff::fmt::strtime::parse;
mod shim;
#[derive(Debug)]
struct Input<'a> {
format: &'a str,
input: &'a str,
}
impl<'a> Arbitrary<'a> for Input<'a> {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let fmt_len: u8 = u.arbitrary()?;
let in_len: u16 = u.arbitrary()?;
let format = u.bytes(fmt_len as usize)?;
let format = core::str::from_utf8(format)
.map_err(|_| arbitrary::Error::IncorrectFormat)?;
let input = u.bytes(in_len as usize)?;
let input = core::str::from_utf8(input)
.map_err(|_| arbitrary::Error::IncorrectFormat)?;
Ok(Input { format, input })
}
fn arbitrary_take_rest(
mut u: Unstructured<'a>,
) -> arbitrary::Result<Self> {
let len: u8 = u.arbitrary()?;
// ignored in take rest, but keep it consistent
let _in_len: u16 = u.arbitrary()?;
let format = u.bytes(len as usize)?;
let format = core::str::from_utf8(format)
.map_err(|_| arbitrary::Error::IncorrectFormat)?;
let input = u.take_rest();
let input = core::str::from_utf8(input)
.map_err(|_| arbitrary::Error::IncorrectFormat)?;
Ok(Input { format, input })
}
}
fn do_fuzz(src: Input) {
if let Ok(first) = parse(src.format, src.input) {
let mut unparsed = Vec::with_capacity(src.input.len());
if first.format(src.format, &mut unparsed).is_err() {
// There are a number of reasons why this may fail. the formatter
// is simply not as strong as the parser, so we accept failure
// here.
return;
}
match parse(src.format, &unparsed) {
Ok(second) => {
// There's not a direct equality here. To get around this, we
// compare unparsed with doubly-unparsed.
let mut unparsed_again = Vec::with_capacity(unparsed.len());
second.format(src.format, &mut unparsed_again).expect(
"We parsed it (twice!), so we should be able to print it",
);
assert_eq!(
unparsed,
unparsed_again,
"expected the initially parsed value \
to be equal to the value after \
printing and re-parsing; \
found `{}', expected `{}'",
String::from_utf8_lossy(&unparsed_again),
String::from_utf8_lossy(&unparsed),
);
}
Err(e) if cfg!(not(feature = "relaxed")) => {
let unparsed_str = String::from_utf8_lossy(&unparsed);
panic!(
"should be able to parse a printed value; \
failed with `{e}` at: `{unparsed_str}`{}, \
corresponding to {first:?}",
if matches!(unparsed_str, Cow::Owned(_)) {
Cow::from(format!(
" (lossy; actual bytes: {unparsed:?})"
))
} else {
Cow::from("")
}
);
}
Err(_) => {}
}
}
}
fuzz_target!(|data: Input<'_>| do_fuzz(data));
maybe_define_main!();

View file

@ -0,0 +1,51 @@
#![cfg_attr(fuzzing, no_main)]
use std::borrow::Cow;
use libfuzzer_sys::fuzz_target;
use jiff::fmt::temporal;
mod shim;
fn do_fuzz(data: &[u8]) {
const TEMPORAL_PARSER: temporal::SpanParser = temporal::SpanParser::new();
const TEMPORAL_PRINTER: temporal::SpanPrinter =
temporal::SpanPrinter::new();
let Ok(first) = TEMPORAL_PARSER.parse_span(data) else { return };
// get a good start at least
let mut unparsed = Vec::with_capacity(data.len());
TEMPORAL_PRINTER
.print_span(&first, &mut unparsed)
.expect("we parsed it, so we should be able to print it");
match TEMPORAL_PARSER.parse_span(&unparsed) {
Ok(second) => {
assert_eq!(
first,
second.fieldwise(),
"expected the initially parsed value \
to be equal to the value after printing and re-parsing",
);
}
Err(e) if cfg!(not(feature = "relaxed")) => {
let unparsed_str = String::from_utf8_lossy(&unparsed);
panic!(
"should be able to parse a printed value; \
failed with `{e}` at: `{unparsed_str}`{}, \
corresponding to {first:?}",
if matches!(unparsed_str, Cow::Owned(_)) {
Cow::from(format!(" (lossy; actual bytes: {unparsed:?})"))
} else {
Cow::from("")
}
);
}
Err(_) => {}
}
}
fuzz_target!(|data: &[u8]| do_fuzz(data));
maybe_define_main!();

View file

@ -3,7 +3,7 @@ use core::time::Duration as UnsignedDuration;
use crate::{
civil::{DateTime, Era, ISOWeekDate, Time, Weekday},
duration::{Duration, SDuration},
error::{err, Error, ErrorContext},
error::{civil::Error as E, Error, ErrorContext},
fmt::{
self,
temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER},
@ -903,7 +903,9 @@ impl Date {
let weekday = weekday.to_iweekday();
let idate = self.to_idate_const();
Ok(Date::from_idate_const(
idate.nth_weekday_of_month(nth, weekday).map_err(Error::shared)?,
idate
.nth_weekday_of_month(nth, weekday)
.map_err(Error::itime_range)?,
))
}
@ -1057,7 +1059,7 @@ impl Date {
let nth = t::SpanWeeks::try_new("nth weekday", nth)?;
if nth == C(0) {
Err(err!("nth weekday cannot be `0`"))
Err(Error::slim_range("nth weekday"))
} else if nth > C(0) {
let nth = nth.max(C(1));
let weekday_diff = weekday.since_ranged(self.weekday().next());
@ -1515,14 +1517,8 @@ impl Date {
-1 => self.yesterday(),
1 => self.tomorrow(),
days => {
let days = UnixEpochDay::try_new("days", days).with_context(
|| {
err!(
"{days} computed from duration {duration:?} \
overflows Jiff's datetime limits",
)
},
)?;
let days = UnixEpochDay::try_new("days", days)
.context(E::OverflowDaysDuration)?;
let days =
self.to_unix_epoch_day().try_checked_add("days", days)?;
Ok(Date::from_unix_epoch_day(days))
@ -2941,11 +2937,9 @@ impl DateDifference {
//
// NOTE: I take the above back. It's actually possible for the
// months component to overflow when largest=month.
return Err(err!(
"rounding the span between two dates must use days \
or bigger for its units, but found {units}",
units = largest.plural(),
));
return Err(Error::from(E::RoundMustUseDaysOrBigger {
unit: largest,
}));
}
if largest <= Unit::Week {
let mut weeks = t::SpanWeeks::rfrom(C(0));
@ -3197,13 +3191,13 @@ impl DateWith {
Some(DateWithDay::OfYear(day)) => {
let year = year.get_unchecked();
let idate = IDate::from_day_of_year(year, day)
.map_err(Error::shared)?;
.map_err(Error::itime_range)?;
return Ok(Date::from_idate_const(idate));
}
Some(DateWithDay::OfYearNoLeap(day)) => {
let year = year.get_unchecked();
let idate = IDate::from_day_of_year_no_leap(year, day)
.map_err(Error::shared)?;
.map_err(Error::itime_range)?;
return Ok(Date::from_idate_const(idate));
}
};

View file

@ -5,7 +5,7 @@ use crate::{
datetime, Date, DateWith, Era, ISOWeekDate, Time, TimeWith, Weekday,
},
duration::{Duration, SDuration},
error::{err, Error, ErrorContext},
error::{civil::Error as E, Error, ErrorContext},
fmt::{
self,
temporal::{self, DEFAULT_DATETIME_PARSER},
@ -1695,25 +1695,18 @@ impl DateTime {
{
(true, true) => Ok(self),
(false, true) => {
let new_date =
old_date.checked_add(span).with_context(|| {
err!("failed to add {span} to {old_date}")
})?;
let new_date = old_date
.checked_add(span)
.context(E::FailedAddSpanDate)?;
Ok(DateTime::from_parts(new_date, old_time))
}
(true, false) => {
let (new_time, leftovers) =
old_time.overflowing_add(span).with_context(|| {
err!("failed to add {span} to {old_time}")
})?;
let new_date =
old_date.checked_add(leftovers).with_context(|| {
err!(
"failed to add overflowing span, {leftovers}, \
from adding {span} to {old_time}, \
to {old_date}",
)
})?;
let (new_time, leftovers) = old_time
.overflowing_add(span)
.context(E::FailedAddSpanTime)?;
let new_date = old_date
.checked_add(leftovers)
.context(E::FailedAddSpanOverflowing)?;
Ok(DateTime::from_parts(new_date, new_time))
}
(false, false) => self.checked_add_span_general(&span),
@ -1727,20 +1720,14 @@ impl DateTime {
let span_date = span.without_lower(Unit::Day);
let span_time = span.only_lower(Unit::Day);
let (new_time, leftovers) =
old_time.overflowing_add(span_time).with_context(|| {
err!("failed to add {span_time} to {old_time}")
})?;
let new_date = old_date.checked_add(span_date).with_context(|| {
err!("failed to add {span_date} to {old_date}")
})?;
let new_date = new_date.checked_add(leftovers).with_context(|| {
err!(
"failed to add overflowing span, {leftovers}, \
from adding {span_time} to {old_time}, \
to {new_date}",
)
})?;
let (new_time, leftovers) = old_time
.overflowing_add(span_time)
.context(E::FailedAddSpanTime)?;
let new_date =
old_date.checked_add(span_date).context(E::FailedAddSpanDate)?;
let new_date = new_date
.checked_add(leftovers)
.context(E::FailedAddSpanOverflowing)?;
Ok(DateTime::from_parts(new_date, new_time))
}
@ -1751,13 +1738,9 @@ impl DateTime {
) -> Result<DateTime, Error> {
let (date, time) = (self.date(), self.time());
let (new_time, leftovers) = time.overflowing_add_duration(duration)?;
let new_date = date.checked_add(leftovers).with_context(|| {
err!(
"failed to add overflowing signed duration, {leftovers:?}, \
from adding {duration:?} to {time},
to {date}",
)
})?;
let new_date = date
.checked_add(leftovers)
.context(E::FailedAddDurationOverflowing)?;
Ok(DateTime::from_parts(new_date, new_time))
}
@ -3552,9 +3535,10 @@ impl DateTimeRound {
// it for good reasons.
match self.smallest {
Unit::Year | Unit::Month | Unit::Week => {
return Err(err!(
"rounding datetimes does not support {unit}",
unit = self.smallest.plural()
return Err(Error::from(
crate::error::util::RoundingIncrementError::Unsupported {
unit: self.smallest,
},
));
}
// We don't do any rounding in this case, so just bail now.
@ -3592,9 +3576,7 @@ impl DateTimeRound {
// supported datetimes.
let end = start
.checked_add(Span::new().days_ranged(days_len))
.with_context(|| {
err!("adding {days_len} days to {start} failed")
})?;
.context(E::FailedAddDays)?;
Ok(DateTime::from_parts(end, time))
}

View file

@ -1,6 +1,7 @@
use crate::{
civil::{Date, DateTime, Weekday},
error::{err, Error},
error::{civil::Error as E, Error},
fmt::temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER},
util::{
rangeint::RInto,
t::{self, ISOWeek, ISOYear, C},
@ -35,6 +36,51 @@ use crate::{
/// specifically want a week oriented calendar, it's likely that you'll never
/// need to care about this type.
///
/// # Parsing and printing
///
/// The `ISOWeekDate` type provides convenient trait implementations of
/// [`std::str::FromStr`] and [`std::fmt::Display`]. These use the format
/// specified by ISO 8601 for week dates:
///
/// ```
/// use jiff::civil::ISOWeekDate;
///
/// let week_date: ISOWeekDate = "2024-W24-7".parse()?;
/// assert_eq!(week_date.to_string(), "2024-W24-7");
/// assert_eq!(week_date.date().to_string(), "2024-06-16");
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// ISO 8601 allows the `-` separator to be absent:
///
/// ```
/// use jiff::civil::ISOWeekDate;
///
/// let week_date: ISOWeekDate = "2024W241".parse()?;
/// assert_eq!(week_date.to_string(), "2024-W24-1");
/// assert_eq!(week_date.date().to_string(), "2024-06-10");
///
/// // But you cannot mix and match. Either `-` separates
/// // both the year and week, or neither.
/// assert!("2024W24-1".parse::<ISOWeekDate>().is_err());
/// assert!("2024-W241".parse::<ISOWeekDate>().is_err());
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// And the `W` may also be lowercase:
///
/// ```
/// use jiff::civil::ISOWeekDate;
///
/// let week_date: ISOWeekDate = "2024-w24-2".parse()?;
/// assert_eq!(week_date.to_string(), "2024-W24-2");
/// assert_eq!(week_date.date().to_string(), "2024-06-11");
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// # Default value
///
/// For convenience, this type implements the `Default` trait. Its default
@ -665,9 +711,7 @@ impl ISOWeekDate {
debug_assert_eq!(t::Year::MIN, ISOYear::MIN);
debug_assert_eq!(t::Year::MAX, ISOYear::MAX);
if week == C(53) && !is_long_year(year) {
return Err(err!(
"ISO week number `{week}` is invalid for year `{year}`"
));
return Err(Error::from(E::InvalidISOWeekNumber));
}
// And also, the maximum Date constrains what we can utter with
// ISOWeekDate so that we can preserve infallible conversions between
@ -747,6 +791,24 @@ impl core::fmt::Debug for ISOWeekDate {
}
}
impl core::fmt::Display for ISOWeekDate {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use crate::fmt::StdFmtWrite;
DEFAULT_DATETIME_PRINTER
.print_iso_week_date(self, StdFmtWrite(f))
.map_err(|_| core::fmt::Error)
}
}
impl core::str::FromStr for ISOWeekDate {
type Err = Error;
fn from_str(string: &str) -> Result<ISOWeekDate, Error> {
DEFAULT_DATETIME_PARSER.parse_iso_week_date(string)
}
}
impl Eq for ISOWeekDate {}
impl PartialEq for ISOWeekDate {
@ -808,6 +870,60 @@ impl<'a> From<&'a Zoned> for ISOWeekDate {
}
}
#[cfg(feature = "serde")]
impl serde_core::Serialize for ISOWeekDate {
#[inline]
fn serialize<S: serde_core::Serializer>(
&self,
serializer: S,
) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
#[cfg(feature = "serde")]
impl<'de> serde_core::Deserialize<'de> for ISOWeekDate {
#[inline]
fn deserialize<D: serde_core::Deserializer<'de>>(
deserializer: D,
) -> Result<ISOWeekDate, D::Error> {
use serde_core::de;
struct ISOWeekDateVisitor;
impl<'de> de::Visitor<'de> for ISOWeekDateVisitor {
type Value = ISOWeekDate;
fn expecting(
&self,
f: &mut core::fmt::Formatter,
) -> core::fmt::Result {
f.write_str("an ISO 8601 week date string")
}
#[inline]
fn visit_bytes<E: de::Error>(
self,
value: &[u8],
) -> Result<ISOWeekDate, E> {
DEFAULT_DATETIME_PARSER
.parse_iso_week_date(value)
.map_err(de::Error::custom)
}
#[inline]
fn visit_str<E: de::Error>(
self,
value: &str,
) -> Result<ISOWeekDate, E> {
self.visit_bytes(value.as_bytes())
}
}
deserializer.deserialize_str(ISOWeekDateVisitor)
}
}
#[cfg(test)]
impl quickcheck::Arbitrary for ISOWeekDate {
fn arbitrary(g: &mut quickcheck::Gen) -> ISOWeekDate {

View file

@ -3,7 +3,7 @@ use core::time::Duration as UnsignedDuration;
use crate::{
civil::{Date, DateTime},
duration::{Duration, SDuration},
error::{err, Error, ErrorContext},
error::{civil::Error as E, Error, ErrorContext},
fmt::{
self,
temporal::{self, DEFAULT_DATETIME_PARSER},
@ -950,7 +950,6 @@ impl Time {
self,
duration: SignedDuration,
) -> Result<Time, Error> {
let original = duration;
let start = t::NoUnits128::rfrom(self.to_nanosecond());
let duration = t::NoUnits128::new_unchecked(duration.as_nanos());
// This can never fail because the maximum duration fits into a
@ -958,15 +957,7 @@ impl Time {
// integer can never overflow a 128-bit integer.
let end = start.try_checked_add("nanoseconds", duration).unwrap();
let end = CivilDayNanosecond::try_rfrom("nanoseconds", end)
.with_context(|| {
err!(
"adding signed duration {duration:?}, equal to
{nanos} nanoseconds, to {time} overflowed",
duration = original,
nanos = original.as_nanos(),
time = self,
)
})?;
.context(E::OverflowTimeNanoseconds)?;
Ok(Time::from_nanosecond(end))
}
@ -2603,11 +2594,9 @@ impl TimeDifference {
}
let largest = self.round.get_largest().unwrap_or(Unit::Hour);
if largest > Unit::Hour {
return Err(err!(
"rounding the span between two times must use hours \
or smaller for its units, but found {units}",
units = largest.plural(),
));
return Err(Error::from(E::RoundMustUseHoursOrSmaller {
unit: largest,
}));
}
let start = t1.to_nanosecond();
let end = t2.to_nanosecond();
@ -3012,22 +3001,13 @@ impl TimeWith {
None => self.original.subsec_nanosecond_ranged(),
Some(subsec_nanosecond) => {
if self.millisecond.is_some() {
return Err(err!(
"cannot set both TimeWith::millisecond \
and TimeWith::subsec_nanosecond",
));
return Err(Error::from(E::IllegalTimeWithMillisecond));
}
if self.microsecond.is_some() {
return Err(err!(
"cannot set both TimeWith::microsecond \
and TimeWith::subsec_nanosecond",
));
return Err(Error::from(E::IllegalTimeWithMicrosecond));
}
if self.nanosecond.is_some() {
return Err(err!(
"cannot set both TimeWith::nanosecond \
and TimeWith::subsec_nanosecond",
));
return Err(Error::from(E::IllegalTimeWithNanosecond));
}
SubsecNanosecond::try_new(
"subsec_nanosecond",

View file

@ -1,7 +1,7 @@
use core::time::Duration as UnsignedDuration;
use crate::{
error::{err, ErrorContext},
error::{duration::Error as E, ErrorContext},
Error, SignedDuration, Span,
};
@ -24,12 +24,8 @@ impl Duration {
Duration::Span(span) => Ok(SDuration::Span(span)),
Duration::Signed(sdur) => Ok(SDuration::Absolute(sdur)),
Duration::Unsigned(udur) => {
let sdur =
SignedDuration::try_from(udur).with_context(|| {
err!(
"unsigned duration {udur:?} exceeds Jiff's limits"
)
})?;
let sdur = SignedDuration::try_from(udur)
.context(E::RangeUnsignedDuration)?;
Ok(SDuration::Absolute(sdur))
}
}
@ -91,9 +87,8 @@ impl Duration {
// Otherwise, this is the only failure point in this entire
// routine. And specifically, we fail here in precisely
// the cases where `udur.as_secs() > |i64::MIN|`.
-SignedDuration::try_from(udur).with_context(|| {
err!("failed to negate unsigned duration {udur:?}")
})?
-SignedDuration::try_from(udur)
.context(E::FailedNegateUnsignedDuration)?
};
Ok(Duration::Signed(sdur))
}

84
src/error/civil.rs Normal file
View file

@ -0,0 +1,84 @@
use crate::{error, Unit};
#[derive(Clone, Debug)]
pub(crate) enum Error {
FailedAddDays,
FailedAddDurationOverflowing,
FailedAddSpanDate,
FailedAddSpanOverflowing,
FailedAddSpanTime,
IllegalTimeWithMicrosecond,
IllegalTimeWithMillisecond,
IllegalTimeWithNanosecond,
InvalidISOWeekNumber,
OverflowDaysDuration,
OverflowTimeNanoseconds,
RoundMustUseDaysOrBigger { unit: Unit },
RoundMustUseHoursOrSmaller { unit: Unit },
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::Civil(err).into()
}
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
FailedAddDays => f.write_str("failed to add days to date"),
FailedAddDurationOverflowing => {
f.write_str("failed to add overflowing duration")
}
FailedAddSpanDate => f.write_str("failed to add span to date"),
FailedAddSpanOverflowing => {
f.write_str("failed to add overflowing span")
}
FailedAddSpanTime => f.write_str("failed to add span to time"),
IllegalTimeWithMicrosecond => f.write_str(
"cannot set both `TimeWith::microsecond` \
and `TimeWith::subsec_nanosecond`",
),
IllegalTimeWithMillisecond => f.write_str(
"cannot set both `TimeWith::millisecond` \
and `TimeWith::subsec_nanosecond`",
),
IllegalTimeWithNanosecond => f.write_str(
"cannot set both `TimeWith::nanosecond` \
and `TimeWith::subsec_nanosecond`",
),
InvalidISOWeekNumber => {
f.write_str("ISO week number is invalid for given year")
}
OverflowDaysDuration => f.write_str(
"number of days derived from duration exceed's \
Jiff's datetime limits",
),
OverflowTimeNanoseconds => {
f.write_str("adding duration to time overflowed")
}
RoundMustUseDaysOrBigger { unit } => write!(
f,
"rounding the span between two dates must use days \
or bigger for its units, but found {unit}",
unit = unit.plural(),
),
RoundMustUseHoursOrSmaller { unit } => write!(
f,
"rounding the span between two times must use hours \
or smaller for its units, but found {unit}",
unit = unit.plural(),
),
}
}
}

36
src/error/duration.rs Normal file
View file

@ -0,0 +1,36 @@
use crate::error;
#[derive(Clone, Debug)]
pub(crate) enum Error {
FailedNegateUnsignedDuration,
RangeUnsignedDuration,
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::Duration(err).into()
}
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
FailedNegateUnsignedDuration => {
f.write_str("failed to negate unsigned duration")
}
RangeUnsignedDuration => {
f.write_str("unsigned duration exceeds Jiff's limits")
}
}
}
}

82
src/error/fmt/friendly.rs Normal file
View file

@ -0,0 +1,82 @@
use crate::{error, util::escape};
#[derive(Clone, Debug)]
pub(crate) enum Error {
Empty,
ExpectedColonAfterMinute,
ExpectedIntegerAfterSign,
ExpectedMinuteAfterHour,
ExpectedOneMoreUnitAfterComma,
ExpectedOneSign,
ExpectedSecondAfterMinute,
ExpectedUnitSuffix,
ExpectedWhitespaceAfterComma { byte: u8 },
ExpectedWhitespaceAfterCommaEndOfInput,
Failed,
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::FmtFriendly(err).into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
Empty => f.write_str("an empty string is not valid"),
ExpectedColonAfterMinute => f.write_str(
"when parsing the `HH:MM:SS` format, \
expected to parse `:` following minute",
),
ExpectedIntegerAfterSign => f.write_str(
"expected duration to start \
with a unit value (a decimal integer) after an \
optional sign, but no integer was found",
),
ExpectedMinuteAfterHour => f.write_str(
"when parsing the `HH:MM:SS` format, \
expected to parse minute following hour",
),
ExpectedOneMoreUnitAfterComma => f.write_str(
"found comma at the end of duration, \
but a comma indicates at least one more \
unit follows",
),
ExpectedOneSign => f.write_str(
"expected to find either a prefix sign (+/-) or \
a suffix sign (`ago`), but found both",
),
ExpectedSecondAfterMinute => f.write_str(
"when parsing the `HH:MM:SS` format, \
expected to parse second following minute",
),
ExpectedUnitSuffix => f.write_str(
"expected to find unit designator suffix \
(e.g., `years` or `secs`) after parsing \
integer",
),
ExpectedWhitespaceAfterComma { byte } => write!(
f,
"expected whitespace after comma, but found `{byte}`",
byte = escape::Byte(byte),
),
ExpectedWhitespaceAfterCommaEndOfInput => f.write_str(
"expected whitespace after comma, but found end of input",
),
Failed => f.write_str(
"failed to parse input in the \"friendly\" duration format",
),
}
}
}

87
src/error/fmt/mod.rs Normal file
View file

@ -0,0 +1,87 @@
use crate::{error, util::escape};
pub(crate) mod friendly;
pub(crate) mod offset;
pub(crate) mod rfc2822;
pub(crate) mod rfc9557;
pub(crate) mod strtime;
pub(crate) mod temporal;
pub(crate) mod util;
#[derive(Clone, Debug)]
pub(crate) enum Error {
HybridDurationEmpty,
HybridDurationPrefix {
sign: u8,
},
IntoFull {
#[cfg(feature = "alloc")]
value: alloc::boxed::Box<str>,
#[cfg(feature = "alloc")]
unparsed: alloc::boxed::Box<[u8]>,
},
StdFmtWriteAdapter,
}
impl Error {
pub(crate) fn into_full_error(
_value: &dyn core::fmt::Display,
_unparsed: &[u8],
) -> Error {
Error::IntoFull {
#[cfg(feature = "alloc")]
value: alloc::string::ToString::to_string(_value).into(),
#[cfg(feature = "alloc")]
unparsed: _unparsed.into(),
}
}
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::Fmt(err).into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
HybridDurationEmpty => f.write_str(
"an empty string is not a valid duration in either \
the ISO 8601 format or Jiff's \"friendly\" format",
),
HybridDurationPrefix { sign } => write!(
f,
"found nothing after sign `{sign}`, \
which is not a valid duration in either \
the ISO 8601 format or Jiff's \"friendly\" format",
sign = escape::Byte(sign),
),
#[cfg(not(feature = "alloc"))]
IntoFull { .. } => f.write_str(
"parsed value, but unparsed input remains \
(expected no unparsed input)",
),
#[cfg(feature = "alloc")]
IntoFull { ref value, ref unparsed } => write!(
f,
"parsed value '{value}', but unparsed input {unparsed:?} \
remains (expected no unparsed input)",
unparsed = escape::Bytes(unparsed),
),
StdFmtWriteAdapter => {
f.write_str("an error occurred when formatting an argument")
}
}
}
}

153
src/error/fmt/offset.rs Normal file
View file

@ -0,0 +1,153 @@
use crate::{error, util::escape};
#[derive(Clone, Debug)]
pub(crate) enum Error {
ColonAfterHours,
EndOfInput,
EndOfInputHour,
EndOfInputMinute,
EndOfInputNumeric,
EndOfInputSecond,
InvalidHours,
InvalidMinutes,
InvalidSeconds,
InvalidSecondsFractional,
InvalidSign,
InvalidSignPlusOrMinus,
MissingMinuteAfterHour,
MissingSecondAfterMinute,
NoColonAfterHours,
ParseHours,
ParseMinutes,
ParseSeconds,
PrecisionLoss,
RangeHours,
RangeMinutes,
RangeSeconds,
SeparatorAfterHours,
SeparatorAfterMinutes,
SubminutePrecisionNotEnabled,
SubsecondPrecisionNotEnabled,
UnexpectedLetterOffsetNoZulu(u8),
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::FmtOffset(err).into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
ColonAfterHours => f.write_str(
"parsed hour component of time zone offset, \
but found colon after hours which is not allowed",
),
EndOfInput => {
f.write_str("expected UTC offset, but found end of input")
}
EndOfInputHour => f.write_str(
"expected two digit hour after sign, but found end of input",
),
EndOfInputMinute => f.write_str(
"expected two digit minute after hours, \
but found end of input",
),
EndOfInputNumeric => f.write_str(
"expected UTC numeric offset, but found end of input",
),
EndOfInputSecond => f.write_str(
"expected two digit second after minutes, \
but found end of input",
),
InvalidHours => {
f.write_str("failed to parse hours in UTC numeric offset")
}
InvalidMinutes => {
f.write_str("failed to parse minutes in UTC numeric offset")
}
InvalidSeconds => {
f.write_str("failed to parse seconds in UTC numeric offset")
}
InvalidSecondsFractional => f.write_str(
"failed to parse fractional seconds in UTC numeric offset",
),
InvalidSign => {
f.write_str("failed to parse sign in UTC numeric offset")
}
InvalidSignPlusOrMinus => f.write_str(
"expected `+` or `-` sign at start of UTC numeric offset",
),
MissingMinuteAfterHour => f.write_str(
"parsed hour component of time zone offset, \
but could not find required minute component",
),
MissingSecondAfterMinute => f.write_str(
"parsed hour and minute components of time zone offset, \
but could not find required second component",
),
NoColonAfterHours => f.write_str(
"parsed hour component of time zone offset, \
but could not find required colon separator",
),
ParseHours => f.write_str(
"failed to parse hours (requires a two digit integer)",
),
ParseMinutes => f.write_str(
"failed to parse minutes (requires a two digit integer)",
),
ParseSeconds => f.write_str(
"failed to parse seconds (requires a two digit integer)",
),
PrecisionLoss => f.write_str(
"due to precision loss, offset is \
rounded to a value that is out of bounds",
),
RangeHours => {
f.write_str("hour in time zone offset is out of range")
}
RangeMinutes => {
f.write_str("minute in time zone offset is out of range")
}
RangeSeconds => {
f.write_str("second in time zone offset is out of range")
}
SeparatorAfterHours => f.write_str(
"failed to parse separator after hours in \
UTC numeric offset",
),
SeparatorAfterMinutes => f.write_str(
"failed to parse separator after minutes in \
UTC numeric offset",
),
SubminutePrecisionNotEnabled => f.write_str(
"subminute precision for UTC numeric offset \
is not enabled in this context (must provide only \
integral minutes)",
),
SubsecondPrecisionNotEnabled => f.write_str(
"subsecond precision for UTC numeric offset \
is not enabled in this context (must provide only \
integral minutes or seconds)",
),
UnexpectedLetterOffsetNoZulu(byte) => write!(
f,
"found `{z}` where a numeric UTC offset \
was expected (this context does not permit \
the Zulu offset)",
z = escape::Byte(byte),
),
}
}
}

231
src/error/fmt/rfc2822.rs Normal file
View file

@ -0,0 +1,231 @@
use crate::{civil::Weekday, error, util::escape};
#[derive(Clone, Debug)]
pub(crate) enum Error {
CommentClosingParenWithoutOpen,
CommentOpeningParenWithoutClose,
CommentTooManyNestedParens,
EndOfInputDay,
Empty,
EmptyAfterWhitespace,
EndOfInputComma,
EndOfInputHour,
EndOfInputMinute,
EndOfInputMonth,
EndOfInputOffset,
EndOfInputSecond,
EndOfInputTimeSeparator,
FailedTimestamp,
FailedZoned,
InconsistentWeekday { parsed: Weekday, from_date: Weekday },
InvalidDate,
InvalidHour,
InvalidMinute,
InvalidMonth,
InvalidObsoleteOffset,
InvalidOffsetHour,
InvalidOffsetMinute,
InvalidSecond,
InvalidWeekday { got_non_digit: u8 },
InvalidYear,
NegativeYear,
ParseDay,
ParseHour,
ParseMinute,
ParseOffsetHour,
ParseOffsetMinute,
ParseSecond,
ParseYear,
TooShortMonth { len: u8 },
TooShortOffset,
TooShortWeekday { got_non_digit: u8, len: u8 },
TooShortYear { len: u8 },
UnexpectedByteComma { byte: u8 },
UnexpectedByteTimeSeparator { byte: u8 },
WhitespaceAfterDay,
WhitespaceAfterMonth,
WhitespaceAfterTime,
WhitespaceAfterTimeForObsoleteOffset,
WhitespaceAfterYear,
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::FmtRfc2822(err).into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
CommentClosingParenWithoutOpen => f.write_str(
"found closing parenthesis in comment with \
no matching opening parenthesis",
),
CommentOpeningParenWithoutClose => f.write_str(
"found opening parenthesis in comment with \
no matching closing parenthesis",
),
CommentTooManyNestedParens => {
f.write_str("found too many nested parenthesis in comment")
}
Empty => {
f.write_str("expected RFC 2822 datetime, but got empty string")
}
EmptyAfterWhitespace => f.write_str(
"expected RFC 2822 datetime, but got empty string \
after trimming leading whitespace",
),
EndOfInputComma => f.write_str(
"expected comma after parsed weekday in \
RFC 2822 datetime, but found end of input instead",
),
EndOfInputDay => {
f.write_str("expected numeric day, but found end of input")
}
EndOfInputHour => {
f.write_str("expected two digit hour, but found end of input")
}
EndOfInputMinute => f.write_str(
"expected two digit minute, but found end of input",
),
EndOfInputMonth => f.write_str(
"expected abbreviated month name, but found end of input",
),
EndOfInputOffset => f.write_str(
"expected sign for time zone offset, \
(or a legacy time zone name abbreviation), \
but found end of input",
),
EndOfInputSecond => f.write_str(
"expected two digit second, but found end of input",
),
EndOfInputTimeSeparator => f.write_str(
"expected time separator of `:`, but found end of input",
),
FailedTimestamp => f.write_str(
"failed to parse RFC 2822 datetime into Jiff timestamp",
),
FailedZoned => f.write_str(
"failed to parse RFC 2822 datetime into Jiff zoned datetime",
),
InconsistentWeekday { parsed, from_date } => write!(
f,
"found parsed weekday of `{parsed:?}`, \
but parsed datetime has weekday `{from_date:?}`",
),
InvalidDate => f.write_str("invalid date"),
InvalidHour => f.write_str("invalid hour"),
InvalidMinute => f.write_str("invalid minute"),
InvalidMonth => f.write_str(
"expected abbreviated month name, \
but did not recognize a valid abbreviated month name",
),
InvalidObsoleteOffset => f.write_str(
"expected obsolete RFC 2822 time zone abbreviation, \
but did not recognize a valid abbreviation",
),
InvalidOffsetHour => f.write_str("invalid time zone offset hour"),
InvalidOffsetMinute => {
f.write_str("invalid time zone offset minute")
}
InvalidSecond => f.write_str("invalid second"),
InvalidWeekday { got_non_digit } => write!(
f,
"expected day at beginning of RFC 2822 datetime \
since first non-whitespace byte, `{first}`, \
is not a digit, but did not recognize a valid \
weekday abbreviation",
first = escape::Byte(got_non_digit),
),
InvalidYear => f.write_str("invalid year"),
NegativeYear => f.write_str(
"datetime has negative year, \
which cannot be formatted with RFC 2822",
),
ParseDay => f.write_str("failed to parse day"),
ParseHour => f.write_str(
"failed to parse hour (expects a two digit integer)",
),
ParseMinute => f.write_str(
"failed to parse minute (expects a two digit integer)",
),
ParseOffsetHour => {
f.write_str("failed to parse hours from time zone offset")
}
ParseOffsetMinute => {
f.write_str("failed to parse minutes from time zone offset")
}
ParseSecond => f.write_str(
"failed to parse second (expects a two digit integer)",
),
ParseYear => f.write_str(
"failed to parse year \
(expects a two, three or four digit integer)",
),
TooShortMonth { len } => write!(
f,
"expected abbreviated month name, but remaining input \
is too short (remaining bytes is {len})",
),
TooShortOffset => write!(
f,
"expected at least four digits for time zone offset \
after sign, but found fewer than four bytes remaining",
),
TooShortWeekday { got_non_digit, len } => write!(
f,
"expected day at beginning of RFC 2822 datetime \
since first non-whitespace byte, `{first}`, \
is not a digit, but given string is too short \
(length is {len})",
first = escape::Byte(got_non_digit),
),
TooShortYear { len } => write!(
f,
"expected at least two ASCII digits for parsing \
a year, but only found {len}",
),
UnexpectedByteComma { byte } => write!(
f,
"expected comma after parsed weekday in \
RFC 2822 datetime, but found `{got}` instead",
got = escape::Byte(byte),
),
UnexpectedByteTimeSeparator { byte } => write!(
f,
"expected time separator of `:`, but found `{got}`",
got = escape::Byte(byte),
),
WhitespaceAfterDay => {
f.write_str("expected whitespace after parsing day")
}
WhitespaceAfterMonth => f.write_str(
"expected whitespace after parsing abbreviated month name",
),
WhitespaceAfterTime => f.write_str(
"expected whitespace after parsing time: \
expected at least one whitespace character \
(space or tab), but found none",
),
WhitespaceAfterTimeForObsoleteOffset => f.write_str(
"expected obsolete RFC 2822 time zone abbreviation, \
but found no remaining non-whitespace characters \
after time",
),
WhitespaceAfterYear => {
f.write_str("expected whitespace after parsing year")
}
}
}
}

114
src/error/fmt/rfc9557.rs Normal file
View file

@ -0,0 +1,114 @@
use crate::{error, util::escape};
#[derive(Clone, Debug)]
pub(crate) enum Error {
EndOfInputAnnotation,
EndOfInputAnnotationClose,
EndOfInputAnnotationKey,
EndOfInputAnnotationSeparator,
EndOfInputAnnotationValue,
EndOfInputTzAnnotationClose,
UnexpectedByteAnnotation { byte: u8 },
UnexpectedByteAnnotationClose { byte: u8 },
UnexpectedByteAnnotationKey { byte: u8 },
UnexpectedByteAnnotationValue { byte: u8 },
UnexpectedByteAnnotationSeparator { byte: u8 },
UnexpectedByteTzAnnotationClose { byte: u8 },
UnexpectedSlashAnnotationSeparator,
UnsupportedAnnotationCritical,
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::FmtRfc9557(err).into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
EndOfInputAnnotation => f.write_str(
"expected the start of an RFC 9557 annotation or IANA \
time zone component name, but found end of input instead",
),
EndOfInputAnnotationClose => f.write_str(
"expected an `]` after parsing an RFC 9557 annotation key \
and value, but found end of input instead",
),
EndOfInputAnnotationKey => f.write_str(
"expected the start of an RFC 9557 annotation key, \
but found end of input instead",
),
EndOfInputAnnotationSeparator => f.write_str(
"expected an `=` after parsing an RFC 9557 annotation key, \
but found end of input instead",
),
EndOfInputAnnotationValue => f.write_str(
"expected the start of an RFC 9557 annotation value, \
but found end of input instead",
),
EndOfInputTzAnnotationClose => f.write_str(
"expected an `]` after parsing an RFC 9557 time zone \
annotation, but found end of input instead",
),
UnexpectedByteAnnotation { byte } => write!(
f,
"expected ASCII alphabetic byte (or underscore or period) \
at the start of an RFC 9557 annotation or time zone \
component name, but found `{byte}` instead",
byte = escape::Byte(byte),
),
UnexpectedByteAnnotationClose { byte } => write!(
f,
"expected an `]` after parsing an RFC 9557 annotation key \
and value, but found `{byte}` instead",
byte = escape::Byte(byte),
),
UnexpectedByteAnnotationKey { byte } => write!(
f,
"expected lowercase alphabetic byte (or underscore) \
at the start of an RFC 9557 annotation key, \
but found `{byte}` instead",
byte = escape::Byte(byte),
),
UnexpectedByteAnnotationValue { byte } => write!(
f,
"expected alphanumeric ASCII byte \
at the start of an RFC 9557 annotation value, \
but found `{byte}` instead",
byte = escape::Byte(byte),
),
UnexpectedByteAnnotationSeparator { byte } => write!(
f,
"expected an `=` after parsing an RFC 9557 annotation \
key, but found `{byte}` instead",
byte = escape::Byte(byte),
),
UnexpectedByteTzAnnotationClose { byte } => write!(
f,
"expected an `]` after parsing an RFC 9557 time zone \
annotation, but found `{byte}` instead",
byte = escape::Byte(byte),
),
UnexpectedSlashAnnotationSeparator => f.write_str(
"expected an `=` after parsing an RFC 9557 annotation \
key, but found `/` instead (time zone annotations must \
come first)",
),
UnsupportedAnnotationCritical => f.write_str(
"found unsupported RFC 9557 annotation \
with the critical flag (`!`) set",
),
}
}
}

517
src/error/fmt/strtime.rs Normal file
View file

@ -0,0 +1,517 @@
use crate::{civil::Weekday, error, tz::Offset, util::escape};
#[derive(Clone, Debug)]
pub(crate) enum Error {
ColonCount {
directive: u8,
},
DirectiveFailure {
directive: u8,
colons: u8,
},
DirectiveFailureDot {
directive: u8,
},
ExpectedDirectiveAfterColons,
ExpectedDirectiveAfterFlag {
flag: u8,
},
ExpectedDirectiveAfterWidth,
FailedStrftime,
FailedStrptime,
FailedWidth,
InvalidDate,
InvalidISOWeekDate,
InvalidWeekdayMonday {
got: Weekday,
},
InvalidWeekdaySunday {
got: Weekday,
},
MismatchOffset {
parsed: Offset,
got: Offset,
},
MismatchWeekday {
parsed: Weekday,
got: Weekday,
},
MissingTimeHourForFractional,
MissingTimeHourForMinute,
MissingTimeHourForSecond,
MissingTimeMinuteForFractional,
MissingTimeMinuteForSecond,
MissingTimeSecondForFractional,
RangeTimestamp,
RangeWidth,
RequiredDateForDateTime,
RequiredDateTimeForTimestamp,
RequiredDateTimeForZoned,
RequiredOffsetForTimestamp,
RequiredSomeDayForDate,
RequiredTimeForDateTime,
RequiredYearForDate,
UnconsumedStrptime {
#[cfg(feature = "alloc")]
remaining: alloc::boxed::Box<[u8]>,
},
UnexpectedEndAfterDot,
UnexpectedEndAfterPercent,
UnknownDirectiveAfterDot {
directive: u8,
},
UnknownDirective {
directive: u8,
},
ZonedOffsetOrTz,
}
impl Error {
pub(crate) fn unconsumed(_remaining: &[u8]) -> Error {
Error::UnconsumedStrptime {
#[cfg(feature = "alloc")]
remaining: _remaining.into(),
}
}
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::FmtStrtime(err).into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
ColonCount { directive } => write!(
f,
"invalid number of `:` in `%{directive}` directive",
directive = escape::Byte(directive),
),
DirectiveFailure { directive, colons } => write!(
f,
"%{colons}{directive} failed",
colons = escape::RepeatByte { byte: b':', count: colons },
directive = escape::Byte(directive),
),
DirectiveFailureDot { directive } => write!(
f,
"%.{directive} failed",
directive = escape::Byte(directive),
),
ExpectedDirectiveAfterColons => f.write_str(
"expected to find specifier directive after colons, \
but found end of format string",
),
ExpectedDirectiveAfterFlag { flag } => write!(
f,
"expected to find specifier directive after flag \
`{flag}`, but found end of format string",
flag = escape::Byte(flag),
),
ExpectedDirectiveAfterWidth => f.write_str(
"expected to find specifier directive after parsed width, \
but found end of format string",
),
FailedStrftime => f.write_str("strftime formatting failed"),
FailedStrptime => f.write_str("strptime parsing failed"),
FailedWidth => {
f.write_str("failed to parse conversion specifier width")
}
InvalidDate => f.write_str("invalid date"),
InvalidISOWeekDate => f.write_str("invalid ISO 8601 week date"),
InvalidWeekdayMonday { got } => write!(
f,
"weekday `{got:?}` is not valid for \
Monday based week number",
),
InvalidWeekdaySunday { got } => write!(
f,
"weekday `{got:?}` is not valid for \
Sunday based week number",
),
MissingTimeHourForFractional => f.write_str(
"parsing format did not include hour directive, \
but did include fractional second directive (cannot have \
smaller time units with bigger time units missing)",
),
MissingTimeHourForMinute => f.write_str(
"parsing format did not include hour directive, \
but did include minute directive (cannot have \
smaller time units with bigger time units missing)",
),
MissingTimeHourForSecond => f.write_str(
"parsing format did not include hour directive, \
but did include second directive (cannot have \
smaller time units with bigger time units missing)",
),
MissingTimeMinuteForFractional => f.write_str(
"parsing format did not include minute directive, \
but did include fractional second directive (cannot have \
smaller time units with bigger time units missing)",
),
MissingTimeMinuteForSecond => f.write_str(
"parsing format did not include minute directive, \
but did include second directive (cannot have \
smaller time units with bigger time units missing)",
),
MissingTimeSecondForFractional => f.write_str(
"parsing format did not include second directive, \
but did include fractional second directive (cannot have \
smaller time units with bigger time units missing)",
),
MismatchOffset { parsed, got } => write!(
f,
"parsed time zone offset `{parsed}`, but \
offset from timestamp and time zone is `{got}`",
),
MismatchWeekday { parsed, got } => write!(
f,
"parsed weekday `{parsed:?}` does not match \
weekday `{got:?}` from parsed date",
),
RangeTimestamp => f.write_str(
"parsed datetime and offset, \
but combining them into a zoned datetime \
is outside Jiff's supported timestamp range",
),
RangeWidth => write!(
f,
"parsed width is too big, max is {max}",
max = u8::MAX
),
RequiredDateForDateTime => {
f.write_str("date required to parse datetime")
}
RequiredDateTimeForTimestamp => {
f.write_str("datetime required to parse timestamp")
}
RequiredDateTimeForZoned => {
f.write_str("datetime required to parse zoned datetime")
}
RequiredOffsetForTimestamp => {
f.write_str("offset required to parse timestamp")
}
RequiredSomeDayForDate => f.write_str(
"a month/day, day-of-year or week date must be \
present to create a date, but none were found",
),
RequiredTimeForDateTime => {
f.write_str("time required to parse datetime")
}
RequiredYearForDate => f.write_str("year required to parse date"),
#[cfg(feature = "alloc")]
UnconsumedStrptime { ref remaining } => write!(
f,
"strptime expects to consume the entire input, but \
`{remaining}` remains unparsed",
remaining = escape::Bytes(remaining),
),
#[cfg(not(feature = "alloc"))]
UnconsumedStrptime {} => f.write_str(
"strptime expects to consume the entire input, but \
there is unparsed input remaining",
),
UnexpectedEndAfterDot => f.write_str(
"invalid format string, expected directive after `%.`",
),
UnexpectedEndAfterPercent => f.write_str(
"invalid format string, expected byte after `%`, \
but found end of format string",
),
UnknownDirective { directive } => write!(
f,
"found unrecognized specifier directive `{directive}`",
directive = escape::Byte(directive),
),
UnknownDirectiveAfterDot { directive } => write!(
f,
"found unrecognized specifier directive `{directive}` \
following `%.`",
directive = escape::Byte(directive),
),
ZonedOffsetOrTz => f.write_str(
"either offset (from `%z`) or IANA time zone identifier \
(from `%Q`) is required for parsing zoned datetime",
),
}
}
}
#[derive(Clone, Debug)]
pub(crate) enum ParseError {
ExpectedAmPm,
ExpectedAmPmTooShort,
ExpectedIanaTz,
ExpectedIanaTzEndOfInput,
ExpectedMonthAbbreviation,
ExpectedMonthAbbreviationTooShort,
ExpectedWeekdayAbbreviation,
ExpectedWeekdayAbbreviationTooShort,
ExpectedChoice {
available: &'static [&'static [u8]],
},
ExpectedFractionalDigit,
ExpectedMatchLiteralByte {
expected: u8,
got: u8,
},
ExpectedMatchLiteralEndOfInput {
expected: u8,
},
ExpectedNonEmpty {
directive: u8,
},
#[cfg(not(feature = "alloc"))]
NotAllowedAlloc {
directive: u8,
colons: u8,
},
NotAllowedLocaleClockTime,
NotAllowedLocaleDate,
NotAllowedLocaleDateAndTime,
NotAllowedLocaleTwelveHourClockTime,
NotAllowedTimeZoneAbbreviation,
ParseDay,
ParseDayOfYear,
ParseCentury,
ParseFractionalSeconds,
ParseHour,
ParseIsoWeekNumber,
ParseIsoWeekYear,
ParseIsoWeekYearTwoDigit,
ParseMinute,
ParseMondayWeekNumber,
ParseMonth,
ParseSecond,
ParseSundayWeekNumber,
ParseTimestamp,
ParseWeekdayNumber,
ParseYear,
ParseYearTwoDigit,
UnknownMonthName,
UnknownWeekdayAbbreviation,
}
impl error::IntoError for ParseError {
fn into_error(self) -> error::Error {
self.into()
}
}
impl From<ParseError> for error::Error {
#[cold]
#[inline(never)]
fn from(err: ParseError) -> error::Error {
error::ErrorKind::FmtStrtimeParse(err).into()
}
}
impl core::fmt::Display for ParseError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::ParseError::*;
match *self {
ExpectedAmPm => f.write_str("expected to find `AM` or `PM`"),
ExpectedAmPmTooShort => f.write_str(
"expected to find `AM` or `PM`, \
but the remaining input is too short \
to contain one",
),
ExpectedIanaTz => f.write_str(
"expected to find the start of an IANA time zone \
identifier name or component",
),
ExpectedIanaTzEndOfInput => f.write_str(
"expected to find the start of an IANA time zone \
identifier name or component, \
but found end of input instead",
),
ExpectedMonthAbbreviation => {
f.write_str("expected to find month name abbreviation")
}
ExpectedMonthAbbreviationTooShort => f.write_str(
"expected to find month name abbreviation, \
but the remaining input is too short \
to contain one",
),
ExpectedWeekdayAbbreviation => {
f.write_str("expected to find weekday abbreviation")
}
ExpectedWeekdayAbbreviationTooShort => f.write_str(
"expected to find weekday abbreviation, \
but the remaining input is too short \
to contain one",
),
ExpectedChoice { available } => {
f.write_str(
"failed to find expected value, available choices are: ",
)?;
for (i, choice) in available.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
write!(f, "{}", escape::Bytes(choice))?;
}
Ok(())
}
ExpectedFractionalDigit => f.write_str(
"expected at least one fractional decimal digit, \
but did not find any",
),
ExpectedMatchLiteralByte { expected, got } => write!(
f,
"expected to match literal byte `{expected}` from \
format string, but found byte `{got}` in input",
expected = escape::Byte(expected),
got = escape::Byte(got),
),
ExpectedMatchLiteralEndOfInput { expected } => write!(
f,
"expected to match literal byte `{expected}` from \
format string, but found end of input",
expected = escape::Byte(expected),
),
ExpectedNonEmpty { directive } => write!(
f,
"expected non-empty input for directive `%{directive}`, \
but found end of input",
directive = escape::Byte(directive),
),
#[cfg(not(feature = "alloc"))]
NotAllowedAlloc { directive, colons } => write!(
f,
"cannot parse `%{colons}{directive}` \
without Jiff's `alloc` feature enabled",
colons = escape::RepeatByte { byte: b':', count: colons },
directive = escape::Byte(directive),
),
NotAllowedLocaleClockTime => {
f.write_str("parsing locale clock time is not allowed")
}
NotAllowedLocaleDate => {
f.write_str("parsing locale date is not allowed")
}
NotAllowedLocaleDateAndTime => {
f.write_str("parsing locale date and time is not allowed")
}
NotAllowedLocaleTwelveHourClockTime => {
f.write_str("parsing locale 12-hour clock time is not allowed")
}
NotAllowedTimeZoneAbbreviation => {
f.write_str("parsing time zone abbreviation is not allowed")
}
ParseCentury => {
f.write_str("failed to parse year number for century")
}
ParseDay => f.write_str("failed to parse day number"),
ParseDayOfYear => {
f.write_str("failed to parse day of year number")
}
ParseFractionalSeconds => f.write_str(
"failed to parse fractional second component \
(up to 9 digits, nanosecond precision)",
),
ParseHour => f.write_str("failed to parse hour number"),
ParseMinute => f.write_str("failed to parse minute number"),
ParseWeekdayNumber => {
f.write_str("failed to parse weekday number")
}
ParseIsoWeekNumber => {
f.write_str("failed to parse ISO 8601 week number")
}
ParseIsoWeekYear => {
f.write_str("failed to parse ISO 8601 week year")
}
ParseIsoWeekYearTwoDigit => {
f.write_str("failed to parse 2-digit ISO 8601 week year")
}
ParseMondayWeekNumber => {
f.write_str("failed to parse Monday-based week number")
}
ParseMonth => f.write_str("failed to parse month number"),
ParseSecond => f.write_str("failed to parse second number"),
ParseSundayWeekNumber => {
f.write_str("failed to parse Sunday-based week number")
}
ParseTimestamp => {
f.write_str("failed to parse Unix timestamp (in seconds)")
}
ParseYear => f.write_str("failed to parse year"),
ParseYearTwoDigit => f.write_str("failed to parse 2-digit year"),
UnknownMonthName => f.write_str("unrecognized month name"),
UnknownWeekdayAbbreviation => {
f.write_str("unrecognized weekday abbreviation")
}
}
}
}
#[derive(Clone, Debug)]
pub(crate) enum FormatError {
RequiresDate,
RequiresInstant,
RequiresOffset,
RequiresTime,
RequiresTimeZone,
RequiresTimeZoneOrOffset,
InvalidUtf8,
ZeroPrecisionFloat,
ZeroPrecisionNano,
}
impl error::IntoError for FormatError {
fn into_error(self) -> error::Error {
self.into()
}
}
impl From<FormatError> for error::Error {
#[cold]
#[inline(never)]
fn from(err: FormatError) -> error::Error {
error::ErrorKind::FmtStrtimeFormat(err).into()
}
}
impl core::fmt::Display for FormatError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::FormatError::*;
match *self {
RequiresDate => f.write_str("requires date to format"),
RequiresInstant => f.write_str(
"requires instant (a timestamp or a date, time and offset)",
),
RequiresTime => f.write_str("requires time to format"),
RequiresOffset => f.write_str("requires time zone offset"),
RequiresTimeZone => {
f.write_str("requires IANA time zone identifier")
}
RequiresTimeZoneOrOffset => f.write_str(
"requires IANA time zone identifier or \
time zone offset, but neither were present",
),
InvalidUtf8 => {
f.write_str("invalid format string, it must be valid UTF-8")
}
ZeroPrecisionFloat => {
f.write_str("zero precision with %f is not allowed")
}
ZeroPrecisionNano => {
f.write_str("zero precision with %N is not allowed")
}
}
}
}

354
src/error/fmt/temporal.rs Normal file
View file

@ -0,0 +1,354 @@
use crate::{error, tz::Offset, util::escape};
#[derive(Clone, Debug)]
pub(crate) enum Error {
#[cfg(not(feature = "alloc"))]
AllocPosixTimeZone,
AmbiguousTimeMonthDay,
AmbiguousTimeYearMonth,
CivilDateTimeZulu,
ConvertDateTimeToTimestamp {
offset: Offset,
},
EmptyTimeZone,
ExpectedDateDesignatorFoundByte {
byte: u8,
},
ExpectedDateDesignatorFoundEndOfInput,
ExpectedDurationDesignatorFoundByte {
byte: u8,
},
ExpectedDurationDesignatorFoundEndOfInput,
ExpectedFourDigitYear,
ExpectedNoSeparator,
ExpectedOneDigitWeekday,
ExpectedSeparatorFoundByte {
byte: u8,
},
ExpectedSeparatorFoundEndOfInput,
ExpectedSixDigitYear,
ExpectedTimeDesignator,
ExpectedTimeDesignatorFoundByte {
byte: u8,
},
ExpectedTimeDesignatorFoundEndOfInput,
ExpectedTimeUnits,
ExpectedTwoDigitDay,
ExpectedTwoDigitHour,
ExpectedTwoDigitMinute,
ExpectedTwoDigitMonth,
ExpectedTwoDigitSecond,
ExpectedTwoDigitWeekNumber,
ExpectedWeekPrefixFoundByte {
byte: u8,
},
ExpectedWeekPrefixFoundEndOfInput,
FailedDayInDate,
FailedDayInMonthDay,
FailedFractionalSecondInTime,
FailedHourInTime,
FailedMinuteInTime,
FailedMonthInDate,
FailedMonthInMonthDay,
FailedMonthInYearMonth,
FailedOffsetNumeric,
FailedSecondInTime,
FailedSeparatorAfterMonth,
FailedSeparatorAfterWeekNumber,
FailedSeparatorAfterYear,
FailedTzdbLookup,
FailedWeekNumberInDate,
FailedWeekNumberPrefixInDate,
FailedWeekdayInDate,
FailedYearInDate,
FailedYearInYearMonth,
InvalidDate,
InvalidDay,
InvalidHour,
InvalidMinute,
InvalidMonth,
InvalidMonthDay,
InvalidSecond,
InvalidTimeZoneUtf8,
InvalidWeekDate,
InvalidWeekNumber,
InvalidWeekday,
InvalidYear,
InvalidYearMonth,
InvalidYearZero,
MissingOffsetInTimestamp,
MissingTimeInDate,
MissingTimeInTimestamp,
MissingTimeZoneAnnotation,
ParseDayTwoDigit,
ParseHourTwoDigit,
ParseMinuteTwoDigit,
ParseMonthTwoDigit,
ParseSecondTwoDigit,
ParseWeekNumberTwoDigit,
ParseWeekdayOneDigit,
ParseYearFourDigit,
ParseYearSixDigit,
// This is the only error for formatting a Temporal value. And
// actually, it's not even part of Temporal, but just lives in that
// module (for convenience reasons).
PrintTimeZoneFailure,
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::FmtTemporal(err).into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
#[cfg(not(feature = "alloc"))]
AllocPosixTimeZone => f.write_str(
"cannot parsed time zones other than IANA time zone \
identifiers or fixed offsets \
without the `alloc` crate feature enabled for `jiff`",
),
AmbiguousTimeMonthDay => {
f.write_str("parsed time is ambiguous with a month-day date")
}
AmbiguousTimeYearMonth => {
f.write_str("parsed time is ambiguous with a year-month date")
}
CivilDateTimeZulu => f.write_str(
"cannot parse civil date/time from string with a Zulu \
offset, parse as a `jiff::Timestamp` first \
and convert to a civil date/time instead",
),
ConvertDateTimeToTimestamp { offset } => write!(
f,
"failed to convert civil datetime to timestamp \
with offset {offset}",
),
EmptyTimeZone => {
f.write_str("an empty string is not a valid time zone")
}
ExpectedDateDesignatorFoundByte { byte } => write!(
f,
"expected to find date unit designator suffix \
(`Y`, `M`, `W` or `D`), but found `{byte}` instead",
byte = escape::Byte(byte),
),
ExpectedDateDesignatorFoundEndOfInput => f.write_str(
"expected to find date unit designator suffix \
(`Y`, `M`, `W` or `D`), but found end of input",
),
ExpectedDurationDesignatorFoundByte { byte } => write!(
f,
"expected to find duration beginning with `P` or `p`, \
but found `{byte}` instead",
byte = escape::Byte(byte),
),
ExpectedDurationDesignatorFoundEndOfInput => f.write_str(
"expected to find duration beginning with `P` or `p`, \
but found end of input",
),
ExpectedFourDigitYear => f.write_str(
"expected four digit year (or leading sign for \
six digit year), but found end of input",
),
ExpectedNoSeparator => f.write_str(
"expected no separator since none was \
found after the year, but found a `-` separator",
),
ExpectedOneDigitWeekday => f.write_str(
"expected one digit weekday, but found end of input",
),
ExpectedSeparatorFoundByte { byte } => write!(
f,
"expected `-` separator, but found `{byte}`",
byte = escape::Byte(byte),
),
ExpectedSeparatorFoundEndOfInput => {
f.write_str("expected `-` separator, but found end of input")
}
ExpectedSixDigitYear => f.write_str(
"expected six digit year (because of a leading sign), \
but found end of input",
),
ExpectedTimeDesignator => f.write_str(
"parsing ISO 8601 duration in this context requires \
that the duration contain a time component and no \
components of days or greater",
),
ExpectedTimeDesignatorFoundByte { byte } => write!(
f,
"expected to find time unit designator suffix \
(`H`, `M` or `S`), but found `{byte}` instead",
byte = escape::Byte(byte),
),
ExpectedTimeDesignatorFoundEndOfInput => f.write_str(
"expected to find time unit designator suffix \
(`H`, `M` or `S`), but found end of input",
),
ExpectedTimeUnits => f.write_str(
"found a time designator (`T` or `t`) in an ISO 8601 \
duration string, but did not find any time units",
),
ExpectedTwoDigitDay => {
f.write_str("expected two digit day, but found end of input")
}
ExpectedTwoDigitHour => {
f.write_str("expected two digit hour, but found end of input")
}
ExpectedTwoDigitMinute => f.write_str(
"expected two digit minute, but found end of input",
),
ExpectedTwoDigitMonth => {
f.write_str("expected two digit month, but found end of input")
}
ExpectedTwoDigitSecond => f.write_str(
"expected two digit second, but found end of input",
),
ExpectedTwoDigitWeekNumber => f.write_str(
"expected two digit week number, but found end of input",
),
ExpectedWeekPrefixFoundByte { byte } => write!(
f,
"expected `W` or `w`, but found `{byte}` instead",
byte = escape::Byte(byte),
),
ExpectedWeekPrefixFoundEndOfInput => {
f.write_str("expected `W` or `w`, but found end of input")
}
FailedDayInDate => f.write_str("failed to parse day in date"),
FailedDayInMonthDay => {
f.write_str("failed to payse day in month-day")
}
FailedFractionalSecondInTime => {
f.write_str("failed to parse fractional seconds in time")
}
FailedHourInTime => f.write_str("failed to parse hour in time"),
FailedMinuteInTime => {
f.write_str("failed to parse minute in time")
}
FailedMonthInDate => f.write_str("failed to parse month in date"),
FailedMonthInMonthDay => {
f.write_str("failed to parse month in month-day")
}
FailedMonthInYearMonth => {
f.write_str("failed to parse month in year-month")
}
FailedOffsetNumeric => f.write_str(
"offset successfully parsed, \
but failed to convert to numeric `jiff::tz::Offset`",
),
FailedSecondInTime => {
f.write_str("failed to parse second in time")
}
FailedSeparatorAfterMonth => {
f.write_str("failed to parse separator after month")
}
FailedSeparatorAfterWeekNumber => {
f.write_str("failed to parse separator after week number")
}
FailedSeparatorAfterYear => {
f.write_str("failed to parse separator after year")
}
FailedTzdbLookup => f.write_str(
"parsed apparent IANA time zone identifier, \
but the tzdb lookup failed",
),
FailedWeekNumberInDate => {
f.write_str("failed to parse week number in date")
}
FailedWeekNumberPrefixInDate => {
f.write_str("failed to parse week number prefix in date")
}
FailedWeekdayInDate => {
f.write_str("failed to parse weekday in date")
}
FailedYearInDate => f.write_str("failed to parse year in date"),
FailedYearInYearMonth => {
f.write_str("failed to parse year in year-month")
}
InvalidDate => f.write_str("parsed date is not valid"),
InvalidDay => f.write_str("parsed day is not valid"),
InvalidHour => f.write_str("parsed hour is not valid"),
InvalidMinute => f.write_str("parsed minute is not valid"),
InvalidMonth => f.write_str("parsed month is not valid"),
InvalidMonthDay => f.write_str("parsed month-day is not valid"),
InvalidSecond => f.write_str("parsed second is not valid"),
InvalidTimeZoneUtf8 => f.write_str(
"found plausible IANA time zone identifier, \
but it is not valid UTF-8",
),
InvalidWeekDate => f.write_str("parsed week date is not valid"),
InvalidWeekNumber => {
f.write_str("parsed week number is not valid")
}
InvalidWeekday => f.write_str("parsed weekday is not valid"),
InvalidYear => f.write_str("parsed year is not valid"),
InvalidYearMonth => f.write_str("parsed year-month is not valid"),
InvalidYearZero => f.write_str(
"year zero must be written without a sign or a \
positive sign, but not a negative sign",
),
MissingOffsetInTimestamp => f.write_str(
"failed to find offset component, \
which is required for parsing a timestamp",
),
MissingTimeInDate => f.write_str(
"successfully parsed date, but no time component was found",
),
MissingTimeInTimestamp => f.write_str(
"failed to find time component, \
which is required for parsing a timestamp",
),
MissingTimeZoneAnnotation => f.write_str(
"failed to find time zone annotation in square brackets, \
which is required for parsing a zoned datetime",
),
ParseDayTwoDigit => {
f.write_str("failed to parse two digit integer as day")
}
ParseHourTwoDigit => {
f.write_str("failed to parse two digit integer as hour")
}
ParseMinuteTwoDigit => {
f.write_str("failed to parse two digit integer as minute")
}
ParseMonthTwoDigit => {
f.write_str("failed to parse two digit integer as month")
}
ParseSecondTwoDigit => {
f.write_str("failed to parse two digit integer as second")
}
ParseWeekNumberTwoDigit => {
f.write_str("failed to parse two digit integer as week number")
}
ParseWeekdayOneDigit => {
f.write_str("failed to parse one digit integer as weekday")
}
ParseYearFourDigit => {
f.write_str("failed to parse four digit integer as year")
}
ParseYearSixDigit => {
f.write_str("failed to parse six digit integer as year")
}
PrintTimeZoneFailure => f.write_str(
"time zones without IANA identifiers that aren't either \
fixed offsets or a POSIX time zone can't be serialized \
(this typically occurs when this is a system time zone \
derived from `/etc/localtime` on Unix systems that \
isn't symlinked to an entry in `/usr/share/zoneinfo`)",
),
}
}
}

115
src/error/fmt/util.rs Normal file
View file

@ -0,0 +1,115 @@
use crate::{error, Unit};
#[derive(Clone, Debug)]
pub(crate) enum Error {
ConversionToSecondsFailed { unit: Unit },
EmptyDuration,
FailedValueSet { unit: Unit },
InvalidFraction,
InvalidFractionNanos,
MissingFractionalDigits,
NotAllowedCalendarUnit { unit: Unit },
NotAllowedFractionalUnit { found: Unit },
NotAllowedNegative,
OutOfOrderHMS { found: Unit },
OutOfOrderUnits { found: Unit, previous: Unit },
OverflowForUnit { unit: Unit },
OverflowForUnitFractional { unit: Unit },
SignedOverflowForUnit { unit: Unit },
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::FmtUtil(err).into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
ConversionToSecondsFailed { unit } => write!(
f,
"converting {unit} to seconds overflows \
a signed 64-bit integer",
unit = unit.plural(),
),
EmptyDuration => f.write_str("no parsed duration components"),
FailedValueSet { unit } => write!(
f,
"failed to set value for {unit} unit on span",
unit = unit.singular(),
),
InvalidFraction => f.write_str(
"failed to parse fractional component \
(up to 9 digits, nanosecond precision is allowed)",
),
InvalidFractionNanos => f.write_str(
"failed to set nanosecond value from fractional component",
),
MissingFractionalDigits => f.write_str(
"found decimal after seconds component, \
but did not find any digits after decimal",
),
NotAllowedCalendarUnit { unit } => write!(
f,
"parsing calendar units ({unit} in this case) \
in this context is not supported \
(perhaps try parsing into a `jiff::Span` instead)",
unit = unit.plural(),
),
NotAllowedFractionalUnit { found } => write!(
f,
"fractional {found} are not supported",
found = found.plural(),
),
NotAllowedNegative => f.write_str(
"cannot parse negative duration into unsigned \
`std::time::Duration`",
),
OutOfOrderHMS { found } => write!(
f,
"found `HH:MM:SS` after unit {found}, \
but `HH:MM:SS` can only appear after \
years, months, weeks or days",
found = found.singular(),
),
OutOfOrderUnits { found, previous } => write!(
f,
"found value with unit {found} \
after unit {previous}, but units must be \
written from largest to smallest \
(and they can't be repeated)",
found = found.singular(),
previous = previous.singular(),
),
OverflowForUnit { unit } => write!(
f,
"accumulated duration \
overflowed when adding value to unit {unit}",
unit = unit.singular(),
),
OverflowForUnitFractional { unit } => write!(
f,
"accumulated duration \
overflowed when adding fractional value to unit {unit}",
unit = unit.singular(),
),
SignedOverflowForUnit { unit } => write!(
f,
"value for {unit} is too big (or small) to fit into \
a signed 64-bit integer",
unit = unit.plural(),
),
}
}
}

View file

@ -1,16 +1,14 @@
use crate::{shared::util::error::Error as SharedError, util::sync::Arc};
use crate::util::sync::Arc;
/// Creates a new ad hoc error with no causal chain.
///
/// This accepts the same arguments as the `format!` macro. The error it
/// creates is just a wrapper around the string created by `format!`.
macro_rules! err {
($($tt:tt)*) => {{
crate::error::Error::adhoc_from_args(format_args!($($tt)*))
}}
}
pub(crate) use err;
pub(crate) mod civil;
pub(crate) mod duration;
pub(crate) mod fmt;
pub(crate) mod signed_duration;
pub(crate) mod span;
pub(crate) mod timestamp;
pub(crate) mod tz;
pub(crate) mod util;
pub(crate) mod zoned;
/// An error that can occur in this crate.
///
@ -29,8 +27,11 @@ pub(crate) use err;
///
/// Other than implementing the [`std::error::Error`] trait when the
/// `std` feature is enabled, the [`core::fmt::Debug`] trait and the
/// [`core::fmt::Display`] trait, this error type currently provides no
/// introspection capabilities.
/// [`core::fmt::Display`] trait, this error type currently provides
/// very limited introspection capabilities. Simple predicates like
/// `Error::is_range` are provided, but the predicates are not
/// exhaustive. That is, there exist some errors that do not return
/// `true` for any of the `Error::is_*` predicates.
///
/// # Design
///
@ -65,50 +66,6 @@ struct ErrorInner {
cause: Option<Error>,
}
/// The underlying kind of a [`Error`].
#[derive(Debug)]
#[cfg_attr(not(feature = "alloc"), derive(Clone))]
enum ErrorKind {
/// An ad hoc error that is constructed from anything that implements
/// the `core::fmt::Display` trait.
///
/// In theory we try to avoid these, but they tend to be awfully
/// convenient. In practice, we use them a lot, and only use a structured
/// representation when a lot of different error cases fit neatly into a
/// structure (like range errors).
Adhoc(AdhocError),
/// An error that occurs when a number is not within its allowed range.
///
/// This can occur directly as a result of a number provided by the caller
/// of a public API, or as a result of an operation on a number that
/// results in it being out of range.
Range(RangeError),
/// An error that occurs within `jiff::shared`.
///
/// It has its own error type to avoid bringing in this much bigger error
/// type.
Shared(SharedError),
/// An error associated with a file path.
///
/// This is generally expected to always have a cause attached to it
/// explaining what went wrong. The error variant is just a path to make
/// it composable with other error types.
///
/// The cause is typically `Adhoc` or `IO`.
///
/// When `std` is not enabled, this variant can never be constructed.
#[allow(dead_code)] // not used in some feature configs
FilePath(FilePathError),
/// An error that occurs when interacting with the file system.
///
/// This is effectively a wrapper around `std::io::Error` coupled with a
/// `std::path::PathBuf`.
///
/// When `std` is not enabled, this variant can never be constructed.
#[allow(dead_code)] // not used in some feature configs
IO(IOError),
}
impl Error {
/// Creates a new error value from `core::fmt::Arguments`.
///
@ -120,6 +77,14 @@ impl Error {
/// circumstances, it can be convenient to manufacture a Jiff error value
/// specifically.
///
/// # Core-only environments
///
/// In core-only environments without a dynamic memory allocator, error
/// messages may be degraded in some cases. For example, if the given
/// `core::fmt::Arguments` could not be converted to a simple borrowed
/// `&str`, then this will ignore the input given and return an "unknown"
/// Jiff error.
///
/// # Example
///
/// ```
@ -132,79 +97,114 @@ impl Error {
Error::from(ErrorKind::Adhoc(AdhocError::from_args(message)))
}
#[inline(never)]
#[cold]
fn context_impl(self, consequent: Error) -> Error {
#[cfg(feature = "alloc")]
{
let mut err = consequent;
if err.inner.is_none() {
err = err!("unknown jiff error");
}
let inner = err.inner.as_mut().unwrap();
assert!(
inner.cause.is_none(),
"cause of consequence must be `None`"
);
// OK because we just created this error so the Arc
// has one reference.
Arc::get_mut(inner).unwrap().cause = Some(self);
err
}
#[cfg(not(feature = "alloc"))]
{
// We just completely drop `self`. :-(
consequent
}
/// Returns true when this error originated as a result of a value being
/// out of Jiff's supported range.
///
/// # Example
///
/// ```
/// use jiff::civil::Date;
///
/// assert!(Date::new(2025, 2, 29).unwrap_err().is_range());
/// assert!("2025-02-29".parse::<Date>().unwrap_err().is_range());
/// assert!(Date::strptime("%Y-%m-%d", "2025-02-29").unwrap_err().is_range());
/// ```
pub fn is_range(&self) -> bool {
use self::ErrorKind::*;
matches!(*self.root().kind(), Range(_) | SlimRange(_) | ITimeRange(_))
}
/// Returns true when this error originated as a result of an invalid
/// configuration of parameters to a function call.
///
/// This particular error category is somewhat nebulous, but it's generally
/// meant to cover errors that _could_ have been statically prevented by
/// Jiff with more types in its API. Instead, a smaller API is preferred.
///
/// # Example: invalid rounding options
///
/// ```
/// use jiff::{SpanRound, ToSpan, Unit};
///
/// let span = 44.seconds();
/// let err = span.round(
/// SpanRound::new().smallest(Unit::Second).increment(45),
/// ).unwrap_err();
/// // Rounding increments for seconds must divide evenly into `60`.
/// // But `45` does not. Thus, this is a "configuration" error.
/// assert!(err.is_invalid_parameter());
/// ```
///
/// # Example: invalid units
///
/// One cannot round a span between dates to units less than days:
///
/// ```
/// use jiff::{civil::date, Unit};
///
/// let date1 = date(2025, 3, 18);
/// let date2 = date(2025, 12, 21);
/// let err = date1.until((Unit::Hour, date2)).unwrap_err();
/// assert!(err.is_invalid_parameter());
/// ```
///
/// Similarly, one cannot round a span between times to units greater than
/// hours:
///
/// ```
/// use jiff::{civil::time, Unit};
///
/// let time1 = time(9, 39, 0, 0);
/// let time2 = time(17, 0, 0, 0);
/// let err = time1.until((Unit::Day, time2)).unwrap_err();
/// assert!(err.is_invalid_parameter());
/// ```
pub fn is_invalid_parameter(&self) -> bool {
use self::ErrorKind::*;
use self::{
civil::Error as CivilError, span::Error as SpanError,
tz::offset::Error as OffsetError, util::RoundingIncrementError,
};
matches!(
*self.root().kind(),
RoundingIncrement(
RoundingIncrementError::GreaterThanZero { .. }
| RoundingIncrementError::InvalidDivide { .. }
| RoundingIncrementError::Unsupported { .. }
) | Span(
SpanError::NotAllowedCalendarUnits { .. }
| SpanError::NotAllowedLargestSmallerThanSmallest { .. }
| SpanError::RequiresRelativeWeekOrDay { .. }
| SpanError::RequiresRelativeYearOrMonth { .. }
| SpanError::RequiresRelativeYearOrMonthGivenDaysAre24Hours { .. }
) | Civil(
CivilError::IllegalTimeWithMicrosecond
| CivilError::IllegalTimeWithMillisecond
| CivilError::IllegalTimeWithNanosecond
| CivilError::RoundMustUseDaysOrBigger { .. }
| CivilError::RoundMustUseHoursOrSmaller { .. }
) | TzOffset(OffsetError::RoundInvalidUnit { .. })
)
}
/// Returns true when this error originated as a result of an operation
/// failing because an appropriate Jiff crate feature was not enabled.
///
/// # Example
///
/// ```ignore
/// use jiff::tz::TimeZone;
///
/// // This passes when the `tz-system` crate feature is NOT enabled.
/// assert!(TimeZone::try_system().unwrap_err().is_crate_feature());
/// ```
pub fn is_crate_feature(&self) -> bool {
matches!(*self.root().kind(), ErrorKind::CrateFeature(_))
}
}
impl Error {
/// Creates a new "ad hoc" error value.
///
/// An ad hoc error value is just an opaque string.
#[cfg(feature = "alloc")]
#[inline(never)]
#[cold]
pub(crate) fn adhoc<'a>(message: impl core::fmt::Display + 'a) -> Error {
Error::from(ErrorKind::Adhoc(AdhocError::from_display(message)))
}
/// Like `Error::adhoc`, but accepts a `core::fmt::Arguments`.
///
/// This is used with the `err!` macro so that we can thread a
/// `core::fmt::Arguments` down. This lets us extract a `&'static str`
/// from some messages in core-only mode and provide somewhat decent error
/// messages in some cases.
#[inline(never)]
#[cold]
pub(crate) fn adhoc_from_args<'a>(
message: core::fmt::Arguments<'a>,
) -> Error {
Error::from(ErrorKind::Adhoc(AdhocError::from_args(message)))
}
/// Like `Error::adhoc`, but creates an error from a `String` directly.
///
/// This exists to explicitly monomorphize a very common case.
#[cfg(feature = "alloc")]
#[inline(never)]
#[cold]
fn adhoc_from_string(message: alloc::string::String) -> Error {
Error::adhoc(message)
}
/// Like `Error::adhoc`, but creates an error from a `&'static str`
/// directly.
///
/// This is useful in contexts where you know you have a `&'static str`,
/// and avoids relying on `alloc`-only routines like `Error::adhoc`.
#[inline(never)]
#[cold]
pub(crate) fn adhoc_from_static_str(message: &'static str) -> Error {
Error::from(ErrorKind::Adhoc(AdhocError::from_static_str(message)))
}
/// Creates a new error indicating that a `given` value is out of the
/// specified `min..=max` range. The given `what` label is used in the
/// error message as a human readable description of what exactly is out
@ -220,9 +220,36 @@ impl Error {
Error::from(ErrorKind::Range(RangeError::new(what, given, min, max)))
}
/// Creates a new error indicating that a `given` value is out of the
/// allowed range.
///
/// This is similar to `Error::range`, but the error message doesn't
/// include the illegal value or the allowed range. This is useful for
/// ad hoc range errors but should generally be used sparingly.
#[inline(never)]
#[cold]
pub(crate) fn slim_range(what: &'static str) -> Error {
Error::from(ErrorKind::SlimRange(SlimRangeError::new(what)))
}
/// Creates a new error from the special "shared" error type.
pub(crate) fn shared(err: SharedError) -> Error {
Error::from(ErrorKind::Shared(err))
pub(crate) fn itime_range(
err: crate::shared::util::itime::RangeError,
) -> Error {
Error::from(ErrorKind::ITimeRange(err))
}
/// Creates a new error from the special TZif error type.
#[cfg(feature = "alloc")]
pub(crate) fn tzif(err: crate::shared::tzif::TzifError) -> Error {
Error::from(ErrorKind::Tzif(err))
}
/// Creates a new error from the special `PosixTimeZoneError` type.
pub(crate) fn posix_tz(
err: crate::shared::posix::PosixTimeZoneError,
) -> Error {
Error::from(ErrorKind::PosixTz(err))
}
/// A convenience constructor for building an I/O error.
@ -258,7 +285,7 @@ impl Error {
///
/// The benefit of this API is that it permits creating an `Error` in a
/// `const` context. But the error message quality is currently pretty
/// bad: it's just a generic "unknown jiff error" message.
/// bad: it's just a generic "unknown Jiff error" message.
///
/// This could be improved to take a `&'static str`, but I believe this
/// will require pointer tagging in order to avoid increasing the size of
@ -268,6 +295,83 @@ impl Error {
Error { inner: None }
}
*/
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) fn context(self, consequent: impl IntoError) -> Error {
self.context_impl(consequent.into_error())
}
#[inline(never)]
#[cold]
fn context_impl(self, _consequent: Error) -> Error {
#[cfg(feature = "alloc")]
{
let mut err = _consequent;
if err.inner.is_none() {
err = Error::from(ErrorKind::Unknown);
}
let inner = err.inner.as_mut().unwrap();
assert!(
inner.cause.is_none(),
"cause of consequence must be `None`"
);
// OK because we just created this error so the Arc
// has one reference.
Arc::get_mut(inner).unwrap().cause = Some(self);
err
}
#[cfg(not(feature = "alloc"))]
{
// We just completely drop `self`. :-(
//
// 2025-12-21: ... actually, we used to drop self, but this
// ends up dropping the root cause. And the root cause
// is how the predicates on `Error` work. So we drop the
// consequent instead.
self
}
}
/// Returns the root error in this chain.
fn root(&self) -> &Error {
// OK because `Error::chain` is guaranteed to return a non-empty
// iterator.
self.chain().last().unwrap()
}
/// Returns a chain of error values.
///
/// This starts with the most recent error added to the chain. That is,
/// the highest level context. The last error in the chain is always the
/// "root" cause. That is, the error closest to the point where something
/// has gone wrong.
///
/// The iterator returned is guaranteed to yield at least one error.
fn chain(&self) -> impl Iterator<Item = &Error> {
#[cfg(feature = "alloc")]
{
let mut err = self;
core::iter::once(err).chain(core::iter::from_fn(move || {
err = err
.inner
.as_ref()
.and_then(|inner| inner.cause.as_ref())?;
Some(err)
}))
}
#[cfg(not(feature = "alloc"))]
{
core::iter::once(self)
}
}
/// Returns the kind of this error.
fn kind(&self) -> &ErrorKind {
self.inner
.as_ref()
.map(|inner| &inner.kind)
.unwrap_or(&ErrorKind::Unknown)
}
}
#[cfg(feature = "std")]
@ -275,30 +379,14 @@ impl std::error::Error for Error {}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
#[cfg(feature = "alloc")]
{
let mut err = self;
loop {
let Some(ref inner) = err.inner else {
write!(f, "unknown jiff error")?;
break;
};
write!(f, "{}", inner.kind)?;
err = match inner.cause.as_ref() {
None => break,
Some(err) => err,
};
write!(f, ": ")?;
}
Ok(())
}
#[cfg(not(feature = "alloc"))]
{
match self.inner {
None => write!(f, "unknown jiff error"),
Some(ref inner) => write!(f, "{}", inner.kind),
let mut it = self.chain().peekable();
while let Some(err) = it.next() {
core::fmt::Display::fmt(err.kind(), f)?;
if it.peek().is_some() {
f.write_str(": ")?;
}
}
Ok(())
}
}
@ -328,14 +416,98 @@ impl core::fmt::Debug for Error {
}
}
/// The underlying kind of a [`Error`].
#[derive(Debug)]
#[cfg_attr(not(feature = "alloc"), derive(Clone))]
enum ErrorKind {
Adhoc(AdhocError),
Civil(self::civil::Error),
CrateFeature(CrateFeatureError),
Duration(self::duration::Error),
#[allow(dead_code)] // not used in some feature configs
FilePath(FilePathError),
Fmt(self::fmt::Error),
FmtFriendly(self::fmt::friendly::Error),
FmtOffset(self::fmt::offset::Error),
FmtRfc2822(self::fmt::rfc2822::Error),
FmtRfc9557(self::fmt::rfc9557::Error),
FmtTemporal(self::fmt::temporal::Error),
FmtUtil(self::fmt::util::Error),
FmtStrtime(self::fmt::strtime::Error),
FmtStrtimeFormat(self::fmt::strtime::FormatError),
FmtStrtimeParse(self::fmt::strtime::ParseError),
#[allow(dead_code)] // not used in some feature configs
IO(IOError),
ITimeRange(crate::shared::util::itime::RangeError),
OsStrUtf8(self::util::OsStrUtf8Error),
ParseInt(self::util::ParseIntError),
ParseFraction(self::util::ParseFractionError),
PosixTz(crate::shared::posix::PosixTimeZoneError),
Range(RangeError),
RoundingIncrement(self::util::RoundingIncrementError),
SignedDuration(self::signed_duration::Error),
SlimRange(SlimRangeError),
Span(self::span::Error),
Timestamp(self::timestamp::Error),
TzAmbiguous(self::tz::ambiguous::Error),
TzDb(self::tz::db::Error),
TzConcatenated(self::tz::concatenated::Error),
TzOffset(self::tz::offset::Error),
TzPosix(self::tz::posix::Error),
TzSystem(self::tz::system::Error),
TzTimeZone(self::tz::timezone::Error),
#[allow(dead_code)]
TzZic(self::tz::zic::Error),
#[cfg(feature = "alloc")]
Tzif(crate::shared::tzif::TzifError),
Unknown,
Zoned(self::zoned::Error),
}
impl core::fmt::Display for ErrorKind {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::ErrorKind::*;
match *self {
ErrorKind::Adhoc(ref msg) => msg.fmt(f),
ErrorKind::Range(ref err) => err.fmt(f),
ErrorKind::Shared(ref err) => err.fmt(f),
ErrorKind::FilePath(ref err) => err.fmt(f),
ErrorKind::IO(ref err) => err.fmt(f),
Adhoc(ref msg) => msg.fmt(f),
Civil(ref err) => err.fmt(f),
CrateFeature(ref err) => err.fmt(f),
Duration(ref err) => err.fmt(f),
FilePath(ref err) => err.fmt(f),
Fmt(ref err) => err.fmt(f),
FmtFriendly(ref err) => err.fmt(f),
FmtOffset(ref err) => err.fmt(f),
FmtRfc2822(ref err) => err.fmt(f),
FmtRfc9557(ref err) => err.fmt(f),
FmtUtil(ref err) => err.fmt(f),
FmtStrtime(ref err) => err.fmt(f),
FmtStrtimeFormat(ref err) => err.fmt(f),
FmtStrtimeParse(ref err) => err.fmt(f),
FmtTemporal(ref err) => err.fmt(f),
IO(ref err) => err.fmt(f),
ITimeRange(ref err) => err.fmt(f),
OsStrUtf8(ref err) => err.fmt(f),
ParseInt(ref err) => err.fmt(f),
ParseFraction(ref err) => err.fmt(f),
PosixTz(ref err) => err.fmt(f),
Range(ref err) => err.fmt(f),
RoundingIncrement(ref err) => err.fmt(f),
SignedDuration(ref err) => err.fmt(f),
SlimRange(ref err) => err.fmt(f),
Span(ref err) => err.fmt(f),
Timestamp(ref err) => err.fmt(f),
TzAmbiguous(ref err) => err.fmt(f),
TzDb(ref err) => err.fmt(f),
TzConcatenated(ref err) => err.fmt(f),
TzOffset(ref err) => err.fmt(f),
TzPosix(ref err) => err.fmt(f),
TzSystem(ref err) => err.fmt(f),
TzTimeZone(ref err) => err.fmt(f),
TzZic(ref err) => err.fmt(f),
#[cfg(feature = "alloc")]
Tzif(ref err) => err.fmt(f),
Unknown => f.write_str("unknown Jiff error"),
Zoned(ref err) => err.fmt(f),
}
}
}
@ -355,10 +527,10 @@ impl From<ErrorKind> for Error {
/// A generic error message.
///
/// This somewhat unfortunately represents most of the errors in Jiff. When I
/// first started building Jiff, I had a goal of making every error structured.
/// But this ended up being a ton of work, and I find it much easier and nicer
/// for error messages to be embedded where they occur.
/// This used to be used to represent most errors in Jiff. But then I switched
/// to more structured error types (internally). We still keep this around to
/// support the `Error::from_args` public API, which permits users of Jiff to
/// manifest their own `Error` values from an arbitrary message.
#[cfg_attr(not(feature = "alloc"), derive(Clone))]
struct AdhocError {
#[cfg(feature = "alloc")]
@ -368,18 +540,13 @@ struct AdhocError {
}
impl AdhocError {
#[cfg(feature = "alloc")]
fn from_display<'a>(message: impl core::fmt::Display + 'a) -> AdhocError {
use alloc::string::ToString;
let message = message.to_string().into_boxed_str();
AdhocError { message }
}
fn from_args<'a>(message: core::fmt::Arguments<'a>) -> AdhocError {
#[cfg(feature = "alloc")]
{
AdhocError::from_display(message)
use alloc::string::ToString;
let message = message.to_string().into_boxed_str();
AdhocError { message }
}
#[cfg(not(feature = "alloc"))]
{
@ -387,17 +554,6 @@ impl AdhocError {
"unknown Jiff error (better error messages require \
enabling the `alloc` feature for the `jiff` crate)",
);
AdhocError::from_static_str(message)
}
}
fn from_static_str(message: &'static str) -> AdhocError {
#[cfg(feature = "alloc")]
{
AdhocError::from_display(message)
}
#[cfg(not(feature = "alloc"))]
{
AdhocError { message }
}
}
@ -476,6 +632,75 @@ impl core::fmt::Display for RangeError {
}
}
/// A slim error that occurs when an input value is out of bounds.
///
/// Unlike `RangeError`, this only includes a static description of the
/// value that is out of bounds. It doesn't include the out-of-range value
/// or the min/max values.
#[derive(Clone, Debug)]
struct SlimRangeError {
what: &'static str,
}
impl SlimRangeError {
fn new(what: &'static str) -> SlimRangeError {
SlimRangeError { what }
}
}
#[cfg(feature = "std")]
impl std::error::Error for SlimRangeError {}
impl core::fmt::Display for SlimRangeError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let SlimRangeError { what } = *self;
write!(f, "parameter '{what}' is not in the required range")
}
}
/// An error used whenever a failure is caused by a missing crate feature.
///
/// This enum doesn't necessarily contain every Jiff crate feature. It only
/// contains the features whose absence can result in an error.
#[derive(Clone, Debug)]
pub(crate) enum CrateFeatureError {
#[cfg(not(feature = "tz-system"))]
TzSystem,
#[cfg(not(feature = "tzdb-concatenated"))]
TzdbConcatenated,
#[cfg(not(feature = "tzdb-zoneinfo"))]
TzdbZoneInfo,
}
impl From<CrateFeatureError> for Error {
#[cold]
#[inline(never)]
fn from(err: CrateFeatureError) -> Error {
ErrorKind::CrateFeature(err).into()
}
}
impl core::fmt::Display for CrateFeatureError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
#[allow(unused_imports)]
use self::CrateFeatureError::*;
f.write_str("operation failed because Jiff crate feature `")?;
#[allow(unused_variables)]
let name: &str = match *self {
#[cfg(not(feature = "tz-system"))]
TzSystem => "tz-system",
#[cfg(not(feature = "tzdb-concatenated"))]
TzdbConcatenated => "tzdb-concatenated",
#[cfg(not(feature = "tzdb-zoneinfo"))]
TzdbZoneInfo => "tzdb-zoneinfo",
};
#[allow(unreachable_code)]
core::fmt::Display::fmt(name, f)?;
f.write_str("` is not enabled")
}
}
/// A `std::io::Error`.
///
/// This type is itself always available, even when the `std` feature is not
@ -581,21 +806,6 @@ impl IntoError for Error {
}
}
impl IntoError for &'static str {
#[inline(always)]
fn into_error(self) -> Error {
Error::adhoc_from_static_str(self)
}
}
#[cfg(feature = "alloc")]
impl IntoError for alloc::string::String {
#[inline(always)]
fn into_error(self) -> Error {
Error::adhoc_from_string(self)
}
}
/// A trait for contextualizing error values.
///
/// This makes it easy to contextualize either `Error` or `Result<T, Error>`.
@ -603,7 +813,7 @@ impl IntoError for alloc::string::String {
/// `map_err` everywhere one wants to add context to an error.
///
/// This trick was borrowed from `anyhow`.
pub(crate) trait ErrorContext {
pub(crate) trait ErrorContext<T, E> {
/// Contextualize the given consequent error with this (`self`) error as
/// the cause.
///
@ -612,7 +822,7 @@ pub(crate) trait ErrorContext {
/// Note that if an `Error` is given for `kind`, then this panics if it has
/// a cause. (Because the cause would otherwise be dropped. An error causal
/// chain is just a linked list, not a tree.)
fn context(self, consequent: impl IntoError) -> Self;
fn context(self, consequent: impl IntoError) -> Result<T, Error>;
/// Like `context`, but hides error construction within a closure.
///
@ -623,39 +833,31 @@ pub(crate) trait ErrorContext {
///
/// Usually this only makes sense to use on a `Result<T, Error>`, otherwise
/// the closure is just executed immediately anyway.
fn with_context<E: IntoError>(
fn with_context<C: IntoError>(
self,
consequent: impl FnOnce() -> E,
) -> Self;
consequent: impl FnOnce() -> C,
) -> Result<T, Error>;
}
impl ErrorContext for Error {
#[cfg_attr(feature = "perf-inline", inline(always))]
fn context(self, consequent: impl IntoError) -> Error {
self.context_impl(consequent.into_error())
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn with_context<E: IntoError>(
self,
consequent: impl FnOnce() -> E,
) -> Error {
self.context_impl(consequent().into_error())
}
}
impl<T> ErrorContext for Result<T, Error> {
impl<T, E> ErrorContext<T, E> for Result<T, E>
where
E: IntoError,
{
#[cfg_attr(feature = "perf-inline", inline(always))]
fn context(self, consequent: impl IntoError) -> Result<T, Error> {
self.map_err(|err| err.context_impl(consequent.into_error()))
self.map_err(|err| {
err.into_error().context_impl(consequent.into_error())
})
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn with_context<E: IntoError>(
fn with_context<C: IntoError>(
self,
consequent: impl FnOnce() -> E,
consequent: impl FnOnce() -> C,
) -> Result<T, Error> {
self.map_err(|err| err.context_impl(consequent().into_error()))
self.map_err(|err| {
err.into_error().context_impl(consequent().into_error())
})
}
}
@ -690,7 +892,12 @@ mod tests {
// then we could make `Error` a zero sized type. Which might
// actually be the right trade-off for core-only, but I'll hold off
// until we have some real world use cases.
expected_size *= 3;
//
// OK... after switching to structured errors, this jumped
// back up to `expected_size *= 6`. And that was with me being
// conscientious about what data we store inside of error types.
// Blech.
expected_size *= 6;
}
assert_eq!(expected_size, core::mem::size_of::<Error>());
}

View file

@ -0,0 +1,55 @@
use crate::{error, Unit};
#[derive(Clone, Debug)]
pub(crate) enum Error {
ConvertNonFinite,
ConvertSystemTime,
RoundCalendarUnit { unit: Unit },
RoundOverflowed { unit: Unit },
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::SignedDuration(err).into()
}
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
ConvertNonFinite => f.write_str(
"could not convert non-finite \
floating point seconds to signed duration \
(floating point seconds must be finite)",
),
ConvertSystemTime => f.write_str(
"failed to get duration between \
`std::time::SystemTime` values",
),
RoundCalendarUnit { unit } => write!(
f,
"rounding `jiff::SignedDuration` failed because \
a calendar unit of '{plural}' was provided \
(to round by calendar units, you must use a `jiff::Span`)",
plural = unit.plural(),
),
RoundOverflowed { unit } => write!(
f,
"rounding signed duration to nearest {singular} \
resulted in a value outside the supported range \
of a `jiff::SignedDuration`",
singular = unit.singular(),
),
}
}
}

139
src/error/span.rs Normal file
View file

@ -0,0 +1,139 @@
use crate::{error, Unit};
#[derive(Clone, Debug)]
pub(crate) enum Error {
ConvertDateTimeToTimestamp,
ConvertNanoseconds { unit: Unit },
ConvertNegative,
ConvertSpanToSignedDuration,
FailedSpanBetweenDateTimes { unit: Unit },
FailedSpanBetweenZonedDateTimes { unit: Unit },
NotAllowedCalendarUnits { unit: Unit },
NotAllowedLargestSmallerThanSmallest { smallest: Unit, largest: Unit },
OptionLargest,
OptionLargestInSpan,
OptionSmallest,
RequiresRelativeWeekOrDay { unit: Unit },
RequiresRelativeYearOrMonth { unit: Unit },
RequiresRelativeYearOrMonthGivenDaysAre24Hours { unit: Unit },
ToDurationCivil,
ToDurationDaysAre24Hours,
ToDurationZoned,
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::Span(err).into()
}
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
ConvertDateTimeToTimestamp => f.write_str(
"failed to interpret datetime in UTC \
in order to convert it to a timestamp",
),
ConvertNanoseconds { unit } => write!(
f,
"failed to convert rounded nanoseconds \
to span for largest unit set to '{unit}'",
unit = unit.plural(),
),
ConvertNegative => f.write_str(
"cannot convert negative span \
to unsigned `std::time::Duration`",
),
ConvertSpanToSignedDuration => f.write_str(
"failed to convert span to duration without relative datetime \
(must use `jiff::Span::to_duration` instead)",
),
FailedSpanBetweenDateTimes { unit } => write!(
f,
"failed to get span between datetimes \
with largest unit set to '{unit}'",
unit = unit.plural(),
),
FailedSpanBetweenZonedDateTimes { unit } => write!(
f,
"failed to get span between zoned datetimes \
with largest unit set to '{unit}'",
unit = unit.plural(),
),
OptionLargest => {
f.write_str("error with `largest` rounding option")
}
OptionLargestInSpan => {
f.write_str("error with largest unit in span to be rounded")
}
OptionSmallest => {
f.write_str("error with `smallest` rounding option")
}
NotAllowedCalendarUnits { unit } => write!(
f,
"operation can only be performed with units of hours \
or smaller, but found non-zero '{unit}' units \
(operations on `jiff::Timestamp`, `jiff::tz::Offset` \
and `jiff::civil::Time` don't support calendar \
units in a `jiff::Span`)",
unit = unit.singular(),
),
NotAllowedLargestSmallerThanSmallest { smallest, largest } => {
write!(
f,
"largest unit ('{largest}') cannot be smaller than \
smallest unit ('{smallest}')",
largest = largest.singular(),
smallest = smallest.singular(),
)
}
RequiresRelativeWeekOrDay { unit } => write!(
f,
"using unit '{unit}' in a span or configuration \
requires that either a relative reference time be given \
or `jiff::SpanRelativeTo::days_are_24_hours()` is used to \
indicate invariant 24-hour days, \
but neither were provided",
unit = unit.singular(),
),
RequiresRelativeYearOrMonth { unit } => write!(
f,
"using unit '{unit}' in a span or configuration \
requires that a relative reference time be given, \
but none was provided",
unit = unit.singular(),
),
RequiresRelativeYearOrMonthGivenDaysAre24Hours { unit } => write!(
f,
"using unit '{unit}' in span or configuration \
requires that a relative reference time be given \
(`jiff::SpanRelativeTo::days_are_24_hours()` was given \
but this only permits using days and weeks \
without a relative reference time)",
unit = unit.singular(),
),
ToDurationCivil => f.write_str(
"could not compute normalized relative span \
from civil datetime",
),
ToDurationDaysAre24Hours => f.write_str(
"could not compute normalized relative span \
when all days are assumed to be 24 hours",
),
ToDurationZoned => f.write_str(
"could not compute normalized relative span \
from zoned datetime",
),
}
}
}

38
src/error/timestamp.rs Normal file
View file

@ -0,0 +1,38 @@
use crate::error;
#[derive(Clone, Debug)]
pub(crate) enum Error {
OverflowAddDuration,
OverflowAddSpan,
RequiresSaturatingTimeUnits,
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::Timestamp(err).into()
}
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
OverflowAddDuration => {
f.write_str("adding duration overflowed timestamp")
}
OverflowAddSpan => f.write_str("adding span overflowed timestamp"),
RequiresSaturatingTimeUnits => f.write_str(
"saturating timestamp arithmetic requires only time units",
),
}
}
}

49
src/error/tz/ambiguous.rs Normal file
View file

@ -0,0 +1,49 @@
use crate::{
error,
tz::{Offset, TimeZone},
};
#[derive(Clone, Debug)]
pub(crate) enum Error {
BecauseFold { before: Offset, after: Offset },
BecauseGap { before: Offset, after: Offset },
InTimeZone { tz: TimeZone },
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::TzAmbiguous(err).into()
}
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
BecauseFold { before, after } => write!(
f,
"datetime is ambiguous since it falls into a \
fold between offsets {before} and {after}",
),
BecauseGap { before, after } => write!(
f,
"datetime is ambiguous since it falls into a \
gap between offsets {before} and {after}",
),
InTimeZone { ref tz } => write!(
f,
"error converting datetime to instant in time zone {tz}",
tz = tz.diagnostic_name(),
),
}
}
}

View file

@ -0,0 +1,119 @@
use crate::error;
// At time of writing, the biggest TZif data file is a few KB. And the
// index block is tens of KB. So impose a limit that is a couple of orders
// of magnitude bigger, but still overall pretty small for... some systems.
// Anyway, I welcome improvements to this heuristic!
pub(crate) const ALLOC_LIMIT: usize = 10 * 1 << 20;
#[derive(Clone, Debug)]
pub(crate) enum Error {
AllocRequestOverLimit,
AllocFailed,
AllocOverflow,
ExpectedFirstSixBytes,
ExpectedIanaName,
ExpectedLastByte,
#[cfg(test)]
ExpectedMoreData,
ExpectedVersion,
FailedReadData,
FailedReadHeader,
FailedReadIndex,
#[cfg(all(feature = "std", all(not(unix), not(windows))))]
FailedSeek,
InvalidIndexDataOffsets,
InvalidLengthIndexBlock,
#[cfg(all(feature = "std", windows))]
InvalidOffsetOverflowFile,
#[cfg(test)]
InvalidOffsetOverflowSlice,
#[cfg(test)]
InvalidOffsetTooBig,
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::TzConcatenated(err).into()
}
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
AllocRequestOverLimit => write!(
f,
"attempted to allocate more than {ALLOC_LIMIT} bytes \
while reading concatenated TZif data, which \
exceeds a heuristic limit to prevent huge allocations \
(please file a bug if this error is inappropriate)",
),
AllocFailed => f.write_str(
"failed to allocate additional room \
for reading concatenated TZif data",
),
AllocOverflow => {
f.write_str("total allocation length overflowed `usize`")
}
ExpectedFirstSixBytes => f.write_str(
"expected first 6 bytes of concatenated TZif header \
to be `tzdata`",
),
ExpectedIanaName => f.write_str(
"expected IANA time zone identifier to be valid UTF-8",
),
ExpectedLastByte => f.write_str(
"expected last byte of concatenated TZif header \
to be `NUL`",
),
#[cfg(test)]
ExpectedMoreData => f.write_str(
"unexpected EOF, expected more bytes based on size \
of caller provided buffer",
),
ExpectedVersion => f.write_str(
"expected version in concatenated TZif header to \
be valid UTF-8",
),
FailedReadData => f.write_str("failed to read TZif data block"),
FailedReadHeader => {
f.write_str("failed to read concatenated TZif header")
}
FailedReadIndex => f.write_str("failed to read index block"),
#[cfg(all(feature = "std", all(not(unix), not(windows))))]
FailedSeek => {
f.write_str("failed to seek to offset in `std::fs::File`")
}
InvalidIndexDataOffsets => f.write_str(
"invalid index and data offsets, \
expected index offset to be less than or equal \
to data offset",
),
InvalidLengthIndexBlock => {
f.write_str("length of index block is not a valid multiple")
}
#[cfg(all(feature = "std", windows))]
InvalidOffsetOverflowFile => f.write_str(
"offset overflow when reading from `std::fs::File`",
),
#[cfg(test)]
InvalidOffsetOverflowSlice => {
f.write_str("offset overflowed `usize`")
}
#[cfg(test)]
InvalidOffsetTooBig => {
f.write_str("offset too big for given slice of data")
}
}
}
}

141
src/error/tz/db.rs Normal file
View file

@ -0,0 +1,141 @@
use crate::error;
#[derive(Clone, Debug)]
pub(crate) enum Error {
#[cfg(feature = "tzdb-concatenated")]
ConcatenatedMissingIanaIdentifiers,
#[cfg(all(feature = "std", not(feature = "tzdb-concatenated")))]
DisabledConcatenated,
#[cfg(all(feature = "std", not(feature = "tzdb-zoneinfo")))]
DisabledZoneInfo,
FailedTimeZone {
#[cfg(feature = "alloc")]
name: alloc::boxed::Box<str>,
},
FailedTimeZoneNoDatabaseConfigured {
#[cfg(feature = "alloc")]
name: alloc::boxed::Box<str>,
},
#[cfg(feature = "tzdb-zoneinfo")]
ZoneInfoNoTzifFiles,
#[cfg(feature = "tzdb-zoneinfo")]
ZoneInfoStripPrefix,
}
impl Error {
pub(crate) fn failed_time_zone(_time_zone_name: &str) -> Error {
Error::FailedTimeZone {
#[cfg(feature = "alloc")]
name: _time_zone_name.into(),
}
}
pub(crate) fn failed_time_zone_no_database_configured(
_time_zone_name: &str,
) -> Error {
Error::FailedTimeZoneNoDatabaseConfigured {
#[cfg(feature = "alloc")]
name: _time_zone_name.into(),
}
}
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::TzDb(err).into()
}
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
#[cfg(feature = "tzdb-concatenated")]
ConcatenatedMissingIanaIdentifiers => f.write_str(
"found no IANA time zone identifiers in \
concatenated tzdata file",
),
#[cfg(all(feature = "std", not(feature = "tzdb-concatenated")))]
DisabledConcatenated => {
f.write_str("system concatenated tzdb unavailable")
}
#[cfg(all(feature = "std", not(feature = "tzdb-zoneinfo")))]
DisabledZoneInfo => {
f.write_str("system zoneinfo tzdb unavailable")
}
FailedTimeZone {
#[cfg(feature = "alloc")]
ref name,
} => {
#[cfg(feature = "alloc")]
{
write!(
f,
"failed to find time zone `{name}` \
in time zone database",
)
}
#[cfg(not(feature = "alloc"))]
{
f.write_str(
"failed to find time zone in time zone database",
)
}
}
FailedTimeZoneNoDatabaseConfigured {
#[cfg(feature = "alloc")]
ref name,
} => {
#[cfg(feature = "std")]
{
write!(
f,
"failed to find time zone `{name}` since there is no \
time zone database configured",
)
}
#[cfg(all(not(feature = "std"), feature = "alloc"))]
{
write!(
f,
"failed to find time zone `{name}`, since there is no \
global time zone database configured (and is \
currently impossible to do so without Jiff's `std` \
feature enabled, if you need this functionality, \
please file an issue on Jiff's tracker with your \
use case)",
)
}
#[cfg(all(not(feature = "std"), not(feature = "alloc")))]
{
f.write_str(
"failed to find time zone, since there is no \
global time zone database configured (and is \
currently impossible to do so without Jiff's `std` \
feature enabled, if you need this functionality, \
please file an issue on Jiff's tracker with your \
use case)",
)
}
}
#[cfg(feature = "tzdb-zoneinfo")]
ZoneInfoNoTzifFiles => f.write_str(
"did not find any TZif files in zoneinfo time zone database",
),
#[cfg(feature = "tzdb-zoneinfo")]
ZoneInfoStripPrefix => f.write_str(
"failed to strip zoneinfo time zone database directory \
path from path to TZif file",
),
}
}
}

8
src/error/tz/mod.rs Normal file
View file

@ -0,0 +1,8 @@
pub(crate) mod ambiguous;
pub(crate) mod concatenated;
pub(crate) mod db;
pub(crate) mod offset;
pub(crate) mod posix;
pub(crate) mod system;
pub(crate) mod timezone;
pub(crate) mod zic;

110
src/error/tz/offset.rs Normal file
View file

@ -0,0 +1,110 @@
use crate::{
error,
tz::{Offset, TimeZone},
Unit,
};
#[derive(Clone, Debug)]
pub(crate) enum Error {
ConvertDateTimeToTimestamp {
offset: Offset,
},
OverflowAddSignedDuration,
OverflowSignedDuration,
ResolveRejectFold {
given: Offset,
before: Offset,
after: Offset,
tz: TimeZone,
},
ResolveRejectGap {
given: Offset,
before: Offset,
after: Offset,
tz: TimeZone,
},
ResolveRejectUnambiguous {
given: Offset,
offset: Offset,
tz: TimeZone,
},
RoundInvalidUnit {
unit: Unit,
},
RoundOverflow,
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::TzOffset(err).into()
}
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
ConvertDateTimeToTimestamp { offset } => write!(
f,
"converting datetime with time zone offset `{offset}` \
to timestamp overflowed",
),
OverflowAddSignedDuration => f.write_str(
"adding signed duration to time zone offset overflowed",
),
OverflowSignedDuration => {
f.write_str("signed duration overflows time zone offset")
}
ResolveRejectFold { given, before, after, ref tz } => write!(
f,
"datetime could not resolve to timestamp \
since `reject` conflict resolution was chosen, and \
because datetime has offset `{given}`, but the time \
zone `{tzname}` for the given datetime falls in a fold \
between offsets `{before}` and `{after}`, neither of which \
match the offset",
tzname = tz.diagnostic_name(),
),
ResolveRejectGap { given, before, after, ref tz } => write!(
f,
"datetime could not resolve to timestamp \
since `reject` conflict resolution was chosen, and \
because datetime has offset `{given}`, but the time \
zone `{tzname}` for the given datetime falls in a gap \
(between offsets `{before}` and `{after}`), and all \
offsets for a gap are regarded as invalid",
tzname = tz.diagnostic_name(),
),
ResolveRejectUnambiguous { given, offset, ref tz } => write!(
f,
"datetime could not resolve to a timestamp since \
`reject` conflict resolution was chosen, and because \
datetime has offset `{given}`, but the time \
zone `{tzname}` for the given datetime \
unambiguously has offset `{offset}`",
tzname = tz.diagnostic_name(),
),
RoundInvalidUnit { unit } => write!(
f,
"rounding time zone offset failed because \
a unit of {unit} was provided, \
but time zone offset rounding \
can only use hours, minutes or seconds",
unit = unit.plural(),
),
RoundOverflow => f.write_str(
"rounding time zone offset resulted in a duration \
that overflows",
),
}
}
}

35
src/error/tz/posix.rs Normal file
View file

@ -0,0 +1,35 @@
use crate::error;
#[derive(Clone, Debug)]
pub(crate) enum Error {
ColonPrefixInvalidUtf8,
InvalidPosixTz,
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::TzPosix(err).into()
}
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
ColonPrefixInvalidUtf8 => f.write_str(
"POSIX time zone string with a `:` prefix \
contains invalid UTF-8",
),
InvalidPosixTz => f.write_str("invalid POSIX time zone string"),
}
}
}

104
src/error/tz/system.rs Normal file
View file

@ -0,0 +1,104 @@
#[cfg(not(feature = "tz-system"))]
pub(crate) use self::disabled::*;
#[cfg(feature = "tz-system")]
pub(crate) use self::enabled::*;
#[cfg(not(feature = "tz-system"))]
mod disabled {
#[derive(Clone, Debug)]
pub(crate) enum Error {}
impl core::fmt::Display for Error {
fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
unreachable!()
}
}
}
#[cfg(feature = "tz-system")]
mod enabled {
use crate::error;
#[derive(Clone, Debug)]
pub(crate) enum Error {
FailedEnvTz,
FailedEnvTzAsTzif,
FailedPosixTzAndUtf8,
FailedSystemTimeZone,
FailedUnnamedTzifInvalid,
FailedUnnamedTzifRead,
#[cfg(windows)]
WindowsMissingIanaMapping,
#[cfg(windows)]
WindowsTimeZoneKeyName,
#[cfg(windows)]
WindowsUtf16DecodeInvalid,
#[cfg(windows)]
WindowsUtf16DecodeNul,
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::TzSystem(err).into()
}
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
FailedEnvTz => f.write_str(
"`TZ` environment variable set, but failed to read value",
),
FailedEnvTzAsTzif => f.write_str(
"failed to read `TZ` environment variable value \
as a TZif file after attempting (and failing) a tzdb \
lookup for that same value",
),
FailedPosixTzAndUtf8 => f.write_str(
"failed to parse `TZ` environment variable as either \
a POSIX time zone transition string or as valid UTF-8",
),
FailedSystemTimeZone => {
f.write_str("failed to find system time zone")
}
FailedUnnamedTzifInvalid => f.write_str(
"found invalid TZif data in unnamed time zone file",
),
FailedUnnamedTzifRead => f.write_str(
"failed to read TZif data from unnamed time zone file",
),
#[cfg(windows)]
WindowsMissingIanaMapping => f.write_str(
"found Windows time zone name, \
but could not find a mapping for it to an \
IANA time zone name",
),
#[cfg(windows)]
WindowsTimeZoneKeyName => f.write_str(
"could not get `TimeZoneKeyName` from \
winapi `DYNAMIC_TIME_ZONE_INFORMATION`",
),
#[cfg(windows)]
WindowsUtf16DecodeInvalid => f.write_str(
"failed to convert `u16` slice to UTF-8 \
(invalid UTF-16)",
),
#[cfg(windows)]
WindowsUtf16DecodeNul => f.write_str(
"failed to convert `u16` slice to UTF-8 \
(no NUL terminator found)",
),
}
}
}
}

40
src/error/tz/timezone.rs Normal file
View file

@ -0,0 +1,40 @@
use crate::error;
#[derive(Clone, Debug)]
pub(crate) enum Error {
ConvertNonFixed {
kind: &'static str,
},
#[cfg(not(feature = "tz-system"))]
FailedSystem,
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::TzTimeZone(err).into()
}
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
ConvertNonFixed { kind } => write!(
f,
"cannot convert non-fixed {kind} time zone to offset \
without a timestamp or civil datetime",
),
#[cfg(not(feature = "tz-system"))]
FailedSystem => f.write_str("failed to get system time zone"),
}
}
}

323
src/error/tz/zic.rs Normal file
View file

@ -0,0 +1,323 @@
#[cfg(not(test))]
pub(crate) use self::disabled::*;
#[cfg(test)]
pub(crate) use self::enabled::*;
#[cfg(not(test))]
mod disabled {
#[derive(Clone, Debug)]
pub(crate) enum Error {}
impl core::fmt::Display for Error {
fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
unreachable!()
}
}
}
#[cfg(test)]
mod enabled {
use alloc::boxed::Box;
use crate::error;
// `man zic` says that the max line length including the line
// terminator is 2048. The `core::str::Lines` iterator doesn't include
// the terminator, so we subtract 1 to account for that. Note that this
// could potentially allow one extra byte in the case of a \r\n line
// terminator, but this seems fine.
pub(crate) const MAX_LINE_LEN: usize = 2047;
#[derive(Clone, Debug)]
pub(crate) enum Error {
DuplicateLink { name: Box<str> },
DuplicateLinkZone { name: Box<str> },
DuplicateZone { name: Box<str> },
DuplicateZoneLink { name: Box<str> },
ExpectedCloseQuote,
ExpectedColonAfterHour,
ExpectedColonAfterMinute,
ExpectedContinuationZoneThreeFields,
ExpectedContinuationZoneLine { name: Box<str> },
ExpectedDotAfterSeconds,
ExpectedFirstZoneFourFields,
ExpectedLinkTwoFields,
ExpectedMinuteAfterHours,
ExpectedNameBegin,
ExpectedNanosecondDigits,
ExpectedNonEmptyAbbreviation,
ExpectedNonEmptyAt,
ExpectedNonEmptyName,
ExpectedNonEmptySave,
ExpectedNonEmptyZoneName,
ExpectedNothingAfterTime,
ExpectedRuleNineFields { got: usize },
ExpectedSecondAfterMinutes,
ExpectedTimeOneHour,
ExpectedUntilYear,
ExpectedWhitespaceAfterQuotedField,
ExpectedZoneNameComponentNoDots { component: Box<str> },
FailedContinuationZone,
FailedLinkLine,
FailedParseDay,
FailedParseFieldAt,
FailedParseFieldFormat,
FailedParseFieldFrom,
FailedParseFieldIn,
FailedParseFieldLetters,
FailedParseFieldLinkName,
FailedParseFieldLinkTarget,
FailedParseFieldName,
FailedParseFieldOn,
FailedParseFieldRules,
FailedParseFieldSave,
FailedParseFieldStdOff,
FailedParseFieldTo,
FailedParseFieldUntil,
FailedParseHour,
FailedParseMinute,
FailedParseMonth,
FailedParseNanosecond,
FailedParseSecond,
FailedParseTimeDuration,
FailedParseYear,
FailedRule { name: Box<str> },
FailedRuleLine,
FailedZoneFirst,
Line { number: usize },
LineMaxLength,
LineNul,
LineOverflow,
InvalidAbbreviation,
InvalidRuleYear { start: i16, end: i16 },
InvalidUtf8,
UnrecognizedAtTimeSuffix,
UnrecognizedDayOfMonthFormat,
UnrecognizedDayOfWeek,
UnrecognizedMonthName,
UnrecognizedSaveTimeSuffix,
UnrecognizedTrailingTimeDuration,
UnrecognizedZicLine,
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::TzZic(err).into()
}
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
DuplicateLink { ref name } => {
write!(f, "found duplicate link with name `{name}`")
}
DuplicateLinkZone { ref name } => write!(
f,
"found link with name `{name}` that conflicts \
with a zone of the same name",
),
DuplicateZone { ref name } => {
write!(f, "found duplicate zone with name `{name}`")
}
DuplicateZoneLink { ref name } => write!(
f,
"found zone with name `{name}` that conflicts \
with a link of the same name",
),
ExpectedCloseQuote => {
f.write_str("found unclosed quote for field")
}
ExpectedColonAfterHour => {
f.write_str("expected `:` after hours")
}
ExpectedColonAfterMinute => {
f.write_str("expected `:` after minutes")
}
ExpectedContinuationZoneLine { ref name } => write!(
f,
"expected continuation zone line for `{name}`, \
but found end of data instead",
),
ExpectedContinuationZoneThreeFields => f.write_str(
"expected continuation `ZONE` line \
to have at least 3 fields",
),
ExpectedDotAfterSeconds => {
f.write_str("expected `.` after seconds")
}
ExpectedFirstZoneFourFields => f.write_str(
"expected first `ZONE` line to have at least 4 fields",
),
ExpectedLinkTwoFields => {
f.write_str("expected exactly 2 fields after `LINK`")
}
ExpectedMinuteAfterHours => {
f.write_str("expected minute digits after `HH:`")
}
ExpectedNameBegin => f.write_str(
"`NAME` field cannot begin with a digit, `+` or `-`, \
but found `NAME` that begins with one of those",
),
ExpectedNanosecondDigits => {
f.write_str("expected nanosecond digits after `HH:MM:SS.`")
}
ExpectedNonEmptyAbbreviation => f.write_str(
"empty time zone abbreviations are not allowed",
),
ExpectedNonEmptyAt => {
f.write_str("`AT` field for rule cannot be empty")
}
ExpectedNonEmptyName => {
f.write_str("`NAME` field for rule cannot be empty")
}
ExpectedNonEmptySave => {
f.write_str("`SAVE` field for rule cannot be empty")
}
ExpectedNonEmptyZoneName => {
f.write_str("zone names cannot be empty")
}
ExpectedNothingAfterTime => f.write_str(
"expected no more fields after time of day, \
but found at least one",
),
ExpectedRuleNineFields { got } => write!(
f,
"expected exactly 9 fields for rule, \
but found {got} fields",
),
ExpectedSecondAfterMinutes => {
f.write_str("expected second digits after `HH:MM:`")
}
ExpectedTimeOneHour => f.write_str(
"expected time duration to contain \
at least one hour digit",
),
ExpectedUntilYear => f.write_str("expected at least a year"),
ExpectedWhitespaceAfterQuotedField => {
f.write_str("expected whitespace after quoted field")
}
ExpectedZoneNameComponentNoDots { ref component } => write!(
f,
"component `{component}` in zone name cannot \
be \".\" or \"..\"",
),
FailedContinuationZone => {
f.write_str("failed to parse continuation `Zone` line")
}
FailedLinkLine => f.write_str("failed to parse `Link` line"),
FailedParseDay => f.write_str("failed to parse day"),
FailedParseFieldAt => {
f.write_str("failed to parse `NAME` field")
}
FailedParseFieldFormat => {
f.write_str("failed to parse `FORMAT` field")
}
FailedParseFieldFrom => {
f.write_str("failed to parse `FROM` field")
}
FailedParseFieldIn => {
f.write_str("failed to parse `IN` field")
}
FailedParseFieldLetters => {
f.write_str("failed to parse `LETTERS` field")
}
FailedParseFieldLinkName => {
f.write_str("failed to parse `LINK` name field")
}
FailedParseFieldLinkTarget => {
f.write_str("failed to parse `LINK` target field")
}
FailedParseFieldName => {
f.write_str("failed to parse `NAME` field")
}
FailedParseFieldOn => {
f.write_str("failed to parse `ON` field")
}
FailedParseFieldRules => {
f.write_str("failed to parse `RULES` field")
}
FailedParseFieldSave => {
f.write_str("failed to parse `SAVE` field")
}
FailedParseFieldStdOff => {
f.write_str("failed to parse `STDOFF` field")
}
FailedParseFieldTo => {
f.write_str("failed to parse `TO` field")
}
FailedParseFieldUntil => {
f.write_str("failed to parse `UNTIL` field")
}
FailedParseHour => f.write_str("failed to parse hour"),
FailedParseMinute => f.write_str("failed to parse minute"),
FailedParseMonth => f.write_str("failed to parse month"),
FailedParseNanosecond => {
f.write_str("failed to parse nanosecond")
}
FailedParseSecond => f.write_str("failed to parse second"),
FailedParseTimeDuration => {
f.write_str("failed to parse time duration")
}
FailedParseYear => f.write_str("failed to parse year"),
FailedRule { name: ref rule } => {
write!(f, "failed to parse rule `{rule}`")
}
FailedRuleLine => f.write_str("failed to parse `Rule` line"),
FailedZoneFirst => {
f.write_str("failed to parse first `Zone` line")
}
InvalidAbbreviation => f.write_str(
"time zone abbreviation \
contains invalid character; only \"+\", \"-\" and \
ASCII alpha-numeric characters are allowed",
),
InvalidRuleYear { start, end } => write!(
f,
"found start year={start} \
to be greater than end year={end}"
),
InvalidUtf8 => f.write_str("invalid UTF-8"),
Line { number } => write!(f, "line {number}"),
LineMaxLength => write!(
f,
"found line with length that exceeds \
max length of {MAX_LINE_LEN}",
),
LineNul => f.write_str(
"found line with NUL byte, which isn't allowed",
),
LineOverflow => f.write_str("line count overflowed"),
UnrecognizedAtTimeSuffix => {
f.write_str("unrecognized `AT` time suffix")
}
UnrecognizedDayOfMonthFormat => {
f.write_str("unrecognized format for day-of-month")
}
UnrecognizedDayOfWeek => {
f.write_str("unrecognized day of the week")
}
UnrecognizedMonthName => {
f.write_str("unrecognized month name")
}
UnrecognizedSaveTimeSuffix => {
f.write_str("unrecognized `SAVE` time suffix")
}
UnrecognizedTrailingTimeDuration => {
f.write_str("found unrecognized suffix in time duration")
}
UnrecognizedZicLine => f.write_str("unrecognized zic line"),
}
}
}
}

194
src/error/util.rs Normal file
View file

@ -0,0 +1,194 @@
use crate::{error, util::escape::Byte, Unit};
#[derive(Clone, Debug)]
pub(crate) enum RoundingIncrementError {
ForDateTime,
ForSpan,
ForTime,
ForTimestamp,
GreaterThanZero { unit: Unit },
InvalidDivide { unit: Unit, must_divide: i64 },
Unsupported { unit: Unit },
}
impl From<RoundingIncrementError> for error::Error {
#[cold]
#[inline(never)]
fn from(err: RoundingIncrementError) -> error::Error {
error::ErrorKind::RoundingIncrement(err).into()
}
}
impl error::IntoError for RoundingIncrementError {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for RoundingIncrementError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::RoundingIncrementError::*;
match *self {
ForDateTime => f.write_str("failed rounding datetime"),
ForSpan => f.write_str("failed rounding span"),
ForTime => f.write_str("failed rounding time"),
ForTimestamp => f.write_str("failed rounding timestamp"),
GreaterThanZero { unit } => write!(
f,
"rounding increment for {unit} must be greater than zero",
unit = unit.plural(),
),
InvalidDivide { unit, must_divide } => write!(
f,
"increment for rounding to {unit} \
must be 1) less than {must_divide}, 2) divide into \
it evenly and 3) greater than zero",
unit = unit.plural(),
),
Unsupported { unit } => write!(
f,
"rounding to {unit} is not supported",
unit = unit.plural(),
),
}
}
}
#[derive(Clone, Debug)]
pub(crate) enum ParseIntError {
NoDigitsFound,
InvalidDigit(u8),
TooBig,
}
impl From<ParseIntError> for error::Error {
#[cold]
#[inline(never)]
fn from(err: ParseIntError) -> error::Error {
error::ErrorKind::ParseInt(err).into()
}
}
impl error::IntoError for ParseIntError {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for ParseIntError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::ParseIntError::*;
match *self {
NoDigitsFound => write!(f, "invalid number, no digits found"),
InvalidDigit(got) => {
write!(f, "invalid digit, expected 0-9 but got {}", Byte(got))
}
TooBig => {
write!(f, "number too big to parse into 64-bit integer")
}
}
}
}
#[derive(Clone, Debug)]
pub(crate) enum ParseFractionError {
NoDigitsFound,
TooManyDigits,
InvalidDigit(u8),
TooBig,
}
impl ParseFractionError {
pub(crate) const MAX_PRECISION: usize = 9;
}
impl From<ParseFractionError> for error::Error {
#[cold]
#[inline(never)]
fn from(err: ParseFractionError) -> error::Error {
error::ErrorKind::ParseFraction(err).into()
}
}
impl error::IntoError for ParseFractionError {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for ParseFractionError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::ParseFractionError::*;
match *self {
NoDigitsFound => write!(f, "invalid fraction, no digits found"),
TooManyDigits => write!(
f,
"invalid fraction, too many digits \
(at most {max} are allowed)",
max = ParseFractionError::MAX_PRECISION,
),
InvalidDigit(got) => {
write!(
f,
"invalid fractional digit, expected 0-9 but got {}",
Byte(got)
)
}
TooBig => {
write!(
f,
"fractional number too big to parse into 64-bit integer"
)
}
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct OsStrUtf8Error {
#[cfg(feature = "std")]
value: alloc::boxed::Box<std::ffi::OsStr>,
}
#[cfg(feature = "std")]
impl From<&std::ffi::OsStr> for OsStrUtf8Error {
#[cold]
#[inline(never)]
fn from(value: &std::ffi::OsStr) -> OsStrUtf8Error {
OsStrUtf8Error { value: value.into() }
}
}
impl From<OsStrUtf8Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: OsStrUtf8Error) -> error::Error {
error::ErrorKind::OsStrUtf8(err).into()
}
}
impl error::IntoError for OsStrUtf8Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for OsStrUtf8Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
#[cfg(feature = "std")]
{
write!(
f,
"environment value `{value:?}` is not valid UTF-8",
value = self.value
)
}
#[cfg(not(feature = "std"))]
{
write!(f, "<BUG: SHOULD NOT EXIST>")
}
}
}

71
src/error/zoned.rs Normal file
View file

@ -0,0 +1,71 @@
use crate::{error, Unit};
#[derive(Clone, Debug)]
pub(crate) enum Error {
AddDateTime,
AddDays,
AddTimestamp,
ConvertDateTimeToTimestamp,
ConvertIntermediateDatetime,
FailedLengthOfDay,
FailedSpanNanoseconds,
FailedStartOfDay,
MismatchTimeZoneUntil { largest: Unit },
}
impl From<Error> for error::Error {
#[cold]
#[inline(never)]
fn from(err: Error) -> error::Error {
error::ErrorKind::Zoned(err).into()
}
}
impl error::IntoError for Error {
fn into_error(self) -> error::Error {
self.into()
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::Error::*;
match *self {
AddDateTime => f.write_str(
"failed to add span to datetime from zoned datetime",
),
AddDays => {
f.write_str("failed to add days to date in zoned datetime")
}
AddTimestamp => f.write_str(
"failed to add span to timestamp from zoned datetime",
),
ConvertDateTimeToTimestamp => {
f.write_str("failed to convert civil datetime to timestamp")
}
ConvertIntermediateDatetime => f.write_str(
"failed to convert intermediate datetime \
to zoned timestamp",
),
FailedLengthOfDay => f.write_str(
"failed to add 1 day to zoned datetime to find length of day",
),
FailedSpanNanoseconds => f.write_str(
"failed to compute span in nanoseconds \
between zoned datetimes",
),
FailedStartOfDay => {
f.write_str("failed to find start of day for zoned datetime")
}
MismatchTimeZoneUntil { largest } => write!(
f,
"computing the span between zoned datetimes, with \
{largest} units, requires that the time zones are \
equivalent, but the zoned datetimes have distinct \
time zones",
largest = largest.singular(),
),
}
}
}

View file

@ -256,7 +256,9 @@ assert_eq!(dur, Duration::new(30 * 24 * 60 * 60 + 38_016, 0));
// In contrast, Jiff will reject `1M`:
assert_eq!(
"1M".parse::<jiff::Span>().unwrap_err().to_string(),
"failed to parse \"1M\" in the \"friendly\" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found input beginning with \"M\" instead",
"failed to parse input in the \"friendly\" duration format: \
expected to find unit designator suffix \
(e.g., `years` or `secs`) after parsing integer",
);
# Ok::<(), Box<dyn std::error::Error>>(())
@ -335,7 +337,9 @@ assert_eq!(
// Jiff is saving you from doing something wrong
assert_eq!(
"1 day".parse::<SignedDuration>().unwrap_err().to_string(),
"failed to parse \"1 day\" in the \"friendly\" format: parsing day units into a `SignedDuration` is not supported (perhaps try parsing into a `Span` instead)",
"failed to parse input in the \"friendly\" duration format: \
parsing calendar units (days in this case) in this context \
is not supported (perhaps try parsing into a `jiff::Span` instead)",
);
```

View file

@ -1,11 +1,11 @@
use crate::{
error::{err, ErrorContext},
error::{fmt::friendly::Error as E, ErrorContext},
fmt::{
friendly::parser_label,
util::{parse_temporal_fraction, DurationUnits},
Parsed,
},
util::{c::Sign, escape, parse},
util::{c::Sign, parse},
Error, SignedDuration, Span, Unit,
};
@ -188,12 +188,7 @@ impl SpanParser {
}
let input = input.as_ref();
imp(self, input).with_context(|| {
err!(
"failed to parse {input:?} in the \"friendly\" format",
input = escape::Bytes(input)
)
})
imp(self, input).context(E::Failed)
}
/// Run the parser on the given string (which may be plain bytes) and,
@ -248,12 +243,7 @@ impl SpanParser {
}
let input = input.as_ref();
imp(self, input).with_context(|| {
err!(
"failed to parse {input:?} in the \"friendly\" format",
input = escape::Bytes(input)
)
})
imp(self, input).context(E::Failed)
}
/// Run the parser on the given string (which may be plain bytes) and,
@ -312,12 +302,7 @@ impl SpanParser {
}
let input = input.as_ref();
imp(self, input).with_context(|| {
err!(
"failed to parse {input:?} in the \"friendly\" format",
input = escape::Bytes(input)
)
})
imp(self, input).context(E::Failed)
}
#[cfg_attr(feature = "perf-inline", inline(always))]
@ -327,7 +312,7 @@ impl SpanParser {
builder: &mut DurationUnits,
) -> Result<Parsed<'i, ()>, Error> {
if input.is_empty() {
return Err(err!("an empty string is not a valid duration"));
return Err(Error::from(E::Empty));
}
// Guard prefix sign parsing to avoid the function call, which is
// marked unlineable to keep the fast path tighter.
@ -342,11 +327,7 @@ impl SpanParser {
let Parsed { value, input } = self.parse_unit_value(input)?;
let Some(first_unit_value) = value else {
return Err(err!(
"parsing a friendly duration requires it to start \
with a unit value (a decimal integer) after an \
optional sign, but no integer was found",
));
return Err(Error::from(E::ExpectedIntegerAfterSign));
};
let Parsed { input, .. } =
@ -434,11 +415,7 @@ impl SpanParser {
parsed_any_after_comma = true;
}
if !parsed_any_after_comma {
return Err(err!(
"found comma at the end of duration, \
but a comma indicates at least one more \
unit follows",
));
return Err(Error::from(E::ExpectedOneMoreUnitAfterComma));
}
Ok(Parsed { value: (), input })
}
@ -454,10 +431,13 @@ impl SpanParser {
input: &'i [u8],
hour: u64,
) -> Result<Parsed<'i, Option<HMS>>, Error> {
if !input.first().map_or(false, |&b| b == b':') {
let Some((&first, tail)) = input.split_first() else {
return Ok(Parsed { input, value: None });
};
if first != b':' {
return Ok(Parsed { input, value: None });
}
let Parsed { input, value } = self.parse_hms(&input[1..], hour)?;
let Parsed { input, value } = self.parse_hms(tail, hour)?;
Ok(Parsed { input, value: Some(value) })
}
@ -477,26 +457,16 @@ impl SpanParser {
hour: u64,
) -> Result<Parsed<'i, HMS>, Error> {
let Parsed { input, value } = self.parse_unit_value(input)?;
let Some(minute) = value else {
return Err(err!(
"expected to parse minute in 'HH:MM:SS' format \
following parsed hour of {hour}",
));
};
if !input.first().map_or(false, |&b| b == b':') {
return Err(err!(
"when parsing 'HH:MM:SS' format, expected to \
see a ':' after the parsed minute of {minute}",
));
let minute = value.ok_or(E::ExpectedMinuteAfterHour)?;
let (&first, input) =
input.split_first().ok_or(E::ExpectedColonAfterMinute)?;
if first != b':' {
return Err(Error::from(E::ExpectedColonAfterMinute));
}
let input = &input[1..];
let Parsed { input, value } = self.parse_unit_value(input)?;
let Some(second) = value else {
return Err(err!(
"expected to parse second in 'HH:MM:SS' format \
following parsed minute of {minute}",
));
};
let second = value.ok_or(E::ExpectedSecondAfterMinute)?;
let (fraction, input) =
if input.first().map_or(false, |&b| b == b'.' || b == b',') {
let parsed = parse_temporal_fraction(input)?;
@ -540,22 +510,8 @@ impl SpanParser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, Unit>, Error> {
let Some((unit, len)) = parser_label::find(input) else {
if input.is_empty() {
return Err(err!(
"expected to find unit designator suffix \
(e.g., 'years' or 'secs'), \
but found end of input",
));
} else {
return Err(err!(
"expected to find unit designator suffix \
(e.g., 'years' or 'secs'), \
but found input beginning with {found:?} instead",
found = escape::Bytes(&input[..input.len().min(20)]),
));
}
};
let (unit, len) =
parser_label::find(input).ok_or(E::ExpectedUnitSuffix)?;
Ok(Parsed { value: unit, input: &input[len..] })
}
@ -606,17 +562,15 @@ impl SpanParser {
}
// Eat any additional whitespace we find before looking for 'ago'.
input = self.parse_optional_whitespace(&input[1..]).input;
let (suffix_sign, input) = if input.starts_with(b"ago") {
(Some(Sign::Negative), &input[3..])
} else {
(None, input)
};
let (suffix_sign, input) =
if let Some(tail) = input.strip_prefix(b"ago") {
(Some(Sign::Negative), tail)
} else {
(None, input)
};
let sign = match (prefix_sign, suffix_sign) {
(Some(_), Some(_)) => {
return Err(err!(
"expected to find either a prefix sign (+/-) or \
a suffix sign (ago), but found both",
))
return Err(Error::from(E::ExpectedOneSign));
}
(Some(sign), None) => sign,
(None, Some(sign)) => sign,
@ -637,24 +591,24 @@ impl SpanParser {
#[inline(never)]
fn parse_optional_comma<'i>(
&self,
mut input: &'i [u8],
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
if !input.first().map_or(false, |&b| b == b',') {
let Some((&first, tail)) = input.split_first() else {
return Ok(Parsed { value: (), input });
};
if first != b',' {
return Ok(Parsed { value: (), input });
}
input = &input[1..];
if input.is_empty() {
return Err(err!(
"expected whitespace after comma, but found end of input"
));
let (second, input) = tail
.split_first()
.ok_or(E::ExpectedWhitespaceAfterCommaEndOfInput)?;
if !is_whitespace(second) {
return Err(Error::from(E::ExpectedWhitespaceAfterComma {
byte: *second,
}));
}
if !is_whitespace(&input[0]) {
return Err(err!(
"expected whitespace after comma, but found {found:?}",
found = escape::Byte(input[0]),
));
}
Ok(Parsed { value: (), input: &input[1..] })
Ok(Parsed { value: (), input })
}
/// Parses zero or more bytes of ASCII whitespace.
@ -776,35 +730,35 @@ mod tests {
insta::assert_snapshot!(
p(""),
@r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###,
@r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#,
);
insta::assert_snapshot!(
p(" "),
@r###"failed to parse " " in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
@r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
);
insta::assert_snapshot!(
p("a"),
@r###"failed to parse "a" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
@r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
);
insta::assert_snapshot!(
p("2 months 1 year"),
@r###"failed to parse "2 months 1 year" in the "friendly" format: found value 1 with unit year after unit month, but units must be written from largest to smallest (and they can't be repeated)"###,
@r#"failed to parse input in the "friendly" duration format: found value with unit year after unit month, but units must be written from largest to smallest (and they can't be repeated)"#,
);
insta::assert_snapshot!(
p("1 year 1 mont"),
@r###"failed to parse "1 year 1 mont" in the "friendly" format: parsed value 'P1Y1M', but unparsed input "nt" remains (expected no unparsed input)"###,
@r#"failed to parse input in the "friendly" duration format: parsed value 'P1Y1M', but unparsed input "nt" remains (expected no unparsed input)"#,
);
insta::assert_snapshot!(
p("2 months,"),
@r###"failed to parse "2 months," in the "friendly" format: expected whitespace after comma, but found end of input"###,
@r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#,
);
insta::assert_snapshot!(
p("2 months, "),
@r#"failed to parse "2 months, " in the "friendly" format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
@r#"failed to parse input in the "friendly" duration format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
);
insta::assert_snapshot!(
p("2 months ,"),
@r###"failed to parse "2 months ," in the "friendly" format: parsed value 'P2M', but unparsed input "," remains (expected no unparsed input)"###,
@r#"failed to parse input in the "friendly" duration format: parsed value 'P2M', but unparsed input "," remains (expected no unparsed input)"#,
);
}
@ -814,19 +768,19 @@ mod tests {
insta::assert_snapshot!(
p("1yago"),
@r###"failed to parse "1yago" in the "friendly" format: parsed value 'P1Y', but unparsed input "ago" remains (expected no unparsed input)"###,
@r#"failed to parse input in the "friendly" duration format: parsed value 'P1Y', but unparsed input "ago" remains (expected no unparsed input)"#,
);
insta::assert_snapshot!(
p("1 year 1 monthago"),
@r###"failed to parse "1 year 1 monthago" in the "friendly" format: parsed value 'P1Y1M', but unparsed input "ago" remains (expected no unparsed input)"###,
@r#"failed to parse input in the "friendly" duration format: parsed value 'P1Y1M', but unparsed input "ago" remains (expected no unparsed input)"#,
);
insta::assert_snapshot!(
p("+1 year 1 month ago"),
@r###"failed to parse "+1 year 1 month ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
@r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
);
insta::assert_snapshot!(
p("-1 year 1 month ago"),
@r###"failed to parse "-1 year 1 month ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
@r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
);
}
@ -840,7 +794,7 @@ mod tests {
// the maximum number of microseconds is subtracted off, and we're
// left over with a value that overflows an i64.
pe("640330789636854776 micros"),
@r#"failed to parse "640330789636854776 micros" in the "friendly" format: failed to set value 640330789636854776 as microsecond unit on span: failed to set nanosecond value 9223372036854776000 (it overflows `i64`) on span determined from 640330789636854776.0"#,
@r#"failed to parse input in the "friendly" duration format: failed to set value for microsecond unit on span: failed to set nanosecond value from fractional component"#,
);
// one fewer is okay
insta::assert_snapshot!(
@ -853,7 +807,7 @@ mod tests {
// different error path by using an explicit fraction. Here, if
// we had x.807 micros, it would parse successfully.
pe("640330789636854775.808 micros"),
@r#"failed to parse "640330789636854775.808 micros" in the "friendly" format: failed to set nanosecond value 9223372036854775808 (it overflows `i64`) on span determined from 640330789636854775.808000000"#,
@r#"failed to parse input in the "friendly" duration format: failed to set nanosecond value from fractional component"#,
);
// one fewer is okay
insta::assert_snapshot!(
@ -868,47 +822,47 @@ mod tests {
insta::assert_snapshot!(
p("19999 years"),
@r###"failed to parse "19999 years" in the "friendly" format: failed to set value 19999 as year unit on span: parameter 'years' with value 19999 is not in the required range of -19998..=19998"###,
@r#"failed to parse input in the "friendly" duration format: failed to set value for year unit on span: parameter 'years' with value 19999 is not in the required range of -19998..=19998"#,
);
insta::assert_snapshot!(
p("19999 years ago"),
@r#"failed to parse "19999 years ago" in the "friendly" format: failed to set value -19999 as year unit on span: parameter 'years' with value -19999 is not in the required range of -19998..=19998"#,
@r#"failed to parse input in the "friendly" duration format: failed to set value for year unit on span: parameter 'years' with value -19999 is not in the required range of -19998..=19998"#,
);
insta::assert_snapshot!(
p("239977 months"),
@r###"failed to parse "239977 months" in the "friendly" format: failed to set value 239977 as month unit on span: parameter 'months' with value 239977 is not in the required range of -239976..=239976"###,
@r#"failed to parse input in the "friendly" duration format: failed to set value for month unit on span: parameter 'months' with value 239977 is not in the required range of -239976..=239976"#,
);
insta::assert_snapshot!(
p("239977 months ago"),
@r#"failed to parse "239977 months ago" in the "friendly" format: failed to set value -239977 as month unit on span: parameter 'months' with value -239977 is not in the required range of -239976..=239976"#,
@r#"failed to parse input in the "friendly" duration format: failed to set value for month unit on span: parameter 'months' with value -239977 is not in the required range of -239976..=239976"#,
);
insta::assert_snapshot!(
p("1043498 weeks"),
@r###"failed to parse "1043498 weeks" in the "friendly" format: failed to set value 1043498 as week unit on span: parameter 'weeks' with value 1043498 is not in the required range of -1043497..=1043497"###,
@r#"failed to parse input in the "friendly" duration format: failed to set value for week unit on span: parameter 'weeks' with value 1043498 is not in the required range of -1043497..=1043497"#,
);
insta::assert_snapshot!(
p("1043498 weeks ago"),
@r#"failed to parse "1043498 weeks ago" in the "friendly" format: failed to set value -1043498 as week unit on span: parameter 'weeks' with value -1043498 is not in the required range of -1043497..=1043497"#,
@r#"failed to parse input in the "friendly" duration format: failed to set value for week unit on span: parameter 'weeks' with value -1043498 is not in the required range of -1043497..=1043497"#,
);
insta::assert_snapshot!(
p("7304485 days"),
@r###"failed to parse "7304485 days" in the "friendly" format: failed to set value 7304485 as day unit on span: parameter 'days' with value 7304485 is not in the required range of -7304484..=7304484"###,
@r#"failed to parse input in the "friendly" duration format: failed to set value for day unit on span: parameter 'days' with value 7304485 is not in the required range of -7304484..=7304484"#,
);
insta::assert_snapshot!(
p("7304485 days ago"),
@r#"failed to parse "7304485 days ago" in the "friendly" format: failed to set value -7304485 as day unit on span: parameter 'days' with value -7304485 is not in the required range of -7304484..=7304484"#,
@r#"failed to parse input in the "friendly" duration format: failed to set value for day unit on span: parameter 'days' with value -7304485 is not in the required range of -7304484..=7304484"#,
);
insta::assert_snapshot!(
p("9223372036854775808 nanoseconds"),
@r#"failed to parse "9223372036854775808 nanoseconds" in the "friendly" format: `9223372036854775808` nanoseconds is too big (or small) to fit into a signed 64-bit integer"#,
@r#"failed to parse input in the "friendly" duration format: value for nanoseconds is too big (or small) to fit into a signed 64-bit integer"#,
);
insta::assert_snapshot!(
p("9223372036854775808 nanoseconds ago"),
@r#"failed to parse "9223372036854775808 nanoseconds ago" in the "friendly" format: failed to set value -9223372036854775808 as nanosecond unit on span: parameter 'nanoseconds' with value -9223372036854775808 is not in the required range of -9223372036854775807..=9223372036854775807"#,
@r#"failed to parse input in the "friendly" duration format: failed to set value for nanosecond unit on span: parameter 'nanoseconds' with value -9223372036854775808 is not in the required range of -9223372036854775807..=9223372036854775807"#,
);
}
@ -918,11 +872,11 @@ mod tests {
insta::assert_snapshot!(
p("1.5 years"),
@r#"failed to parse "1.5 years" in the "friendly" format: fractional years are not supported"#,
@r#"failed to parse input in the "friendly" duration format: fractional years are not supported"#,
);
insta::assert_snapshot!(
p("1.5 nanos"),
@r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#,
@r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#,
);
}
@ -932,19 +886,19 @@ mod tests {
insta::assert_snapshot!(
p("05:"),
@r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###,
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#,
);
insta::assert_snapshot!(
p("05:06"),
@r###"failed to parse "05:06" in the "friendly" format: when parsing 'HH:MM:SS' format, expected to see a ':' after the parsed minute of 6"###,
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#,
);
insta::assert_snapshot!(
p("05:06:"),
@r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###,
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#,
);
insta::assert_snapshot!(
p("2 hours, 05:06:07"),
@r#"failed to parse "2 hours, 05:06:07" in the "friendly" format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
@r#"failed to parse input in the "friendly" duration format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
);
}
@ -968,7 +922,7 @@ mod tests {
);
insta::assert_snapshot!(
perr("9223372036854775808s"),
@r#"failed to parse "9223372036854775808s" in the "friendly" format: `9223372036854775808` seconds is too big (or small) to fit into a signed 64-bit integer"#,
@r#"failed to parse input in the "friendly" duration format: value for seconds is too big (or small) to fit into a signed 64-bit integer"#,
);
insta::assert_snapshot!(
p("-9223372036854775808s"),
@ -1032,21 +986,21 @@ mod tests {
insta::assert_snapshot!(p("-2562047788015215hours"), @"-PT2562047788015215H");
insta::assert_snapshot!(
pe("2562047788015216hrs"),
@r#"failed to parse "2562047788015216hrs" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 2562047788015216 of unit hour"#,
@r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit hour"#,
);
insta::assert_snapshot!(p("153722867280912930minutes"), @"PT2562047788015215H30M");
insta::assert_snapshot!(p("153722867280912930minutes ago"), @"-PT2562047788015215H30M");
insta::assert_snapshot!(
pe("153722867280912931mins"),
@r#"failed to parse "153722867280912931mins" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 153722867280912931 of unit minute"#,
@r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit minute"#,
);
insta::assert_snapshot!(p("9223372036854775807seconds"), @"PT2562047788015215H30M7S");
insta::assert_snapshot!(p("-9223372036854775807seconds"), @"-PT2562047788015215H30M7S");
insta::assert_snapshot!(
pe("9223372036854775808s"),
@r#"failed to parse "9223372036854775808s" in the "friendly" format: `9223372036854775808` seconds is too big (or small) to fit into a signed 64-bit integer"#,
@r#"failed to parse input in the "friendly" duration format: value for seconds is too big (or small) to fit into a signed 64-bit integer"#,
);
insta::assert_snapshot!(
p("-9223372036854775808s"),
@ -1060,39 +1014,39 @@ mod tests {
insta::assert_snapshot!(
p(""),
@r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###,
@r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#,
);
insta::assert_snapshot!(
p(" "),
@r###"failed to parse " " in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
@r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
);
insta::assert_snapshot!(
p("5"),
@r###"failed to parse "5" in the "friendly" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found end of input"###,
@r#"failed to parse input in the "friendly" duration format: expected to find unit designator suffix (e.g., `years` or `secs`) after parsing integer"#,
);
insta::assert_snapshot!(
p("a"),
@r###"failed to parse "a" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
@r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
);
insta::assert_snapshot!(
p("2 minutes 1 hour"),
@r###"failed to parse "2 minutes 1 hour" in the "friendly" format: found value 1 with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"###,
@r#"failed to parse input in the "friendly" duration format: found value with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"#,
);
insta::assert_snapshot!(
p("1 hour 1 minut"),
@r###"failed to parse "1 hour 1 minut" in the "friendly" format: parsed value 'PT1H1M', but unparsed input "ut" remains (expected no unparsed input)"###,
@r#"failed to parse input in the "friendly" duration format: parsed value 'PT1H1M', but unparsed input "ut" remains (expected no unparsed input)"#,
);
insta::assert_snapshot!(
p("2 minutes,"),
@r###"failed to parse "2 minutes," in the "friendly" format: expected whitespace after comma, but found end of input"###,
@r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#,
);
insta::assert_snapshot!(
p("2 minutes, "),
@r#"failed to parse "2 minutes, " in the "friendly" format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
@r#"failed to parse input in the "friendly" duration format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
);
insta::assert_snapshot!(
p("2 minutes ,"),
@r###"failed to parse "2 minutes ," in the "friendly" format: parsed value 'PT2M', but unparsed input "," remains (expected no unparsed input)"###,
@r#"failed to parse input in the "friendly" duration format: parsed value 'PT2M', but unparsed input "," remains (expected no unparsed input)"#,
);
}
@ -1102,19 +1056,19 @@ mod tests {
insta::assert_snapshot!(
p("1hago"),
@r###"failed to parse "1hago" in the "friendly" format: parsed value 'PT1H', but unparsed input "ago" remains (expected no unparsed input)"###,
@r#"failed to parse input in the "friendly" duration format: parsed value 'PT1H', but unparsed input "ago" remains (expected no unparsed input)"#,
);
insta::assert_snapshot!(
p("1 hour 1 minuteago"),
@r###"failed to parse "1 hour 1 minuteago" in the "friendly" format: parsed value 'PT1H1M', but unparsed input "ago" remains (expected no unparsed input)"###,
@r#"failed to parse input in the "friendly" duration format: parsed value 'PT1H1M', but unparsed input "ago" remains (expected no unparsed input)"#,
);
insta::assert_snapshot!(
p("+1 hour 1 minute ago"),
@r###"failed to parse "+1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
@r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
);
insta::assert_snapshot!(
p("-1 hour 1 minute ago"),
@r###"failed to parse "-1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
@r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
);
}
@ -1127,7 +1081,7 @@ mod tests {
// Unlike `Span`, this just overflows because it can't be parsed
// as a 64-bit integer.
pe("9223372036854775808 micros"),
@r#"failed to parse "9223372036854775808 micros" in the "friendly" format: `9223372036854775808` microseconds is too big (or small) to fit into a signed 64-bit integer"#,
@r#"failed to parse input in the "friendly" duration format: value for microseconds is too big (or small) to fit into a signed 64-bit integer"#,
);
// one fewer is okay
insta::assert_snapshot!(
@ -1142,7 +1096,7 @@ mod tests {
insta::assert_snapshot!(
p("1.5 nanos"),
@r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#,
@r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#,
);
}
@ -1152,19 +1106,19 @@ mod tests {
insta::assert_snapshot!(
p("05:"),
@r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###,
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#,
);
insta::assert_snapshot!(
p("05:06"),
@r###"failed to parse "05:06" in the "friendly" format: when parsing 'HH:MM:SS' format, expected to see a ':' after the parsed minute of 6"###,
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#,
);
insta::assert_snapshot!(
p("05:06:"),
@r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###,
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#,
);
insta::assert_snapshot!(
p("2 hours, 05:06:07"),
@r#"failed to parse "2 hours, 05:06:07" in the "friendly" format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
@r#"failed to parse input in the "friendly" duration format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
);
}
@ -1205,11 +1159,11 @@ mod tests {
);
insta::assert_snapshot!(
perr("18446744073709551616s"),
@r#"failed to parse "18446744073709551616s" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#,
@r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#,
);
insta::assert_snapshot!(
perr("-1s"),
@r#"failed to parse "-1s" in the "friendly" format: cannot parse negative duration into unsigned `std::time::Duration`"#,
@r#"failed to parse input in the "friendly" duration format: cannot parse negative duration into unsigned `std::time::Duration`"#,
);
}
@ -1263,19 +1217,19 @@ mod tests {
insta::assert_snapshot!(p("5124095576030431hours"), @"PT5124095576030431H");
insta::assert_snapshot!(
pe("5124095576030432hrs"),
@r#"failed to parse "5124095576030432hrs" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 5124095576030432 of unit hour"#,
@r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit hour"#,
);
insta::assert_snapshot!(p("307445734561825860minutes"), @"PT5124095576030431H");
insta::assert_snapshot!(
pe("307445734561825861mins"),
@r#"failed to parse "307445734561825861mins" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 307445734561825861 of unit minute"#,
@r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit minute"#,
);
insta::assert_snapshot!(p("18446744073709551615seconds"), @"PT5124095576030431H15S");
insta::assert_snapshot!(
pe("18446744073709551616s"),
@r#"failed to parse "18446744073709551616s" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#,
@r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#,
);
}
@ -1287,39 +1241,39 @@ mod tests {
insta::assert_snapshot!(
p(""),
@r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###,
@r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#,
);
insta::assert_snapshot!(
p(" "),
@r###"failed to parse " " in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
@r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
);
insta::assert_snapshot!(
p("5"),
@r###"failed to parse "5" in the "friendly" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found end of input"###,
@r#"failed to parse input in the "friendly" duration format: expected to find unit designator suffix (e.g., `years` or `secs`) after parsing integer"#,
);
insta::assert_snapshot!(
p("a"),
@r###"failed to parse "a" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
@r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
);
insta::assert_snapshot!(
p("2 minutes 1 hour"),
@r###"failed to parse "2 minutes 1 hour" in the "friendly" format: found value 1 with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"###,
@r#"failed to parse input in the "friendly" duration format: found value with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"#,
);
insta::assert_snapshot!(
p("1 hour 1 minut"),
@r#"failed to parse "1 hour 1 minut" in the "friendly" format: parsed value '3660s', but unparsed input "ut" remains (expected no unparsed input)"#,
@r#"failed to parse input in the "friendly" duration format: parsed value '3660s', but unparsed input "ut" remains (expected no unparsed input)"#,
);
insta::assert_snapshot!(
p("2 minutes,"),
@r###"failed to parse "2 minutes," in the "friendly" format: expected whitespace after comma, but found end of input"###,
@r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#,
);
insta::assert_snapshot!(
p("2 minutes, "),
@r#"failed to parse "2 minutes, " in the "friendly" format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
@r#"failed to parse input in the "friendly" duration format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
);
insta::assert_snapshot!(
p("2 minutes ,"),
@r#"failed to parse "2 minutes ," in the "friendly" format: parsed value '120s', but unparsed input "," remains (expected no unparsed input)"#,
@r#"failed to parse input in the "friendly" duration format: parsed value '120s', but unparsed input "," remains (expected no unparsed input)"#,
);
}
@ -1331,19 +1285,19 @@ mod tests {
insta::assert_snapshot!(
p("1hago"),
@r#"failed to parse "1hago" in the "friendly" format: parsed value '3600s', but unparsed input "ago" remains (expected no unparsed input)"#,
@r#"failed to parse input in the "friendly" duration format: parsed value '3600s', but unparsed input "ago" remains (expected no unparsed input)"#,
);
insta::assert_snapshot!(
p("1 hour 1 minuteago"),
@r#"failed to parse "1 hour 1 minuteago" in the "friendly" format: parsed value '3660s', but unparsed input "ago" remains (expected no unparsed input)"#,
@r#"failed to parse input in the "friendly" duration format: parsed value '3660s', but unparsed input "ago" remains (expected no unparsed input)"#,
);
insta::assert_snapshot!(
p("+1 hour 1 minute ago"),
@r###"failed to parse "+1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
@r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
);
insta::assert_snapshot!(
p("-1 hour 1 minute ago"),
@r###"failed to parse "-1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
@r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
);
}
@ -1362,7 +1316,7 @@ mod tests {
// Unlike `Span`, this just overflows because it can't be parsed
// as a 64-bit integer.
pe("18446744073709551616 micros"),
@r#"failed to parse "18446744073709551616 micros" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#,
@r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#,
);
// one fewer is okay
insta::assert_snapshot!(
@ -1379,7 +1333,7 @@ mod tests {
insta::assert_snapshot!(
p("1.5 nanos"),
@r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#,
@r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#,
);
}
@ -1391,19 +1345,19 @@ mod tests {
insta::assert_snapshot!(
p("05:"),
@r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###,
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#,
);
insta::assert_snapshot!(
p("05:06"),
@r###"failed to parse "05:06" in the "friendly" format: when parsing 'HH:MM:SS' format, expected to see a ':' after the parsed minute of 6"###,
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#,
);
insta::assert_snapshot!(
p("05:06:"),
@r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###,
@r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#,
);
insta::assert_snapshot!(
p("2 hours, 05:06:07"),
@r#"failed to parse "2 hours, 05:06:07" in the "friendly" format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
@r#"failed to parse input in the "friendly" duration format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
);
}
}

View file

@ -1,6 +1,6 @@
use crate::{
fmt::{
util::{DecimalFormatter, FractionalFormatter},
util::{FractionalFormatter, IntegerFormatter},
Write, WriteExt,
},
Error, SignedDuration, Span, Unit,
@ -1223,19 +1223,19 @@ impl SpanPrinter {
) -> Result<(), Error> {
let span = span.abs();
if span.get_years() != 0 {
wtr.write(Unit::Year, span.get_years().unsigned_abs())?;
wtr.write(Unit::Year, span.get_years().unsigned_abs().into())?;
}
if span.get_months() != 0 {
wtr.write(Unit::Month, span.get_months().unsigned_abs())?;
wtr.write(Unit::Month, span.get_months().unsigned_abs().into())?;
}
if span.get_weeks() != 0 {
wtr.write(Unit::Week, span.get_weeks().unsigned_abs())?;
wtr.write(Unit::Week, span.get_weeks().unsigned_abs().into())?;
}
if span.get_days() != 0 {
wtr.write(Unit::Day, span.get_days().unsigned_abs())?;
wtr.write(Unit::Day, span.get_days().unsigned_abs().into())?;
}
if span.get_hours() != 0 {
wtr.write(Unit::Hour, span.get_hours().unsigned_abs())?;
wtr.write(Unit::Hour, span.get_hours().unsigned_abs().into())?;
}
if span.get_minutes() != 0 {
wtr.write(Unit::Minute, span.get_minutes().unsigned_abs())?;
@ -1310,7 +1310,7 @@ impl SpanPrinter {
span_time = span_time.abs();
let fmtint =
DecimalFormatter::new().padding(self.padding.unwrap_or(2));
IntegerFormatter::new().padding(self.padding.unwrap_or(2));
let fmtfraction = FractionalFormatter::new().precision(self.precision);
wtr.wtr.write_int(&fmtint, span_time.get_hours_ranged().get())?;
wtr.wtr.write_str(":")?;
@ -1366,10 +1366,16 @@ impl SpanPrinter {
wtr.write(Unit::Minute, secs / SECS_PER_MIN)?;
wtr.write(Unit::Second, secs % SECS_PER_MIN)?;
let mut nanos = dur.subsec_nanos();
wtr.write(Unit::Millisecond, nanos / NANOS_PER_MILLI)?;
wtr.write(
Unit::Millisecond,
(nanos / NANOS_PER_MILLI).into(),
)?;
nanos %= NANOS_PER_MILLI;
wtr.write(Unit::Microsecond, nanos / NANOS_PER_MICRO)?;
wtr.write(Unit::Nanosecond, nanos % NANOS_PER_MICRO)?;
wtr.write(
Unit::Microsecond,
(nanos / NANOS_PER_MICRO).into(),
)?;
wtr.write(Unit::Nanosecond, (nanos % NANOS_PER_MICRO).into())?;
}
Some(FractionalUnit::Hour) => {
wtr.write_fractional_duration(FractionalUnit::Hour, &dur)?;
@ -1421,7 +1427,10 @@ impl SpanPrinter {
wtr.write(Unit::Minute, secs / SECS_PER_MIN)?;
wtr.write(Unit::Second, secs % SECS_PER_MIN)?;
let mut nanos = dur.subsec_nanos();
wtr.write(Unit::Millisecond, nanos / NANOS_PER_MILLI)?;
wtr.write(
Unit::Millisecond,
(nanos / NANOS_PER_MILLI).into(),
)?;
nanos %= NANOS_PER_MILLI;
let leftovers = core::time::Duration::new(0, nanos);
@ -1479,7 +1488,7 @@ impl SpanPrinter {
// bigger.
let fmtint =
DecimalFormatter::new().padding(self.padding.unwrap_or(2));
IntegerFormatter::new().padding(self.padding.unwrap_or(2));
let fmtfraction = FractionalFormatter::new().precision(self.precision);
let mut secs = udur.as_secs();
@ -1609,7 +1618,7 @@ struct DesignatorWriter<'p, 'w, W> {
wtr: &'w mut W,
desig: Designators,
sign: Option<DirectionSign>,
fmtint: DecimalFormatter,
fmtint: IntegerFormatter,
fmtfraction: FractionalFormatter,
written_non_zero_unit: bool,
}
@ -1624,7 +1633,7 @@ impl<'p, 'w, W: Write> DesignatorWriter<'p, 'w, W> {
let desig = Designators::new(printer.designator);
let sign = printer.direction.sign(printer, has_calendar, signum);
let fmtint =
DecimalFormatter::new().padding(printer.padding.unwrap_or(0));
IntegerFormatter::new().padding(printer.padding.unwrap_or(0));
let fmtfraction =
FractionalFormatter::new().precision(printer.precision);
DesignatorWriter {
@ -1670,12 +1679,7 @@ impl<'p, 'w, W: Write> DesignatorWriter<'p, 'w, W> {
Ok(())
}
fn write(
&mut self,
unit: Unit,
value: impl Into<u64>,
) -> Result<(), Error> {
let value = value.into();
fn write(&mut self, unit: Unit, value: u64) -> Result<(), Error> {
if value == 0 {
return Ok(());
}
@ -1729,7 +1733,7 @@ impl<'p, 'w, W: Write> DesignatorWriter<'p, 'w, W> {
struct FractionalPrinter {
integer: u64,
fraction: u32,
fmtint: DecimalFormatter,
fmtint: IntegerFormatter,
fmtfraction: FractionalFormatter,
}
@ -1746,7 +1750,7 @@ impl FractionalPrinter {
fn from_span(
span: &Span,
unit: FractionalUnit,
fmtint: DecimalFormatter,
fmtint: IntegerFormatter,
fmtfraction: FractionalFormatter,
) -> FractionalPrinter {
debug_assert!(span.largest_unit() <= Unit::from(unit));
@ -1758,7 +1762,7 @@ impl FractionalPrinter {
fn from_duration(
dur: &core::time::Duration,
unit: FractionalUnit,
fmtint: DecimalFormatter,
fmtint: IntegerFormatter,
fmtfraction: FractionalFormatter,
) -> FractionalPrinter {
match unit {

View file

@ -166,11 +166,11 @@ and features.)
*/
use crate::{
error::{err, Error},
error::{fmt::Error as E, Error},
util::escape,
};
use self::util::{Decimal, DecimalFormatter, Fractional, FractionalFormatter};
use self::util::{Fractional, FractionalFormatter, Integer, IntegerFormatter};
pub mod friendly;
mod offset;
@ -218,12 +218,7 @@ impl<'i, V: core::fmt::Display> Parsed<'i, V> {
if self.input.is_empty() {
return Ok(self.value);
}
Err(err!(
"parsed value '{value}', but unparsed input {unparsed:?} \
remains (expected no unparsed input)",
value = self.value,
unparsed = escape::Bytes(self.input),
))
Err(Error::from(E::into_full_error(&self.value, self.input)))
}
}
@ -244,12 +239,7 @@ impl<'i, V> Parsed<'i, V> {
if self.input.is_empty() {
return Ok(self.value);
}
Err(err!(
"parsed value '{value}', but unparsed input {unparsed:?} \
remains (expected no unparsed input)",
value = display,
unparsed = escape::Bytes(self.input),
))
Err(Error::from(E::into_full_error(&display, self.input)))
}
}
@ -334,6 +324,17 @@ impl<W: Write> Write for &mut W {
}
}
impl Write for &mut dyn Write {
fn write_str(&mut self, string: &str) -> Result<(), Error> {
(**self).write_str(string)
}
#[inline]
fn write_char(&mut self, char: char) -> Result<(), Error> {
(**self).write_char(char)
}
}
/// An adapter for using `std::io::Write` implementations with `fmt::Write`.
///
/// This is useful when one wants to format a datetime or span directly
@ -368,7 +369,7 @@ pub struct StdIoWrite<W>(pub W);
impl<W: std::io::Write> Write for StdIoWrite<W> {
#[inline]
fn write_str(&mut self, string: &str) -> Result<(), Error> {
self.0.write_all(string.as_bytes()).map_err(Error::adhoc)
self.0.write_all(string.as_bytes()).map_err(Error::io)
}
}
@ -411,7 +412,7 @@ impl<W: core::fmt::Write> Write for StdFmtWrite<W> {
fn write_str(&mut self, string: &str) -> Result<(), Error> {
self.0
.write_str(string)
.map_err(|_| err!("an error occurred when formatting an argument"))
.map_err(|_| Error::from(E::StdFmtWriteAdapter))
}
}
@ -433,7 +434,7 @@ trait WriteExt: Write {
#[inline]
fn write_int(
&mut self,
formatter: &DecimalFormatter,
formatter: &IntegerFormatter,
n: impl Into<i64>,
) -> Result<(), Error> {
self.write_decimal(&formatter.format_signed(n.into()))
@ -444,7 +445,7 @@ trait WriteExt: Write {
#[inline]
fn write_uint(
&mut self,
formatter: &DecimalFormatter,
formatter: &IntegerFormatter,
n: impl Into<u64>,
) -> Result<(), Error> {
self.write_decimal(&formatter.format_unsigned(n.into()))
@ -463,7 +464,7 @@ trait WriteExt: Write {
/// Write the given decimal number to this buffer.
#[inline]
fn write_decimal(&mut self, decimal: &Decimal) -> Result<(), Error> {
fn write_decimal(&mut self, decimal: &Integer) -> Result<(), Error> {
self.write_str(decimal.as_str())
}

View file

@ -102,7 +102,7 @@ from [Temporal's hybrid grammar].
// support a span of time of about 52 hours or so.)
use crate::{
error::{err, Error, ErrorContext},
error::{fmt::offset::Error as E, Error, ErrorContext},
fmt::{
temporal::{PiecesNumericOffset, PiecesOffset},
util::{parse_temporal_fraction, FractionalFormatter},
@ -110,7 +110,7 @@ use crate::{
},
tz::Offset,
util::{
escape, parse,
parse,
rangeint::{ri8, RFrom},
t::{self, C},
},
@ -237,13 +237,7 @@ impl Numeric {
if part_nanoseconds >= C(500_000_000) {
seconds = seconds
.try_checked_add("offset-seconds", C(1))
.with_context(|| {
err!(
"due to precision loss, UTC offset '{}' is \
rounded to a value that is out of bounds",
self,
)
})?;
.context(E::PrecisionLoss)?;
}
}
Ok(Offset::from_seconds_ranged(seconds * self.sign))
@ -254,11 +248,7 @@ impl Numeric {
// `Offset` fails.
impl core::fmt::Display for Numeric {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
if self.sign == C(-1) {
write!(f, "-")?;
} else {
write!(f, "+")?;
}
f.write_str(if self.sign == C(-1) { "-" } else { "+" })?;
write!(f, "{:02}", self.hours)?;
if let Some(minutes) = self.minutes {
write!(f, ":{:02}", minutes)?;
@ -268,11 +258,8 @@ impl core::fmt::Display for Numeric {
}
if let Some(nanos) = self.nanoseconds {
static FMT: FractionalFormatter = FractionalFormatter::new();
write!(
f,
".{}",
FMT.format(i32::from(nanos).unsigned_abs()).as_str()
)?;
f.write_str(".")?;
f.write_str(FMT.format(i32::from(nanos).unsigned_abs()).as_str())?;
}
Ok(())
}
@ -413,18 +400,14 @@ impl Parser {
mut input: &'i [u8],
) -> Result<Parsed<'i, ParsedOffset>, Error> {
if input.is_empty() {
return Err(err!("expected UTC offset, but found end of input"));
return Err(Error::from(E::EndOfInput));
}
if input[0] == b'Z' || input[0] == b'z' {
if !self.zulu {
return Err(err!(
"found {z:?} in {original:?} where a numeric UTC offset \
was expected (this context does not permit \
the Zulu offset)",
z = escape::Byte(input[0]),
original = escape::Bytes(input),
));
return Err(Error::from(E::UnexpectedLetterOffsetNoZulu(
input[0],
)));
}
input = &input[1..];
let value = ParsedOffset { kind: ParsedOffsetKind::Zulu };
@ -464,40 +447,24 @@ impl Parser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, Numeric>, Error> {
let original = escape::Bytes(input);
// Parse sign component.
let Parsed { value: sign, input } =
self.parse_sign(input).with_context(|| {
err!("failed to parse sign in UTC numeric offset {original:?}")
})?;
self.parse_sign(input).context(E::InvalidSign)?;
// Parse hours component.
let Parsed { value: hours, input } =
self.parse_hours(input).with_context(|| {
err!(
"failed to parse hours in UTC numeric offset {original:?}"
)
})?;
self.parse_hours(input).context(E::InvalidHours)?;
let extended = match self.colon {
Colon::Optional => input.starts_with(b":"),
Colon::Required => {
if !input.is_empty() && !input.starts_with(b":") {
return Err(err!(
"parsed hour component of time zone offset from \
{original:?}, but could not find required colon \
separator",
));
return Err(Error::from(E::NoColonAfterHours));
}
true
}
Colon::Absent => {
if !input.is_empty() && input.starts_with(b":") {
return Err(err!(
"parsed hour component of time zone offset from \
{original:?}, but found colon after hours which \
is not allowed",
));
return Err(Error::from(E::ColonAfterHours));
}
false
}
@ -513,32 +480,22 @@ impl Parser {
};
// Parse optional separator after hours.
let Parsed { value: has_minutes, input } =
self.parse_separator(input, extended).with_context(|| {
err!(
"failed to parse separator after hours in \
UTC numeric offset {original:?}"
)
})?;
let Parsed { value: has_minutes, input } = self
.parse_separator(input, extended)
.context(E::SeparatorAfterHours)?;
if !has_minutes {
if self.require_minute || (self.subminute && self.require_second) {
return Err(err!(
"parsed hour component of time zone offset from \
{original:?}, but could not find required minute \
component",
));
}
return Ok(Parsed { value: numeric, input });
return if self.require_minute
|| (self.subminute && self.require_second)
{
Err(Error::from(E::MissingMinuteAfterHour))
} else {
Ok(Parsed { value: numeric, input })
};
}
// Parse minutes component.
let Parsed { value: minutes, input } =
self.parse_minutes(input).with_context(|| {
err!(
"failed to parse minutes in UTC numeric offset \
{original:?}"
)
})?;
self.parse_minutes(input).context(E::InvalidMinutes)?;
numeric.minutes = Some(minutes);
// If subminute resolution is not supported, then we're done here.
@ -549,65 +506,42 @@ impl Parser {
// more precision than is supported. So we return an error here.
// If this winds up being problematic, we can make this error
// configurable or remove it altogether (unfortunate).
if input.get(0).map_or(false, |&b| b == b':') {
return Err(err!(
"subminute precision for UTC numeric offset {original:?} \
is not enabled in this context (must provide only \
integral minutes)",
));
}
return Ok(Parsed { value: numeric, input });
return if input.get(0).map_or(false, |&b| b == b':') {
Err(Error::from(E::SubminutePrecisionNotEnabled))
} else {
Ok(Parsed { value: numeric, input })
};
}
// Parse optional separator after minutes.
let Parsed { value: has_seconds, input } =
self.parse_separator(input, extended).with_context(|| {
err!(
"failed to parse separator after minutes in \
UTC numeric offset {original:?}"
)
})?;
let Parsed { value: has_seconds, input } = self
.parse_separator(input, extended)
.context(E::SeparatorAfterMinutes)?;
if !has_seconds {
if self.require_second {
return Err(err!(
"parsed hour and minute components of time zone offset \
from {original:?}, but could not find required second \
component",
));
}
return Ok(Parsed { value: numeric, input });
return if self.require_second {
Err(Error::from(E::MissingSecondAfterMinute))
} else {
Ok(Parsed { value: numeric, input })
};
}
// Parse seconds component.
let Parsed { value: seconds, input } =
self.parse_seconds(input).with_context(|| {
err!(
"failed to parse seconds in UTC numeric offset \
{original:?}"
)
})?;
self.parse_seconds(input).context(E::InvalidSeconds)?;
numeric.seconds = Some(seconds);
// If subsecond resolution is not supported, then we're done here.
if !self.subsecond {
if input.get(0).map_or(false, |&b| b == b'.' || b == b',') {
return Err(err!(
"subsecond precision for UTC numeric offset {original:?} \
is not enabled in this context (must provide only \
integral minutes or seconds)",
));
return Err(Error::from(E::SubsecondPrecisionNotEnabled));
}
return Ok(Parsed { value: numeric, input });
}
// Parse an optional fractional component.
let Parsed { value: nanoseconds, input } =
parse_temporal_fraction(input).with_context(|| {
err!(
"failed to parse fractional nanoseconds in \
UTC numeric offset {original:?}",
)
})?;
parse_temporal_fraction(input)
.context(E::InvalidSecondsFractional)?;
// OK because `parse_temporal_fraction` guarantees `0..=999_999_999`.
numeric.nanoseconds =
nanoseconds.map(|n| t::SubsecNanosecond::new(n).unwrap());
@ -619,19 +553,13 @@ impl Parser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, t::Sign>, Error> {
let sign = input.get(0).copied().ok_or_else(|| {
err!("expected UTC numeric offset, but found end of input")
})?;
let sign = input.get(0).copied().ok_or(E::EndOfInputNumeric)?;
let sign = if sign == b'+' {
t::Sign::N::<1>()
} else if sign == b'-' {
t::Sign::N::<-1>()
} else {
return Err(err!(
"expected '+' or '-' sign at start of UTC numeric offset, \
but found {found:?} instead",
found = escape::Byte(sign),
));
return Err(Error::from(E::InvalidSignPlusOrMinus));
};
Ok(Parsed { value: sign, input: &input[1..] })
}
@ -641,22 +569,16 @@ impl Parser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ParsedOffsetHours>, Error> {
let (hours, input) = parse::split(input, 2).ok_or_else(|| {
err!("expected two digit hour after sign, but found end of input",)
})?;
let hours = parse::i64(hours).with_context(|| {
err!(
"failed to parse {hours:?} as hours (a two digit integer)",
hours = escape::Bytes(hours),
)
})?;
let (hours, input) =
parse::split(input, 2).ok_or(E::EndOfInputHour)?;
let hours = parse::i64(hours).context(E::ParseHours)?;
// Note that we support a slightly bigger range of offsets than
// Temporal. Temporal seems to support only up to 23 hours, but
// we go up to 25 hours. This is done to support POSIX time zone
// strings, which also require 25 hours (plus the maximal minute/second
// components).
let hours = ParsedOffsetHours::try_new("hours", hours)
.context("offset hours are not valid")?;
.context(E::RangeHours)?;
Ok(Parsed { value: hours, input })
}
@ -665,20 +587,11 @@ impl Parser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ParsedOffsetMinutes>, Error> {
let (minutes, input) = parse::split(input, 2).ok_or_else(|| {
err!(
"expected two digit minute after hours, \
but found end of input",
)
})?;
let minutes = parse::i64(minutes).with_context(|| {
err!(
"failed to parse {minutes:?} as minutes (a two digit integer)",
minutes = escape::Bytes(minutes),
)
})?;
let (minutes, input) =
parse::split(input, 2).ok_or(E::EndOfInputMinute)?;
let minutes = parse::i64(minutes).context(E::ParseMinutes)?;
let minutes = ParsedOffsetMinutes::try_new("minutes", minutes)
.context("minutes are not valid")?;
.context(E::RangeMinutes)?;
Ok(Parsed { value: minutes, input })
}
@ -687,20 +600,11 @@ impl Parser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ParsedOffsetSeconds>, Error> {
let (seconds, input) = parse::split(input, 2).ok_or_else(|| {
err!(
"expected two digit second after hours, \
but found end of input",
)
})?;
let seconds = parse::i64(seconds).with_context(|| {
err!(
"failed to parse {seconds:?} as seconds (a two digit integer)",
seconds = escape::Bytes(seconds),
)
})?;
let (seconds, input) =
parse::split(input, 2).ok_or(E::EndOfInputSecond)?;
let seconds = parse::i64(seconds).context(E::ParseSeconds)?;
let seconds = ParsedOffsetSeconds::try_new("seconds", seconds)
.context("time zone offset seconds are not valid")?;
.context(E::RangeSeconds)?;
Ok(Parsed { value: seconds, input })
}
@ -941,7 +845,7 @@ mod tests {
fn err_numeric_empty() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"").unwrap_err(),
@r###"failed to parse sign in UTC numeric offset "": expected UTC numeric offset, but found end of input"###,
@"failed to parse sign in UTC numeric offset: expected UTC numeric offset, but found end of input",
);
}
@ -950,7 +854,7 @@ mod tests {
fn err_numeric_notsign() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"*").unwrap_err(),
@r###"failed to parse sign in UTC numeric offset "*": expected '+' or '-' sign at start of UTC numeric offset, but found "*" instead"###,
@"failed to parse sign in UTC numeric offset: expected `+` or `-` sign at start of UTC numeric offset",
);
}
@ -959,7 +863,7 @@ mod tests {
fn err_numeric_hours_too_short() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"+a").unwrap_err(),
@r###"failed to parse hours in UTC numeric offset "+a": expected two digit hour after sign, but found end of input"###,
@"failed to parse hours in UTC numeric offset: expected two digit hour after sign, but found end of input",
);
}
@ -968,7 +872,7 @@ mod tests {
fn err_numeric_hours_invalid_digits() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"+ab").unwrap_err(),
@r###"failed to parse hours in UTC numeric offset "+ab": failed to parse "ab" as hours (a two digit integer): invalid digit, expected 0-9 but got a"###,
@"failed to parse hours in UTC numeric offset: failed to parse hours (requires a two digit integer): invalid digit, expected 0-9 but got a",
);
}
@ -977,7 +881,7 @@ mod tests {
fn err_numeric_hours_out_of_range() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-26").unwrap_err(),
@r###"failed to parse hours in UTC numeric offset "-26": offset hours are not valid: parameter 'hours' with value 26 is not in the required range of 0..=25"###,
@"failed to parse hours in UTC numeric offset: hour in time zone offset is out of range: parameter 'hours' with value 26 is not in the required range of 0..=25",
);
}
@ -986,7 +890,7 @@ mod tests {
fn err_numeric_minutes_too_short() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"+05:a").unwrap_err(),
@r###"failed to parse minutes in UTC numeric offset "+05:a": expected two digit minute after hours, but found end of input"###,
@"failed to parse minutes in UTC numeric offset: expected two digit minute after hours, but found end of input",
);
}
@ -995,7 +899,7 @@ mod tests {
fn err_numeric_minutes_invalid_digits() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"+05:ab").unwrap_err(),
@r###"failed to parse minutes in UTC numeric offset "+05:ab": failed to parse "ab" as minutes (a two digit integer): invalid digit, expected 0-9 but got a"###,
@"failed to parse minutes in UTC numeric offset: failed to parse minutes (requires a two digit integer): invalid digit, expected 0-9 but got a",
);
}
@ -1004,7 +908,7 @@ mod tests {
fn err_numeric_minutes_out_of_range() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-05:60").unwrap_err(),
@r###"failed to parse minutes in UTC numeric offset "-05:60": minutes are not valid: parameter 'minutes' with value 60 is not in the required range of 0..=59"###,
@"failed to parse minutes in UTC numeric offset: minute in time zone offset is out of range: parameter 'minutes' with value 60 is not in the required range of 0..=59",
);
}
@ -1013,7 +917,7 @@ mod tests {
fn err_numeric_seconds_too_short() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"+05:30:a").unwrap_err(),
@r###"failed to parse seconds in UTC numeric offset "+05:30:a": expected two digit second after hours, but found end of input"###,
@"failed to parse seconds in UTC numeric offset: expected two digit second after minutes, but found end of input",
);
}
@ -1022,7 +926,7 @@ mod tests {
fn err_numeric_seconds_invalid_digits() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"+05:30:ab").unwrap_err(),
@r###"failed to parse seconds in UTC numeric offset "+05:30:ab": failed to parse "ab" as seconds (a two digit integer): invalid digit, expected 0-9 but got a"###,
@"failed to parse seconds in UTC numeric offset: failed to parse seconds (requires a two digit integer): invalid digit, expected 0-9 but got a",
);
}
@ -1031,7 +935,7 @@ mod tests {
fn err_numeric_seconds_out_of_range() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-05:30:60").unwrap_err(),
@r###"failed to parse seconds in UTC numeric offset "-05:30:60": time zone offset seconds are not valid: parameter 'seconds' with value 60 is not in the required range of 0..=59"###,
@"failed to parse seconds in UTC numeric offset: second in time zone offset is out of range: parameter 'seconds' with value 60 is not in the required range of 0..=59",
);
}
@ -1041,31 +945,31 @@ mod tests {
fn err_numeric_fraction_non_empty() {
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-05:30:44.").unwrap_err(),
@r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44.": found decimal after seconds component, but did not find any decimal digits after decimal"###,
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
);
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-05:30:44,").unwrap_err(),
@r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44,": found decimal after seconds component, but did not find any decimal digits after decimal"###,
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
);
// Instead of end-of-string, add invalid digit.
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-05:30:44.a").unwrap_err(),
@r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44.a": found decimal after seconds component, but did not find any decimal digits after decimal"###,
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
);
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-05:30:44,a").unwrap_err(),
@r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44,a": found decimal after seconds component, but did not find any decimal digits after decimal"###,
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
);
// And also test basic format.
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-053044.a").unwrap_err(),
@r###"failed to parse fractional nanoseconds in UTC numeric offset "-053044.a": found decimal after seconds component, but did not find any decimal digits after decimal"###,
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
);
insta::assert_snapshot!(
Parser::new().parse_numeric(b"-053044,a").unwrap_err(),
@r###"failed to parse fractional nanoseconds in UTC numeric offset "-053044,a": found decimal after seconds component, but did not find any decimal digits after decimal"###,
@"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal",
);
}
@ -1076,7 +980,7 @@ mod tests {
fn err_numeric_subminute_disabled_but_desired() {
insta::assert_snapshot!(
Parser::new().subminute(false).parse_numeric(b"-05:59:32").unwrap_err(),
@r###"subminute precision for UTC numeric offset "-05:59:32" is not enabled in this context (must provide only integral minutes)"###,
@"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)",
);
}
@ -1086,11 +990,11 @@ mod tests {
fn err_zulu_disabled_but_desired() {
insta::assert_snapshot!(
Parser::new().zulu(false).parse(b"Z").unwrap_err(),
@r###"found "Z" in "Z" where a numeric UTC offset was expected (this context does not permit the Zulu offset)"###,
@"found `Z` where a numeric UTC offset was expected (this context does not permit the Zulu offset)",
);
insta::assert_snapshot!(
Parser::new().zulu(false).parse(b"z").unwrap_err(),
@r###"found "z" in "z" where a numeric UTC offset was expected (this context does not permit the Zulu offset)"###,
@"found `z` where a numeric UTC offset was expected (this context does not permit the Zulu offset)",
);
}
@ -1118,7 +1022,7 @@ mod tests {
};
insta::assert_snapshot!(
numeric.to_offset().unwrap_err(),
@"due to precision loss, UTC offset '+25:59:59.5' is rounded to a value that is out of bounds: parameter 'offset-seconds' with value 1 is not in the required range of -93599..=93599",
@"due to precision loss, offset is rounded to a value that is out of bounds: parameter 'offset-seconds' with value 1 is not in the required range of -93599..=93599",
);
}
@ -1143,7 +1047,7 @@ mod tests {
};
insta::assert_snapshot!(
numeric.to_offset().unwrap_err(),
@"due to precision loss, UTC offset '-25:59:59.5' is rounded to a value that is out of bounds: parameter 'offset-seconds' with value 1 is not in the required range of -93599..=93599",
@"due to precision loss, offset is rounded to a value that is out of bounds: parameter 'offset-seconds' with value 1 is not in the required range of -93599..=93599",
);
}
}

View file

@ -43,11 +43,11 @@ general interchange format for new applications.
use crate::{
civil::{Date, DateTime, Time, Weekday},
error::{err, ErrorContext},
fmt::{util::DecimalFormatter, Parsed, Write, WriteExt},
error::{fmt::rfc2822::Error as E, ErrorContext},
fmt::{util::IntegerFormatter, Parsed, Write, WriteExt},
tz::{Offset, TimeZone},
util::{
escape, parse,
parse,
rangeint::{ri8, RFrom},
t::{self, C},
},
@ -313,9 +313,7 @@ impl DateTimeParser {
let input = input.as_ref();
let zdt = self
.parse_zoned_internal(input)
.context(
"failed to parse RFC 2822 datetime into Jiff zoned datetime",
)?
.context(E::FailedZoned)?
.into_full()?;
Ok(zdt)
}
@ -351,7 +349,7 @@ impl DateTimeParser {
let input = input.as_ref();
let ts = self
.parse_timestamp_internal(input)
.context("failed to parse RFC 2822 datetime into Jiff timestamp")?
.context(E::FailedTimestamp)?
.into_full()?;
Ok(ts)
}
@ -367,9 +365,7 @@ impl DateTimeParser {
) -> Result<Parsed<'i, Zoned>, Error> {
let Parsed { value: (dt, offset), input } =
self.parse_datetime_offset(input)?;
let ts = offset
.to_timestamp(dt)
.context("RFC 2822 datetime out of Jiff's range")?;
let ts = offset.to_timestamp(dt)?;
let zdt = ts.to_zoned(TimeZone::fixed(offset));
Ok(Parsed { value: zdt, input })
}
@ -385,9 +381,7 @@ impl DateTimeParser {
) -> Result<Parsed<'i, Timestamp>, Error> {
let Parsed { value: (dt, offset), input } =
self.parse_datetime_offset(input)?;
let ts = offset
.to_timestamp(dt)
.context("RFC 2822 datetime out of Jiff's range")?;
let ts = offset.to_timestamp(dt)?;
Ok(Parsed { value: ts, input })
}
@ -425,16 +419,11 @@ impl DateTimeParser {
input: &'i [u8],
) -> Result<Parsed<'i, DateTime>, Error> {
if input.is_empty() {
return Err(err!(
"expected RFC 2822 datetime, but got empty string"
));
return Err(Error::from(E::Empty));
}
let Parsed { input, .. } = self.skip_whitespace(input);
if input.is_empty() {
return Err(err!(
"expected RFC 2822 datetime, but got empty string after \
trimming whitespace",
));
return Err(Error::from(E::EmptyAfterWhitespace));
}
let Parsed { value: wd, input } = self.parse_weekday(input)?;
let Parsed { value: day, input } = self.parse_day(input)?;
@ -451,26 +440,19 @@ impl DateTimeParser {
self.skip_whitespace(input);
let (second, input) = if !input.starts_with(b":") {
if !whitespace_after_minute {
return Err(err!(
"expected whitespace after parsing time: \
expected at least one whitespace character \
(space or tab), but found none",
));
return Err(Error::from(E::WhitespaceAfterTime));
}
(t::Second::N::<0>(), input)
} else {
let Parsed { input, .. } = self.parse_time_separator(input)?;
let Parsed { input, .. } = self.skip_whitespace(input);
let Parsed { value: second, input } = self.parse_second(input)?;
let Parsed { input, .. } =
self.parse_whitespace(input).with_context(|| {
err!("expected whitespace after parsing time")
})?;
let Parsed { input, .. } = self.parse_whitespace(input)?;
(second, input)
};
let date =
Date::new_ranged(year, month, day).context("invalid date")?;
Date::new_ranged(year, month, day).context(E::InvalidDate)?;
let time = Time::new_ranged(
hour,
minute,
@ -480,13 +462,10 @@ impl DateTimeParser {
let dt = DateTime::from_parts(date, time);
if let Some(wd) = wd {
if !self.relaxed_weekday && wd != dt.weekday() {
return Err(err!(
"found parsed weekday of {parsed}, \
but parsed datetime of {dt} has weekday \
{has}",
parsed = weekday_abbrev(wd),
has = weekday_abbrev(dt.weekday()),
));
return Err(Error::from(E::InconsistentWeekday {
parsed: wd,
from_date: dt.weekday(),
}));
}
}
Ok(Parsed { value: dt, input })
@ -517,15 +496,13 @@ impl DateTimeParser {
if matches!(input[0], b'0'..=b'9') {
return Ok(Parsed { value: None, input });
}
if input.len() < 4 {
return Err(err!(
"expected day at beginning of RFC 2822 datetime \
since first non-whitespace byte, {first:?}, \
is not a digit, but given string is too short \
(length is {length})",
first = escape::Byte(input[0]),
length = input.len(),
));
if let Ok(len) = u8::try_from(input.len()) {
if len < 4 {
return Err(Error::from(E::TooShortWeekday {
got_non_digit: input[0],
len,
}));
}
}
let b1 = input[0];
let b2 = input[1];
@ -543,31 +520,19 @@ impl DateTimeParser {
b"fri" => Weekday::Friday,
b"sat" => Weekday::Saturday,
_ => {
return Err(err!(
"expected day at beginning of RFC 2822 datetime \
since first non-whitespace byte, {first:?}, \
is not a digit, but did not recognize {got:?} \
as a valid weekday abbreviation",
first = escape::Byte(input[0]),
got = escape::Bytes(&input[..3]),
));
return Err(Error::from(E::InvalidWeekday {
got_non_digit: input[0],
}));
}
};
let Parsed { input, .. } = self.skip_whitespace(&input[3..]);
let Some(should_be_comma) = input.get(0).copied() else {
return Err(err!(
"expected comma after parsed weekday `{weekday}` in \
RFC 2822 datetime, but found end of string instead",
weekday = escape::Bytes(&[b1, b2, b3]),
));
return Err(Error::from(E::EndOfInputComma));
};
if should_be_comma != b',' {
return Err(err!(
"expected comma after parsed weekday `{weekday}` in \
RFC 2822 datetime, but found `{got:?}` instead",
weekday = escape::Bytes(&[b1, b2, b3]),
got = escape::Byte(should_be_comma),
));
return Err(Error::from(E::UnexpectedByteComma {
byte: should_be_comma,
}));
}
let Parsed { input, .. } = self.skip_whitespace(&input[1..]);
Ok(Parsed { value: Some(wd), input })
@ -586,21 +551,17 @@ impl DateTimeParser {
input: &'i [u8],
) -> Result<Parsed<'i, t::Day>, Error> {
if input.is_empty() {
return Err(err!("expected day, but found end of input"));
return Err(Error::from(E::EndOfInputDay));
}
let mut digits = 1;
if input.len() >= 2 && matches!(input[1], b'0'..=b'9') {
digits = 2;
}
let (day, input) = input.split_at(digits);
let day = parse::i64(day).with_context(|| {
err!("failed to parse {day:?} as day", day = escape::Bytes(day))
})?;
let day = t::Day::try_new("day", day).context("day is not valid")?;
let day = parse::i64(day).context(E::ParseDay)?;
let day = t::Day::try_new("day", day).context(E::ParseDay)?;
let Parsed { input, .. } =
self.parse_whitespace(input).with_context(|| {
err!("expected whitespace after parsing day {day}")
})?;
self.parse_whitespace(input).context(E::WhitespaceAfterDay)?;
Ok(Parsed { value: day, input })
}
@ -617,16 +578,12 @@ impl DateTimeParser {
input: &'i [u8],
) -> Result<Parsed<'i, t::Month>, Error> {
if input.is_empty() {
return Err(err!(
"expected abbreviated month name, but found end of input"
));
return Err(Error::from(E::EndOfInputMonth));
}
if input.len() < 3 {
return Err(err!(
"expected abbreviated month name, but remaining input \
is too short (remaining bytes is {length})",
length = input.len(),
));
if let Ok(len) = u8::try_from(input.len()) {
if len < 3 {
return Err(Error::from(E::TooShortMonth { len }));
}
}
let b1 = input[0].to_ascii_lowercase();
let b2 = input[1].to_ascii_lowercase();
@ -644,22 +601,14 @@ impl DateTimeParser {
b"oct" => 10,
b"nov" => 11,
b"dec" => 12,
_ => {
return Err(err!(
"expected abbreviated month name, \
but did not recognize {got:?} \
as a valid month",
got = escape::Bytes(&input[..3]),
));
}
_ => return Err(Error::from(E::InvalidMonth)),
};
// OK because we just assigned a numeric value ourselves
// above, and all values are valid months.
let month = t::Month::new(month).unwrap();
let Parsed { input, .. } =
self.parse_whitespace(&input[3..]).with_context(|| {
err!("expected whitespace after parsing month name")
})?;
let Parsed { input, .. } = self
.parse_whitespace(&input[3..])
.context(E::WhitespaceAfterMonth)?;
Ok(Parsed { value: month, input })
}
@ -692,31 +641,22 @@ impl DateTimeParser {
{
digits += 1;
}
if digits <= 1 {
return Err(err!(
"expected at least two ASCII digits for parsing \
a year, but only found {digits}",
));
if let Ok(len) = u8::try_from(digits) {
if len <= 1 {
return Err(Error::from(E::TooShortYear { len }));
}
}
let (year, input) = input.split_at(digits);
let year = parse::i64(year).with_context(|| {
err!(
"failed to parse {year:?} as year \
(a two, three or four digit integer)",
year = escape::Bytes(year),
)
})?;
let year = parse::i64(year).context(E::ParseYear)?;
let year = match digits {
2 if year <= 49 => year + 2000,
2 | 3 => year + 1900,
4 => year,
_ => unreachable!("digits={digits} must be 2, 3 or 4"),
};
let year =
t::Year::try_new("year", year).context("year is not valid")?;
let Parsed { input, .. } = self
.parse_whitespace(input)
.with_context(|| err!("expected whitespace after parsing year"))?;
let year = t::Year::try_new("year", year).context(E::InvalidYear)?;
let Parsed { input, .. } =
self.parse_whitespace(input).context(E::WhitespaceAfterYear)?;
Ok(Parsed { value: year, input })
}
@ -730,17 +670,9 @@ impl DateTimeParser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, t::Hour>, Error> {
let (hour, input) = parse::split(input, 2).ok_or_else(|| {
err!("expected two digit hour, but found end of input")
})?;
let hour = parse::i64(hour).with_context(|| {
err!(
"failed to parse {hour:?} as hour (a two digit integer)",
hour = escape::Bytes(hour),
)
})?;
let hour =
t::Hour::try_new("hour", hour).context("hour is not valid")?;
let (hour, input) = parse::split(input, 2).ok_or(E::EndOfInputHour)?;
let hour = parse::i64(hour).context(E::ParseHour)?;
let hour = t::Hour::try_new("hour", hour).context(E::InvalidHour)?;
Ok(Parsed { value: hour, input })
}
@ -751,17 +683,11 @@ impl DateTimeParser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, t::Minute>, Error> {
let (minute, input) = parse::split(input, 2).ok_or_else(|| {
err!("expected two digit minute, but found end of input")
})?;
let minute = parse::i64(minute).with_context(|| {
err!(
"failed to parse {minute:?} as minute (a two digit integer)",
minute = escape::Bytes(minute),
)
})?;
let minute = t::Minute::try_new("minute", minute)
.context("minute is not valid")?;
let (minute, input) =
parse::split(input, 2).ok_or(E::EndOfInputMinute)?;
let minute = parse::i64(minute).context(E::ParseMinute)?;
let minute =
t::Minute::try_new("minute", minute).context(E::InvalidMinute)?;
Ok(Parsed { value: minute, input })
}
@ -772,20 +698,14 @@ impl DateTimeParser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, t::Second>, Error> {
let (second, input) = parse::split(input, 2).ok_or_else(|| {
err!("expected two digit second, but found end of input")
})?;
let mut second = parse::i64(second).with_context(|| {
err!(
"failed to parse {second:?} as second (a two digit integer)",
second = escape::Bytes(second),
)
})?;
let (second, input) =
parse::split(input, 2).ok_or(E::EndOfInputSecond)?;
let mut second = parse::i64(second).context(E::ParseSecond)?;
if second == 60 {
second = 59;
}
let second = t::Second::try_new("second", second)
.context("second is not valid")?;
let second =
t::Second::try_new("second", second).context(E::InvalidSecond)?;
Ok(Parsed { value: second, input })
}
@ -801,13 +721,7 @@ impl DateTimeParser {
type ParsedOffsetHours = ri8<0, { t::SpanZoneOffsetHours::MAX }>;
type ParsedOffsetMinutes = ri8<0, { t::SpanZoneOffsetMinutes::MAX }>;
let sign = input.get(0).copied().ok_or_else(|| {
err!(
"expected sign for time zone offset, \
(or a legacy time zone name abbreviation), \
but found end of input",
)
})?;
let sign = input.get(0).copied().ok_or(E::EndOfInputOffset)?;
let sign = if sign == b'+' {
t::Sign::N::<1>()
} else if sign == b'-' {
@ -816,32 +730,16 @@ impl DateTimeParser {
return self.parse_offset_obsolete(input);
};
let input = &input[1..];
let (hhmm, input) = parse::split(input, 4).ok_or_else(|| {
err!(
"expected at least 4 digits for time zone offset \
after sign, but found only {len} bytes remaining",
len = input.len(),
)
})?;
let (hhmm, input) = parse::split(input, 4).ok_or(E::TooShortOffset)?;
let hh = parse::i64(&hhmm[0..2]).with_context(|| {
err!(
"failed to parse hours from time zone offset {hhmm}",
hhmm = escape::Bytes(hhmm)
)
})?;
let hh = parse::i64(&hhmm[0..2]).context(E::ParseOffsetHour)?;
let hh = ParsedOffsetHours::try_new("zone-offset-hours", hh)
.context("time zone offset hours are not valid")?;
.context(E::InvalidOffsetHour)?;
let hh = t::SpanZoneOffset::rfrom(hh);
let mm = parse::i64(&hhmm[2..4]).with_context(|| {
err!(
"failed to parse minutes from time zone offset {hhmm}",
hhmm = escape::Bytes(hhmm)
)
})?;
let mm = parse::i64(&hhmm[2..4]).context(E::ParseOffsetMinute)?;
let mm = ParsedOffsetMinutes::try_new("zone-offset-minutes", mm)
.context("time zone offset minutes are not valid")?;
.context(E::InvalidOffsetMinute)?;
let mm = t::SpanZoneOffset::rfrom(mm);
let seconds = hh * C(3_600) + mm * C(60);
@ -865,11 +763,7 @@ impl DateTimeParser {
len += 1;
}
if len == 0 {
return Err(err!(
"expected obsolete RFC 2822 time zone abbreviation, \
but found no remaining non-whitespace characters \
after time",
));
return Err(Error::from(E::WhitespaceAfterTimeForObsoleteOffset));
}
let offset = match &letters[..len] {
b"ut" | b"gmt" | b"z" => Offset::UTC,
@ -917,11 +811,7 @@ impl DateTimeParser {
Offset::UTC
} else {
// But anything else we throw our hands up I guess.
return Err(err!(
"expected obsolete RFC 2822 time zone abbreviation, \
but found {found:?}",
found = escape::Bytes(&input[..len]),
));
return Err(Error::from(E::InvalidObsoleteOffset));
}
}
};
@ -936,15 +826,12 @@ impl DateTimeParser {
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
if input.is_empty() {
return Err(err!(
"expected time separator of ':', but found end of input",
));
return Err(Error::from(E::EndOfInputTimeSeparator));
}
if input[0] != b':' {
return Err(err!(
"expected time separator of ':', but found {got}",
got = escape::Byte(input[0]),
));
return Err(Error::from(E::UnexpectedByteTimeSeparator {
byte: input[0],
}));
}
Ok(Parsed { value: (), input: &input[1..] })
}
@ -959,10 +846,7 @@ impl DateTimeParser {
let Parsed { input, value: had_whitespace } =
self.skip_whitespace(input);
if !had_whitespace {
return Err(err!(
"expected at least one whitespace character (space or tab), \
but found none",
));
return Err(Error::from(E::WhitespaceAfterTime));
}
Ok(Parsed { value: (), input })
}
@ -1012,26 +896,20 @@ impl DateTimeParser {
// I believe this error case is actually impossible, since as
// soon as we hit 0, we break out. If there is more "comment,"
// then it will flag an error as unparsed input.
depth = depth.checked_sub(1).ok_or_else(|| {
err!(
"found closing parenthesis in comment with \
no matching opening parenthesis"
)
})?;
depth = depth
.checked_sub(1)
.ok_or(E::CommentClosingParenWithoutOpen)?;
if depth == 0 {
break;
}
} else if byte == b'(' {
depth = depth.checked_add(1).ok_or_else(|| {
err!("found too many nested parenthesis in comment")
})?;
depth = depth
.checked_add(1)
.ok_or(E::CommentTooManyNestedParens)?;
}
}
if depth > 0 {
return Err(err!(
"found opening parenthesis in comment with \
no matching closing parenthesis"
));
return Err(Error::from(E::CommentOpeningParenWithoutClose));
}
let Parsed { input, .. } = self.skip_whitespace(input);
Ok(Parsed { value: (), input })
@ -1414,19 +1292,16 @@ impl DateTimePrinter {
offset: Option<Offset>,
mut wtr: W,
) -> Result<(), Error> {
static FMT_DAY: DecimalFormatter = DecimalFormatter::new();
static FMT_YEAR: DecimalFormatter = DecimalFormatter::new().padding(4);
static FMT_TIME_UNIT: DecimalFormatter =
DecimalFormatter::new().padding(2);
static FMT_DAY: IntegerFormatter = IntegerFormatter::new();
static FMT_YEAR: IntegerFormatter = IntegerFormatter::new().padding(4);
static FMT_TIME_UNIT: IntegerFormatter =
IntegerFormatter::new().padding(2);
if dt.year() < 0 {
// RFC 2822 actually says the year must be at least 1900, but
// other implementations (like Chrono) allow any positive 4-digit
// year.
return Err(err!(
"datetime {dt} has negative year, \
which cannot be formatted with RFC 2822",
));
return Err(Error::from(E::NegativeYear));
}
wtr.write_str(weekday_abbrev(dt.weekday()))?;
@ -1474,20 +1349,17 @@ impl DateTimePrinter {
timestamp: &Timestamp,
mut wtr: W,
) -> Result<(), Error> {
static FMT_DAY: DecimalFormatter = DecimalFormatter::new().padding(2);
static FMT_YEAR: DecimalFormatter = DecimalFormatter::new().padding(4);
static FMT_TIME_UNIT: DecimalFormatter =
DecimalFormatter::new().padding(2);
static FMT_DAY: IntegerFormatter = IntegerFormatter::new().padding(2);
static FMT_YEAR: IntegerFormatter = IntegerFormatter::new().padding(4);
static FMT_TIME_UNIT: IntegerFormatter =
IntegerFormatter::new().padding(2);
let dt = TimeZone::UTC.to_datetime(*timestamp);
if dt.year() < 0 {
// RFC 2822 actually says the year must be at least 1900, but
// other implementations (like Chrono) allow any positive 4-digit
// year.
return Err(err!(
"datetime {dt} has negative year, \
which cannot be formatted with RFC 2822",
));
return Err(Error::from(E::NegativeYear));
}
wtr.write_str(weekday_abbrev(dt.weekday()))?;
@ -1743,7 +1615,7 @@ mod tests {
insta::assert_snapshot!(
p("Thu, 10 Jan 2024 05:34:45 -0500"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: found parsed weekday of Thu, but parsed datetime of 2024-01-10T05:34:45 has weekday Wed",
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: found parsed weekday of `Thursday`, but parsed datetime has weekday `Wednesday`",
);
insta::assert_snapshot!(
p("Wed, 29 Feb 2023 05:34:45 -0500"),
@ -1755,11 +1627,11 @@ mod tests {
);
insta::assert_snapshot!(
p("Tue, 32 Jun 2024 05:34:45 -0500"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: day is not valid: parameter 'day' with value 32 is not in the required range of 1..=31",
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: failed to parse day: parameter 'day' with value 32 is not in the required range of 1..=31",
);
insta::assert_snapshot!(
p("Sun, 30 Jun 2024 24:00:00 -0500"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: hour is not valid: parameter 'hour' with value 24 is not in the required range of 0..=23",
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: invalid hour: parameter 'hour' with value 24 is not in the required range of 0..=23",
);
// No whitespace after time
insta::assert_snapshot!(
@ -1780,43 +1652,43 @@ mod tests {
);
insta::assert_snapshot!(
p(" "),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected RFC 2822 datetime, but got empty string after trimming whitespace",
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected RFC 2822 datetime, but got empty string after trimming leading whitespace",
);
insta::assert_snapshot!(
p("Wat"),
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, "W", is not a digit, but given string is too short (length is 3)"###,
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, `W`, is not a digit, but given string is too short (length is 3)",
);
insta::assert_snapshot!(
p("Wed"),
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, "W", is not a digit, but given string is too short (length is 3)"###,
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, `W`, is not a digit, but given string is too short (length is 3)",
);
insta::assert_snapshot!(
p("Wed "),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected comma after parsed weekday `Wed` in RFC 2822 datetime, but found end of string instead",
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected comma after parsed weekday in RFC 2822 datetime, but found end of input instead",
);
insta::assert_snapshot!(
p("Wed ,"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input",
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input",
);
insta::assert_snapshot!(
p("Wed , "),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input",
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input",
);
insta::assert_snapshot!(
p("Wat, "),
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, "W", is not a digit, but did not recognize "Wat" as a valid weekday abbreviation"###,
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, `W`, is not a digit, but did not recognize a valid weekday abbreviation",
);
insta::assert_snapshot!(
p("Wed, "),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input",
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input",
);
insta::assert_snapshot!(
p("Wed, 1"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day 1: expected at least one whitespace character (space or tab), but found none",
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none",
);
insta::assert_snapshot!(
p("Wed, 10"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day 10: expected at least one whitespace character (space or tab), but found none",
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none",
);
insta::assert_snapshot!(
p("Wed, 10 J"),
@ -1824,11 +1696,11 @@ mod tests {
);
insta::assert_snapshot!(
p("Wed, 10 Wat"),
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but did not recognize "Wat" as a valid month"###,
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but did not recognize a valid abbreviated month name",
);
insta::assert_snapshot!(
p("Wed, 10 Jan"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing month name: expected at least one whitespace character (space or tab), but found none",
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing abbreviated month name: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none",
);
insta::assert_snapshot!(
p("Wed, 10 Jan 2"),
@ -1836,15 +1708,15 @@ mod tests {
);
insta::assert_snapshot!(
p("Wed, 10 Jan 2024"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing year: expected at least one whitespace character (space or tab), but found none",
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing year: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none",
);
insta::assert_snapshot!(
p("Wed, 10 Jan 2024 05"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of ':', but found end of input",
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of `:`, but found end of input",
);
insta::assert_snapshot!(
p("Wed, 10 Jan 2024 053"),
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of ':', but found 3",
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of `:`, but found `3`",
);
insta::assert_snapshot!(
p("Wed, 10 Jan 2024 05:34"),
@ -1860,7 +1732,7 @@ mod tests {
);
insta::assert_snapshot!(
p("Wed, 10 Jan 2024 05:34:45 J"),
@r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected obsolete RFC 2822 time zone abbreviation, but found "J""###,
@"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected obsolete RFC 2822 time zone abbreviation, but did not recognize a valid abbreviation",
);
}
@ -2040,7 +1912,7 @@ mod tests {
.at(5, 34, 45, 0)
.in_tz("America/New_York")
.unwrap();
insta::assert_snapshot!(p(&zdt), @"datetime -000001-01-10T05:34:45 has negative year, which cannot be formatted with RFC 2822");
insta::assert_snapshot!(p(&zdt), @"datetime has negative year, which cannot be formatted with RFC 2822");
}
#[test]
@ -2062,6 +1934,6 @@ mod tests {
.in_tz("America/New_York")
.unwrap()
.timestamp();
insta::assert_snapshot!(p(ts), @"datetime -000001-01-10T10:30:47 has negative year, which cannot be formatted with RFC 2822");
insta::assert_snapshot!(p(ts), @"datetime has negative year, which cannot be formatted with RFC 2822");
}
}

View file

@ -95,13 +95,13 @@ including by returning an error if it isn't supported.
// UTCOffsetMinutePrecision
use crate::{
error::{err, Error},
error::{fmt::rfc9557::Error as E, Error},
fmt::{
offset::{self, ParsedOffset},
temporal::{TimeZoneAnnotation, TimeZoneAnnotationKind},
Parsed,
},
util::{escape, parse},
util::parse,
};
/// The result of parsing RFC 9557 annotations.
@ -112,11 +112,6 @@ use crate::{
/// only validated at a syntax level.
#[derive(Debug)]
pub(crate) struct ParsedAnnotations<'i> {
/// The original input that all of the annotations were parsed from.
///
/// N.B. This is currently unused, but potentially useful, so we leave it.
#[allow(dead_code)]
input: escape::Bytes<'i>,
/// An optional time zone annotation that was extracted from the input.
time_zone: Option<ParsedTimeZone<'i>>,
// While we parse/validate them, we don't support any other annotations
@ -127,7 +122,7 @@ pub(crate) struct ParsedAnnotations<'i> {
impl<'i> ParsedAnnotations<'i> {
/// Return an empty parsed annotations.
pub(crate) fn none() -> ParsedAnnotations<'static> {
ParsedAnnotations { input: escape::Bytes(&[]), time_zone: None }
ParsedAnnotations { time_zone: None }
}
/// Turns this parsed time zone into a structured time zone annotation,
@ -212,8 +207,6 @@ impl Parser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ParsedAnnotations<'i>>, Error> {
let mkslice = parse::slicer(input);
let Parsed { value: time_zone, mut input } =
self.parse_time_zone_annotation(input)?;
loop {
@ -229,10 +222,7 @@ impl Parser {
input = unconsumed;
}
let value = ParsedAnnotations {
input: escape::Bytes(mkslice(input)),
time_zone,
};
let value = ParsedAnnotations { time_zone };
Ok(Parsed { value, input })
}
@ -241,14 +231,18 @@ impl Parser {
mut input: &'i [u8],
) -> Result<Parsed<'i, Option<ParsedTimeZone<'i>>>, Error> {
let unconsumed = input;
if input.is_empty() || input[0] != b'[' {
let Some((&first, tail)) = input.split_first() else {
return Ok(Parsed { value: None, input: unconsumed });
};
if first != b'[' {
return Ok(Parsed { value: None, input: unconsumed });
}
input = &input[1..];
input = tail;
let critical = input.starts_with(b"!");
if critical {
input = &input[1..];
let mut critical = false;
if let Some(tail) = input.strip_prefix(b"!") {
critical = true;
input = tail;
}
// If we're starting with a `+` or `-`, then we know we MUST have a
@ -284,8 +278,8 @@ impl Parser {
// a generic key/value annotation.
return Ok(Parsed { value: None, input: unconsumed });
}
while input.starts_with(b"/") {
input = &input[1..];
while let Some(tail) = input.strip_prefix(b"/") {
input = tail;
let Parsed { input: unconsumed, .. } =
self.parse_tz_annotation_iana_name(input)?;
input = unconsumed;
@ -306,17 +300,21 @@ impl Parser {
&self,
mut input: &'i [u8],
) -> Result<Parsed<'i, bool>, Error> {
if input.is_empty() || input[0] != b'[' {
let Some((&first, tail)) = input.split_first() else {
return Ok(Parsed { value: false, input });
};
if first != b'[' {
return Ok(Parsed { value: false, input });
}
input = &input[1..];
input = tail;
let critical = input.starts_with(b"!");
if critical {
input = &input[1..];
let mut critical = false;
if let Some(tail) = input.strip_prefix(b"!") {
critical = true;
input = tail;
}
let Parsed { value: key, input } = self.parse_annotation_key(input)?;
let Parsed { input, .. } = self.parse_annotation_key(input)?;
let Parsed { input, .. } = self.parse_annotation_separator(input)?;
let Parsed { input, .. } = self.parse_annotation_values(input)?;
let Parsed { input, .. } = self.parse_annotation_close(input)?;
@ -326,11 +324,7 @@ impl Parser {
// critical flag isn't set, we're "permissive" and just validate that
// the syntax is correct (as we've already done at this point).
if critical {
return Err(err!(
"found unsupported RFC 9557 annotation with key {key:?} \
with the critical flag ('!') set",
key = escape::Bytes(key),
));
return Err(Error::from(E::UnsupportedAnnotationCritical));
}
Ok(Parsed { value: true, input })
@ -381,8 +375,8 @@ impl Parser {
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
let Parsed { mut input, .. } = self.parse_annotation_value(input)?;
while input.starts_with(b"-") {
input = &input[1..];
while let Some(tail) = input.strip_prefix(b"-") {
input = tail;
let Parsed { input: unconsumed, .. } =
self.parse_annotation_value(input)?;
input = unconsumed;
@ -413,173 +407,137 @@ impl Parser {
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
if input.is_empty() {
return Err(err!(
"expected the start of an RFC 9557 annotation or IANA \
time zone component name, but found end of input instead",
));
let Some((&first, tail)) = input.split_first() else {
return Err(Error::from(E::EndOfInputAnnotation));
};
if !matches!(first, b'_' | b'.' | b'A'..=b'Z' | b'a'..=b'z') {
return Err(Error::from(E::UnexpectedByteAnnotation {
byte: first,
}));
}
if !matches!(input[0], b'_' | b'.' | b'A'..=b'Z' | b'a'..=b'z') {
return Err(err!(
"expected ASCII alphabetic byte (or underscore or period) \
at the start of an RFC 9557 annotation or time zone \
component name, but found {:?} instead",
escape::Byte(input[0]),
));
}
Ok(Parsed { value: (), input: &input[1..] })
Ok(Parsed { value: (), input: tail })
}
fn parse_tz_annotation_char<'i>(
&self,
input: &'i [u8],
) -> Parsed<'i, bool> {
let is_tz_annotation_char = |byte| {
matches!(
byte,
b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z',
)
let Some((&first, tail)) = input.split_first() else {
return Parsed { value: false, input };
};
if input.is_empty() || !is_tz_annotation_char(input[0]) {
if !matches!(
first,
b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z',
) {
return Parsed { value: false, input };
}
Parsed { value: true, input: &input[1..] }
Parsed { value: true, input: tail }
}
fn parse_annotation_key_leading_char<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
if input.is_empty() {
return Err(err!(
"expected the start of an RFC 9557 annotation key, \
but found end of input instead",
));
let Some((&first, tail)) = input.split_first() else {
return Err(Error::from(E::EndOfInputAnnotationKey));
};
if !matches!(first, b'_' | b'a'..=b'z') {
return Err(Error::from(E::UnexpectedByteAnnotationKey {
byte: first,
}));
}
if !matches!(input[0], b'_' | b'a'..=b'z') {
return Err(err!(
"expected lowercase alphabetic byte (or underscore) \
at the start of an RFC 9557 annotation key, \
but found {:?} instead",
escape::Byte(input[0]),
));
}
Ok(Parsed { value: (), input: &input[1..] })
Ok(Parsed { value: (), input: tail })
}
fn parse_annotation_key_char<'i>(
&self,
input: &'i [u8],
) -> Parsed<'i, bool> {
let is_annotation_key_char =
|byte| matches!(byte, b'_' | b'-' | b'0'..=b'9' | b'a'..=b'z');
if input.is_empty() || !is_annotation_key_char(input[0]) {
let Some((&first, tail)) = input.split_first() else {
return Parsed { value: false, input };
};
if !matches!(first, b'_' | b'-' | b'0'..=b'9' | b'a'..=b'z') {
return Parsed { value: false, input };
}
Parsed { value: true, input: &input[1..] }
Parsed { value: true, input: tail }
}
fn parse_annotation_value_leading_char<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
if input.is_empty() {
return Err(err!(
"expected the start of an RFC 9557 annotation value, \
but found end of input instead",
));
let Some((&first, tail)) = input.split_first() else {
return Err(Error::from(E::EndOfInputAnnotationValue));
};
if !matches!(first, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z') {
return Err(Error::from(E::UnexpectedByteAnnotationValue {
byte: first,
}));
}
if !matches!(input[0], b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z') {
return Err(err!(
"expected alphanumeric ASCII byte \
at the start of an RFC 9557 annotation value, \
but found {:?} instead",
escape::Byte(input[0]),
));
}
Ok(Parsed { value: (), input: &input[1..] })
Ok(Parsed { value: (), input: tail })
}
fn parse_annotation_value_char<'i>(
&self,
input: &'i [u8],
) -> Parsed<'i, bool> {
let is_annotation_value_char =
|byte| matches!(byte, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z');
if input.is_empty() || !is_annotation_value_char(input[0]) {
let Some((&first, tail)) = input.split_first() else {
return Parsed { value: false, input };
};
if !matches!(first, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z') {
return Parsed { value: false, input };
}
Parsed { value: true, input: &input[1..] }
Parsed { value: true, input: tail }
}
fn parse_annotation_separator<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
if input.is_empty() {
return Err(err!(
"expected an '=' after parsing an RFC 9557 annotation key, \
but found end of input instead",
));
}
if input[0] != b'=' {
let Some((&first, tail)) = input.split_first() else {
return Err(Error::from(E::EndOfInputAnnotationSeparator));
};
if first != b'=' {
// If we see a /, then it's likely the user was trying to insert a
// time zone annotation in the wrong place.
return Err(if input[0] == b'/' {
err!(
"expected an '=' after parsing an RFC 9557 annotation \
key, but found / instead (time zone annotations must \
come first)",
)
return Err(Error::from(if first == b'/' {
E::UnexpectedSlashAnnotationSeparator
} else {
err!(
"expected an '=' after parsing an RFC 9557 annotation \
key, but found {:?} instead",
escape::Byte(input[0]),
)
});
E::UnexpectedByteAnnotationSeparator { byte: first }
}));
}
Ok(Parsed { value: (), input: &input[1..] })
Ok(Parsed { value: (), input: tail })
}
fn parse_annotation_close<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
if input.is_empty() {
return Err(err!(
"expected an ']' after parsing an RFC 9557 annotation key \
and value, but found end of input instead",
));
let Some((&first, tail)) = input.split_first() else {
return Err(Error::from(E::EndOfInputAnnotationClose));
};
if first != b']' {
return Err(Error::from(E::UnexpectedByteAnnotationClose {
byte: first,
}));
}
if input[0] != b']' {
return Err(err!(
"expected an ']' after parsing an RFC 9557 annotation key \
and value, but found {:?} instead",
escape::Byte(input[0]),
));
}
Ok(Parsed { value: (), input: &input[1..] })
Ok(Parsed { value: (), input: tail })
}
fn parse_tz_annotation_close<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
if input.is_empty() {
return Err(err!(
"expected an ']' after parsing an RFC 9557 time zone \
annotation, but found end of input instead",
));
let Some((&first, tail)) = input.split_first() else {
return Err(Error::from(E::EndOfInputTzAnnotationClose));
};
if first != b']' {
return Err(Error::from(E::UnexpectedByteTzAnnotationClose {
byte: first,
}));
}
if input[0] != b']' {
return Err(err!(
"expected an ']' after parsing an RFC 9557 time zone \
annotation, but found {:?} instead",
escape::Byte(input[0]),
));
}
Ok(Parsed { value: (), input: &input[1..] })
Ok(Parsed { value: (), input: tail })
}
}
@ -665,24 +623,22 @@ mod tests {
fn ok_empty() {
let p = |input| Parser::new().parse(input).unwrap();
insta::assert_debug_snapshot!(p(b""), @r###"
insta::assert_debug_snapshot!(p(b""), @r#"
Parsed {
value: ParsedAnnotations {
input: "",
time_zone: None,
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"blah"), @r###"
"#);
insta::assert_debug_snapshot!(p(b"blah"), @r#"
Parsed {
value: ParsedAnnotations {
input: "",
time_zone: None,
},
input: "blah",
}
"###);
"#);
}
#[test]
@ -691,39 +647,36 @@ mod tests {
insta::assert_debug_snapshot!(
p(b"[u-ca=chinese]"),
@r###"
@r#"
Parsed {
value: ParsedAnnotations {
input: "[u-ca=chinese]",
time_zone: None,
},
input: "",
}
"###,
"#,
);
insta::assert_debug_snapshot!(
p(b"[u-ca=chinese-japanese]"),
@r###"
@r#"
Parsed {
value: ParsedAnnotations {
input: "[u-ca=chinese-japanese]",
time_zone: None,
},
input: "",
}
"###,
"#,
);
insta::assert_debug_snapshot!(
p(b"[u-ca=chinese-japanese-russian]"),
@r###"
@r#"
Parsed {
value: ParsedAnnotations {
input: "[u-ca=chinese-japanese-russian]",
time_zone: None,
},
input: "",
}
"###,
"#,
);
}
@ -731,10 +684,9 @@ mod tests {
fn ok_iana() {
let p = |input| Parser::new().parse(input).unwrap();
insta::assert_debug_snapshot!(p(b"[America/New_York]"), @r###"
insta::assert_debug_snapshot!(p(b"[America/New_York]"), @r#"
Parsed {
value: ParsedAnnotations {
input: "[America/New_York]",
time_zone: Some(
Named {
critical: false,
@ -744,11 +696,10 @@ mod tests {
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"[!America/New_York]"), @r###"
"#);
insta::assert_debug_snapshot!(p(b"[!America/New_York]"), @r#"
Parsed {
value: ParsedAnnotations {
input: "[!America/New_York]",
time_zone: Some(
Named {
critical: true,
@ -758,11 +709,10 @@ mod tests {
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"[UTC]"), @r###"
"#);
insta::assert_debug_snapshot!(p(b"[UTC]"), @r#"
Parsed {
value: ParsedAnnotations {
input: "[UTC]",
time_zone: Some(
Named {
critical: false,
@ -772,11 +722,10 @@ mod tests {
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"[.._foo_../.0+-]"), @r###"
"#);
insta::assert_debug_snapshot!(p(b"[.._foo_../.0+-]"), @r#"
Parsed {
value: ParsedAnnotations {
input: "[.._foo_../.0+-]",
time_zone: Some(
Named {
critical: false,
@ -786,17 +735,16 @@ mod tests {
},
input: "",
}
"###);
"#);
}
#[test]
fn ok_offset() {
let p = |input| Parser::new().parse(input).unwrap();
insta::assert_debug_snapshot!(p(b"[-00]"), @r###"
insta::assert_debug_snapshot!(p(b"[-00]"), @r#"
Parsed {
value: ParsedAnnotations {
input: "[-00]",
time_zone: Some(
Offset {
critical: false,
@ -810,11 +758,10 @@ mod tests {
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"[+00]"), @r###"
"#);
insta::assert_debug_snapshot!(p(b"[+00]"), @r#"
Parsed {
value: ParsedAnnotations {
input: "[+00]",
time_zone: Some(
Offset {
critical: false,
@ -828,11 +775,10 @@ mod tests {
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"[-05]"), @r###"
"#);
insta::assert_debug_snapshot!(p(b"[-05]"), @r#"
Parsed {
value: ParsedAnnotations {
input: "[-05]",
time_zone: Some(
Offset {
critical: false,
@ -846,11 +792,10 @@ mod tests {
},
input: "",
}
"###);
insta::assert_debug_snapshot!(p(b"[!+05:12]"), @r###"
"#);
insta::assert_debug_snapshot!(p(b"[!+05:12]"), @r#"
Parsed {
value: ParsedAnnotations {
input: "[!+05:12]",
time_zone: Some(
Offset {
critical: true,
@ -864,7 +809,7 @@ mod tests {
},
input: "",
}
"###);
"#);
}
#[test]
@ -873,10 +818,9 @@ mod tests {
insta::assert_debug_snapshot!(
p(b"[America/New_York][u-ca=chinese-japanese-russian]"),
@r###"
@r#"
Parsed {
value: ParsedAnnotations {
input: "[America/New_York][u-ca=chinese-japanese-russian]",
time_zone: Some(
Named {
critical: false,
@ -886,7 +830,7 @@ mod tests {
},
input: "",
}
"###,
"#,
);
}
@ -894,11 +838,11 @@ mod tests {
fn err_iana() {
insta::assert_snapshot!(
Parser::new().parse(b"[0/Foo]").unwrap_err(),
@r###"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found "0" instead"###,
@"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found `0` instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[Foo/0Bar]").unwrap_err(),
@r###"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found "0" instead"###,
@"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found `0` instead",
);
}
@ -906,23 +850,23 @@ mod tests {
fn err_offset() {
insta::assert_snapshot!(
Parser::new().parse(b"[+").unwrap_err(),
@r###"failed to parse hours in UTC numeric offset "+": expected two digit hour after sign, but found end of input"###,
@"failed to parse hours in UTC numeric offset: expected two digit hour after sign, but found end of input",
);
insta::assert_snapshot!(
Parser::new().parse(b"[+26]").unwrap_err(),
@r###"failed to parse hours in UTC numeric offset "+26]": offset hours are not valid: parameter 'hours' with value 26 is not in the required range of 0..=25"###,
@"failed to parse hours in UTC numeric offset: hour in time zone offset is out of range: parameter 'hours' with value 26 is not in the required range of 0..=25",
);
insta::assert_snapshot!(
Parser::new().parse(b"[-26]").unwrap_err(),
@r###"failed to parse hours in UTC numeric offset "-26]": offset hours are not valid: parameter 'hours' with value 26 is not in the required range of 0..=25"###,
@"failed to parse hours in UTC numeric offset: hour in time zone offset is out of range: parameter 'hours' with value 26 is not in the required range of 0..=25",
);
insta::assert_snapshot!(
Parser::new().parse(b"[+05:12:34]").unwrap_err(),
@r###"subminute precision for UTC numeric offset "+05:12:34]" is not enabled in this context (must provide only integral minutes)"###,
@"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)",
);
insta::assert_snapshot!(
Parser::new().parse(b"[+05:12:34.123456789]").unwrap_err(),
@r###"subminute precision for UTC numeric offset "+05:12:34.123456789]" is not enabled in this context (must provide only integral minutes)"###,
@"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)",
);
}
@ -930,7 +874,7 @@ mod tests {
fn err_critical_unsupported() {
insta::assert_snapshot!(
Parser::new().parse(b"[!u-ca=chinese]").unwrap_err(),
@r###"found unsupported RFC 9557 annotation with key "u-ca" with the critical flag ('!') set"###,
@"found unsupported RFC 9557 annotation with the critical flag (`!`) set",
);
}
@ -942,7 +886,7 @@ mod tests {
);
insta::assert_snapshot!(
Parser::new().parse(b"[&").unwrap_err(),
@r###"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found "&" instead"###,
@"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found `&` instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[Foo][").unwrap_err(),
@ -950,7 +894,7 @@ mod tests {
);
insta::assert_snapshot!(
Parser::new().parse(b"[Foo][&").unwrap_err(),
@r###"expected lowercase alphabetic byte (or underscore) at the start of an RFC 9557 annotation key, but found "&" instead"###,
@"expected lowercase alphabetic byte (or underscore) at the start of an RFC 9557 annotation key, but found `&` instead",
);
}
@ -958,27 +902,27 @@ mod tests {
fn err_separator() {
insta::assert_snapshot!(
Parser::new().parse(b"[abc").unwrap_err(),
@"expected an ']' after parsing an RFC 9557 time zone annotation, but found end of input instead",
@"expected an `]` after parsing an RFC 9557 time zone annotation, but found end of input instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[_abc").unwrap_err(),
@"expected an ']' after parsing an RFC 9557 time zone annotation, but found end of input instead",
@"expected an `]` after parsing an RFC 9557 time zone annotation, but found end of input instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[abc^").unwrap_err(),
@r###"expected an ']' after parsing an RFC 9557 time zone annotation, but found "^" instead"###,
@"expected an `]` after parsing an RFC 9557 time zone annotation, but found `^` instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[Foo][abc").unwrap_err(),
@"expected an '=' after parsing an RFC 9557 annotation key, but found end of input instead",
@"expected an `=` after parsing an RFC 9557 annotation key, but found end of input instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[Foo][_abc").unwrap_err(),
@"expected an '=' after parsing an RFC 9557 annotation key, but found end of input instead",
@"expected an `=` after parsing an RFC 9557 annotation key, but found end of input instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[Foo][abc^").unwrap_err(),
@r###"expected an '=' after parsing an RFC 9557 annotation key, but found "^" instead"###,
@"expected an `=` after parsing an RFC 9557 annotation key, but found `^` instead",
);
}
@ -994,11 +938,11 @@ mod tests {
);
insta::assert_snapshot!(
Parser::new().parse(b"[abc=^").unwrap_err(),
@r###"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found "^" instead"###,
@"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found `^` instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[abc=]").unwrap_err(),
@r###"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found "]" instead"###,
@"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found `]` instead",
);
}
@ -1006,11 +950,11 @@ mod tests {
fn err_close() {
insta::assert_snapshot!(
Parser::new().parse(b"[abc=123").unwrap_err(),
@"expected an ']' after parsing an RFC 9557 annotation key and value, but found end of input instead",
@"expected an `]` after parsing an RFC 9557 annotation key and value, but found end of input instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[abc=123*").unwrap_err(),
@r###"expected an ']' after parsing an RFC 9557 annotation key and value, but found "*" instead"###,
@"expected an `]` after parsing an RFC 9557 annotation key and value, but found `*` instead",
);
}
@ -1046,7 +990,7 @@ mod tests {
let p = |input| Parser::new().parse(input).unwrap_err();
insta::assert_snapshot!(
p(b"[america/new_york][america/new_york]"),
@"expected an '=' after parsing an RFC 9557 annotation key, but found / instead (time zone annotations must come first)",
@"expected an `=` after parsing an RFC 9557 annotation key, but found `/` instead (time zone annotations must come first)",
);
}
}

View file

@ -1778,28 +1778,24 @@ pub mod unsigned_duration {
fn parse_iso_or_friendly(
bytes: &[u8],
) -> Result<core::time::Duration, crate::Error> {
if bytes.is_empty() {
return Err(crate::error::err!(
"an empty string is not a valid `std::time::Duration`, \
expected either a ISO 8601 or Jiff's 'friendly' \
format",
let Some((&byte, tail)) = bytes.split_first() else {
return Err(crate::Error::from(
crate::error::fmt::Error::HybridDurationEmpty,
));
}
let mut first = bytes[0];
};
let mut first = byte;
// N.B. Unsigned durations don't support negative durations (of
// course), but we still check for it here so that we can defer to
// the dedicated parsers. They will provide their own error messages.
if first == b'+' || first == b'-' {
if bytes.len() == 1 {
return Err(crate::error::err!(
"found nothing after sign `{sign}`, \
which is not a valid `std::time::Duration`, \
expected either a ISO 8601 or Jiff's 'friendly' \
format",
sign = crate::util::escape::Byte(first),
let Some(&byte) = tail.first() else {
return Err(crate::Error::from(
crate::error::fmt::Error::HybridDurationPrefix {
sign: first,
},
));
}
first = bytes[1];
};
first = byte;
}
let dur = if first == b'P' || first == b'p' {
crate::fmt::temporal::DEFAULT_SPAN_PARSER

View file

@ -1,16 +1,19 @@
use crate::{
error::{err, ErrorContext},
error::{
fmt::strtime::{Error as E, FormatError as FE},
ErrorContext,
},
fmt::{
strtime::{
month_name_abbrev, month_name_full, weekday_name_abbrev,
weekday_name_full, BrokenDownTime, Config, Custom, Extension,
Flag,
},
util::{DecimalFormatter, FractionalFormatter},
util::{FractionalFormatter, IntegerFormatter},
Write, WriteExt,
},
tz::Offset,
util::{escape, utf8},
util::utf8,
Error,
};
@ -39,10 +42,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
self.wtr.write_str("%")?;
break;
}
return Err(err!(
"invalid format string, expected byte after '%', \
but found end of format string",
));
return Err(E::UnexpectedEndAfterPercent.into());
}
let orig = self.fmt;
if let Err(err) = self.format_one() {
@ -61,100 +61,92 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
}
fn format_one(&mut self) -> Result<(), Error> {
let failc =
|directive, colons| E::DirectiveFailure { directive, colons };
let fail = |directive| failc(directive, 0);
// Parse extensions like padding/case options and padding width.
let ext = self.parse_extension()?;
match self.f() {
b'%' => self.wtr.write_str("%").context("%% failed")?,
b'A' => self.fmt_weekday_full(&ext).context("%A failed")?,
b'a' => self.fmt_weekday_abbrev(&ext).context("%a failed")?,
b'B' => self.fmt_month_full(&ext).context("%B failed")?,
b'b' => self.fmt_month_abbrev(&ext).context("%b failed")?,
b'C' => self.fmt_century(&ext).context("%C failed")?,
b'c' => self.fmt_datetime(&ext).context("%c failed")?,
b'D' => self.fmt_american_date(&ext).context("%D failed")?,
b'd' => self.fmt_day_zero(&ext).context("%d failed")?,
b'e' => self.fmt_day_space(&ext).context("%e failed")?,
b'F' => self.fmt_iso_date(&ext).context("%F failed")?,
b'f' => self.fmt_fractional(&ext).context("%f failed")?,
b'G' => self.fmt_iso_week_year(&ext).context("%G failed")?,
b'g' => self.fmt_iso_week_year2(&ext).context("%g failed")?,
b'H' => self.fmt_hour24_zero(&ext).context("%H failed")?,
b'h' => self.fmt_month_abbrev(&ext).context("%b failed")?,
b'I' => self.fmt_hour12_zero(&ext).context("%H failed")?,
b'j' => self.fmt_day_of_year(&ext).context("%j failed")?,
b'k' => self.fmt_hour24_space(&ext).context("%k failed")?,
b'l' => self.fmt_hour12_space(&ext).context("%l failed")?,
b'M' => self.fmt_minute(&ext).context("%M failed")?,
b'm' => self.fmt_month(&ext).context("%m failed")?,
b'N' => self.fmt_nanoseconds(&ext).context("%N failed")?,
b'n' => self.fmt_literal("\n").context("%n failed")?,
b'P' => self.fmt_ampm_lower(&ext).context("%P failed")?,
b'p' => self.fmt_ampm_upper(&ext).context("%p failed")?,
b'%' => self.wtr.write_str("%").context(fail(b'%')),
b'A' => self.fmt_weekday_full(&ext).context(fail(b'A')),
b'a' => self.fmt_weekday_abbrev(&ext).context(fail(b'a')),
b'B' => self.fmt_month_full(&ext).context(fail(b'B')),
b'b' => self.fmt_month_abbrev(&ext).context(fail(b'b')),
b'C' => self.fmt_century(&ext).context(fail(b'C')),
b'c' => self.fmt_datetime(&ext).context(fail(b'c')),
b'D' => self.fmt_american_date(&ext).context(fail(b'D')),
b'd' => self.fmt_day_zero(&ext).context(fail(b'd')),
b'e' => self.fmt_day_space(&ext).context(fail(b'e')),
b'F' => self.fmt_iso_date(&ext).context(fail(b'F')),
b'f' => self.fmt_fractional(&ext).context(fail(b'f')),
b'G' => self.fmt_iso_week_year(&ext).context(fail(b'G')),
b'g' => self.fmt_iso_week_year2(&ext).context(fail(b'g')),
b'H' => self.fmt_hour24_zero(&ext).context(fail(b'H')),
b'h' => self.fmt_month_abbrev(&ext).context(fail(b'b')),
b'I' => self.fmt_hour12_zero(&ext).context(fail(b'H')),
b'j' => self.fmt_day_of_year(&ext).context(fail(b'j')),
b'k' => self.fmt_hour24_space(&ext).context(fail(b'k')),
b'l' => self.fmt_hour12_space(&ext).context(fail(b'l')),
b'M' => self.fmt_minute(&ext).context(fail(b'M')),
b'm' => self.fmt_month(&ext).context(fail(b'm')),
b'N' => self.fmt_nanoseconds(&ext).context(fail(b'N')),
b'n' => self.fmt_literal("\n").context(fail(b'n')),
b'P' => self.fmt_ampm_lower(&ext).context(fail(b'P')),
b'p' => self.fmt_ampm_upper(&ext).context(fail(b'p')),
b'Q' => match ext.colons {
0 => self.fmt_iana_nocolon().context("%Q failed")?,
1 => self.fmt_iana_colon().context("%:Q failed")?,
_ => {
return Err(err!(
"invalid number of `:` in `%Q` directive"
))
}
0 => self.fmt_iana_nocolon().context(fail(b'Q')),
1 => self.fmt_iana_colon().context(failc(b'Q', 1)),
_ => return Err(E::ColonCount { directive: b'Q' }.into()),
},
b'q' => self.fmt_quarter(&ext).context("%q failed")?,
b'R' => self.fmt_clock_nosecs(&ext).context("%R failed")?,
b'r' => self.fmt_12hour_time(&ext).context("%r failed")?,
b'S' => self.fmt_second(&ext).context("%S failed")?,
b's' => self.fmt_timestamp(&ext).context("%s failed")?,
b'T' => self.fmt_clock_secs(&ext).context("%T failed")?,
b't' => self.fmt_literal("\t").context("%t failed")?,
b'U' => self.fmt_week_sun(&ext).context("%U failed")?,
b'u' => self.fmt_weekday_mon(&ext).context("%u failed")?,
b'V' => self.fmt_week_iso(&ext).context("%V failed")?,
b'W' => self.fmt_week_mon(&ext).context("%W failed")?,
b'w' => self.fmt_weekday_sun(&ext).context("%w failed")?,
b'X' => self.fmt_time(&ext).context("%X failed")?,
b'x' => self.fmt_date(&ext).context("%x failed")?,
b'Y' => self.fmt_year(&ext).context("%Y failed")?,
b'y' => self.fmt_year2(&ext).context("%y failed")?,
b'Z' => self.fmt_tzabbrev(&ext).context("%Z failed")?,
b'q' => self.fmt_quarter(&ext).context(fail(b'q')),
b'R' => self.fmt_clock_nosecs(&ext).context(fail(b'R')),
b'r' => self.fmt_12hour_time(&ext).context(fail(b'r')),
b'S' => self.fmt_second(&ext).context(fail(b'S')),
b's' => self.fmt_timestamp(&ext).context(fail(b's')),
b'T' => self.fmt_clock_secs(&ext).context(fail(b'T')),
b't' => self.fmt_literal("\t").context(fail(b't')),
b'U' => self.fmt_week_sun(&ext).context(fail(b'U')),
b'u' => self.fmt_weekday_mon(&ext).context(fail(b'u')),
b'V' => self.fmt_week_iso(&ext).context(fail(b'V')),
b'W' => self.fmt_week_mon(&ext).context(fail(b'W')),
b'w' => self.fmt_weekday_sun(&ext).context(fail(b'w')),
b'X' => self.fmt_time(&ext).context(fail(b'X')),
b'x' => self.fmt_date(&ext).context(fail(b'x')),
b'Y' => self.fmt_year(&ext).context(fail(b'Y')),
b'y' => self.fmt_year2(&ext).context(fail(b'y')),
b'Z' => self.fmt_tzabbrev(&ext).context(fail(b'Z')),
b'z' => match ext.colons {
0 => self.fmt_offset_nocolon().context("%z failed")?,
1 => self.fmt_offset_colon().context("%:z failed")?,
2 => self.fmt_offset_colon2().context("%::z failed")?,
3 => self.fmt_offset_colon3().context("%:::z failed")?,
_ => {
return Err(err!(
"invalid number of `:` in `%z` directive"
))
}
0 => self.fmt_offset_nocolon().context(fail(b'z')),
1 => self.fmt_offset_colon().context(failc(b'z', 1)),
2 => self.fmt_offset_colon2().context(failc(b'z', 2)),
3 => self.fmt_offset_colon3().context(failc(b'z', 3)),
_ => return Err(E::ColonCount { directive: b'z' }.into()),
},
b'.' => {
if !self.bump_fmt() {
return Err(err!(
"invalid format string, expected directive after '%.'",
));
return Err(E::UnexpectedEndAfterDot.into());
}
// Parse precision settings after the `.`, effectively
// overriding any digits that came before it.
let ext = Extension { width: self.parse_width()?, ..ext };
match self.f() {
b'f' => {
self.fmt_dot_fractional(&ext).context("%.f failed")?
}
b'f' => self
.fmt_dot_fractional(&ext)
.context(E::DirectiveFailureDot { directive: b'f' }),
unk => {
return Err(err!(
"found unrecognized directive %{unk} following %.",
unk = escape::Byte(unk),
return Err(Error::from(
E::UnknownDirectiveAfterDot { directive: unk },
));
}
}
}
unk => {
return Err(err!(
"found unrecognized specifier directive %{unk}",
unk = escape::Byte(unk),
));
return Err(Error::from(E::UnknownDirective {
directive: unk,
}))
}
}
}?;
self.bump_fmt();
Ok(())
}
@ -200,21 +192,17 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
/// some remaining bytes to parse.
#[cold]
#[inline(never)]
fn utf8_decode_and_bump(&mut self) -> Result<char, Error> {
fn utf8_decode_and_bump(&mut self) -> Result<char, FE> {
match utf8::decode(self.fmt).expect("non-empty fmt") {
Ok(ch) => {
self.fmt = &self.fmt[ch.len_utf8()..];
return Ok(ch);
}
Err(errant_bytes) if self.config.lenient => {
self.fmt = &self.fmt[errant_bytes.len()..];
Err(err) if self.config.lenient => {
self.fmt = &self.fmt[err.len()..];
return Ok(char::REPLACEMENT_CHARACTER);
}
Err(errant_bytes) => Err(err!(
"found invalid UTF-8 byte {errant_bytes:?} in format \
string (format strings must be valid UTF-8)",
errant_bytes = escape::Bytes(errant_bytes),
)),
Err(_) => Err(FE::InvalidUtf8),
}
}
@ -270,11 +258,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
/// %P
fn fmt_ampm_lower(&mut self, ext: &Extension) -> Result<(), Error> {
let hour = self
.tm
.hour_ranged()
.ok_or_else(|| err!("requires time to format AM/PM"))?
.get();
let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get();
ext.write_str(
Case::AsIs,
if hour < 12 { "am" } else { "pm" },
@ -284,11 +268,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
/// %p
fn fmt_ampm_upper(&mut self, ext: &Extension) -> Result<(), Error> {
let hour = self
.tm
.hour_ranged()
.ok_or_else(|| err!("requires time to format AM/PM"))?
.get();
let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get();
// Manually specialize this case to avoid hitting `write_str_cold`.
let s = if matches!(ext.flag, Some(Flag::Swapcase)) {
if hour < 12 {
@ -339,8 +319,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let day = self
.tm
.day
.or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged()))
.ok_or_else(|| err!("requires date to format day"))?
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.day_ranged()),
)
.ok_or(FE::RequiresDate)?
.get();
ext.write_int(b'0', Some(2), day, self.wtr)
}
@ -350,19 +333,18 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let day = self
.tm
.day
.or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged()))
.ok_or_else(|| err!("requires date to format day"))?
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.day_ranged()),
)
.ok_or(FE::RequiresDate)?
.get();
ext.write_int(b' ', Some(2), day, self.wtr)
}
/// %I
fn fmt_hour12_zero(&mut self, ext: &Extension) -> Result<(), Error> {
let mut hour = self
.tm
.hour_ranged()
.ok_or_else(|| err!("requires time to format hour"))?
.get();
let mut hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get();
if hour == 0 {
hour = 12;
} else if hour > 12 {
@ -373,21 +355,13 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
/// %H
fn fmt_hour24_zero(&mut self, ext: &Extension) -> Result<(), Error> {
let hour = self
.tm
.hour_ranged()
.ok_or_else(|| err!("requires time to format hour"))?
.get();
let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get();
ext.write_int(b'0', Some(2), hour, self.wtr)
}
/// %l
fn fmt_hour12_space(&mut self, ext: &Extension) -> Result<(), Error> {
let mut hour = self
.tm
.hour_ranged()
.ok_or_else(|| err!("requires time to format hour"))?
.get();
let mut hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get();
if hour == 0 {
hour = 12;
} else if hour > 12 {
@ -398,11 +372,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
/// %k
fn fmt_hour24_space(&mut self, ext: &Extension) -> Result<(), Error> {
let hour = self
.tm
.hour_ranged()
.ok_or_else(|| err!("requires time to format hour"))?
.get();
let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get();
ext.write_int(b' ', Some(2), hour, self.wtr)
}
@ -418,11 +388,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
/// %M
fn fmt_minute(&mut self, ext: &Extension) -> Result<(), Error> {
let minute = self
.tm
.minute
.ok_or_else(|| err!("requires time to format minute"))?
.get();
let minute = self.tm.minute.ok_or(FE::RequiresTime)?.get();
ext.write_int(b'0', Some(2), minute, self.wtr)
}
@ -431,8 +397,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let month = self
.tm
.month
.or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
.ok_or_else(|| err!("requires date to format month"))?
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.month_ranged()),
)
.ok_or(FE::RequiresDate)?
.get();
ext.write_int(b'0', Some(2), month, self.wtr)
}
@ -442,8 +411,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let month = self
.tm
.month
.or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
.ok_or_else(|| err!("requires date to format month"))?;
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.month_ranged()),
)
.ok_or(FE::RequiresDate)?;
ext.write_str(Case::AsIs, month_name_full(month), self.wtr)
}
@ -452,20 +424,18 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let month = self
.tm
.month
.or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
.ok_or_else(|| err!("requires date to format month"))?;
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.month_ranged()),
)
.ok_or(FE::RequiresDate)?;
ext.write_str(Case::AsIs, month_name_abbrev(month), self.wtr)
}
/// %Q
fn fmt_iana_nocolon(&mut self) -> Result<(), Error> {
let Some(iana) = self.tm.iana_time_zone() else {
let offset = self.tm.offset.ok_or_else(|| {
err!(
"requires IANA time zone identifier or time \
zone offset, but none were present"
)
})?;
let offset = self.tm.offset.ok_or(FE::RequiresTimeZoneOrOffset)?;
return write_offset(offset, false, true, false, &mut self.wtr);
};
self.wtr.write_str(iana)?;
@ -475,12 +445,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
/// %:Q
fn fmt_iana_colon(&mut self) -> Result<(), Error> {
let Some(iana) = self.tm.iana_time_zone() else {
let offset = self.tm.offset.ok_or_else(|| {
err!(
"requires IANA time zone identifier or time \
zone offset, but none were present"
)
})?;
let offset = self.tm.offset.ok_or(FE::RequiresTimeZoneOrOffset)?;
return write_offset(offset, true, true, false, &mut self.wtr);
};
self.wtr.write_str(iana)?;
@ -489,62 +454,44 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
/// %z
fn fmt_offset_nocolon(&mut self) -> Result<(), Error> {
let offset = self.tm.offset.ok_or_else(|| {
err!("requires offset to format time zone offset")
})?;
let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
write_offset(offset, false, true, false, self.wtr)
}
/// %:z
fn fmt_offset_colon(&mut self) -> Result<(), Error> {
let offset = self.tm.offset.ok_or_else(|| {
err!("requires offset to format time zone offset")
})?;
let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
write_offset(offset, true, true, false, self.wtr)
}
/// %::z
fn fmt_offset_colon2(&mut self) -> Result<(), Error> {
let offset = self.tm.offset.ok_or_else(|| {
err!("requires offset to format time zone offset")
})?;
let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
write_offset(offset, true, true, true, self.wtr)
}
/// %:::z
fn fmt_offset_colon3(&mut self) -> Result<(), Error> {
let offset = self.tm.offset.ok_or_else(|| {
err!("requires offset to format time zone offset")
})?;
let offset = self.tm.offset.ok_or(FE::RequiresOffset)?;
write_offset(offset, true, false, false, self.wtr)
}
/// %S
fn fmt_second(&mut self, ext: &Extension) -> Result<(), Error> {
let second = self
.tm
.second
.ok_or_else(|| err!("requires time to format second"))?
.get();
let second = self.tm.second.ok_or(FE::RequiresTime)?.get();
ext.write_int(b'0', Some(2), second, self.wtr)
}
/// %s
fn fmt_timestamp(&mut self, ext: &Extension) -> Result<(), Error> {
let timestamp = self.tm.to_timestamp().map_err(|_| {
err!(
"requires instant (a date, time and offset) \
to format Unix timestamp",
)
})?;
let timestamp =
self.tm.to_timestamp().map_err(|_| FE::RequiresInstant)?;
ext.write_int(b' ', None, timestamp.as_second(), self.wtr)
}
/// %f
fn fmt_fractional(&mut self, ext: &Extension) -> Result<(), Error> {
let subsec = self.tm.subsec.ok_or_else(|| {
err!("requires time to format subsecond nanoseconds")
})?;
let subsec = self.tm.subsec.ok_or(FE::RequiresTime)?;
let subsec = i32::from(subsec).unsigned_abs();
// For %f, we always want to emit at least one digit. The only way we
// wouldn't is if our fractional component is zero. One exception to
@ -553,7 +500,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
// but this seems very odd. And an empty string cannot be parsed by
// `%f`.
if ext.width == Some(0) {
return Err(err!("zero precision with %f is not allowed"));
return Err(Error::from(FE::ZeroPrecisionFloat));
}
if subsec == 0 && ext.width.is_none() {
self.wtr.write_str("0")?;
@ -577,11 +524,9 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
/// %N
fn fmt_nanoseconds(&mut self, ext: &Extension) -> Result<(), Error> {
let subsec = self.tm.subsec.ok_or_else(|| {
err!("requires time to format subsecond nanoseconds")
})?;
let subsec = self.tm.subsec.ok_or(FE::RequiresTime)?;
if ext.width == Some(0) {
return Err(err!("zero precision with %N is not allowed"));
return Err(Error::from(FE::ZeroPrecisionNano));
}
let subsec = i32::from(subsec).unsigned_abs();
// Since `%N` is actually an alias for `%9f`, when the precision
@ -596,14 +541,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
/// %Z
fn fmt_tzabbrev(&mut self, ext: &Extension) -> Result<(), Error> {
let tz =
self.tm.tz.as_ref().ok_or_else(|| {
err!("requires time zone in broken down time")
})?;
let ts = self
.tm
.to_timestamp()
.context("requires timestamp in broken down time")?;
let tz = self.tm.tz.as_ref().ok_or(FE::RequiresTimeZone)?;
let ts = self.tm.to_timestamp().map_err(|_| FE::RequiresInstant)?;
let oinfo = tz.to_offset_info(ts);
ext.write_str(Case::Upper, oinfo.abbreviation(), self.wtr)
}
@ -613,8 +552,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let weekday = self
.tm
.weekday
.or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
.ok_or_else(|| err!("requires date to format weekday"))?;
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.weekday()),
)
.ok_or(FE::RequiresDate)?;
ext.write_str(Case::AsIs, weekday_name_full(weekday), self.wtr)
}
@ -623,8 +565,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let weekday = self
.tm
.weekday
.or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
.ok_or_else(|| err!("requires date to format weekday"))?;
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.weekday()),
)
.ok_or(FE::RequiresDate)?;
ext.write_str(Case::AsIs, weekday_name_abbrev(weekday), self.wtr)
}
@ -633,8 +578,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let weekday = self
.tm
.weekday
.or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
.ok_or_else(|| err!("requires date to format weekday number"))?;
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.weekday()),
)
.ok_or(FE::RequiresDate)?;
ext.write_int(b' ', None, weekday.to_monday_one_offset(), self.wtr)
}
@ -643,8 +591,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let weekday = self
.tm
.weekday
.or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
.ok_or_else(|| err!("requires date to format weekday number"))?;
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.weekday()),
)
.ok_or(FE::RequiresDate)?;
ext.write_int(b' ', None, weekday.to_sunday_zero_offset(), self.wtr)
}
@ -658,17 +609,19 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
.tm
.day_of_year
.map(|day| day.get())
.or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
.ok_or_else(|| {
err!("requires date to format Sunday-based week number")
})?;
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.day_of_year()),
)
.ok_or(FE::RequiresDate)?;
let weekday = self
.tm
.weekday
.or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
.ok_or_else(|| {
err!("requires date to format Sunday-based week number")
})?
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.weekday()),
)
.ok_or(FE::RequiresDate)?
.to_sunday_zero_offset();
// Example: 2025-01-05 is the first Sunday in 2025, and thus the start
// of week 1. This means that 2025-01-04 (Saturday) is in week 0.
@ -684,12 +637,16 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let weeknum = self
.tm
.iso_week
.or_else(|| {
self.tm.to_date().ok().map(|d| d.iso_week_date().week_ranged())
})
.ok_or_else(|| {
err!("requires date to format ISO 8601 week number")
})?;
.or_else(
#[inline(never)]
|| {
self.tm
.to_date()
.ok()
.map(|d| d.iso_week_date().week_ranged())
},
)
.ok_or(FE::RequiresDate)?;
ext.write_int(b'0', Some(2), weeknum, self.wtr)
}
@ -703,17 +660,19 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
.tm
.day_of_year
.map(|day| day.get())
.or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
.ok_or_else(|| {
err!("requires date to format Monday-based week number")
})?;
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.day_of_year()),
)
.ok_or(FE::RequiresDate)?;
let weekday = self
.tm
.weekday
.or_else(|| self.tm.to_date().ok().map(|d| d.weekday()))
.ok_or_else(|| {
err!("requires date to format Monday-based week number")
})?
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.weekday()),
)
.ok_or(FE::RequiresDate)?
.to_sunday_zero_offset();
// Example: 2025-01-06 is the first Monday in 2025, and thus the start
// of week 1. This means that 2025-01-05 (Sunday) is in week 0.
@ -729,8 +688,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let year = self
.tm
.year
.or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
.ok_or_else(|| err!("requires date to format year"))?
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.year_ranged()),
)
.ok_or(FE::RequiresDate)?
.get();
ext.write_int(b'0', Some(4), year, self.wtr)
}
@ -740,8 +702,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let year = self
.tm
.year
.or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
.ok_or_else(|| err!("requires date to format year (2-digit)"))?
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.year_ranged()),
)
.ok_or(FE::RequiresDate)?
.get();
let year = year % 100;
ext.write_int(b'0', Some(2), year, self.wtr)
@ -752,8 +717,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let year = self
.tm
.year
.or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
.ok_or_else(|| err!("requires date to format century (2-digit)"))?
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.year_ranged()),
)
.ok_or(FE::RequiresDate)?
.get();
let century = year / 100;
ext.write_int(b' ', None, century, self.wtr)
@ -764,12 +732,16 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let year = self
.tm
.iso_week_year
.or_else(|| {
self.tm.to_date().ok().map(|d| d.iso_week_date().year_ranged())
})
.ok_or_else(|| {
err!("requires date to format ISO 8601 week-based year")
})?
.or_else(
#[inline(never)]
|| {
self.tm
.to_date()
.ok()
.map(|d| d.iso_week_date().year_ranged())
},
)
.ok_or(FE::RequiresDate)?
.get();
ext.write_int(b'0', Some(4), year, self.wtr)
}
@ -779,15 +751,16 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let year = self
.tm
.iso_week_year
.or_else(|| {
self.tm.to_date().ok().map(|d| d.iso_week_date().year_ranged())
})
.ok_or_else(|| {
err!(
"requires date to format \
ISO 8601 week-based year (2-digit)"
)
})?
.or_else(
#[inline(never)]
|| {
self.tm
.to_date()
.ok()
.map(|d| d.iso_week_date().year_ranged())
},
)
.ok_or(FE::RequiresDate)?
.get();
let year = year % 100;
ext.write_int(b'0', Some(2), year, self.wtr)
@ -798,8 +771,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
let month = self
.tm
.month
.or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
.ok_or_else(|| err!("requires date to format quarter"))?
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.month_ranged()),
)
.ok_or(FE::RequiresDate)?
.get();
let quarter = match month {
1..=3 => 1,
@ -817,8 +793,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> {
.tm
.day_of_year
.map(|day| day.get())
.or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year()))
.ok_or_else(|| err!("requires date to format day of year"))?;
.or_else(
#[inline(never)]
|| self.tm.to_date().ok().map(|d| d.day_of_year()),
)
.ok_or(FE::RequiresDate)?;
ext.write_int(b'0', Some(3), day, self.wtr)
}
@ -871,7 +850,7 @@ fn write_offset<W: Write>(
second: bool,
wtr: &mut W,
) -> Result<(), Error> {
static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2);
let hours = offset.part_hours_ranged().abs().get();
let minutes = offset.part_minutes_ranged().abs().get();
@ -967,7 +946,7 @@ impl Extension {
self.width.or(pad_width)
};
let mut formatter = DecimalFormatter::new().padding_byte(pad_byte);
let mut formatter = IntegerFormatter::new().padding_byte(pad_byte);
if let Some(width) = pad_width {
formatter = formatter.padding(width);
}
@ -1508,7 +1487,7 @@ mod tests {
let dt = date(2025, 1, 20).at(13, 9, 0, 0);
insta::assert_snapshot!(
f("%s", dt),
@"strftime formatting failed: %s failed: requires instant (a date, time and offset) to format Unix timestamp",
@"strftime formatting failed: %s failed: requires instant (a timestamp or a date, time and offset)",
);
}
@ -1521,7 +1500,7 @@ mod tests {
);
insta::assert_snapshot!(
format(b"abc %F \xFFxyz", d).unwrap_err(),
@r#"strftime formatting failed: found invalid UTF-8 byte "\xff" in format string (format strings must be valid UTF-8)"#,
@"strftime formatting failed: invalid format string, it must be valid UTF-8",
);
}

View file

@ -85,7 +85,7 @@ use jiff::{civil::time, fmt::strtime};
let t = time(23, 59, 59, 0);
assert_eq!(
strtime::format("%Y", t).unwrap_err().to_string(),
"strftime formatting failed: %Y failed: requires date to format year",
"strftime formatting failed: %Y failed: requires date to format",
);
```
@ -275,7 +275,7 @@ The following things are currently unsupported:
use crate::{
civil::{Date, DateTime, ISOWeekDate, Time, Weekday},
error::{err, ErrorContext},
error::{fmt::strtime::Error as E, ErrorContext},
fmt::{
strtime::{format::Formatter, parse::Parser},
Write,
@ -555,7 +555,7 @@ impl<C> Config<C> {
/// assert_eq!(
/// tm.to_string("%F %z").unwrap_err().to_string(),
/// "strftime formatting failed: %z failed: \
/// requires offset to format time zone offset",
/// requires time zone offset",
/// );
///
/// // Now enable lenient mode:
@ -946,13 +946,9 @@ impl BrokenDownTime {
fn parse_mono(fmt: &[u8], inp: &[u8]) -> Result<BrokenDownTime, Error> {
let mut pieces = BrokenDownTime::default();
let mut p = Parser { fmt, inp, tm: &mut pieces };
p.parse().context("strptime parsing failed")?;
p.parse().context(E::FailedStrptime)?;
if !p.inp.is_empty() {
return Err(err!(
"strptime expects to consume the entire input, but \
{remaining:?} remains unparsed",
remaining = escape::Bytes(p.inp),
));
return Err(Error::from(E::unconsumed(p.inp)));
}
Ok(pieces)
}
@ -1055,7 +1051,7 @@ impl BrokenDownTime {
let mkoffset = util::parse::offseter(inp);
let mut pieces = BrokenDownTime::default();
let mut p = Parser { fmt, inp, tm: &mut pieces };
p.parse().context("strptime parsing failed")?;
p.parse().context(E::FailedStrptime)?;
let remainder = mkoffset(p.inp);
Ok((pieces, remainder))
}
@ -1158,7 +1154,7 @@ impl BrokenDownTime {
) -> Result<(), Error> {
let fmt = format.as_ref();
let mut formatter = Formatter { config, fmt, tm: self, wtr };
formatter.format().context("strftime formatting failed")?;
formatter.format().context(E::FailedStrftime)?;
Ok(())
}
@ -1337,10 +1333,11 @@ impl BrokenDownTime {
/// )?.to_zoned();
/// assert_eq!(
/// result.unwrap_err().to_string(),
/// "datetime 2024-07-14T21:14:00 could not resolve to a \
/// timestamp since 'reject' conflict resolution was chosen, \
/// and because datetime has offset -05, but the time zone \
/// US/Eastern for the given datetime unambiguously has offset -04",
/// "datetime could not resolve to a timestamp since `reject` \
/// conflict resolution was chosen, and because \
/// datetime has offset `-05`, \
/// but the time zone `US/Eastern` for the given datetime \
/// unambiguously has offset `-04`",
/// );
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
@ -1435,26 +1432,18 @@ impl BrokenDownTime {
if let Some(ts) = self.timestamp {
return Ok(ts.to_zoned(TimeZone::unknown()));
}
Err(err!(
"either offset (from %z) or IANA time zone identifier \
(from %Q) is required for parsing zoned datetime",
))
Err(Error::from(E::ZonedOffsetOrTz))
}
(Some(offset), None) => {
let ts = match self.timestamp {
Some(ts) => ts,
None => {
let dt = self.to_datetime().context(
"datetime required to parse zoned datetime",
)?;
let ts =
offset.to_timestamp(dt).with_context(|| {
err!(
"parsed datetime {dt} and offset {offset}, \
but combining them into a zoned datetime \
is outside Jiff's supported timestamp range",
)
})?;
let dt = self
.to_datetime()
.context(E::RequiredDateTimeForZoned)?;
let ts = offset
.to_timestamp(dt)
.context(E::RangeTimestamp)?;
ts
}
};
@ -1465,9 +1454,9 @@ impl BrokenDownTime {
match self.timestamp {
Some(ts) => Ok(ts.to_zoned(tz)),
None => {
let dt = self.to_datetime().context(
"datetime required to parse zoned datetime",
)?;
let dt = self
.to_datetime()
.context(E::RequiredDateTimeForZoned)?;
Ok(tz.to_zoned(dt)?)
}
}
@ -1478,19 +1467,17 @@ impl BrokenDownTime {
Some(ts) => {
let zdt = ts.to_zoned(tz);
if zdt.offset() != offset {
return Err(err!(
"parsed time zone offset `{offset}`, but \
offset from timestamp `{ts}` for time zone \
`{iana}` is `{got}`",
got = zdt.offset(),
));
return Err(Error::from(E::MismatchOffset {
parsed: offset,
got: zdt.offset(),
}));
}
Ok(zdt)
}
None => {
let dt = self.to_datetime().context(
"datetime required to parse zoned datetime",
)?;
let dt = self
.to_datetime()
.context(E::RequiredDateTimeForZoned)?;
let azdt =
OffsetConflict::Reject.resolve(dt, offset, tz)?;
// Guaranteed that if OffsetConflict::Reject doesn't
@ -1584,31 +1571,20 @@ impl BrokenDownTime {
/// ```
#[inline]
pub fn to_timestamp(&self) -> Result<Timestamp, Error> {
// Previously, I had used this as the "fast path" and
// put the conversion code below into a cold unlineable
// function. But this "fast path" is actually the unusual
// case. It's rare to parse a timestamp (as an integer
// number of seconds since the Unix epoch) directly.
// So the code below, while bigger, is the common case.
// So it probably makes sense to keep it inlined.
if let Some(timestamp) = self.timestamp() {
return Ok(timestamp);
}
let dt = self
.to_datetime()
.context("datetime required to parse timestamp")?;
let offset =
self.to_offset().context("offset required to parse timestamp")?;
offset.to_timestamp(dt).with_context(|| {
err!(
"parsed datetime {dt} and offset {offset}, \
but combining them into a timestamp is outside \
Jiff's supported timestamp range",
)
})
}
#[inline]
fn to_offset(&self) -> Result<Offset, Error> {
let Some(offset) = self.offset else {
return Err(err!(
"parsing format did not include time zone offset directive",
));
};
Ok(offset)
let dt =
self.to_datetime().context(E::RequiredDateTimeForTimestamp)?;
let offset = self.offset.ok_or(E::RequiredOffsetForTimestamp)?;
offset.to_timestamp(dt).context(E::RangeTimestamp)
}
/// Extracts a civil datetime from this broken down time.
@ -1638,10 +1614,8 @@ impl BrokenDownTime {
/// ```
#[inline]
pub fn to_datetime(&self) -> Result<DateTime, Error> {
let date =
self.to_date().context("date required to parse datetime")?;
let time =
self.to_time().context("time required to parse datetime")?;
let date = self.to_date().context(E::RequiredDateForDateTime)?;
let time = self.to_time().context(E::RequiredTimeForDateTime)?;
Ok(DateTime::from_parts(date, time))
}
@ -1660,8 +1634,12 @@ impl BrokenDownTime {
/// # Errors
///
/// This returns an error if there weren't enough components to construct
/// a civil date. This means there must be at least a year and a way to
/// determine the day of the year.
/// a civil date, or if the components don't form into a valid date. This
/// means there must be at least a year and a way to determine the day of
/// the year.
///
/// This will also return an error when there is a weekday component
/// set to a value inconsistent with the date returned.
///
/// It's okay if there are more units than are needed to construct a civil
/// datetime. For example, if this broken down time contains a civil time,
@ -1681,42 +1659,62 @@ impl BrokenDownTime {
/// ```
#[inline]
pub fn to_date(&self) -> Result<Date, Error> {
let Some(year) = self.year else {
// The Gregorian year and ISO week year may be parsed separately.
// That is, they are two different fields. So if the Gregorian year
// is absent, we might still have an ISO 8601 week date.
if let Some(date) = self.to_date_from_iso()? {
return Ok(date);
#[cold]
#[inline(never)]
fn to_date(tm: &BrokenDownTime) -> Result<Date, Error> {
let Some(year) = tm.year else {
// The Gregorian year and ISO week year may be parsed
// separately. That is, they are two different fields. So if
// the Gregorian year is absent, we might still have an ISO
// 8601 week date.
if let Some(date) = tm.to_date_from_iso()? {
return Ok(date);
}
return Err(Error::from(E::RequiredYearForDate));
};
let mut date = tm.to_date_from_gregorian(year)?;
if date.is_none() {
date = tm.to_date_from_iso()?;
}
return Err(err!("missing year, date cannot be created"));
};
let mut date = self.to_date_from_gregorian(year)?;
if date.is_none() {
date = self.to_date_from_iso()?;
}
if date.is_none() {
date = self.to_date_from_day_of_year(year)?;
}
if date.is_none() {
date = self.to_date_from_week_sun(year)?;
}
if date.is_none() {
date = self.to_date_from_week_mon(year)?;
}
let Some(date) = date else {
return Err(err!(
"a month/day, day-of-year or week date must be \
present to create a date, but none were found",
));
if date.is_none() {
date = tm.to_date_from_day_of_year(year)?;
}
if date.is_none() {
date = tm.to_date_from_week_sun(year)?;
}
if date.is_none() {
date = tm.to_date_from_week_mon(year)?;
}
let Some(date) = date else {
return Err(Error::from(E::RequiredSomeDayForDate));
};
if let Some(weekday) = tm.weekday {
if weekday != date.weekday() {
return Err(Error::from(E::MismatchWeekday {
parsed: weekday,
got: date.weekday(),
}));
}
}
Ok(date)
}
// The common case is a simple Gregorian date.
// We put the rest behind a non-inlineable function
// to avoid code bloat for very uncommon cases.
let (Some(year), Some(month), Some(day)) =
(self.year, self.month, self.day)
else {
return to_date(self);
};
let date =
Date::new_ranged(year, month, day).context(E::InvalidDate)?;
if let Some(weekday) = self.weekday {
if weekday != date.weekday() {
return Err(err!(
"parsed weekday {weekday} does not match \
weekday {got} from parsed date {date}",
weekday = weekday_name_full(weekday),
got = weekday_name_full(date.weekday()),
));
return Err(Error::from(E::MismatchWeekday {
parsed: weekday,
got: date.weekday(),
}));
}
}
Ok(date)
@ -1730,7 +1728,7 @@ impl BrokenDownTime {
let (Some(month), Some(day)) = (self.month, self.day) else {
return Ok(None);
};
Ok(Some(Date::new_ranged(year, month, day).context("invalid date")?))
Ok(Some(Date::new_ranged(year, month, day).context(E::InvalidDate)?))
}
#[inline]
@ -1746,7 +1744,7 @@ impl BrokenDownTime {
.with()
.day_of_year(doy.get())
.build()
.context("invalid date")?
.context(E::InvalidDate)?
}))
}
@ -1757,8 +1755,8 @@ impl BrokenDownTime {
else {
return Ok(None);
};
let wd = ISOWeekDate::new_ranged(y, w, d)
.context("invalid ISO 8601 week date")?;
let wd =
ISOWeekDate::new_ranged(y, w, d).context(E::InvalidISOWeekDate)?;
Ok(Some(wd.date()))
}
@ -1773,28 +1771,20 @@ impl BrokenDownTime {
let week = i16::from(week);
let wday = i16::from(weekday.to_sunday_zero_offset());
let first_of_year = Date::new_ranged(year, C(1).rinto(), C(1).rinto())
.context("invalid date")?;
.context(E::InvalidDate)?;
let first_sunday = first_of_year
.nth_weekday_of_month(1, Weekday::Sunday)
.map(|d| d.day_of_year())
.context("invalid date")?;
.context(E::InvalidDate)?;
let doy = if week == 0 {
let days_before_first_sunday = 7 - wday;
let doy = first_sunday
.checked_sub(days_before_first_sunday)
.ok_or_else(|| {
err!(
"weekday `{weekday:?}` is not valid for \
Sunday based week number `{week}` \
in year `{year}`",
)
})?;
.ok_or(E::InvalidWeekdaySunday { got: weekday })?;
if doy == 0 {
return Err(err!(
"weekday `{weekday:?}` is not valid for \
Sunday based week number `{week}` \
in year `{year}`",
));
return Err(Error::from(E::InvalidWeekdaySunday {
got: weekday,
}));
}
doy
} else {
@ -1806,7 +1796,7 @@ impl BrokenDownTime {
.with()
.day_of_year(doy)
.build()
.context("invalid date")?;
.context(E::InvalidDate)?;
Ok(Some(date))
}
@ -1821,28 +1811,20 @@ impl BrokenDownTime {
let week = i16::from(week);
let wday = i16::from(weekday.to_monday_zero_offset());
let first_of_year = Date::new_ranged(year, C(1).rinto(), C(1).rinto())
.context("invalid date")?;
.context(E::InvalidDate)?;
let first_monday = first_of_year
.nth_weekday_of_month(1, Weekday::Monday)
.map(|d| d.day_of_year())
.context("invalid date")?;
.context(E::InvalidDate)?;
let doy = if week == 0 {
let days_before_first_monday = 7 - wday;
let doy = first_monday
.checked_sub(days_before_first_monday)
.ok_or_else(|| {
err!(
"weekday `{weekday:?}` is not valid for \
Monday based week number `{week}` \
in year `{year}`",
)
})?;
.ok_or(E::InvalidWeekdayMonday { got: weekday })?;
if doy == 0 {
return Err(err!(
"weekday `{weekday:?}` is not valid for \
Monday based week number `{week}` \
in year `{year}`",
));
return Err(Error::from(E::InvalidWeekdayMonday {
got: weekday,
}));
}
doy
} else {
@ -1854,7 +1836,7 @@ impl BrokenDownTime {
.with()
.day_of_year(doy)
.build()
.context("invalid date")?;
.context(E::InvalidDate)?;
Ok(Some(date))
}
@ -1936,52 +1918,28 @@ impl BrokenDownTime {
pub fn to_time(&self) -> Result<Time, Error> {
let Some(hour) = self.hour_ranged() else {
if self.minute.is_some() {
return Err(err!(
"parsing format did not include hour directive, \
but did include minute directive (cannot have \
smaller time units with bigger time units missing)",
));
return Err(Error::from(E::MissingTimeHourForMinute));
}
if self.second.is_some() {
return Err(err!(
"parsing format did not include hour directive, \
but did include second directive (cannot have \
smaller time units with bigger time units missing)",
));
return Err(Error::from(E::MissingTimeHourForSecond));
}
if self.subsec.is_some() {
return Err(err!(
"parsing format did not include hour directive, \
but did include fractional second directive (cannot have \
smaller time units with bigger time units missing)",
));
return Err(Error::from(E::MissingTimeHourForFractional));
}
return Ok(Time::midnight());
};
let Some(minute) = self.minute else {
if self.second.is_some() {
return Err(err!(
"parsing format did not include minute directive, \
but did include second directive (cannot have \
smaller time units with bigger time units missing)",
));
return Err(Error::from(E::MissingTimeMinuteForSecond));
}
if self.subsec.is_some() {
return Err(err!(
"parsing format did not include minute directive, \
but did include fractional second directive (cannot have \
smaller time units with bigger time units missing)",
));
return Err(Error::from(E::MissingTimeMinuteForFractional));
}
return Ok(Time::new_ranged(hour, C(0), C(0), C(0)));
};
let Some(second) = self.second else {
if self.subsec.is_some() {
return Err(err!(
"parsing format did not include second directive, \
but did include fractional second directive (cannot have \
smaller time units with bigger time units missing)",
));
return Err(Error::from(E::MissingTimeSecondForFractional));
}
return Ok(Time::new_ranged(hour, minute, C(0), C(0)));
};
@ -2121,8 +2079,8 @@ impl BrokenDownTime {
/// // An error only occurs when you try to extract a date:
/// assert_eq!(
/// tm.to_date().unwrap_err().to_string(),
/// "invalid date: day-of-year=366 is out of range \
/// for year=2023, must be in range 1..=365",
/// "invalid date: number of days for `2023` is invalid, \
/// must be in range `1..=365`",
/// );
/// // But parsing a value that is always illegal will
/// // result in an error:
@ -3479,7 +3437,7 @@ impl Extension {
fn parse_flag<'i>(
fmt: &'i [u8],
) -> Result<(Option<Flag>, &'i [u8]), Error> {
let byte = fmt[0];
let (&byte, tail) = fmt.split_first().unwrap();
let flag = match byte {
b'_' => Flag::PadSpace,
b'0' => Flag::PadZero,
@ -3488,15 +3446,12 @@ impl Extension {
b'#' => Flag::Swapcase,
_ => return Ok((None, fmt)),
};
let fmt = &fmt[1..];
if fmt.is_empty() {
return Err(err!(
"expected to find specifier directive after flag \
{byte:?}, but found end of format string",
byte = escape::Byte(byte),
));
if tail.is_empty() {
return Err(Error::from(E::ExpectedDirectiveAfterFlag {
flag: byte,
}));
}
Ok((Some(flag), fmt))
Ok((Some(flag), tail))
}
/// Parses an optional width that comes after a (possibly absent) flag and
@ -3520,16 +3475,10 @@ impl Extension {
return Ok((None, fmt));
}
let (digits, fmt) = util::parse::split(fmt, digits).unwrap();
let width = util::parse::i64(digits)
.context("failed to parse conversion specifier width")?;
let width = u8::try_from(width).map_err(|_| {
err!("{width} is too big, max is {max}", max = u8::MAX)
})?;
let width = util::parse::i64(digits).context(E::FailedWidth)?;
let width = u8::try_from(width).map_err(|_| E::RangeWidth)?;
if fmt.is_empty() {
return Err(err!(
"expected to find specifier directive after width \
{width}, but found end of format string",
));
return Err(Error::from(E::ExpectedDirectiveAfterWidth));
}
Ok((Some(width), fmt))
}
@ -3549,10 +3498,7 @@ impl Extension {
}
let fmt = &fmt[usize::from(colons)..];
if colons > 0 && fmt.is_empty() {
return Err(err!(
"expected to find specifier directive after {colons} colons, \
but found end of format string",
));
return Err(Error::from(E::ExpectedDirectiveAfterColons));
}
Ok((u8::try_from(colons).unwrap(), fmt))
}

File diff suppressed because it is too large Load diff

View file

@ -171,7 +171,7 @@ There is some more [background on Temporal's format] available.
*/
use crate::{
civil,
civil::{self, ISOWeekDate},
error::Error,
fmt::Write,
span::Span,
@ -320,13 +320,12 @@ impl DateTimeParser {
/// );
/// assert_eq!(
/// result.unwrap_err().to_string(),
/// "parsing \"2006-04-02T02:30-05[America/Indiana/Vevay]\" failed: \
/// datetime 2006-04-02T02:30:00 could not resolve to timestamp \
/// since 'reject' conflict resolution was chosen, and because \
/// datetime has offset -05, but the time zone America/Indiana/Vevay \
/// for the given datetime falls in a gap \
/// (between offsets -05 and -04), \
/// and all offsets for a gap are regarded as invalid",
/// "datetime could not resolve to timestamp since `reject` \
/// conflict resolution was chosen, and because datetime \
/// has offset `-05`, but the time zone `America/Indiana/Vevay` \
/// for the given datetime falls in a gap (between offsets \
/// `-05` and `-04`), and all offsets for a gap are \
/// regarded as invalid",
/// );
/// ```
///
@ -410,11 +409,10 @@ impl DateTimeParser {
/// );
/// assert_eq!(
/// result.unwrap_err().to_string(),
/// "parsing \"2025-06-20T17:30+00[America/New_York]\" failed: \
/// datetime 2025-06-20T17:30:00 could not resolve to a timestamp \
/// since 'reject' conflict resolution was chosen, and because \
/// datetime has offset +00, but the time zone America/New_York \
/// for the given datetime unambiguously has offset -04",
/// "datetime could not resolve to a timestamp since `reject` \
/// conflict resolution was chosen, and because datetime has \
/// offset `+00`, but the time zone `America/New_York` \
/// for the given datetime unambiguously has offset `-04`",
/// );
/// ```
///
@ -1027,9 +1025,8 @@ impl DateTimeParser {
/// // Normally this operation will fail.
/// assert_eq!(
/// PARSER.parse_zoned(timestamp).unwrap_err().to_string(),
/// "failed to find time zone in square brackets in \
/// \"2025-01-02T15:13-05\", which is required for \
/// parsing a zoned instant",
/// "failed to find time zone annotation in square brackets, \
/// which is required for parsing a zoned datetime",
/// );
///
/// // But you can work-around this with `Pieces`, which gives you direct
@ -1073,8 +1070,8 @@ impl DateTimeParser {
///
/// assert_eq!(
/// PARSER.parse_date("2024-03-10T00:00:00Z").unwrap_err().to_string(),
/// "cannot parse civil date from string with a Zulu offset, \
/// parse as a `Timestamp` and convert to a civil date instead",
/// "cannot parse civil date/time from string with a Zulu offset, \
/// parse as a `jiff::Timestamp` first and convert to a civil date/time instead",
/// );
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
@ -1110,6 +1107,22 @@ impl DateTimeParser {
let pieces = parsed.to_pieces()?;
Ok(pieces)
}
/// Parses an ISO 8601 week date.
///
/// This isn't exported because it's not clear that it's worth it.
/// Moreover, this isn't part of the Temporal spec, so it's a little odd
/// to have it here. If this really needs to be exported, we probably need
/// a new module that wraps and re-uses this module's internal parser to
/// avoid too much code duplication.
pub(crate) fn parse_iso_week_date<I: AsRef<[u8]>>(
&self,
input: I,
) -> Result<ISOWeekDate, Error> {
let input = input.as_ref();
let wd = self.p.parse_iso_week_date(input)?.into_full()?;
Ok(wd)
}
}
/// A printer for Temporal datetimes.
@ -1964,6 +1977,25 @@ impl DateTimePrinter {
) -> Result<(), Error> {
self.p.print_pieces(pieces, wtr)
}
/// Prints an ISO 8601 week date.
///
/// This isn't exported because it's not clear that it's worth it.
/// Moreover, this isn't part of the Temporal spec, so it's a little odd
/// to have it here. But it's very convenient to have the ISO 8601 week
/// date parser in this module, and so we stick the printer here along
/// with it.
///
/// Note that this printer will use `w` when `lowercase` is enabled. (It
/// isn't possible to enable this using the current Jiff public API. But
/// it's probably fine.)
pub(crate) fn print_iso_week_date<W: Write>(
&self,
iso_week_date: &ISOWeekDate,
wtr: W,
) -> Result<(), Error> {
self.p.print_iso_week_date(iso_week_date, wtr)
}
}
/// A parser for Temporal durations.
@ -2455,7 +2487,7 @@ mod tests {
);
insta::assert_snapshot!(
DateTimeParser::new().parse_date("-000000-01-01").unwrap_err(),
@r###"failed to parse year in date "-000000-01-01": year zero must be written without a sign or a positive sign, but not a negative sign"###,
@"failed to parse year in date: year zero must be written without a sign or a positive sign, but not a negative sign",
);
}

File diff suppressed because it is too large Load diff

View file

@ -146,9 +146,8 @@ use crate::{
///
/// assert_eq!(
/// "2025-01-03T17:28-05".parse::<Zoned>().unwrap_err().to_string(),
/// "failed to find time zone in square brackets in \
/// \"2025-01-03T17:28-05\", which is required for \
/// parsing a zoned instant",
/// "failed to find time zone annotation in square brackets, \
/// which is required for parsing a zoned datetime",
/// );
/// ```
///

View file

@ -1,9 +1,9 @@
use crate::{
civil::{Date, DateTime, Time},
error::{err, Error},
civil::{Date, DateTime, ISOWeekDate, Time},
error::{fmt::temporal::Error as E, Error},
fmt::{
temporal::{Pieces, PiecesOffset, TimeZoneAnnotationKind},
util::{DecimalFormatter, FractionalFormatter},
util::{FractionalFormatter, IntegerFormatter},
Write, WriteExt,
},
span::Span,
@ -108,11 +108,11 @@ impl DateTimePrinter {
date: &Date,
mut wtr: W,
) -> Result<(), Error> {
static FMT_YEAR_POSITIVE: DecimalFormatter =
DecimalFormatter::new().padding(4);
static FMT_YEAR_NEGATIVE: DecimalFormatter =
DecimalFormatter::new().padding(6);
static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
static FMT_YEAR_POSITIVE: IntegerFormatter =
IntegerFormatter::new().padding(4);
static FMT_YEAR_NEGATIVE: IntegerFormatter =
IntegerFormatter::new().padding(6);
static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2);
if date.year() >= 0 {
wtr.write_int(&FMT_YEAR_POSITIVE, date.year())?;
@ -132,7 +132,7 @@ impl DateTimePrinter {
time: &Time,
mut wtr: W,
) -> Result<(), Error> {
static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2);
static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new();
wtr.write_int(&FMT_TWO, time.hour())?;
@ -197,13 +197,7 @@ impl DateTimePrinter {
//
// Anyway, if you're seeing this error and think there should be a
// different behavior, please file an issue.
Err(err!(
"time zones without IANA identifiers that aren't either \
fixed offsets or a POSIX time zone can't be serialized \
(this typically occurs when this is a system time zone \
derived from `/etc/localtime` on Unix systems that \
isn't symlinked to an entry in `/usr/share/zoneinfo`)",
))
Err(Error::from(E::PrintTimeZoneFailure))
}
pub(super) fn print_pieces<W: Write>(
@ -255,6 +249,34 @@ impl DateTimePrinter {
Ok(())
}
pub(super) fn print_iso_week_date<W: Write>(
&self,
iso_week_date: &ISOWeekDate,
mut wtr: W,
) -> Result<(), Error> {
static FMT_YEAR_POSITIVE: IntegerFormatter =
IntegerFormatter::new().padding(4);
static FMT_YEAR_NEGATIVE: IntegerFormatter =
IntegerFormatter::new().padding(6);
static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2);
static FMT_ONE: IntegerFormatter = IntegerFormatter::new().padding(1);
if iso_week_date.year() >= 0 {
wtr.write_int(&FMT_YEAR_POSITIVE, iso_week_date.year())?;
} else {
wtr.write_int(&FMT_YEAR_NEGATIVE, iso_week_date.year())?;
}
wtr.write_str("-")?;
wtr.write_char(if self.lowercase { 'w' } else { 'W' })?;
wtr.write_int(&FMT_TWO, iso_week_date.week())?;
wtr.write_str("-")?;
wtr.write_int(
&FMT_ONE,
iso_week_date.weekday().to_monday_one_offset(),
)?;
Ok(())
}
/// Formats the given "pieces" offset into the writer given.
fn print_pieces_offset<W: Write>(
&self,
@ -282,7 +304,7 @@ impl DateTimePrinter {
offset: &Offset,
mut wtr: W,
) -> Result<(), Error> {
static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2);
wtr.write_str(if offset.is_negative() { "-" } else { "+" })?;
let mut hours = offset.part_hours_ranged().abs().get();
@ -317,7 +339,7 @@ impl DateTimePrinter {
offset: &Offset,
mut wtr: W,
) -> Result<(), Error> {
static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2);
static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2);
wtr.write_str(if offset.is_negative() { "-" } else { "+" })?;
let hours = offset.part_hours_ranged().abs().get();
@ -405,7 +427,7 @@ impl SpanPrinter {
span: &Span,
mut wtr: W,
) -> Result<(), Error> {
static FMT_INT: DecimalFormatter = DecimalFormatter::new();
static FMT_INT: IntegerFormatter = IntegerFormatter::new();
static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new();
if span.is_negative() {
@ -519,7 +541,7 @@ impl SpanPrinter {
dur: &SignedDuration,
mut wtr: W,
) -> Result<(), Error> {
static FMT_INT: DecimalFormatter = DecimalFormatter::new();
static FMT_INT: IntegerFormatter = IntegerFormatter::new();
static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new();
let mut non_zero_greater_than_second = false;
@ -568,7 +590,7 @@ impl SpanPrinter {
dur: &core::time::Duration,
mut wtr: W,
) -> Result<(), Error> {
static FMT_INT: DecimalFormatter = DecimalFormatter::new();
static FMT_INT: IntegerFormatter = IntegerFormatter::new();
static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new();
let mut non_zero_greater_than_second = false;
@ -620,7 +642,10 @@ impl SpanPrinter {
mod tests {
use alloc::string::String;
use crate::{civil::date, span::ToSpan};
use crate::{
civil::{date, Weekday},
span::ToSpan,
};
use super::*;
@ -922,4 +947,22 @@ mod tests {
@"PT5124095576030431H15.999999999S",
);
}
#[test]
fn print_iso_week_date() {
let p = |d: ISOWeekDate| -> String {
let mut buf = String::new();
DateTimePrinter::new().print_iso_week_date(&d, &mut buf).unwrap();
buf
};
insta::assert_snapshot!(
p(ISOWeekDate::new(2024, 52, Weekday::Monday).unwrap()),
@"2024-W52-1",
);
insta::assert_snapshot!(
p(ISOWeekDate::new(2004, 1, Weekday::Sunday).unwrap()),
@"2004-W01-7",
);
}
}

View file

@ -1,7 +1,7 @@
use crate::{
error::{err, ErrorContext},
error::{fmt::util::Error as E, ErrorContext},
fmt::Parsed,
util::{c::Sign, escape, parse, t},
util::{c::Sign, parse, t},
Error, SignedDuration, Span, Unit,
};
@ -14,52 +14,31 @@ use crate::{
/// faster. We roll our own which is a bit slower, but gets us enough of a win
/// to be satisfied with and with (almost) pure safe code.
///
/// By default, this only includes the sign if it's negative. To always include
/// the sign, set `force_sign` to `true`.
/// This only includes the sign when formatting a negative signed integer.
#[derive(Clone, Copy, Debug)]
pub(crate) struct DecimalFormatter {
force_sign: Option<bool>,
pub(crate) struct IntegerFormatter {
minimum_digits: u8,
padding_byte: u8,
}
impl DecimalFormatter {
/// Creates a new decimal formatter using the default configuration.
pub(crate) const fn new() -> DecimalFormatter {
DecimalFormatter {
force_sign: None,
minimum_digits: 0,
padding_byte: b'0',
}
impl IntegerFormatter {
/// Creates a new integer formatter using the default configuration.
pub(crate) const fn new() -> IntegerFormatter {
IntegerFormatter { minimum_digits: 0, padding_byte: b'0' }
}
/// Format the given value using this configuration as a signed decimal
/// Format the given value using this configuration as a signed integer
/// ASCII number.
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) const fn format_signed(&self, value: i64) -> Decimal {
Decimal::signed(self, value)
pub(crate) const fn format_signed(&self, value: i64) -> Integer {
Integer::signed(self, value)
}
/// Format the given value using this configuration as an unsigned decimal
/// Format the given value using this configuration as an unsigned integer
/// ASCII number.
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) const fn format_unsigned(&self, value: u64) -> Decimal {
Decimal::unsigned(self, value)
}
/// Forces the sign to be rendered, even if it's positive.
///
/// When `zero_is_positive` is true, then a zero value is formatted with a
/// positive sign. Otherwise, it is formatted with a negative sign.
///
/// Regardless of this setting, a sign is never emitted when formatting an
/// unsigned integer.
#[cfg(test)]
pub(crate) const fn force_sign(
self,
zero_is_positive: bool,
) -> DecimalFormatter {
DecimalFormatter { force_sign: Some(zero_is_positive), ..self }
pub(crate) const fn format_unsigned(&self, value: u64) -> Integer {
Integer::unsigned(self, value)
}
/// The minimum number of digits/padding that this number should be
@ -69,181 +48,113 @@ impl DecimalFormatter {
///
/// The minimum number of digits is capped at the maximum number of digits
/// for an i64 value (19) or a u64 value (20).
pub(crate) const fn padding(self, mut digits: u8) -> DecimalFormatter {
if digits > Decimal::MAX_I64_DIGITS {
digits = Decimal::MAX_I64_DIGITS;
pub(crate) const fn padding(self, mut digits: u8) -> IntegerFormatter {
if digits > Integer::MAX_LEN {
digits = Integer::MAX_LEN;
}
DecimalFormatter { minimum_digits: digits, ..self }
IntegerFormatter { minimum_digits: digits, ..self }
}
/// The padding byte to use when `padding` is set.
///
/// The default is `0`.
pub(crate) const fn padding_byte(self, byte: u8) -> DecimalFormatter {
DecimalFormatter { padding_byte: byte, ..self }
pub(crate) const fn padding_byte(self, byte: u8) -> IntegerFormatter {
IntegerFormatter { padding_byte: byte, ..self }
}
/// Returns the minimum number of digits for a signed value.
const fn get_signed_minimum_digits(&self) -> u8 {
if self.minimum_digits <= Decimal::MAX_I64_DIGITS {
/// Returns the minimum number of digits for an integer value.
const fn get_minimum_digits(&self) -> u8 {
if self.minimum_digits <= Integer::MAX_LEN {
self.minimum_digits
} else {
Decimal::MAX_I64_DIGITS
}
}
/// Returns the minimum number of digits for an unsigned value.
const fn get_unsigned_minimum_digits(&self) -> u8 {
if self.minimum_digits <= Decimal::MAX_U64_DIGITS {
self.minimum_digits
} else {
Decimal::MAX_U64_DIGITS
Integer::MAX_LEN
}
}
}
impl Default for DecimalFormatter {
fn default() -> DecimalFormatter {
DecimalFormatter::new()
impl Default for IntegerFormatter {
fn default() -> IntegerFormatter {
IntegerFormatter::new()
}
}
/// A formatted decimal number that can be converted to a sequence of bytes.
/// A formatted integer number that can be converted to a sequence of bytes.
#[derive(Debug)]
pub(crate) struct Decimal {
pub(crate) struct Integer {
buf: [u8; Self::MAX_LEN as usize],
start: u8,
end: u8,
}
impl Decimal {
impl Integer {
/// Discovered via
/// `i64::MIN.to_string().len().max(u64::MAX.to_string().len())`.
const MAX_LEN: u8 = 20;
/// Discovered via `i64::MAX.to_string().len()`.
const MAX_I64_DIGITS: u8 = 19;
/// Discovered via `u64::MAX.to_string().len()`.
const MAX_U64_DIGITS: u8 = 20;
/// Using the given formatter, turn the value given into an unsigned
/// decimal representation using ASCII bytes.
/// integer representation using ASCII bytes.
#[cfg_attr(feature = "perf-inline", inline(always))]
const fn unsigned(
formatter: &DecimalFormatter,
formatter: &IntegerFormatter,
mut value: u64,
) -> Decimal {
let mut decimal = Decimal {
buf: [0; Self::MAX_LEN as usize],
start: Self::MAX_LEN,
end: Self::MAX_LEN,
};
) -> Integer {
let mut integer =
Integer { buf: [0; Self::MAX_LEN as usize], start: Self::MAX_LEN };
loop {
decimal.start -= 1;
integer.start -= 1;
let digit = (value % 10) as u8;
value /= 10;
decimal.buf[decimal.start as usize] = b'0' + digit;
integer.buf[integer.start as usize] = b'0' + digit;
if value == 0 {
break;
}
}
while decimal.len() < formatter.get_unsigned_minimum_digits() {
decimal.start -= 1;
decimal.buf[decimal.start as usize] = formatter.padding_byte;
while integer.len() < formatter.get_minimum_digits() {
integer.start -= 1;
integer.buf[integer.start as usize] = formatter.padding_byte;
}
decimal
integer
}
/// Using the given formatter, turn the value given into a signed decimal
/// Using the given formatter, turn the value given into a signed integer
/// representation using ASCII bytes.
#[cfg_attr(feature = "perf-inline", inline(always))]
const fn signed(formatter: &DecimalFormatter, mut value: i64) -> Decimal {
const fn signed(formatter: &IntegerFormatter, value: i64) -> Integer {
// Specialize the common case to generate tighter codegen.
if value >= 0 && formatter.force_sign.is_none() {
let mut decimal = Decimal {
buf: [0; Self::MAX_LEN as usize],
start: Self::MAX_LEN,
end: Self::MAX_LEN,
};
loop {
decimal.start -= 1;
let digit = (value % 10) as u8;
value /= 10;
decimal.buf[decimal.start as usize] = b'0' + digit;
if value == 0 {
break;
}
}
while decimal.len() < formatter.get_signed_minimum_digits() {
decimal.start -= 1;
decimal.buf[decimal.start as usize] = formatter.padding_byte;
}
return decimal;
if value >= 0 {
return Integer::unsigned(formatter, value.unsigned_abs());
}
Decimal::signed_cold(formatter, value)
Integer::signed_cold(formatter, value)
}
#[cold]
#[inline(never)]
const fn signed_cold(formatter: &DecimalFormatter, value: i64) -> Decimal {
let sign = value.signum();
let Some(mut value) = value.checked_abs() else {
let buf = [
b'-', b'9', b'2', b'2', b'3', b'3', b'7', b'2', b'0', b'3',
b'6', b'8', b'5', b'4', b'7', b'7', b'5', b'8', b'0', b'8',
];
return Decimal { buf, start: 0, end: Self::MAX_LEN };
};
let mut decimal = Decimal {
buf: [0; Self::MAX_LEN as usize],
start: Self::MAX_LEN,
end: Self::MAX_LEN,
};
loop {
decimal.start -= 1;
let digit = (value % 10) as u8;
value /= 10;
decimal.buf[decimal.start as usize] = b'0' + digit;
if value == 0 {
break;
}
const fn signed_cold(formatter: &IntegerFormatter, value: i64) -> Integer {
let mut integer = Integer::unsigned(formatter, value.unsigned_abs());
if value < 0 {
integer.start -= 1;
integer.buf[integer.start as usize] = b'-';
}
while decimal.len() < formatter.get_signed_minimum_digits() {
decimal.start -= 1;
decimal.buf[decimal.start as usize] = formatter.padding_byte;
}
if sign < 0 {
decimal.start -= 1;
decimal.buf[decimal.start as usize] = b'-';
} else if let Some(zero_is_positive) = formatter.force_sign {
let ascii_sign =
if sign > 0 || zero_is_positive { b'+' } else { b'-' };
decimal.start -= 1;
decimal.buf[decimal.start as usize] = ascii_sign;
}
decimal
integer
}
/// Returns the total number of ASCII bytes (including the sign) that are
/// used to represent this decimal number.
/// used to represent this integer number.
#[inline]
const fn len(&self) -> u8 {
self.end - self.start
Self::MAX_LEN - self.start
}
/// Returns the ASCII representation of this decimal as a byte slice.
/// Returns the ASCII representation of this integer as a byte slice.
///
/// The slice returned is guaranteed to be valid ASCII.
#[inline]
fn as_bytes(&self) -> &[u8] {
&self.buf[usize::from(self.start)..usize::from(self.end)]
&self.buf[usize::from(self.start)..]
}
/// Returns the ASCII representation of this decimal as a string slice.
/// Returns the ASCII representation of this integer as a string slice.
#[inline]
pub(crate) fn as_str(&self) -> &str {
// SAFETY: This is safe because all bytes written to `self.buf` are
@ -462,14 +373,10 @@ impl DurationUnits {
if let Some(min) = self.min {
if min <= unit {
return Err(err!(
"found value {value:?} with unit {unit} \
after unit {prev_unit}, but units must be \
written from largest to smallest \
(and they can't be repeated)",
unit = unit.singular(),
prev_unit = min.singular(),
));
return Err(Error::from(E::OutOfOrderUnits {
found: unit,
previous: min,
}));
}
}
// Given the above check, the given unit must be smaller than any we
@ -503,12 +410,7 @@ impl DurationUnits {
) -> Result<(), Error> {
if let Some(min) = self.min {
if min <= Unit::Hour {
return Err(err!(
"found `HH:MM:SS` after unit {min}, \
but `HH:MM:SS` can only appear after \
years, months, weeks or days",
min = min.singular(),
));
return Err(Error::from(E::OutOfOrderHMS { found: min }));
}
}
self.set_unit_value(Unit::Hour, hours)?;
@ -539,15 +441,11 @@ impl DurationUnits {
/// return an error if the minimum unit is bigger than `Unit::Hour`.
pub(crate) fn set_fraction(&mut self, fraction: u32) -> Result<(), Error> {
assert!(fraction <= 999_999_999);
if self.min == Some(Unit::Nanosecond) {
return Err(err!("fractional nanoseconds are not supported"));
}
if let Some(min) = self.min {
if min > Unit::Hour {
return Err(err!(
"fractional {plural} are not supported",
plural = min.plural()
));
if min > Unit::Hour || min == Unit::Nanosecond {
return Err(Error::from(E::NotAllowedFractionalUnit {
found: min,
}));
}
}
self.fraction = Some(fraction);
@ -642,13 +540,6 @@ impl DurationUnits {
#[cold]
#[inline(never)]
fn to_span_general(&self) -> Result<Span, Error> {
fn error_context(unit: Unit, value: i64) -> Error {
err!(
"failed to set value {value:?} as {unit} unit on span",
unit = unit.singular(),
)
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn set_time_unit(
unit: Unit,
@ -682,7 +573,7 @@ impl DurationUnits {
set(span)
.or_else(|err| fractional_fallback(err, unit, value, span))
.with_context(|| error_context(unit, value))
.context(E::FailedValueSet { unit })
}
let (min, _) = self.get_min_max_units()?;
@ -692,25 +583,25 @@ impl DurationUnits {
let value = self.get_unit_value(Unit::Year)?;
span = span
.try_years(value)
.with_context(|| error_context(Unit::Year, value))?;
.context(E::FailedValueSet { unit: Unit::Year })?;
}
if self.values[Unit::Month.as_usize()] != 0 {
let value = self.get_unit_value(Unit::Month)?;
span = span
.try_months(value)
.with_context(|| error_context(Unit::Month, value))?;
.context(E::FailedValueSet { unit: Unit::Month })?;
}
if self.values[Unit::Week.as_usize()] != 0 {
let value = self.get_unit_value(Unit::Week)?;
span = span
.try_weeks(value)
.with_context(|| error_context(Unit::Week, value))?;
.context(E::FailedValueSet { unit: Unit::Week })?;
}
if self.values[Unit::Day.as_usize()] != 0 {
let value = self.get_unit_value(Unit::Day)?;
span = span
.try_days(value)
.with_context(|| error_context(Unit::Day, value))?;
.context(E::FailedValueSet { unit: Unit::Day })?;
}
if self.values[Unit::Hour.as_usize()] != 0 {
let value = self.get_unit_value(Unit::Hour)?;
@ -822,11 +713,7 @@ impl DurationUnits {
fn to_signed_duration_general(&self) -> Result<SignedDuration, Error> {
let (min, max) = self.get_min_max_units()?;
if max > Unit::Hour {
return Err(err!(
"parsing {unit} units into a `SignedDuration` is not supported \
(perhaps try parsing into a `Span` instead)",
unit = max.singular(),
));
return Err(Error::from(E::NotAllowedCalendarUnit { unit: max }));
}
let mut sdur = SignedDuration::ZERO;
@ -834,85 +721,43 @@ impl DurationUnits {
let value = self.get_unit_value(Unit::Hour)?;
sdur = SignedDuration::try_from_hours(value)
.and_then(|nanos| sdur.checked_add(nanos))
.ok_or_else(|| {
err!(
"accumulated `SignedDuration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Hour.singular(),
)
})?;
.ok_or(E::OverflowForUnit { unit: Unit::Hour })?;
}
if self.values[Unit::Minute.as_usize()] != 0 {
let value = self.get_unit_value(Unit::Minute)?;
sdur = SignedDuration::try_from_mins(value)
.and_then(|nanos| sdur.checked_add(nanos))
.ok_or_else(|| {
err!(
"accumulated `SignedDuration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Minute.singular(),
)
})?;
.ok_or(E::OverflowForUnit { unit: Unit::Minute })?;
}
if self.values[Unit::Second.as_usize()] != 0 {
let value = self.get_unit_value(Unit::Second)?;
sdur = SignedDuration::from_secs(value)
.checked_add(sdur)
.ok_or_else(|| {
err!(
"accumulated `SignedDuration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Second.singular(),
)
})?;
.ok_or(E::OverflowForUnit { unit: Unit::Second })?;
}
if self.values[Unit::Millisecond.as_usize()] != 0 {
let value = self.get_unit_value(Unit::Millisecond)?;
sdur = SignedDuration::from_millis(value)
.checked_add(sdur)
.ok_or_else(|| {
err!(
"accumulated `SignedDuration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Millisecond.singular(),
)
})?;
.ok_or(E::OverflowForUnit { unit: Unit::Millisecond })?;
}
if self.values[Unit::Microsecond.as_usize()] != 0 {
let value = self.get_unit_value(Unit::Microsecond)?;
sdur = SignedDuration::from_micros(value)
.checked_add(sdur)
.ok_or_else(|| {
err!(
"accumulated `SignedDuration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Microsecond.singular(),
)
})?;
.ok_or(E::OverflowForUnit { unit: Unit::Microsecond })?;
}
if self.values[Unit::Nanosecond.as_usize()] != 0 {
let value = self.get_unit_value(Unit::Nanosecond)?;
sdur = SignedDuration::from_nanos(value)
.checked_add(sdur)
.ok_or_else(|| {
err!(
"accumulated `SignedDuration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Nanosecond.singular(),
)
})?;
.ok_or(E::OverflowForUnit { unit: Unit::Nanosecond })?;
}
if let Some(fraction) = self.get_fraction()? {
sdur = sdur
.checked_add(fractional_duration(min, fraction)?)
.ok_or_else(|| {
err!(
"accumulated `SignedDuration` of `{sdur:?}` \
overflowed when adding 0.{fraction} of unit {unit}",
unit = min.singular(),
)
})?;
.ok_or(E::OverflowForUnitFractional { unit: min })?;
}
Ok(sdur)
@ -1003,19 +848,12 @@ impl DurationUnits {
}
if self.sign.is_negative() {
return Err(err!(
"cannot parse negative duration into unsigned \
`std::time::Duration`",
));
return Err(Error::from(E::NotAllowedNegative));
}
let (min, max) = self.get_min_max_units()?;
if max > Unit::Hour {
return Err(err!(
"parsing {unit} units into a `std::time::Duration` \
is not supported (perhaps try parsing into a `Span` instead)",
unit = max.singular(),
));
return Err(Error::from(E::NotAllowedCalendarUnit { unit: max }));
}
let mut sdur = core::time::Duration::ZERO;
@ -1023,73 +861,37 @@ impl DurationUnits {
let value = self.values[Unit::Hour.as_usize()];
sdur = try_from_hours(value)
.and_then(|nanos| sdur.checked_add(nanos))
.ok_or_else(|| {
err!(
"accumulated `std::time::Duration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Hour.singular(),
)
})?;
.ok_or(E::OverflowForUnit { unit: Unit::Hour })?;
}
if self.values[Unit::Minute.as_usize()] != 0 {
let value = self.values[Unit::Minute.as_usize()];
sdur = try_from_mins(value)
.and_then(|nanos| sdur.checked_add(nanos))
.ok_or_else(|| {
err!(
"accumulated `std::time::Duration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Minute.singular(),
)
})?;
.ok_or(E::OverflowForUnit { unit: Unit::Minute })?;
}
if self.values[Unit::Second.as_usize()] != 0 {
let value = self.values[Unit::Second.as_usize()];
sdur = core::time::Duration::from_secs(value)
.checked_add(sdur)
.ok_or_else(|| {
err!(
"accumulated `std::time::Duration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Second.singular(),
)
})?;
.ok_or(E::OverflowForUnit { unit: Unit::Second })?;
}
if self.values[Unit::Millisecond.as_usize()] != 0 {
let value = self.values[Unit::Millisecond.as_usize()];
sdur = core::time::Duration::from_millis(value)
.checked_add(sdur)
.ok_or_else(|| {
err!(
"accumulated `std::time::Duration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Millisecond.singular(),
)
})?;
.ok_or(E::OverflowForUnit { unit: Unit::Millisecond })?;
}
if self.values[Unit::Microsecond.as_usize()] != 0 {
let value = self.values[Unit::Microsecond.as_usize()];
sdur = core::time::Duration::from_micros(value)
.checked_add(sdur)
.ok_or_else(|| {
err!(
"accumulated `std::time::Duration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Microsecond.singular(),
)
})?;
.ok_or(E::OverflowForUnit { unit: Unit::Microsecond })?;
}
if self.values[Unit::Nanosecond.as_usize()] != 0 {
let value = self.values[Unit::Nanosecond.as_usize()];
sdur = core::time::Duration::from_nanos(value)
.checked_add(sdur)
.ok_or_else(|| {
err!(
"accumulated `std::time::Duration` of `{sdur:?}` \
overflowed when adding {value} of unit {unit}",
unit = Unit::Nanosecond.singular(),
)
})?;
.ok_or(E::OverflowForUnit { unit: Unit::Nanosecond })?;
}
if let Some(fraction) = self.get_fraction()? {
@ -1097,13 +899,7 @@ impl DurationUnits {
.checked_add(
fractional_duration(min, fraction)?.unsigned_abs(),
)
.ok_or_else(|| {
err!(
"accumulated `std::time::Duration` of `{sdur:?}` \
overflowed when adding 0.{fraction} of unit {unit}",
unit = min.singular(),
)
})?;
.ok_or(E::OverflowForUnitFractional { unit: Unit::Hour })?;
}
Ok(sdur)
@ -1122,7 +918,7 @@ impl DurationUnits {
/// were no parsed duration components.)
fn get_min_max_units(&self) -> Result<(Unit, Unit), Error> {
let (Some(min), Some(max)) = (self.min, self.max) else {
return Err(err!("no parsed duration components"));
return Err(Error::from(E::EmptyDuration));
};
Ok((min, max))
}
@ -1143,21 +939,12 @@ impl DurationUnits {
}
// Otherwise, if a conversion to `i64` fails, then that failure
// is correct.
let mut value = i64::try_from(value).map_err(|_| {
err!(
"`{sign}{value}` {unit} is too big (or small) \
to fit into a signed 64-bit integer",
unit = unit.plural()
)
})?;
let mut value = i64::try_from(value)
.map_err(|_| E::SignedOverflowForUnit { unit })?;
if sign.is_negative() {
value = value.checked_neg().ok_or_else(|| {
err!(
"`{sign}{value}` {unit} is too big (or small) \
to fit into a signed 64-bit integer",
unit = unit.plural()
)
})?;
value = value
.checked_neg()
.ok_or(E::SignedOverflowForUnit { unit })?;
}
Ok(value)
}
@ -1258,21 +1045,13 @@ pub(crate) fn parse_temporal_fraction<'i>(
}
let digits = mkdigits(input);
if digits.is_empty() {
return Err(err!(
"found decimal after seconds component, \
but did not find any decimal digits after decimal",
));
return Err(Error::from(E::MissingFractionalDigits));
}
// I believe this error can never happen, since we know we have no more
// than 9 ASCII digits. Any sequence of 9 ASCII digits can be parsed
// into an `i64`.
let nanoseconds = parse::fraction(digits).map_err(|err| {
err!(
"failed to parse {digits:?} as fractional component \
(up to 9 digits, nanosecond precision): {err}",
digits = escape::Bytes(digits),
)
})?;
let nanoseconds =
parse::fraction(digits).context(E::InvalidFraction)?;
// OK because parsing is forcefully limited to 9 digits,
// which can never be greater than `999_999_99`,
// which is less than `u32::MAX`.
@ -1411,18 +1190,10 @@ fn fractional_time_to_span(
}
if !sdur.is_zero() {
let nanos = sdur.as_nanos();
let nanos64 = i64::try_from(nanos).map_err(|_| {
err!(
"failed to set nanosecond value {nanos} (it overflows \
`i64`) on span determined from {value}.{fraction}",
)
})?;
span = span.try_nanoseconds(nanos64).with_context(|| {
err!(
"failed to set nanosecond value {nanos64} on span \
determined from {value}.{fraction}",
)
})?;
let nanos64 =
i64::try_from(nanos).map_err(|_| E::InvalidFractionNanos)?;
span =
span.try_nanoseconds(nanos64).context(E::InvalidFractionNanos)?;
}
Ok(span)
@ -1452,13 +1223,9 @@ fn fractional_time_to_duration(
) -> Result<SignedDuration, Error> {
let sdur = duration_unit_value(unit, value)?;
let fraction_dur = fractional_duration(unit, fraction)?;
sdur.checked_add(fraction_dur).ok_or_else(|| {
err!(
"accumulated `SignedDuration` of `{sdur:?}` overflowed \
when adding `{fraction_dur:?}` (from fractional {unit} units)",
unit = unit.singular(),
)
})
Ok(sdur
.checked_add(fraction_dur)
.ok_or(E::OverflowForUnitFractional { unit })?)
}
/// Converts the fraction of the given unit to a signed duration.
@ -1488,10 +1255,9 @@ fn fractional_duration(
Unit::Millisecond => fraction / t::NANOS_PER_MICRO.value(),
Unit::Microsecond => fraction / t::NANOS_PER_MILLI.value(),
unit => {
return Err(err!(
"fractional {unit} units are not allowed",
unit = unit.singular(),
))
return Err(Error::from(E::NotAllowedFractionalUnit {
found: unit,
}));
}
};
Ok(SignedDuration::from_nanos(nanos))
@ -1516,17 +1282,13 @@ fn duration_unit_value(
Unit::Hour => {
let seconds = value
.checked_mul(t::SECONDS_PER_HOUR.value())
.ok_or_else(|| {
err!("converting {value} hours to seconds overflows i64")
})?;
.ok_or(E::ConversionToSecondsFailed { unit: Unit::Hour })?;
SignedDuration::from_secs(seconds)
}
Unit::Minute => {
let seconds = value
.checked_mul(t::SECONDS_PER_MINUTE.value())
.ok_or_else(|| {
err!("converting {value} minutes to seconds overflows i64")
})?;
.ok_or(E::ConversionToSecondsFailed { unit: Unit::Minute })?;
SignedDuration::from_secs(seconds)
}
Unit::Second => SignedDuration::from_secs(value),
@ -1534,11 +1296,9 @@ fn duration_unit_value(
Unit::Microsecond => SignedDuration::from_micros(value),
Unit::Nanosecond => SignedDuration::from_nanos(value),
unsupported => {
return Err(err!(
"parsing {unit} units into a `SignedDuration` is not supported \
(perhaps try parsing into a `Span` instead)",
unit = unsupported.singular(),
));
return Err(Error::from(E::NotAllowedCalendarUnit {
unit: unsupported,
}))
}
};
Ok(sdur)
@ -1551,43 +1311,30 @@ mod tests {
use super::*;
#[test]
fn decimal() {
let x = DecimalFormatter::new().format_signed(i64::MIN);
fn integer() {
let x = IntegerFormatter::new().format_signed(i64::MIN);
assert_eq!(x.as_str(), "-9223372036854775808");
let x = DecimalFormatter::new().format_signed(i64::MIN + 1);
let x = IntegerFormatter::new().format_signed(i64::MIN + 1);
assert_eq!(x.as_str(), "-9223372036854775807");
let x = DecimalFormatter::new().format_signed(i64::MAX);
let x = IntegerFormatter::new().format_signed(i64::MAX);
assert_eq!(x.as_str(), "9223372036854775807");
let x =
DecimalFormatter::new().force_sign(true).format_signed(i64::MAX);
assert_eq!(x.as_str(), "+9223372036854775807");
let x = DecimalFormatter::new().format_signed(0);
let x = IntegerFormatter::new().format_signed(0);
assert_eq!(x.as_str(), "0");
let x = DecimalFormatter::new().force_sign(true).format_signed(0);
assert_eq!(x.as_str(), "+0");
let x = DecimalFormatter::new().force_sign(false).format_signed(0);
assert_eq!(x.as_str(), "-0");
let x = DecimalFormatter::new().padding(4).format_signed(0);
let x = IntegerFormatter::new().padding(4).format_signed(0);
assert_eq!(x.as_str(), "0000");
let x = DecimalFormatter::new().padding(4).format_signed(789);
let x = IntegerFormatter::new().padding(4).format_signed(789);
assert_eq!(x.as_str(), "0789");
let x = DecimalFormatter::new().padding(4).format_signed(-789);
let x = IntegerFormatter::new().padding(4).format_signed(-789);
assert_eq!(x.as_str(), "-0789");
let x = DecimalFormatter::new()
.force_sign(true)
.padding(4)
.format_signed(789);
assert_eq!(x.as_str(), "+0789");
let x = IntegerFormatter::new().padding(4).format_signed(789);
assert_eq!(x.as_str(), "0789");
}
#[test]

View file

@ -87,10 +87,10 @@ impl Logger {
/// Create a new logger that logs to stderr and initialize it as the
/// global logger. If there was a problem setting the logger, then an
/// error is returned.
pub(crate) fn init() -> Result<(), crate::Error> {
pub(crate) fn init() -> Result<(), log::SetLoggerError> {
#[cfg(all(feature = "std", feature = "logging"))]
{
log::set_logger(LOGGER).map_err(crate::Error::adhoc)?;
log::set_logger(LOGGER)?;
log::set_max_level(log::LevelFilter::Trace);
Ok(())
}

View file

@ -83,15 +83,16 @@ mod sys {
} else {
SystemTime::UNIX_EPOCH.checked_sub(duration)
};
// It's a little sad that we have to panic here, but the standard
// SystemTime::now() API is infallible, so we kind of have to match it.
// With that said, a panic here would be highly unusual. It would imply
// that the system time is set to some extreme timestamp very far in the
// future or the past.
// It's a little sad that we have to panic here, but the
// standard SystemTime::now() API is infallible, so we kind
// of have to match it. With that said, a panic here would be
// highly unusual. It would imply that the system time is set
// to some extreme timestamp very far in the future or the
// past.
let Some(timestamp) = result else {
panic!(
"failed to get current time: \
subtracting {duration:?} from Unix epoch overflowed"
"failed to get current time from Javascript date: \
arithmetic on Unix epoch overflowed"
)
};
timestamp

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,46 +0,0 @@
macro_rules! err {
($($tt:tt)*) => {{
crate::shared::util::error::Error::from_args(format_args!($($tt)*))
}}
}
pub(crate) use err;
/// An error that can be returned when parsing.
#[derive(Clone, Debug)]
pub struct Error {
#[cfg(feature = "alloc")]
message: alloc::boxed::Box<str>,
// only-jiff-start
#[cfg(not(feature = "alloc"))]
message: &'static str,
// only-jiff-end
}
impl Error {
pub(crate) fn from_args<'a>(message: core::fmt::Arguments<'a>) -> Error {
#[cfg(feature = "alloc")]
{
use alloc::string::ToString;
let message = message.to_string().into_boxed_str();
Error { message }
}
// only-jiff-start
#[cfg(not(feature = "alloc"))]
{
let message = message.as_str().unwrap_or(
"unknown Jiff error (better error messages require \
enabling the `alloc` feature for the `jiff` crate)",
);
Error { message }
}
// only-jiff-end
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
core::fmt::Display::fmt(&self.message, f)
}
}

View file

@ -1,88 +0,0 @@
/*!
Provides convenience routines for escaping raw bytes.
This was copied from `regex-automata` with a few light edits.
*/
use super::utf8;
/// Provides a convenient `Debug` implementation for a `u8`.
///
/// The `Debug` impl treats the byte as an ASCII, and emits a human
/// readable representation of it. If the byte isn't ASCII, then it's
/// emitted as a hex escape sequence.
#[derive(Clone, Copy)]
pub(crate) struct Byte(pub u8);
impl core::fmt::Display for Byte {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
if self.0 == b' ' {
return write!(f, " ");
}
// 10 bytes is enough for any output from ascii::escape_default.
let mut bytes = [0u8; 10];
let mut len = 0;
for (i, mut b) in core::ascii::escape_default(self.0).enumerate() {
// capitalize \xab to \xAB
if i >= 2 && b'a' <= b && b <= b'f' {
b -= 32;
}
bytes[len] = b;
len += 1;
}
write!(f, "{}", core::str::from_utf8(&bytes[..len]).unwrap())
}
}
impl core::fmt::Debug for Byte {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "\"")?;
core::fmt::Display::fmt(self, f)?;
write!(f, "\"")?;
Ok(())
}
}
/// Provides a convenient `Debug` implementation for `&[u8]`.
///
/// This generally works best when the bytes are presumed to be mostly
/// UTF-8, but will work for anything. For any bytes that aren't UTF-8,
/// they are emitted as hex escape sequences.
#[derive(Clone, Copy)]
pub(crate) struct Bytes<'a>(pub &'a [u8]);
impl<'a> core::fmt::Display for Bytes<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
// This is a sad re-implementation of a similar impl found in bstr.
let mut bytes = self.0;
while let Some(result) = utf8::decode(bytes) {
let ch = match result {
Ok(ch) => ch,
Err(errant_bytes) => {
// The decode API guarantees `errant_bytes` is non-empty.
write!(f, r"\x{:02x}", errant_bytes[0])?;
bytes = &bytes[1..];
continue;
}
};
bytes = &bytes[ch.len_utf8()..];
match ch {
'\0' => write!(f, "\\0")?,
'\x01'..='\x7f' => {
write!(f, "{}", (ch as u8).escape_ascii())?;
}
_ => write!(f, "{}", ch.escape_debug())?,
}
}
Ok(())
}
}
impl<'a> core::fmt::Debug for Bytes<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "\"")?;
core::fmt::Display::fmt(self, f)?;
write!(f, "\"")?;
Ok(())
}
}

View file

@ -22,8 +22,6 @@ they are internal types. Specifically, to distinguish them from Jiff's public
types. For example, `Date` versus `IDate`.
*/
use super::error::{err, Error};
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub(crate) struct ITimestamp {
pub(crate) second: i64,
@ -141,11 +139,13 @@ impl IDateTime {
pub(crate) fn checked_add_seconds(
&self,
seconds: i32,
) -> Result<IDateTime, Error> {
let day_second =
self.time.to_second().second.checked_add(seconds).ok_or_else(
|| err!("adding `{seconds}s` to datetime overflowed"),
)?;
) -> Result<IDateTime, RangeError> {
let day_second = self
.time
.to_second()
.second
.checked_add(seconds)
.ok_or_else(|| RangeError::DateTimeSeconds)?;
let days = day_second.div_euclid(86400);
let second = day_second.rem_euclid(86400);
let date = self.date.checked_add_days(days)?;
@ -160,8 +160,8 @@ pub(crate) struct IEpochDay {
}
impl IEpochDay {
const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 };
const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 };
pub(crate) const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 };
pub(crate) const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 };
/// Converts days since the Unix epoch to a Gregorian date.
///
@ -217,20 +217,17 @@ impl IEpochDay {
/// If this would overflow an `i32` or result in an out-of-bounds epoch
/// day, then this returns an error.
#[inline]
pub(crate) fn checked_add(&self, amount: i32) -> Result<IEpochDay, Error> {
pub(crate) fn checked_add(
&self,
amount: i32,
) -> Result<IEpochDay, RangeError> {
let epoch_day = self.epoch_day;
let sum = epoch_day.checked_add(amount).ok_or_else(|| {
err!("adding `{amount}` to epoch day `{epoch_day}` overflowed i32")
})?;
let sum = epoch_day
.checked_add(amount)
.ok_or_else(|| RangeError::EpochDayI32)?;
let ret = IEpochDay { epoch_day: sum };
if !(IEpochDay::MIN <= ret && ret <= IEpochDay::MAX) {
return Err(err!(
"adding `{amount}` to epoch day `{epoch_day}` \
resulted in `{sum}`, which is not in the required \
epoch day range of `{min}..={max}`",
min = IEpochDay::MIN.epoch_day,
max = IEpochDay::MAX.epoch_day,
));
return Err(RangeError::EpochDayDays);
}
Ok(ret)
}
@ -258,14 +255,11 @@ impl IDate {
year: i16,
month: i8,
day: i8,
) -> Result<IDate, Error> {
) -> Result<IDate, RangeError> {
if day > 28 {
let max_day = days_in_month(year, month);
if day > max_day {
return Err(err!(
"day={day} is out of range for year={year} \
and month={month}, must be in range 1..={max_day}",
));
return Err(RangeError::DateInvalidDays { year, month });
}
}
Ok(IDate { year, month, day })
@ -281,37 +275,22 @@ impl IDate {
pub(crate) fn from_day_of_year(
year: i16,
day: i16,
) -> Result<IDate, Error> {
) -> Result<IDate, RangeError> {
if !(1 <= day && day <= 366) {
return Err(err!(
"day-of-year={day} is out of range for year={year}, \
must be in range 1..={max_day}",
max_day = days_in_year(year),
));
return Err(RangeError::DateInvalidDayOfYear { year });
}
let start = IDate { year, month: 1, day: 1 }.to_epoch_day();
let end = start
.checked_add(i32::from(day) - 1)
.map_err(|_| {
err!(
"failed to find date for \
year={year} and day-of-year={day}: \
adding `{day}` to `{start}` overflows \
Jiff's range",
start = start.epoch_day,
)
})?
// This can only happen when `year=9999` and `day=366`.
.map_err(|_| RangeError::DayOfYear)?
.to_date();
// If we overflowed into the next year, then `day` is too big.
if year != end.year {
// Can only happen given day=366 and this is a leap year.
debug_assert_eq!(day, 366);
debug_assert!(!is_leap_year(year));
return Err(err!(
"day-of-year={day} is out of range for year={year}, \
must be in range 1..={max_day}",
max_day = days_in_year(year),
));
return Err(RangeError::DateInvalidDayOfYear { year });
}
Ok(end)
}
@ -327,12 +306,9 @@ impl IDate {
pub(crate) fn from_day_of_year_no_leap(
year: i16,
mut day: i16,
) -> Result<IDate, Error> {
) -> Result<IDate, RangeError> {
if !(1 <= day && day <= 365) {
return Err(err!(
"day-of-year={day} is out of range for year={year}, \
must be in range 1..=365",
));
return Err(RangeError::DateInvalidDayOfYearNoLeap);
}
if day >= 60 && is_leap_year(year) {
day += 1;
@ -390,12 +366,9 @@ impl IDate {
&self,
nth: i8,
weekday: IWeekday,
) -> Result<IDate, Error> {
) -> Result<IDate, RangeError> {
if nth == 0 || !(-5 <= nth && nth <= 5) {
return Err(err!(
"got nth weekday of `{nth}`, but \
must be non-zero and in range `-5..=5`",
));
return Err(RangeError::NthWeekdayOfMonth);
}
if nth > 0 {
let first_weekday = self.first_of_month().weekday();
@ -412,13 +385,10 @@ impl IDate {
// of `Day`, we can't let this boundary condition escape. So we
// check it here.
if day < 1 {
return Err(err!(
"day={day} is out of range for year={year} \
and month={month}, must be in range 1..={max_day}",
year = self.year,
month = self.month,
max_day = days_in_month(self.year, self.month),
));
return Err(RangeError::DateInvalidDays {
year: self.year,
month: self.month,
});
}
IDate::try_new(self.year, self.month, day)
}
@ -426,16 +396,12 @@ impl IDate {
/// Returns the day before this date.
#[inline]
pub(crate) fn yesterday(self) -> Result<IDate, Error> {
pub(crate) fn yesterday(self) -> Result<IDate, RangeError> {
if self.day == 1 {
if self.month == 1 {
let year = self.year - 1;
if year <= -10000 {
return Err(err!(
"returning yesterday for -9999-01-01 is not \
possible because it is less than Jiff's supported
minimum date",
));
return Err(RangeError::Yesterday);
}
return Ok(IDate { year, month: 12, day: 31 });
}
@ -448,16 +414,12 @@ impl IDate {
/// Returns the day after this date.
#[inline]
pub(crate) fn tomorrow(self) -> Result<IDate, Error> {
pub(crate) fn tomorrow(self) -> Result<IDate, RangeError> {
if self.day >= 28 && self.day == days_in_month(self.year, self.month) {
if self.month == 12 {
let year = self.year + 1;
if year >= 10000 {
return Err(err!(
"returning tomorrow for 9999-12-31 is not \
possible because it is greater than Jiff's supported
maximum date",
));
return Err(RangeError::Tomorrow);
}
return Ok(IDate { year, month: 1, day: 1 });
}
@ -469,34 +431,20 @@ impl IDate {
/// Returns the year one year before this date.
#[inline]
pub(crate) fn prev_year(self) -> Result<i16, Error> {
pub(crate) fn prev_year(self) -> Result<i16, RangeError> {
let year = self.year - 1;
if year <= -10_000 {
return Err(err!(
"returning previous year for {year:04}-{month:02}-{day:02} is \
not possible because it is less than Jiff's supported \
minimum date",
year = self.year,
month = self.month,
day = self.day,
));
return Err(RangeError::YearPrevious);
}
Ok(year)
}
/// Returns the year one year from this date.
#[inline]
pub(crate) fn next_year(self) -> Result<i16, Error> {
pub(crate) fn next_year(self) -> Result<i16, RangeError> {
let year = self.year + 1;
if year >= 10_000 {
return Err(err!(
"returning next year for {year:04}-{month:02}-{day:02} is \
not possible because it is greater than Jiff's supported \
maximum date",
year = self.year,
month = self.month,
day = self.day,
));
return Err(RangeError::YearNext);
}
Ok(year)
}
@ -506,7 +454,7 @@ impl IDate {
pub(crate) fn checked_add_days(
&self,
amount: i32,
) -> Result<IDate, Error> {
) -> Result<IDate, RangeError> {
match amount {
0 => Ok(*self),
-1 => self.yesterday(),
@ -718,6 +666,84 @@ pub(crate) enum IAmbiguousOffset {
Fold { before: IOffset, after: IOffset },
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum RangeError {
DateInvalidDayOfYear { year: i16 },
DateInvalidDayOfYearNoLeap,
DateInvalidDays { year: i16, month: i8 },
DateTimeSeconds,
DayOfYear,
EpochDayDays,
EpochDayI32,
NthWeekdayOfMonth,
Tomorrow,
YearNext,
YearPrevious,
Yesterday,
}
impl core::fmt::Display for RangeError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::RangeError::*;
match *self {
DateInvalidDayOfYear { year } => write!(
f,
"number of days for `{year:04}` is invalid, \
must be in range `1..={max_day}`",
max_day = days_in_year(year),
),
DateInvalidDayOfYearNoLeap => f.write_str(
"number of days is invalid, must be in range `1..=365`",
),
DateInvalidDays { year, month } => write!(
f,
"number of days for `{year:04}-{month:02}` is invalid, \
must be in range `1..={max_day}`",
max_day = days_in_month(year, month),
),
DateTimeSeconds => {
f.write_str("adding seconds to datetime overflowed")
}
DayOfYear => f.write_str("day of year is invalid"),
EpochDayDays => write!(
f,
"adding to epoch day resulted in a value outside \
the allowed range of `{min}..={max}`",
min = IEpochDay::MIN.epoch_day,
max = IEpochDay::MAX.epoch_day,
),
EpochDayI32 => f.write_str(
"adding to epoch day overflowed 32-bit signed integer",
),
NthWeekdayOfMonth => f.write_str(
"invalid nth weekday of month, \
must be non-zero and in range `-5..=5`",
),
Tomorrow => f.write_str(
"returning tomorrow for `9999-12-31` is not \
possible because it is greater than Jiff's supported
maximum date",
),
YearNext => f.write_str(
"creating a date for a year following `9999` is \
not possible because it is greater than Jiff's supported \
maximum date",
),
YearPrevious => f.write_str(
"creating a date for a year preceding `-9999` is \
not possible because it is less than Jiff's supported \
minimum date",
),
Yesterday => f.write_str(
"returning yesterday for `-9999-01-01` is not \
possible because it is less than Jiff's supported
minimum date",
),
}
}
}
/// Returns true if and only if the given year is a leap year.
///
/// A leap year is a year with 366 days. Typical years have 365 days.
@ -920,4 +946,20 @@ mod tests {
let d1 = IDate { year: 9999, month: 12, day: 31 };
assert_eq!(d1.tomorrow().ok(), None);
}
#[test]
fn from_day_of_year() {
assert_eq!(
IDate::from_day_of_year(9999, 365),
Ok(IDate { year: 9999, month: 12, day: 31 }),
);
assert_eq!(
IDate::from_day_of_year(9998, 366),
Err(RangeError::DateInvalidDayOfYear { year: 9998 }),
);
assert_eq!(
IDate::from_day_of_year(9999, 366),
Err(RangeError::DayOfYear),
);
}
}

View file

@ -1,5 +1,2 @@
pub(crate) mod array_str;
pub(crate) mod error;
pub(crate) mod escape;
pub(crate) mod itime;
pub(crate) mod utf8;

View file

@ -1,37 +0,0 @@
/// Decodes the next UTF-8 encoded codepoint from the given byte slice.
///
/// If no valid encoding of a codepoint exists at the beginning of the
/// given byte slice, then a 1-3 byte slice is returned (which is guaranteed
/// to be a prefix of `bytes`). That byte slice corresponds either to a single
/// invalid byte, or to a prefix of a valid UTF-8 encoding of a Unicode scalar
/// value (but which ultimately did not lead to a valid encoding).
///
/// This returns `None` if and only if `bytes` is empty.
///
/// This never panics.
///
/// *WARNING*: This is not designed for performance. If you're looking for
/// a fast UTF-8 decoder, this is not it. If you feel like you need one in
/// this crate, then please file an issue and discuss your use case.
pub(crate) fn decode(bytes: &[u8]) -> Option<Result<char, &[u8]>> {
if bytes.is_empty() {
return None;
}
let string = match core::str::from_utf8(&bytes[..bytes.len().min(4)]) {
Ok(s) => s,
Err(ref err) if err.valid_up_to() > 0 => {
core::str::from_utf8(&bytes[..err.valid_up_to()]).unwrap()
}
// In this case, we want to return 1-3 bytes that make up a prefix of
// a potentially valid codepoint.
Err(err) => {
return Some(Err(
&bytes[..err.error_len().unwrap_or_else(|| bytes.len())]
))
}
};
// OK because we guaranteed above that `string`
// must be non-empty. And thus, `str::chars` must
// yield at least one Unicode scalar value.
Some(Ok(string.chars().next().unwrap()))
}

View file

@ -2,10 +2,10 @@ use core::time::Duration;
use crate::{
civil::{Date, DateTime, Time},
error::{err, ErrorContext},
error::{signed_duration::Error as E, ErrorContext},
fmt::{friendly, temporal},
tz::Offset,
util::{escape, rangeint::TryRFrom, t},
util::{rangeint::TryRFrom, t},
Error, RoundMode, Timestamp, Unit, Zoned,
};
@ -65,8 +65,7 @@ const MINS_PER_HOUR: i64 = 60;
///
/// assert_eq!(
/// "P1d".parse::<SignedDuration>().unwrap_err().to_string(),
/// "failed to parse \"P1d\" as an ISO 8601 duration string: \
/// parsing ISO 8601 duration into a `SignedDuration` requires that \
/// "parsing ISO 8601 duration in this context requires that \
/// the duration contain a time component and no components of days or \
/// greater",
/// );
@ -1456,24 +1455,13 @@ impl SignedDuration {
#[inline]
pub fn try_from_secs_f64(secs: f64) -> Result<SignedDuration, Error> {
if !secs.is_finite() {
return Err(err!(
"could not convert non-finite seconds \
{secs} to signed duration",
));
return Err(Error::from(E::ConvertNonFinite));
}
if secs < (i64::MIN as f64) {
return Err(err!(
"floating point seconds {secs} overflows signed duration \
minimum value of {:?}",
SignedDuration::MIN,
));
return Err(Error::slim_range("floating point seconds"));
}
if secs > (i64::MAX as f64) {
return Err(err!(
"floating point seconds {secs} overflows signed duration \
maximum value of {:?}",
SignedDuration::MAX,
));
return Err(Error::slim_range("floating point seconds"));
}
let mut int_secs = secs.trunc() as i64;
@ -1481,15 +1469,9 @@ impl SignedDuration {
(secs.fract() * (NANOS_PER_SEC as f64)).round() as i32;
if int_nanos.unsigned_abs() == 1_000_000_000 {
let increment = i64::from(int_nanos.signum());
int_secs = int_secs.checked_add(increment).ok_or_else(|| {
err!(
"floating point seconds {secs} overflows signed duration \
maximum value of {max:?} after rounding its fractional \
component of {fract:?}",
max = SignedDuration::MAX,
fract = secs.fract(),
)
})?;
int_secs = int_secs
.checked_add(increment)
.ok_or_else(|| Error::slim_range("floating point seconds"))?;
int_nanos = 0;
}
Ok(SignedDuration::new_unchecked(int_secs, int_nanos))
@ -1528,24 +1510,13 @@ impl SignedDuration {
#[inline]
pub fn try_from_secs_f32(secs: f32) -> Result<SignedDuration, Error> {
if !secs.is_finite() {
return Err(err!(
"could not convert non-finite seconds \
{secs} to signed duration",
));
return Err(Error::from(E::ConvertNonFinite));
}
if secs < (i64::MIN as f32) {
return Err(err!(
"floating point seconds {secs} overflows signed duration \
minimum value of {:?}",
SignedDuration::MIN,
));
return Err(Error::slim_range("floating point seconds"));
}
if secs > (i64::MAX as f32) {
return Err(err!(
"floating point seconds {secs} overflows signed duration \
maximum value of {:?}",
SignedDuration::MAX,
));
return Err(Error::slim_range("floating point seconds"));
}
let mut int_nanos =
(secs.fract() * (NANOS_PER_SEC as f32)).round() as i32;
@ -1553,15 +1524,9 @@ impl SignedDuration {
if int_nanos.unsigned_abs() == 1_000_000_000 {
let increment = i64::from(int_nanos.signum());
// N.B. I haven't found a way to trigger this error path in tests.
int_secs = int_secs.checked_add(increment).ok_or_else(|| {
err!(
"floating point seconds {secs} overflows signed duration \
maximum value of {max:?} after rounding its fractional \
component of {fract:?}",
max = SignedDuration::MAX,
fract = secs.fract(),
)
})?;
int_secs = int_secs
.checked_add(increment)
.ok_or_else(|| Error::slim_range("floating point seconds"))?;
int_nanos = 0;
}
Ok(SignedDuration::new_unchecked(int_secs, int_nanos))
@ -2023,25 +1988,18 @@ impl SignedDuration {
time2: std::time::SystemTime,
) -> Result<SignedDuration, Error> {
match time2.duration_since(time1) {
Ok(dur) => SignedDuration::try_from(dur).with_context(|| {
err!(
"unsigned duration {dur:?} for system time since \
Unix epoch overflowed signed duration"
)
}),
Ok(dur) => {
SignedDuration::try_from(dur).context(E::ConvertSystemTime)
}
Err(err) => {
let dur = err.duration();
let dur =
SignedDuration::try_from(dur).with_context(|| {
err!(
"unsigned duration {dur:?} for system time before \
Unix epoch overflowed signed duration"
)
})?;
dur.checked_neg().ok_or_else(|| {
err!("negating duration {dur:?} from before the Unix epoch \
overflowed signed duration")
})
let dur = SignedDuration::try_from(dur)
.context(E::ConvertSystemTime)?;
dur.checked_neg()
.ok_or_else(|| {
Error::slim_range("signed duration seconds")
})
.context(E::ConvertSystemTime)
}
}
}
@ -2155,17 +2113,15 @@ impl SignedDuration {
///
/// assert_eq!(
/// SignedDuration::MAX.round(Unit::Hour).unwrap_err().to_string(),
/// "rounding `2562047788015215h 30m 7s 999ms 999µs 999ns` to \
/// nearest hour in increments of 1 resulted in \
/// 9223372036854777600 seconds, which does not fit into an i64 \
/// and thus overflows `SignedDuration`",
/// "rounding signed duration to nearest hour \
/// resulted in a value outside the supported \
/// range of a `jiff::SignedDuration`",
/// );
/// assert_eq!(
/// SignedDuration::MIN.round(Unit::Hour).unwrap_err().to_string(),
/// "rounding `2562047788015215h 30m 8s 999ms 999µs 999ns ago` to \
/// nearest hour in increments of 1 resulted in \
/// -9223372036854777600 seconds, which does not fit into an i64 \
/// and thus overflows `SignedDuration`",
/// "rounding signed duration to nearest hour \
/// resulted in a value outside the supported \
/// range of a `jiff::SignedDuration`",
/// );
/// ```
///
@ -2176,9 +2132,9 @@ impl SignedDuration {
///
/// assert_eq!(
/// SignedDuration::ZERO.round(Unit::Day).unwrap_err().to_string(),
/// "rounding `SignedDuration` failed \
/// because a calendar unit of days was provided \
/// (to round by calendar units, you must use a `Span`)",
/// "rounding `jiff::SignedDuration` failed \
/// because a calendar unit of 'days' was provided \
/// (to round by calendar units, you must use a `jiff::Span`)",
/// );
/// ```
#[inline]
@ -2391,16 +2347,19 @@ impl core::fmt::Debug for SignedDuration {
if f.alternate() {
if self.subsec_nanos() == 0 {
write!(f, "{}s", self.as_secs())
core::fmt::Display::fmt(&self.as_secs(), f)?;
f.write_str("s")
} else if self.as_secs() == 0 {
write!(f, "{}ns", self.subsec_nanos())
core::fmt::Display::fmt(&self.subsec_nanos(), f)?;
f.write_str("ns")
} else {
write!(
core::fmt::Display::fmt(&self.as_secs(), f)?;
f.write_str("s ")?;
core::fmt::Display::fmt(
&self.subsec_nanos().unsigned_abs(),
f,
"{}s {}ns",
self.as_secs(),
self.subsec_nanos().unsigned_abs()
)
)?;
f.write_str("ns")
}
} else {
friendly::DEFAULT_SPAN_PRINTER
@ -2414,9 +2373,8 @@ impl TryFrom<Duration> for SignedDuration {
type Error = Error;
fn try_from(d: Duration) -> Result<SignedDuration, Error> {
let secs = i64::try_from(d.as_secs()).map_err(|_| {
err!("seconds in unsigned duration {d:?} overflowed i64")
})?;
let secs = i64::try_from(d.as_secs())
.map_err(|_| Error::slim_range("unsigned duration seconds"))?;
// Guaranteed to succeed since 0<=nanos<=999,999,999.
let nanos = i32::try_from(d.subsec_nanos()).unwrap();
Ok(SignedDuration::new_unchecked(secs, nanos))
@ -2429,14 +2387,10 @@ impl TryFrom<SignedDuration> for Duration {
fn try_from(sd: SignedDuration) -> Result<Duration, Error> {
// This isn't needed, but improves error messages.
if sd.is_negative() {
return Err(err!(
"cannot convert negative duration `{sd:?}` to \
unsigned `std::time::Duration`",
));
return Err(Error::slim_range("negative duration seconds"));
}
let secs = u64::try_from(sd.as_secs()).map_err(|_| {
err!("seconds in signed duration {sd:?} overflowed u64")
})?;
let secs = u64::try_from(sd.as_secs())
.map_err(|_| Error::slim_range("signed duration seconds"))?;
// Guaranteed to succeed because the above only succeeds
// when `sd` is non-negative. And when `sd` is non-negative,
// we are guaranteed that 0<=nanos<=999,999,999.
@ -2771,12 +2725,9 @@ impl SignedDurationRound {
/// Does the actual duration rounding.
fn round(&self, dur: SignedDuration) -> Result<SignedDuration, Error> {
if self.smallest > Unit::Hour {
return Err(err!(
"rounding `SignedDuration` failed because \
a calendar unit of {plural} was provided \
(to round by calendar units, you must use a `Span`)",
plural = self.smallest.plural(),
));
return Err(Error::from(E::RoundCalendarUnit {
unit: self.smallest,
}));
}
let nanos = t::NoUnits128::new_unchecked(dur.as_nanos());
let increment = t::NoUnits::new_unchecked(self.increment);
@ -2789,12 +2740,7 @@ impl SignedDurationRound {
let seconds = rounded / t::NANOS_PER_SECOND;
let seconds =
t::NoUnits::try_rfrom("seconds", seconds).map_err(|_| {
err!(
"rounding `{dur:#}` to nearest {singular} in increments \
of {increment} resulted in {seconds} seconds, which does \
not fit into an i64 and thus overflows `SignedDuration`",
singular = self.smallest.singular(),
)
Error::from(E::RoundOverflowed { unit: self.smallest })
})?;
let subsec_nanos = rounded % t::NANOS_PER_SECOND;
// OK because % 1_000_000_000 above guarantees that the result fits
@ -2838,25 +2784,22 @@ impl From<(Unit, i64)> for SignedDurationRound {
/// (We do the same thing for `Span`.)
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_iso_or_friendly(bytes: &[u8]) -> Result<SignedDuration, Error> {
if bytes.is_empty() {
return Err(err!(
"an empty string is not a valid `SignedDuration`, \
expected either a ISO 8601 or Jiff's 'friendly' \
format",
let Some((&byte, tail)) = bytes.split_first() else {
return Err(crate::Error::from(
crate::error::fmt::Error::HybridDurationEmpty,
));
}
let mut first = bytes[0];
};
let mut first = byte;
// N.B. Unsigned durations don't support negative durations (of
// course), but we still check for it here so that we can defer to
// the dedicated parsers. They will provide their own error messages.
if first == b'+' || first == b'-' {
if bytes.len() == 1 {
return Err(err!(
"found nothing after sign `{sign}`, \
which is not a valid `SignedDuration`, \
expected either a ISO 8601 or Jiff's 'friendly' \
format",
sign = escape::Byte(first),
let Some(&byte) = tail.first() else {
return Err(crate::Error::from(
crate::error::fmt::Error::HybridDurationPrefix { sign: first },
));
}
first = bytes[1];
};
first = byte;
}
if first == b'P' || first == b'p' {
temporal::DEFAULT_SPAN_PARSER.parse_duration(bytes)
@ -3048,15 +2991,15 @@ mod tests {
insta::assert_snapshot!(
p("").unwrap_err(),
@"an empty string is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format",
@r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
);
insta::assert_snapshot!(
p("+").unwrap_err(),
@"found nothing after sign `+`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format",
@r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
);
insta::assert_snapshot!(
p("-").unwrap_err(),
@"found nothing after sign `-`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format",
@r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
);
}
@ -3093,15 +3036,15 @@ mod tests {
insta::assert_snapshot!(
p("").unwrap_err(),
@"an empty string is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 2",
@r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 2"#,
);
insta::assert_snapshot!(
p("+").unwrap_err(),
@"found nothing after sign `+`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 3",
@r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 3"#,
);
insta::assert_snapshot!(
p("-").unwrap_err(),
@"found nothing after sign `-`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 3",
@r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 3"#,
);
}

View file

@ -3,12 +3,11 @@ use core::{cmp::Ordering, time::Duration as UnsignedDuration};
use crate::{
civil::{Date, DateTime, Time},
duration::{Duration, SDuration},
error::{err, Error, ErrorContext},
error::{span::Error as E, Error, ErrorContext},
fmt::{friendly, temporal},
tz::TimeZone,
util::{
borrow::DumbCow,
escape,
rangeint::{ri64, ri8, RFrom, RInto, TryRFrom, TryRInto},
round::increment,
t::{self, Constant, NoUnits, NoUnits128, Sign, C},
@ -557,12 +556,13 @@ pub(crate) use span_eq;
/// span.total(Unit::Hour).unwrap_err().to_string(),
/// "using unit 'day' in a span or configuration requires that either \
/// a relative reference time be given or \
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// invariant 24-hour days, but neither were provided",
/// );
/// // Opt into invariant 24 hour days without a relative date:
/// let marker = SpanRelativeTo::days_are_24_hours();
/// let hours = span.total((Unit::Hour, marker))?;
/// assert_eq!(hours, 24.0);
/// // Or use a relative civil date, and all days are 24 hours:
/// let date = civil::date(2020, 1, 1);
/// let hours = span.total((Unit::Hour, date))?;
@ -662,10 +662,11 @@ pub(crate) use span_eq;
/// assert_eq!(
/// Duration::try_from(span).unwrap_err().to_string(),
/// "failed to convert span to duration without relative datetime \
/// (must use `Span::to_duration` instead): using unit 'day' in a \
/// span or configuration requires that either a relative reference \
/// time be given or `SpanRelativeTo::days_are_24_hours()` is used \
/// to indicate invariant 24-hour days, but neither were provided",
/// (must use `jiff::Span::to_duration` instead): using unit 'day' \
/// in a span or configuration requires that either a relative \
/// reference time be given or \
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// invariant 24-hour days, but neither were provided",
/// );
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
@ -2325,7 +2326,8 @@ impl Span {
/// Converts a `Span` to a [`SignedDuration`] relative to the date given.
///
/// In most cases, it is unlikely that you'll need to use this routine to
/// convert a `Span` to a `SignedDuration`. Namely, by default:
/// convert a `Span` to a `SignedDuration` and instead will be ably to
/// use `SignedDuration::try_from(span)`. Namely, by default:
///
/// * [`Zoned::until`] guarantees that the biggest non-zero unit is hours.
/// * [`Timestamp::until`] guarantees that the biggest non-zero unit is
@ -2336,12 +2338,14 @@ impl Span {
/// * [`Time::until`] guarantees that the biggest non-zero unit is hours.
///
/// In the above, only [`DateTime::until`] and [`Date::until`] return
/// calendar units by default. In which case, one may pass
/// [`SpanRelativeTo::days_are_24_hours`] or an actual relative date to
/// resolve the length of a day.
/// calendar units by default, and thus would require this routine. (In
/// which case, one may pass [`SpanRelativeTo::days_are_24_hours`] or an
/// actual relative date to resolve the length of a day.)
///
/// Of course, any of the above can be changed by asking, for example,
/// `Zoned::until` to return units up to years.
/// Of course, one may change the defaults. For example, if one
/// uses `Zoned::until` with the largest unit set to `Unit::Year`
/// and the resulting `Span` includes non-zero calendar units, then
/// `SignedDuration::try_from` will fail because there is no relative date.
///
/// # Errors
///
@ -2398,24 +2402,10 @@ impl Span {
let relspan = result
.and_then(|r| r.into_relative_span(Unit::Second, *self))
.with_context(|| match relative.kind {
SpanRelativeToKind::Civil(dt) => {
err!(
"could not compute normalized relative span \
from datetime {dt} and span {self}",
)
}
SpanRelativeToKind::Zoned(ref zdt) => {
err!(
"could not compute normalized relative span \
from datetime {zdt} and span {self}",
)
}
SpanRelativeToKind::Civil(_) => E::ToDurationCivil,
SpanRelativeToKind::Zoned(_) => E::ToDurationZoned,
SpanRelativeToKind::DaysAre24Hours => {
err!(
"could not compute normalized relative span \
from {self} when all days are assumed to be \
24 hours",
)
E::ToDurationDaysAre24Hours
}
})?;
debug_assert!(relspan.span.largest_unit() <= Unit::Second);
@ -3198,13 +3188,7 @@ impl Span {
&self,
) -> Option<Error> {
let non_time_unit = self.largest_calendar_unit()?;
Some(err!(
"operation can only be performed with units of hours \
or smaller, but found non-zero {unit} units \
(operations on `Timestamp`, `tz::Offset` and `civil::Time` \
don't support calendar units in a `Span`)",
unit = non_time_unit.singular(),
))
Some(Error::from(E::NotAllowedCalendarUnits { unit: non_time_unit }))
}
/// Returns the largest non-zero calendar unit, or `None` if there are no
@ -3272,7 +3256,7 @@ impl Span {
if self.nanoseconds != C(0) {
write!(buf, ", nanoseconds: {:?}", self.nanoseconds).unwrap();
}
write!(buf, " }}").unwrap();
buf.push_str(" }}");
buf
}
@ -3305,8 +3289,8 @@ impl Span {
match (span.is_zero(), new_is_zero) {
(_, true) => Sign::N::<0>(),
(true, false) => units.signum().rinto(),
// If the old and new span are both non-zero, and we know our new
// units are not negative, then the sign remains unchanged.
// If the old and new span are both non-zero, and we know our
// new units are not negative, then the sign remains unchanged.
(false, false) => new.sign,
}
}
@ -3473,10 +3457,7 @@ impl TryFrom<Span> for UnsignedDuration {
fn try_from(sp: Span) -> Result<UnsignedDuration, Error> {
// This isn't needed, but improves error messages.
if sp.is_negative() {
return Err(err!(
"cannot convert negative span {sp:?} \
to unsigned std::time::Duration",
));
return Err(Error::from(E::ConvertNegative));
}
SignedDuration::try_from(sp).and_then(UnsignedDuration::try_from)
}
@ -3543,18 +3524,15 @@ impl TryFrom<UnsignedDuration> for Span {
#[inline]
fn try_from(d: UnsignedDuration) -> Result<Span, Error> {
let seconds = i64::try_from(d.as_secs()).map_err(|_| {
err!("seconds from {d:?} overflows a 64-bit signed integer")
})?;
let seconds = i64::try_from(d.as_secs())
.map_err(|_| Error::slim_range("unsigned duration seconds"))?;
let nanoseconds = i64::from(d.subsec_nanos());
let milliseconds = nanoseconds / t::NANOS_PER_MILLI.value();
let microseconds = (nanoseconds % t::NANOS_PER_MILLI.value())
/ t::NANOS_PER_MICRO.value();
let nanoseconds = nanoseconds % t::NANOS_PER_MICRO.value();
let span = Span::new().try_seconds(seconds).with_context(|| {
err!("duration {d:?} overflows limits of a Jiff `Span`")
})?;
let span = Span::new().try_seconds(seconds)?;
// These are all OK because `Duration::subsec_nanos` is guaranteed to
// return less than 1_000_000_000 nanoseconds. And splitting that up
// into millis, micros and nano components is guaranteed to fit into
@ -3606,10 +3584,8 @@ impl TryFrom<Span> for SignedDuration {
#[inline]
fn try_from(sp: Span) -> Result<SignedDuration, Error> {
requires_relative_date_err(sp.largest_unit()).context(
"failed to convert span to duration without relative datetime \
(must use `Span::to_duration` instead)",
)?;
requires_relative_date_err(sp.largest_unit())
.context(E::ConvertSpanToSignedDuration)?;
Ok(sp.to_duration_invariant())
}
}
@ -3678,9 +3654,7 @@ impl TryFrom<SignedDuration> for Span {
/ t::NANOS_PER_MICRO.value();
let nanoseconds = nanoseconds % t::NANOS_PER_MICRO.value();
let span = Span::new().try_seconds(seconds).with_context(|| {
err!("signed duration {d:?} overflows limits of a Jiff `Span`")
})?;
let span = Span::new().try_seconds(seconds)?;
// These are all OK because `|SignedDuration::subsec_nanos|` is
// guaranteed to return less than 1_000_000_000 nanoseconds. And
// splitting that up into millis, micros and nano components is
@ -4454,7 +4428,7 @@ impl<'a> SpanArithmetic<'a> {
/// span1.checked_add(span2).unwrap_err().to_string(),
/// "using unit 'day' in a span or configuration requires that \
/// either a relative reference time be given or \
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// invariant 24-hour days, but neither were provided",
/// );
/// let sum = span1.checked_add(
@ -4684,7 +4658,7 @@ impl<'a> SpanCompare<'a> {
/// required. Otherwise, you get an error.
///
/// ```
/// use jiff::{SpanCompare, ToSpan, Unit};
/// use jiff::{SpanCompare, ToSpan};
///
/// let span1 = 2.days().hours(12);
/// let span2 = 60.hours();
@ -4693,7 +4667,7 @@ impl<'a> SpanCompare<'a> {
/// span1.compare(span2).unwrap_err().to_string(),
/// "using unit 'day' in a span or configuration requires that \
/// either a relative reference time be given or \
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// invariant 24-hour days, but neither were provided",
/// );
/// let ordering = span1.compare(
@ -4924,7 +4898,7 @@ impl<'a> SpanTotal<'a> {
/// span.total(Unit::Hour).unwrap_err().to_string(),
/// "using unit 'day' in a span or configuration requires that either \
/// a relative reference time be given or \
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// invariant 24-hour days, but neither were provided",
/// );
///
@ -5432,8 +5406,9 @@ impl<'a> SpanRound<'a> {
/// span.round(Unit::Day).unwrap_err().to_string(),
/// "error with `smallest` rounding option: using unit 'day' in a \
/// span or configuration requires that either a relative reference \
/// time be given or `SpanRelativeTo::days_are_24_hours()` is used \
/// to indicate invariant 24-hour days, but neither were provided",
/// time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is \
/// used to indicate invariant 24-hour days, but neither were \
/// provided",
/// );
/// let rounded = span.round(
/// SpanRound::new().smallest(Unit::Day).days_are_24_hours(),
@ -5486,11 +5461,8 @@ impl<'a> SpanRound<'a> {
let max = existing_largest.max(largest);
let increment = increment::for_span(smallest, self.increment)?;
if largest < smallest {
return Err(err!(
"largest unit ('{largest}') cannot be smaller than \
smallest unit ('{smallest}')",
largest = largest.singular(),
smallest = smallest.singular(),
return Err(Error::from(
E::NotAllowedLargestSmallerThanSmallest { smallest, largest },
));
}
let relative = match self.relative {
@ -5516,14 +5488,13 @@ impl<'a> SpanRound<'a> {
// no reasonable invariant interpretation of the span. And this
// is only true when everything is less than 'day'.
requires_relative_date_err(smallest)
.context("error with `smallest` rounding option")?;
.context(E::OptionSmallest)?;
if let Some(largest) = self.largest {
requires_relative_date_err(largest)
.context("error with `largest` rounding option")?;
.context(E::OptionLargest)?;
}
requires_relative_date_err(existing_largest).context(
"error with largest unit in span to be rounded",
)?;
requires_relative_date_err(existing_largest)
.context(E::OptionLargestInSpan)?;
assert!(max <= Unit::Week);
return Ok(round_span_invariant(
span, smallest, largest, increment, mode,
@ -5673,7 +5644,7 @@ impl<'a> SpanRelativeTo<'a> {
/// span.total(Unit::Hour).unwrap_err().to_string(),
/// "using unit 'day' in a span or configuration requires that either \
/// a relative reference time be given or \
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// invariant 24-hour days, but neither were provided",
/// );
/// // Opt into invariant 24 hour days without a relative date:
@ -5709,7 +5680,7 @@ impl<'a> SpanRelativeTo<'a> {
/// span.total(Unit::Hour).unwrap_err().to_string(),
/// "using unit 'week' in a span or configuration requires that either \
/// a relative reference time be given or \
/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \
/// invariant 24-hour days, but neither were provided",
/// );
/// // Opt into invariant 24 hour days without a relative date:
@ -5814,13 +5785,10 @@ impl<'a> SpanRelativeTo<'a> {
}
SpanRelativeToKind::DaysAre24Hours => {
if matches!(unit, Unit::Year | Unit::Month) {
return Err(err!(
"using unit '{unit}' in span or configuration \
requires that a relative reference time be given \
(`SpanRelativeTo::days_are_24_hours()` was given \
but this only permits using days and weeks \
without a relative reference time)",
unit = unit.singular(),
return Err(Error::from(
E::RequiresRelativeYearOrMonthGivenDaysAre24Hours {
unit,
},
));
}
Ok(None)
@ -6229,27 +6197,12 @@ impl<'a> RelativeSpanKind<'a> {
RelativeSpanKind::Civil { ref start, ref end } => start
.datetime
.until((largest, end.datetime))
.with_context(|| {
err!(
"failed to get span between {start} and {end} \
with largest unit as {unit}",
start = start.datetime,
end = end.datetime,
unit = largest.plural(),
)
})?,
RelativeSpanKind::Zoned { ref start, ref end } => start
.zoned
.until((largest, &*end.zoned))
.with_context(|| {
err!(
"failed to get span between {start} and {end} \
with largest unit as {unit}",
start = start.zoned,
end = end.zoned,
unit = largest.plural(),
)
})?,
.context(E::FailedSpanBetweenDateTimes { unit: largest })?,
RelativeSpanKind::Zoned { ref start, ref end } => {
start.zoned.until((largest, &*end.zoned)).context(
E::FailedSpanBetweenZonedDateTimes { unit: largest },
)?
}
};
Ok(RelativeSpan { span, kind: self })
}
@ -6290,9 +6243,7 @@ impl RelativeCivil {
fn new(datetime: DateTime) -> Result<RelativeCivil, Error> {
let timestamp = datetime
.to_zoned(TimeZone::UTC)
.with_context(|| {
err!("failed to convert {datetime} to timestamp")
})?
.context(E::ConvertDateTimeToTimestamp)?
.timestamp();
Ok(RelativeCivil { datetime, timestamp })
}
@ -6308,14 +6259,10 @@ impl RelativeCivil {
/// converted to a timestamp in UTC. This only occurs near the minimum and
/// maximum datetime values.
fn checked_add(&self, span: Span) -> Result<RelativeCivil, Error> {
let datetime = self.datetime.checked_add(span).with_context(|| {
err!("failed to add {span} to {dt}", dt = self.datetime)
})?;
let datetime = self.datetime.checked_add(span)?;
let timestamp = datetime
.to_zoned(TimeZone::UTC)
.with_context(|| {
err!("failed to convert {datetime} to timestamp")
})?
.context(E::ConvertDateTimeToTimestamp)?
.timestamp();
Ok(RelativeCivil { datetime, timestamp })
}
@ -6335,15 +6282,10 @@ impl RelativeCivil {
&self,
duration: SignedDuration,
) -> Result<RelativeCivil, Error> {
let datetime =
self.datetime.checked_add(duration).with_context(|| {
err!("failed to add {duration:?} to {dt}", dt = self.datetime)
})?;
let datetime = self.datetime.checked_add(duration)?;
let timestamp = datetime
.to_zoned(TimeZone::UTC)
.with_context(|| {
err!("failed to convert {datetime} to timestamp")
})?
.context(E::ConvertDateTimeToTimestamp)?
.timestamp();
Ok(RelativeCivil { datetime, timestamp })
}
@ -6361,15 +6303,9 @@ impl RelativeCivil {
largest: Unit,
other: &RelativeCivil,
) -> Result<Span, Error> {
self.datetime.until((largest, other.datetime)).with_context(|| {
err!(
"failed to get span between {dt1} and {dt2} \
with largest unit as {unit}",
unit = largest.plural(),
dt1 = self.datetime,
dt2 = other.datetime,
)
})
self.datetime
.until((largest, other.datetime))
.context(E::FailedSpanBetweenDateTimes { unit: largest })
}
}
@ -6390,9 +6326,7 @@ impl<'a> RelativeZoned<'a> {
&self,
span: Span,
) -> Result<RelativeZoned<'static>, Error> {
let zoned = self.zoned.checked_add(span).with_context(|| {
err!("failed to add {span} to {zoned}", zoned = self.zoned)
})?;
let zoned = self.zoned.checked_add(span)?;
Ok(RelativeZoned { zoned: DumbCow::Owned(zoned) })
}
@ -6406,9 +6340,7 @@ impl<'a> RelativeZoned<'a> {
&self,
duration: SignedDuration,
) -> Result<RelativeZoned<'static>, Error> {
let zoned = self.zoned.checked_add(duration).with_context(|| {
err!("failed to add {duration:?} to {zoned}", zoned = self.zoned)
})?;
let zoned = self.zoned.checked_add(duration)?;
Ok(RelativeZoned { zoned: DumbCow::Owned(zoned) })
}
@ -6425,15 +6357,9 @@ impl<'a> RelativeZoned<'a> {
largest: Unit,
other: &RelativeZoned<'a>,
) -> Result<Span, Error> {
self.zoned.until((largest, &*other.zoned)).with_context(|| {
err!(
"failed to get span between {zdt1} and {zdt2} \
with largest unit as {unit}",
unit = largest.plural(),
zdt1 = self.zoned,
zdt2 = other.zoned,
)
})
self.zoned
.until((largest, &*other.zoned))
.context(E::FailedSpanBetweenZonedDateTimes { unit: largest })
}
/// Returns the borrowed version of self; useful when you need to convert
@ -6512,13 +6438,7 @@ impl Nudge {
increment,
);
let span = Span::from_invariant_nanoseconds(largest, rounded_nanos)
.with_context(|| {
err!(
"failed to convert rounded nanoseconds {rounded_nanos} \
to span for largest unit as {unit}",
unit = largest.plural(),
)
})?
.context(E::ConvertNanoseconds { unit: largest })?
.years_ranged(balanced.get_years_ranged())
.months_ranged(balanced.get_months_ranged())
.weeks_ranged(balanced.get_weeks_ranged());
@ -6551,13 +6471,7 @@ impl Nudge {
* balanced.get_units_ranged(smallest).div_ceil(increment);
let span = balanced
.without_lower(smallest)
.try_units_ranged(smallest, truncated.rinto())
.with_context(|| {
err!(
"failed to set {unit} to {truncated} on span {balanced}",
unit = smallest.singular()
)
})?;
.try_units_ranged(smallest, truncated.rinto())?;
let (relative0, relative1) = clamp_relative_span(
relative_start,
span,
@ -6578,14 +6492,7 @@ impl Nudge {
let grew_big_unit =
((rounded.get() as f64) - exact).signum() == (sign.get() as f64);
let span = span
.try_units_ranged(smallest, rounded.rinto())
.with_context(|| {
err!(
"failed to set {unit} to {truncated} on span {span}",
unit = smallest.singular()
)
})?;
let span = span.try_units_ranged(smallest, rounded.rinto())?;
let rounded_relative_end =
if grew_big_unit { relative1 } else { relative0 };
Ok(Nudge { span, rounded_relative_end, grew_big_unit })
@ -6631,13 +6538,7 @@ impl Nudge {
let span =
Span::from_invariant_nanoseconds(Unit::Hour, rounded_time_nanos)
.with_context(|| {
err!(
"failed to convert rounded nanoseconds \
{rounded_time_nanos} to span for largest unit as {unit}",
unit = Unit::Hour.plural(),
)
})?
.context(E::ConvertNanoseconds { unit: Unit::Hour })?
.years_ranged(balanced.get_years_ranged())
.months_ranged(balanced.get_months_ranged())
.weeks_ranged(balanced.get_weeks_ranged())
@ -6682,23 +6583,8 @@ impl Nudge {
let span_start = balanced.without_lower(unit);
let new_units = span_start
.get_units_ranged(unit)
.try_checked_add("bubble-units", sign)
.with_context(|| {
err!(
"failed to add sign {sign} to {unit} value {value}",
unit = unit.plural(),
value = span_start.get_units_ranged(unit),
)
})?;
let span_end = span_start
.try_units_ranged(unit, new_units)
.with_context(|| {
err!(
"failed to set {unit} to value \
{new_units} on span {span_start}",
unit = unit.plural(),
)
})?;
.try_checked_add("bubble-units", sign)?;
let span_end = span_start.try_units_ranged(unit, new_units)?;
let threshold = match relative.kind {
RelativeSpanKind::Civil { ref start, .. } => {
start.checked_add(span_end)?.timestamp
@ -6742,13 +6628,8 @@ fn round_span_invariant(
let nanos = span.to_invariant_nanoseconds();
let rounded =
mode.round_by_unit_in_nanoseconds(nanos, smallest, increment);
Span::from_invariant_nanoseconds(largest, rounded).with_context(|| {
err!(
"failed to convert rounded nanoseconds {rounded} \
to span for largest unit as {unit}",
unit = largest.plural(),
)
})
Span::from_invariant_nanoseconds(largest, rounded)
.context(E::ConvertNanoseconds { unit: largest })
}
/// Returns the nanosecond timestamps of `relative + span` and `relative +
@ -6772,24 +6653,9 @@ fn clamp_relative_span(
unit: Unit,
amount: NoUnits,
) -> Result<(NoUnits128, NoUnits128), Error> {
let amount = span
.get_units_ranged(unit)
.try_checked_add("clamp-units", amount)
.with_context(|| {
err!(
"failed to add {amount} to {unit} \
value {value} on span {span}",
unit = unit.plural(),
value = span.get_units_ranged(unit),
)
})?;
let span_amount =
span.try_units_ranged(unit, amount).with_context(|| {
err!(
"failed to set {unit} unit to {amount} on span {span}",
unit = unit.plural(),
)
})?;
let amount =
span.get_units_ranged(unit).try_checked_add("clamp-units", amount)?;
let span_amount = span.try_units_ranged(unit, amount)?;
let relative0 = relative.checked_add(span)?.to_nanosecond();
let relative1 = relative.checked_add(span_amount)?.to_nanosecond();
Ok((relative0, relative1))
@ -6811,25 +6677,22 @@ fn clamp_relative_span(
/// (We do the same thing for `SignedDuration`.)
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_iso_or_friendly(bytes: &[u8]) -> Result<Span, Error> {
if bytes.is_empty() {
return Err(err!(
"an empty string is not a valid `Span`, \
expected either a ISO 8601 or Jiff's 'friendly' \
format",
let Some((&byte, tail)) = bytes.split_first() else {
return Err(crate::Error::from(
crate::error::fmt::Error::HybridDurationEmpty,
));
}
let mut first = bytes[0];
};
let mut first = byte;
// N.B. Unsigned durations don't support negative durations (of
// course), but we still check for it here so that we can defer to
// the dedicated parsers. They will provide their own error messages.
if first == b'+' || first == b'-' {
if bytes.len() == 1 {
return Err(err!(
"found nothing after sign `{sign}`, \
which is not a valid `Span`, \
expected either a ISO 8601 or Jiff's 'friendly' \
format",
sign = escape::Byte(first),
let Some(&byte) = tail.first() else {
return Err(crate::Error::from(
crate::error::fmt::Error::HybridDurationPrefix { sign: first },
));
}
first = bytes[1];
};
first = byte;
}
if first == b'P' || first == b'p' {
temporal::DEFAULT_SPAN_PARSER.parse_span(bytes)
@ -6840,23 +6703,11 @@ fn parse_iso_or_friendly(bytes: &[u8]) -> Result<Span, Error> {
fn requires_relative_date_err(unit: Unit) -> Result<(), Error> {
if unit.is_variable() {
return Err(if matches!(unit, Unit::Week | Unit::Day) {
err!(
"using unit '{unit}' in a span or configuration \
requires that either a relative reference time be given \
or `SpanRelativeTo::days_are_24_hours()` is used to \
indicate invariant 24-hour days, \
but neither were provided",
unit = unit.singular(),
)
return Err(Error::from(if matches!(unit, Unit::Week | Unit::Day) {
E::RequiresRelativeWeekOrDay { unit }
} else {
err!(
"using unit '{unit}' in a span or configuration \
requires that a relative reference time be given, \
but none was provided",
unit = unit.singular(),
)
});
E::RequiresRelativeYearOrMonth { unit }
}));
}
Ok(())
}
@ -7390,15 +7241,15 @@ mod tests {
insta::assert_snapshot!(
p("").unwrap_err(),
@"an empty string is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format",
@r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
);
insta::assert_snapshot!(
p("+").unwrap_err(),
@"found nothing after sign `+`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format",
@r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
);
insta::assert_snapshot!(
p("-").unwrap_err(),
@"found nothing after sign `-`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format",
@r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#,
);
}
@ -7435,15 +7286,15 @@ mod tests {
insta::assert_snapshot!(
p("").unwrap_err(),
@"an empty string is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 2",
@r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 2"#,
);
insta::assert_snapshot!(
p("+").unwrap_err(),
@"found nothing after sign `+`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 3",
@r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 3"#,
);
insta::assert_snapshot!(
p("-").unwrap_err(),
@"found nothing after sign `-`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 3",
@r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 3"#,
);
}
}

View file

@ -2,7 +2,7 @@ use core::time::Duration as UnsignedDuration;
use crate::{
duration::{Duration, SDuration},
error::{err, Error, ErrorContext},
error::{timestamp::Error as E, Error, ErrorContext},
fmt::{
self,
temporal::{self, DEFAULT_DATETIME_PARSER},
@ -279,8 +279,7 @@ use crate::{
/// let result = "2024-06-30 08:30[America/New_York]".parse::<Timestamp>();
/// assert_eq!(
/// result.unwrap_err().to_string(),
/// "failed to find offset component in \
/// \"2024-06-30 08:30[America/New_York]\", \
/// "failed to find offset component, \
/// which is required for parsing a timestamp",
/// );
/// ```
@ -1520,9 +1519,7 @@ impl Timestamp {
let time_seconds = self.as_second_ranged();
let sum = time_seconds
.try_checked_add("span", span_seconds)
.with_context(|| {
err!("adding {span} to {self} overflowed")
})?;
.context(E::OverflowAddSpan)?;
return Ok(Timestamp::from_second_ranged(sum));
}
}
@ -1530,7 +1527,7 @@ impl Timestamp {
let span_nanos = span.to_invariant_nanoseconds();
let sum = time_nanos
.try_checked_add("span", span_nanos)
.with_context(|| err!("adding {span} to {self} overflowed"))?;
.context(E::OverflowAddSpan)?;
Ok(Timestamp::from_nanosecond_ranged(sum))
}
@ -1540,9 +1537,7 @@ impl Timestamp {
duration: SignedDuration,
) -> Result<Timestamp, Error> {
let start = self.as_duration();
let end = start.checked_add(duration).ok_or_else(|| {
err!("overflow when adding {duration:?} to {self}")
})?;
let end = start.checked_add(duration).ok_or(E::OverflowAddDuration)?;
Timestamp::from_duration(end)
}
@ -1648,9 +1643,7 @@ impl Timestamp {
duration: A,
) -> Result<Timestamp, Error> {
let duration: TimestampArithmetic = duration.into();
duration.saturating_add(self).context(
"saturating `Timestamp` arithmetic requires only time units",
)
duration.saturating_add(self).context(E::RequiresSaturatingTimeUnits)
}
/// This routine is identical to [`Timestamp::saturating_add`] with the
@ -3429,11 +3422,10 @@ impl TimestampDifference {
.get_largest()
.unwrap_or_else(|| self.round.get_smallest().max(Unit::Second));
if largest >= Unit::Day {
return Err(err!(
"unit {largest} is not supported when computing the \
difference between timestamps (must use units smaller \
than 'day')",
largest = largest.singular(),
return Err(Error::from(
crate::error::util::RoundingIncrementError::Unsupported {
unit: largest,
},
));
}
let nano1 = t1.as_nanosecond_ranged().without_bounds();
@ -3856,7 +3848,7 @@ mod tests {
fn timestamp_saturating_add() {
insta::assert_snapshot!(
Timestamp::MIN.saturating_add(Span::new().days(1)).unwrap_err(),
@"saturating `Timestamp` arithmetic requires only time units: operation can only be performed with units of hours or smaller, but found non-zero day units (operations on `Timestamp`, `tz::Offset` and `civil::Time` don't support calendar units in a `Span`)",
@"saturating timestamp arithmetic requires only time units: operation can only be performed with units of hours or smaller, but found non-zero 'day' units (operations on `jiff::Timestamp`, `jiff::tz::Offset` and `jiff::civil::Time` don't support calendar units in a `jiff::Span`)",
)
}
@ -3864,7 +3856,7 @@ mod tests {
fn timestamp_saturating_sub() {
insta::assert_snapshot!(
Timestamp::MAX.saturating_sub(Span::new().days(1)).unwrap_err(),
@"saturating `Timestamp` arithmetic requires only time units: operation can only be performed with units of hours or smaller, but found non-zero day units (operations on `Timestamp`, `tz::Offset` and `civil::Time` don't support calendar units in a `Span`)",
@"saturating timestamp arithmetic requires only time units: operation can only be performed with units of hours or smaller, but found non-zero 'day' units (operations on `jiff::Timestamp`, `jiff::tz::Offset` and `jiff::civil::Time` don't support calendar units in a `jiff::Span`)",
)
}

View file

@ -1,6 +1,6 @@
use crate::{
civil::DateTime,
error::{err, Error, ErrorContext},
error::{tz::ambiguous::Error as E, Error, ErrorContext},
shared::util::itime::IAmbiguousOffset,
tz::{Offset, TimeZone},
Timestamp, Zoned,
@ -655,18 +655,10 @@ impl AmbiguousTimestamp {
let offset = match self.offset() {
AmbiguousOffset::Unambiguous { offset } => offset,
AmbiguousOffset::Gap { before, after } => {
return Err(err!(
"the datetime {dt} is ambiguous since it falls into \
a gap between offsets {before} and {after}",
dt = self.dt,
));
return Err(Error::from(E::BecauseGap { before, after }));
}
AmbiguousOffset::Fold { before, after } => {
return Err(err!(
"the datetime {dt} is ambiguous since it falls into \
a fold between offsets {before} and {after}",
dt = self.dt,
));
return Err(Error::from(E::BecauseFold { before, after }));
}
};
offset.to_timestamp(self.dt)
@ -1039,13 +1031,10 @@ impl AmbiguousZoned {
/// ```
#[inline]
pub fn compatible(self) -> Result<Zoned, Error> {
let ts = self.ts.compatible().with_context(|| {
err!(
"error converting datetime {dt} to instant in time zone {tz}",
dt = self.datetime(),
tz = self.time_zone().diagnostic_name(),
)
})?;
let ts = self
.ts
.compatible()
.with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
Ok(ts.to_zoned(self.tz))
}
@ -1101,13 +1090,10 @@ impl AmbiguousZoned {
/// ```
#[inline]
pub fn earlier(self) -> Result<Zoned, Error> {
let ts = self.ts.earlier().with_context(|| {
err!(
"error converting datetime {dt} to instant in time zone {tz}",
dt = self.datetime(),
tz = self.time_zone().diagnostic_name(),
)
})?;
let ts = self
.ts
.earlier()
.with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
Ok(ts.to_zoned(self.tz))
}
@ -1163,13 +1149,10 @@ impl AmbiguousZoned {
/// ```
#[inline]
pub fn later(self) -> Result<Zoned, Error> {
let ts = self.ts.later().with_context(|| {
err!(
"error converting datetime {dt} to instant in time zone {tz}",
dt = self.datetime(),
tz = self.time_zone().diagnostic_name(),
)
})?;
let ts = self
.ts
.later()
.with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
Ok(ts.to_zoned(self.tz))
}
@ -1220,13 +1203,10 @@ impl AmbiguousZoned {
/// ```
#[inline]
pub fn unambiguous(self) -> Result<Zoned, Error> {
let ts = self.ts.unambiguous().with_context(|| {
err!(
"error converting datetime {dt} to instant in time zone {tz}",
dt = self.datetime(),
tz = self.time_zone().diagnostic_name(),
)
})?;
let ts = self
.ts
.unambiguous()
.with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
Ok(ts.to_zoned(self.tz))
}

View file

@ -4,7 +4,10 @@ use alloc::{
};
use crate::{
error::{err, Error, ErrorContext},
error::{
tz::concatenated::{Error as E, ALLOC_LIMIT},
Error, ErrorContext,
},
tz::TimeZone,
util::{array_str::ArrayStr, escape, utf8},
};
@ -71,7 +74,7 @@ impl<R: Read> ConcatenatedTzif<R> {
alloc(scratch1, self.header.index_len())?;
self.rdr
.read_exact_at(scratch1, self.header.index_offset)
.context("failed to read index block")?;
.context(E::FailedReadIndex)?;
let mut index = &**scratch1;
while !index.is_empty() {
@ -94,7 +97,7 @@ impl<R: Read> ConcatenatedTzif<R> {
let start = self.header.data_offset.saturating_add(entry.start());
self.rdr
.read_exact_at(scratch2, start)
.context("failed to read TZif data block")?;
.context(E::FailedReadData)?;
return TimeZone::tzif(name, scratch2).map(Some);
}
Ok(None)
@ -114,7 +117,7 @@ impl<R: Read> ConcatenatedTzif<R> {
alloc(scratch, self.header.index_len())?;
self.rdr
.read_exact_at(scratch, self.header.index_offset)
.context("failed to read index block")?;
.context(E::FailedReadIndex)?;
let names_len = self.header.index_len() / IndexEntry::LEN;
// Why are we careless with this alloc? Well, its size is proportional
@ -154,31 +157,17 @@ impl Header {
fn read<R: Read + ?Sized>(rdr: &R) -> Result<Header, Error> {
// 12 bytes plus 3 4-byte big endian integers.
let mut buf = [0; 12 + 3 * 4];
rdr.read_exact_at(&mut buf, 0)
.context("failed to read concatenated TZif header")?;
rdr.read_exact_at(&mut buf, 0).context(E::FailedReadHeader)?;
if &buf[..6] != b"tzdata" {
return Err(err!(
"expected first 6 bytes of concatenated TZif header \
to be `tzdata`, but found `{found}`",
found = escape::Bytes(&buf[..6]),
));
return Err(Error::from(E::ExpectedFirstSixBytes));
}
if buf[11] != 0 {
return Err(err!(
"expected last byte of concatenated TZif header \
to be NUL, but found `{found}`",
found = escape::Bytes(&buf[..12]),
));
return Err(Error::from(E::ExpectedLastByte));
}
let version = {
let version = core::str::from_utf8(&buf[6..11]).map_err(|_| {
err!(
"expected version in concatenated TZif header to \
be valid UTF-8, but found `{found}`",
found = escape::Bytes(&buf[6..11]),
)
})?;
let version = core::str::from_utf8(&buf[6..11])
.map_err(|_| E::ExpectedVersion)?;
// OK because `version` is exactly 5 bytes, by construction.
ArrayStr::new(version).unwrap()
};
@ -187,19 +176,12 @@ impl Header {
// OK because the sub-slice is sized to exactly 4 bytes.
let data_offset = u64::from(read_be32(&buf[16..20]));
if index_offset > data_offset {
return Err(err!(
"invalid index ({index_offset}) and data ({data_offset}) \
offsets, expected index offset to be less than or equal \
to data offset",
));
return Err(Error::from(E::InvalidIndexDataOffsets));
}
// we don't read 20..24 since we don't care about zonetab (yet)
let header = Header { version, index_offset, data_offset };
if header.index_len() % IndexEntry::LEN != 0 {
return Err(err!(
"length of index block is not a multiple {len}",
len = IndexEntry::LEN,
));
return Err(Error::from(E::InvalidLengthIndexBlock));
}
Ok(header)
}
@ -268,12 +250,8 @@ impl<'a> IndexEntry<'a> {
///
/// This returns an error if the name isn't valid UTF-8.
fn name(&self) -> Result<&str, Error> {
core::str::from_utf8(self.name_bytes()).map_err(|_| {
err!(
"IANA time zone identifier `{name}` is not valid UTF-8",
name = escape::Bytes(self.name_bytes()),
)
})
core::str::from_utf8(self.name_bytes())
.map_err(|_| Error::from(E::ExpectedIanaName))
}
/// Returns the IANA time zone identifier as a byte slice.
@ -350,20 +328,12 @@ fn read_be32(bytes: &[u8]) -> u32 {
impl Read for [u8] {
fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> Result<(), Error> {
let offset = usize::try_from(offset)
.map_err(|_| err!("offset `{offset}` overflowed `usize`"))?;
.map_err(|_| E::InvalidOffsetOverflowSlice)?;
let Some(slice) = self.get(offset..) else {
return Err(err!(
"given offset `{offset}` is not valid \
(only {len} bytes are available)",
len = self.len(),
));
return Err(Error::from(E::InvalidOffsetTooBig));
};
if buf.len() > slice.len() {
return Err(err!(
"unexpected EOF, expected {len} bytes but only have {have}",
len = buf.len(),
have = slice.len()
));
return Err(Error::from(E::ExpectedMoreData));
}
buf.copy_from_slice(&slice[..buf.len()]);
Ok(())
@ -395,9 +365,7 @@ impl Read for std::fs::File {
offset = u64::try_from(n)
.ok()
.and_then(|n| n.checked_add(offset))
.ok_or_else(|| {
err!("offset overflow when reading from `File`")
})?;
.ok_or(E::InvalidOffsetOverflowFile)?;
}
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => return Err(Error::io(e)),
@ -419,9 +387,9 @@ impl Read for std::fs::File {
fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> Result<(), Error> {
use std::io::{Read as _, Seek as _, SeekFrom};
let mut file = self;
file.seek(SeekFrom::Start(offset)).map_err(Error::io).with_context(
|| err!("failed to seek to offset {offset} in `File`"),
)?;
file.seek(SeekFrom::Start(offset))
.map_err(Error::io)
.context(E::FailedSeek)?;
file.read_exact(buf).map_err(Error::io)
}
}
@ -443,31 +411,13 @@ impl Read for std::fs::File {
/// enough in that kind of environment by far. The goal is to avoid OOM for
/// exorbitantly large allocations through some kind of attack vector.
fn alloc(bytes: &mut Vec<u8>, additional: usize) -> Result<(), Error> {
// At time of writing, the biggest TZif data file is a few KB. And the
// index block is tens of KB. So impose a limit that is a couple of orders
// of magnitude bigger, but still overall pretty small for... some systems.
// Anyway, I welcome improvements to this heuristic!
const LIMIT: usize = 10 * 1 << 20;
if additional > LIMIT {
return Err(err!(
"attempted to allocate more than {LIMIT} bytes \
while reading concatenated TZif data, which \
exceeds a heuristic limit to prevent huge allocations \
(please file a bug if this error is inappropriate)",
));
if additional > ALLOC_LIMIT {
return Err(Error::from(E::AllocRequestOverLimit));
}
bytes.try_reserve_exact(additional).map_err(|_| {
err!(
"failed to allocation {additional} bytes \
for reading concatenated TZif data"
)
})?;
bytes.try_reserve_exact(additional).map_err(|_| E::AllocFailed)?;
// This... can't actually happen right?
let new_len = bytes
.len()
.checked_add(additional)
.ok_or_else(|| err!("total allocation length overflowed `usize`"))?;
let new_len =
bytes.len().checked_add(additional).ok_or(E::AllocOverflow)?;
bytes.resize(new_len, 0);
Ok(())
}

View file

@ -25,6 +25,6 @@ impl Database {
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "Bundled(unavailable)")
f.write_str("Bundled(unavailable)")
}
}

View file

@ -26,8 +26,8 @@ impl Database {
Err(_err) => {
warn!(
"failed to parse TZif data from bundled \
tzdb for time zone {canonical_name} \
(this is like a bug, please report it): {_err}"
tzdb for time zone `{canonical_name}` \
(this is likely a bug, please report it): {_err}"
);
return None;
}
@ -48,7 +48,7 @@ impl Database {
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "Bundled(available)")
f.write_str("Bundled(available)")
}
}

View file

@ -10,14 +10,12 @@ impl Database {
#[cfg(feature = "std")]
pub(crate) fn from_path(
path: &std::path::Path,
_path: &std::path::Path,
) -> Result<Database, crate::Error> {
Err(crate::error::err!(
"system concatenated tzdb unavailable: \
crate feature `tzdb-concatenated` is disabled, \
opening tzdb at {path} has therefore failed",
path = path.display(),
))
Err(crate::error::Error::from(
crate::error::CrateFeatureError::TzdbConcatenated,
)
.context(crate::error::tz::db::Error::DisabledConcatenated))
}
pub(crate) fn none() -> Database {
@ -41,6 +39,6 @@ impl Database {
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "Concatenated(unavailable)")
f.write_str("Concatenated(unavailable)")
}
}

View file

@ -13,7 +13,7 @@ use std::{
};
use crate::{
error::{err, Error},
error::{tz::db::Error as E, Error},
timestamp::Timestamp,
tz::{
concatenated::ConcatenatedTzif, db::special_time_zone, TimeZone,
@ -203,13 +203,13 @@ impl Database {
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "Concatenated(")?;
f.write_str("Concatenated(")?;
if let Some(ref path) = self.path {
write!(f, "{}", path.display())?;
path.display().fmt(f)?;
} else {
write!(f, "unavailable")?;
f.write_str("unavailable")?;
}
write!(f, ")")
f.write_str(")")
}
}
@ -540,11 +540,7 @@ fn read_names_and_version(
let names: Vec<Arc<str>> =
db.available(scratch)?.into_iter().map(Arc::from).collect();
if names.is_empty() {
return Err(err!(
"found no IANA time zone identifiers in \
concatenated tzdata file at {path}",
path = path.display(),
));
return Err(Error::from(E::ConcatenatedMissingIanaIdentifiers));
}
Ok((names, db.version()))
}

View file

@ -1,5 +1,5 @@
use crate::{
error::{err, Error},
error::{tz::db::Error as E, Error},
tz::TimeZone,
util::{sync::Arc, utf8},
};
@ -457,22 +457,10 @@ impl TimeZoneDatabase {
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub fn get(&self, name: &str) -> Result<TimeZone, Error> {
let inner = self.inner.as_deref().ok_or_else(|| {
if cfg!(feature = "std") {
err!(
"failed to find time zone `{name}` since there is no \
time zone database configured",
)
} else {
err!(
"failed to find time zone `{name}`, there is no \
global time zone database configured (and is currently \
impossible to do so without Jiff's `std` feature \
enabled, if you need this functionality, please file \
an issue on Jiff's tracker with your use case)",
)
}
})?;
let inner = self
.inner
.as_deref()
.ok_or_else(|| E::failed_time_zone_no_database_configured(name))?;
match *inner {
Kind::ZoneInfo(ref db) => {
if let Some(tz) = db.get(name) {
@ -493,7 +481,7 @@ impl TimeZoneDatabase {
}
}
}
Err(err!("failed to find time zone `{name}` in time zone database"))
Err(Error::from(E::failed_time_zone(name)))
}
/// Returns a list of all available time zone identifiers from this
@ -572,16 +560,16 @@ impl TimeZoneDatabase {
impl core::fmt::Debug for TimeZoneDatabase {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "TimeZoneDatabase(")?;
f.write_str("TimeZoneDatabase(")?;
let Some(inner) = self.inner.as_deref() else {
return write!(f, "unavailable)");
return f.write_str("unavailable)");
};
match *inner {
Kind::ZoneInfo(ref db) => write!(f, "{db:?}")?,
Kind::Concatenated(ref db) => write!(f, "{db:?}")?,
Kind::Bundled(ref db) => write!(f, "{db:?}")?,
Kind::ZoneInfo(ref db) => core::fmt::Debug::fmt(db, f)?,
Kind::Concatenated(ref db) => core::fmt::Debug::fmt(db, f)?,
Kind::Bundled(ref db) => core::fmt::Debug::fmt(db, f)?,
}
write!(f, ")")
f.write_str(")")
}
}
@ -687,7 +675,7 @@ impl<'d> TimeZoneName<'d> {
impl<'d> core::fmt::Display for TimeZoneName<'d> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "{}", self.as_str())
f.write_str(self.as_str())
}
}

View file

@ -10,14 +10,12 @@ impl Database {
#[cfg(feature = "std")]
pub(crate) fn from_dir(
dir: &std::path::Path,
_dir: &std::path::Path,
) -> Result<Database, crate::Error> {
Err(crate::error::err!(
"system tzdb unavailable: \
crate feature `tzdb-zoneinfo` is disabled, \
opening tzdb at {dir} has therefore failed",
dir = dir.display(),
))
Err(crate::error::Error::from(
crate::error::CrateFeatureError::TzdbZoneInfo,
)
.context(crate::error::tz::db::Error::DisabledZoneInfo))
}
pub(crate) fn none() -> Database {
@ -41,6 +39,6 @@ impl Database {
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "ZoneInfo(unavailable)")
f.write_str("ZoneInfo(unavailable)")
}
}

View file

@ -17,7 +17,7 @@ use std::{
};
use crate::{
error::{err, Error},
error::{tz::db::Error as E, Error},
timestamp::Timestamp,
tz::{
db::special_time_zone, tzif::is_possibly_tzif, TimeZone,
@ -204,13 +204,13 @@ impl Database {
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "ZoneInfo(")?;
f.write_str("ZoneInfo(")?;
if let Some(ref dir) = self.dir {
write!(f, "{}", dir.display())?;
core::fmt::Display::fmt(&dir.display(), f)?;
} else {
write!(f, "unavailable")?;
f.write_str("unavailable")?;
}
write!(f, ")")
f.write_str(")")
}
}
@ -560,7 +560,7 @@ impl ZoneInfoName {
fn new(base: &Path, time_zone_name: &Path) -> Result<ZoneInfoName, Error> {
let full = base.join(time_zone_name);
let original = parse::os_str_utf8(time_zone_name.as_os_str())
.map_err(|err| err.path(base))?;
.map_err(|err| Error::from(err).path(base))?;
let lower = original.to_ascii_lowercase();
let inner = ZoneInfoNameInner {
full,
@ -792,14 +792,18 @@ fn walk(start: &Path) -> Result<Vec<ZoneInfoName>, Error> {
let time_zone_name = match path.strip_prefix(start) {
Ok(time_zone_name) => time_zone_name,
Err(err) => {
// I think this error case is actually not possible.
// Or if it does, is a legitimate bug. Namely, `start`
// should always be a prefix of `path`, since `path`
// is itself derived, ultimately, from `start`.
Err(_err) => {
trace!(
"failed to extract time zone name from {} \
using {} as a base: {err}",
using {} as a base: {_err}",
path.display(),
start.display(),
);
seterr(&path, Error::adhoc(err));
seterr(&path, Error::from(E::ZoneInfoStripPrefix));
continue;
}
};
@ -817,7 +821,7 @@ fn walk(start: &Path) -> Result<Vec<ZoneInfoName>, Error> {
if names.is_empty() {
let err = first_err
.take()
.unwrap_or_else(|| err!("{}: no TZif files", start.display()));
.unwrap_or_else(|| Error::from(E::ZoneInfoNoTzifFiles));
Err(err)
} else {
// If we found at least one valid name, then we declare success and

View file

@ -6,7 +6,7 @@ use core::{
use crate::{
civil,
duration::{Duration, SDuration},
error::{err, Error, ErrorContext},
error::{tz::offset::Error as E, Error, ErrorContext},
shared::util::itime::IOffset,
span::Span,
timestamp::Timestamp,
@ -526,12 +526,8 @@ impl Offset {
.to_idatetime()
.zip2(self.to_ioffset())
.map(|(idt, ioff)| idt.to_timestamp(ioff));
Timestamp::from_itimestamp(its).with_context(|| {
err!(
"converting {dt} with offset {offset} to timestamp overflowed",
offset = self,
)
})
Timestamp::from_itimestamp(its)
.context(E::ConvertDateTimeToTimestamp { offset: self })
}
/// Adds the given span of time to this offset.
@ -660,21 +656,11 @@ impl Offset {
) -> Result<Offset, Error> {
let duration =
t::SpanZoneOffset::try_new("duration-seconds", duration.as_secs())
.with_context(|| {
err!(
"adding signed duration {duration:?} \
to offset {self} overflowed maximum offset seconds"
)
})?;
.context(E::OverflowAddSignedDuration)?;
let offset_seconds = self.seconds_ranged();
let seconds = offset_seconds
.try_checked_add("offset-seconds", duration)
.with_context(|| {
err!(
"adding signed duration {duration:?} \
to offset {self} overflowed"
)
})?;
.context(E::OverflowAddSignedDuration)?;
Ok(Offset::from_seconds_ranged(seconds))
}
@ -975,8 +961,7 @@ impl Offset {
/// assert_eq!(Offset::MAX.to_string(), "+25:59:59");
/// assert_eq!(
/// Offset::MAX.round(Unit::Minute).unwrap_err().to_string(),
/// "rounding offset `+25:59:59` resulted in a duration of 26h, \
/// which overflows `Offset`",
/// "rounding time zone offset resulted in a duration that overflows",
/// );
/// ```
#[inline]
@ -1125,7 +1110,7 @@ impl core::fmt::Display for Offset {
let minutes = self.part_minutes_ranged().abs().get();
let seconds = self.part_seconds_ranged().abs().get();
if hours == 0 && minutes == 0 && seconds == 0 {
write!(f, "+00")
f.write_str("+00")
} else if hours != 0 && minutes == 0 && seconds == 0 {
write!(f, "{sign}{hours:02}")
} else if minutes != 0 && seconds == 0 {
@ -1347,11 +1332,10 @@ impl TryFrom<SignedDuration> for Offset {
} else if subsec <= -500_000_000 {
seconds = seconds.saturating_sub(1);
}
let seconds = i32::try_from(seconds).map_err(|_| {
err!("`SignedDuration` of {sdur} overflows `Offset`")
})?;
let seconds =
i32::try_from(seconds).map_err(|_| E::OverflowSignedDuration)?;
Offset::from_seconds(seconds)
.map_err(|_| err!("`SignedDuration` of {sdur} overflows `Offset`"))
.map_err(|_| Error::from(E::OverflowSignedDuration))
}
}
@ -1599,20 +1583,11 @@ impl OffsetRound {
fn round(&self, offset: Offset) -> Result<Offset, Error> {
let smallest = self.0.get_smallest();
if !(Unit::Second <= smallest && smallest <= Unit::Hour) {
return Err(err!(
"rounding `Offset` failed because \
a unit of {plural} was provided, but offset rounding \
can only use hours, minutes or seconds",
plural = smallest.plural(),
));
return Err(Error::from(E::RoundInvalidUnit { unit: smallest }));
}
let rounded_sdur = SignedDuration::from(offset).round(self.0)?;
Offset::try_from(rounded_sdur).map_err(|_| {
err!(
"rounding offset `{offset}` resulted in a duration \
of {rounded_sdur:?}, which overflows `Offset`",
)
})
Offset::try_from(rounded_sdur)
.map_err(|_| Error::from(E::RoundOverflow))
}
}
@ -1888,10 +1863,10 @@ impl OffsetConflict {
/// let result = OffsetConflict::Reject.resolve(dt, offset, tz.clone());
/// assert_eq!(
/// result.unwrap_err().to_string(),
/// "datetime 1968-02-01T23:15:00 could not resolve to a timestamp \
/// since 'reject' conflict resolution was chosen, and because \
/// datetime has offset -00:45, but the time zone Africa/Monrovia \
/// for the given datetime unambiguously has offset -00:44:30",
/// "datetime could not resolve to a timestamp since `reject` \
/// conflict resolution was chosen, and because datetime has offset \
/// `-00:45`, but the time zone `Africa/Monrovia` for the given \
/// datetime unambiguously has offset `-00:44:30`",
/// );
/// let is_equal = |parsed: Offset, candidate: Offset| {
/// parsed == candidate || candidate.round(Unit::Minute).map_or(
@ -1950,11 +1925,10 @@ impl OffsetConflict {
/// let result = "1970-06-01T00-00:45:00[Africa/Monrovia]".parse::<Zoned>();
/// assert_eq!(
/// result.unwrap_err().to_string(),
/// "parsing \"1970-06-01T00-00:45:00[Africa/Monrovia]\" failed: \
/// datetime 1970-06-01T00:00:00 could not resolve to a timestamp \
/// since 'reject' conflict resolution was chosen, and because \
/// datetime has offset -00:45, but the time zone Africa/Monrovia \
/// for the given datetime unambiguously has offset -00:44:30",
/// "datetime could not resolve to a timestamp since `reject` \
/// conflict resolution was chosen, and because datetime has offset \
/// `-00:45`, but the time zone `Africa/Monrovia` for the given \
/// datetime unambiguously has offset `-00:44:30`",
/// );
/// ```
pub fn resolve_with<F>(
@ -2046,13 +2020,13 @@ impl OffsetConflict {
let amb = tz.to_ambiguous_timestamp(dt);
match amb.offset() {
Unambiguous { offset } if !is_equal(given, offset) => Err(err!(
"datetime {dt} could not resolve to a timestamp since \
'reject' conflict resolution was chosen, and because \
datetime has offset {given}, but the time zone {tzname} for \
the given datetime unambiguously has offset {offset}",
tzname = tz.diagnostic_name(),
)),
Unambiguous { offset } if !is_equal(given, offset) => {
Err(Error::from(E::ResolveRejectUnambiguous {
given,
offset,
tz,
}))
}
Unambiguous { .. } => Ok(amb.into_ambiguous_zoned(tz)),
Gap { before, after } => {
// In `jiff 0.1`, we reported an error when we found a gap
@ -2065,28 +2039,22 @@ impl OffsetConflict {
// changed to treat all offsets in a gap as invalid).
//
// Ref: https://github.com/tc39/proposal-temporal/issues/2892
Err(err!(
"datetime {dt} could not resolve to timestamp \
since 'reject' conflict resolution was chosen, and \
because datetime has offset {given}, but the time \
zone {tzname} for the given datetime falls in a gap \
(between offsets {before} and {after}), and all \
offsets for a gap are regarded as invalid",
tzname = tz.diagnostic_name(),
))
Err(Error::from(E::ResolveRejectGap {
given,
before,
after,
tz,
}))
}
Fold { before, after }
if !is_equal(given, before) && !is_equal(given, after) =>
{
Err(err!(
"datetime {dt} could not resolve to timestamp \
since 'reject' conflict resolution was chosen, and \
because datetime has offset {given}, but the time \
zone {tzname} for the given datetime falls in a fold \
between offsets {before} and {after}, neither of which \
match the offset",
tzname = tz.diagnostic_name(),
))
Err(Error::from(E::ResolveRejectFold {
given,
before,
after,
tz,
}))
}
Fold { .. } => {
let kind = Unambiguous { offset: given };

View file

@ -72,14 +72,14 @@ use core::fmt::Debug;
use crate::{
civil::DateTime,
error::{err, Error, ErrorContext},
error::{tz::posix::Error as E, Error, ErrorContext},
shared,
timestamp::Timestamp,
tz::{
timezone::TimeZoneAbbreviation, AmbiguousOffset, Dst, Offset,
TimeZoneOffsetInfo, TimeZoneTransition,
},
util::{array_str::Abbreviation, escape::Bytes, parse},
util::{array_str::Abbreviation, parse},
};
/// The result of parsing the POSIX `TZ` environment variable.
@ -114,11 +114,7 @@ impl PosixTzEnv {
let bytes = bytes.as_ref();
if bytes.get(0) == Some(&b':') {
let Ok(string) = core::str::from_utf8(&bytes[1..]) else {
return Err(err!(
"POSIX time zone string with a ':' prefix contains \
invalid UTF-8: {:?}",
Bytes(&bytes[1..]),
));
return Err(Error::from(E::ColonPrefixInvalidUtf8));
};
Ok(PosixTzEnv::Implementation(string.into()))
} else {
@ -138,8 +134,11 @@ impl PosixTzEnv {
impl core::fmt::Display for PosixTzEnv {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match *self {
PosixTzEnv::Rule(ref tz) => write!(f, "{tz}"),
PosixTzEnv::Implementation(ref imp) => write!(f, ":{imp}"),
PosixTzEnv::Rule(ref tz) => core::fmt::Display::fmt(tz, f),
PosixTzEnv::Implementation(ref imp) => {
f.write_str(":")?;
core::fmt::Display::fmt(imp, f)
}
}
}
}
@ -211,10 +210,8 @@ impl PosixTimeZone<Abbreviation> {
) -> Result<PosixTimeZoneOwned, Error> {
let bytes = bytes.as_ref();
let inner = shared::PosixTimeZone::parse(bytes.as_ref())
.map_err(Error::shared)
.map_err(|e| {
e.context(err!("invalid POSIX TZ string {:?}", Bytes(bytes)))
})?;
.map_err(Error::posix_tz)
.context(E::InvalidPosixTz)?;
Ok(PosixTimeZone { inner })
}
@ -227,13 +224,8 @@ impl PosixTimeZone<Abbreviation> {
let bytes = bytes.as_ref();
let (inner, remaining) =
shared::PosixTimeZone::parse_prefix(bytes.as_ref())
.map_err(Error::shared)
.map_err(|e| {
e.context(err!(
"invalid POSIX TZ string {:?}",
Bytes(bytes)
))
})?;
.map_err(Error::posix_tz)
.context(E::InvalidPosixTz)?;
Ok((PosixTimeZone { inner }, remaining))
}

View file

@ -3,7 +3,7 @@ use std::{sync::RwLock, time::Duration};
use alloc::string::ToString;
use crate::{
error::{err, Error, ErrorContext},
error::{tz::system::Error as E, Error, ErrorContext},
tz::{posix::PosixTzEnv, TimeZone, TimeZoneDatabase},
util::cache::Expiration,
};
@ -141,22 +141,20 @@ pub(crate) fn get(db: &TimeZoneDatabase) -> Result<TimeZone, Error> {
pub(crate) fn get_force(db: &TimeZoneDatabase) -> Result<TimeZone, Error> {
match get_env_tz(db) {
Ok(Some(tz)) => {
debug!("checked TZ environment variable and found {tz:?}");
debug!("checked `TZ` environment variable and found {tz:?}");
return Ok(tz);
}
Ok(None) => {
debug!("TZ environment variable is not set");
debug!("`TZ` environment variable is not set");
}
Err(err) => {
return Err(err.context(
"TZ environment variable set, but failed to read value",
));
return Err(err.context(E::FailedEnvTz));
}
}
if let Some(tz) = sys::get(db) {
return Ok(tz);
}
Err(err!("failed to find system time zone"))
Err(Error::from(E::FailedSystemTimeZone))
}
/// Materializes a `TimeZone` from a `TZ` environment variable.
@ -184,8 +182,8 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
// `TZ=UTC`.
if tzenv.is_empty() {
debug!(
"TZ environment variable set to empty value, \
assuming TZ=UTC in order to conform to \
"`TZ` environment variable set to empty value, \
assuming `TZ=UTC` in order to conform to \
widespread convention among Unix tooling",
);
return Ok(Some(TimeZone::UTC));
@ -196,15 +194,7 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
"failed to parse {tzenv:?} as POSIX TZ rule \
(attempting to treat it as an IANA time zone): {_err}",
);
tzenv
.to_str()
.ok_or_else(|| {
err!(
"failed to parse {tzenv:?} as a POSIX TZ transition \
string, or as valid UTF-8",
)
})?
.to_string()
tzenv.to_str().ok_or(E::FailedPosixTzAndUtf8)?.to_string()
}
Ok(PosixTzEnv::Implementation(string)) => string.to_string(),
Ok(PosixTzEnv::Rule(tz)) => {
@ -231,26 +221,20 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
// No zoneinfo means this is probably a IANA Time Zone name. But...
// it could just be a file path.
debug!(
"could not find {needle:?} in TZ={tz_name_or_path:?}, \
therefore attempting lookup in {db:?}",
"could not find {needle:?} in `TZ={tz_name_or_path:?}`, \
therefore attempting lookup in `{db:?}`",
);
return match db.get(&tz_name_or_path) {
Ok(tz) => Ok(Some(tz)),
Err(_err) => {
debug!(
"using TZ={tz_name_or_path:?} as time zone name failed, \
could not find time zone in zoneinfo database {db:?} \
"using `TZ={tz_name_or_path:?}` as time zone name failed, \
could not find time zone in zoneinfo database `{db:?}` \
(continuing to try and read `{tz_name_or_path}` as \
a TZif file)",
);
sys::read(db, &tz_name_or_path)
.ok_or_else(|| {
err!(
"failed to read TZ={tz_name_or_path:?} \
as a TZif file after attempting a tzdb \
lookup for `{tz_name_or_path}`",
)
})
.ok_or_else(|| Error::from(E::FailedEnvTzAsTzif))
.map(Some)
}
};
@ -260,16 +244,16 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
// `zoneinfo/`. Once we have that, we try to look it up in our tzdb.
let name = &tz_name_or_path[rpos + needle.len()..];
debug!(
"extracted {name:?} from TZ={tz_name_or_path:?} \
"extracted `{name}` from `TZ={tz_name_or_path}` \
and assuming it is an IANA time zone name",
);
match db.get(&name) {
Ok(tz) => return Ok(Some(tz)),
Err(_err) => {
debug!(
"using {name:?} from TZ={tz_name_or_path:?}, \
could not find time zone in zoneinfo database {db:?} \
(continuing to try and use {tz_name_or_path:?})",
"using `{name}` from `TZ={tz_name_or_path}`, \
could not find time zone in zoneinfo database `{db:?}` \
(continuing to try and use `{tz_name_or_path}`)",
);
}
}
@ -279,13 +263,7 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
// and read the data as TZif. This will give us time zone data if it works,
// but without a name.
sys::read(db, &tz_name_or_path)
.ok_or_else(|| {
err!(
"failed to read TZ={tz_name_or_path:?} \
as a TZif file after attempting a tzdb \
lookup for `{name}`",
)
})
.ok_or_else(|| Error::from(E::FailedEnvTzAsTzif))
.map(Some)
}
@ -298,8 +276,8 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
fn read_unnamed_tzif_file(path: &str) -> Result<TimeZone, Error> {
let data = std::fs::read(path)
.map_err(Error::io)
.with_context(|| err!("failed to read {path:?} as TZif file"))?;
let tz = TimeZone::tzif_system(&data)
.with_context(|| err!("found invalid TZif data at {path:?}"))?;
.context(E::FailedUnnamedTzifRead)?;
let tz =
TimeZone::tzif_system(&data).context(E::FailedUnnamedTzifInvalid)?;
Ok(tz)
}

View file

@ -8,7 +8,7 @@ use windows_sys::Win32::System::Time::{
};
use crate::{
error::{err, Error, ErrorContext},
error::{tz::system::Error as E, Error, ErrorContext},
tz::{TimeZone, TimeZoneDatabase},
util::utf8,
};
@ -79,16 +79,12 @@ fn windows_to_iana(tz_key_name: &str) -> Result<&'static str, Error> {
utf8::cmp_ignore_ascii_case(win_name, &tz_key_name)
});
let Ok(index) = result else {
return Err(err!(
"found Windows time zone name {tz_key_name}, \
but could not find a mapping for it to an \
IANA time zone name",
));
return Err(Error::from(E::WindowsMissingIanaMapping));
};
let iana_name = WINDOWS_TO_IANA[index].1;
trace!(
"found Windows time zone name {tz_key_name}, and \
successfully mapped it to IANA time zone {iana_name}",
"found Windows time zone name `{tz_key_name}`, and \
successfully mapped it to IANA time zone `{iana_name}`",
);
Ok(iana_name)
}
@ -107,24 +103,19 @@ fn get_tz_key_name() -> Result<String, Error> {
// when `info` is properly initialized.
let info = unsafe { info.assume_init() };
let tz_key_name = nul_terminated_utf16_to_string(&info.TimeZoneKeyName)
.context(
"could not get TimeZoneKeyName from \
winapi DYNAMIC_TIME_ZONE_INFORMATION",
)?;
.context(E::WindowsTimeZoneKeyName)?;
Ok(tz_key_name)
}
fn nul_terminated_utf16_to_string(
code_units: &[u16],
) -> Result<String, Error> {
let nul = code_units.iter().position(|&cu| cu == 0).ok_or_else(|| {
err!("failed to convert u16 slice to UTF-8 (no NUL terminator found)")
})?;
let nul = code_units
.iter()
.position(|&cu| cu == 0)
.ok_or(E::WindowsUtf16DecodeNul)?;
let string = String::from_utf16(&code_units[..nul])
.map_err(Error::adhoc)
.with_context(|| {
err!("failed to convert u16 slice to UTF-8 (invalid UTF-16)")
})?;
.map_err(|_| E::WindowsUtf16DecodeInvalid)?;
Ok(string)
}

View file

@ -1,6 +1,6 @@
use crate::{
civil::DateTime,
error::{err, Error},
error::{tz::timezone::Error as E, Error},
tz::{
ambiguous::{AmbiguousOffset, AmbiguousTimestamp, AmbiguousZoned},
offset::{Dst, Offset},
@ -392,10 +392,8 @@ impl TimeZone {
pub fn try_system() -> Result<TimeZone, Error> {
#[cfg(not(feature = "tz-system"))]
{
Err(err!(
"failed to get system time zone since 'tz-system' \
crate feature is not enabled",
))
Err(Error::from(crate::error::CrateFeatureError::TzSystem)
.context(E::FailedSystem))
}
#[cfg(feature = "tz-system")]
{
@ -916,7 +914,7 @@ impl TimeZone {
/// assert_eq!(
/// tz.to_fixed_offset().unwrap_err().to_string(),
/// "cannot convert non-fixed IANA time zone \
/// to offset without timestamp or civil datetime",
/// to offset without a timestamp or civil datetime",
/// );
///
/// let tz = TimeZone::UTC;
@ -935,11 +933,7 @@ impl TimeZone {
#[inline]
pub fn to_fixed_offset(&self) -> Result<Offset, Error> {
let mkerr = || {
err!(
"cannot convert non-fixed {kind} time zone to offset \
without timestamp or civil datetime",
kind = self.kind_description(),
)
Error::from(E::ConvertNonFixed { kind: self.kind_description() })
};
repr::each! {
&self.repr,
@ -1392,7 +1386,7 @@ impl TimeZone {
/// Returns a short description about the kind of this time zone.
///
/// This is useful in error messages.
fn kind_description(&self) -> &str {
fn kind_description(&self) -> &'static str {
repr::each! {
&self.repr,
UTC => "UTC",
@ -1887,12 +1881,12 @@ impl<'a> core::fmt::Display for DiagnosticName<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
repr::each! {
&self.0.repr,
UTC => write!(f, "UTC"),
UNKNOWN => write!(f, "Etc/Unknown"),
FIXED(offset) => write!(f, "{offset}"),
STATIC_TZIF(tzif) => write!(f, "{}", tzif.name().unwrap_or("Local")),
ARC_TZIF(tzif) => write!(f, "{}", tzif.name().unwrap_or("Local")),
ARC_POSIX(posix) => write!(f, "{posix}"),
UTC => f.write_str("UTC"),
UNKNOWN => f.write_str("Etc/Unknown"),
FIXED(offset) => offset.fmt(f),
STATIC_TZIF(tzif) => f.write_str(tzif.name().unwrap_or("Local")),
ARC_TZIF(tzif) => f.write_str(tzif.name().unwrap_or("Local")),
ARC_POSIX(posix) => posix.fmt(f),
}
}
}
@ -1939,6 +1933,10 @@ impl<'t> TimeZoneAbbreviation<'t> {
///
/// This module exists to _encapsulate_ the representation rigorously and
/// expose a safe and sound API.
// To squash warnings on older versions of Rust. Our polyfill below should
// match what std does on newer versions of Rust, so the confusability should
// be fine. ---AG
#[allow(unstable_name_collisions)]
mod repr {
use core::mem::ManuallyDrop;
@ -2271,9 +2269,9 @@ mod repr {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
each! {
self,
UTC => write!(f, "UTC"),
UNKNOWN => write!(f, "Etc/Unknown"),
FIXED(offset) => write!(f, "{offset:?}"),
UTC => f.write_str("UTC"),
UNKNOWN => f.write_str("Etc/Unknown"),
FIXED(offset) => core::fmt::Debug::fmt(&offset, f),
STATIC_TZIF(tzif) => {
// The full debug output is a bit much, so constrain it.
let field = tzif.name().unwrap_or("Local");
@ -2284,7 +2282,11 @@ mod repr {
let field = tzif.name().unwrap_or("Local");
f.debug_tuple("TZif").field(&field).finish()
},
ARC_POSIX(posix) => write!(f, "Posix({posix})"),
ARC_POSIX(posix) => {
f.write_str("Posix(")?;
core::fmt::Display::fmt(&posix, f)?;
f.write_str(")")
},
}
}
}

View file

@ -157,8 +157,7 @@ impl TzifOwned {
name: Option<String>,
bytes: &[u8],
) -> Result<Self, Error> {
let sh =
shared::TzifOwned::parse(name, bytes).map_err(Error::shared)?;
let sh = shared::TzifOwned::parse(name, bytes).map_err(Error::tzif)?;
Ok(TzifOwned::from_shared_owned(sh))
}
@ -571,9 +570,9 @@ impl shared::TzifLocalTimeType {
impl core::fmt::Display for shared::TzifIndicator {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match *self {
shared::TzifIndicator::LocalWall => write!(f, "local/wall"),
shared::TzifIndicator::LocalStandard => write!(f, "local/std"),
shared::TzifIndicator::UTStandard => write!(f, "ut/std"),
shared::TzifIndicator::LocalWall => f.write_str("local/wall"),
shared::TzifIndicator::LocalStandard => f.write_str("local/std"),
shared::TzifIndicator::UTStandard => f.write_str("ut/std"),
}
}
}

View file

@ -111,7 +111,10 @@ use alloc::{
use crate::{
civil::{Date, DateTime, Time, Weekday},
error::{err, Error, ErrorContext},
error::{
tz::zic::{Error as E, MAX_LINE_LEN},
Error, ErrorContext,
},
span::{Span, SpanFieldwise, ToSpan},
timestamp::Timestamp,
tz::{Dst, Offset},
@ -205,12 +208,12 @@ impl Rules {
"every name in rule group must be identical"
);
let dst = Dst::from(r.save.suffix() == RuleSaveSuffixP::Dst);
let offset = r.save.to_offset().map_err(|e| {
err!("SAVE value in rule {:?} is too big: {e}", inner.name)
let offset = r.save.to_offset().with_context(|| {
E::FailedRule { name: inner.name.as_str().into() }
})?;
let years = r.years().with_context(|| E::FailedRule {
name: inner.name.as_str().into(),
})?;
let years = r
.years()
.map_err(|e| e.context(err!("rule {:?}", inner.name)))?;
let month = r.inn.month;
let letters = r.letters.part;
let day = r.on;
@ -260,13 +263,12 @@ impl ZicP {
) -> Result<(), Error> {
while parser.read_next_fields()? {
self.parse_one(&mut parser)
.map_err(|e| e.context(err!("line {}", parser.line_number)))?;
.context(E::Line { number: parser.line_number })?;
}
if let Some(ref name) = parser.continuation_zone_for {
return Err(err!(
"expected continuation zone line for {name:?}, \
but found end of data instead",
));
return Err(Error::from(E::ExpectedContinuationZoneLine {
name: name.as_str().into(),
}));
}
Ok(())
}
@ -277,9 +279,8 @@ impl ZicP {
assert!(!p.fields.is_empty());
if let Some(name) = p.continuation_zone_for.take() {
let zone = ZoneContinuationP::parse(&p.fields).map_err(|e| {
e.context("failed to parse continuation 'Zone' line")
})?;
let zone = ZoneContinuationP::parse(&p.fields)
.context(E::FailedContinuationZone)?;
let more_continuations = zone.until.is_some();
// OK because `p.continuation_zone_for` is only set when we have
// seen a first zone with the corresponding name.
@ -293,51 +294,45 @@ impl ZicP {
let (first, rest) = (&p.fields[0], &p.fields[1..]);
if first.starts_with("R") && "Rule".starts_with(first) {
let rule = RuleP::parse(rest)
.map_err(|e| e.context("failed to parse 'Rule' line"))?;
let rule = RuleP::parse(rest).context(E::FailedRuleLine)?;
let name = rule.name.name.clone();
self.rules.entry(name).or_default().push(rule);
} else if first.starts_with("Z") && "Zone".starts_with(first) {
let first = ZoneFirstP::parse(rest)
.map_err(|e| e.context("failed to parse first 'Zone' line"))?;
let first = ZoneFirstP::parse(rest).context(E::FailedZoneFirst)?;
let name = first.name.name.clone();
if first.until.is_some() {
p.continuation_zone_for = Some(name.clone());
}
let zone = ZoneP { first, continuations: vec![] };
if self.links.contains_key(&name) {
return Err(err!(
"found zone with name {name:?} that conflicts \
with a link of the same name",
));
return Err(Error::from(E::DuplicateZoneLink {
name: name.into(),
}));
}
if let Some(previous_zone) = self.zones.insert(name, zone) {
return Err(err!(
"found duplicate zone for {:?}",
previous_zone.first.name.name,
));
return Err(Error::from(E::DuplicateZone {
name: previous_zone.first.name.name.into(),
}));
}
} else if first.starts_with("L") && "Link".starts_with(first) {
let link = LinkP::parse(rest)
.map_err(|e| e.context("failed to parse 'Link' line"))?;
let link = LinkP::parse(rest).context(E::FailedLinkLine)?;
let name = link.name.name.clone();
if self.zones.contains_key(&name) {
return Err(err!(
"found link with name {name:?} that conflicts \
with a zone of the same name",
));
return Err(Error::from(E::DuplicateLinkZone {
name: name.into(),
}));
}
if let Some(previous_link) = self.links.insert(name, link) {
return Err(err!(
"found duplicate link for {:?}",
previous_link.name.name,
));
return Err(Error::from(E::DuplicateLink {
name: previous_link.name.name.into(),
}));
}
// N.B. We don't check that the link's target name refers to some
// other zone/link here, because the corresponding zone/link might
// be defined later.
} else {
return Err(err!("unrecognized zic line: {first:?}"));
return Err(Error::from(E::UnrecognizedZicLine)
.context(E::Line { number: p.line_number }));
}
Ok(())
}
@ -369,10 +364,9 @@ struct RuleP {
impl RuleP {
fn parse(fields: &[&str]) -> Result<RuleP, Error> {
if fields.len() != 9 {
return Err(err!(
"expected exactly 9 fields for rule, but found {} fields",
fields.len(),
));
return Err(Error::from(E::ExpectedRuleNineFields {
got: fields.len(),
}));
}
let (name_field, fields) = (fields[0], &fields[1..]);
let (from_field, fields) = (fields[0], &fields[1..]);
@ -386,28 +380,21 @@ impl RuleP {
let name = name_field
.parse::<RuleNameP>()
.map_err(|e| e.context("failed to parse NAME field"))?;
.context(E::FailedParseFieldName)?;
let from = from_field
.parse::<RuleFromP>()
.map_err(|e| e.context("failed to parse FROM field"))?;
let to = to_field
.parse::<RuleToP>()
.map_err(|e| e.context("failed to parse TO field"))?;
let inn = in_field
.parse::<RuleInP>()
.map_err(|e| e.context("failed to parse IN field"))?;
let on = on_field
.parse::<RuleOnP>()
.map_err(|e| e.context("failed to parse ON field"))?;
let at = at_field
.parse::<RuleAtP>()
.map_err(|e| e.context("failed to parse AT field"))?;
.context(E::FailedParseFieldFrom)?;
let to = to_field.parse::<RuleToP>().context(E::FailedParseFieldTo)?;
let inn =
in_field.parse::<RuleInP>().context(E::FailedParseFieldIn)?;
let on = on_field.parse::<RuleOnP>().context(E::FailedParseFieldOn)?;
let at = at_field.parse::<RuleAtP>().context(E::FailedParseFieldAt)?;
let save = save_field
.parse::<RuleSaveP>()
.map_err(|e| e.context("failed to parse SAVE field"))?;
.context(E::FailedParseFieldSave)?;
let letters = letters_field
.parse::<RuleLettersP>()
.map_err(|e| e.context("failed to parse LETTERS field"))?;
.context(E::FailedParseFieldLetters)?;
Ok(RuleP { name, from, to, inn, on, at, save, letters })
}
@ -420,9 +407,10 @@ impl RuleP {
RuleToP::Year { year } => year,
};
if start > end {
return Err(err!(
"found start year {start} to be greater than end year {end}"
));
return Err(Error::from(E::InvalidRuleYear {
start: start.get(),
end: end.get(),
}));
}
Ok(start..=end)
}
@ -462,7 +450,7 @@ struct ZoneFirstP {
impl ZoneFirstP {
fn parse(fields: &[&str]) -> Result<ZoneFirstP, Error> {
if fields.len() < 4 {
return Err(err!("first ZONE line must have at least 4 fields"));
return Err(Error::from(E::ExpectedFirstZoneFourFields));
}
let (name_field, fields) = (fields[0], &fields[1..]);
let (stdoff_field, fields) = (fields[0], &fields[1..]);
@ -470,23 +458,20 @@ impl ZoneFirstP {
let (format_field, fields) = (fields[0], &fields[1..]);
let name = name_field
.parse::<ZoneNameP>()
.map_err(|e| e.context("failed to parse NAME field"))?;
.context(E::FailedParseFieldName)?;
let stdoff = stdoff_field
.parse::<ZoneStdoffP>()
.map_err(|e| e.context("failed to parse STDOFF field"))?;
.context(E::FailedParseFieldStdOff)?;
let rules = rules_field
.parse::<ZoneRulesP>()
.map_err(|e| e.context("failed to parse RULES field"))?;
.context(E::FailedParseFieldRules)?;
let format = format_field
.parse::<ZoneFormatP>()
.map_err(|e| e.context("failed to parse FORMAT field"))?;
.context(E::FailedParseFieldFormat)?;
let until = if fields.is_empty() {
None
} else {
Some(
ZoneUntilP::parse(fields)
.map_err(|e| e.context("failed to parse UNTIL field"))?,
)
Some(ZoneUntilP::parse(fields).context(E::FailedParseFieldUntil)?)
};
Ok(ZoneFirstP { name, stdoff, rules, format, until })
}
@ -512,29 +497,24 @@ struct ZoneContinuationP {
impl ZoneContinuationP {
fn parse(fields: &[&str]) -> Result<ZoneContinuationP, Error> {
if fields.len() < 3 {
return Err(err!(
"continuation ZONE line must have at least 3 fields"
));
return Err(Error::from(E::ExpectedContinuationZoneThreeFields));
}
let (stdoff_field, fields) = (fields[0], &fields[1..]);
let (rules_field, fields) = (fields[0], &fields[1..]);
let (format_field, fields) = (fields[0], &fields[1..]);
let stdoff = stdoff_field
.parse::<ZoneStdoffP>()
.map_err(|e| e.context("failed to parse STDOFF field"))?;
.context(E::FailedParseFieldStdOff)?;
let rules = rules_field
.parse::<ZoneRulesP>()
.map_err(|e| e.context("failed to parse RULES field"))?;
.context(E::FailedParseFieldRules)?;
let format = format_field
.parse::<ZoneFormatP>()
.map_err(|e| e.context("failed to parse FORMAT field"))?;
.context(E::FailedParseFieldFormat)?;
let until = if fields.is_empty() {
None
} else {
Some(
ZoneUntilP::parse(fields)
.map_err(|e| e.context("failed to parse UNTIL field"))?,
)
Some(ZoneUntilP::parse(fields).context(E::FailedParseFieldUntil)?)
};
Ok(ZoneContinuationP { stdoff, rules, format, until })
}
@ -553,17 +533,14 @@ struct LinkP {
impl LinkP {
fn parse(fields: &[&str]) -> Result<LinkP, Error> {
if fields.len() != 2 {
return Err(err!(
"expected exactly 2 fields after LINK, but found {}",
fields.len()
));
return Err(Error::from(E::ExpectedLinkTwoFields));
}
let target = fields[0]
.parse::<ZoneNameP>()
.map_err(|e| e.context("failed to parse LINK target"))?;
.context(E::FailedParseFieldLinkTarget)?;
let name = fields[1]
.parse::<ZoneNameP>()
.map_err(|e| e.context("failed to parse LINK name"))?;
.context(E::FailedParseFieldLinkName)?;
Ok(LinkP { target, name })
}
}
@ -592,12 +569,9 @@ impl FromStr for RuleNameP {
// or not. We erase that information. We could rejigger things to keep
// that information around, but... Meh.
if name.is_empty() {
Err(err!("NAME field for rule cannot be empty"))
Err(Error::from(E::ExpectedNonEmptyName))
} else if name.starts_with(|ch| matches!(ch, '0'..='9' | '+' | '-')) {
Err(err!(
"NAME field cannot begin with a digit, + or -, \
but {name:?} begins with one of those",
))
Err(Error::from(E::ExpectedNameBegin))
} else {
Ok(RuleNameP { name: name.to_string() })
}
@ -614,8 +588,7 @@ impl FromStr for RuleFromP {
type Err = Error;
fn from_str(from: &str) -> Result<RuleFromP, Error> {
let year = parse_year(from)
.map_err(|e| e.context("failed to parse FROM field"))?;
let year = parse_year(from).context(E::FailedParseFieldFrom)?;
Ok(RuleFromP { year })
}
}
@ -642,8 +615,7 @@ impl FromStr for RuleToP {
} else if to.starts_with("o") && "only".starts_with(to) {
Ok(RuleToP::Only)
} else {
let year = parse_year(to)
.map_err(|e| e.context("failed to parse TO field"))?;
let year = parse_year(to).context(E::FailedParseFieldTo)?;
Ok(RuleToP::Year { year })
}
}
@ -679,7 +651,7 @@ impl FromStr for RuleInP {
return Ok(RuleInP { month });
}
}
Err(err!("unrecognized month name: {field:?}"))
Err(Error::from(E::UnrecognizedMonthName))
}
}
@ -747,7 +719,7 @@ impl FromStr for RuleOnP {
// field. That gets checked at a higher level.
Ok(RuleOnP::Day { day })
} else {
Err(err!("unrecognized format for day-of-month: {field:?}"))
Err(Error::from(E::UnrecognizedDayOfMonthFormat))
}
}
}
@ -782,7 +754,7 @@ impl FromStr for RuleAtP {
fn from_str(at: &str) -> Result<RuleAtP, Error> {
if at.is_empty() {
return Err(err!("empty field is not a valid AT value"));
return Err(Error::from(E::ExpectedNonEmptyAt));
}
let (span_string, suffix_string) = at.split_at(at.len() - 1);
if suffix_string.chars().all(|ch| ch.is_ascii_alphabetic()) {
@ -813,7 +785,7 @@ impl FromStr for RuleAtSuffixP {
"w" => Ok(RuleAtSuffixP::Wall),
"s" => Ok(RuleAtSuffixP::Standard),
"u" | "g" | "z" => Ok(RuleAtSuffixP::Universal),
_ => Err(err!("unrecognized AT time suffix {suffix:?}")),
_ => Err(Error::from(E::UnrecognizedAtTimeSuffix)),
}
}
}
@ -867,7 +839,7 @@ impl FromStr for RuleSaveP {
fn from_str(at: &str) -> Result<RuleSaveP, Error> {
if at.is_empty() {
return Err(err!("empty field is not a valid SAVE value"));
return Err(Error::from(E::ExpectedNonEmptySave));
}
let (span_string, suffix_string) = at.split_at(at.len() - 1);
if suffix_string.chars().all(|ch| ch.is_ascii_alphabetic()) {
@ -902,7 +874,7 @@ impl FromStr for RuleSaveSuffixP {
match suffix {
"s" => Ok(RuleSaveSuffixP::Standard),
"d" => Ok(RuleSaveSuffixP::Dst),
_ => Err(err!("unrecognized SAVE time suffix {suffix:?}")),
_ => Err(Error::from(E::UnrecognizedSaveTimeSuffix)),
}
}
}
@ -939,14 +911,13 @@ impl FromStr for ZoneNameP {
fn from_str(name: &str) -> Result<ZoneNameP, Error> {
if name.is_empty() {
return Err(err!("zone names cannot be empty"));
return Err(Error::from(E::ExpectedNonEmptyZoneName));
}
for component in name.split('/') {
if component == "." || component == ".." {
return Err(err!(
"component {component:?} in zone name {name:?} cannot \
be \".\" or \"..\"",
));
return Err(Error::from(E::ExpectedZoneNameComponentNoDots {
component: component.into(),
}));
}
}
Ok(ZoneNameP { name: name.to_string() })
@ -1035,16 +1006,12 @@ impl FromStr for ZoneFormatP {
fn from_str(format: &str) -> Result<ZoneFormatP, Error> {
fn check_abbrev(abbrev: &str) -> Result<String, Error> {
if abbrev.is_empty() {
return Err(err!("empty abbreviations are not allowed"));
return Err(Error::from(E::ExpectedNonEmptyAbbreviation));
}
let is_ok =
|ch| matches!(ch, '+'|'-'|'0'..='9'|'A'..='Z'|'a'..='z');
if !abbrev.chars().all(is_ok) {
return Err(err!(
"abbreviation {abbrev:?} \
contains invalid character; only \"+\", \"-\" and \
ASCII alpha-numeric characters are allowed"
));
return Err(Error::from(E::InvalidAbbreviation));
}
Ok(abbrev.to_string())
}
@ -1098,28 +1065,24 @@ enum ZoneUntilP {
impl ZoneUntilP {
fn parse(fields: &[&str]) -> Result<ZoneUntilP, Error> {
if fields.is_empty() {
return Err(err!("expected at least a year"));
return Err(Error::from(E::ExpectedUntilYear));
}
let (year_field, fields) = (fields[0], &fields[1..]);
let year = parse_year(year_field)
.map_err(|e| e.context("failed to parse year"))?;
let year = parse_year(year_field).context(E::FailedParseYear)?;
if fields.is_empty() {
return Ok(ZoneUntilP::Year { year });
}
let (month_field, fields) = (fields[0], &fields[1..]);
let month = month_field
.parse::<RuleInP>()
.map_err(|e| e.context("failed to parse month"))?;
let month =
month_field.parse::<RuleInP>().context(E::FailedParseMonth)?;
if fields.is_empty() {
return Ok(ZoneUntilP::YearMonth { year, month });
}
let (day_field, fields) = (fields[0], &fields[1..]);
let day = day_field
.parse::<RuleOnP>()
.map_err(|e| e.context("failed to parse day"))?;
let day = day_field.parse::<RuleOnP>().context(E::FailedParseDay)?;
if fields.is_empty() {
return Ok(ZoneUntilP::YearMonthDay { year, month, day });
}
@ -1127,13 +1090,9 @@ impl ZoneUntilP {
let (duration_field, fields) = (fields[0], &fields[1..]);
let duration = duration_field
.parse::<RuleAtP>()
.map_err(|e| e.context("failed to parse time duration"))?;
.context(E::FailedParseTimeDuration)?;
if !fields.is_empty() {
return Err(err!(
"expected no more fields after time of day, \
but found: {fields:?}",
fields = fields.join(" "),
));
return Err(Error::from(E::ExpectedNothingAfterTime));
}
Ok(ZoneUntilP::YearMonthDayTime { year, month, day, duration })
}
@ -1200,10 +1159,8 @@ fn parse_year(year: &str) -> Result<t::Year, Error> {
} else {
(t::Sign::N::<1>(), year)
};
let number = parse::i64(rest.as_bytes())
.map_err(|e| e.context("failed to parse year"))?;
let year = t::Year::new(number)
.ok_or_else(|| err!("year is out of range: {number}"))?;
let number = parse::i64(rest.as_bytes()).context(E::FailedParseYear)?;
let year = t::Year::try_new("year", number)?;
Ok(year * sign)
}
@ -1239,36 +1196,28 @@ fn parse_span(span: &str) -> Result<Span, Error> {
let hour_len = rest.chars().take_while(|c| c.is_ascii_digit()).count();
let (hour_digits, rest) = rest.split_at(hour_len);
if hour_digits.is_empty() {
return Err(err!(
"expected time duration to contain at least one hour digit"
));
return Err(Error::from(E::ExpectedTimeOneHour));
}
let hours = parse::i64(hour_digits.as_bytes())
.map_err(|e| e.context("failed to parse hours in time duration"))?;
span = span
.try_hours(hours.saturating_mul(i64::from(sign.get())))
.map_err(|_| err!("duration hours '{hours:?}' is out of range"))?;
let hours =
parse::i64(hour_digits.as_bytes()).context(E::FailedParseHour)?;
span = span.try_hours(hours.saturating_mul(i64::from(sign.get())))?;
if rest.is_empty() {
return Ok(span);
}
// Now pluck out the minute component.
if !rest.starts_with(":") {
return Err(err!("expected ':' after hours, but found {rest:?}"));
return Err(Error::from(E::ExpectedColonAfterHour));
}
let rest = &rest[1..];
let minute_len = rest.chars().take_while(|c| c.is_ascii_digit()).count();
let (minute_digits, rest) = rest.split_at(minute_len);
if minute_digits.is_empty() {
return Err(err!(
"expected minute digits after 'HH:', but found {rest:?} instead"
));
return Err(Error::from(E::ExpectedMinuteAfterHours));
}
let minutes = parse::i64(minute_digits.as_bytes())
.map_err(|e| e.context("failed to parse minutes in time duration"))?;
let minutes_ranged = t::Minute::new(minutes).ok_or_else(|| {
err!("duration minutes '{minutes:?}' is out of range")
})?;
let minutes =
parse::i64(minute_digits.as_bytes()).context(E::FailedParseMinute)?;
let minutes_ranged = t::Minute::try_new("minutes", minutes)?;
span = span.minutes_ranged((minutes_ranged * sign).rinto());
if rest.is_empty() {
return Ok(span);
@ -1276,21 +1225,17 @@ fn parse_span(span: &str) -> Result<Span, Error> {
// Now pluck out the second component.
if !rest.starts_with(":") {
return Err(err!("expected ':' after minutes, but found {rest:?}"));
return Err(Error::from(E::ExpectedColonAfterMinute));
}
let rest = &rest[1..];
let second_len = rest.chars().take_while(|c| c.is_ascii_digit()).count();
let (second_digits, rest) = rest.split_at(second_len);
if second_digits.is_empty() {
return Err(err!(
"expected second digits after 'MM:', but found {rest:?} instead"
));
return Err(Error::from(E::ExpectedSecondAfterMinutes));
}
let seconds = parse::i64(second_digits.as_bytes())
.map_err(|e| e.context("failed to parse seconds in time duration"))?;
let seconds_ranged = t::Second::new(seconds).ok_or_else(|| {
err!("duration seconds '{seconds:?}' is out of range")
})?;
let seconds =
parse::i64(second_digits.as_bytes()).context(E::FailedParseSecond)?;
let seconds_ranged = t::Second::try_new("seconds", seconds)?;
span = span.seconds_ranged((seconds_ranged * sign).rinto());
if rest.is_empty() {
return Ok(span);
@ -1298,33 +1243,24 @@ fn parse_span(span: &str) -> Result<Span, Error> {
// Now look for the fractional nanosecond component.
if !rest.starts_with(".") {
return Err(err!("expected '.' after seconds, but found {rest:?}"));
return Err(Error::from(E::ExpectedDotAfterSeconds));
}
let rest = &rest[1..];
let nanosecond_len =
rest.chars().take_while(|c| c.is_ascii_digit()).count();
let (nanosecond_digits, rest) = rest.split_at(nanosecond_len);
if nanosecond_digits.is_empty() {
return Err(err!(
"expected nanosecond digits after 'SS.', \
but found {rest:?} instead"
));
return Err(Error::from(E::ExpectedNanosecondDigits));
}
let nanoseconds =
parse::fraction(nanosecond_digits.as_bytes()).map_err(|e| {
e.context("failed to parse nanoseconds in time duration")
})?;
let nanoseconds_ranged = t::FractionalNanosecond::new(nanoseconds)
.ok_or_else(|| {
err!("duration nanoseconds '{nanoseconds:?}' is out of range")
})?;
let nanoseconds = parse::fraction(nanosecond_digits.as_bytes())
.context(E::FailedParseNanosecond)?;
let nanoseconds_ranged =
t::FractionalNanosecond::try_new("nanoseconds", nanoseconds)?;
span = span.nanoseconds_ranged((nanoseconds_ranged * sign).rinto());
// We should have consumed everything at this point.
if !rest.is_empty() {
return Err(err!(
"found unrecognized trailing {rest:?} in time duration"
));
return Err(Error::from(E::UnrecognizedTrailingTimeDuration));
}
span.rebalance(Unit::Hour)
}
@ -1334,10 +1270,8 @@ fn parse_span(span: &str) -> Result<Span, Error> {
/// This checks that the day is in the range 1-31, but otherwise doesn't
/// check that it is valid for a particular month.
fn parse_day(string: &str) -> Result<t::Day, Error> {
let number = parse::i64(string.as_bytes())
.map_err(|e| e.context("failed to parse number for day"))?;
let day = t::Day::new(number)
.ok_or_else(|| err!("{number} is not a valid day"))?;
let number = parse::i64(string.as_bytes()).context(E::FailedParseDay)?;
let day = t::Day::try_new("day", number)?;
Ok(day)
}
@ -1357,7 +1291,7 @@ fn parse_weekday(string: &str) -> Result<Weekday, Error> {
return Ok(weekday);
}
}
Err(err!("unrecognized day of the week: {string:?}"))
Err(Error::from(E::UnrecognizedDayOfWeek))
}
/// A parser that emits lines as sequences of fields.
@ -1393,7 +1327,7 @@ impl<'a> FieldParser<'a> {
/// This returns an error if the given bytes are not valid UTF-8.
fn from_bytes(src: &'a [u8]) -> Result<FieldParser, Error> {
let src = core::str::from_utf8(src)
.map_err(|e| err!("invalid UTF-8: {e}"))?;
.map_err(|_| Error::from(E::InvalidUtf8))?;
Ok(FieldParser::new(src))
}
@ -1409,12 +1343,10 @@ impl<'a> FieldParser<'a> {
self.fields.clear();
loop {
let Some(mut line) = self.lines.next() else { return Ok(false) };
self.line_number = self
.line_number
.checked_add(1)
.ok_or_else(|| err!("line count overflowed"))?;
self.line_number =
self.line_number.checked_add(1).ok_or(E::LineOverflow)?;
parse_fields(&line, &mut self.fields)
.with_context(|| err!("line {}", self.line_number))?;
.context(E::Line { number: self.line_number })?;
if self.fields.is_empty() {
continue;
}
@ -1452,13 +1384,6 @@ fn parse_fields<'a>(
matches!(ch, ' ' | '\x0C' | '\n' | '\r' | '\t' | '\x0B')
}
// `man zic` says that the max line length including the line
// terminator is 2048. The `core::str::Lines` iterator doesn't include
// the terminator, so we subtract 1 to account for that. Note that this
// could potentially allow one extra byte in the case of a \r\n line
// terminator, but this seems fine.
const MAX_LINE_LEN: usize = 2047;
// The different possible states of the field parser below.
enum State {
Whitespace,
@ -1469,15 +1394,11 @@ fn parse_fields<'a>(
fields.clear();
if line.len() > MAX_LINE_LEN {
return Err(err!(
"line with length {} exceeds \
max length of {MAX_LINE_LEN}",
line.len()
));
return Err(Error::from(E::LineMaxLength));
}
// Do a quick scan for a NUL terminator. They are illegal in all cases.
if line.contains('\x00') {
return Err(err!("found line with NUL byte, which isn't allowed"));
return Err(Error::from(E::LineNul));
}
// The current state of the parser. We start at whitespace, since it also
// means "before a field."
@ -1522,9 +1443,8 @@ fn parse_fields<'a>(
}
State::AfterQuote => {
if !is_space(ch) {
return Err(err!(
"expected whitespace after quoted field, \
but found {ch:?} instead",
return Err(Error::from(
E::ExpectedWhitespaceAfterQuotedField,
));
}
State::Whitespace
@ -1537,7 +1457,7 @@ fn parse_fields<'a>(
fields.push(&line[start..]);
}
State::InQuote => {
return Err(err!("found unclosed quote"));
return Err(Error::from(E::ExpectedCloseQuote));
}
}
Ok(())

View file

@ -49,7 +49,7 @@ impl Sign {
impl core::fmt::Display for Sign {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
if self.is_negative() {
write!(f, "-")
f.write_str("-")
} else {
Ok(())
}

View file

@ -36,15 +36,13 @@ impl Expiration {
impl core::fmt::Display for Expiration {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let Some(instant) = self.0 else {
return write!(f, "expired");
};
let Some(now) = crate::now::monotonic_time() else {
return write!(f, "expired");
};
let Some(duration) = instant.checked_duration_since(now) else {
return write!(f, "expired");
};
write!(f, "{duration:?}")
let maybe_duration = self.0.and_then(|instant| {
crate::now::monotonic_time()
.and_then(|now| instant.checked_duration_since(now))
});
match maybe_duration {
None => f.write_str("expired"),
Some(duration) => core::fmt::Debug::fmt(&duration, f),
}
}
}

View file

@ -4,8 +4,121 @@ Provides convenience routines for escaping raw bytes.
This was copied from `regex-automata` with a few light edits.
*/
// These were originally defined here, but they got moved to
// shared since they're needed there. We re-export them here
// because this is really where they should live, but they're
// in shared because `jiff-tzdb-static` needs it.
pub(crate) use crate::shared::util::escape::{Byte, Bytes};
use super::utf8;
/// Provides a convenient `Debug` implementation for a `u8`.
///
/// The `Debug` impl treats the byte as an ASCII, and emits a human
/// readable representation of it. If the byte isn't ASCII, then it's
/// emitted as a hex escape sequence.
#[derive(Clone, Copy)]
pub(crate) struct Byte(pub u8);
impl core::fmt::Display for Byte {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
if self.0 == b' ' {
return f.write_str(" ");
}
// 10 bytes is enough for any output from ascii::escape_default.
let mut bytes = [0u8; 10];
let mut len = 0;
for (i, mut b) in core::ascii::escape_default(self.0).enumerate() {
// capitalize \xab to \xAB
if i >= 2 && b'a' <= b && b <= b'f' {
b -= 32;
}
bytes[len] = b;
len += 1;
}
f.write_str(core::str::from_utf8(&bytes[..len]).unwrap())
}
}
impl core::fmt::Debug for Byte {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("\"")?;
core::fmt::Display::fmt(self, f)?;
f.write_str("\"")?;
Ok(())
}
}
/// Provides a convenient `Debug` implementation for `&[u8]`.
///
/// This generally works best when the bytes are presumed to be mostly
/// UTF-8, but will work for anything. For any bytes that aren't UTF-8,
/// they are emitted as hex escape sequences.
#[derive(Clone, Copy)]
pub(crate) struct Bytes<'a>(pub &'a [u8]);
impl<'a> core::fmt::Display for Bytes<'a> {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
// This is a sad re-implementation of a similar impl found in bstr.
let mut bytes = self.0;
while let Some(result) = utf8::decode(bytes) {
let ch = match result {
Ok(ch) => ch,
Err(err) => {
// The decode API guarantees `errant_bytes` is non-empty.
write!(f, r"\x{:02x}", err.as_slice()[0])?;
bytes = &bytes[1..];
continue;
}
};
bytes = &bytes[ch.len_utf8()..];
match ch {
'\0' => f.write_str(r"\0")?,
'\x01'..='\x7f' => {
core::fmt::Display::fmt(&(ch as u8).escape_ascii(), f)?;
}
_ => {
core::fmt::Display::fmt(&ch.escape_debug(), f)?;
}
}
}
Ok(())
}
}
impl<'a> core::fmt::Debug for Bytes<'a> {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("\"")?;
core::fmt::Display::fmt(self, f)?;
f.write_str("\"")?;
Ok(())
}
}
/// A helper for repeating a single byte utilizing `Byte`.
///
/// This is limited to repeating a byte up to `u8::MAX` times in order
/// to reduce its size overhead. And in practice, Jiff just doesn't
/// need more than this (at time of writing, 2025-11-29).
pub(crate) struct RepeatByte {
pub(crate) byte: u8,
pub(crate) count: u8,
}
impl core::fmt::Display for RepeatByte {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
for _ in 0..self.count {
core::fmt::Display::fmt(&Byte(self.byte), f)?;
}
Ok(())
}
}
impl core::fmt::Debug for RepeatByte {
#[inline(never)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("\"")?;
core::fmt::Display::fmt(self, f)?;
f.write_str("\"")?;
Ok(())
}
}

View file

@ -1,7 +1,4 @@
use crate::{
error::{err, Error},
util::escape::{Byte, Bytes},
};
use crate::error::util::{ParseFractionError, ParseIntError};
/// Parses an `i64` number from the beginning to the end of the given slice of
/// ASCII digit characters.
@ -13,38 +10,20 @@ use crate::{
/// integers, and because a higher level routine might want to parse the sign
/// and then apply it to the result of this routine.)
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) fn i64(bytes: &[u8]) -> Result<i64, Error> {
pub(crate) fn i64(bytes: &[u8]) -> Result<i64, ParseIntError> {
if bytes.is_empty() {
return Err(err!("invalid number, no digits found"));
return Err(ParseIntError::NoDigitsFound);
}
let mut n: i64 = 0;
for &byte in bytes {
let digit = match byte.checked_sub(b'0') {
None => {
return Err(err!(
"invalid digit, expected 0-9 but got {}",
Byte(byte),
));
}
Some(digit) if digit > 9 => {
return Err(err!(
"invalid digit, expected 0-9 but got {}",
Byte(byte),
))
}
Some(digit) => {
debug_assert!((0..=9).contains(&digit));
i64::from(digit)
}
};
n = n.checked_mul(10).and_then(|n| n.checked_add(digit)).ok_or_else(
|| {
err!(
"number '{}' too big to parse into 64-bit integer",
Bytes(bytes),
)
},
)?;
if !(b'0' <= byte && byte <= b'9') {
return Err(ParseIntError::InvalidDigit(byte));
}
let digit = i64::from(byte - b'0');
n = n
.checked_mul(10)
.and_then(|n| n.checked_add(digit))
.ok_or(ParseIntError::TooBig)?;
}
Ok(n)
}
@ -65,7 +44,9 @@ pub(crate) fn i64(bytes: &[u8]) -> Result<i64, Error> {
///
/// When the parsed integer cannot fit into a `u64`.
#[cfg_attr(feature = "perf-inline", inline(always))]
pub(crate) fn u64_prefix(bytes: &[u8]) -> Result<(Option<u64>, &[u8]), Error> {
pub(crate) fn u64_prefix(
bytes: &[u8],
) -> Result<(Option<u64>, &[u8]), ParseIntError> {
// Discovered via `u64::MAX.to_string().len()`.
const MAX_U64_DIGITS: usize = 20;
@ -79,15 +60,10 @@ pub(crate) fn u64_prefix(bytes: &[u8]) -> Result<(Option<u64>, &[u8]), Error> {
digit_count += 1;
// OK because we confirmed `byte` is an ASCII digit.
let digit = u64::from(byte - b'0');
n = n.checked_mul(10).and_then(|n| n.checked_add(digit)).ok_or_else(
#[inline(never)]
|| {
err!(
"number `{}` too big to parse into 64-bit integer",
Bytes(&bytes[..digit_count]),
)
},
)?;
n = n
.checked_mul(10)
.and_then(|n| n.checked_add(digit))
.ok_or(ParseIntError::TooBig)?;
}
if digit_count == 0 {
return Ok((None, bytes));
@ -105,54 +81,33 @@ pub(crate) fn u64_prefix(bytes: &[u8]) -> Result<(Option<u64>, &[u8]), Error> {
///
/// If any byte in the given slice is not `[0-9]`, then this returns an error.
/// Notably, this routine does not permit parsing a negative integer.
pub(crate) fn fraction(bytes: &[u8]) -> Result<u32, Error> {
const MAX_PRECISION: usize = 9;
pub(crate) fn fraction(bytes: &[u8]) -> Result<u32, ParseFractionError> {
if bytes.is_empty() {
return Err(err!("invalid fraction, no digits found"));
} else if bytes.len() > MAX_PRECISION {
return Err(err!(
"invalid fraction, too many digits \
(at most {MAX_PRECISION} are allowed"
));
return Err(ParseFractionError::NoDigitsFound);
} else if bytes.len() > ParseFractionError::MAX_PRECISION {
return Err(ParseFractionError::TooManyDigits);
}
let mut n: u32 = 0;
for &byte in bytes {
let digit = match byte.checked_sub(b'0') {
None => {
return Err(err!(
"invalid fractional digit, expected 0-9 but got {}",
Byte(byte),
));
return Err(ParseFractionError::InvalidDigit(byte));
}
Some(digit) if digit > 9 => {
return Err(err!(
"invalid fractional digit, expected 0-9 but got {}",
Byte(byte),
))
return Err(ParseFractionError::InvalidDigit(byte));
}
Some(digit) => {
debug_assert!((0..=9).contains(&digit));
u32::from(digit)
}
};
n = n.checked_mul(10).and_then(|n| n.checked_add(digit)).ok_or_else(
|| {
err!(
"fractional '{}' too big to parse into 64-bit integer",
Bytes(bytes),
)
},
)?;
n = n
.checked_mul(10)
.and_then(|n| n.checked_add(digit))
.ok_or_else(|| ParseFractionError::TooBig)?;
}
for _ in bytes.len()..MAX_PRECISION {
n = n.checked_mul(10).ok_or_else(|| {
err!(
"fractional '{}' too big to parse into 64-bit integer \
(too much precision supported)",
Bytes(bytes)
)
})?;
for _ in bytes.len()..ParseFractionError::MAX_PRECISION {
n = n.checked_mul(10).ok_or_else(|| ParseFractionError::TooBig)?;
}
Ok(n)
}
@ -161,15 +116,17 @@ pub(crate) fn fraction(bytes: &[u8]) -> Result<u32, Error> {
///
/// This is effectively `OsStr::to_str`, but with a slightly better error
/// message.
#[cfg(feature = "tzdb-zoneinfo")]
pub(crate) fn os_str_utf8<'o, O>(os_str: &'o O) -> Result<&'o str, Error>
#[cfg(any(feature = "tz-system", feature = "tzdb-zoneinfo"))]
pub(crate) fn os_str_utf8<'o, O>(
os_str: &'o O,
) -> Result<&'o str, crate::error::util::OsStrUtf8Error>
where
O: ?Sized + AsRef<std::ffi::OsStr>,
{
let os_str = os_str.as_ref();
os_str
.to_str()
.ok_or_else(|| err!("environment value {os_str:?} is not valid UTF-8"))
.ok_or_else(|| crate::error::util::OsStrUtf8Error::from(os_str))
}
/// Parses an `OsStr` into a `&str` when `&[u8]` isn't easily available.
@ -178,7 +135,9 @@ where
/// be a zero-cost conversion on Unix platforms to `&[u8]`. On Windows, this
/// will do UTF-8 validation and return an error if it's invalid UTF-8.
#[cfg(feature = "tz-system")]
pub(crate) fn os_str_bytes<'o, O>(os_str: &'o O) -> Result<&'o [u8], Error>
pub(crate) fn os_str_bytes<'o, O>(
os_str: &'o O,
) -> Result<&'o [u8], crate::error::util::OsStrUtf8Error>
where
O: ?Sized + AsRef<std::ffi::OsStr>,
{
@ -190,16 +149,13 @@ where
}
#[cfg(not(unix))]
{
let string = os_str.to_str().ok_or_else(|| {
err!("environment value {os_str:?} is not valid UTF-8")
})?;
// It is suspect that we're doing UTF-8 validation and then throwing
// away the fact that we did UTF-8 validation. So this could lead
// to an extra UTF-8 check if the caller ultimately needs UTF-8. If
// that's important, we can add a new API that returns a `&str`. But it
// probably won't matter because an `OsStr` in this crate is usually
// just an environment variable.
Ok(string.as_bytes())
Ok(os_str_utf8(os_str)?.as_bytes())
}
}

Some files were not shown because too many files have changed in this diff Show more