From e14c262673f873b8eea6d8801f039248d3d82b74 Mon Sep 17 00:00:00 2001 From: upstream Date: Sat, 10 Oct 2026 12:30:25 +0200 Subject: [PATCH] Upstream BurntSushi/jiff state of the touched files before 070c9a5a6b --- CHANGELOG.md | 1695 +++++++++ crates/jiff/src/fmt/friendly/printer.rs | 4173 +++++++++++++++++++++++ crates/jiff/src/fmt/temporal/printer.rs | 1933 +++++++++++ 3 files changed, 7801 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 crates/jiff/src/fmt/friendly/printer.rs create mode 100644 crates/jiff/src/fmt/temporal/printer.rs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..14dc7fe --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1695 @@ +# CHANGELOG + +0.2.38 (2026-10-06) +=================== +This release updates Jiff's bundled copy of the [IANA Time Zone Database] +to `2026e`. See the [`2026e` release announcement] for more details. + +[`2026e` release announcement]: https://lists.iana.org/hyperkitty/list/tz-announce@iana.org/thread/VXIA4AU73OQL3OZ3ZBZHWIASIIVBGUJV/ + + +0.2.37 (2026-09-12) +=================== +This is a redux release of `0.2.36` that fixes a compilation bug. + +Bug fixes: + +* [#647](https://github.com/BurntSushi/jiff/issues/647): +Fixes a compilation bug due to incorrect include paths. + + +0.2.36 (2026-09-12) +=================== +This release has a couple bug fixes and adds opt-in integration with the +`arbitrary` crate. + +Enhancements: + +* [#631](https://github.com/BurntSushi/jiff/issues/631): +Add optional `Arbitrary` trait implementations from the `arbitrary` crate. + +Bug fixes: + +* [#617](https://github.com/BurntSushi/jiff/pull/617): +Fixes a bug in `jiff_core::tz::Offset` checked subtraction routine. +* [#627](https://github.com/BurntSushi/jiff/issues/627): +Use `serialize_some` for optional Serde helpers in `jiff::fmt::serde`. + + +0.2.35 (2026-07-25) +=================== +This release fixes a bug in duration parsing where, e.g., `-PT0.5S` would be +parsed as if the `-` sign didn't exist. This bug also affects friendly +durations such as `-0.5s`. + +Bug fixes: + +* [#613](https://github.com/BurntSushi/jiff/issues/613): +Fixes a bug where the sign in a duration could be ignored while parsing. + + +0.2.34 (2026-07-19) +=================== +This release contains a few small enhancements. Namely, some error messages +for parsing datetime values have been improved by removing some redundant +messaging. Additionally, the `strftime` APIs on datetime values can no longer +panic. Instead, they'll silently pass through invalid formatting directives. + +Enhancements: + +* [#519](https://github.com/BurntSushi/jiff/issues/519): +Improve some parsing error messages by removing redundancy. +* [#585](https://github.com/BurntSushi/jiff/issues/585): +Change `Timestamp::strftime` (and similar methods) so that they will never +panic. + + +0.2.33 (2026-07-18) +=================== +This release permits the use of a global time zone database in `alloc`-only +environments. It also contains a massive internal refactor that introduces a +new `jiff-core` crate for better code sharing. + +Enhancements: + +* [#285](https://github.com/BurntSushi/jiff/issues/285): +Decouple naming of time zones from their TZif data. +* [#373](https://github.com/BurntSushi/jiff/issues/373): +Add a new `jiff-core` crate for better code sharing. +* [#533](https://github.com/BurntSushi/jiff/issues/533): +A bundled `TimeZoneDatabase` is now available when only `alloc` is enabled. + + +0.2.32 (2026-07-08) +=================== +This release updates Jiff's bundled copy of the [IANA Time Zone Database] +to `2026c`. See the [`2026c` release announcement] for more details. + +[`2026c` release announcement]: https://lists.iana.org//hyperkitty/list/tz-announce@iana.org/thread/NVHSX2PAQIT44U5FCCEVNJJYXQMMTJSA/ + + +0.2.31 (2026-06-29) +=================== +This release fixes a minor bug in tests that prevents the crate from running +through standard build processes in Linux distributions downstream. + +* [#594](https://github.com/BurntSushi/jiff/pull/591): +Fix imports in recently added tests. + + +0.2.30 (2026-06-29) +=================== +This release fixes a safety soundness bug in Jiff's lower level printing +APIs. There is also a nice performance improvement to `civil::Date::weekday`, +which also improves downstread routines like `civil::Date::nth_weekday` and +datetime<-->timestamp conversions in some cases. + +Enhancements: + +* [#591](https://github.com/BurntSushi/jiff/pull/591): +Improve the performance of weekday calculations from Gregorian dates by 30-50%. + +Bug fixes: + +* [#592](https://github.com/BurntSushi/jiff/issues/592): +Fix safety soundness bug when using a non-empty `String` destination buffer +with lower level printing APIs inside of `jiff::fmt`. + + +0.2.29 (2026-06-20) +=================== +This release adds support for [`defmt`], which is a highly efficient logging +framework that targets resource-constrained devices, like microcontrollers. + +Enhancements: + +* [#505](https://github.com/BurntSushi/jiff/issues/505): +Add support for the `defmt` crate by implementing `defmt::Format` on the +principle public types in Jiff. +* [#584](https://github.com/BurntSushi/jiff/pull/584): +Add `Zoned::UNIX_EPOCH` as a constant. +* [#587](https://github.com/BurntSushi/jiff/pull/587): +Change the `Debug` output for `civil::ISOWeekDate` to match the `Debug` output +style of other primitive datetime types. + +Bug fixes: + +* [#525](https://github.com/BurntSushi/jiff/issues/525): +Fix a bug that prevented time zone lookups when using an on-disk time zone +database on Windows. +* [#539](https://github.com/BurntSushi/jiff/issues/539): +Fix a bug where rendering a datetime in an error message would omit a `T` +separator. + + +0.2.28 (2026-05-28) +=================== +This is a small release with a fix for test failures on 32-bit targets in +`no-alloc` environments. + +Bug fixes: + +* [#573](https://github.com/BurntSushi/jiff/issues/573): +Fix test failure on 32-bit targets in `no-alloc` environments. + + +0.2.27 (2026-05-26) +=================== +This is a small release with a bug fix for build errors on Windows for very old +versions of Rust. + +Bug fixes: + +* [#566](https://github.com/BurntSushi/jiff/issues/566): +Fix build error on Windows for very old versions of Rust (e.g., 1.71). + + +0.2.26 (2026-05-25) +=================== +This release has a couple enhancements. + +Firstly, Jiff now uses `windows-link` for calling FFI routines on Windows +instead of `windows-sys`. Using `windows-link` means less churn and fewer +duplicates in the dependency graph. + +Secondly, a new `jiff-sqlx 0.2.0` release has been put out to support +`sqlx 0.9.0`. + +Enhancements: + +* [#538](https://github.com/BurntSushi/jiff/pull/538): +Replace use of `windows-sys` with `windows-link` and inline bindings. +* [#561](https://github.com/BurntSushi/jiff/pull/561): +Update `jiff-sqlx` to use `sqlx 0.9.0` and release `jiff-sqlx 0.2.0`. + +Bug fixes: + +* [#548](https://github.com/BurntSushi/jiff/pull/548): +Absolutetize incorrect relative size terms in the documentation of `RoundMode`. + + +0.2.25 (2026-05-24) +=================== +This release updates Jiff's bundled copy of the [IANA Time Zone Database] +to `2026b`. See the [`2026b` release announcement] for more details. + +[`2026b` release announcement]: https://lists.iana.org//hyperkitty/list/tz-announce@iana.org/thread/VX2Z3CBO6KHTYZNBBKFFWM7ZCI6TVCXP/ + + +0.2.24 (2026-04-23) +=================== +This release primarily adds a new `memory_usage` routine for reporting +heap allocation sizes for the `TimeZone` and `Zoned` types. This +release also acknowledges and updates the timeline expectations for a +Jiff 1.0 release in `README.md`. + +Enhancements: + +* [#520](https://github.com/BurntSushi/jiff/issues/520): +Add `memory_usage` to the `TimeZone` and `Zoned` types. +* [#535](https://github.com/BurntSushi/jiff/pull/535): +Improve comment in `Span::checked_add` example. + +Bug fixes: + +* [#541](https://github.com/BurntSushi/jiff/pull/541): +Update Jiff 1.0 timeline. + + +0.2.23 (2026-03-03) +=================== +This release updates Jiff's bundled copy of the [IANA Time Zone Database] +to `2026a`. See the [`2026a` release announcement] for more details. + +[`2026a` release announcement]: https://lists.iana.org/hyperkitty/list/tz-announce@iana.org/thread/ASPLBE3A4BAEXIOQ3KZ6EJSJWBU6L53G/ + +0.2.22 (2026-02-28) +=================== +This release includes a bug fix where fallible conversions from signed +durations to unsigned durations could panic in some cases. + +Bug fixes: + +* [#526](https://github.com/BurntSushi/jiff/issues/526): +Fix a panicking bug that occurs for +`std::time::Duration::try_from(SignedDuration::new(0, -1))`. + + +0.2.21 (2026-02-22) +=================== +This release contains a performance improvement and a bug fix for +`civil::Date::new` where it could panic on some inputs. + +Bug fixes: + +* [#523](https://github.com/BurntSushi/jiff/issues/523): +Fix a bug where `Date::new` could panic. This was a regression introduced in +`jiff 0.2.20`. + +Performance: + +* [#518](https://github.com/BurntSushi/jiff/pull/518): +Improve `Timestamp` to `civil::DateTime` conversion performance by ~15%. + + +0.2.20 (2026-02-11) +=================== +This release contains a major internal refactor that moves off of using ranged +integers internally. There are also some small bug fixes and added support for +finding the system time zone on emscripten targets. + +Enhancements: + +* [#11](https://github.com/BurntSushi/jiff/issues/11): +Stop using ranged integers internally. +* [#490](https://github.com/BurntSushi/jiff/issues/490): +Add support for retrieving the system time zone on emscripten targets. +* [#500](https://github.com/BurntSushi/jiff/issues/500): +Update comparison with the `time` crate in the Jiff documentation. +* [#502](https://github.com/BurntSushi/jiff/issues/502): +Enable some non-default features for the Rust Playground deployment. + +Bug fixes: + +* [#485](https://github.com/BurntSushi/jiff/issues/485): +Fix bug with padding for negative integers in `strftime`. +* [#486](https://github.com/BurntSushi/jiff/issues/486): +Make `%^c` result in uppercase strings where appropriate. + + +0.2.19 (2026-02-05) +=================== +This is a small release with a performance optimization (with respect to doing +heavily contended arithmetic on `Zoned` values) and a bug fix for a panic +that can occur when using `%2s` in `strftime`. + +Enhancements: + +* [#491](https://github.com/BurntSushi/jiff/issues/491): +Avoid cloning `TimeZone` for consuming operations on `Zoned`. + +Bug fixes: + +* [#497](https://github.com/BurntSushi/jiff/issues/497): +Fix a panic in `timestamp.strftime("%2s")`. + + +0.2.18 (2026-01-05) +=================== +This release ships a sizeable refactor to the RFC 2822, RFC 9110, RC +3339, RFC 9557, ISO 8601 and friendly format printers. Specifically, +they are now all monomorphic internally (instead of being generic over +`jiff::fmt::Write`) and write to uninitialized buffers. This improves +runtime performance (sometimes dramatically so), and to a more modest +degree, decreases binary size and improves compile times. + +This release also includes a bug fix where `DateTime::MIN.to_zoned(..)` +could panic. + +Enhancements: + +* [#460](https://github.com/BurntSushi/jiff/pull/460): +Improve runtime performance and binary size of RFC 2822 printer. +* [#461](https://github.com/BurntSushi/jiff/pull/461): +Tweak behavior of printing min/max offsets in RFC 2822 and Temporal printers. +* [#462](https://github.com/BurntSushi/jiff/pull/462): +Export fallible constructors for `jiff::SignedDuration`. +* [#465](https://github.com/BurntSushi/jiff/pull/465): +Improve runtime performance and binary size of the "friendly" duration printer. +* [#468](https://github.com/BurntSushi/jiff/pull/468): +Improve runtime performance and binary size of the Temporal ISO 8601 duration +printer. +* [#470](https://github.com/BurntSushi/jiff/pull/470): +Improve runtime performance and binary size of the Temporal ISO 8601 datetime +printer. +* [#474](https://github.com/BurntSushi/jiff/pull/474): +Improve runtime performance and binary size of Jiff's `strftime` +implementation. +* [#477](https://github.com/BurntSushi/jiff/pull/477): +Fix a bug where time zone lookups for `civil::DateTime::MIN` could panic. + + +0.2.17 (2025-12-24) +=================== +This release contains binary size improvements to Jiff, more succinct error +messages and some new minor APIs. + +While Jiff 1.0 is overdue, I've been doing a lot of experimenting with +improving Jiff's binary size and compile times. In particular, I want to spend +time doing this before Jiff 1.0 so that we don't box ourselves into a corner. +(For example, some binary size improvements may require minor API breaking +changes.) + +In this release, Jiff has switched to structured error handling internally +in an effort to provide error predicates and also hopefully improve binary +sizes and compile times. Overall this didn't have as big of an impact on +binary sizes or compile times as I was hoping. I did take this opportunity to +make Jiff's error messages a bit more succinct. In many cases, this involved +de-duplicating some aspects of error messages and omitting user provided input +in the messages. If you feel like there is a significant decrease in error +message quality that isn't easily amended by callers providing additional +context themselves, please open an issue. + +This release also updates Jiff's bundled copy of the [IANA Time Zone Database] +to `2025c`. See the [`2025c` release announcement] for more details. + +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. +* [#453](https://github.com/BurntSushi/jiff/pull/453), + [#454](https://github.com/BurntSushi/jiff/pull/454): +Switch to structured error handling internally. +* [#456](https://github.com/BurntSushi/jiff/pull/456), + [#457](https://github.com/BurntSushi/jiff/pull/457), + [#458](https://github.com/BurntSushi/jiff/pull/458): +Various improvements to binary size. + +[`2025c` release announcement]: https://lists.iana.org/hyperkitty/list/tz-announce@iana.org/thread/TAGXKYLMAQRZRFTERQ33CEKOW7KRJVAK/ + + +0.2.16 (2025-11-07) +=================== +This release contains a number of enhancements and bug fixes that have accrued +over the last few months. Most are small polishes. A couple of the bug fixes +apply to panics that could occur when parsing invalid `TZ` strings or invalid +`strptime` format strings. + +Also, parsing into a `Span` should now be much faster (for both the ISO 8601 +and "friendly" duration formats). + +Enhancements: + +* [#298](https://github.com/BurntSushi/jiff/issues/298): +Add Serde helpers for (de)serializing `std::time::Duration` values. +* [#396](https://github.com/BurntSushi/jiff/issues/396): +Add `Sub` and `Add` trait implementations for `Zoned` (in addition to the +already existing trait implementations for `&Zoned`). +* [#397](https://github.com/BurntSushi/jiff/pull/397): +Add `BrokenDownTime::set_meridiem` and ensure it overrides the hour when +formatting. +* [#409](https://github.com/BurntSushi/jiff/pull/409): +Switch dependency on `serde` to `serde_core`. This should help speed up +compilation times in some cases. +* [#430](https://github.com/BurntSushi/jiff/pull/430): +Add new `Zoned::series` API, making it consistent with the same API on other +datetime types. +* [#432](https://github.com/BurntSushi/jiff/pull/432): +When `lenient` mode is enabled for `strftime`, Jiff will no longer error when +the formatting string contains invalid UTF-8. +* [#432](https://github.com/BurntSushi/jiff/pull/432): +Formatting of `%y` and `%g` no longer fails based on the specific year value. +* [#432](https://github.com/BurntSushi/jiff/pull/432): +Parsing of `%s` is now a bit more consistent with other fields. Moreover, +`BrokenDownTime::{to_timestamp,to_zoned}` will now prefer timestamps parsed +with `%s` over any other fields that have been parsed. +* [#433](https://github.com/BurntSushi/jiff/pull/433): +Allow parsing just a `%s` into a `Zoned` via the `Etc/Unknown` time zone. + +Bug fixes: + +* [#386](https://github.com/BurntSushi/jiff/issues/386): +Fix a bug where `2087-12-31T23:00:00Z` in the `Africa/Casablanca` time zone +could not be round-tripped (because its offset was calculated incorrectly as +a result of not handling "permanent DST" POSIX time zones). +* [#407](https://github.com/BurntSushi/jiff/issues/407): +Fix a panic that occurred when parsing an empty string as a POSIX time zone. +* [#410](https://github.com/BurntSushi/jiff/issues/410): +Fix a panic that could occur when parsing `%:` via `strptime` APIs. +* [#414](https://github.com/BurntSushi/jiff/pull/414): +Update some parts of the documentation to indicate that `TimeZone::unknown()` +is a fallback for `TimeZone::system()` (instead of the `jiff 0.1` behavior of +using `TimeZone::UTC`). +* [#423](https://github.com/BurntSushi/jiff/issues/423): +Fix a panicking bug when reading malformed TZif data. +* [#426](https://github.com/BurntSushi/jiff/issues/426): +Fix a panicking bug when parsing century (`%C`) via `strptime`. +* [#445](https://github.com/BurntSushi/jiff/pull/445): +Fixed bugs with parsing durations like `-9223372036854775808s` +and `-PT9223372036854775808S`. + +Performance: + +* [#445](https://github.com/BurntSushi/jiff/pull/445): +Parsing into `Span` or `SignedDuration` is now a fair bit faster in some cases. + + +0.2.15 (2025-06-13) +=================== +This release fixes a bug where error values were being constructed during +parsing even in the success case. This was a regression introduced in `0.2.14` +as a result of trying to improve compilation times. Thankfully, fixing this +regression doesn't seem to meaningfully impact the amount of IR generated by +compiling Jiff. + +Bug fixes: + +* [#385](https://github.com/BurntSushi/jiff/pull/385): +Fixes a performance regression for parsing. + + +0.2.14 (2025-05-20) +=================== +This release includes a smattering of bug fixes, and hopefully a small +improvement to the time it takes to compile Jiff. Also, in this release, when +`TZ` is set to a non-empty but invalid value, Jiff will always fall back to +`Etc/Unknown` when using `TimeZone::system()`. This differs from previous +behavior where Jiff would, in this case, attempt to read the system's default +time zone. This change brings Jiff into consistency with existing Unix tooling +like `date`, and is also arguably a better failure mode. That is, when `TZ` is +set but invalid, we shouldn't silently fall back to the default system time +zone, but instead do something that indicates something has potentially gone +wrong. + +Enhancements: + +* [#364](https://github.com/BurntSushi/jiff/issues/364): +Jiff now falls back to `Etc/Unknown` for invalid `TZ` values. +* [#379](https://github.com/BurntSushi/jiff/pull/379): +Improve compilation times. + +Bug fixes: + +* [#365](https://github.com/BurntSushi/jiff/issues/365): +Fixes a compile error in Jiff when only the `tzdb-concatenated` feature was +enabled. +* [#366](https://github.com/BurntSushi/jiff/issues/366): +Fixes slow initial `Zoned::now()` in environments where `/usr/share/zoneinfo` +is on a very slow file system (like CI environments). +* [#376](https://github.com/BurntSushi/jiff/issues/376): +Avoids searching for a tzdb at `/usr/share/zoneinfo` on Windows. + + +0.2.13 (2025-05-05) +=================== +This release fixes a bug in a corner case where `TimeZone::following` could +sometimes omit the last time zone transition. This could only happen in time +zones that once had daylight saving time, but ended it. This did not have an +impact on other Jiff APIs that handle daylight saving time. + +Bug fixes: + +* [#362](https://github.com/BurntSushi/jiff/issues/362): +Fix a bug where `TimeZone::following` could omit the last historical time zone +transition. + + +0.2.12 (2025-05-03) +=================== +This release fixes a couple bugs in Jiff's parser. In particular, a regression +was introduced in `jiff 0.2.11` where its RFC 2822 parser could panic on some +inputs. Previous releases of Jiff are unaffected. + +Bug fixes: + +* [#357](https://github.com/BurntSushi/jiff/issues/357): +Fix a bug where parsing `1970-06-01T00-00:45:00[Africa/Monrovia]` succeeded +but it should fail. +* [#359](https://github.com/BurntSushi/jiff/issues/359): +Fix a bug where the RFC 2822 parser could panic on some inputs. + + +0.2.11 (2025-05-01) +=================== +This release includes new APIs for customizing Jiff's `strtime` behavior along +with a few minor bug fixes. Jiff's `strtime` formatting API has also been +optimized. It's about twice as fast as it was. + +This release also coincides with the publication of `jiff-icu 0.2.0-beta.2`, +which has support for `icu 2.0.0-beta.2`. + +Enhancements: + +* [#338](https://github.com/BurntSushi/jiff/pull/338): +Add support for the `%c`, `%r`, `%X` and `%x` conversion specifiers. +* [#341](https://github.com/BurntSushi/jiff/issues/341): +Add support for `%q` in `jiff::fmt::strtime` (prints quarter of year). +* [#342](https://github.com/BurntSushi/jiff/issues/342): +Add support for `%::z` and `%:::z` in `jiff::fmt::strtime`. +* [#344](https://github.com/BurntSushi/jiff/issues/344): +Add support for `%N` in `jiff::fmt::strtime` (alias for `%9f`). +* [#350](https://github.com/BurntSushi/jiff/issues/350): +Add a "lenient" mode for `strtime` formatting APIs that ignores most errors. + +Bug fixes: + +* [#328](https://github.com/BurntSushi/jiff/issues/328): +Document default precision behavior of `Display` impls for datetime types. +* [#340](https://github.com/BurntSushi/jiff/issues/340): +Allow whitespace in more places in RFC 2822 parser (improves spec compliance). +* [#346](https://github.com/BurntSushi/jiff/issues/346): +`TimeZone::get("UTC")` should now always return `TimeZone::UTC`. + +Performance: + +* [#338](https://github.com/BurntSushi/jiff/pull/338): +Jiff's `strftime` APIs are now approximately twice as fast as they were. +Performance should be comparable to `chrono` and `time`'s prebuilt APIs. + + +0.2.10 (2025-04-21) +=================== +This release includes a bug fix for parsing `Tuesday` when using `%A` via +Jiff's `strptime` APIs. Specifically, it would recognize `Tueday` instead of +`Tuesday`. + +Bug fixes: + +* [#333](https://github.com/BurntSushi/jiff/issues/333): +Fix typo in `strptime` parsing from `Tueday` to `Tuesday`. + + +0.2.9 (2025-04-19) +================== +This release includes a bug fix that, in debug mode, could result in datetime +types having different hashes for the same value. This could cause problems, +for example, if you are using datetimes as keys in a hash map. This problem +didn't exist when Jiff was compiled in release mode. + +This release also improves the panic message shown when the `js` feature isn't +enabled and the current time is requested on `wasm32-unknown-unknown` targets. + +Enhancements: + +* [#296](https://github.com/BurntSushi/jiff/issues/296): +Provide a better panic message when `Zoned::now()` fails on WASM. + +Bug fixes: + +* [#330](https://github.com/BurntSushi/jiff/issues/330): +Fix bug where `Hash` on datetime types could yield different hash values for +the same underlying date/time. + + +0.2.8 (2025-04-13) +================== +This release fixes a bug where the constructors on `SignedDuration` +for floating point durations could panic (in debug mode) or produce +incorrect results (in release mode). This bug only impacts users of +the `try_from_secs_{f32,f64}` and `from_secs_{f32,f64}` methods on +`SignedDuration`. + +Enhancements: + +* [#326](https://github.com/BurntSushi/jiff/pull/326): +Add an alternate `Debug` impl for `SignedDuration` that only shows its second +and nanosecond components (while using only one component when the other is +zero). + +Bug fixes: + +* [#324](https://github.com/BurntSushi/jiff/issues/324): +Fix a bug that could produce a panic or incorrect results in +`SignedDuration::(try_)?from_secs_{f32,f64}`. + + +0.2.7 (2025-04-13) +================== +This release includes a bug fix that changes how an empty but set `TZ` +environment variable is interpreted (as indistinguishable from `TZ=UTC`). +This also includes a new enabled by default create feature, `perf-inline`, +which allows toggling Jiff's use of `inline(always)`. This may help improve +compile times or decrease binary size. + +Enhancements: + +* [#320](https://github.com/BurntSushi/jiff/pull/320): +Remove some internal uses of generics to mildly improve compile times. +* [#321](https://github.com/BurntSushi/jiff/pull/321): +Add `perf-inline` crate feature for controlling `inline(always)` annotations. + +Bug fixes: + +* [#311](https://github.com/BurntSushi/jiff/issues/311): +Make `TZ=` indistinguishable from `TZ=UTC`. + + +0.2.6 (2025-04-07) +================== +This release includes a few bug fixes and support for discovering the IANA Time +Zone Database automatically on Illumos. + +Enhancements: + +* [#315](https://github.com/BurntSushi/jiff/issues/315): +Add support for automatically finding the tzdb on Illumos. + +Bug fixes: + +* [#305](https://github.com/BurntSushi/jiff/issues/305): +Fixed `Zoned` rounding on days with DST time zone transitions. +* [#309](https://github.com/BurntSushi/jiff/issues/309): +Fixed bug where `TimeZone::preceding` could omit historical time zone +transitions for time zones that have eliminated DST in the present. +* [#312](https://github.com/BurntSushi/jiff/issues/312): +Fixed `nth_weekday_in_month`, where it would sometimes incorrectly return an +error. + + +0.2.5 (2025-03-22) +================== +This release updates Jiff's bundled copy of the [IANA Time Zone Database] to +`2025b`. See the [`2025b` release announcement] for more details. + +Enhancements: + +* [#300](https://github.com/BurntSushi/jiff/pull/300): +Update `jiff-tzdb` to `2025b`. + +[`2025b` release announcement]: https://lists.iana.org/hyperkitty/list/tz-announce@iana.org/thread/6JVHNHLB6I2WAYTQ75L6KEPEQHFXAJK3/ + + +0.2.4 (2025-03-10) +================== +This is another small release that fixes a problem where Jiff could break +builds if they relied on inference for integer comparisons. Specifically, Jiff +uses internal trait impls to make comparing its internal ranged integers +more convenient. But Rust the language has no concept of "internal" trait +impls, and thus this can impact type inference. If code was written in a way +that relies on a singular trait impl that is available, then adding Jiff to +the project can cause it to break. + +This isn't arguably Jiff's fault per se, but since these trait impls were just +about internal convenience and not essential to Jiff's design, we adopt a +pragmatic approach and just remove them. + +Bug fixes: + +* [#293](https://github.com/BurntSushi/jiff/issues/293): +Remove internal trait impls that can cause breaks due to inference failures. + + +0.2.3 (2025-03-07) +================== +This is a small release that fixes a bug in the handling of POSIX time zones +in some cases. Specifically, the implementation of `Date::yesterday` was wrong +when the date was the first of the month. This was a regression introduced in +`0.2.2` and was not present in older releases. More test coverage has been +added. + +Bug fixes: + +* [#290](https://github.com/BurntSushi/jiff/issues/290): +Fix bug in implementation of `Date::yesterday`. + + +0.2.2 (2025-03-06) +================== +This release of Jiff includes a new opt-in proc macro for embedding a +`TimeZone` into your binary. Just enable Jiff's `static` feature, and this will +print the current time in the `America/New_York` time zone: + +```rust +use jiff::{ + tz::{self, TimeZone}, + Timestamp, +}; + +fn main() { + static TZ: TimeZone = tz::get!("America/New_York"); + let zdt = Timestamp::now().to_zoned(TZ.clone()); + println!("{zdt}"); +} +``` + +This enables `TimeZone` to be meaningfully used in core-only environments, +even when dynamic memory allocation isn't available. + +This release also features a number of performance improvements for time zone +lookups. In some cases, the improvement is significant (by an order of +magnitude). + +Additionally, the IANA Time Zone Database embedded into `jiff-tzdb` now +uses "rearguard" semantics. This means that the boolean flag indicating +whether daylight saving time is active or not (only accessible via +`TimeZone::to_offset_info`) will respect the actual definition of +daylight saving time. (This is relevant, for example, for time zones like +`Europe/Dublin`, where their summer time is _legally_ known as their standard +time, but is in effect daylight saving time.) + +Enhancements: + +* [#256](https://github.com/BurntSushi/jiff/issues/256): +Add `tz::{get,include}` macros for time zone support in core-only environments. +* [#258](https://github.com/BurntSushi/jiff/issues/258): +Switch to `rearguard` tzdb data in `jiff-tzdb` and document it. +* [#259](https://github.com/BurntSushi/jiff/pull/259): +De-duplicate TZif data in `jiff-tzdb`. +* [#273](https://github.com/BurntSushi/jiff/issues/273): +Add "crate features" documentation to `jiff-sqlx` and `jiff-diesel`. +* [#277](https://github.com/BurntSushi/jiff/issues/277): +Document semver guarantee for error conditions on `Timestamp` constructors. +* [#277](https://github.com/BurntSushi/jiff/pull/287): +Greatly optimize time zone lookups (for both timestamps and civil datetimes). + +Bug fixes: + +* [#261](https://github.com/BurntSushi/jiff/issues/261): +Improve the documentation for `ZonedWith::nanosecond` and +`ZonedWith::subsec_nanosecond`. + + +0.2.1 (2025-02-16) +================== +This release includes a massive number of optimizations that significantly +improves performance in some cases. If you had a workload whose performance +with Jiff was underwhelming, please give it a try. I welcome questions via +[Discussions on GitHub]. + +This release also provides a new API, `Timestamp::constant`, for constructing +`Timestamp` values in a `const` context. + +Enhancements: + +* [#235](https://github.com/BurntSushi/jiff/discussions/235), + [#255](https://github.com/BurntSushi/jiff/discussions/255), + [#266](https://github.com/BurntSushi/jiff/pull/266): +A massive number of performance improvements. Jiff should now generally be +faster than `chrono` and `time`. +* [#263](https://github.com/BurntSushi/jiff/issues/263): +Add `Timestamp::constant` for constructing timestamps in `const` context. + + +0.2.0 (2025-02-10) +================== +This is a new semver incompatible release of Jiff. It contains several breaking +changes. I expect most users of Jiff to be able to upgrade without any changes. +The fundamental API organization of Jiff has not changed. + +Some of the highlights of this release include reducing footguns and better +ecosystem integration. + +For reducing footguns, APIs on `Span` will no longer implicitly assume that +days are always 24 hours long. And `Span` no longer implements `PartialEq` or +`Eq` (instead favoring `span.fieldwise()` to create a value that supports naive +fieldwise comparison). Moreover, when using `TimeZone::system()` (perhaps via +`Zoned::now()`), if the system time zone could not be detected, then a special +`Etc/Unknown` time zone will be used instead. This avoids erroring, but also +surfaces itself to make it clearer that something has (perhaps) gone wrong. + +As for ecosystem integration, this release coincides with the publication +of the [`jiff-icu`], [`jiff-sqlx`] and [`jiff-diesel`] crates. `jiff-icu` +integrates with the [ICU4X project], and is now the recommended way to use +Jiff to work with non-Gregorian calendars or to localize datetimes for end +users. `jiff-sqlx` and `jiff-diesel` provide wrapper types that implement the +necessary traits to make it ergonomic to store and retrieve Jiff values in a +database using [SQLx] or [Diesel], respectively. + +Unless something unexpected happens, my plan is for the next breaking change +release to be Jiff 1.0 in about 6 months. Once Jiff 1.0 is out, I plan to +commit to it indefinitely. + +**BREAKING CHANGES**: + +This is an exhaustive list of breaking changes. Changes with the bolded +**RUNTIME** prefix are changes that will not be caught by the Rust compiler. +That is, they are changes in runtime behavior. + +* [#28](https://github.com/BurntSushi/jiff/issues/28): +The deprecated `intz` routines on `Zoned`, `Timestamp`, `civil::DateTime` and +`civil::Date` have been removed. You can use `in_tz` instead. This change was +made because many found the name `intz` to be unclear. +* [#32](https://github.com/BurntSushi/jiff/issues/32): +The `PartialEq` and `Eq` trait implementations on `Span` have been removed. +Ideally these shouldn't have been used, but if you do need them, please use +`Span::fieldwise` to create a `SpanFieldwise`, which does have the `PartialEq` +and `Eq` traits implemented. These were removed on `Span` itself because they +made it very easy to commit subtle bugs. +* [#36](https://github.com/BurntSushi/jiff/issues/36): +Turn panics during `Timestamp::saturing_add` into errors. Callers adding +spans that are known to contain units of hours or smaller are guaranteed that +this will not return an error. +* **RUNTIME** [#48](https://github.com/BurntSushi/jiff/issues/48): +On `Span` APIs, days are no longer silently assumed to always be 24 hours when +a relative datetime is not provided. Instead, to perform operations on units +of days or bigger, callers must either provide a relative date or opt into +invariant 24-hour days with `SpanRelativeTo::days_are_24_hours`. Shortcuts have +been added to the span builders. For example, `SpanTotal::days_are_24_hours`. +* **RUNTIME** [#147](https://github.com/BurntSushi/jiff/issues/147): +Change the behavior of the deprecated `%V` conversion specifier in +`jiff::fmt::strtime` from formatting an IANA time zone identifier to formatting +an ISO 8601 week number. To format an IANA time zone identifier, use `%Q` or +`%:Q` (which were introduced in `jiff 0.1`). +* **RUNTIME** [#212](https://github.com/BurntSushi/jiff/issues/212): +When parsing into a `Zoned` with a civil time corresponding to a gap, we treat +all offsets as invalid and return an error. Previously, we would accept the +offset as given. This brings us into line with Temporal's behavior. For +example, previously Jiff accepted `2006-04-02T02:30-05[America/Indiana/Vevay]` +but will now return an error. This is desirable for cases where a datetime in +the future is serialized before a change in the daylight saving time rules. +For more examples, see `jiff::fmt::temporal::DateTimeParser::offset_conflict` +for details on how to change Jiff's default behavior. This behavior change also +applies to `tz::OffsetConflict::PreferOffset`. +* **RUNTIME** [#213](https://github.com/BurntSushi/jiff/issues/213): +Tweak the semantics of `tz::TimeZoneDatabase` so that it only initializes one +internal tzdb instead of trying to find as many as possible. It is unlikely +that you'll be impacted by this change, but it's meant to make the semantics +be a bit more sensible. (In `jiff 0.1`, it was in theory possible for one tz +lookup to succeed in the system zoneinfo and then another tz lookup to fail +in zoneinfo but succeed automatically via the bundled copy. But this seems +confusing and possibly not desirable. Hence the change.) +* [#218](https://github.com/BurntSushi/jiff/issues/218): +In order to make naming a little more consistent between `Zoned` +and `civil::Date`, the `civil::Date::to_iso_week_date` and +`civil::ISOWeekDate::to_date` APIs were renamed to `civil::Date::iso_week_date` +and `civil::ISOWeekDate::date`. +* [#220](https://github.com/BurntSushi/jiff/issues/220): +Remove `Span::to_duration` for converting a `Span` to a `std::time::Duration` +and rename `Span::to_jiff_duration` to `Span::to_duration`. This prioritizes +`SignedDuration` as the "primary" non-calendar duration type in Jiff. And makes +it more consistent with APIs like `Zoned::duration_since`. For non-calendar +spans, the `TryFrom for std::time::Duration` still exists. For calendar +durations, use `Span::to_duration` and then convert the `SignedDuration` to +`std::time::Duration`. Additionally, `Timestamp::from_jiff_duration` and +`Timestamp::as_jiff_duration` were renamed to `Timestamp::from_duration` and +`Timestamp::as_duration`, respectively. The old deprecated routines on the +unsigned `std::time::Duration` have been removed. +* [#221](https://github.com/BurntSushi/jiff/issues/221): +Change the type of the value yielded by the `jiff::tz::TimeZoneNameIter` +iterator from `String` to `jiff::tz::TimeZoneName`. This opaque type is more +API evolution friendly. To access the string, either use `TimeZoneName`'s +`Display` trait implementation, or its `as_str` method. +* [#222](https://github.com/BurntSushi/jiff/issues/222): +Split `TimeZone::to_offset` into two methods. One that just returns the +offset, and another, `TimeZone::to_offset_info`, which includes the offset, +DST status and time zone abbreviation. The extra info is rarely needed and +is sometimes more costly to compute. Also, make the lifetime of the time +zone abbreviation returned by `TimeZoneTransition::abbreviation` tied to +the transition instead of the time zone (for future API flexibility, likely +in core-only environments). This change was overall motivated by wanting to +do less work in the common case (where we only need the offset), and for +reducing the size of a `TimeZone` considerably in core-only environments. +Callers previously using `TimeZone::to_offset` to get DST status and time zone +abbreviation should now use `TimeZone::to_offset_info`. +* **RUNTIME** [#230](https://github.com/BurntSushi/jiff/issues/230): +When `TimeZone::system()` cannot find a system configured time zone, `jiff +0.1` would automatically fall back to `TimeZone::UTC` (with a WARN-level log +message). In `jiff 0.2`, the fall back is now to `TimeZone::unknown()`, which +has a special `Etc/Unknown` identifier (as specified by Unicode and reserved by +the IANA time zone database). The fallback otherwise still behaves as if it +were `TimeZone::UTC`. This helps surface error conditions related to finding +the system time zone without causing unrecoverable failure. + +Enhancements: + +* [#136](https://github.com/BurntSushi/jiff/issues/136): +When the special `SpanRelativeTo::days_are_24_hours()` marker is used, weeks +will also be treated as invariant. That is, seven 24-hour days. In all cases, +working with years and months still requires a relative date. +* [#228](https://github.com/BurntSushi/jiff/issues/228): +It is now possible to forcefully use a bundled copy of the IANA time zone +database without relying on disabling crate features. This can be done by +enabling the `tzdb-bundle-always` crate feature and explicitly creating a +`jiff::tz::TimeZoneDatabase::bundled()` database. Once in hand, you must use +APIs like `TimeZoneDatabase::get` to create a `TimeZone` and avoid any APIs +that implicitly use the global time zone database (like `Timestamp::in_tz` or +even `Zoned::from_str`). +* [#238](https://github.com/BurntSushi/jiff/pull/238): +Add integration with the ICU4X project via the [`jiff-icu`] crate. `jiff-icu` +provides traits for easily converting between datetime types defined in Jiff +and datetime types defined in ICU4X. +* [#240](https://github.com/BurntSushi/jiff/pull/240): +Add integration with the SQLx project via the [`jiff-sqlx`] crate. `jiff-sqlx` +provides wrapper types that implement the necessary traits in SQLx for +reasonably ergonomic integration. This includes PostgreSQL and SQLite support, +but not MySQL support. (It's not clear if it's possible at present to provide +MySQL support for SQLx for datetime types outside of SQLx itself.) +* [#241](https://github.com/BurntSushi/jiff/pull/241): +Add integration with the Diesel project via the [`jiff-diesel`] crate. +`jiff-diesel` provides wrapper types that implement the necessary traits in +Diesel for reasonably ergonomic integration. This includes MySQL, PostgreSQL +and SQLite support. + + +0.1.29 (2025-02-02) +=================== +This release includes a few small enhancements and a bug fix. In particular, +there is now Serde support for `TimeZone` and the `ISOWeekDate` API has been +filled out a bit more. + +Unless a serious issue is uncovered, my plan is that this will be the last +release before `jiff 0.2`. + +Enhancements: + +* [#89](https://github.com/BurntSushi/jiff/issues/89): +Opt-in support for using Serde with `jiff::tz::TimeZone` has been added. +* [#227](https://github.com/BurntSushi/jiff/issues/227): +The `civil::ISOWeekDate` API has been beefed up with a few convenience methods. +* [#233](https://github.com/BurntSushi/jiff/issues/233): +Add `tz::Offset::round` for rounding time zone offsets. + +Bug fixes: + +* [#231](https://github.com/BurntSushi/jiff/issues/231): +Use more flexible offset equality when parsing offsets with fractional minutes. + + +0.1.28 (2025-01-27) +=================== +This is a small release that just removes the dev-dependency on `serde_yml`. +It has been replaced with the deprecated `serde_yaml`. See +[this post about `serde_yml` shenanigans](https://x.com/davidtolnay/status/1883906113428676938) +for why this was done. Note that this was only a dev-dependency and thus doesn't +impact folks using Jiff. + +Bug fixes: + +* [#225](https://github.com/BurntSushi/jiff/pull/225): +Remove dependency on `serde_yml` in favor of `serde_yaml`. + + +0.1.27 (2025-01-25) +=================== +This is a small release with a bug fix for precision loss in some cases when +doing arithmetic on `Timestamp` or `Zoned`. + +Bug fixes: + +* [#223](https://github.com/BurntSushi/jiff/issues/223): +Fix the check for fractional seconds before taking the fast path. + + +0.1.26 (2025-01-23) +=================== +This is a small release with another deprecation and a new API for doing +prefix parsing via `strptime`. There's also a bug fix for a corner case +when dealing with daylight saving time gaps with the `Zoned::with` API. + +Deprecations: + +* [#210](https://github.com/BurntSushi/jiff/pull/210): + Deprecate `ISOWeekDate::to_date` in favor of `ISOWeekDate::date`. + +Enhancements: + +* [#209](https://github.com/BurntSushi/jiff/issues/209): + Add `fmt::strtime::BrokenDownTime::parse_prefix` for parsing only a prefix. + +Bug fixes: + +* [#211](https://github.com/BurntSushi/jiff/issues/211): + Fix unintuitive behavior of `Zoned::with` when time falls in a DST gap. + + +0.1.25 (2025-01-21) +=================== +This release contains a number of deprecations in preparation for a `jiff 0.2` +release. The deprecations are meant to facilitate a smoother transition. The +deprecations, when possible, come with new APIs that will permit users to write +forward compatible code that will work in both `jiff 0.1` and `jiff 0.2`. + +This release also includes a handful of new conversion specifiers in Jiff's +`strftime` and `strptime` APIs. This improves compatibility with the analogous +implementation with GNU libc. + +Deprecations: + +* [#28](https://github.com/BurntSushi/jiff/issues/28): +The `intz` methods on `Zoned`, `Timestamp`, `civil::DateTime` and `civil::Date` +have been deprecated in favor of `in_tz`. +* [#32](https://github.com/BurntSushi/jiff/issues/32): +The `Eq` and `PartialEq` trait implementations on `Span` have been deprecated +in favor of using the new `SpanFieldwise` type. +* [#48](https://github.com/BurntSushi/jiff/issues/48): +Silently assuming days are always 24 hours in some `Span` APIs has now been +deprecated. This will become an error in `jiff 0.2`. To continue assuming +days are 24 hours without a relative reference date, you can use the new +`SpanRelativeTo::days_are_24_hours` API. In `jiff 0.1`, you'll seen a +WARN-level log message emitted if you're code will be broken by `jiff 0.2`. +* [#147](https://github.com/BurntSushi/jiff/issues/147): +Both `%V` and `%:V` have been deprecated in favor of `%Q` and `%:Q`. In +`jiff 0.2`, `%V` will correspond to the ISO 8601 week number and `%:V` will +result in an error. This change was made to improve compatibility with other +`strtime` implementations. `%V` and `%:V` continue to correspond to IANA +time zone identifiers in `jiff 0.1`, but using them for parsing or formatting +will result in a WARN-level deprecation message. + +Enhancements: + +* [#147](https://github.com/BurntSushi/jiff/issues/147): +Adds a number of new conversion specifiers to Jiff's `strftime` and +`strptime` APIs. Specifically, `%C`, `%G`, `%g`, `%j`, `%k`, `%l`, `%n`, `%R`, +`%s`, `%t`, `%U`, `%u`, `%W`, `%w`. Their behavior should match the +corresponding specifiers in GNU libc. + + +0.1.24 (2025-01-16) +=================== +This release updates Jiff's bundled copy of the [IANA Time Zone Database] to +`2025a`. See the [`2025a` release announcement] for more details. + +Enhancements: + +* [#206](https://github.com/BurntSushi/jiff/pull/206): +Update `jiff-tzdb` to `2025a`. + +[`2025a` release announcement]: https://lists.iana.org/hyperkitty/list/tz-announce@iana.org/thread/MWII7R3HMCEDNUCIYQKSSTYYR7UWK4OQ/ + + +0.1.23 (2025-01-13) +=================== +This release includes some bug fixes, particularly for compilation on +`aarch64-linux-android`. There are also some minor enhancements, such as making +`Zoned::iso_week_date` a convenience function for `civil::Date::iso_week_date`, +in line with similar functions. + +My current plan is to make a reasonably quick transition to `jiff 0.2` with a +few pending breaking changes. I will be making some `jiff 0.1` releases with +deprecations in order to make the transition as smooth as possible. If all goes +well with `jiff 0.2`, then my plan is still to do a Jiff 1.0 release in the +Summer of 2025. + +Deprecations: + +* [#197](https://github.com/BurntSushi/jiff/discussions/197): +`Date::to_iso_week_date` has been deprecated in favor of `Date::iso_week_date`. + +Enhancements: + +* [#196](https://github.com/BurntSushi/jiff/discussions/196): +Improve ISO week date documentation regarding weekday offsets. +* [#197](https://github.com/BurntSushi/jiff/discussions/197): +Add `Zoned::iso_week_date`, `DateTime::iso_week_date` and +`Date::iso_week_date`. + +Bug fixes: + +* [#200](https://github.com/BurntSushi/jiff/issues/200): +Fix compilation failure on Android in a Termux shell. +* [#202](https://github.com/BurntSushi/jiff/pull/202): +Re-add license files to crate artifact. + + +0.1.22 (2025-01-12) +=================== +This release adds support for Android. This support means that Jiff will +automatically read its special concatenated time zone database, and will +read the `persist.sys.timezone` property to determine the system's current +time zone. + +See [PLATFORM] for more specific information about Android support. + +Note that this release also removed all non-essential files (including tests +and test data) for the artifact uploaded to crates.io. If you need or want +these files, please open a new issue. + +Enhancements: + +* [#140](https://github.com/BurntSushi/jiff/issues/140): +Add support for the Android platform. + + +0.1.21 (2025-01-04) +=================== +This release includes a new API for setting the unit designator label in a +friendly formatted duration for zero-length durations. + +Enhancements: + +* [#192](https://github.com/BurntSushi/jiff/issues/192): +Add option to the friendly printer for setting the unit when writing a +zero-length duration. + + +0.1.20 (2025-01-03) +=================== +This release includes a new type, `Pieces`, in the `jiff::fmt::temporal` +sub-module. This exposes the individual components of a parsed Temporal +ISO 8601 datetime string. It allows users of Jiff to circumvent the checks +in the higher level parsing routines that prevent you from shooting yourself +in the foot. + +For example, parsing into a `Zoned` will return an error for raw RFC 3339 +timestamps like `2025-01-03T22:03-05` because there is no time zone annotation. +Without a time zone, Jiff cannot do time zone aware arithmetic and rounding. +Instead, such a datetime can only be parsed into a `Timestamp`. This lower +level `Pieces` API now permits users of Jiff to parse this string into its +component parts and assemble it into a `Zoned` if they so choose. + +Enhancements: + +* [#188](https://github.com/BurntSushi/jiff/issues/188): +Add `fmt::temporal::Pieces` for granular datetime parsing and formatting. + + +0.1.19 (2025-01-02) +=================== +This releases includes a UTF-8 related bug fix and a few enhancements. + +Firstly, a `Span`'s default `Display` implementation now writes uppercase +unit designator labels. That means you'll get `P1Y2M3DT4H5M6S` instead +of `P1y2m3dT4h5m6s` by default. You can restore previous behavior via +`jiff::fmt::temporal::SpanPrinter::lowercase`. This change was made to improve +interoperability. + +Secondly, `SignedDuration` now supports rounding via `SignedDuration::round`. +Note that it only supports rounding time units (hours or smaller). In order to +round with calendar units, you'll still need to use a `Span`. + +Enhancements: + +* [#130](https://github.com/BurntSushi/jiff/issues/130): +Document value ranges for methods like `year`, `day`, `hour` and so on. +* [#187](https://github.com/BurntSushi/jiff/issues/187): +Add a rounding API (for time units only) on `SignedDuration`. +* [#190](https://github.com/BurntSushi/jiff/issues/190): +`Span` and `SignedDuration` now use uppercase unit designator labels in their +default ISO 8601 `Display` implementation. + +Bug fixes: + +* [#155](https://github.com/BurntSushi/jiff/issues/155): +Relax `strftime` format strings from ASCII-only to all of UTF-8. + + +0.1.18 (2024-12-31) +=================== +This release includes a few minor enhancements. Namely, the ability to iterate +over time zone transitions (in the future or the past), and some improvements +to failure modes when `Timestamp` and `Span` arithmetic fails. + +Enhancements: + +* [#144](https://github.com/BurntSushi/jiff/issues/144): +Add APIs for iterating over the transitions of a time zone. +* [#145](https://github.com/BurntSushi/jiff/issues/145): +Improve docs and error messages around fallible `Timestamp` arithmetic. + + +0.1.17 (2024-12-31) +=================== +This release enhances Jiff's support for `no_std` environments by making its +`alloc` feature optional. When `alloc` is disabled, only fixed offset time +zones are supported and error messages are significantly degraded. If you have +core-only use cases for Jiff, I'd love to hear about them on the issue tracker. + +Enhancements: + +* [#162](https://github.com/BurntSushi/jiff/issues/162): +Support platforms that do not have atomics in `std`. +* [#168](https://github.com/BurntSushi/jiff/issues/168): +Jiff now supports disabling the `alloc` feature, which enables core-only mode. +* [#169](https://github.com/BurntSushi/jiff/issues/169): +Add `TimeZone::to_fixed_offset` for accessing an invariant offset if possible. + + +0.1.16 (2024-12-26) +=================== +This release includes a new `jiff::fmt::friendly` module for formatting and +parsing durations in a more human readable format than what ISO 8601 specifies. +ISO 8601 remains the "default" duration format in Jiff due to its widespread +support. Here are some examples: + +```text +40d +40 days +1y1d +1yr 1d +3d4h59m +3 days, 4 hours, 59 minutes +3d 4h 59m +2h30m +2h 30m +1mo +1w +1 week +1w4d +1 wk 4 days +1m +0.0021s +0s +0d +0 days +3 mins 34s 123ms +3 mins 34.123 secs +3 mins 34,123s +1y1mo1d1h1m1.1s +1yr 1mo 1day 1hr 1min 1.1sec +1 year, 1 month, 1 day, 1 hour, 1 minute 1.1 seconds +1 year, 1 month, 1 day, 01:01:01.1 +``` + +To quickly demonstrate this new feature, here's a simple CLI program using +Clap: + +```ignore +use clap::Parser; +use jiff::{Span, Zoned}; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + duration: Span, +} + +fn main() { + let args = Args::parse(); + println!("adding duration to now: {}", &Zoned::now() + args.duration); +} +``` + +And running the program: + +```text +$ cargo run -q -- '1 year, 2 months, 5 hours' +adding duration to now: 2026-02-26T18:58:22-05:00[America/New_York] +$ cargo run -q -- 'P1Y2MT5H' # ISO 8601 durations are supported too! +adding duration to now: 2026-02-26T19:00:57-05:00[America/New_York] +``` + +With Jiff, you should no longer need to pull in crates like +[`humantime`](https://docs.rs/humantime) and +[`humantime-serde`](https://docs.rs/humantime-serde) +to accomplish a similar task. + +While this new format doesn't support any kind of internationalization, the +prevalence of the `humantime` crate suggests there's a desire for something +like this. The "friendly" format is meant to service all the same use cases +as `humantime` does for durations, but in a way that doesn't let you shoot +yourself in the foot. + +The new "friendly" format is now the default for the `Debug` implementations +of both `Span` and `SignedDuration`. It's also available via the "alternate" +`Display` implementations for `Span` and `SignedDuration` as well. Moreover, +the `FromStr` trait implementations for both `Span` and `SignedDuration` will +parse _both_ the ISO 8601 duration and this new "friendly" format. Finally, +when `serde` integration is enabled, the `Deserialize` implementations for +`SignedDuration` and `Span` also automatically parse either ISO 8601 or the +friendly format. For serialization, ISO 8601 remains the default, but the +`jiff::fmt::serde` module provides easy to use helpers to switch to the +friendly format. + +The `jiff::fmt::friendly` module documentation provides many more details, +including a complete grammar for the format. + +Enhancements: + +* [#143](https://github.com/BurntSushi/jiff/pull/143): +Add `Hash` implementation for `Zoned` and `Timestamp`. + +Bug fixes: + +* [#60](https://github.com/BurntSushi/jiff/issues/60): +Use a better `Debug` output format for `SignedDuration` and `Span`. +* [#111](https://github.com/BurntSushi/jiff/issues/111): +Add a new "friendly" duration format. +* [#138](https://github.com/BurntSushi/jiff/issues/138): +Fix deserialization in `serde_yml` and `serde_yaml` crates. +* [#161](https://github.com/BurntSushi/jiff/pull/161): +Fix `serde` dependency configuration so that it builds in no-std mode. + + +0.1.15 (2024-11-30) +=================== +This release fixes a bug where Jiff would sometimes fail to parse TZif files +(found, typically, in `/usr/share/zoneinfo` on Unix systems). This occurred +when the TZif file contained a time zone transition outside the range of Jiff's +`Timestamp` type (which is `-9999-01-01` to `9999-12-31`). The bug fix works by +clamping the out-of-range transitions to Jiff's supported range. + +This bug only seems to occur in some environments where their TZif files +contain more extreme values than what is typically found. + +Bug fixes: + +* [#163](https://github.com/BurntSushi/jiff/issues/163): +Fix a bug where Jiff would fail to parse some TZif files. + + +0.1.14 (2024-11-01) +=================== +This release introduces new APIs to the RFC 2822 printer that explicitly +print timestamps in a format strictly compatible with [RFC 9110]. + +Enhancements: + +* [#151](https://github.com/BurntSushi/jiff/issues/151): +Add `rfc2822::DateTimePrinter::timestamp_to_rfc9110_string` method. + + +0.1.13 (2024-09-07) +=================== +This release introduces a new `jiff::tz::TimeZone::try_system` API. It is like +`TimeZone::system`, but returns an error instead of an automatic fall back to +UTC when the system time zone could not be discovered. + +This also includes an update to the bundled [IANA Time Zone Database] to the +`2024b` release in the `jiff-tzdb` crate. As a reminder, the bundled database +is not used or included on Unix platforms by default. See [PLATFORM] for more +details. + +Enhancements: + +* [#65](https://github.com/BurntSushi/jiff/issues/65): +Add `TimeZone::try_system` for fallibly determining the system's time zone. +* [#125](https://github.com/BurntSushi/jiff/pull/125): +Update to the `2024b` release of the [IANA Time Zone Database]. + + +0.1.12 (2024-08-31) +=================== +This release introduces some new minor APIs that support formatting +`Timestamp` values as RFC 3339 strings with a specific offset. + +Previously, using the standard formatting routines that Jiff provides, it was +only possible to format a `Timestamp` using Zulu time. For example: + +```rust +use jiff::Timestamp; + +assert_eq!( + Timestamp::UNIX_EPOCH.to_string(), + "1970-01-01T00:00:00Z", +); +``` + +This is fine most use cases, but it can be useful on occasion to format +a `Timestamp` with a specific offset. While this isn't as expressive +as formatting a datetime with a time zone (e.g., with an IANA time +zone identifier), it may be useful in contexts where you just want to +"hint" at what a user's local time is. To that end, there is a new +`Timestamp::display_with_offset` method that makes this possible: + +```rust +use jiff::{tz, Timestamp}; + +assert_eq!( + Timestamp::UNIX_EPOCH.display_with_offset(tz::offset(-5)).to_string(), + "1969-12-31T19:00:00-05:00", +); +``` + +A corresponding API was added to `jiff::fmt::temporal::DateTimePrinter` for +lower level use. + +Moreover, this release also includes new convenience APIs on the Temporal and +RFC 2822 printer types for returning strings. For example, previously, if you +were using the RFC 2822 printer to format a `Timestamp`, you had to do this: + +```rust +use jiff::{fmt::rfc2822::DateTimePrinter, Timestamp}; + +let mut buf = String::new(); +DateTimePrinter::new().print_timestamp(&Timestamp::UNIX_EPOCH, &mut buf).unwrap(); +assert_eq!(buf, "Thu, 1 Jan 1970 00:00:00 -0000"); +``` + +But now you can just do this: + +```rust +use jiff::{fmt::rfc2822::DateTimePrinter, Timestamp}; + +assert_eq!( + DateTimePrinter::new().timestamp_to_string(&Timestamp::UNIX_EPOCH).unwrap(), + "Thu, 1 Jan 1970 00:00:00 -0000", +); +``` + +Enhancements: + +* [#122](https://github.com/BurntSushi/jiff/pull/122): +Support formatting `Timestamp` to an RFC 3339 string with a specific offset. + + +0.1.11 (2024-08-28) +=================== +This release includes a few small enhancements that have been requested over +the last several weeks. The biggest enhancement is a new `jiff::fmt::serde` +sub-module. It provides convenience routines similar to Chrono's +`chrono::serde` sub-module for serializing and deserializing between a +`jiff::Timestamp` and an integer number of seconds, milliseconds, microseconds +or nanoseconds. For example: + +```rust +use jiff::Timestamp; + +#[derive(serde::Serialize, serde::Deserialize)] +struct Record { + #[serde(with = "jiff::fmt::serde::timestamp::second::required")] + timestamp: Timestamp, +} + +let json = r#"{"timestamp":1517644800}"#; +let got: Record = serde_json::from_str(&json).unwrap(); +assert_eq!(got.timestamp, Timestamp::from_second(1517644800).unwrap()); +assert_eq!(serde_json::to_string(&got).unwrap(), json); +``` + +If you need to support optional timestamps via `Option`, then use +`jiff::fmt::serde::timestamp::second::optional` instead. Similarly, if you +need to support milliseconds instead of seconds, then replace `second` with +`millisecond` in the module path. + +Enhancements: + +* [#78](https://github.com/BurntSushi/jiff/issues/78): +Add `BrokenDownTime::set_{offset,iana_time_zone}` APIs. +* [#93](https://github.com/BurntSushi/jiff/issues/93): +Add note about using `Timestamp::now().to_zoned()` instead of +`Zoned::now().with_time_zone()`. +* [#101](https://github.com/BurntSushi/jiff/issues/101): +Add new `jiff::fmt::serde` module for integration with integer timestamps. +* [#117](https://github.com/BurntSushi/jiff/pull/117): +Remove `unsafe` usage in `libm` functions (applicable only to no-std users). + + +0.1.10 (2024-08-23) +=================== +This release features a small bug fix where Jiff will detect an IANA time +zone identifier in some cases where it wouldn't before. While Jiff would +previously read the symlink metadata on `/etc/localtime` by default to discover +the system configured time zone on Unix systems, it *wouldn't* do so when +`TZ=/etc/localtime`. There's really no reason not to, so this release of Jiff +is fixed to use symlink sniffing on file paths provided by the `TZ` environment +variable. + +Bug fixes: + +* [#113](https://github.com/BurntSushi/jiff/issues/113): +When `TZ=/etc/localtime`, use symlink metadata to detect IANA identifier. + + +0.1.9 (2024-08-23) +================== +This release introduces new options for controlling the precision +of fractional seconds when printing `Zoned`, `Timestamp`, +`civil::DateTime` or `civil::Time` values. This is principally exposed +via `jiff::fmt::temporal::DateTimePrinter::precision`, but it's also +available via the standard library's formatting machinery. For example, +if `zdt` is a `jiff::Zoned`, then `format!("{zdt:.6}")` will format +it into a string with microsecond precision, even if its fractional +component is zero. + +Enhancements: + +* [#92](https://github.com/BurntSushi/jiff/issues/92): +Support setting the precision of fractional seconds when printing datetimes. + + +0.1.8 (2024-08-19) +================== +This releases fixes a build error in Jiff's `alloc`-only configuration. This +regression was introduced in `jiff 0.1.6`. + +Bug fixes: + +* [#108](https://github.com/BurntSushi/jiff/issues/108): +Use `core::time::Duration` everywhere instead of `std::time::Duration`. + + +0.1.7 (2024-08-18) +================== +This release relaxes Jiff's dependency on `windows-sys` to include multiple +semver incompatible releases. The purpose of this relaxation is to enable +Jiff to work with different versions of `windows-sys` in the hopes that this +reduces the likelihood that multiple copies of `windows-sys` are included in +your dependency tree. + +Dependencies: + +* [#106](https://github.com/BurntSushi/jiff/pull/106): +Relax `windows-sys` dependency constraint to `>=0.52.0, <=0.59.*`. + + +0.1.6 (2024-08-18) +================== +This release includes a new top-level type, `SignedDuration`, that provides a +near exact replica of `std::time::Duration`, but signed. It is meant to provide +alternative APIs for working with durations at a lower level than what `Span` +provides, and to facilitate better integration with the standard library. + +A `SignedDuration` has also been integrated with all of Jiff's datetime types. +For example, previously, `Zoned::checked_add` only accepted a concrete +`jiff::Span`. But now it accepts a `jiff::Span`, `jiff::SignedDuration` or even +a `std::time::Duration`. Moreover, all of the `until` and `since` APIs on +datetime types have been ported and copied to return `SignedDuration` under the +`duration_until` and `duration_since` names. + +This marks an initial integration phase with `SignedDuration`. It is planned +to integrate it more with the datetime types. Currently, there are integrations +on `Timestamp` and `Span`, but more will be added in the future. + +Overall, folks should still use `Span`. That is the intended default duration +type in Jiff and will continue to be. Users of Jiff may find `SignedDuration` +useful in contexts where speed is important or when one needs to integrate +with the standard library. + +This release also includes a few related deprecations as the APIs involving +`std::time::Duration` are phased out in favor of `SignedDuration`. + +Deprecations: + +* `Timestamp::as_duration`: replaced with `as_jiff_duration`, which will be +renamed to `as_duration` in `jiff 0.2`. +* `Timestamp::from_duration`: replaced with `from_jiff_duration`, which will be +renamed to `from_duration` in `jiff 0.2`. +* `Timestamp::from_signed_duration`: replaced with `from_jiff_duration`. +* `Span::to_duration`: replaced with `to_jiff_duration`, which will be renamed +to `to_duration` in `jiff 0.2`. + +Basically, all of the above APIs either accept or return a +`std::time::Duration`. To avoid breaking changes at this point, new methods +for `SignedDuration` were added. For example, `Timestamp::as_jiff_duration`. +In `jiff 0.2`, the above deprecated methods will be removed and replaced with +equivalent methods that accept or return a `SignedDuration` instead. Callers +can then convert between a `SignedDuration` and a `std::time::Duration` using +appropriate `TryFrom` trait implementations. + +Enhancements: + +* [#21](https://github.com/BurntSushi/jiff/issues/21): +Add new top-level `SignedDuration` type. +* [#90](https://github.com/BurntSushi/jiff/issues/90): +Improve error message when using `Span` with >=day units with a `Timestamp`. + +Performance: + +* [#104](https://github.com/BurntSushi/jiff/pull/104) +Optimize offset calculations in time zones without DST. +* [#105](https://github.com/BurntSushi/jiff/pull/105) +Optimize offset calculations for timestamps after last DST rule change. + + +0.1.5 (2024-08-09) +================== +This release includes some improvements and bug fixes, particularly for Jiff's +`strtime` APIs. + +Enhancements: + +* [#63](https://github.com/BurntSushi/jiff/issues/63): +Add link to original Chrono maintainer's commentary in `DESIGN.md`. +* [#75](https://github.com/BurntSushi/jiff/issues/75): +Add support for `%V` for formatting _and_ parsing IANA time zone identifiers. +* [#79](https://github.com/BurntSushi/jiff/pull/79): +Add `devcontainer.json` to support GitHub Codespaces. +* [#85](https://github.com/BurntSushi/jiff/pull/85): +Set correct ranges for internal tracking in return value of `days_in_month`. + +Bug fixes: + +* [#59](https://github.com/BurntSushi/jiff/issues/59): +Fixes a bug where some `Span`s could not be roundtripped through ISO 8601. +* [#71](https://github.com/BurntSushi/jiff/issues/71): +Tweak wording in documentation of "printf"-style API. +* [#73](https://github.com/BurntSushi/jiff/issues/73): +Make it so `%.Nf` only formats to `N` decimal places. +* [#77](https://github.com/BurntSushi/jiff/pull/77): +Disable optimizations when running tests. + + +0.1.4 (2024-08-01) +================== +This release includes a small improvement for `strptime` that permits +`%Y%m%d` to parse `20240730` correctly. + +Enhancements: + +* [#62](https://github.com/BurntSushi/jiff/issues/62): +Tweak `strptime` so that things like `%Y` aren't unceremoniously greedy. + + +0.1.3 (2024-07-30) +================== +This release features support for `wasm32-unknown-unknown`. That is, when +Jiff's new `js` crate feature is enabled, Jiff will automatically use +JavaScript APIs to determine the current time and time zone. + +Enhancements: + +* [#58](https://github.com/BurntSushi/jiff/pull/58): +Add WASM support and a new `PLATFORM.md` guide. + + +0.1.2 (2024-07-28) +================== +This release features a few new APIs that a need for arose while experimenting +with actually using Jiff in real projects. Namely, the `jiff::fmt::strtime` +module now has `%f` and `%.f` directives for parsing and formatting fractional +seconds. And both `jiff::fmt::rfc2822` and `jiff::fmt::strtime` now have +support for skipping weekday checks during parsing. (Previously, Jiff required +that an English weekday be consistent with the date parsed, and there was no +way to opt out. While this is still the default behavior, callers can disable +this check.) + +Enhancements: + +* [#52](https://github.com/BurntSushi/jiff/pull/52): +Improve documentation for `Span` getter methods. +* [#53](https://github.com/BurntSushi/jiff/pull/53): +Add support for skipping weekday checking when parsing datetimes. +* [#55](https://github.com/BurntSushi/jiff/pull/55): +Add support for fractional seconds in `jiff::fmt::strtime`. + +Bug fixes: + +* [#49](https://github.com/BurntSushi/jiff/pull/49): +Fix informational regex describing ISO 8601 format. +* [#51](https://github.com/BurntSushi/jiff/pull/51): +Explicitly allow new deny-by-default lint `ambiguous_negative_literals`. + + +0.1.1 (2024-07-25) +================== +This is a new semver compatible release. The principle addition are APIs for +converting between a `jiff::Span` and a `std::time::Duration`. Specifically, +there are now `TryFrom for Duration` and `TryFrom for Span` +trait implementations. There is also a `Span::to_duration`, which requires a +relative date, for converting spans with non-uniform units (like months) to a +`Duration`. + +Enhancements: + +* [#21](https://github.com/BurntSushi/jiff/issues/21), + [#40](https://github.com/BurntSushi/jiff/issues/40): +Adds APIs for converting between `Span` and `std::time::Duration`. + +Bug fixes: + +* [#36](https://github.com/BurntSushi/jiff/issues/36): +Saturating arithmetic for `Timestamp` panics with day-or-greater units. +* [#38](https://github.com/BurntSushi/jiff/issues/38): +Fix some bugs in the micro-benchmarks. +* [#39](https://github.com/BurntSushi/jiff/issues/39): +Document that the RFC 2822 parser is not technically fully spec compliant. + + +0.1.0 (2024-07-21) +================== +The initial release of Jiff. + +[IANA Time Zone Database]: https://www.iana.org/time-zones +[PLATFORM]: PLATFORM.md +[RFC 9110]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.7-15 +[`jiff-icu`]: https://docs.rs/jiff-icu +[`jiff-sqlx`]: https://docs.rs/jiff-sqlx +[`jiff-diesel`]: https://docs.rs/jiff-diesel +[ICU4X project]: https://github.com/unicode-org/icu4x +[SQLx]: https://github.com/launchbadge/sqlx +[Diesel]: https://github.com/diesel-rs/diesel +[Discussions on GitHub]: https://github.com/BurntSushi/jiff/discussions +[`defmt`]: https://github.com/BurntSushi/jiff/issues/587 diff --git a/crates/jiff/src/fmt/friendly/printer.rs b/crates/jiff/src/fmt/friendly/printer.rs new file mode 100644 index 0000000..c34d8f4 --- /dev/null +++ b/crates/jiff/src/fmt/friendly/printer.rs @@ -0,0 +1,4173 @@ +use jcore::constants as c; + +use crate::{ + fmt::{ + buffer::{ArrayBuffer, BorrowedBuffer}, + Write, + }, + Error, SignedDuration, Span, Unit, +}; + +const SECS_PER_HOUR: u64 = c::SECS_PER_HOUR as u64; +const SECS_PER_MIN: u64 = c::SECS_PER_MIN as u64; +const MINS_PER_HOUR: u64 = c::MINS_PER_HOUR as u64; +const NANOS_PER_HOUR: u128 = c::NANOS_PER_HOUR as u128; +const NANOS_PER_MIN: u128 = c::NANOS_PER_MIN as u128; +const NANOS_PER_MILLI: u32 = c::NANOS_PER_MILLI_32 as u32; +const NANOS_PER_MICRO: u32 = c::NANOS_PER_MICRO_32 as u32; + +/// Defines the maximum possible length (in bytes) of a `Span` printed in the +/// friendly format. +/// +/// The way I computed this length was by using settings that would produce the +/// maximal amount of text and using a negative `Span` with each unit set to +/// its minimum value: +/// +/// ```ignore +/// SpanPrinter::new() +/// .padding(u8::MAX) +/// .designator(friendly::Designator::Verbose) +/// .spacing(friendly::Spacing::BetweenUnitsAndDesignators) +/// .comma_after_designator(true); +/// ``` +const MAX_SPAN_LEN: usize = 306; + +/// Defines the maximum possible length (in bytes) of a `SignedDuration` +/// printed in the friendly format. +/// +/// See `MAX_SPAN_LEN` for how this was computed. In this case, +/// `SignedDuration::MIN` was used. +const MAX_SIGNED_DURATION_LEN: usize = 194; + +/// Defines the maximum possible length (in bytes) of a `std::time::Duration` +/// printed in the friendly format. +/// +/// See `MAX_SPAN_LEN` for how this was computed. In this case, +/// `std::time::Duration::MAX - std::time::Duration::from_secs(16)` was used +/// to find the maximal length. This was a little tricky because the maximal +/// unsigned duration has minute units equivalent to `0`, and is thus quite a +/// bit shorter than the maximum possible length. +/// +/// Note that this is shorter than `MAX_SIGNED_DURATION` because one cannot +/// get a negative formatted duration from a `std::time::Duration`. So there's +/// no way to get an ` ago` suffix (or a `-` prefix). +const MAX_UNSIGNED_DURATION_LEN: usize = 190; + +/// Configuration for [`SpanPrinter::designator`]. +/// +/// This controls which kinds of designators to use when formatting a +/// "friendly" duration. Generally, this only provides one axis of control: +/// the length of each designator. +/// +/// # Example +/// +/// ``` +/// use jiff::{fmt::friendly::{Designator, SpanPrinter}, ToSpan}; +/// +/// let span = 1.year().months(2); +/// +/// let printer = SpanPrinter::new(); +/// assert_eq!(printer.span_to_string(&span), "1y 2mo"); +/// +/// let printer = SpanPrinter::new().designator(Designator::Short); +/// assert_eq!(printer.span_to_string(&span), "1yr 2mos"); +/// +/// let printer = SpanPrinter::new().designator(Designator::Verbose); +/// assert_eq!(printer.span_to_string(&span), "1year 2months"); +/// +/// let printer = SpanPrinter::new().designator(Designator::HumanTime); +/// assert_eq!(printer.span_to_string(&span), "1y 2months"); +/// ``` +#[derive(Clone, Copy, Debug)] +#[non_exhaustive] +pub enum Designator { + /// This writes out the full word of each unit designation. For example, + /// `year`. + Verbose, + /// This writes out a short but not minimal label for each unit. For + /// example, `yr` for `year` and `yrs` for `years`. + Short, + /// This writes out the shortest possible label for each unit that is still + /// generally recognizable. For example, `y`. Note that in the compact + /// representation, and unlike the verbose and short representations, there + /// is no distinction between singular or plural. + Compact, + /// A special mode that uses designator labels that are known to be + /// compatible with the `humantime` crate. + /// + /// None of `Verbose`, `Short` or `Compact` are compatible with + /// `humantime`. + /// + /// `Compact` is, on its own, nearly compatible. When using `Compact`, all + /// designator labels are parsable by `humantime` except for months and + /// microseconds. For months, Jiff uses `mo` and `mos`, but `humantime` + /// only parses `months`, `month` and `M`. Jiff specifically doesn't + /// support `M` for months because of the confusability with minutes. + /// For microseconds, Jiff uses `µs` which `humantime` does not support + /// parsing. + /// + /// Most of the designator labels Jiff uses for `Short` aren't supported + /// by `humantime`. And even when they are, `humantime` is inconsistent. + /// For example, `humantime` supports `sec` and `secs`, but only `nsec` + /// and not `nsecs`. + /// + /// Finally, for `Verbose`, humantime supports spelling out some units + /// in their entirety (e.g., `seconds`) but not others (e.g., `nanoseconds` + /// is not supported by `humantime`). + /// + /// Therefore, this custom variant is provided so that designator labels + /// that are compatible with both Jiff and `humantime`, even when there + /// isn't a coherent concept otherwise connecting their style. + HumanTime, +} + +/// Configuration for [`SpanPrinter::spacing`]. +/// +/// This controls how much or how little whitespace is inserted into a +/// "friendly" formatted duration. Typically, one wants less whitespace when +/// using short unit designators (i.e., `y` instead of `years`), and more +/// whitespace when using longer unit designators. +/// +/// # Example +/// +/// ``` +/// use jiff::{ +/// fmt::friendly::{Designator, Spacing, SpanPrinter}, +/// ToSpan, +/// }; +/// +/// let span = 1.year().months(2); +/// +/// // The default tries to balance spacing with compact +/// // unit designators. +/// let printer = SpanPrinter::new(); +/// assert_eq!(printer.span_to_string(&span), "1y 2mo"); +/// +/// // But you can use slightly more descriptive +/// // designators without being too verbose. +/// let printer = SpanPrinter::new() +/// .designator(Designator::Short); +/// assert_eq!(printer.span_to_string(&span), "1yr 2mos"); +/// +/// // When spacing is removed, it usually looks nicer +/// // to use compact unit designators. +/// let printer = SpanPrinter::new() +/// .spacing(Spacing::None) +/// .designator(Designator::Compact); +/// assert_eq!(printer.span_to_string(&span), "1y2mo"); +/// +/// // Conversely, when using more spacing, it usually +/// // looks nicer to use verbose unit designators. +/// let printer = SpanPrinter::new() +/// .spacing(Spacing::BetweenUnitsAndDesignators) +/// .designator(Designator::Verbose); +/// assert_eq!(printer.span_to_string(&span), "1 year 2 months"); +/// ``` +#[derive(Clone, Copy, Debug)] +#[non_exhaustive] +pub enum Spacing { + /// Does not insert any ASCII whitespace. + /// + /// Except in the case that [`SpanPrinter::hours_minutes_seconds`] is + /// enabled and one is formatting a span with non-zero calendar units, then + /// an ASCII whitespace is inserted between the calendar and non-calendar + /// units even when `Spacing::None` is used. + None, + /// Inserts one ASCII whitespace between the unit designator and the next + /// unit value. + /// + /// For example, `1year 2months`. + BetweenUnits, + /// Inserts one ASCII whitespace between the unit value and the unit + /// designator, in addition to inserting one ASCII whitespace between the + /// unit designator and the next unit value. + /// + /// For example, `1 year 2 months`. + BetweenUnitsAndDesignators, +} + +impl Spacing { + fn between_units(self) -> Option { + match self { + Spacing::None => None, + Spacing::BetweenUnits => Some(b' '), + Spacing::BetweenUnitsAndDesignators => Some(b' '), + } + } + + fn between_units_and_designators(self) -> Option { + match self { + Spacing::None => None, + Spacing::BetweenUnits => None, + Spacing::BetweenUnitsAndDesignators => Some(b' '), + } + } +} + +/// Configuration for [`SpanPrinter::direction`]. +/// +/// This controls how the sign, if at all, is included in the formatted +/// duration. +/// +/// When using the "hours-minutes-seconds" format, `Auto` and `Suffix` are +/// both treated as equivalent to `Sign` when all calendar units (days and +/// greater) are zero. +/// +/// # Example +/// +/// ``` +/// use jiff::{fmt::friendly::{Direction, SpanPrinter}, SignedDuration}; +/// +/// let duration = SignedDuration::from_secs(-1); +/// +/// let printer = SpanPrinter::new(); +/// assert_eq!(printer.duration_to_string(&duration), "1s ago"); +/// +/// let printer = SpanPrinter::new().direction(Direction::Sign); +/// assert_eq!(printer.duration_to_string(&duration), "-1s"); +/// ``` +#[derive(Clone, Copy, Debug)] +#[non_exhaustive] +pub enum Direction { + /// Sets the sign format based on other configuration options. + /// + /// When [`SpanPrinter::spacing`] is set to [`Spacing::None`], then + /// `Auto` is equivalent to `Sign`. + /// + /// When using the "hours-minutes-seconds" format, `Auto` is equivalent to + /// `Sign` when all calendar units (days and greater) are zero. + /// + /// Otherwise, `Auto` is equivalent to `Suffix`. + /// + /// This is the default used by [`SpanPrinter`]. + Auto, + /// When set, a sign is only written when the span or duration is negative. + /// And when it is written, it is written as a prefix of the formatted + /// duration. + Sign, + /// A sign is always written, with `-` for negative spans and `+` for all + /// non-negative spans. The sign is always written as a prefix of the + /// formatted duration. + ForceSign, + /// When set, a sign is only written when the span or duration is negative. + /// And when it is written, it is written as a suffix via a trailing ` ago` + /// string. + Suffix, +} + +impl Direction { + /// Returns the sign string to use (as either a prefix or a suffix) based + /// on the given parameters. + /// + /// This lets us do the case analysis for how to write the sign exactly + /// once. + fn sign( + self, + printer: &SpanPrinter, + has_calendar: bool, + signum: i8, + ) -> Option { + match self { + Direction::Auto => match printer.spacing { + Spacing::None => { + if signum < 0 { + Some(DirectionSign::Prefix(b'-')) + } else { + None + } + } + Spacing::BetweenUnits + | Spacing::BetweenUnitsAndDesignators => { + if signum < 0 { + if printer.hms && !has_calendar { + Some(DirectionSign::Prefix(b'-')) + } else { + Some(DirectionSign::Suffix) + } + } else { + None + } + } + }, + Direction::Sign => { + if signum < 0 { + Some(DirectionSign::Prefix(b'-')) + } else { + None + } + } + Direction::ForceSign => { + Some(DirectionSign::Prefix(if signum < 0 { + b'-' + } else { + b'+' + })) + } + Direction::Suffix => { + if signum < 0 { + Some(DirectionSign::Suffix) + } else { + None + } + } + } + } +} + +/// The sign to write and whether it should be a prefix or a suffix. +#[derive(Clone, Copy, Debug)] +enum DirectionSign { + Prefix(u8), + Suffix, +} + +/// Configuration for [`SpanPrinter::fractional`]. +/// +/// This controls what kind of fractional unit to use when printing a duration. +/// The default, unless [`SpanPrinter::hours_minutes_seconds`] is enabled, is +/// to not write any fractional numbers at all. +/// +/// The fractional unit set refers to the smallest whole integer that can occur +/// in a "friendly" formatted duration. If there are any non-zero units less +/// than the fractional unit in the duration, then they are formatted as a +/// fraction. +/// +/// # Example +/// +/// This example shows how to write the same duration with different +/// fractional settings: +/// +/// ``` +/// use jiff::{fmt::friendly::{FractionalUnit, SpanPrinter}, SignedDuration}; +/// +/// let duration = SignedDuration::from_secs(3663); +/// +/// let printer = SpanPrinter::new() +/// .fractional(Some(FractionalUnit::Hour)); +/// assert_eq!(printer.duration_to_string(&duration), "1.0175h"); +/// +/// let printer = SpanPrinter::new() +/// .fractional(Some(FractionalUnit::Minute)); +/// assert_eq!(printer.duration_to_string(&duration), "1h 1.05m"); +/// +/// let printer = SpanPrinter::new() +/// .fractional(Some(FractionalUnit::Second)); +/// assert_eq!(printer.duration_to_string(&duration), "1h 1m 3s"); +/// ``` +#[derive(Clone, Copy, Debug)] +#[non_exhaustive] +pub enum FractionalUnit { + /// The smallest whole integer unit allowed is hours. + /// + /// **WARNING**: Since fractional units are limited to 9 decimal places, + /// using this setting could result in precision loss. + Hour, + /// The smallest whole integer unit allowed is minutes. + /// + /// **WARNING**: Since fractional units are limited to 9 decimal places, + /// using this setting could result in precision loss. + Minute, + /// The smallest whole integer unit allowed is seconds. + Second, + /// The smallest whole integer unit allowed is milliseconds. + Millisecond, + /// The smallest whole integer unit allowed is microseconds. + Microsecond, +} + +impl From for Unit { + fn from(u: FractionalUnit) -> Unit { + match u { + FractionalUnit::Hour => Unit::Hour, + FractionalUnit::Minute => Unit::Minute, + FractionalUnit::Second => Unit::Second, + FractionalUnit::Millisecond => Unit::Millisecond, + FractionalUnit::Microsecond => Unit::Microsecond, + } + } +} + +/// A printer for Jiff's "friendly" duration format. +/// +/// This printer provides a lot of different knobs for controlling how +/// durations are formatted. It supports formatting both [`SignedDuration`] +/// and [`Span`]. +/// +/// # Example: automatic use through `Display` +/// +/// The default configuration of this printer is used for "alternate" display +/// formatting for both [`SignedDuration`] and [`Span`]: +/// +/// ``` +/// use jiff::{SignedDuration, ToSpan}; +/// +/// let span = 1.year().months(2).hours(15).seconds(30).nanoseconds(1); +/// assert_eq!(format!("{span:#}"), "1y 2mo 15h 30s 1ns"); +/// +/// let sdur = SignedDuration::new(15 * 60 * 60 + 30, 1); +/// assert_eq!(format!("{sdur:#}"), "15h 30s 1ns"); +/// ``` +/// +/// # Example: variety of formatting configurations +/// +/// This example shows a few different ways of formatting the same `Span`: +/// +/// ``` +/// use jiff::{ +/// fmt::friendly::{Designator, Spacing, SpanPrinter}, +/// ToSpan, +/// }; +/// +/// let span = 1.year().months(2).hours(15).seconds(30).nanoseconds(1); +/// +/// let printer = SpanPrinter::new(); +/// assert_eq!( +/// printer.span_to_string(&span), +/// "1y 2mo 15h 30s 1ns", +/// ); +/// +/// let printer = SpanPrinter::new() +/// .designator(Designator::Short); +/// assert_eq!( +/// printer.span_to_string(&span), +/// "1yr 2mos 15hrs 30secs 1nsec", +/// ); +/// +/// let printer = SpanPrinter::new() +/// .spacing(Spacing::None) +/// .designator(Designator::Compact); +/// assert_eq!( +/// printer.span_to_string(&span), +/// "1y2mo15h30s1ns", +/// ); +/// +/// let printer = SpanPrinter::new() +/// .spacing(Spacing::BetweenUnitsAndDesignators) +/// .comma_after_designator(true) +/// .designator(Designator::Verbose); +/// assert_eq!( +/// printer.span_to_string(&span), +/// "1 year, 2 months, 15 hours, 30 seconds, 1 nanosecond", +/// ); +/// +/// let printer = SpanPrinter::new() +/// .hours_minutes_seconds(true) +/// .spacing(Spacing::BetweenUnitsAndDesignators) +/// .comma_after_designator(true) +/// .designator(Designator::Verbose); +/// assert_eq!( +/// printer.span_to_string(&span), +/// "1 year, 2 months, 15:00:30.000000001", +/// ); +/// ``` +/// +/// # Example: negative durations +/// +/// By default, a negative duration will be represented with an ` ago` suffix: +/// +/// ``` +/// use jiff::{fmt::friendly::SpanPrinter, ToSpan}; +/// +/// let span = -1.year().months(2).hours(15).seconds(30).nanoseconds(1); +/// +/// let printer = SpanPrinter::new(); +/// assert_eq!( +/// printer.span_to_string(&span), +/// "1y 2mo 15h 30s 1ns ago", +/// ); +/// ``` +/// +/// But one can also use a prefix `-` sign instead. Usually this works better +/// without any spacing and compact designators: +/// +/// ``` +/// use jiff::{fmt::friendly::{Designator, Spacing, SpanPrinter}, ToSpan}; +/// +/// let span = -1.year().months(2).hours(15).seconds(30).nanoseconds(1); +/// +/// let printer = SpanPrinter::new() +/// .spacing(Spacing::None) +/// .designator(Designator::Compact); +/// assert_eq!( +/// printer.span_to_string(&span), +/// "-1y2mo15h30s1ns", +/// ); +/// ``` +#[derive(Clone, Debug)] +pub struct SpanPrinter { + designators: &'static Designators, + spacing: Spacing, + direction: Direction, + fractional: Option, + comma_after_designator: bool, + hms: bool, + padding: Option, + precision: Option, + zero_unit: Unit, +} + +impl SpanPrinter { + /// Creates a new printer for the "friendly" duration format. + /// + /// The printer returned uses the default configuration. This is + /// identical to `SpanPrinter::default`, but it can be used in a `const` + /// context. + /// + /// # Example + /// + /// This example shows how to format a duration directly to a `Vec`. + /// + /// ``` + /// use jiff::{fmt::friendly::SpanPrinter, ToSpan}; + /// + /// static PRINTER: SpanPrinter = SpanPrinter::new(); + /// + /// let span = 1.year().months(2); + /// let mut buf = vec![]; + /// // Writing to a `Vec` never fails (aside from OOM). + /// PRINTER.print_span(&span, &mut buf).unwrap(); + /// assert_eq!(buf, b"1y 2mo"); + /// ``` + #[inline] + pub const fn new() -> SpanPrinter { + SpanPrinter { + designators: Designators::new(Designator::Compact), + spacing: Spacing::BetweenUnits, + direction: Direction::Auto, + fractional: None, + comma_after_designator: false, + hms: false, + padding: None, + precision: None, + zero_unit: Unit::Second, + } + } + + /// Configures the kind of unit designators to use. + /// + /// There are no specific advantages or disadvantages to the kind + /// of designator you pick other than aesthetic preference. Shorter + /// designators are also likely faster to parse and print. + /// + /// The default is [`Designator::Compact`], which uses things like `yr` + /// instead of `year` (verbose) or `y` (compact). + /// + /// # Example + /// + /// ``` + /// use jiff::{ + /// fmt::friendly::{Designator, SpanPrinter}, + /// ToSpan, + /// }; + /// + /// let span = 1.year().months(2); + /// + /// let printer = SpanPrinter::new(); + /// assert_eq!(printer.span_to_string(&span), "1y 2mo"); + /// + /// let printer = SpanPrinter::new().designator(Designator::Short); + /// assert_eq!(printer.span_to_string(&span), "1yr 2mos"); + /// + /// let printer = SpanPrinter::new().designator(Designator::Verbose); + /// assert_eq!(printer.span_to_string(&span), "1year 2months"); + /// ``` + #[inline] + pub const fn designator(self, designator: Designator) -> SpanPrinter { + SpanPrinter { designators: Designators::new(designator), ..self } + } + + /// Configures the spacing between the units and the designator labels. + /// + /// The default is [`Spacing::BetweenUnits`], which results in durations + /// like `1y 2mo`. `Spacing::None` would result in `1y2mo` and + /// `Spacing::BetweenUnitsAndDesignators` would result in `1 y 2 mo`. + /// + /// # Example + /// + /// ``` + /// use jiff::{ + /// fmt::friendly::{Designator, Spacing, SpanPrinter}, + /// ToSpan, + /// }; + /// + /// let span = 1.year().months(2); + /// + /// // The default tries to balance spacing with compact + /// // unit designators. + /// let printer = SpanPrinter::new(); + /// assert_eq!(printer.span_to_string(&span), "1y 2mo"); + /// + /// // But you can use slightly more descriptive + /// // designators without being too verbose. + /// let printer = SpanPrinter::new() + /// .designator(Designator::Short); + /// assert_eq!(printer.span_to_string(&span), "1yr 2mos"); + /// + /// // When spacing is removed, it usually looks nicer + /// // to use compact unit designators. + /// let printer = SpanPrinter::new() + /// .spacing(Spacing::None) + /// .designator(Designator::Compact); + /// assert_eq!(printer.span_to_string(&span), "1y2mo"); + /// + /// // Conversely, when using more spacing, it usually + /// // looks nicer to use verbose unit designators. + /// let printer = SpanPrinter::new() + /// .spacing(Spacing::BetweenUnitsAndDesignators) + /// .designator(Designator::Verbose); + /// assert_eq!(printer.span_to_string(&span), "1 year 2 months"); + /// ``` + /// + /// # Example: `Spacing::None` can still result in whitespace + /// + /// In the case that [`SpanPrinter::hours_minutes_seconds`] is enabled + /// and one is formatting a span with non-zero calendar units, then an + /// ASCII whitespace is inserted between the calendar and non-calendar + /// units even when `Spacing::None` is used: + /// + /// ``` + /// use jiff::{fmt::friendly::{Spacing, SpanPrinter}, ToSpan}; + /// + /// let span = 1.year().months(2).hours(15); + /// + /// let printer = SpanPrinter::new() + /// .spacing(Spacing::None) + /// .hours_minutes_seconds(true); + /// assert_eq!(printer.span_to_string(&span), "1y2mo 15:00:00"); + /// ``` + #[inline] + pub const fn spacing(self, spacing: Spacing) -> SpanPrinter { + SpanPrinter { spacing, ..self } + } + + /// Configures how and when the sign for the duration is written. + /// + /// The default is [`Direction::Auto`]. In most cases, this results in + /// writing the suffix ` ago` after printing the duration units when the + /// sign of the duration is negative. And when the sign is positive, there + /// is no suffix. However, this can vary based on other settings. For + /// example, when [`SpanPrinter::spacing`] is set to [`Spacing::None`], + /// then `Direction::Auto` is treated as if it were [`Direction::Sign`]. + /// + /// # Example + /// + /// ``` + /// use jiff::{fmt::friendly::{Direction, SpanPrinter}, SignedDuration}; + /// + /// let duration = SignedDuration::from_secs(-1); + /// + /// let printer = SpanPrinter::new(); + /// assert_eq!(printer.duration_to_string(&duration), "1s ago"); + /// + /// let printer = SpanPrinter::new().direction(Direction::Sign); + /// assert_eq!(printer.duration_to_string(&duration), "-1s"); + /// ``` + #[inline] + pub const fn direction(self, direction: Direction) -> SpanPrinter { + SpanPrinter { direction, ..self } + } + + /// Enable fractional formatting for the given unit. + /// + /// When [`SpanPrinter::hours_minutes_seconds`] is enabled, then this + /// setting is automatically set to [`FractionalUnit::Second`]. Otherwise, + /// it defaults to `None`, which means no fractions are ever written. + /// + /// # Example + /// + /// This example shows how to write the same duration with different + /// fractional settings: + /// + /// ``` + /// use jiff::{fmt::friendly::{FractionalUnit, SpanPrinter}, SignedDuration}; + /// + /// let duration = SignedDuration::from_secs(3663); + /// + /// let printer = SpanPrinter::new() + /// .fractional(Some(FractionalUnit::Hour)); + /// assert_eq!(printer.duration_to_string(&duration), "1.0175h"); + /// + /// let printer = SpanPrinter::new() + /// .fractional(Some(FractionalUnit::Minute)); + /// assert_eq!(printer.duration_to_string(&duration), "1h 1.05m"); + /// + /// let printer = SpanPrinter::new() + /// .fractional(Some(FractionalUnit::Second)); + /// assert_eq!(printer.duration_to_string(&duration), "1h 1m 3s"); + /// ``` + /// + /// # Example: precision loss + /// + /// Because the "friendly" format is limited to 9 decimal places, when + /// using `FractionalUnit::Hour` or `FractionalUnit::Minute`, it is + /// possible for precision loss to occur. + /// + /// ``` + /// use jiff::{fmt::friendly::{FractionalUnit, SpanPrinter}, SignedDuration}; + /// + /// // one nanosecond + /// let duration = SignedDuration::new(0, 1); + /// + /// let printer = SpanPrinter::new() + /// .fractional(Some(FractionalUnit::Hour)); + /// assert_eq!(printer.duration_to_string(&duration), "0h"); + /// + /// let printer = SpanPrinter::new() + /// .fractional(Some(FractionalUnit::Minute)); + /// assert_eq!(printer.duration_to_string(&duration), "0m"); + /// ``` + #[inline] + pub const fn fractional( + self, + unit: Option, + ) -> SpanPrinter { + SpanPrinter { fractional: unit, ..self } + } + + /// When enabled, commas are written after unit designators. + /// + /// This is disabled by default. + /// + /// # Example + /// + /// ``` + /// use jiff::{fmt::friendly::{Designator, Spacing, SpanPrinter}, ToSpan}; + /// + /// static PRINTER: SpanPrinter = SpanPrinter::new() + /// .designator(Designator::Verbose) + /// .spacing(Spacing::BetweenUnitsAndDesignators) + /// .comma_after_designator(true); + /// + /// let span = 5.years().months(3).milliseconds(123); + /// assert_eq!( + /// PRINTER.span_to_string(&span), + /// "5 years, 3 months, 123 milliseconds", + /// ); + /// ``` + #[inline] + pub const fn comma_after_designator(self, yes: bool) -> SpanPrinter { + SpanPrinter { comma_after_designator: yes, ..self } + } + + /// Formats the span or duration into a `HH:MM:SS[.fffffffff]` format. + /// + /// When formatting a `Span` with non-zero calendar units (units of days + /// or greater), then the calendar units are formatted as typical with + /// their corresponding designators. For example, `1d 01:00:00`. Note + /// that when formatting a `SignedDuration`, calendar units are never used. + /// + /// When this is enabled, many of the other options are either ignored or + /// fixed to a specific setting: + /// + /// * Since this format does not use any unit designators for units of + /// hours or smaller, the [`SpanPrinter::designator`] setting is ignored + /// for hours or smaller. It is still used when formatting a `Span` with + /// non-zero calendar units. + /// * [`SpanPrinter::spacing`] setting is ignored for units of hours or + /// smaller. + /// * The [`SpanPrinter::fractional`] setting is forcefully set to + /// [`FractionalUnit::Second`]. It cannot be changed. + /// * The [`SpanPrinter::comma_after_designator`] setting is ignored for + /// units of hours or smaller. + /// * When the padding is not specified, it defaults to `2` for hours, + /// minutes and seconds and `0` for any calendar units present. + /// * The precision setting is respected as documented. + /// + /// This format is useful in contexts for interfacing with existing systems + /// that require this style of format, or if the `HH:MM:SS` is just in + /// general preferred. + /// + /// # Loss of fidelity + /// + /// When using this format with a `Span`, sub-second units are formatted + /// as a fractional second. This means that `1000 milliseconds` and + /// `1 second` format to precisely the same string. This is similar to the + /// loss of fidelity when using [`fmt::temporal`](crate::fmt::temporal) + /// to format spans in the ISO 8601 duration format. + /// + /// # Example + /// + /// This shows how to format a `Span` in `HH:MM:SS` format: + /// + /// ``` + /// use jiff::{fmt::friendly::SpanPrinter, ToSpan}; + /// + /// static PRINTER: SpanPrinter = + /// SpanPrinter::new().hours_minutes_seconds(true); + /// + /// let span = 2.hours().minutes(59).seconds(15).milliseconds(123); + /// assert_eq!(PRINTER.span_to_string(&span), "02:59:15.123"); + /// assert_eq!(PRINTER.span_to_string(&-span), "-02:59:15.123"); + /// + /// // This shows what happens with calendar units. + /// let span = 15.days().hours(2).minutes(59).seconds(15).milliseconds(123); + /// assert_eq!(PRINTER.span_to_string(&span), "15d 02:59:15.123"); + /// // Notice that because calendar units are specified and the sign + /// // setting is set to "auto" by default, it has switched to a suffix. + /// assert_eq!(PRINTER.span_to_string(&-span), "15d 02:59:15.123 ago"); + /// ``` + /// + /// And this shows the same, but with a [`SignedDuration`]: + /// + /// ``` + /// use jiff::{fmt::friendly::SpanPrinter, SignedDuration}; + /// + /// static PRINTER: SpanPrinter = + /// SpanPrinter::new().hours_minutes_seconds(true); + /// + /// let duration = SignedDuration::new( + /// 2 * 60 * 60 + 59 * 60 + 15, + /// 123_000_000, + /// ); + /// assert_eq!(PRINTER.duration_to_string(&duration), "02:59:15.123"); + /// assert_eq!(PRINTER.duration_to_string(&-duration), "-02:59:15.123"); + /// ``` + /// + /// # Example: `Span` versus `SignedDuration` + /// + /// The main advantage of a `Span` is that, except for fractional + /// components, the unit values emitted correspond precisely to the values + /// in the `Span`. Where as for a `SignedDuration`, the units are always + /// computed from a single absolute duration in a way that is always + /// balanced: + /// + /// ``` + /// use jiff::{fmt::friendly::SpanPrinter, SignedDuration, ToSpan}; + /// + /// static PRINTER: SpanPrinter = + /// SpanPrinter::new().hours_minutes_seconds(true); + /// + /// let span = 120.minutes(); + /// assert_eq!(PRINTER.span_to_string(&span), "00:120:00"); + /// + /// let duration = SignedDuration::from_mins(120); + /// assert_eq!(PRINTER.duration_to_string(&duration), "02:00:00"); + /// ``` + /// + /// Of course, a balanced duration is sometimes what you want. But `Span` + /// affords the flexibility of controlling precisely what the unit values + /// are. + #[inline] + pub const fn hours_minutes_seconds(self, yes: bool) -> SpanPrinter { + SpanPrinter { hms: yes, ..self } + } + + /// The padding to use when writing unit values. + /// + /// If a unit value has fewer digits than specified here, it is padded to + /// the left with zeroes. (To control precision, i.e., padding to the right + /// when writing fractional values, use [`SpanPrinter::precision`].) + /// + /// By default, when writing in the hours-minutes-seconds format, a padding + /// of `2` is used for units of hours, minutes and seconds. Otherwise, a + /// padding of `0` is used. + /// + /// Padding is clamped to a maximum value of `20` (corresponding to the + /// number of digits in `u64::MAX`). + /// + /// # Example + /// + /// This shows some examples of configuring padding when writing in default + /// format with unit designators: + /// + /// ``` + /// use jiff::{fmt::friendly::SpanPrinter, ToSpan}; + /// + /// let printer = SpanPrinter::new(); + /// assert_eq!(printer.span_to_string(&1.hour()), "1h"); + /// let printer = SpanPrinter::new().padding(3); + /// assert_eq!(printer.span_to_string(&1.hour()), "001h"); + /// ``` + /// + /// And this shows some examples with the hours-minutes-seconds format. + /// Notice how padding is enabled by default. + /// + /// ``` + /// use jiff::{fmt::friendly::SpanPrinter, ToSpan}; + /// + /// let printer = SpanPrinter::new().hours_minutes_seconds(true); + /// assert_eq!(printer.span_to_string(&1.hour()), "01:00:00"); + /// let printer = SpanPrinter::new().hours_minutes_seconds(true).padding(0); + /// assert_eq!(printer.span_to_string(&1.hour()), "1:0:0"); + /// + /// // In this case, under the default configuration, the padding + /// // for calendar units is 0 but the padding for time units is 2. + /// let printer = SpanPrinter::new().hours_minutes_seconds(true); + /// assert_eq!(printer.span_to_string(&1.day().hours(1)), "1d 01:00:00"); + /// ``` + #[inline] + pub const fn padding(self, digits: u8) -> SpanPrinter { + SpanPrinter { padding: Some(digits), ..self } + } + + /// The precision to use when writing fractional unit values. + /// + /// This setting has no effect if fractional formatting isn't enabled. + /// Fractional formatting is only enabled when [`SpanPrinter::fractional`] + /// is set or if [`SpanPrinter::hours_minutes_seconds`] are enabled. + /// Neither are enabled by default. + /// + /// A precision of `Some(0)` implies that truncation of any fractional + /// component always occurs. + /// + /// The default value is `None`, which means the precision is automatically + /// determined from the value. If no fractional component is needed, then + /// none will be printed. + /// + /// Precision is capped to a maximum value of `9` (corresponding to the + /// maximum precision supported by Jiff). + /// + /// # Example + /// + /// ``` + /// use jiff::{fmt::friendly::{FractionalUnit, SpanPrinter}, ToSpan}; + /// + /// // No effect, because fractions aren't enabled. + /// let printer = SpanPrinter::new().precision(Some(2)); + /// assert_eq!(printer.span_to_string(&1.hour()), "1h"); + /// + /// // Precision setting takes effect! + /// let printer = SpanPrinter::new() + /// .precision(Some(2)) + /// .fractional(Some(FractionalUnit::Hour)); + /// assert_eq!(printer.span_to_string(&1.hour()), "1.00h"); + /// + /// // The HH:MM:SS format automatically enables fractional + /// // second values. + /// let printer = SpanPrinter::new() + /// // Truncate to millisecond precision. + /// .precision(Some(3)) + /// .hours_minutes_seconds(true); + /// let span = 1.second().milliseconds(1).microseconds(1).nanoseconds(1); + /// assert_eq!(printer.span_to_string(&span), "00:00:01.001"); + /// + /// // Same as above, but with the designator or "expanded" + /// // format. This requires explicitly enabling fractional + /// // units. + /// let printer = SpanPrinter::new() + /// // Truncate to millisecond precision. + /// .precision(Some(3)) + /// .fractional(Some(FractionalUnit::Second)); + /// let span = 1.second().milliseconds(1).microseconds(1).nanoseconds(1); + /// assert_eq!(printer.span_to_string(&span), "1.001s"); + /// ``` + #[inline] + pub const fn precision(self, precision: Option) -> SpanPrinter { + SpanPrinter { precision, ..self } + } + + /// Sets the unit to use when printing a duration that is zero. + /// + /// When [`SpanPrinter::fractional`] is set, then this setting is ignored + /// and the zero unit corresponds to the fractional unit specified. + /// + /// This defaults to [`Unit::Second`]. + /// + /// # Example + /// + /// ``` + /// use jiff::{fmt::friendly::{FractionalUnit, SpanPrinter}, ToSpan, Unit}; + /// + /// // The default just always uses seconds. + /// let printer = SpanPrinter::new(); + /// assert_eq!(printer.span_to_string(&0.years()), "0s"); + /// + /// // We can set our own unit. + /// let printer = SpanPrinter::new().zero_unit(Unit::Year); + /// assert_eq!(printer.span_to_string(&0.years()), "0y"); + /// + /// // But it's overridden if fractional units are set. + /// let printer = SpanPrinter::new() + /// .zero_unit(Unit::Year) + /// .fractional(Some(FractionalUnit::Minute)); + /// assert_eq!(printer.span_to_string(&0.years()), "0m"); + /// + /// // One use case for this option is if you're rounding + /// // spans and want the zero unit to reflect the smallest + /// // unit you're using. + /// let printer = SpanPrinter::new().zero_unit(Unit::Minute); + /// let span = 5.hours().minutes(30).seconds(59); + /// let rounded = span.round(Unit::Minute)?; + /// assert_eq!(printer.span_to_string(&rounded), "5h 31m"); + /// + /// let span = 5.seconds(); + /// let rounded = span.round(Unit::Minute)?; + /// assert_eq!(printer.span_to_string(&rounded), "0m"); + /// + /// # Ok::<(), Box>(()) + /// ``` + /// + /// The same applies for `SignedDuration`: + /// + /// ``` + /// use jiff::{fmt::friendly::SpanPrinter, SignedDuration, Unit}; + /// + /// // The default just always uses seconds. + /// let printer = SpanPrinter::new(); + /// assert_eq!(printer.duration_to_string(&SignedDuration::ZERO), "0s"); + /// + /// // We can set our own unit. + /// let printer = SpanPrinter::new().zero_unit(Unit::Minute); + /// assert_eq!(printer.duration_to_string(&SignedDuration::ZERO), "0m"); + /// ``` + #[inline] + pub const fn zero_unit(self, unit: Unit) -> SpanPrinter { + SpanPrinter { zero_unit: unit, ..self } + } + + /// Format a `Span` into a string using the "friendly" format. + /// + /// This is a convenience routine for [`SpanPrinter::print_span`] with a + /// `String`. + /// + /// # Example + /// + /// ``` + /// use jiff::{fmt::friendly::SpanPrinter, ToSpan}; + /// + /// static PRINTER: SpanPrinter = SpanPrinter::new(); + /// + /// let span = 3.years().months(5); + /// assert_eq!(PRINTER.span_to_string(&span), "3y 5mo"); + /// ``` + #[cfg(any(test, feature = "alloc"))] + pub fn span_to_string(&self, span: &Span) -> alloc::string::String { + let mut buf = alloc::string::String::with_capacity(4); + // OK because writing to `String` never fails. + self.print_span(span, &mut buf).unwrap(); + buf + } + + /// Format a `SignedDuration` into a string using the "friendly" format. + /// + /// This balances the units of the duration up to at most hours + /// automatically. + /// + /// This is a convenience routine for [`SpanPrinter::print_duration`] with + /// a `String`. + /// + /// # Example + /// + /// ``` + /// use jiff::{fmt::friendly::{FractionalUnit, SpanPrinter}, SignedDuration}; + /// + /// static PRINTER: SpanPrinter = SpanPrinter::new(); + /// + /// let dur = SignedDuration::new(86_525, 123_000_789); + /// assert_eq!( + /// PRINTER.duration_to_string(&dur), + /// "24h 2m 5s 123ms 789ns", + /// ); + /// assert_eq!( + /// PRINTER.duration_to_string(&-dur), + /// "24h 2m 5s 123ms 789ns ago", + /// ); + /// + /// // Or, if you prefer fractional seconds: + /// static PRINTER_FRACTIONAL: SpanPrinter = SpanPrinter::new() + /// .fractional(Some(FractionalUnit::Second)); + /// assert_eq!( + /// PRINTER_FRACTIONAL.duration_to_string(&-dur), + /// "24h 2m 5.123000789s ago", + /// ); + /// ``` + #[cfg(any(test, feature = "alloc"))] + pub fn duration_to_string( + &self, + duration: &SignedDuration, + ) -> alloc::string::String { + let mut buf = alloc::string::String::with_capacity(4); + // OK because writing to `String` never fails. + self.print_duration(duration, &mut buf).unwrap(); + buf + } + + /// Format a `std::time::Duration` into a string using the "friendly" + /// format. + /// + /// This balances the units of the duration up to at most hours + /// automatically. + /// + /// This is a convenience routine for + /// [`SpanPrinter::print_unsigned_duration`] with a `String`. + /// + /// # Example + /// + /// ``` + /// use std::time::Duration; + /// + /// use jiff::fmt::friendly::{FractionalUnit, SpanPrinter}; + /// + /// static PRINTER: SpanPrinter = SpanPrinter::new(); + /// + /// let dur = Duration::new(86_525, 123_000_789); + /// assert_eq!( + /// PRINTER.unsigned_duration_to_string(&dur), + /// "24h 2m 5s 123ms 789ns", + /// ); + /// + /// // Or, if you prefer fractional seconds: + /// static PRINTER_FRACTIONAL: SpanPrinter = SpanPrinter::new() + /// .fractional(Some(FractionalUnit::Second)); + /// assert_eq!( + /// PRINTER_FRACTIONAL.unsigned_duration_to_string(&dur), + /// "24h 2m 5.123000789s", + /// ); + /// ``` + #[cfg(any(test, feature = "alloc"))] + pub fn unsigned_duration_to_string( + &self, + duration: &core::time::Duration, + ) -> alloc::string::String { + let mut buf = alloc::string::String::with_capacity(4); + // OK because writing to `String` never fails. + self.print_unsigned_duration(duration, &mut buf).unwrap(); + buf + } + + /// Print a `Span` to the given writer using the "friendly" format. + /// + /// # Errors + /// + /// This only returns an error when writing to the given [`Write`] + /// implementation would fail. Some such implementations, like for `String` + /// and `Vec`, never fail (unless memory allocation fails). In such + /// cases, it would be appropriate to call `unwrap()` on the result. + /// + /// # Example + /// + /// ``` + /// use jiff::{fmt::friendly::SpanPrinter, ToSpan}; + /// + /// static PRINTER: SpanPrinter = SpanPrinter::new(); + /// + /// let span = 3.years().months(5); + /// + /// let mut buf = String::new(); + /// // Printing to a `String` can never fail. + /// PRINTER.print_span(&span, &mut buf).unwrap(); + /// assert_eq!(buf, "3y 5mo"); + /// ``` + pub fn print_span( + &self, + span: &Span, + mut wtr: W, + ) -> Result<(), Error> { + let mut buf = ArrayBuffer::::default(); + let mut bbuf = buf.as_borrowed(); + if self.hms { + self.print_span_hms(span, &mut bbuf); + } else { + self.print_span_designators(span, &mut bbuf); + } + wtr.write_str(bbuf.filled()) + } + + /// Print a `SignedDuration` to the given writer using the "friendly" + /// format. + /// + /// This balances the units of the duration up to at most hours + /// automatically. + /// + /// # Errors + /// + /// This only returns an error when writing to the given [`Write`] + /// implementation would fail. Some such implementations, like for `String` + /// and `Vec`, never fail (unless memory allocation fails). In such + /// cases, it would be appropriate to call `unwrap()` on the result. + /// + /// # Example + /// + /// ``` + /// use jiff::{fmt::friendly::SpanPrinter, SignedDuration}; + /// + /// static PRINTER: SpanPrinter = SpanPrinter::new(); + /// + /// let dur = SignedDuration::new(86_525, 123_000_789); + /// + /// let mut buf = String::new(); + /// // Printing to a `String` can never fail. + /// PRINTER.print_duration(&dur, &mut buf).unwrap(); + /// assert_eq!(buf, "24h 2m 5s 123ms 789ns"); + /// + /// // Negative durations are supported. + /// buf.clear(); + /// PRINTER.print_duration(&-dur, &mut buf).unwrap(); + /// assert_eq!(buf, "24h 2m 5s 123ms 789ns ago"); + /// ``` + pub fn print_duration( + &self, + duration: &SignedDuration, + mut wtr: W, + ) -> Result<(), Error> { + let mut buf = ArrayBuffer::::default(); + let mut bbuf = buf.as_borrowed(); + if self.hms { + self.print_signed_duration_hms(duration, &mut bbuf); + } else { + self.print_signed_duration_designators(duration, &mut bbuf); + } + wtr.write_str(bbuf.filled()) + } + + /// Print a `std::time::Duration` to the given writer using the "friendly" + /// format. + /// + /// This balances the units of the duration up to at most hours + /// automatically. + /// + /// # Errors + /// + /// This only returns an error when writing to the given [`Write`] + /// implementation would fail. Some such implementations, like for `String` + /// and `Vec`, never fail (unless memory allocation fails). In such + /// cases, it would be appropriate to call `unwrap()` on the result. + /// + /// # Example + /// + /// ``` + /// use std::time::Duration; + /// + /// use jiff::fmt::friendly::SpanPrinter; + /// + /// static PRINTER: SpanPrinter = SpanPrinter::new(); + /// + /// let dur = Duration::new(86_525, 123_000_789); + /// + /// let mut buf = String::new(); + /// // Printing to a `String` can never fail. + /// PRINTER.print_unsigned_duration(&dur, &mut buf).unwrap(); + /// assert_eq!(buf, "24h 2m 5s 123ms 789ns"); + /// ``` + pub fn print_unsigned_duration( + &self, + duration: &core::time::Duration, + mut wtr: W, + ) -> Result<(), Error> { + let mut buf = ArrayBuffer::::default(); + let mut bbuf = buf.as_borrowed(); + if self.hms { + self.print_unsigned_duration_hms(duration, &mut bbuf); + } else { + self.print_unsigned_duration_designators(duration, &mut bbuf); + } + wtr.write_str(bbuf.filled()) + } + + fn print_span_designators( + &self, + span: &Span, + bbuf: &mut BorrowedBuffer<'_>, + ) { + let mut wtr = DesignatorWriter::new(self, bbuf, false, span.signum()); + wtr.maybe_write_prefix_sign(); + match self.fractional { + None => { + self.print_span_designators_non_fraction(span, &mut wtr); + } + Some(unit) => { + self.print_span_designators_fractional(span, unit, &mut wtr); + } + } + wtr.maybe_write_zero(); + wtr.maybe_write_suffix_sign(); + } + + fn print_span_designators_non_fraction<'p, 'w, 'd>( + &self, + span: &Span, + wtr: &mut DesignatorWriter<'p, 'w, 'd>, + ) { + let units = span.units(); + + if units.contains(Unit::Year) { + wtr.write(Unit::Year, span.get_years_unsigned().into()); + } + if units.contains(Unit::Month) { + wtr.write(Unit::Month, span.get_months_unsigned().into()); + } + if units.contains(Unit::Week) { + wtr.write(Unit::Week, span.get_weeks_unsigned().into()); + } + if units.contains(Unit::Day) { + wtr.write(Unit::Day, span.get_days_unsigned().into()); + } + if units.contains(Unit::Hour) { + wtr.write(Unit::Hour, span.get_hours_unsigned().into()); + } + if units.contains(Unit::Minute) { + wtr.write(Unit::Minute, span.get_minutes_unsigned()); + } + if units.contains(Unit::Second) { + wtr.write(Unit::Second, span.get_seconds_unsigned()); + } + if units.contains(Unit::Millisecond) { + wtr.write(Unit::Millisecond, span.get_milliseconds_unsigned()); + } + if units.contains(Unit::Microsecond) { + wtr.write(Unit::Microsecond, span.get_microseconds_unsigned()); + } + if units.contains(Unit::Nanosecond) { + wtr.write(Unit::Nanosecond, span.get_nanoseconds_unsigned()); + } + } + + fn print_span_calendar_designators_non_fraction<'p, 'w, 'd>( + &self, + span: &Span, + wtr: &mut DesignatorWriter<'p, 'w, 'd>, + ) { + let units = span.units(); + + if units.contains(Unit::Year) { + wtr.write(Unit::Year, span.get_years_unsigned().into()); + } + if units.contains(Unit::Month) { + wtr.write(Unit::Month, span.get_months_unsigned().into()); + } + if units.contains(Unit::Week) { + wtr.write(Unit::Week, span.get_weeks_unsigned().into()); + } + if units.contains(Unit::Day) { + wtr.write(Unit::Day, span.get_days_unsigned().into()); + } + } + + #[inline(never)] + fn print_span_designators_fractional<'p, 'w, 'd>( + &self, + span: &Span, + unit: FractionalUnit, + wtr: &mut DesignatorWriter<'p, 'w, 'd>, + ) { + // OK because the biggest FractionalUnit is Hour, and there is always + // a Unit bigger than hour. + let split_at = Unit::from(unit).next().unwrap(); + let non_fractional = span.without_lower(split_at); + let fractional = span.only_lower(split_at); + self.print_span_designators_non_fraction(&non_fractional, wtr); + wtr.write_fractional_duration( + unit, + &fractional.to_invariant_duration().unsigned_abs(), + ); + } + + fn print_span_hms(&self, span: &Span, bbuf: &mut BorrowedBuffer<'_>) { + let has_cal = !span.units().only_calendar().is_empty(); + let mut wtr = + DesignatorWriter::new(self, bbuf, has_cal, span.signum()); + let span = span.abs(); + + wtr.maybe_write_prefix_sign(); + if has_cal { + self.print_span_calendar_designators_non_fraction(&span, &mut wtr); + wtr.finish_preceding(); + // When spacing is disabled, then `finish_preceding` won't write + // any spaces. But this would result in, e.g., `1yr15:00:00`, which + // is just totally wrong. So detect that case here and insert a + // space forcefully. + if matches!(self.spacing, Spacing::None) { + wtr.bbuf.write_ascii_char(b' '); + } + } + + let padding = self.padding.unwrap_or(2); + wtr.bbuf.write_int_pad(span.get_hours_unsigned(), b'0', padding); + wtr.bbuf.write_ascii_char(b':'); + wtr.bbuf.write_int_pad(span.get_minutes_unsigned(), b'0', padding); + wtr.bbuf.write_ascii_char(b':'); + // You'd think we could do better here from a code size + // perspective. But when I tried to inline the logic to + // get a `SignedDuration` from just sub-minute units, + // code size actually increased. ¯\_(ツ)_/¯ + let fp = FractionalPrinter::from_span_seconds( + &span.only_lower(Unit::Minute), + padding, + self.precision, + ); + fp.print(wtr.bbuf); + wtr.maybe_write_suffix_sign(); + } + + fn print_signed_duration_designators( + &self, + dur: &SignedDuration, + bbuf: &mut BorrowedBuffer<'_>, + ) { + let mut wtr = DesignatorWriter::new(self, bbuf, false, dur.signum()); + wtr.maybe_write_prefix_sign(); + self.print_duration_designators(&dur.unsigned_abs(), &mut wtr); + wtr.maybe_write_zero(); + wtr.maybe_write_suffix_sign(); + } + + fn print_unsigned_duration_designators( + &self, + dur: &core::time::Duration, + bbuf: &mut BorrowedBuffer<'_>, + ) { + let mut wtr = DesignatorWriter::new(self, bbuf, false, 1); + wtr.maybe_write_prefix_sign(); + self.print_duration_designators(dur, &mut wtr); + wtr.maybe_write_zero(); + } + + fn print_duration_designators( + &self, + dur: &core::time::Duration, + wtr: &mut DesignatorWriter<'_, '_, '_>, + ) { + match self.fractional { + None => { + let mut secs = dur.as_secs(); + wtr.write(Unit::Hour, secs / SECS_PER_HOUR); + secs %= MINS_PER_HOUR * SECS_PER_MIN; + 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).into()); + nanos %= NANOS_PER_MILLI; + 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); + } + Some(FractionalUnit::Minute) => { + let mut secs = dur.as_secs(); + wtr.write(Unit::Hour, secs / SECS_PER_HOUR); + secs %= MINS_PER_HOUR * SECS_PER_MIN; + + let leftovers = + core::time::Duration::new(secs, dur.subsec_nanos()); + wtr.write_fractional_duration( + FractionalUnit::Minute, + &leftovers, + ); + } + Some(FractionalUnit::Second) => { + let mut secs = dur.as_secs(); + wtr.write(Unit::Hour, secs / SECS_PER_HOUR); + secs %= MINS_PER_HOUR * SECS_PER_MIN; + wtr.write(Unit::Minute, secs / SECS_PER_MIN); + secs %= SECS_PER_MIN; + + let leftovers = + core::time::Duration::new(secs, dur.subsec_nanos()); + wtr.write_fractional_duration( + FractionalUnit::Second, + &leftovers, + ); + } + Some(FractionalUnit::Millisecond) => { + let mut secs = dur.as_secs(); + wtr.write(Unit::Hour, secs / SECS_PER_HOUR); + secs %= MINS_PER_HOUR * SECS_PER_MIN; + wtr.write(Unit::Minute, secs / SECS_PER_MIN); + wtr.write(Unit::Second, secs % SECS_PER_MIN); + + let leftovers = + core::time::Duration::new(0, dur.subsec_nanos()); + wtr.write_fractional_duration( + FractionalUnit::Millisecond, + &leftovers, + ); + } + Some(FractionalUnit::Microsecond) => { + let mut secs = dur.as_secs(); + wtr.write(Unit::Hour, secs / SECS_PER_HOUR); + secs %= MINS_PER_HOUR * SECS_PER_MIN; + 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).into()); + nanos %= NANOS_PER_MILLI; + + let leftovers = core::time::Duration::new(0, nanos); + wtr.write_fractional_duration( + FractionalUnit::Microsecond, + &leftovers, + ); + } + } + } + + fn print_signed_duration_hms( + &self, + dur: &SignedDuration, + bbuf: &mut BorrowedBuffer<'_>, + ) { + if dur.is_negative() { + if !matches!(self.direction, Direction::Suffix) { + bbuf.write_ascii_char(b'-'); + } + } else if let Direction::ForceSign = self.direction { + bbuf.write_ascii_char(b'+'); + } + self.print_duration_hms(&dur.unsigned_abs(), bbuf); + if dur.is_negative() { + if matches!(self.direction, Direction::Suffix) { + bbuf.write_str(" ago"); + } + } + } + + fn print_unsigned_duration_hms( + &self, + dur: &core::time::Duration, + bbuf: &mut BorrowedBuffer<'_>, + ) { + if let Direction::ForceSign = self.direction { + bbuf.write_ascii_char(b'+'); + } + self.print_duration_hms(dur, bbuf); + } + + fn print_duration_hms( + &self, + udur: &core::time::Duration, + bbuf: &mut BorrowedBuffer<'_>, + ) { + // N.B. It should be technically correct to convert a `SignedDuration` + // (or `core::time::Duration`) to `Span` (since this process balances) + // and then format the `Span` as-is. But this doesn't work because the + // range of a `SignedDuration` (and `core::time::Duration`) is much + // bigger. + + let padding = self.padding.unwrap_or(2); + + let mut secs = udur.as_secs(); + // OK because guaranteed to be bigger than i64::MIN. + let hours = secs / (MINS_PER_HOUR * SECS_PER_MIN); + secs %= MINS_PER_HOUR * SECS_PER_MIN; + // OK because guaranteed to be bigger than i64::MIN. + let minutes = secs / SECS_PER_MIN; + // OK because guaranteed to be bigger than i64::MIN. + secs = secs % SECS_PER_MIN; + + bbuf.write_int_pad(hours, b'0', padding); + bbuf.write_ascii_char(b':'); + bbuf.write_int_pad(minutes, b'0', padding); + bbuf.write_ascii_char(b':'); + let fp = FractionalPrinter::from_duration_seconds( + // OK because -999_999_999 <= nanos <= 999_999_999 and secs < 60. + &core::time::Duration::new(secs, udur.subsec_nanos()), + padding, + self.precision, + ); + fp.print(bbuf); + } +} + +impl Default for SpanPrinter { + fn default() -> SpanPrinter { + SpanPrinter::new() + } +} + +/// A type that represents the designator choice. +/// +/// Basically, whether we want verbose, short or compact designators. This in +/// turn permits lookups based on `Unit`, which makes writing generic code for +/// writing designators a bit nicer and still fast. +#[derive(Clone, Debug)] +struct Designators { + singular: [&'static str; 10], + plural: [&'static str; 10], +} + +impl Designators { + const VERBOSE_SINGULAR: [&'static str; 10] = [ + "nanosecond", + "microsecond", + "millisecond", + "second", + "minute", + "hour", + "day", + "week", + "month", + "year", + ]; + const VERBOSE_PLURAL: [&'static str; 10] = [ + "nanoseconds", + "microseconds", + "milliseconds", + "seconds", + "minutes", + "hours", + "days", + "weeks", + "months", + "years", + ]; + + const SHORT_SINGULAR: [&'static str; 10] = + ["nsec", "µsec", "msec", "sec", "min", "hr", "day", "wk", "mo", "yr"]; + const SHORT_PLURAL: [&'static str; 10] = [ + "nsecs", "µsecs", "msecs", "secs", "mins", "hrs", "days", "wks", + "mos", "yrs", + ]; + + const COMPACT: [&'static str; 10] = + ["ns", "µs", "ms", "s", "m", "h", "d", "w", "mo", "y"]; + + const HUMAN_TIME_SINGULAR: [&'static str; 10] = + ["ns", "us", "ms", "s", "m", "h", "d", "w", "month", "y"]; + const HUMAN_TIME_PLURAL: [&'static str; 10] = + ["ns", "us", "ms", "s", "m", "h", "d", "w", "months", "y"]; + + const fn new(config: Designator) -> &'static Designators { + match config { + Designator::Verbose => &Designators { + singular: Designators::VERBOSE_SINGULAR, + plural: Designators::VERBOSE_PLURAL, + }, + Designator::Short => &Designators { + singular: Designators::SHORT_SINGULAR, + plural: Designators::SHORT_PLURAL, + }, + Designator::Compact => &Designators { + singular: Designators::COMPACT, + plural: Designators::COMPACT, + }, + Designator::HumanTime => &Designators { + singular: Designators::HUMAN_TIME_SINGULAR, + plural: Designators::HUMAN_TIME_PLURAL, + }, + } + } + + fn designator(&self, unit: impl Into, plural: bool) -> &'static str { + let unit = unit.into(); + let index = unit as usize; + if plural { + self.plural[index] + } else { + self.singular[index] + } + } +} + +/// An abstraction for writing the "designator" variant of the friendly format. +/// +/// This takes care of computing some initial state and keeping track of some +/// mutable state that influences printing. For example, whether to write a +/// delimiter or not (one should only come after a unit that has been written). +#[derive(Debug)] +struct DesignatorWriter<'p, 'w, 'd> { + printer: &'p SpanPrinter, + bbuf: &'w mut BorrowedBuffer<'d>, + sign: Option, + padding: u8, + precision: Option, + written_non_zero_unit: bool, +} + +impl<'p, 'w, 'd> DesignatorWriter<'p, 'w, 'd> { + fn new( + printer: &'p SpanPrinter, + bbuf: &'w mut BorrowedBuffer<'d>, + has_calendar: bool, + signum: i8, + ) -> DesignatorWriter<'p, 'w, 'd> { + let sign = printer.direction.sign(printer, has_calendar, signum); + DesignatorWriter { + printer, + bbuf, + sign, + padding: printer.padding.unwrap_or(0), + precision: printer.precision, + written_non_zero_unit: false, + } + } + + fn maybe_write_prefix_sign(&mut self) { + if let Some(DirectionSign::Prefix(sign)) = self.sign { + self.bbuf.write_ascii_char(sign); + } + } + + fn maybe_write_suffix_sign(&mut self) { + if let Some(DirectionSign::Suffix) = self.sign { + self.bbuf.write_str(" ago"); + } + } + + fn maybe_write_zero(&mut self) { + #[cold] + #[inline(never)] + fn imp(wtr: &mut DesignatorWriter<'_, '_, '_>) { + wtr.bbuf.write_int_pad(0u64, b'0', wtr.padding); + if let Some(byte) = + wtr.printer.spacing.between_units_and_designators() + { + wtr.bbuf.write_ascii_char(byte); + } + // If a fractional unit is set, then we should use that unit + // specifically to express "zero." + let unit = wtr + .printer + .fractional + .map(Unit::from) + .unwrap_or(wtr.printer.zero_unit); + wtr.bbuf.write_str(wtr.printer.designators.designator(unit, true)); + } + + if !self.written_non_zero_unit { + imp(self); + } + } + + #[inline(never)] + fn write(&mut self, unit: Unit, value: u64) { + if value == 0 { + return; + } + self.finish_preceding(); + self.written_non_zero_unit = true; + self.bbuf.write_int_pad0(value, self.padding); + if let Some(byte) = + self.printer.spacing.between_units_and_designators() + { + self.bbuf.write_ascii_char(byte); + } + self.bbuf + .write_str(self.printer.designators.designator(unit, value != 1)); + } + + fn write_fractional_duration( + &mut self, + unit: FractionalUnit, + duration: &core::time::Duration, + ) { + let fp = FractionalPrinter::from_duration( + duration, + unit, + self.padding, + self.precision, + ); + if !fp.must_write_digits() { + return; + } + self.finish_preceding(); + self.written_non_zero_unit = true; + fp.print(&mut *self.bbuf); + if let Some(byte) = + self.printer.spacing.between_units_and_designators() + { + self.bbuf.write_ascii_char(byte); + } + self.bbuf.write_str( + self.printer.designators.designator(unit, fp.is_plural()), + ); + } + + fn finish_preceding(&mut self) { + if self.written_non_zero_unit { + if self.printer.comma_after_designator { + self.bbuf.write_ascii_char(b','); + } + if let Some(byte) = self.printer.spacing.between_units() { + self.bbuf.write_ascii_char(byte); + } + } + } +} + +/// A printer for a fraction with an integer and fraction component. +/// +/// This also includes the formatter for the integer component and the +/// formatter for the fractional component. +struct FractionalPrinter { + integer: u64, + fraction: u32, + padding: u8, + precision: Option, +} + +impl FractionalPrinter { + /// Build a fractional printer for the `Span` given. This includes the `.`. + /// + /// Callers must ensure that all units greater than `FractionalUnit` are + /// zero in the span given. + /// + /// Note that the printer returned only prints a fractional component + /// if necessary. For example, if the fractional component is zero and + /// precision is `None`, or if `precision` is `Some(0)`, then no fractional + /// component will be emitted. + fn from_span_seconds( + span: &Span, + padding: u8, + precision: Option, + ) -> FractionalPrinter { + FractionalPrinter::from_duration_seconds( + &span.to_invariant_duration().unsigned_abs(), + padding, + precision, + ) + } + + /// Like `from_span_seconds`, but for `SignedDuration`. + fn from_duration_seconds( + dur: &core::time::Duration, + padding: u8, + precision: Option, + ) -> FractionalPrinter { + let integer = dur.as_secs(); + let fraction = u32::from(dur.subsec_nanos()); + FractionalPrinter { integer, fraction, padding, precision } + } + + /// Like `from_duration_seconds`, but for any fractional unit. + fn from_duration( + dur: &core::time::Duration, + unit: FractionalUnit, + padding: u8, + precision: Option, + ) -> FractionalPrinter { + match unit { + FractionalUnit::Hour => { + let integer = dur.as_secs() / SECS_PER_HOUR; + let mut fraction = dur.as_nanos() % NANOS_PER_HOUR; + // Drop precision since we're only allowed 9 decimal places. + fraction /= u128::from(SECS_PER_HOUR); + // OK because NANOS_PER_HOUR / SECS_PER_HOUR fits in a u32. + let fraction = u32::try_from(fraction).unwrap(); + FractionalPrinter { integer, fraction, padding, precision } + } + FractionalUnit::Minute => { + let integer = dur.as_secs() / SECS_PER_MIN; + let mut fraction = dur.as_nanos() % NANOS_PER_MIN; + // Drop precision since we're only allowed 9 decimal places. + fraction /= u128::from(SECS_PER_MIN); + // OK because NANOS_PER_MIN fits in an u32. + let fraction = u32::try_from(fraction).unwrap(); + FractionalPrinter { integer, fraction, padding, precision } + } + FractionalUnit::Second => { + let integer = dur.as_secs(); + let fraction = u32::from(dur.subsec_nanos()); + FractionalPrinter { integer, fraction, padding, precision } + } + FractionalUnit::Millisecond => { + // Unwrap is OK, but this is subtle. For printing a + // SignedDuration, as_millis() can never return anything + // bigger than 1 second, because the duration given is reduced + // in a balanced fashion before hitting this routine. But + // for printing a Span, it can, since spans can be totally + // unbalanced. But Spans have limits on their units such that + // each will fit into an i64. So this is also okay in that case + // too. + let integer = u64::try_from(dur.as_millis()).unwrap(); + let fraction = + u32::from((dur.subsec_nanos() % NANOS_PER_MILLI) * 1_000); + FractionalPrinter { integer, fraction, padding, precision } + } + FractionalUnit::Microsecond => { + // Unwrap is OK, but this is subtle. For printing a + // SignedDuration, as_millis() can never return anything + // bigger than 1 second, because the duration given is reduced + // in a balanced fashion before hitting this routine. But + // for printing a Span, it can, since spans can be totally + // unbalanced. But Spans have limits on their units such that + // each will fit into an i64. So this is also okay in that case + // too. + let integer = u64::try_from(dur.as_micros()).unwrap(); + let fraction = u32::from( + (dur.subsec_nanos() % NANOS_PER_MICRO) * 1_000_000, + ); + FractionalPrinter { integer, fraction, padding, precision } + } + } + } + + /// Returns true if both the integer and fractional component are zero. + fn is_zero(&self) -> bool { + self.integer == 0 && self.fraction == 0 + } + + /// Returns true if this integer/fraction should be considered plural + /// when choosing what designator to use. + fn is_plural(&self) -> bool { + self.integer != 1 + || (self.fraction != 0 && !self.has_zero_fixed_precision()) + } + + /// Returns true if and only if this printer must write some kind of number + /// when `print` is called. + /// + /// The only case where this returns `false` is when both the integer and + /// fractional component are zero *and* the precision is fixed to a number + /// greater than zero. + fn must_write_digits(&self) -> bool { + !self.is_zero() || self.has_non_zero_fixed_precision() + } + + /// Returns true if and only if at least one digit will be written for the + /// given value. + /// + /// This is useful for callers that need to know whether to write + /// a decimal separator, e.g., `.`, before the digits. + fn will_write_digits(&self) -> bool { + self.precision.map_or_else(|| self.fraction != 0, |p| p > 0) + } + + /// Returns true if and only if this formatter has an explicit non-zero + /// precision setting. + /// + /// This is useful for determining whether something like `0.000` needs to + /// be written in the case of a `precision=Some(3)` setting and a zero + /// value. + fn has_non_zero_fixed_precision(&self) -> bool { + self.precision.map_or(false, |p| p > 0) + } + + /// Returns true if and only if this formatter has fixed zero precision. + /// That is, no matter what is given as input, a fraction is never written. + fn has_zero_fixed_precision(&self) -> bool { + self.precision.map_or(false, |p| p == 0) + } + + /// Prints the integer and optional fractional component. + /// + /// This will always print the integer, even if it's zero. Therefore, if + /// the caller wants to omit printing zero, the caller should do their own + /// conditional logic. + fn print(&self, bbuf: &mut BorrowedBuffer<'_>) { + bbuf.write_int_pad(self.integer, b'0', self.padding); + if self.will_write_digits() { + bbuf.write_ascii_char(b'.'); + bbuf.write_fraction(self.precision, self.fraction); + } + } +} + +#[cfg(feature = "alloc")] +#[cfg(test)] +mod tests { + use crate::ToSpan; + + use super::*; + + #[test] + fn print_span_designator_default() { + let printer = || SpanPrinter::new(); + let p = |span| printer().span_to_string(&span); + + insta::assert_snapshot!(p(1.second()), @"1s"); + insta::assert_snapshot!(p(2.seconds()), @"2s"); + insta::assert_snapshot!(p(10.seconds()), @"10s"); + insta::assert_snapshot!(p(1.minute().seconds(40)), @"1m 40s"); + + insta::assert_snapshot!(p(1.minute()), @"1m"); + insta::assert_snapshot!(p(2.minutes()), @"2m"); + insta::assert_snapshot!(p(10.minutes()), @"10m"); + insta::assert_snapshot!(p(1.hour().minutes(40)), @"1h 40m"); + + insta::assert_snapshot!(p(1.hour()), @"1h"); + insta::assert_snapshot!(p(2.hours()), @"2h"); + insta::assert_snapshot!(p(10.hours()), @"10h"); + insta::assert_snapshot!(p(100.hours()), @"100h"); + + insta::assert_snapshot!( + p(1.hour().minutes(1).seconds(1)), + @"1h 1m 1s", + ); + insta::assert_snapshot!( + p(2.hours().minutes(2).seconds(2)), + @"2h 2m 2s", + ); + insta::assert_snapshot!( + p(10.hours().minutes(10).seconds(10)), + @"10h 10m 10s", + ); + insta::assert_snapshot!( + p(100.hours().minutes(100).seconds(100)), + @"100h 100m 100s", + ); + + insta::assert_snapshot!(p(-1.hour()), @"1h ago"); + insta::assert_snapshot!(p(-1.hour().seconds(30)), @"1h 30s ago"); + + insta::assert_snapshot!( + p(1.second().milliseconds(2000)), + @"1s 2000ms", + ); + } + + #[test] + fn print_span_designator_verbose() { + let printer = || SpanPrinter::new().designator(Designator::Verbose); + let p = |span| printer().span_to_string(&span); + + insta::assert_snapshot!(p(1.second()), @"1second"); + insta::assert_snapshot!(p(2.seconds()), @"2seconds"); + insta::assert_snapshot!(p(10.seconds()), @"10seconds"); + insta::assert_snapshot!(p(1.minute().seconds(40)), @"1minute 40seconds"); + + insta::assert_snapshot!(p(1.minute()), @"1minute"); + insta::assert_snapshot!(p(2.minutes()), @"2minutes"); + insta::assert_snapshot!(p(10.minutes()), @"10minutes"); + insta::assert_snapshot!(p(1.hour().minutes(40)), @"1hour 40minutes"); + + insta::assert_snapshot!(p(1.hour()), @"1hour"); + insta::assert_snapshot!(p(2.hours()), @"2hours"); + insta::assert_snapshot!(p(10.hours()), @"10hours"); + insta::assert_snapshot!(p(100.hours()), @"100hours"); + + insta::assert_snapshot!( + p(1.hour().minutes(1).seconds(1)), + @"1hour 1minute 1second", + ); + insta::assert_snapshot!( + p(2.hours().minutes(2).seconds(2)), + @"2hours 2minutes 2seconds", + ); + insta::assert_snapshot!( + p(10.hours().minutes(10).seconds(10)), + @"10hours 10minutes 10seconds", + ); + insta::assert_snapshot!( + p(100.hours().minutes(100).seconds(100)), + @"100hours 100minutes 100seconds", + ); + + insta::assert_snapshot!(p(-1.hour()), @"1hour ago"); + insta::assert_snapshot!(p(-1.hour().seconds(30)), @"1hour 30seconds ago"); + } + + #[test] + fn print_span_designator_short() { + let printer = || SpanPrinter::new().designator(Designator::Short); + let p = |span| printer().span_to_string(&span); + + insta::assert_snapshot!(p(1.second()), @"1sec"); + insta::assert_snapshot!(p(2.seconds()), @"2secs"); + insta::assert_snapshot!(p(10.seconds()), @"10secs"); + insta::assert_snapshot!(p(1.minute().seconds(40)), @"1min 40secs"); + + insta::assert_snapshot!(p(1.minute()), @"1min"); + insta::assert_snapshot!(p(2.minutes()), @"2mins"); + insta::assert_snapshot!(p(10.minutes()), @"10mins"); + insta::assert_snapshot!(p(1.hour().minutes(40)), @"1hr 40mins"); + + insta::assert_snapshot!(p(1.hour()), @"1hr"); + insta::assert_snapshot!(p(2.hours()), @"2hrs"); + insta::assert_snapshot!(p(10.hours()), @"10hrs"); + insta::assert_snapshot!(p(100.hours()), @"100hrs"); + + insta::assert_snapshot!( + p(1.hour().minutes(1).seconds(1)), + @"1hr 1min 1sec", + ); + insta::assert_snapshot!( + p(2.hours().minutes(2).seconds(2)), + @"2hrs 2mins 2secs", + ); + insta::assert_snapshot!( + p(10.hours().minutes(10).seconds(10)), + @"10hrs 10mins 10secs", + ); + insta::assert_snapshot!( + p(100.hours().minutes(100).seconds(100)), + @"100hrs 100mins 100secs", + ); + + insta::assert_snapshot!(p(-1.hour()), @"1hr ago"); + insta::assert_snapshot!(p(-1.hour().seconds(30)), @"1hr 30secs ago"); + } + + #[test] + fn print_span_designator_compact() { + let printer = || SpanPrinter::new().designator(Designator::Compact); + let p = |span| printer().span_to_string(&span); + + insta::assert_snapshot!(p(1.second()), @"1s"); + insta::assert_snapshot!(p(2.seconds()), @"2s"); + insta::assert_snapshot!(p(10.seconds()), @"10s"); + insta::assert_snapshot!(p(1.minute().seconds(40)), @"1m 40s"); + + insta::assert_snapshot!(p(1.minute()), @"1m"); + insta::assert_snapshot!(p(2.minutes()), @"2m"); + insta::assert_snapshot!(p(10.minutes()), @"10m"); + insta::assert_snapshot!(p(1.hour().minutes(40)), @"1h 40m"); + + insta::assert_snapshot!(p(1.hour()), @"1h"); + insta::assert_snapshot!(p(2.hours()), @"2h"); + insta::assert_snapshot!(p(10.hours()), @"10h"); + insta::assert_snapshot!(p(100.hours()), @"100h"); + + insta::assert_snapshot!( + p(1.hour().minutes(1).seconds(1)), + @"1h 1m 1s", + ); + insta::assert_snapshot!( + p(2.hours().minutes(2).seconds(2)), + @"2h 2m 2s", + ); + insta::assert_snapshot!( + p(10.hours().minutes(10).seconds(10)), + @"10h 10m 10s", + ); + insta::assert_snapshot!( + p(100.hours().minutes(100).seconds(100)), + @"100h 100m 100s", + ); + + insta::assert_snapshot!(p(-1.hour()), @"1h ago"); + insta::assert_snapshot!(p(-1.hour().seconds(30)), @"1h 30s ago"); + } + + #[test] + fn print_span_designator_direction_force() { + let printer = || SpanPrinter::new().direction(Direction::ForceSign); + let p = |span| printer().span_to_string(&span); + + insta::assert_snapshot!(p(1.second()), @"+1s"); + insta::assert_snapshot!(p(2.seconds()), @"+2s"); + insta::assert_snapshot!(p(10.seconds()), @"+10s"); + insta::assert_snapshot!(p(1.minute().seconds(40)), @"+1m 40s"); + + insta::assert_snapshot!(p(1.minute()), @"+1m"); + insta::assert_snapshot!(p(2.minutes()), @"+2m"); + insta::assert_snapshot!(p(10.minutes()), @"+10m"); + insta::assert_snapshot!(p(1.hour().minutes(40)), @"+1h 40m"); + + insta::assert_snapshot!(p(1.hour()), @"+1h"); + insta::assert_snapshot!(p(2.hours()), @"+2h"); + insta::assert_snapshot!(p(10.hours()), @"+10h"); + insta::assert_snapshot!(p(100.hours()), @"+100h"); + + insta::assert_snapshot!( + p(1.hour().minutes(1).seconds(1)), + @"+1h 1m 1s", + ); + insta::assert_snapshot!( + p(2.hours().minutes(2).seconds(2)), + @"+2h 2m 2s", + ); + insta::assert_snapshot!( + p(10.hours().minutes(10).seconds(10)), + @"+10h 10m 10s", + ); + insta::assert_snapshot!( + p(100.hours().minutes(100).seconds(100)), + @"+100h 100m 100s", + ); + + insta::assert_snapshot!(p(-1.hour()), @"-1h"); + insta::assert_snapshot!(p(-1.hour().seconds(30)), @"-1h 30s"); + } + + #[test] + fn print_span_designator_padding() { + let printer = || SpanPrinter::new().padding(2); + let p = |span| printer().span_to_string(&span); + + insta::assert_snapshot!(p(1.second()), @"01s"); + insta::assert_snapshot!(p(2.seconds()), @"02s"); + insta::assert_snapshot!(p(10.seconds()), @"10s"); + insta::assert_snapshot!(p(1.minute().seconds(40)), @"01m 40s"); + + insta::assert_snapshot!(p(1.minute()), @"01m"); + insta::assert_snapshot!(p(2.minutes()), @"02m"); + insta::assert_snapshot!(p(10.minutes()), @"10m"); + insta::assert_snapshot!(p(1.hour().minutes(40)), @"01h 40m"); + + insta::assert_snapshot!(p(1.hour()), @"01h"); + insta::assert_snapshot!(p(2.hours()), @"02h"); + insta::assert_snapshot!(p(10.hours()), @"10h"); + insta::assert_snapshot!(p(100.hours()), @"100h"); + + insta::assert_snapshot!( + p(1.hour().minutes(1).seconds(1)), + @"01h 01m 01s", + ); + insta::assert_snapshot!( + p(2.hours().minutes(2).seconds(2)), + @"02h 02m 02s", + ); + insta::assert_snapshot!( + p(10.hours().minutes(10).seconds(10)), + @"10h 10m 10s", + ); + insta::assert_snapshot!( + p(100.hours().minutes(100).seconds(100)), + @"100h 100m 100s", + ); + + insta::assert_snapshot!(p(-1.hour()), @"01h ago"); + insta::assert_snapshot!(p(-1.hour().seconds(30)), @"01h 30s ago"); + } + + #[test] + fn print_span_designator_spacing_none() { + let printer = || SpanPrinter::new().spacing(Spacing::None); + let p = |span| printer().span_to_string(&span); + + insta::assert_snapshot!(p(1.second()), @"1s"); + insta::assert_snapshot!(p(2.seconds()), @"2s"); + insta::assert_snapshot!(p(10.seconds()), @"10s"); + insta::assert_snapshot!(p(1.minute().seconds(40)), @"1m40s"); + + insta::assert_snapshot!(p(1.minute()), @"1m"); + insta::assert_snapshot!(p(2.minutes()), @"2m"); + insta::assert_snapshot!(p(10.minutes()), @"10m"); + insta::assert_snapshot!(p(1.hour().minutes(40)), @"1h40m"); + + insta::assert_snapshot!(p(1.hour()), @"1h"); + insta::assert_snapshot!(p(2.hours()), @"2h"); + insta::assert_snapshot!(p(10.hours()), @"10h"); + insta::assert_snapshot!(p(100.hours()), @"100h"); + + insta::assert_snapshot!( + p(1.hour().minutes(1).seconds(1)), + @"1h1m1s", + ); + insta::assert_snapshot!( + p(2.hours().minutes(2).seconds(2)), + @"2h2m2s", + ); + insta::assert_snapshot!( + p(10.hours().minutes(10).seconds(10)), + @"10h10m10s", + ); + insta::assert_snapshot!( + p(100.hours().minutes(100).seconds(100)), + @"100h100m100s", + ); + + insta::assert_snapshot!(p(-1.hour()), @"-1h"); + insta::assert_snapshot!(p(-1.hour().seconds(30)), @"-1h30s"); + } + + #[test] + fn print_span_designator_spacing_more() { + let printer = + || SpanPrinter::new().spacing(Spacing::BetweenUnitsAndDesignators); + let p = |span| printer().span_to_string(&span); + + insta::assert_snapshot!(p(1.second()), @"1 s"); + insta::assert_snapshot!(p(2.seconds()), @"2 s"); + insta::assert_snapshot!(p(10.seconds()), @"10 s"); + insta::assert_snapshot!(p(1.minute().seconds(40)), @"1 m 40 s"); + + insta::assert_snapshot!(p(1.minute()), @"1 m"); + insta::assert_snapshot!(p(2.minutes()), @"2 m"); + insta::assert_snapshot!(p(10.minutes()), @"10 m"); + insta::assert_snapshot!(p(1.hour().minutes(40)), @"1 h 40 m"); + + insta::assert_snapshot!(p(1.hour()), @"1 h"); + insta::assert_snapshot!(p(2.hours()), @"2 h"); + insta::assert_snapshot!(p(10.hours()), @"10 h"); + insta::assert_snapshot!(p(100.hours()), @"100 h"); + + insta::assert_snapshot!( + p(1.hour().minutes(1).seconds(1)), + @"1 h 1 m 1 s", + ); + insta::assert_snapshot!( + p(2.hours().minutes(2).seconds(2)), + @"2 h 2 m 2 s", + ); + insta::assert_snapshot!( + p(10.hours().minutes(10).seconds(10)), + @"10 h 10 m 10 s", + ); + insta::assert_snapshot!( + p(100.hours().minutes(100).seconds(100)), + @"100 h 100 m 100 s", + ); + + insta::assert_snapshot!(p(-1.hour()), @"1 h ago"); + insta::assert_snapshot!(p(-1.hour().seconds(30)), @"1 h 30 s ago"); + } + + #[test] + fn print_span_designator_spacing_comma() { + let printer = || { + SpanPrinter::new() + .comma_after_designator(true) + .spacing(Spacing::BetweenUnitsAndDesignators) + }; + let p = |span| printer().span_to_string(&span); + + insta::assert_snapshot!(p(1.second()), @"1 s"); + insta::assert_snapshot!(p(2.seconds()), @"2 s"); + insta::assert_snapshot!(p(10.seconds()), @"10 s"); + insta::assert_snapshot!(p(1.minute().seconds(40)), @"1 m, 40 s"); + + insta::assert_snapshot!(p(1.minute()), @"1 m"); + insta::assert_snapshot!(p(2.minutes()), @"2 m"); + insta::assert_snapshot!(p(10.minutes()), @"10 m"); + insta::assert_snapshot!(p(1.hour().minutes(40)), @"1 h, 40 m"); + + insta::assert_snapshot!(p(1.hour()), @"1 h"); + insta::assert_snapshot!(p(2.hours()), @"2 h"); + insta::assert_snapshot!(p(10.hours()), @"10 h"); + insta::assert_snapshot!(p(100.hours()), @"100 h"); + + insta::assert_snapshot!( + p(1.hour().minutes(1).seconds(1)), + @"1 h, 1 m, 1 s", + ); + insta::assert_snapshot!( + p(2.hours().minutes(2).seconds(2)), + @"2 h, 2 m, 2 s", + ); + insta::assert_snapshot!( + p(10.hours().minutes(10).seconds(10)), + @"10 h, 10 m, 10 s", + ); + insta::assert_snapshot!( + p(100.hours().minutes(100).seconds(100)), + @"100 h, 100 m, 100 s", + ); + + insta::assert_snapshot!(p(-1.hour()), @"1 h ago"); + insta::assert_snapshot!(p(-1.hour().seconds(30)), @"1 h, 30 s ago"); + } + + #[test] + fn print_span_designator_fractional_hour() { + let printer = + || SpanPrinter::new().fractional(Some(FractionalUnit::Hour)); + let p = |span| printer().span_to_string(&span); + let pp = |precision, span| { + printer().precision(Some(precision)).span_to_string(&span) + }; + + insta::assert_snapshot!(p(1.hour()), @"1h"); + insta::assert_snapshot!(pp(0, 1.hour()), @"1h"); + insta::assert_snapshot!(pp(1, 1.hour()), @"1.0h"); + insta::assert_snapshot!(pp(2, 1.hour()), @"1.00h"); + + insta::assert_snapshot!(p(1.hour().minutes(30)), @"1.5h"); + insta::assert_snapshot!(pp(0, 1.hour().minutes(30)), @"1h"); + insta::assert_snapshot!(pp(1, 1.hour().minutes(30)), @"1.5h"); + insta::assert_snapshot!(pp(2, 1.hour().minutes(30)), @"1.50h"); + + insta::assert_snapshot!(p(1.hour().minutes(3)), @"1.05h"); + insta::assert_snapshot!(p(1.hour().minutes(3).nanoseconds(1)), @"1.05h"); + insta::assert_snapshot!(p(1.second()), @"0.000277777h"); + // precision loss! + insta::assert_snapshot!(p(1.second().nanoseconds(1)), @"0.000277777h"); + insta::assert_snapshot!(p(0.seconds()), @"0h"); + // precision loss! + insta::assert_snapshot!(p(1.nanosecond()), @"0h"); + } + + #[test] + fn print_span_designator_fractional_minute() { + let printer = + || SpanPrinter::new().fractional(Some(FractionalUnit::Minute)); + let p = |span| printer().span_to_string(&span); + let pp = |precision, span| { + printer().precision(Some(precision)).span_to_string(&span) + }; + + insta::assert_snapshot!(p(1.hour()), @"1h"); + insta::assert_snapshot!(p(1.hour().minutes(30)), @"1h 30m"); + + insta::assert_snapshot!(p(1.minute()), @"1m"); + insta::assert_snapshot!(pp(0, 1.minute()), @"1m"); + insta::assert_snapshot!(pp(1, 1.minute()), @"1.0m"); + insta::assert_snapshot!(pp(2, 1.minute()), @"1.00m"); + + insta::assert_snapshot!(p(1.minute().seconds(30)), @"1.5m"); + insta::assert_snapshot!(pp(0, 1.minute().seconds(30)), @"1m"); + insta::assert_snapshot!(pp(1, 1.minute().seconds(30)), @"1.5m"); + insta::assert_snapshot!(pp(2, 1.minute().seconds(30)), @"1.50m"); + + insta::assert_snapshot!(p(1.hour().nanoseconds(1)), @"1h"); + insta::assert_snapshot!(p(1.minute().seconds(3)), @"1.05m"); + insta::assert_snapshot!(p(1.minute().seconds(3).nanoseconds(1)), @"1.05m"); + insta::assert_snapshot!(p(1.second()), @"0.016666666m"); + // precision loss! + insta::assert_snapshot!(p(1.second().nanoseconds(1)), @"0.016666666m"); + insta::assert_snapshot!(p(0.seconds()), @"0m"); + // precision loss! + insta::assert_snapshot!(p(1.nanosecond()), @"0m"); + } + + #[test] + fn print_span_designator_fractional_second() { + let printer = + || SpanPrinter::new().fractional(Some(FractionalUnit::Second)); + let p = |span| printer().span_to_string(&span); + let pp = |precision, span| { + printer().precision(Some(precision)).span_to_string(&span) + }; + + insta::assert_snapshot!(p(1.hour()), @"1h"); + insta::assert_snapshot!(p(1.hour().minutes(30)), @"1h 30m"); + + insta::assert_snapshot!(p(1.second()), @"1s"); + insta::assert_snapshot!(pp(0, 1.second()), @"1s"); + insta::assert_snapshot!(pp(1, 1.second()), @"1.0s"); + insta::assert_snapshot!(pp(2, 1.second()), @"1.00s"); + + insta::assert_snapshot!(p(1.second().milliseconds(500)), @"1.5s"); + insta::assert_snapshot!(pp(0, 1.second().milliseconds(500)), @"1s"); + insta::assert_snapshot!(pp(1, 1.second().milliseconds(500)), @"1.5s"); + insta::assert_snapshot!(pp(2, 1.second().milliseconds(500)), @"1.50s"); + + insta::assert_snapshot!(p(1.second().nanoseconds(1)), @"1.000000001s"); + insta::assert_snapshot!(p(1.nanosecond()), @"0.000000001s"); + insta::assert_snapshot!(p(0.seconds()), @"0s"); + + insta::assert_snapshot!(p(1.second().milliseconds(2000)), @"3s"); + } + + #[test] + fn print_span_designator_fractional_millisecond() { + let printer = || { + SpanPrinter::new().fractional(Some(FractionalUnit::Millisecond)) + }; + let p = |span| printer().span_to_string(&span); + let pp = |precision, span| { + printer().precision(Some(precision)).span_to_string(&span) + }; + + insta::assert_snapshot!(p(1.hour()), @"1h"); + insta::assert_snapshot!(p(1.hour().minutes(30)), @"1h 30m"); + insta::assert_snapshot!( + p(1.hour().minutes(30).seconds(10)), + @"1h 30m 10s", + ); + + insta::assert_snapshot!(p(1.second()), @"1s"); + insta::assert_snapshot!(pp(0, 1.second()), @"1s"); + insta::assert_snapshot!(pp(1, 1.second()), @"1s 0.0ms"); + insta::assert_snapshot!(pp(2, 1.second()), @"1s 0.00ms"); + + insta::assert_snapshot!(p(1.second().milliseconds(500)), @"1s 500ms"); + insta::assert_snapshot!( + pp(0, 1.second().milliseconds(1).microseconds(500)), + @"1s 1ms", + ); + insta::assert_snapshot!( + pp(1, 1.second().milliseconds(1).microseconds(500)), + @"1s 1.5ms", + ); + insta::assert_snapshot!( + pp(2, 1.second().milliseconds(1).microseconds(500)), + @"1s 1.50ms", + ); + + insta::assert_snapshot!(p(1.millisecond().nanoseconds(1)), @"1.000001ms"); + insta::assert_snapshot!(p(1.nanosecond()), @"0.000001ms"); + insta::assert_snapshot!(p(0.seconds()), @"0ms"); + } + + #[test] + fn print_span_designator_fractional_microsecond() { + let printer = || { + SpanPrinter::new().fractional(Some(FractionalUnit::Microsecond)) + }; + let p = |span| printer().span_to_string(&span); + let pp = |precision, span| { + printer().precision(Some(precision)).span_to_string(&span) + }; + + insta::assert_snapshot!(p(1.hour()), @"1h"); + insta::assert_snapshot!(p(1.hour().minutes(30)), @"1h 30m"); + insta::assert_snapshot!( + p(1.hour().minutes(30).seconds(10)), + @"1h 30m 10s", + ); + + insta::assert_snapshot!(p(1.second()), @"1s"); + insta::assert_snapshot!(pp(0, 1.second()), @"1s"); + insta::assert_snapshot!(pp(1, 1.second()), @"1s 0.0µs"); + insta::assert_snapshot!(pp(2, 1.second()), @"1s 0.00µs"); + + insta::assert_snapshot!(p(1.second().milliseconds(500)), @"1s 500ms"); + insta::assert_snapshot!( + pp(0, 1.second().milliseconds(1).microseconds(500)), + @"1s 1ms 500µs", + ); + insta::assert_snapshot!( + pp(1, 1.second().milliseconds(1).microseconds(500)), + @"1s 1ms 500.0µs", + ); + insta::assert_snapshot!( + pp(2, 1.second().milliseconds(1).microseconds(500)), + @"1s 1ms 500.00µs", + ); + + insta::assert_snapshot!( + p(1.millisecond().nanoseconds(1)), + @"1ms 0.001µs", + ); + insta::assert_snapshot!(p(1.nanosecond()), @"0.001µs"); + insta::assert_snapshot!(p(0.second()), @"0µs"); + } + + #[test] + fn print_signed_duration_designator_default() { + let printer = || SpanPrinter::new(); + let p = |secs| { + printer().duration_to_string(&SignedDuration::from_secs(secs)) + }; + + insta::assert_snapshot!(p(1), @"1s"); + insta::assert_snapshot!(p(2), @"2s"); + insta::assert_snapshot!(p(10), @"10s"); + insta::assert_snapshot!(p(100), @"1m 40s"); + + insta::assert_snapshot!(p(1 * 60), @"1m"); + insta::assert_snapshot!(p(2 * 60), @"2m"); + insta::assert_snapshot!(p(10 * 60), @"10m"); + insta::assert_snapshot!(p(100 * 60), @"1h 40m"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"1h"); + insta::assert_snapshot!(p(2 * 60 * 60), @"2h"); + insta::assert_snapshot!(p(10 * 60 * 60), @"10h"); + insta::assert_snapshot!(p(100 * 60 * 60), @"100h"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"1h 1m 1s", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"2h 2m 2s", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"10h 10m 10s", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"101h 41m 40s", + ); + + insta::assert_snapshot!(p(-1 * 60 * 60), @"1h ago"); + insta::assert_snapshot!(p(-1 * 60 * 60 - 30), @"1h 30s ago"); + } + + #[test] + fn print_signed_duration_designator_verbose() { + let printer = || SpanPrinter::new().designator(Designator::Verbose); + let p = |secs| { + printer().duration_to_string(&SignedDuration::from_secs(secs)) + }; + + insta::assert_snapshot!(p(1), @"1second"); + insta::assert_snapshot!(p(2), @"2seconds"); + insta::assert_snapshot!(p(10), @"10seconds"); + insta::assert_snapshot!(p(100), @"1minute 40seconds"); + + insta::assert_snapshot!(p(1 * 60), @"1minute"); + insta::assert_snapshot!(p(2 * 60), @"2minutes"); + insta::assert_snapshot!(p(10 * 60), @"10minutes"); + insta::assert_snapshot!(p(100 * 60), @"1hour 40minutes"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"1hour"); + insta::assert_snapshot!(p(2 * 60 * 60), @"2hours"); + insta::assert_snapshot!(p(10 * 60 * 60), @"10hours"); + insta::assert_snapshot!(p(100 * 60 * 60), @"100hours"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"1hour 1minute 1second", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"2hours 2minutes 2seconds", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"10hours 10minutes 10seconds", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"101hours 41minutes 40seconds", + ); + + insta::assert_snapshot!(p(-1 * 60 * 60), @"1hour ago"); + insta::assert_snapshot!(p(-1 * 60 * 60 - 30), @"1hour 30seconds ago"); + } + + #[test] + fn print_signed_duration_designator_short() { + let printer = || SpanPrinter::new().designator(Designator::Short); + let p = |secs| { + printer().duration_to_string(&SignedDuration::from_secs(secs)) + }; + + insta::assert_snapshot!(p(1), @"1sec"); + insta::assert_snapshot!(p(2), @"2secs"); + insta::assert_snapshot!(p(10), @"10secs"); + insta::assert_snapshot!(p(100), @"1min 40secs"); + + insta::assert_snapshot!(p(1 * 60), @"1min"); + insta::assert_snapshot!(p(2 * 60), @"2mins"); + insta::assert_snapshot!(p(10 * 60), @"10mins"); + insta::assert_snapshot!(p(100 * 60), @"1hr 40mins"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"1hr"); + insta::assert_snapshot!(p(2 * 60 * 60), @"2hrs"); + insta::assert_snapshot!(p(10 * 60 * 60), @"10hrs"); + insta::assert_snapshot!(p(100 * 60 * 60), @"100hrs"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"1hr 1min 1sec", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"2hrs 2mins 2secs", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"10hrs 10mins 10secs", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"101hrs 41mins 40secs", + ); + + insta::assert_snapshot!(p(-1 * 60 * 60), @"1hr ago"); + insta::assert_snapshot!(p(-1 * 60 * 60 - 30), @"1hr 30secs ago"); + } + + #[test] + fn print_signed_duration_designator_compact() { + let printer = || SpanPrinter::new().designator(Designator::Compact); + let p = |secs| { + printer().duration_to_string(&SignedDuration::from_secs(secs)) + }; + + insta::assert_snapshot!(p(1), @"1s"); + insta::assert_snapshot!(p(2), @"2s"); + insta::assert_snapshot!(p(10), @"10s"); + insta::assert_snapshot!(p(100), @"1m 40s"); + + insta::assert_snapshot!(p(1 * 60), @"1m"); + insta::assert_snapshot!(p(2 * 60), @"2m"); + insta::assert_snapshot!(p(10 * 60), @"10m"); + insta::assert_snapshot!(p(100 * 60), @"1h 40m"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"1h"); + insta::assert_snapshot!(p(2 * 60 * 60), @"2h"); + insta::assert_snapshot!(p(10 * 60 * 60), @"10h"); + insta::assert_snapshot!(p(100 * 60 * 60), @"100h"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"1h 1m 1s", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"2h 2m 2s", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"10h 10m 10s", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"101h 41m 40s", + ); + + insta::assert_snapshot!(p(-1 * 60 * 60), @"1h ago"); + insta::assert_snapshot!(p(-1 * 60 * 60 - 30), @"1h 30s ago"); + } + + #[test] + fn print_signed_duration_designator_direction_force() { + let printer = || SpanPrinter::new().direction(Direction::ForceSign); + let p = |secs| { + printer().duration_to_string(&SignedDuration::from_secs(secs)) + }; + + insta::assert_snapshot!(p(1), @"+1s"); + insta::assert_snapshot!(p(2), @"+2s"); + insta::assert_snapshot!(p(10), @"+10s"); + insta::assert_snapshot!(p(100), @"+1m 40s"); + + insta::assert_snapshot!(p(1 * 60), @"+1m"); + insta::assert_snapshot!(p(2 * 60), @"+2m"); + insta::assert_snapshot!(p(10 * 60), @"+10m"); + insta::assert_snapshot!(p(100 * 60), @"+1h 40m"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"+1h"); + insta::assert_snapshot!(p(2 * 60 * 60), @"+2h"); + insta::assert_snapshot!(p(10 * 60 * 60), @"+10h"); + insta::assert_snapshot!(p(100 * 60 * 60), @"+100h"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"+1h 1m 1s", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"+2h 2m 2s", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"+10h 10m 10s", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"+101h 41m 40s", + ); + + insta::assert_snapshot!(p(-1 * 60 * 60), @"-1h"); + insta::assert_snapshot!(p(-1 * 60 * 60 - 30), @"-1h 30s"); + } + + #[test] + fn print_signed_duration_designator_padding() { + let printer = || SpanPrinter::new().padding(2); + let p = |secs| { + printer().duration_to_string(&SignedDuration::from_secs(secs)) + }; + + insta::assert_snapshot!(p(1), @"01s"); + insta::assert_snapshot!(p(2), @"02s"); + insta::assert_snapshot!(p(10), @"10s"); + insta::assert_snapshot!(p(100), @"01m 40s"); + + insta::assert_snapshot!(p(1 * 60), @"01m"); + insta::assert_snapshot!(p(2 * 60), @"02m"); + insta::assert_snapshot!(p(10 * 60), @"10m"); + insta::assert_snapshot!(p(100 * 60), @"01h 40m"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"01h"); + insta::assert_snapshot!(p(2 * 60 * 60), @"02h"); + insta::assert_snapshot!(p(10 * 60 * 60), @"10h"); + insta::assert_snapshot!(p(100 * 60 * 60), @"100h"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"01h 01m 01s", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"02h 02m 02s", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"10h 10m 10s", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"101h 41m 40s", + ); + + insta::assert_snapshot!(p(-1 * 60 * 60), @"01h ago"); + insta::assert_snapshot!(p(-1 * 60 * 60 - 30), @"01h 30s ago"); + } + + #[test] + fn print_signed_duration_designator_spacing_none() { + let printer = || SpanPrinter::new().spacing(Spacing::None); + let p = |secs| { + printer().duration_to_string(&SignedDuration::from_secs(secs)) + }; + + insta::assert_snapshot!(p(1), @"1s"); + insta::assert_snapshot!(p(2), @"2s"); + insta::assert_snapshot!(p(10), @"10s"); + insta::assert_snapshot!(p(100), @"1m40s"); + + insta::assert_snapshot!(p(1 * 60), @"1m"); + insta::assert_snapshot!(p(2 * 60), @"2m"); + insta::assert_snapshot!(p(10 * 60), @"10m"); + insta::assert_snapshot!(p(100 * 60), @"1h40m"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"1h"); + insta::assert_snapshot!(p(2 * 60 * 60), @"2h"); + insta::assert_snapshot!(p(10 * 60 * 60), @"10h"); + insta::assert_snapshot!(p(100 * 60 * 60), @"100h"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"1h1m1s", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"2h2m2s", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"10h10m10s", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"101h41m40s", + ); + + insta::assert_snapshot!(p(-1 * 60 * 60), @"-1h"); + insta::assert_snapshot!(p(-1 * 60 * 60 - 30), @"-1h30s"); + } + + #[test] + fn print_signed_duration_designator_spacing_more() { + let printer = + || SpanPrinter::new().spacing(Spacing::BetweenUnitsAndDesignators); + let p = |secs| { + printer().duration_to_string(&SignedDuration::from_secs(secs)) + }; + + insta::assert_snapshot!(p(1), @"1 s"); + insta::assert_snapshot!(p(2), @"2 s"); + insta::assert_snapshot!(p(10), @"10 s"); + insta::assert_snapshot!(p(100), @"1 m 40 s"); + + insta::assert_snapshot!(p(1 * 60), @"1 m"); + insta::assert_snapshot!(p(2 * 60), @"2 m"); + insta::assert_snapshot!(p(10 * 60), @"10 m"); + insta::assert_snapshot!(p(100 * 60), @"1 h 40 m"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"1 h"); + insta::assert_snapshot!(p(2 * 60 * 60), @"2 h"); + insta::assert_snapshot!(p(10 * 60 * 60), @"10 h"); + insta::assert_snapshot!(p(100 * 60 * 60), @"100 h"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"1 h 1 m 1 s", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"2 h 2 m 2 s", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"10 h 10 m 10 s", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"101 h 41 m 40 s", + ); + + insta::assert_snapshot!(p(-1 * 60 * 60), @"1 h ago"); + insta::assert_snapshot!(p(-1 * 60 * 60 - 30), @"1 h 30 s ago"); + } + + #[test] + fn print_signed_duration_designator_spacing_comma() { + let printer = || { + SpanPrinter::new() + .comma_after_designator(true) + .spacing(Spacing::BetweenUnitsAndDesignators) + }; + let p = |secs| { + printer().duration_to_string(&SignedDuration::from_secs(secs)) + }; + + insta::assert_snapshot!(p(1), @"1 s"); + insta::assert_snapshot!(p(2), @"2 s"); + insta::assert_snapshot!(p(10), @"10 s"); + insta::assert_snapshot!(p(100), @"1 m, 40 s"); + + insta::assert_snapshot!(p(1 * 60), @"1 m"); + insta::assert_snapshot!(p(2 * 60), @"2 m"); + insta::assert_snapshot!(p(10 * 60), @"10 m"); + insta::assert_snapshot!(p(100 * 60), @"1 h, 40 m"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"1 h"); + insta::assert_snapshot!(p(2 * 60 * 60), @"2 h"); + insta::assert_snapshot!(p(10 * 60 * 60), @"10 h"); + insta::assert_snapshot!(p(100 * 60 * 60), @"100 h"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"1 h, 1 m, 1 s", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"2 h, 2 m, 2 s", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"10 h, 10 m, 10 s", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"101 h, 41 m, 40 s", + ); + + insta::assert_snapshot!(p(-1 * 60 * 60), @"1 h ago"); + insta::assert_snapshot!(p(-1 * 60 * 60 - 30), @"1 h, 30 s ago"); + } + + #[test] + fn print_signed_duration_designator_fractional_hour() { + let printer = + || SpanPrinter::new().fractional(Some(FractionalUnit::Hour)); + let p = |secs, nanos| { + printer().duration_to_string(&SignedDuration::new(secs, nanos)) + }; + let pp = |precision, secs, nanos| { + printer() + .precision(Some(precision)) + .duration_to_string(&SignedDuration::new(secs, nanos)) + }; + + insta::assert_snapshot!(p(1 * 60 * 60, 0), @"1h"); + insta::assert_snapshot!(pp(0, 1 * 60 * 60, 0), @"1h"); + insta::assert_snapshot!(pp(1, 1 * 60 * 60, 0), @"1.0h"); + insta::assert_snapshot!(pp(2, 1 * 60 * 60, 0), @"1.00h"); + + insta::assert_snapshot!(p(1 * 60 * 60 + 30 * 60, 0), @"1.5h"); + insta::assert_snapshot!(pp(0, 1 * 60 * 60 + 30 * 60, 0), @"1h"); + insta::assert_snapshot!(pp(1, 1 * 60 * 60 + 30 * 60, 0), @"1.5h"); + insta::assert_snapshot!(pp(2, 1 * 60 * 60 + 30 * 60, 0), @"1.50h"); + + insta::assert_snapshot!(p(1 * 60 * 60 + 3 * 60, 0), @"1.05h"); + insta::assert_snapshot!(p(1 * 60 * 60 + 3 * 60, 1), @"1.05h"); + insta::assert_snapshot!(p(1, 0), @"0.000277777h"); + // precision loss! + insta::assert_snapshot!(p(1, 1), @"0.000277777h"); + insta::assert_snapshot!(p(0, 0), @"0h"); + // precision loss! + insta::assert_snapshot!(p(0, 1), @"0h"); + + insta::assert_snapshot!( + printer().duration_to_string(&SignedDuration::MIN), + @"2562047788015215.502499999h ago", + ); + } + + #[test] + fn print_signed_duration_designator_fractional_minute() { + let printer = + || SpanPrinter::new().fractional(Some(FractionalUnit::Minute)); + let p = |secs, nanos| { + printer().duration_to_string(&SignedDuration::new(secs, nanos)) + }; + let pp = |precision, secs, nanos| { + printer() + .precision(Some(precision)) + .duration_to_string(&SignedDuration::new(secs, nanos)) + }; + + insta::assert_snapshot!(p(1 * 60 * 60, 0), @"1h"); + insta::assert_snapshot!(p(1 * 60 * 60 + 30 * 60, 0), @"1h 30m"); + + insta::assert_snapshot!(p(60, 0), @"1m"); + insta::assert_snapshot!(pp(0, 60, 0), @"1m"); + insta::assert_snapshot!(pp(1, 60, 0), @"1.0m"); + insta::assert_snapshot!(pp(2, 60, 0), @"1.00m"); + + insta::assert_snapshot!(p(90, 0), @"1.5m"); + insta::assert_snapshot!(pp(0, 90, 0), @"1m"); + insta::assert_snapshot!(pp(1, 90, 0), @"1.5m"); + insta::assert_snapshot!(pp(2, 90, 0), @"1.50m"); + + insta::assert_snapshot!(p(1 * 60 * 60, 1), @"1h"); + insta::assert_snapshot!(p(63, 0), @"1.05m"); + insta::assert_snapshot!(p(63, 1), @"1.05m"); + insta::assert_snapshot!(p(1, 0), @"0.016666666m"); + // precision loss! + insta::assert_snapshot!(p(1, 1), @"0.016666666m"); + insta::assert_snapshot!(p(0, 0), @"0m"); + // precision loss! + insta::assert_snapshot!(p(0, 1), @"0m"); + + insta::assert_snapshot!( + printer().duration_to_string(&SignedDuration::MIN), + @"2562047788015215h 30.149999999m ago", + ); + } + + #[test] + fn print_signed_duration_designator_fractional_second() { + let printer = + || SpanPrinter::new().fractional(Some(FractionalUnit::Second)); + let p = |secs, nanos| { + printer().duration_to_string(&SignedDuration::new(secs, nanos)) + }; + let pp = |precision, secs, nanos| { + printer() + .precision(Some(precision)) + .duration_to_string(&SignedDuration::new(secs, nanos)) + }; + + insta::assert_snapshot!(p(1 * 60 * 60, 0), @"1h"); + insta::assert_snapshot!(p(1 * 60 * 60 + 30 * 60, 0), @"1h 30m"); + + insta::assert_snapshot!(p(1, 0), @"1s"); + insta::assert_snapshot!(pp(0, 1, 0), @"1s"); + insta::assert_snapshot!(pp(1, 1, 0), @"1.0s"); + insta::assert_snapshot!(pp(2, 1, 0), @"1.00s"); + + insta::assert_snapshot!(p(1, 500_000_000), @"1.5s"); + insta::assert_snapshot!(pp(0, 1, 500_000_000), @"1s"); + insta::assert_snapshot!(pp(1, 1, 500_000_000), @"1.5s"); + insta::assert_snapshot!(pp(2, 1, 500_000_000), @"1.50s"); + + insta::assert_snapshot!(p(1, 1), @"1.000000001s"); + insta::assert_snapshot!(p(0, 1), @"0.000000001s"); + insta::assert_snapshot!(p(0, 0), @"0s"); + + insta::assert_snapshot!( + printer().duration_to_string(&SignedDuration::MIN), + @"2562047788015215h 30m 8.999999999s ago", + ); + } + + #[test] + fn print_signed_duration_designator_fractional_millisecond() { + let printer = || { + SpanPrinter::new().fractional(Some(FractionalUnit::Millisecond)) + }; + let p = |secs, nanos| { + printer().duration_to_string(&SignedDuration::new(secs, nanos)) + }; + let pp = |precision, secs, nanos| { + printer() + .precision(Some(precision)) + .duration_to_string(&SignedDuration::new(secs, nanos)) + }; + + insta::assert_snapshot!(p(1 * 60 * 60, 0), @"1h"); + insta::assert_snapshot!(p(1 * 60 * 60 + 30 * 60, 0), @"1h 30m"); + insta::assert_snapshot!( + p(1 * 60 * 60 + 30 * 60 + 10, 0), + @"1h 30m 10s", + ); + + insta::assert_snapshot!(p(1, 0), @"1s"); + insta::assert_snapshot!(pp(0, 1, 0), @"1s"); + insta::assert_snapshot!(pp(1, 1, 0), @"1s 0.0ms"); + insta::assert_snapshot!(pp(2, 1, 0), @"1s 0.00ms"); + + insta::assert_snapshot!(p(1, 500_000_000), @"1s 500ms"); + insta::assert_snapshot!(pp(0, 1, 1_500_000), @"1s 1ms"); + insta::assert_snapshot!(pp(1, 1, 1_500_000), @"1s 1.5ms"); + insta::assert_snapshot!(pp(2, 1, 1_500_000), @"1s 1.50ms"); + + insta::assert_snapshot!(p(0, 1_000_001), @"1.000001ms"); + insta::assert_snapshot!(p(0, 0_000_001), @"0.000001ms"); + insta::assert_snapshot!(p(0, 0), @"0ms"); + + insta::assert_snapshot!( + printer().duration_to_string(&SignedDuration::MIN), + @"2562047788015215h 30m 8s 999.999999ms ago", + ); + } + + #[test] + fn print_signed_duration_designator_fractional_microsecond() { + let printer = || { + SpanPrinter::new().fractional(Some(FractionalUnit::Microsecond)) + }; + let p = |secs, nanos| { + printer().duration_to_string(&SignedDuration::new(secs, nanos)) + }; + let pp = |precision, secs, nanos| { + printer() + .precision(Some(precision)) + .duration_to_string(&SignedDuration::new(secs, nanos)) + }; + + insta::assert_snapshot!(p(1 * 60 * 60, 0), @"1h"); + insta::assert_snapshot!(p(1 * 60 * 60 + 30 * 60, 0), @"1h 30m"); + insta::assert_snapshot!( + p(1 * 60 * 60 + 30 * 60 + 10, 0), + @"1h 30m 10s", + ); + + insta::assert_snapshot!(p(1, 0), @"1s"); + insta::assert_snapshot!(pp(0, 1, 0), @"1s"); + insta::assert_snapshot!(pp(1, 1, 0), @"1s 0.0µs"); + insta::assert_snapshot!(pp(2, 1, 0), @"1s 0.00µs"); + + insta::assert_snapshot!(p(1, 500_000_000), @"1s 500ms"); + insta::assert_snapshot!(pp(0, 1, 1_500_000), @"1s 1ms 500µs"); + insta::assert_snapshot!(pp(1, 1, 1_500_000), @"1s 1ms 500.0µs"); + insta::assert_snapshot!(pp(2, 1, 1_500_000), @"1s 1ms 500.00µs"); + + insta::assert_snapshot!(p(0, 1_000_001), @"1ms 0.001µs"); + insta::assert_snapshot!(p(0, 0_000_001), @"0.001µs"); + insta::assert_snapshot!(p(0, 0), @"0µs"); + + insta::assert_snapshot!( + printer().duration_to_string(&SignedDuration::MIN), + @"2562047788015215h 30m 8s 999ms 999.999µs ago", + ); + } + + #[test] + fn print_unsigned_duration_designator_default() { + let printer = || SpanPrinter::new(); + let p = |secs| { + printer().unsigned_duration_to_string( + &core::time::Duration::from_secs(secs), + ) + }; + + insta::assert_snapshot!(p(1), @"1s"); + insta::assert_snapshot!(p(2), @"2s"); + insta::assert_snapshot!(p(10), @"10s"); + insta::assert_snapshot!(p(100), @"1m 40s"); + + insta::assert_snapshot!(p(1 * 60), @"1m"); + insta::assert_snapshot!(p(2 * 60), @"2m"); + insta::assert_snapshot!(p(10 * 60), @"10m"); + insta::assert_snapshot!(p(100 * 60), @"1h 40m"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"1h"); + insta::assert_snapshot!(p(2 * 60 * 60), @"2h"); + insta::assert_snapshot!(p(10 * 60 * 60), @"10h"); + insta::assert_snapshot!(p(100 * 60 * 60), @"100h"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"1h 1m 1s", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"2h 2m 2s", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"10h 10m 10s", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"101h 41m 40s", + ); + } + + #[test] + fn print_unsigned_duration_designator_verbose() { + let printer = || SpanPrinter::new().designator(Designator::Verbose); + let p = |secs| { + printer().unsigned_duration_to_string( + &core::time::Duration::from_secs(secs), + ) + }; + + insta::assert_snapshot!(p(1), @"1second"); + insta::assert_snapshot!(p(2), @"2seconds"); + insta::assert_snapshot!(p(10), @"10seconds"); + insta::assert_snapshot!(p(100), @"1minute 40seconds"); + + insta::assert_snapshot!(p(1 * 60), @"1minute"); + insta::assert_snapshot!(p(2 * 60), @"2minutes"); + insta::assert_snapshot!(p(10 * 60), @"10minutes"); + insta::assert_snapshot!(p(100 * 60), @"1hour 40minutes"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"1hour"); + insta::assert_snapshot!(p(2 * 60 * 60), @"2hours"); + insta::assert_snapshot!(p(10 * 60 * 60), @"10hours"); + insta::assert_snapshot!(p(100 * 60 * 60), @"100hours"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"1hour 1minute 1second", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"2hours 2minutes 2seconds", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"10hours 10minutes 10seconds", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"101hours 41minutes 40seconds", + ); + } + + #[test] + fn print_unsigned_duration_designator_short() { + let printer = || SpanPrinter::new().designator(Designator::Short); + let p = |secs| { + printer().unsigned_duration_to_string( + &core::time::Duration::from_secs(secs), + ) + }; + + insta::assert_snapshot!(p(1), @"1sec"); + insta::assert_snapshot!(p(2), @"2secs"); + insta::assert_snapshot!(p(10), @"10secs"); + insta::assert_snapshot!(p(100), @"1min 40secs"); + + insta::assert_snapshot!(p(1 * 60), @"1min"); + insta::assert_snapshot!(p(2 * 60), @"2mins"); + insta::assert_snapshot!(p(10 * 60), @"10mins"); + insta::assert_snapshot!(p(100 * 60), @"1hr 40mins"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"1hr"); + insta::assert_snapshot!(p(2 * 60 * 60), @"2hrs"); + insta::assert_snapshot!(p(10 * 60 * 60), @"10hrs"); + insta::assert_snapshot!(p(100 * 60 * 60), @"100hrs"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"1hr 1min 1sec", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"2hrs 2mins 2secs", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"10hrs 10mins 10secs", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"101hrs 41mins 40secs", + ); + } + + #[test] + fn print_unsigned_duration_designator_compact() { + let printer = || SpanPrinter::new().designator(Designator::Compact); + let p = |secs| { + printer().unsigned_duration_to_string( + &core::time::Duration::from_secs(secs), + ) + }; + + insta::assert_snapshot!(p(1), @"1s"); + insta::assert_snapshot!(p(2), @"2s"); + insta::assert_snapshot!(p(10), @"10s"); + insta::assert_snapshot!(p(100), @"1m 40s"); + + insta::assert_snapshot!(p(1 * 60), @"1m"); + insta::assert_snapshot!(p(2 * 60), @"2m"); + insta::assert_snapshot!(p(10 * 60), @"10m"); + insta::assert_snapshot!(p(100 * 60), @"1h 40m"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"1h"); + insta::assert_snapshot!(p(2 * 60 * 60), @"2h"); + insta::assert_snapshot!(p(10 * 60 * 60), @"10h"); + insta::assert_snapshot!(p(100 * 60 * 60), @"100h"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"1h 1m 1s", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"2h 2m 2s", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"10h 10m 10s", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"101h 41m 40s", + ); + } + + #[test] + fn print_unsigned_duration_designator_direction_force() { + let printer = || SpanPrinter::new().direction(Direction::ForceSign); + let p = |secs| { + printer().unsigned_duration_to_string( + &core::time::Duration::from_secs(secs), + ) + }; + + insta::assert_snapshot!(p(1), @"+1s"); + insta::assert_snapshot!(p(2), @"+2s"); + insta::assert_snapshot!(p(10), @"+10s"); + insta::assert_snapshot!(p(100), @"+1m 40s"); + + insta::assert_snapshot!(p(1 * 60), @"+1m"); + insta::assert_snapshot!(p(2 * 60), @"+2m"); + insta::assert_snapshot!(p(10 * 60), @"+10m"); + insta::assert_snapshot!(p(100 * 60), @"+1h 40m"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"+1h"); + insta::assert_snapshot!(p(2 * 60 * 60), @"+2h"); + insta::assert_snapshot!(p(10 * 60 * 60), @"+10h"); + insta::assert_snapshot!(p(100 * 60 * 60), @"+100h"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"+1h 1m 1s", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"+2h 2m 2s", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"+10h 10m 10s", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"+101h 41m 40s", + ); + } + + #[test] + fn print_unsigned_duration_designator_padding() { + let printer = || SpanPrinter::new().padding(2); + let p = |secs| { + printer().unsigned_duration_to_string( + &core::time::Duration::from_secs(secs), + ) + }; + + insta::assert_snapshot!(p(1), @"01s"); + insta::assert_snapshot!(p(2), @"02s"); + insta::assert_snapshot!(p(10), @"10s"); + insta::assert_snapshot!(p(100), @"01m 40s"); + + insta::assert_snapshot!(p(1 * 60), @"01m"); + insta::assert_snapshot!(p(2 * 60), @"02m"); + insta::assert_snapshot!(p(10 * 60), @"10m"); + insta::assert_snapshot!(p(100 * 60), @"01h 40m"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"01h"); + insta::assert_snapshot!(p(2 * 60 * 60), @"02h"); + insta::assert_snapshot!(p(10 * 60 * 60), @"10h"); + insta::assert_snapshot!(p(100 * 60 * 60), @"100h"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"01h 01m 01s", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"02h 02m 02s", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"10h 10m 10s", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"101h 41m 40s", + ); + } + + #[test] + fn print_unsigned_duration_designator_spacing_none() { + let printer = || SpanPrinter::new().spacing(Spacing::None); + let p = |secs| { + printer().unsigned_duration_to_string( + &core::time::Duration::from_secs(secs), + ) + }; + + insta::assert_snapshot!(p(1), @"1s"); + insta::assert_snapshot!(p(2), @"2s"); + insta::assert_snapshot!(p(10), @"10s"); + insta::assert_snapshot!(p(100), @"1m40s"); + + insta::assert_snapshot!(p(1 * 60), @"1m"); + insta::assert_snapshot!(p(2 * 60), @"2m"); + insta::assert_snapshot!(p(10 * 60), @"10m"); + insta::assert_snapshot!(p(100 * 60), @"1h40m"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"1h"); + insta::assert_snapshot!(p(2 * 60 * 60), @"2h"); + insta::assert_snapshot!(p(10 * 60 * 60), @"10h"); + insta::assert_snapshot!(p(100 * 60 * 60), @"100h"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"1h1m1s", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"2h2m2s", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"10h10m10s", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"101h41m40s", + ); + } + + #[test] + fn print_unsigned_duration_designator_spacing_more() { + let printer = + || SpanPrinter::new().spacing(Spacing::BetweenUnitsAndDesignators); + let p = |secs| { + printer().unsigned_duration_to_string( + &core::time::Duration::from_secs(secs), + ) + }; + + insta::assert_snapshot!(p(1), @"1 s"); + insta::assert_snapshot!(p(2), @"2 s"); + insta::assert_snapshot!(p(10), @"10 s"); + insta::assert_snapshot!(p(100), @"1 m 40 s"); + + insta::assert_snapshot!(p(1 * 60), @"1 m"); + insta::assert_snapshot!(p(2 * 60), @"2 m"); + insta::assert_snapshot!(p(10 * 60), @"10 m"); + insta::assert_snapshot!(p(100 * 60), @"1 h 40 m"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"1 h"); + insta::assert_snapshot!(p(2 * 60 * 60), @"2 h"); + insta::assert_snapshot!(p(10 * 60 * 60), @"10 h"); + insta::assert_snapshot!(p(100 * 60 * 60), @"100 h"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"1 h 1 m 1 s", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"2 h 2 m 2 s", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"10 h 10 m 10 s", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"101 h 41 m 40 s", + ); + } + + #[test] + fn print_unsigned_duration_designator_spacing_comma() { + let printer = || { + SpanPrinter::new() + .comma_after_designator(true) + .spacing(Spacing::BetweenUnitsAndDesignators) + }; + let p = |secs| { + printer().unsigned_duration_to_string( + &core::time::Duration::from_secs(secs), + ) + }; + + insta::assert_snapshot!(p(1), @"1 s"); + insta::assert_snapshot!(p(2), @"2 s"); + insta::assert_snapshot!(p(10), @"10 s"); + insta::assert_snapshot!(p(100), @"1 m, 40 s"); + + insta::assert_snapshot!(p(1 * 60), @"1 m"); + insta::assert_snapshot!(p(2 * 60), @"2 m"); + insta::assert_snapshot!(p(10 * 60), @"10 m"); + insta::assert_snapshot!(p(100 * 60), @"1 h, 40 m"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"1 h"); + insta::assert_snapshot!(p(2 * 60 * 60), @"2 h"); + insta::assert_snapshot!(p(10 * 60 * 60), @"10 h"); + insta::assert_snapshot!(p(100 * 60 * 60), @"100 h"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"1 h, 1 m, 1 s", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"2 h, 2 m, 2 s", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"10 h, 10 m, 10 s", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"101 h, 41 m, 40 s", + ); + } + + #[test] + fn print_unsigned_duration_designator_fractional_hour() { + let printer = + || SpanPrinter::new().fractional(Some(FractionalUnit::Hour)); + let p = |secs, nanos| { + printer().unsigned_duration_to_string(&core::time::Duration::new( + secs, nanos, + )) + }; + let pp = |precision, secs, nanos| { + printer() + .precision(Some(precision)) + .duration_to_string(&SignedDuration::new(secs, nanos)) + }; + + insta::assert_snapshot!(p(1 * 60 * 60, 0), @"1h"); + insta::assert_snapshot!(pp(0, 1 * 60 * 60, 0), @"1h"); + insta::assert_snapshot!(pp(1, 1 * 60 * 60, 0), @"1.0h"); + insta::assert_snapshot!(pp(2, 1 * 60 * 60, 0), @"1.00h"); + + insta::assert_snapshot!(p(1 * 60 * 60 + 30 * 60, 0), @"1.5h"); + insta::assert_snapshot!(pp(0, 1 * 60 * 60 + 30 * 60, 0), @"1h"); + insta::assert_snapshot!(pp(1, 1 * 60 * 60 + 30 * 60, 0), @"1.5h"); + insta::assert_snapshot!(pp(2, 1 * 60 * 60 + 30 * 60, 0), @"1.50h"); + + insta::assert_snapshot!(p(1 * 60 * 60 + 3 * 60, 0), @"1.05h"); + insta::assert_snapshot!(p(1 * 60 * 60 + 3 * 60, 1), @"1.05h"); + insta::assert_snapshot!(p(1, 0), @"0.000277777h"); + // precision loss! + insta::assert_snapshot!(p(1, 1), @"0.000277777h"); + insta::assert_snapshot!(p(0, 0), @"0h"); + // precision loss! + insta::assert_snapshot!(p(0, 1), @"0h"); + } + + #[test] + fn print_unsigned_duration_designator_fractional_minute() { + let printer = + || SpanPrinter::new().fractional(Some(FractionalUnit::Minute)); + let p = |secs, nanos| { + printer().unsigned_duration_to_string(&core::time::Duration::new( + secs, nanos, + )) + }; + let pp = |precision, secs, nanos| { + printer() + .precision(Some(precision)) + .duration_to_string(&SignedDuration::new(secs, nanos)) + }; + + insta::assert_snapshot!(p(1 * 60 * 60, 0), @"1h"); + insta::assert_snapshot!(p(1 * 60 * 60 + 30 * 60, 0), @"1h 30m"); + + insta::assert_snapshot!(p(60, 0), @"1m"); + insta::assert_snapshot!(pp(0, 60, 0), @"1m"); + insta::assert_snapshot!(pp(1, 60, 0), @"1.0m"); + insta::assert_snapshot!(pp(2, 60, 0), @"1.00m"); + + insta::assert_snapshot!(p(90, 0), @"1.5m"); + insta::assert_snapshot!(pp(0, 90, 0), @"1m"); + insta::assert_snapshot!(pp(1, 90, 0), @"1.5m"); + insta::assert_snapshot!(pp(2, 90, 0), @"1.50m"); + + insta::assert_snapshot!(p(1 * 60 * 60, 1), @"1h"); + insta::assert_snapshot!(p(63, 0), @"1.05m"); + insta::assert_snapshot!(p(63, 1), @"1.05m"); + insta::assert_snapshot!(p(1, 0), @"0.016666666m"); + // precision loss! + insta::assert_snapshot!(p(1, 1), @"0.016666666m"); + insta::assert_snapshot!(p(0, 0), @"0m"); + // precision loss! + insta::assert_snapshot!(p(0, 1), @"0m"); + } + + #[test] + fn print_unsigned_duration_designator_fractional_second() { + let printer = + || SpanPrinter::new().fractional(Some(FractionalUnit::Second)); + let p = |secs, nanos| { + printer().unsigned_duration_to_string(&core::time::Duration::new( + secs, nanos, + )) + }; + let pp = |precision, secs, nanos| { + printer() + .precision(Some(precision)) + .duration_to_string(&SignedDuration::new(secs, nanos)) + }; + + insta::assert_snapshot!(p(1 * 60 * 60, 0), @"1h"); + insta::assert_snapshot!(p(1 * 60 * 60 + 30 * 60, 0), @"1h 30m"); + + insta::assert_snapshot!(p(1, 0), @"1s"); + insta::assert_snapshot!(pp(0, 1, 0), @"1s"); + insta::assert_snapshot!(pp(1, 1, 0), @"1.0s"); + insta::assert_snapshot!(pp(2, 1, 0), @"1.00s"); + + insta::assert_snapshot!(p(1, 500_000_000), @"1.5s"); + insta::assert_snapshot!(pp(0, 1, 500_000_000), @"1s"); + insta::assert_snapshot!(pp(1, 1, 500_000_000), @"1.5s"); + insta::assert_snapshot!(pp(2, 1, 500_000_000), @"1.50s"); + + insta::assert_snapshot!(p(1, 1), @"1.000000001s"); + insta::assert_snapshot!(p(0, 1), @"0.000000001s"); + insta::assert_snapshot!(p(0, 0), @"0s"); + } + + #[test] + fn print_unsigned_duration_designator_fractional_millisecond() { + let printer = || { + SpanPrinter::new().fractional(Some(FractionalUnit::Millisecond)) + }; + let p = |secs, nanos| { + printer().unsigned_duration_to_string(&core::time::Duration::new( + secs, nanos, + )) + }; + let pp = |precision, secs, nanos| { + printer() + .precision(Some(precision)) + .duration_to_string(&SignedDuration::new(secs, nanos)) + }; + + insta::assert_snapshot!(p(1 * 60 * 60, 0), @"1h"); + insta::assert_snapshot!(p(1 * 60 * 60 + 30 * 60, 0), @"1h 30m"); + insta::assert_snapshot!( + p(1 * 60 * 60 + 30 * 60 + 10, 0), + @"1h 30m 10s", + ); + + insta::assert_snapshot!(p(1, 0), @"1s"); + insta::assert_snapshot!(pp(0, 1, 0), @"1s"); + insta::assert_snapshot!(pp(1, 1, 0), @"1s 0.0ms"); + insta::assert_snapshot!(pp(2, 1, 0), @"1s 0.00ms"); + + insta::assert_snapshot!(p(1, 500_000_000), @"1s 500ms"); + insta::assert_snapshot!(pp(0, 1, 1_500_000), @"1s 1ms"); + insta::assert_snapshot!(pp(1, 1, 1_500_000), @"1s 1.5ms"); + insta::assert_snapshot!(pp(2, 1, 1_500_000), @"1s 1.50ms"); + + insta::assert_snapshot!(p(0, 1_000_001), @"1.000001ms"); + insta::assert_snapshot!(p(0, 0_000_001), @"0.000001ms"); + insta::assert_snapshot!(p(0, 0), @"0ms"); + } + + #[test] + fn print_unsigned_duration_designator_fractional_microsecond() { + let printer = || { + SpanPrinter::new().fractional(Some(FractionalUnit::Microsecond)) + }; + let p = |secs, nanos| { + printer().unsigned_duration_to_string(&core::time::Duration::new( + secs, nanos, + )) + }; + let pp = |precision, secs, nanos| { + printer().precision(Some(precision)).unsigned_duration_to_string( + &core::time::Duration::new(secs, nanos), + ) + }; + + insta::assert_snapshot!(p(1 * 60 * 60, 0), @"1h"); + insta::assert_snapshot!(p(1 * 60 * 60 + 30 * 60, 0), @"1h 30m"); + insta::assert_snapshot!( + p(1 * 60 * 60 + 30 * 60 + 10, 0), + @"1h 30m 10s", + ); + + insta::assert_snapshot!(p(1, 0), @"1s"); + insta::assert_snapshot!(pp(0, 1, 0), @"1s"); + insta::assert_snapshot!(pp(1, 1, 0), @"1s 0.0µs"); + insta::assert_snapshot!(pp(2, 1, 0), @"1s 0.00µs"); + + insta::assert_snapshot!(p(1, 500_000_000), @"1s 500ms"); + insta::assert_snapshot!(pp(0, 1, 1_500_000), @"1s 1ms 500µs"); + insta::assert_snapshot!(pp(1, 1, 1_500_000), @"1s 1ms 500.0µs"); + insta::assert_snapshot!(pp(2, 1, 1_500_000), @"1s 1ms 500.00µs"); + + insta::assert_snapshot!(p(0, 1_000_001), @"1ms 0.001µs"); + insta::assert_snapshot!(p(0, 0_000_001), @"0.001µs"); + insta::assert_snapshot!(p(0, 0), @"0µs"); + } + + #[test] + fn print_span_hms() { + let printer = || SpanPrinter::new().hours_minutes_seconds(true); + let p = |span| printer().span_to_string(&span); + + insta::assert_snapshot!(p(1.second()), @"00:00:01"); + insta::assert_snapshot!(p(2.seconds()), @"00:00:02"); + insta::assert_snapshot!(p(10.seconds()), @"00:00:10"); + insta::assert_snapshot!(p(100.seconds()), @"00:00:100"); + + insta::assert_snapshot!(p(1.minute()), @"00:01:00"); + insta::assert_snapshot!(p(2.minutes()), @"00:02:00"); + insta::assert_snapshot!(p(10.minutes()), @"00:10:00"); + insta::assert_snapshot!(p(100.minutes()), @"00:100:00"); + + insta::assert_snapshot!(p(1.hour()), @"01:00:00"); + insta::assert_snapshot!(p(2.hours()), @"02:00:00"); + insta::assert_snapshot!(p(10.hours()), @"10:00:00"); + insta::assert_snapshot!(p(100.hours()), @"100:00:00"); + + insta::assert_snapshot!( + p(1.hour().minutes(1).seconds(1)), + @"01:01:01", + ); + insta::assert_snapshot!( + p(2.hours().minutes(2).seconds(2)), + @"02:02:02", + ); + insta::assert_snapshot!( + p(10.hours().minutes(10).seconds(10)), + @"10:10:10", + ); + insta::assert_snapshot!( + p(100.hours().minutes(100).seconds(100)), + @"100:100:100", + ); + + insta::assert_snapshot!( + p(1.day().hours(1).minutes(1).seconds(1)), + @"1d 01:01:01", + ); + insta::assert_snapshot!( + p(1.day()), + @"1d 00:00:00", + ); + insta::assert_snapshot!( + p(1.day().seconds(2)), + @"1d 00:00:02", + ); + } + + #[test] + fn print_span_hms_fmt() { + let printer = || { + SpanPrinter::new() + .hours_minutes_seconds(true) + .comma_after_designator(true) + .spacing(Spacing::BetweenUnitsAndDesignators) + }; + let p = |span| printer().span_to_string(&span); + + insta::assert_snapshot!( + p(1.day().hours(1).minutes(1).seconds(1)), + @"1 d, 01:01:01", + ); + insta::assert_snapshot!( + p(1.year().months(1).weeks(1).days(1).hours(1).minutes(1).seconds(1)), + @"1 y, 1 mo, 1 w, 1 d, 01:01:01", + ); + insta::assert_snapshot!( + p(1.day().hours(1).minutes(1).seconds(1).nanoseconds(1)), + @"1 d, 01:01:01.000000001", + ); + } + + #[test] + fn print_span_hms_sign() { + let printer = |direction| { + SpanPrinter::new().hours_minutes_seconds(true).direction(direction) + }; + let p = |direction, span| printer(direction).span_to_string(&span); + + insta::assert_snapshot!( + p(Direction::Auto, 1.hour()), + @"01:00:00", + ); + insta::assert_snapshot!( + p(Direction::Sign, 1.hour()), + @"01:00:00", + ); + insta::assert_snapshot!( + p(Direction::ForceSign, 1.hour()), + @"+01:00:00", + ); + insta::assert_snapshot!( + p(Direction::Suffix, 1.hour()), + @"01:00:00", + ); + insta::assert_snapshot!( + p(Direction::Auto, -1.hour()), + @"-01:00:00", + ); + insta::assert_snapshot!( + p(Direction::Sign, -1.hour()), + @"-01:00:00", + ); + insta::assert_snapshot!( + p(Direction::ForceSign, -1.hour()), + @"-01:00:00", + ); + insta::assert_snapshot!( + p(Direction::Suffix, -1.hour()), + @"01:00:00 ago", + ); + + insta::assert_snapshot!( + p(Direction::Auto, 1.day().hours(1)), + @"1d 01:00:00", + ); + insta::assert_snapshot!( + p(Direction::Sign, 1.day().hours(1)), + @"1d 01:00:00", + ); + insta::assert_snapshot!( + p(Direction::ForceSign, 1.day().hours(1)), + @"+1d 01:00:00", + ); + insta::assert_snapshot!( + p(Direction::Suffix, 1.day().hours(1)), + @"1d 01:00:00", + ); + // This is the main change from above. With non-zero + // calendar units, the default for expressing a negative + // sign switches to a suffix in the HH:MM:SS format. + insta::assert_snapshot!( + p(Direction::Auto, -1.day().hours(1)), + @"1d 01:00:00 ago", + ); + insta::assert_snapshot!( + p(Direction::Sign, -1.day().hours(1)), + @"-1d 01:00:00", + ); + insta::assert_snapshot!( + p(Direction::ForceSign, -1.day().hours(1)), + @"-1d 01:00:00", + ); + insta::assert_snapshot!( + p(Direction::Suffix, -1.day().hours(1)), + @"1d 01:00:00 ago", + ); + } + + #[test] + fn print_span_hms_fraction_auto() { + let printer = || SpanPrinter::new().hours_minutes_seconds(true); + let p = |span| printer().span_to_string(&span); + + insta::assert_snapshot!(p(1.nanosecond()), @"00:00:00.000000001"); + insta::assert_snapshot!(p(-1.nanosecond()), @"-00:00:00.000000001"); + insta::assert_snapshot!( + printer().direction(Direction::ForceSign).span_to_string(&1.nanosecond()), + @"+00:00:00.000000001", + ); + + insta::assert_snapshot!( + p(1.second().nanoseconds(123)), + @"00:00:01.000000123", + ); + insta::assert_snapshot!( + p(1.second().milliseconds(123)), + @"00:00:01.123", + ); + insta::assert_snapshot!( + p(1.second().milliseconds(1_123)), + @"00:00:02.123", + ); + insta::assert_snapshot!( + p(1.second().milliseconds(61_123)), + @"00:00:62.123", + ); + } + + #[test] + fn print_span_hms_fraction_fixed_precision() { + let printer = || SpanPrinter::new().hours_minutes_seconds(true); + let p = |precision, span| { + printer().precision(Some(precision)).span_to_string(&span) + }; + + insta::assert_snapshot!(p(3, 1.second()), @"00:00:01.000"); + insta::assert_snapshot!( + p(3, 1.second().milliseconds(1)), + @"00:00:01.001", + ); + insta::assert_snapshot!( + p(3, 1.second().milliseconds(123)), + @"00:00:01.123", + ); + insta::assert_snapshot!( + p(3, 1.second().milliseconds(100)), + @"00:00:01.100", + ); + + insta::assert_snapshot!(p(0, 1.second()), @"00:00:01"); + insta::assert_snapshot!(p(0, 1.second().milliseconds(1)), @"00:00:01"); + insta::assert_snapshot!( + p(1, 1.second().milliseconds(999)), + @"00:00:01.9", + ); + } + + #[test] + fn print_signed_duration_hms() { + let printer = || SpanPrinter::new().hours_minutes_seconds(true); + let p = |secs| { + printer().duration_to_string(&SignedDuration::from_secs(secs)) + }; + + // Note the differences with `Span`, since with a `SignedDuration`, + // all units are balanced. + + insta::assert_snapshot!(p(1), @"00:00:01"); + insta::assert_snapshot!(p(2), @"00:00:02"); + insta::assert_snapshot!(p(10), @"00:00:10"); + insta::assert_snapshot!(p(100), @"00:01:40"); + + insta::assert_snapshot!(p(1 * 60), @"00:01:00"); + insta::assert_snapshot!(p(2 * 60), @"00:02:00"); + insta::assert_snapshot!(p(10 * 60), @"00:10:00"); + insta::assert_snapshot!(p(100 * 60), @"01:40:00"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"01:00:00"); + insta::assert_snapshot!(p(2 * 60 * 60), @"02:00:00"); + insta::assert_snapshot!(p(10 * 60 * 60), @"10:00:00"); + insta::assert_snapshot!(p(100 * 60 * 60), @"100:00:00"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"01:01:01", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"02:02:02", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"10:10:10", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"101:41:40", + ); + } + + #[test] + fn print_signed_duration_hms_sign() { + let printer = |direction| { + SpanPrinter::new().hours_minutes_seconds(true).direction(direction) + }; + let p = |direction, secs| { + printer(direction) + .duration_to_string(&SignedDuration::from_secs(secs)) + }; + + insta::assert_snapshot!(p(Direction::Auto, 1), @"00:00:01"); + insta::assert_snapshot!(p(Direction::Sign, 1), @"00:00:01"); + insta::assert_snapshot!(p(Direction::ForceSign, 1), @"+00:00:01"); + insta::assert_snapshot!(p(Direction::Suffix, 1), @"00:00:01"); + + insta::assert_snapshot!(p(Direction::Auto, -1), @"-00:00:01"); + insta::assert_snapshot!(p(Direction::Sign, -1), @"-00:00:01"); + insta::assert_snapshot!(p(Direction::ForceSign, -1), @"-00:00:01"); + insta::assert_snapshot!(p(Direction::Suffix, -1), @"00:00:01 ago"); + } + + #[test] + fn print_signed_duration_hms_fraction_auto() { + let printer = || SpanPrinter::new().hours_minutes_seconds(true); + let p = |secs, nanos| { + printer().duration_to_string(&SignedDuration::new(secs, nanos)) + }; + + insta::assert_snapshot!(p(0, 1), @"00:00:00.000000001"); + insta::assert_snapshot!(p(0, -1), @"-00:00:00.000000001"); + insta::assert_snapshot!( + printer().direction(Direction::ForceSign).duration_to_string( + &SignedDuration::new(0, 1), + ), + @"+00:00:00.000000001", + ); + + insta::assert_snapshot!( + p(1, 123), + @"00:00:01.000000123", + ); + insta::assert_snapshot!( + p(1, 123_000_000), + @"00:00:01.123", + ); + insta::assert_snapshot!( + p(1, 1_123_000_000), + @"00:00:02.123", + ); + insta::assert_snapshot!( + p(61, 1_123_000_000), + @"00:01:02.123", + ); + } + + #[test] + fn print_signed_duration_hms_fraction_fixed_precision() { + let printer = || SpanPrinter::new().hours_minutes_seconds(true); + let p = |precision, secs, nanos| { + printer() + .precision(Some(precision)) + .duration_to_string(&SignedDuration::new(secs, nanos)) + }; + + insta::assert_snapshot!(p(3, 1, 0), @"00:00:01.000"); + insta::assert_snapshot!( + p(3, 1, 1_000_000), + @"00:00:01.001", + ); + insta::assert_snapshot!( + p(3, 1, 123_000_000), + @"00:00:01.123", + ); + insta::assert_snapshot!( + p(3, 1, 100_000_000), + @"00:00:01.100", + ); + + insta::assert_snapshot!(p(0, 1, 0), @"00:00:01"); + insta::assert_snapshot!(p(0, 1, 1_000_000), @"00:00:01"); + insta::assert_snapshot!( + p(1, 1, 999_000_000), + @"00:00:01.9", + ); + } + + #[test] + fn print_unsigned_duration_hms() { + let printer = || SpanPrinter::new().hours_minutes_seconds(true); + let p = |secs| { + printer().unsigned_duration_to_string( + &core::time::Duration::from_secs(secs), + ) + }; + + // Note the differences with `Span`, since with a `Duration`, + // all units are balanced. + + insta::assert_snapshot!(p(1), @"00:00:01"); + insta::assert_snapshot!(p(2), @"00:00:02"); + insta::assert_snapshot!(p(10), @"00:00:10"); + insta::assert_snapshot!(p(100), @"00:01:40"); + + insta::assert_snapshot!(p(1 * 60), @"00:01:00"); + insta::assert_snapshot!(p(2 * 60), @"00:02:00"); + insta::assert_snapshot!(p(10 * 60), @"00:10:00"); + insta::assert_snapshot!(p(100 * 60), @"01:40:00"); + + insta::assert_snapshot!(p(1 * 60 * 60), @"01:00:00"); + insta::assert_snapshot!(p(2 * 60 * 60), @"02:00:00"); + insta::assert_snapshot!(p(10 * 60 * 60), @"10:00:00"); + insta::assert_snapshot!(p(100 * 60 * 60), @"100:00:00"); + + insta::assert_snapshot!( + p(60 * 60 + 60 + 1), + @"01:01:01", + ); + insta::assert_snapshot!( + p(2 * 60 * 60 + 2 * 60 + 2), + @"02:02:02", + ); + insta::assert_snapshot!( + p(10 * 60 * 60 + 10 * 60 + 10), + @"10:10:10", + ); + insta::assert_snapshot!( + p(100 * 60 * 60 + 100 * 60 + 100), + @"101:41:40", + ); + } + + #[test] + fn print_unsigned_duration_hms_sign() { + let printer = |direction| { + SpanPrinter::new().hours_minutes_seconds(true).direction(direction) + }; + let p = |direction, secs| { + printer(direction).unsigned_duration_to_string( + &core::time::Duration::from_secs(secs), + ) + }; + + insta::assert_snapshot!(p(Direction::Auto, 1), @"00:00:01"); + insta::assert_snapshot!(p(Direction::Sign, 1), @"00:00:01"); + insta::assert_snapshot!(p(Direction::ForceSign, 1), @"+00:00:01"); + insta::assert_snapshot!(p(Direction::Suffix, 1), @"00:00:01"); + } + + #[test] + fn print_unsigned_duration_hms_fraction_auto() { + let printer = || SpanPrinter::new().hours_minutes_seconds(true); + let p = |secs, nanos| { + printer().unsigned_duration_to_string(&core::time::Duration::new( + secs, nanos, + )) + }; + + insta::assert_snapshot!(p(0, 1), @"00:00:00.000000001"); + insta::assert_snapshot!( + printer().direction(Direction::ForceSign).duration_to_string( + &SignedDuration::new(0, 1), + ), + @"+00:00:00.000000001", + ); + + insta::assert_snapshot!( + p(1, 123), + @"00:00:01.000000123", + ); + insta::assert_snapshot!( + p(1, 123_000_000), + @"00:00:01.123", + ); + insta::assert_snapshot!( + p(1, 1_123_000_000), + @"00:00:02.123", + ); + insta::assert_snapshot!( + p(61, 1_123_000_000), + @"00:01:02.123", + ); + } + + #[test] + fn print_unsigned_duration_hms_fraction_fixed_precision() { + let printer = || SpanPrinter::new().hours_minutes_seconds(true); + let p = |precision, secs, nanos| { + printer().precision(Some(precision)).unsigned_duration_to_string( + &core::time::Duration::new(secs, nanos), + ) + }; + + insta::assert_snapshot!(p(3, 1, 0), @"00:00:01.000"); + insta::assert_snapshot!( + p(3, 1, 1_000_000), + @"00:00:01.001", + ); + insta::assert_snapshot!( + p(3, 1, 123_000_000), + @"00:00:01.123", + ); + insta::assert_snapshot!( + p(3, 1, 100_000_000), + @"00:00:01.100", + ); + + insta::assert_snapshot!(p(0, 1, 0), @"00:00:01"); + insta::assert_snapshot!(p(0, 1, 1_000_000), @"00:00:01"); + insta::assert_snapshot!( + p(1, 1, 999_000_000), + @"00:00:01.9", + ); + } + + /// Tests that we can write the maximal string successfully. + /// + /// This test doesn't guarantee that we'll always attempt the true + /// maximum. The maximums here were determined by human inspection. + #[test] + fn maximums() { + let p = SpanPrinter::new() + .padding(u8::MAX) + .designator(Designator::Verbose) + .spacing(Spacing::BetweenUnitsAndDesignators) + .comma_after_designator(true); + + let span = -19_998 + .year() + .months(239_976) + .weeks(1_043_497) + .days(7_304_484) + .hours(175_307_616) + .minutes(10_518_456_960i64) + .seconds(631_107_417_600i64) + .milliseconds(631_107_417_600_000i64) + .microseconds(631_107_416_600_000_000i64) + .nanoseconds(9_223_372_036_854_775_807i64); + insta::assert_snapshot!( + p.span_to_string(&span), + @"00000000000000019998 years, 00000000000000239976 months, 00000000000001043497 weeks, 00000000000007304484 days, 00000000000175307616 hours, 00000000010518456960 minutes, 00000000631107417600 seconds, 00000631107417600000 milliseconds, 00631107416600000000 microseconds, 09223372036854775807 nanoseconds ago", + ); + + let sdur = SignedDuration::MAX; + insta::assert_snapshot!( + p.duration_to_string(&sdur), + @"00002562047788015215 hours, 00000000000000000030 minutes, 00000000000000000007 seconds, 00000000000000000999 milliseconds, 00000000000000000999 microseconds, 00000000000000000999 nanoseconds", + ); + + let udur = + core::time::Duration::MAX - core::time::Duration::from_secs(16); + insta::assert_snapshot!( + p.unsigned_duration_to_string(&udur), + @"00005124095576030430 hours, 00000000000000000059 minutes, 00000000000000000059 seconds, 00000000000000000999 milliseconds, 00000000000000000999 microseconds, 00000000000000000999 nanoseconds", + ); + } +} diff --git a/crates/jiff/src/fmt/temporal/printer.rs b/crates/jiff/src/fmt/temporal/printer.rs new file mode 100644 index 0000000..cf728ac --- /dev/null +++ b/crates/jiff/src/fmt/temporal/printer.rs @@ -0,0 +1,1933 @@ +use core::time::Duration; + +use jcore::tz::posix; + +use crate::{ + civil::{Date, DateTime, ISOWeekDate, Time}, + error::{fmt::temporal::Error as E, Error}, + fmt::{ + buffer::{ArrayBuffer, BorrowedBuffer, BorrowedWriter}, + temporal::{Pieces, PiecesOffset, TimeZoneAnnotationKind}, + Write, + }, + span::{Span, UnitSet}, + tz::{Offset, TimeZone}, + SignedDuration, Timestamp, Unit, Zoned, +}; + +/// Defines the "maximum" possible length (in bytes) of an RFC 9557 zoned +/// datetime. +/// +/// In practice, the actual maximal string possible is I believe +/// `-009999-03-14T17:30:00.999999999-04:23[America/Argentina/ComodRivadavia]`, +/// which is 72 bytes. Obviously, the length of the IANA tzdb identifier +/// matters and can be highly variable. +/// +/// So why is this called "reasonable" and not "maximum"? Well, it's the +/// IANA tzdb identifiers. They can be arbitrarily long. There also aren't +/// any rules about how long they can be. So in theory, IANA could allocate +/// a new identifier longer than `America/Argentina/ComodRivadavia`. With +/// that said, they do generally try to keep them succinct. +/// +/// Separately from what IANA does, Jiff itself doesn't impose any restrictions +/// on the length of identifiers. Callers can pass in arbitrarily long +/// identifiers via `TimeZone::tzif` or by simply futzing with the names of +/// files in `/usr/share/zoneinfo`. It's also possible to use an arbitrarily +/// long identifier via the "pieces" `TimeZoneAnnotationName` API. Since we +/// don't impose any restrictions, we really do want to at least try to handle +/// arbitrarily long identifiers here. +/// +/// Thus, we define a "reasonable" upper bound. When the RFC 9557 string we +/// want to serialize is known to be under this bound, then we'll use a "fast" +/// path with a fixed size buffer on the stack (or perhaps even write directly +/// into the spare capacity of a caller providd `String`). But when it's above +/// this bound, then we fall back to a slower path that uses a buffering +/// mechanism to permit arbitrarily long IANA tzdb identifiers. +/// +/// For the most part, doing this dance doesn't come with a runtime cost, +/// primarily because we choose to sacrifice code size a bit by duplicating +/// some functions. We could have our cake and eat it too if we enforce a +/// maximum length on IANA tzdb identifiers. Then we could set a true +/// `MAX_ZONED_LEN` and avoid the case where buffering is needed. +const REASONABLE_ZONED_LEN: usize = 72; + +/// Defines the "maximum" possible length (in bytes) of an RFC 9557 zoned +/// datetime via the "pieces" API. +/// +/// This generally should be identical to `REASONABLE_ZONED_LEN`, except its +/// expected maximum is one byte longer. Namely, the pieces API currently +/// lets the caller roundtrip the "criticality" of the timestamp. i.e., +/// the `!` in the time zone annotation. So it's one extra byte longer +/// than zoned datetimes. +/// +/// Note that all the same considerations from variable length IANA tzdb +/// identifiers apply to the pieces printing just as it does to zoned datetime +/// printing. +const REASONABLE_PIECES_LEN: usize = 73; + +/// This is my estimate for the "maximum" length of a time zone serialization. +/// +/// This is effectively +/// +/// ```text +/// max(max_iana_tzdb_id, max_posix_tz_string, offset, len('Etc/Unknown')) +/// ``` +/// +/// where "max" is not a strict upper bound but an expected upper bound based +/// on common or "reasonable" values. For example, a valid POSIX time zone +/// string can have 30 byte abbreviations for both standard and daylight saving +/// time. +const REASONABLE_TIME_ZONE_LEN: usize = 32; + +/// Defines the "maximum" possible length (in bytes) of a POSIX time zone +/// string. +/// +/// Most POSIX time zone strings are just two 3 letter abbreviations and an +/// offset indicating standard time. e.g., `EST5EDT`. +/// +/// We set this to `20` because we write integers and those APIs require a +/// minimum buffer length of `20`. (Even though a POSIX time zone will never +/// have integers nearly that large.) +const REASONABLE_POSIX_TIME_ZONE_LEN: usize = 20; + +/// Defines the maximum possible length (in bytes) of an RFC 3339 timestamp +/// that is always in Zulu time. +/// +/// The longest possible string is `-009999-03-14T21:53:08.999999999Z`. +const MAX_TIMESTAMP_ZULU_LEN: usize = 33; + +/// Defines the maximum possible length (in bytes) of an RFC 3339 timestamp +/// with an offset (up to minute precision). +/// +/// The longest possible string is `-009999-03-15T23:53:07.999999999+25:59`. +const MAX_TIMESTAMP_OFFSET_LEN: usize = 38; + +/// Defines the maximum possible length (in bytes) of an ISO 8601 datetime. +/// +/// The longest possible string is `-009999-03-14T17:30:00.999999999`. +const MAX_DATETIME_LEN: usize = 32; + +/// Defines the maximum possible length (in bytes) of an ISO 8601 date. +/// +/// The longest possible string is `-009999-03-14`. +const MAX_DATE_LEN: usize = 13; + +/// Defines the maximum possible length (in bytes) of an ISO 8601 time. +/// +/// The longest possible string is `17:30:00.999999999`. +const MAX_TIME_LEN: usize = 18; + +/// Defines the maximum possible length (in bytes) of an ISO 8601 week date. +/// +/// The longest possible string is `-009999-W11-3`. +const MAX_ISO_WEEK_DATE_LEN: usize = 13; + +/// Defines the maximum possible length (in bytes) of a `Span` printed in the +/// Temporal ISO 8601 format. +/// +/// The way I computed this length was by using the default printer (since +/// there's only one knob and it doesn't impact the length of the string) +/// and using a negative `Span` with each unit set to +/// its minimum value. +const MAX_SPAN_LEN: usize = 78; + +/// Defines the maximum possible length (in bytes) of a duration printed in the +/// Temporal ISO 8601 format. +/// +/// This applies to both signed and unsigned durations. Unsigned durations have +/// one more digit, but signed durations can have a negative sign. +const MAX_DURATION_LEN: usize = 35; + +#[derive(Clone, Debug)] +pub(super) struct DateTimePrinter { + lowercase: bool, + separator: u8, + precision: Option, +} + +impl DateTimePrinter { + pub(super) const fn new() -> DateTimePrinter { + DateTimePrinter { lowercase: false, separator: b'T', precision: None } + } + + pub(super) const fn lowercase(self, yes: bool) -> DateTimePrinter { + DateTimePrinter { lowercase: yes, ..self } + } + + pub(super) const fn separator(self, ascii_char: u8) -> DateTimePrinter { + assert!(ascii_char.is_ascii(), "RFC3339 separator must be ASCII"); + DateTimePrinter { separator: ascii_char, ..self } + } + + pub(super) const fn precision( + self, + precision: Option, + ) -> DateTimePrinter { + DateTimePrinter { precision, ..self } + } + + pub(super) fn print_zoned( + &self, + zdt: &Zoned, + wtr: &mut dyn Write, + ) -> Result<(), Error> { + /// The base size of an RFC 9557 zoned datetime string. + /// + /// 19 comes from the datetime component, e.g., `2025-01-01T00:00:00` + /// 6 comes from the offset, e.g., `+05:30` + /// 2 comes from the `[` and `]` for the time zone annotation + /// + /// Basically, we always need space for *at least* the above stuff. + /// The actual time zone annotation name, negative year and fractional + /// second component are all optional and can vary quite a bit in + /// length. + /// + /// We do this calculation to get a tighter bound on the spare capacity + /// needed when the caller provides a `&mut String` or a `&mut Vec` + /// to write into. That is, we want to try hard not to over-allocate. + /// + /// Note that memory safety does not depend on us getting this + /// calculation right. If we get it wrong, the printer will panic if + /// it tries to print a string that exceeds the calculated amount. + const BASE: usize = 19 + 6 + 2; + + // An IANA tzdb identifier is variable length data, so add its length + // to the `BASE` for runtime allocation size. When there is no IANA + // identifier, we could write a fixed offset (that's always 6 bytes) or + // `Etc/Unknown` (11 bytes) for when the offset from UTC is not known. + // In the non-IANA case, we just use an upper bound of 11, so we will + // over-allocate a little in the fixed offset case. + let mut runtime_allocation = BASE + + zdt.time_zone().iana_name().map(|name| name.len()).unwrap_or(11); + // A datetime before year 0 means we add a `-00` prefix. e.g., + // `-001234-01-01`. + if zdt.year() < 0 { + runtime_allocation += 3; + } + // If we're printing fractional seconds, then we need more room for + // that. This potentially overallocates because we don't do the extra + // work required to find a tighter bound. + if zdt.subsec_nanosecond() != 0 || self.precision.is_some() { + runtime_allocation += 10; + } + + // The runtime allocation could be greater than what we assume is a + // "reasonable" upper bound on the length of an RFC 9557 string. This + // can only happen when an IANA tzdb identifier is very long. When + // we're under the limit, we use a fast path. + if runtime_allocation <= REASONABLE_ZONED_LEN { + return BorrowedBuffer::with_writer::( + wtr, + runtime_allocation, + |bbuf| Ok(self.print_zoned_buf(zdt, bbuf)), + ); + } + + // ... otherwise, we use a path with a buffered writer that is slower + // but can deal with arbitrarily long IANA tzdb identifiers. + let mut buf = ArrayBuffer::::default(); + let mut bbuf = buf.as_borrowed(); + let mut wtr = BorrowedWriter::new(&mut bbuf, wtr); + self.print_zoned_wtr(zdt, &mut wtr)?; + wtr.finish() + } + + fn print_zoned_buf(&self, zdt: &Zoned, bbuf: &mut BorrowedBuffer<'_>) { + self.print_datetime_buf(&zdt.datetime(), bbuf); + let tz = zdt.time_zone(); + if tz.is_unknown() { + bbuf.write_str("Z[Etc/Unknown]"); + } else { + self.print_offset_rounded_buf(&zdt.offset(), bbuf); + self.print_time_zone_annotation_buf(&tz, &zdt.offset(), bbuf); + } + } + + fn print_zoned_wtr( + &self, + zdt: &Zoned, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + self.print_datetime_wtr(&zdt.datetime(), wtr)?; + let tz = zdt.time_zone(); + if tz.is_unknown() { + wtr.write_str("Z[Etc/Unknown]")?; + } else { + self.print_offset_rounded_wtr(&zdt.offset(), wtr)?; + self.print_time_zone_annotation_wtr(&tz, &zdt.offset(), wtr)?; + } + Ok(()) + } + + pub(super) fn print_timestamp( + &self, + timestamp: &Timestamp, + wtr: &mut dyn Write, + ) -> Result<(), Error> { + let mut runtime_allocation = MAX_TIMESTAMP_ZULU_LEN; + // Don't reserve room for fractional seconds if we don't use them. + if timestamp.subsec_nanosecond() == 0 && self.precision.is_none() { + runtime_allocation -= 10; + } + BorrowedBuffer::with_writer::( + wtr, + runtime_allocation, + |bbuf| Ok(self.print_timestamp_buf(timestamp, bbuf)), + ) + } + + fn print_timestamp_buf( + &self, + timestamp: &Timestamp, + bbuf: &mut BorrowedBuffer<'_>, + ) { + let dt = Offset::UTC.to_datetime(*timestamp); + self.print_datetime_buf(&dt, bbuf); + self.print_zulu_buf(bbuf); + } + + pub(super) fn print_timestamp_with_offset( + &self, + timestamp: &Timestamp, + offset: Offset, + wtr: &mut dyn Write, + ) -> Result<(), Error> { + let mut runtime_allocation = MAX_TIMESTAMP_OFFSET_LEN; + // Don't reserve room for fractional seconds if we don't use them. + if timestamp.subsec_nanosecond() == 0 && self.precision.is_none() { + runtime_allocation -= 10; + } + BorrowedBuffer::with_writer::( + wtr, + runtime_allocation, + |bbuf| { + Ok(self + .print_timestamp_with_offset_buf(timestamp, offset, bbuf)) + }, + ) + } + + fn print_timestamp_with_offset_buf( + &self, + timestamp: &Timestamp, + offset: Offset, + bbuf: &mut BorrowedBuffer<'_>, + ) { + let dt = offset.to_datetime(*timestamp); + self.print_datetime_buf(&dt, bbuf); + self.print_offset_rounded_buf(&offset, bbuf); + } + + pub(super) fn print_datetime( + &self, + dt: &DateTime, + wtr: &mut dyn Write, + ) -> Result<(), Error> { + let mut runtime_allocation = MAX_DATETIME_LEN; + // Don't reserve room for fractional seconds if we don't use them. + if dt.subsec_nanosecond() == 0 && self.precision.is_none() { + runtime_allocation -= 10; + } + BorrowedBuffer::with_writer::( + wtr, + runtime_allocation, + |bbuf| Ok(self.print_datetime_buf(dt, bbuf)), + ) + } + + fn print_datetime_buf( + &self, + dt: &DateTime, + bbuf: &mut BorrowedBuffer<'_>, + ) { + self.print_date_buf(&dt.date(), bbuf); + bbuf.write_ascii_char(if self.lowercase { + self.separator.to_ascii_lowercase() + } else { + self.separator + }); + self.print_time_buf(&dt.time(), bbuf); + } + + fn print_datetime_wtr( + &self, + dt: &DateTime, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + self.print_date_wtr(&dt.date(), wtr)?; + wtr.write_ascii_char(if self.lowercase { + self.separator.to_ascii_lowercase() + } else { + self.separator + })?; + self.print_time_wtr(&dt.time(), wtr)?; + Ok(()) + } + + pub(super) fn print_date( + &self, + date: &Date, + wtr: &mut dyn Write, + ) -> Result<(), Error> { + BorrowedBuffer::with_writer::( + wtr, + MAX_DATE_LEN, + |bbuf| Ok(self.print_date_buf(date, bbuf)), + ) + } + + fn print_date_buf(&self, date: &Date, bbuf: &mut BorrowedBuffer<'_>) { + let year = date.year(); + if year < 0 { + bbuf.write_str("-00"); + } + bbuf.write_int_pad4(year.unsigned_abs()); + bbuf.write_ascii_char(b'-'); + bbuf.write_int_pad2(date.month().unsigned_abs()); + bbuf.write_ascii_char(b'-'); + bbuf.write_int_pad2(date.day().unsigned_abs()); + } + + fn print_date_wtr( + &self, + date: &Date, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + let year = date.year(); + if year < 0 { + wtr.write_str("-00")?; + } + wtr.write_int_pad4(year.unsigned_abs())?; + wtr.write_ascii_char(b'-')?; + wtr.write_int_pad2(date.month().unsigned_abs())?; + wtr.write_ascii_char(b'-')?; + wtr.write_int_pad2(date.day().unsigned_abs())?; + Ok(()) + } + + pub(super) fn print_time( + &self, + time: &Time, + wtr: &mut dyn Write, + ) -> Result<(), Error> { + let mut runtime_allocation = MAX_TIME_LEN; + // Don't reserve room for fractional seconds if we don't use them. + if time.subsec_nanosecond() == 0 && self.precision.is_none() { + runtime_allocation -= 10; + } + BorrowedBuffer::with_writer::( + wtr, + runtime_allocation, + |bbuf| Ok(self.print_time_buf(time, bbuf)), + ) + } + + fn print_time_buf(&self, time: &Time, bbuf: &mut BorrowedBuffer<'_>) { + bbuf.write_int_pad2(time.hour().unsigned_abs()); + bbuf.write_ascii_char(b':'); + bbuf.write_int_pad2(time.minute().unsigned_abs()); + bbuf.write_ascii_char(b':'); + bbuf.write_int_pad2(time.second().unsigned_abs()); + let fractional_nanosecond = time.subsec_nanosecond(); + if self.precision.map_or(fractional_nanosecond != 0, |p| p > 0) { + bbuf.write_ascii_char(b'.'); + bbuf.write_fraction( + self.precision, + fractional_nanosecond.unsigned_abs(), + ); + } + } + + fn print_time_wtr( + &self, + time: &Time, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + wtr.write_int_pad2(time.hour().unsigned_abs())?; + wtr.write_ascii_char(b':')?; + wtr.write_int_pad2(time.minute().unsigned_abs())?; + wtr.write_ascii_char(b':')?; + wtr.write_int_pad2(time.second().unsigned_abs())?; + let fractional_nanosecond = time.subsec_nanosecond(); + if self.precision.map_or(fractional_nanosecond != 0, |p| p > 0) { + wtr.write_ascii_char(b'.')?; + wtr.write_fraction( + self.precision, + fractional_nanosecond.unsigned_abs(), + )?; + } + Ok(()) + } + + pub(super) fn print_time_zone( + &self, + tz: &TimeZone, + wtr: &mut dyn Write, + ) -> Result<(), Error> { + let mut buf = ArrayBuffer::::default(); + let mut bbuf = buf.as_borrowed(); + let mut wtr = BorrowedWriter::new(&mut bbuf, wtr); + self.print_time_zone_wtr(tz, &mut wtr)?; + wtr.finish() + } + + fn print_time_zone_wtr( + &self, + tz: &TimeZone, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + if let Some(iana_name) = tz.iana_name() { + return wtr.write_str(iana_name); + } + if tz.is_unknown() { + return wtr.write_str("Etc/Unknown"); + } + if let Ok(offset) = tz.to_fixed_offset() { + return self.print_offset_full_precision(&offset, wtr); + } + if let Some(posix_tz) = tz.posix_tz() { + return self.print_posix_time_zone(posix_tz, wtr); + } + // Ideally this never actually happens, but it can, and there + // are likely system configurations out there in which it does. + // I can imagine "lightweight" installations that just have a + // `/etc/localtime` as a TZif file that doesn't point to any IANA time + // zone. In which case, serializing a time zone probably doesn't make + // much sense. + // + // Anyway, if you're seeing this error and think there should be a + // different behavior, please file an issue. + Err(Error::from(E::PrintTimeZoneFailure)) + } + + pub(super) fn print_posix_time_zone( + &self, + posix_tz: &posix::TimeZone, + wtr: &mut dyn Write, + ) -> Result<(), Error> { + let mut buf = ArrayBuffer::::default(); + let mut bbuf = buf.as_borrowed(); + let mut wtr = BorrowedWriter::new(&mut bbuf, wtr); + self.print_posix_time_zone_wtr(posix_tz, &mut wtr)?; + wtr.finish() + } + + fn print_posix_time_zone_wtr( + &self, + tz: &posix::TimeZone, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + self.print_posix_abbreviation_wtr(tz.std_abbrev.as_str(), wtr)?; + self.print_posix_offset_wtr(&tz.std_offset, wtr)?; + if let Some(ref dst) = tz.dst { + self.print_posix_dst_wtr(dst, &tz.std_offset, wtr)?; + } + Ok(()) + } + + fn print_posix_dst_wtr( + &self, + dst: &posix::Dst, + std_offset: &jcore::tz::Offset, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + self.print_posix_abbreviation_wtr(dst.abbrev.as_ref(), wtr)?; + // The overwhelmingly common case is that DST is exactly one hour + // ahead of standard time. So common that this is the default. So + // don't write the offset if we don't need to. + if Some(dst.offset) != std_offset.checked_add(3600).ok() { + self.print_posix_offset_wtr(&dst.offset, wtr)?; + } + wtr.write_ascii_char(b',')?; + self.print_posix_rule_wtr(&dst.rule, wtr)?; + Ok(()) + } + + fn print_posix_rule_wtr( + &self, + rule: &posix::Rule, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + self.print_posix_day_time_wtr(&rule.start, wtr)?; + wtr.write_ascii_char(b',')?; + self.print_posix_day_time_wtr(&rule.end, wtr)?; + Ok(()) + } + + fn print_posix_day_time_wtr( + &self, + day_time: &posix::DayTime, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + self.print_posix_day_wtr(&day_time.date, wtr)?; + // This is the default time, so don't write it if we don't need to. + if day_time.time != posix::TransitionCivilTime::DEFAULT { + wtr.write_ascii_char(b'/')?; + self.print_posix_time_wtr(&day_time.time, wtr)?; + } + Ok(()) + } + + fn print_posix_day_wtr( + &self, + day: &posix::Day, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + match *day { + posix::Day::JulianOne(n) => { + wtr.write_ascii_char(b'J')?; + wtr.write_int(n.unsigned_abs())?; + Ok(()) + } + posix::Day::JulianZero(n) => wtr.write_int(n.unsigned_abs()), + posix::Day::WeekdayOfMonth { month, week, weekday } => { + wtr.write_ascii_char(b'M')?; + wtr.write_int(month.unsigned_abs())?; + wtr.write_ascii_char(b'.')?; + wtr.write_int1(week.unsigned_abs())?; + wtr.write_ascii_char(b'.')?; + wtr.write_int1( + weekday.to_sunday_zero_offset().unsigned_abs(), + )?; + Ok(()) + } + } + } + + fn print_posix_time_wtr( + &self, + time: &posix::TransitionCivilTime, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + if time.second.is_negative() { + wtr.write_ascii_char(b'-')?; + // The default is positive, so when + // positive, we write nothing. + } + self.print_posix_time_offset_seconds_wtr(time.second, wtr) + } + + fn print_posix_offset_wtr( + &self, + offset: &jcore::tz::Offset, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + // Yes, this is backwards. Blame POSIX. + // N.B. `+` is the default, so we don't + // need to write that out. + if offset.seconds() > 0 { + wtr.write_ascii_char(b'-')?; + } + self.print_posix_time_offset_seconds_wtr(offset.seconds(), wtr) + } + + fn print_posix_time_offset_seconds_wtr( + &self, + second: i32, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + let second = second.unsigned_abs(); + let h = second / 3600; + let m = (second / 60) % 60; + let s = second % 60; + wtr.write_int(h)?; + if m != 0 || s != 0 { + wtr.write_ascii_char(b':')?; + wtr.write_int_pad2(m)?; + if s != 0 { + wtr.write_ascii_char(b':')?; + wtr.write_int_pad2(s)?; + } + } + Ok(()) + } + + fn print_posix_abbreviation_wtr( + &self, + abbreviation: &str, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + if abbreviation.chars().any(|ch| ch == '+' || ch == '-') { + wtr.write_ascii_char(b'<')?; + wtr.write_str(abbreviation)?; + wtr.write_ascii_char(b'>')?; + } else { + wtr.write_str(abbreviation)?; + } + Ok(()) + } + + pub(super) fn print_pieces( + &self, + pieces: &Pieces, + wtr: &mut dyn Write, + ) -> Result<(), Error> { + // N.B. We don't bother with writing into the spare capacity of a + // `&mut String` here because with `Pieces` it's a little more + // complicated to find a more precise upper bound on the length. + // Plus, I don't think this API is commonly used, so it's not clear + // that it's worth optimizing. But I'm open to a PR with benchmarks + // if there's a good use case. ---AG + let mut buf = ArrayBuffer::::default(); + let mut bbuf = buf.as_borrowed(); + let mut wtr = BorrowedWriter::new(&mut bbuf, wtr); + self.print_pieces_wtr(pieces, &mut wtr)?; + wtr.finish() + } + + fn print_pieces_wtr( + &self, + pieces: &Pieces, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + if let Some(time) = pieces.time() { + let dt = DateTime::from_parts(pieces.date(), time); + self.print_datetime_wtr(&dt, wtr)?; + if let Some(poffset) = pieces.offset() { + self.print_pieces_offset(&poffset, wtr)?; + } + } else if let Some(poffset) = pieces.offset() { + // In this case, we have an offset but no time component. Since + // `2025-01-02-05:00` isn't valid, we forcefully write out the + // default time (which is what would be assumed anyway). + let dt = DateTime::from_parts(pieces.date(), Time::midnight()); + self.print_datetime_wtr(&dt, wtr)?; + self.print_pieces_offset(&poffset, wtr)?; + } else { + // We have no time and no offset, so we can just write the date. + // It's okay to write this followed by an annotation, e.g., + // `2025-01-02[America/New_York]` or even `2025-01-02[-05:00]`. + self.print_date_wtr(&pieces.date(), wtr)?; + } + // For the time zone annotation, a `Pieces` gives us the annotation + // name or offset directly, where as with `Zoned`, we have a + // `TimeZone`. So we hand-roll our own formatter directly from the + // annotation. + if let Some(ann) = pieces.time_zone_annotation() { + wtr.write_ascii_char(b'[')?; + if ann.is_critical() { + wtr.write_ascii_char(b'!')?; + } + match *ann.kind() { + TimeZoneAnnotationKind::Named(ref name) => { + wtr.write_str(name.as_str())?; + } + TimeZoneAnnotationKind::Offset(offset) => { + self.print_offset_rounded_wtr(&offset, wtr)?; + } + } + wtr.write_ascii_char(b']')?; + } + Ok(()) + } + + pub(super) fn print_iso_week_date( + &self, + iso_week_date: &ISOWeekDate, + wtr: &mut dyn Write, + ) -> Result<(), Error> { + BorrowedBuffer::with_writer::( + wtr, + MAX_ISO_WEEK_DATE_LEN, + |bbuf| Ok(self.print_iso_week_date_buf(iso_week_date, bbuf)), + ) + } + + fn print_iso_week_date_buf( + &self, + iso_week_date: &ISOWeekDate, + bbuf: &mut BorrowedBuffer<'_>, + ) { + let year = iso_week_date.year(); + if year < 0 { + bbuf.write_str("-00"); + } + bbuf.write_int_pad4(year.unsigned_abs()); + bbuf.write_ascii_char(b'-'); + bbuf.write_ascii_char(if self.lowercase { b'w' } else { b'W' }); + bbuf.write_int_pad2(iso_week_date.week().unsigned_abs()); + bbuf.write_ascii_char(b'-'); + bbuf.write_int1( + iso_week_date.weekday().to_monday_one_offset().unsigned_abs(), + ); + } + + fn print_pieces_offset( + &self, + poffset: &PiecesOffset, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + match *poffset { + PiecesOffset::Zulu => self.print_zulu_wtr(wtr), + PiecesOffset::Numeric(ref noffset) => { + if noffset.offset().is_zero() && noffset.is_negative() { + wtr.write_str("-00:00") + } else { + self.print_offset_rounded_wtr(&noffset.offset(), wtr) + } + } + } + } + + /// Formats the given offset into the writer given. + /// + /// If the given offset has non-zero seconds, then they are rounded to + /// the nearest minute. + fn print_offset_rounded_buf( + &self, + offset: &Offset, + bbuf: &mut BorrowedBuffer<'_>, + ) { + bbuf.write_ascii_char(if offset.is_negative() { b'-' } else { b'+' }); + let (offset_hours, offset_minutes) = offset.round_to_nearest_minute(); + bbuf.write_int_pad2(offset_hours); + bbuf.write_ascii_char(b':'); + bbuf.write_int_pad2(offset_minutes); + } + + /// Formats the given offset into the writer given. + /// + /// If the given offset has non-zero seconds, then they are rounded to + /// the nearest minute. + fn print_offset_rounded_wtr( + &self, + offset: &Offset, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + wtr.write_ascii_char(if offset.is_negative() { b'-' } else { b'+' })?; + let (offset_hours, offset_minutes) = offset.round_to_nearest_minute(); + wtr.write_int_pad2(offset_hours)?; + wtr.write_ascii_char(b':')?; + wtr.write_int_pad2(offset_minutes)?; + Ok(()) + } + + /// Formats the given offset into the writer given. + /// + /// If the given offset has non-zero seconds, then they are emitted as a + /// third `:`-delimited component of the offset. If seconds are zero, then + /// only the hours and minute components are emitted. + fn print_offset_full_precision( + &self, + offset: &Offset, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + wtr.write_ascii_char(if offset.is_negative() { b'-' } else { b'+' })?; + let hours = offset.part_hours().unsigned_abs(); + let minutes = offset.part_minutes().unsigned_abs(); + let seconds = offset.part_seconds().unsigned_abs(); + wtr.write_int_pad2(hours)?; + wtr.write_ascii_char(b':')?; + wtr.write_int_pad2(minutes)?; + if seconds > 0 { + wtr.write_ascii_char(b':')?; + wtr.write_int_pad2(seconds)?; + } + Ok(()) + } + + /// Prints the "zulu" indicator. + /// + /// This should only be used when the offset is not known. For example, + /// when printing a `Timestamp`. + fn print_zulu_buf(&self, bbuf: &mut BorrowedBuffer<'_>) { + bbuf.write_ascii_char(if self.lowercase { b'z' } else { b'Z' }); + } + + /// Prints the "zulu" indicator. + /// + /// This should only be used when the offset is not known. For example, + /// when printing a `Timestamp`. + fn print_zulu_wtr( + &self, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + wtr.write_ascii_char(if self.lowercase { b'z' } else { b'Z' }) + } + + /// Formats the given time zone name into the writer given as an RFC 9557 + /// time zone annotation. + /// + /// When the given time zone is not an IANA time zone name, then the offset + /// is printed instead. (This means the offset will be printed twice, which + /// is indeed an intended behavior of RFC 9557 for cases where a time zone + /// name is not used or unavailable.) + fn print_time_zone_annotation_buf( + &self, + time_zone: &TimeZone, + offset: &Offset, + bbuf: &mut BorrowedBuffer<'_>, + ) { + bbuf.write_ascii_char(b'['); + if let Some(iana_name) = time_zone.iana_name() { + bbuf.write_str(iana_name); + } else { + self.print_offset_rounded_buf(offset, bbuf); + } + bbuf.write_ascii_char(b']'); + } + + /// Formats the given time zone name into the writer given as an RFC 9557 + /// time zone annotation. + /// + /// When the given time zone is not an IANA time zone name, then the offset + /// is printed instead. (This means the offset will be printed twice, which + /// is indeed an intended behavior of RFC 9557 for cases where a time zone + /// name is not used or unavailable.) + fn print_time_zone_annotation_wtr( + &self, + time_zone: &TimeZone, + offset: &Offset, + wtr: &mut BorrowedWriter<'_, '_, '_>, + ) -> Result<(), Error> { + wtr.write_ascii_char(b'[')?; + if let Some(iana_name) = time_zone.iana_name() { + wtr.write_str(iana_name)?; + } else { + self.print_offset_rounded_wtr(offset, wtr)?; + } + wtr.write_ascii_char(b']')?; + Ok(()) + } +} + +impl Default for DateTimePrinter { + fn default() -> DateTimePrinter { + DateTimePrinter::new() + } +} + +/// A printer for Temporal spans. +/// +/// Note that in Temporal, a "span" is called a "duration." +#[derive(Debug)] +pub(super) struct SpanPrinter { + /// The designators to use. + designators: &'static Designators, +} + +impl SpanPrinter { + /// Create a new Temporal span printer with the default configuration. + pub(super) const fn new() -> SpanPrinter { + SpanPrinter { designators: DESIGNATORS_UPPERCASE } + } + + /// Use lowercase for unit designator labels. + /// + /// By default, unit designator labels are written in uppercase. + pub(super) const fn lowercase(self, yes: bool) -> SpanPrinter { + SpanPrinter { + designators: if yes { + DESIGNATORS_LOWERCASE + } else { + DESIGNATORS_UPPERCASE + }, + } + } + + /// Print the given span to the writer given. + /// + /// This only returns an error when the given writer returns an error. + pub(super) fn print_span( + &self, + span: &Span, + wtr: &mut dyn Write, + ) -> Result<(), Error> { + let mut buf = ArrayBuffer::::default(); + let mut bbuf = buf.as_borrowed(); + self.print_span_impl(span, &mut bbuf); + wtr.write_str(bbuf.filled()) + } + + fn print_span_impl(&self, span: &Span, bbuf: &mut BorrowedBuffer<'_>) { + static SUBSECOND: UnitSet = UnitSet::from_slice(&[ + Unit::Millisecond, + Unit::Microsecond, + Unit::Nanosecond, + ]); + + if span.is_negative() { + bbuf.write_ascii_char(b'-'); + } + bbuf.write_ascii_char(b'P'); + + let units = span.units(); + if units.contains(Unit::Year) { + bbuf.write_int(span.get_years_unsigned()); + bbuf.write_ascii_char(self.label(Unit::Year)); + } + if units.contains(Unit::Month) { + bbuf.write_int(span.get_months_unsigned()); + bbuf.write_ascii_char(self.label(Unit::Month)); + } + if units.contains(Unit::Week) { + bbuf.write_int(span.get_weeks_unsigned()); + bbuf.write_ascii_char(self.label(Unit::Week)); + } + if units.contains(Unit::Day) { + bbuf.write_int(span.get_days_unsigned()); + bbuf.write_ascii_char(self.label(Unit::Day)); + } + + if units.only_time().is_empty() { + if units.only_calendar().is_empty() { + bbuf.write_ascii_char(b'T'); + bbuf.write_ascii_char(b'0'); + bbuf.write_ascii_char(self.label(Unit::Second)); + } + return; + } + + bbuf.write_ascii_char(b'T'); + + if units.contains(Unit::Hour) { + bbuf.write_int(span.get_hours_unsigned()); + bbuf.write_ascii_char(self.label(Unit::Hour)); + } + if units.contains(Unit::Minute) { + bbuf.write_int(span.get_minutes_unsigned()); + bbuf.write_ascii_char(self.label(Unit::Minute)); + } + + // ISO 8601 (and Temporal) don't support writing out milliseconds, + // microseconds or nanoseconds as separate components like for all + // the other units. Instead, they must be incorporated as fractional + // seconds. But we only want to do that work if we need to. + let has_subsecond = !units.intersection(SUBSECOND).is_empty(); + if units.contains(Unit::Second) && !has_subsecond { + bbuf.write_int(span.get_seconds_unsigned()); + bbuf.write_ascii_char(self.label(Unit::Second)); + } else if has_subsecond { + // We want to combine our seconds, milliseconds, microseconds and + // nanoseconds into one single value in terms of nanoseconds. Then + // we can "balance" that out so that we have a number of seconds + // and a number of nanoseconds not greater than 1 second. (Which is + // our fraction.) + let (seconds, millis, micros, nanos) = ( + Duration::from_secs(span.get_seconds_unsigned()), + Duration::from_millis(span.get_milliseconds_unsigned()), + Duration::from_micros(span.get_microseconds_unsigned()), + Duration::from_nanos(span.get_nanoseconds_unsigned()), + ); + // OK because the maximums for a span's seconds, millis, micros + // and nanos combined all fit into a 96-bit integer. (This is + // guaranteed by `Span::to_duration_invariant`.) + let total = seconds + millis + micros + nanos; + let (secs, subsecs) = (total.as_secs(), total.subsec_nanos()); + bbuf.write_int(secs); + if subsecs != 0 { + bbuf.write_ascii_char(b'.'); + bbuf.write_fraction(None, subsecs); + } + bbuf.write_ascii_char(self.label(Unit::Second)); + } + } + + /// Print the given signed duration to the writer given. + /// + /// This only returns an error when the given writer returns an error. + pub(super) fn print_signed_duration( + &self, + dur: &SignedDuration, + wtr: &mut dyn Write, + ) -> Result<(), Error> { + let mut buf = ArrayBuffer::::default(); + let mut bbuf = buf.as_borrowed(); + if dur.is_negative() { + bbuf.write_ascii_char(b'-'); + } + self.print_unsigned_duration_impl(&dur.unsigned_abs(), &mut bbuf); + wtr.write_str(bbuf.filled()) + } + + /// Print the given unsigned duration to the writer given. + /// + /// This only returns an error when the given writer returns an error. + pub(super) fn print_unsigned_duration( + &self, + dur: &Duration, + wtr: &mut dyn Write, + ) -> Result<(), Error> { + let mut buf = ArrayBuffer::::default(); + let mut bbuf = buf.as_borrowed(); + self.print_unsigned_duration_impl(dur, &mut bbuf); + wtr.write_str(bbuf.filled()) + } + + fn print_unsigned_duration_impl( + &self, + dur: &Duration, + bbuf: &mut BorrowedBuffer<'_>, + ) { + bbuf.write_ascii_char(b'P'); + bbuf.write_ascii_char(b'T'); + + let (mut secs, nanos) = (dur.as_secs(), dur.subsec_nanos()); + let non_zero_greater_than_second = secs >= 60; + if non_zero_greater_than_second { + let hours = secs / (60 * 60); + secs %= 60 * 60; + let minutes = secs / 60; + secs = secs % 60; + if hours != 0 { + bbuf.write_int(hours); + bbuf.write_ascii_char(self.label(Unit::Hour)); + } + if minutes != 0 { + bbuf.write_int(minutes); + bbuf.write_ascii_char(self.label(Unit::Minute)); + } + } + if !non_zero_greater_than_second || secs != 0 || nanos != 0 { + bbuf.write_int(secs); + if nanos != 0 { + bbuf.write_ascii_char(b'.'); + bbuf.write_fraction(None, nanos); + } + bbuf.write_ascii_char(self.label(Unit::Second)); + } + } + + /// Converts the ASCII uppercase unit designator label to lowercase if this + /// printer is configured to use lowercase. Otherwise the label is returned + /// unchanged. + fn label(&self, unit: Unit) -> u8 { + self.designators.designator(unit) + } +} + +#[derive(Clone, Debug)] +struct Designators { + // Indexed by `Unit as usize` + map: [u8; 10], +} + +const DESIGNATORS_UPPERCASE: &'static Designators = &Designators { + // N.B. ISO 8601 duration format doesn't have unit + // designators for sub-second units. + map: [0, 0, 0, b'S', b'M', b'H', b'D', b'W', b'M', b'Y'], +}; + +const DESIGNATORS_LOWERCASE: &'static Designators = &Designators { + // N.B. ISO 8601 duration format doesn't have unit + // designators for sub-second units. + map: [0, 0, 0, b's', b'm', b'h', b'd', b'w', b'm', b'y'], +}; + +impl Designators { + /// Returns the designator for the given unit. + fn designator(&self, unit: Unit) -> u8 { + self.map[unit as usize] + } +} + +#[cfg(feature = "alloc")] +#[cfg(test)] +mod tests { + use alloc::string::{String, ToString}; + + use crate::{ + civil::{date, time, Weekday}, + fmt::StdFmtWrite, + span::ToSpan, + util::b, + }; + + use super::*; + + #[test] + fn print_zoned() { + if crate::tz::db().is_definitively_empty() { + return; + } + + let p = || DateTimePrinter::new(); + let via_io = |dtp: DateTimePrinter, zdt| { + let mut buf = String::new(); + dtp.print_zoned(&zdt, &mut StdFmtWrite(&mut buf)).unwrap(); + buf + }; + let to_string = |dtp: DateTimePrinter, zdt| { + let mut buf = String::new(); + dtp.print_zoned(&zdt, &mut buf).unwrap(); + let got_via_io = via_io(dtp, zdt); + assert_eq!( + buf, got_via_io, + "expected writes to `&mut String` to match `&mut StdFmtWrite`" + ); + buf + }; + + let dt = date(2024, 3, 10).at(5, 34, 45, 0); + let zdt = dt.in_tz("America/New_York").unwrap(); + let got = to_string(p(), zdt); + assert_eq!(got, "2024-03-10T05:34:45-04:00[America/New_York]"); + + let dt = date(2024, 3, 10).at(5, 34, 45, 0); + let zdt = dt.in_tz("America/New_York").unwrap(); + let zdt = zdt.with_time_zone(TimeZone::UTC); + let got = to_string(p(), zdt); + assert_eq!(got, "2024-03-10T09:34:45+00:00[UTC]"); + + let dt = date(2024, 3, 10).at(5, 34, 45, 0); + let zdt = dt.to_zoned(TimeZone::fixed(Offset::MIN)).unwrap(); + let got = to_string(p(), zdt); + assert_eq!(got, "2024-03-10T05:34:45-25:59[-25:59]"); + + let dt = date(2024, 3, 10).at(5, 34, 45, 0); + let zdt = dt.to_zoned(TimeZone::fixed(Offset::MAX)).unwrap(); + let got = to_string(p(), zdt); + assert_eq!(got, "2024-03-10T05:34:45+25:59[+25:59]"); + + let dt = date(2024, 3, 10).at(5, 34, 45, 123_456_789); + let zdt = dt.in_tz("America/New_York").unwrap(); + let got = to_string(p(), zdt); + assert_eq!( + got, + "2024-03-10T05:34:45.123456789-04:00[America/New_York]" + ); + + let dt = date(2024, 3, 10).at(5, 34, 45, 0); + let zdt = dt.in_tz("America/New_York").unwrap(); + let got = to_string(p().precision(Some(9)), zdt); + assert_eq!( + got, + "2024-03-10T05:34:45.000000000-04:00[America/New_York]" + ); + + let dt = date(-9999, 3, 1).at(23, 59, 59, 999_999_999); + let zdt = dt.in_tz("America/Argentina/ComodRivadavia").unwrap(); + let got = to_string(p().precision(Some(9)), zdt); + assert_eq!( + got, + "-009999-03-01T23:59:59.999999999-04:23[America/Argentina/ComodRivadavia]", + ); + + // Inject a very long IANA tzdb identifier to ensure it's handled + // properly. + let tz = TimeZone::tzif( + "Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz", + crate::tz::testdata::TzifTestFile::get("America/New_York").data, + ).unwrap(); + let dt = date(-9999, 3, 1).at(23, 59, 59, 999_999_999); + let zdt = dt.to_zoned(tz).unwrap(); + let got = to_string(p().precision(Some(9)), zdt); + assert_eq!( + got, + "-009999-03-01T23:59:59.999999999-04:56[Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz]", + ); + } + + #[test] + fn print_timestamp_zulu() { + let p = || DateTimePrinter::new(); + let via_io = |dtp: DateTimePrinter, ts| { + let mut buf = String::new(); + dtp.print_timestamp(&ts, &mut StdFmtWrite(&mut buf)).unwrap(); + buf + }; + let to_string = |dtp: DateTimePrinter, ts| { + let mut buf = String::new(); + dtp.print_timestamp(&ts, &mut buf).unwrap(); + let got_via_io = via_io(dtp, ts); + assert_eq!( + buf, got_via_io, + "expected writes to `&mut String` to match `&mut StdFmtWrite`" + ); + buf + }; + + let tz = TimeZone::fixed(-Offset::constant(4)); + + let dt = date(2024, 3, 10).at(5, 34, 45, 0); + let zoned: Zoned = dt.to_zoned(tz.clone()).unwrap(); + let got = to_string(p(), zoned.timestamp()); + assert_eq!(got, "2024-03-10T09:34:45Z"); + + let dt = date(-2024, 3, 10).at(5, 34, 45, 0); + let zoned: Zoned = dt.to_zoned(tz.clone()).unwrap(); + let got = to_string(p(), zoned.timestamp()); + assert_eq!(got, "-002024-03-10T09:34:45Z"); + + let dt = date(2024, 3, 10).at(5, 34, 45, 123_456_789); + let zoned: Zoned = dt.to_zoned(tz.clone()).unwrap(); + let got = to_string(p(), zoned.timestamp()); + assert_eq!(got, "2024-03-10T09:34:45.123456789Z"); + + let dt = date(2024, 3, 10).at(5, 34, 45, 0); + let zoned: Zoned = dt.to_zoned(tz.clone()).unwrap(); + let got = to_string(p().precision(Some(9)), zoned.timestamp()); + assert_eq!(got, "2024-03-10T09:34:45.000000000Z"); + } + + #[test] + fn print_timestamp_with_offset() { + let p = || DateTimePrinter::new(); + let via_io = |dtp: DateTimePrinter, ts, offset| { + let mut buf = String::new(); + dtp.print_timestamp_with_offset( + &ts, + offset, + &mut StdFmtWrite(&mut buf), + ) + .unwrap(); + buf + }; + let to_string = |dtp: DateTimePrinter, ts, offset| { + let mut buf = String::new(); + dtp.print_timestamp_with_offset(&ts, offset, &mut buf).unwrap(); + let got_via_io = via_io(dtp, ts, offset); + assert_eq!( + buf, got_via_io, + "expected writes to `&mut String` to match `&mut StdFmtWrite`" + ); + buf + }; + + let tz = TimeZone::fixed(-Offset::constant(4)); + + let dt = date(2024, 3, 10).at(5, 34, 45, 0); + let zoned: Zoned = dt.to_zoned(tz.clone()).unwrap(); + let got = to_string(p(), zoned.timestamp(), zoned.offset()); + assert_eq!(got, "2024-03-10T05:34:45-04:00"); + + let dt = date(-2024, 3, 10).at(5, 34, 45, 0); + let zoned: Zoned = dt.to_zoned(tz.clone()).unwrap(); + let got = to_string(p(), zoned.timestamp(), zoned.offset()); + assert_eq!(got, "-002024-03-10T05:34:45-04:00"); + + let dt = date(2024, 3, 10).at(5, 34, 45, 123_456_789); + let zoned: Zoned = dt.to_zoned(tz.clone()).unwrap(); + let got = to_string(p(), zoned.timestamp(), zoned.offset()); + assert_eq!(got, "2024-03-10T05:34:45.123456789-04:00"); + + let dt = date(2024, 3, 10).at(5, 34, 45, 0); + let zoned: Zoned = dt.to_zoned(tz.clone()).unwrap(); + let got = to_string( + p().precision(Some(9)), + zoned.timestamp(), + zoned.offset(), + ); + assert_eq!(got, "2024-03-10T05:34:45.000000000-04:00"); + + let dt = DateTime::MIN; + let zoned: Zoned = dt.to_zoned(TimeZone::fixed(Offset::MIN)).unwrap(); + let got = to_string( + p().precision(Some(9)), + zoned.timestamp(), + zoned.offset(), + ); + assert_eq!(got, "-009999-01-01T00:00:00.000000000-25:59"); + } + + #[test] + fn print_datetime() { + let p = || DateTimePrinter::new(); + let via_io = |dtp: DateTimePrinter, dt| { + let mut buf = String::new(); + dtp.print_datetime(&dt, &mut StdFmtWrite(&mut buf)).unwrap(); + buf + }; + let to_string = |dtp: DateTimePrinter, dt| { + let mut buf = String::new(); + dtp.print_datetime(&dt, &mut buf).unwrap(); + let got_via_io = via_io(dtp, dt); + assert_eq!( + buf, got_via_io, + "expected writes to `&mut String` to match `&mut StdFmtWrite`" + ); + buf + }; + + let dt = date(2024, 3, 10).at(5, 34, 45, 0); + let got = to_string(p(), dt); + assert_eq!(got, "2024-03-10T05:34:45"); + + let dt = date(-2024, 3, 10).at(5, 34, 45, 0); + let got = to_string(p(), dt); + assert_eq!(got, "-002024-03-10T05:34:45"); + + let dt = date(2024, 3, 10).at(5, 34, 45, 123_456_789); + let got = to_string(p(), dt); + assert_eq!(got, "2024-03-10T05:34:45.123456789"); + + let dt = date(2024, 3, 10).at(5, 34, 45, 0); + let got = to_string(p().precision(Some(9)), dt); + assert_eq!(got, "2024-03-10T05:34:45.000000000"); + + let dt = DateTime::MIN; + let got = to_string(p().precision(Some(9)), dt); + assert_eq!(got, "-009999-01-01T00:00:00.000000000"); + } + + #[test] + fn print_date() { + let p = || DateTimePrinter::new(); + let via_io = |dtp: DateTimePrinter, date| { + let mut buf = String::new(); + dtp.print_date(&date, &mut StdFmtWrite(&mut buf)).unwrap(); + buf + }; + let to_string = |dtp: DateTimePrinter, date| { + let mut buf = String::new(); + dtp.print_date(&date, &mut buf).unwrap(); + let got_via_io = via_io(dtp, date); + assert_eq!( + buf, got_via_io, + "expected writes to `&mut String` to match `&mut StdFmtWrite`" + ); + buf + }; + + let d = date(2024, 3, 10); + let got = to_string(p(), d); + assert_eq!(got, "2024-03-10"); + + let d = date(-2024, 3, 10); + let got = to_string(p(), d); + assert_eq!(got, "-002024-03-10"); + + let d = date(2024, 3, 10); + let got = to_string(p(), d); + assert_eq!(got, "2024-03-10"); + + let d = Date::MIN; + let got = to_string(p().precision(Some(9)), d); + assert_eq!(got, "-009999-01-01"); + } + + #[test] + fn print_time() { + let p = || DateTimePrinter::new(); + let via_io = |dtp: DateTimePrinter, time| { + let mut buf = String::new(); + dtp.print_time(&time, &mut StdFmtWrite(&mut buf)).unwrap(); + buf + }; + let to_string = |dtp: DateTimePrinter, time| { + let mut buf = String::new(); + dtp.print_time(&time, &mut buf).unwrap(); + let got_via_io = via_io(dtp, time); + assert_eq!( + buf, got_via_io, + "expected writes to `&mut String` to match `&mut StdFmtWrite`" + ); + buf + }; + + let t = time(5, 34, 45, 0); + let got = to_string(p(), t); + assert_eq!(got, "05:34:45"); + + let t = time(5, 34, 45, 0); + let got = to_string(p(), t); + assert_eq!(got, "05:34:45"); + + let t = time(5, 34, 45, 123_456_789); + let got = to_string(p(), t); + assert_eq!(got, "05:34:45.123456789"); + + let t = time(5, 34, 45, 0); + let got = to_string(p().precision(Some(9)), t); + assert_eq!(got, "05:34:45.000000000"); + + let t = Time::MIN; + let got = to_string(p().precision(Some(9)), t); + assert_eq!(got, "00:00:00.000000000"); + + let t = Time::MAX; + let got = to_string(p().precision(Some(9)), t); + assert_eq!(got, "23:59:59.999999999"); + } + + #[test] + fn print_iso_week_date() { + let p = || DateTimePrinter::new(); + let via_io = |dtp: DateTimePrinter, date| { + let mut buf = String::new(); + dtp.print_iso_week_date(&date, &mut StdFmtWrite(&mut buf)) + .unwrap(); + buf + }; + let to_string = |dtp: DateTimePrinter, date| { + let mut buf = String::new(); + dtp.print_iso_week_date(&date, &mut buf).unwrap(); + let got_via_io = via_io(dtp, date); + assert_eq!( + buf, got_via_io, + "expected writes to `&mut String` to match `&mut StdFmtWrite`" + ); + buf + }; + + let d = ISOWeekDate::new(2024, 52, Weekday::Monday).unwrap(); + let got = to_string(p(), d); + assert_eq!(got, "2024-W52-1"); + + let d = ISOWeekDate::new(2004, 1, Weekday::Sunday).unwrap(); + let got = to_string(p(), d); + assert_eq!(got, "2004-W01-7"); + + let d = ISOWeekDate::MIN; + let got = to_string(p(), d); + assert_eq!(got, "-009999-W01-1"); + + let d = ISOWeekDate::MAX; + let got = to_string(p(), d); + assert_eq!(got, "9999-W52-5"); + } + + #[test] + fn print_pieces() { + let p = || DateTimePrinter::new(); + let via_io = |dtp: DateTimePrinter, pieces| { + let mut buf = String::new(); + dtp.print_pieces(&pieces, &mut StdFmtWrite(&mut buf)).unwrap(); + buf + }; + let to_string = |dtp: DateTimePrinter, pieces| { + let mut buf = String::new(); + dtp.print_pieces(&pieces, &mut buf).unwrap(); + let got_via_io = via_io(dtp, pieces); + assert_eq!( + buf, got_via_io, + "expected writes to `&mut String` to match `&mut StdFmtWrite`" + ); + buf + }; + + let pieces = Pieces::from(date(2024, 3, 10).at(5, 34, 45, 0)) + .with_offset(Offset::constant(-4)) + .with_time_zone_name("America/New_York"); + let got = to_string(p(), pieces); + assert_eq!(got, "2024-03-10T05:34:45-04:00[America/New_York]"); + + let pieces = Pieces::from(date(2024, 3, 10).at(5, 34, 45, 0)) + .with_offset(Offset::UTC) + .with_time_zone_name("UTC"); + let got = to_string(p(), pieces); + assert_eq!(got, "2024-03-10T05:34:45+00:00[UTC]"); + + let pieces = Pieces::from(date(2024, 3, 10).at(5, 34, 45, 0)) + .with_offset(Offset::MIN) + .with_time_zone_offset(Offset::MIN); + let got = to_string(p(), pieces); + assert_eq!(got, "2024-03-10T05:34:45-25:59[-25:59]"); + + let pieces = Pieces::from(date(2024, 3, 10).at(5, 34, 45, 0)) + .with_offset(Offset::MAX) + .with_time_zone_offset(Offset::MAX); + let got = to_string(p(), pieces); + assert_eq!(got, "2024-03-10T05:34:45+25:59[+25:59]"); + + let pieces = + Pieces::from(date(2024, 3, 10).at(5, 34, 45, 123_456_789)) + .with_offset(Offset::constant(-4)) + .with_time_zone_name("America/New_York"); + let got = to_string(p(), pieces); + assert_eq!( + got, + "2024-03-10T05:34:45.123456789-04:00[America/New_York]" + ); + + let pieces = Pieces::from(date(2024, 3, 10).at(5, 34, 45, 0)) + .with_offset(Offset::constant(-4)) + .with_time_zone_name("America/New_York"); + let got = to_string(p().precision(Some(9)), pieces); + assert_eq!( + got, + "2024-03-10T05:34:45.000000000-04:00[America/New_York]" + ); + + let pieces = + Pieces::from(date(-9999, 3, 1).at(23, 59, 59, 999_999_999)) + .with_offset(Offset::constant(-4)) + .with_time_zone_name("America/Argentina/ComodRivadavia"); + let got = to_string(p().precision(Some(9)), pieces); + assert_eq!( + got, + "-009999-03-01T23:59:59.999999999-04:00[America/Argentina/ComodRivadavia]", + ); + + let pieces = + Pieces::parse( + "-009999-03-01T23:59:59.999999999-04:00[!America/Argentina/ComodRivadavia]", + ).unwrap(); + let got = to_string(p().precision(Some(9)), pieces); + assert_eq!( + got, + "-009999-03-01T23:59:59.999999999-04:00[!America/Argentina/ComodRivadavia]", + ); + + // Inject a very long IANA tzdb identifier to ensure it's handled + let name = "Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz"; + let pieces = + Pieces::from(date(-9999, 3, 1).at(23, 59, 59, 999_999_999)) + .with_offset(Offset::constant(-4)) + .with_time_zone_name(name); + let got = to_string(p().precision(Some(9)), pieces); + assert_eq!( + got, + "-009999-03-01T23:59:59.999999999-04:00[Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz]", + ); + } + + #[test] + fn print_time_zone() { + let p = || DateTimePrinter::new(); + let via_io = |dtp: DateTimePrinter, tz| { + let mut buf = String::new(); + dtp.print_time_zone(&tz, &mut StdFmtWrite(&mut buf)).unwrap(); + buf + }; + let to_string = |dtp: DateTimePrinter, tz| { + let mut buf = String::new(); + dtp.print_time_zone(&tz, &mut buf).unwrap(); + let got_via_io = via_io(dtp, tz); + assert_eq!( + buf, got_via_io, + "expected writes to `&mut String` to match `&mut StdFmtWrite`" + ); + buf + }; + + let tztest = + crate::tz::testdata::TzifTestFile::get("America/New_York"); + let tz = TimeZone::tzif(tztest.name, tztest.data).unwrap(); + let got = to_string(p(), tz); + assert_eq!(got, "America/New_York"); + + // Inject a very long IANA tzdb identifier to ensure it's handled + // properly. + let tz = TimeZone::tzif( + "Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz", + tztest.data, + ).unwrap(); + let got = to_string(p(), tz); + assert_eq!( + got, + "Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz/Abc/Def/Ghi/Jkl/Mno/Pqr/Stu/Vwx/Yzz", + ); + + let tz = TimeZone::UTC; + let got = to_string(p(), tz); + assert_eq!(got, "UTC"); + + let tz = TimeZone::unknown(); + let got = to_string(p(), tz); + assert_eq!(got, "Etc/Unknown"); + + let tz = TimeZone::fixed(Offset::MIN); + let got = to_string(p(), tz); + assert_eq!(got, "-25:59:59"); + + let tz = TimeZone::fixed(Offset::MAX); + let got = to_string(p(), tz); + assert_eq!(got, "+25:59:59"); + + let tz = TimeZone::posix("EST5EDT,M3.2.0,M11.1.0").unwrap(); + let got = to_string(p(), tz); + assert_eq!(got, "EST5EDT,M3.2.0,M11.1.0"); + + let tz = TimeZone::posix("EST5EDT5,M3.2.0,M11.1.0").unwrap(); + let got = to_string(p(), tz); + assert_eq!(got, "EST5EDT5,M3.2.0,M11.1.0"); + + let tz = TimeZone::posix("EST5EDT,J1,J365/5:12:34").unwrap(); + let got = to_string(p(), tz); + assert_eq!(got, "EST5EDT,J1,J365/5:12:34"); + + let tz = + TimeZone::posix("<+09>-9<-10>-10,M3.2.0/1:23:45,J365/-1").unwrap(); + let got = to_string(p(), tz); + assert_eq!(got, "<+09>-9<-10>,M3.2.0/1:23:45,J365/-1"); + + let tz = TimeZone::posix( + "ABCDEFGHIJKLMNOPQRSTUVWXY5ABCDEFGHIJKLMNOPQRSTUVWXYT,M3.2.0,M11.1.0", + ).unwrap(); + let got = to_string(p(), tz); + assert_eq!( + got, + "ABCDEFGHIJKLMNOPQRSTUVWXY5ABCDEFGHIJKLMNOPQRSTUVWXYT,M3.2.0,M11.1.0", + ); + + // This isn't a public API, but this lets us test the error + // case: a valid time zone but without a succinct name. + #[cfg(feature = "tz-system")] + { + let tz = TimeZone::tzif_system(tztest.data).unwrap(); + let mut buf = String::new(); + let err = p().print_time_zone(&tz, &mut buf).unwrap_err(); + assert_eq!( + err.to_string(), + "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`)", + ); + } + } + + #[cfg(not(miri))] + #[test] + fn print_span_basic() { + let p = |span: Span| -> String { + let mut buf = String::new(); + SpanPrinter::new().print_span(&span, &mut buf).unwrap(); + buf + }; + + insta::assert_snapshot!(p(Span::new()), @"PT0S"); + insta::assert_snapshot!(p(1.second()), @"PT1S"); + insta::assert_snapshot!(p(-1.second()), @"-PT1S"); + insta::assert_snapshot!(p( + 1.second().milliseconds(1).microseconds(1).nanoseconds(1), + ), @"PT1.001001001S"); + insta::assert_snapshot!(p( + 0.second().milliseconds(999).microseconds(999).nanoseconds(999), + ), @"PT0.999999999S"); + insta::assert_snapshot!(p( + 1.year().months(1).weeks(1).days(1) + .hours(1).minutes(1).seconds(1) + .milliseconds(1).microseconds(1).nanoseconds(1), + ), @"P1Y1M1W1DT1H1M1.001001001S"); + insta::assert_snapshot!(p( + -1.year().months(1).weeks(1).days(1) + .hours(1).minutes(1).seconds(1) + .milliseconds(1).microseconds(1).nanoseconds(1), + ), @"-P1Y1M1W1DT1H1M1.001001001S"); + } + + #[cfg(not(miri))] + #[test] + fn print_span_subsecond_positive() { + let p = |span: Span| -> String { + let mut buf = String::new(); + SpanPrinter::new().print_span(&span, &mut buf).unwrap(); + buf + }; + + // These are all sub-second trickery tests. + insta::assert_snapshot!(p( + 0.second().milliseconds(1000).microseconds(1000).nanoseconds(1000), + ), @"PT1.001001S"); + insta::assert_snapshot!(p( + 1.second().milliseconds(1000).microseconds(1000).nanoseconds(1000), + ), @"PT2.001001S"); + insta::assert_snapshot!(p( + 0.second() + .milliseconds(b::SpanMilliseconds::MAX), + ), @"PT631107417600S"); + insta::assert_snapshot!(p( + 0.second() + .microseconds(b::SpanMicroseconds::MAX), + ), @"PT631107417600S"); + insta::assert_snapshot!(p( + 0.second() + .nanoseconds(b::SpanNanoseconds::MAX), + ), @"PT9223372036.854775807S"); + + insta::assert_snapshot!(p( + 0.second() + .milliseconds(b::SpanMilliseconds::MAX) + .microseconds(999_999), + ), @"PT631107417600.999999S"); + // This is 1 microsecond more than the maximum number of seconds + // representable in a span. + insta::assert_snapshot!(p( + 0.second() + .milliseconds(b::SpanMilliseconds::MAX) + .microseconds(1_000_000), + ), @"PT631107417601S"); + insta::assert_snapshot!(p( + 0.second() + .milliseconds(b::SpanMilliseconds::MAX) + .microseconds(1_000_001), + ), @"PT631107417601.000001S"); + // This is 1 nanosecond more than the maximum number of seconds + // representable in a span. + insta::assert_snapshot!(p( + 0.second() + .milliseconds(b::SpanMilliseconds::MAX) + .nanoseconds(1_000_000_000), + ), @"PT631107417601S"); + insta::assert_snapshot!(p( + 0.second() + .milliseconds(b::SpanMilliseconds::MAX) + .nanoseconds(1_000_000_001), + ), @"PT631107417601.000000001S"); + + // The max millis, micros and nanos, combined. + insta::assert_snapshot!(p( + 0.second() + .milliseconds(b::SpanMilliseconds::MAX) + .microseconds(b::SpanMicroseconds::MAX) + .nanoseconds(b::SpanNanoseconds::MAX), + ), @"PT1271438207236.854775807S"); + // The max seconds, millis, micros and nanos, combined. + insta::assert_snapshot!(p( + Span::new() + .seconds(b::SpanSeconds::MAX) + .milliseconds(b::SpanMilliseconds::MAX) + .microseconds(b::SpanMicroseconds::MAX) + .nanoseconds(b::SpanNanoseconds::MAX), + ), @"PT1902545624836.854775807S"); + } + + #[cfg(not(miri))] + #[test] + fn print_span_subsecond_negative() { + let p = |span: Span| -> String { + let mut buf = String::new(); + SpanPrinter::new().print_span(&span, &mut buf).unwrap(); + buf + }; + + // These are all sub-second trickery tests. + insta::assert_snapshot!(p( + -0.second().milliseconds(1000).microseconds(1000).nanoseconds(1000), + ), @"-PT1.001001S"); + insta::assert_snapshot!(p( + -1.second().milliseconds(1000).microseconds(1000).nanoseconds(1000), + ), @"-PT2.001001S"); + insta::assert_snapshot!(p( + 0.second() + .milliseconds(b::SpanMilliseconds::MIN), + ), @"-PT631107417600S"); + insta::assert_snapshot!(p( + 0.second() + .microseconds(b::SpanMicroseconds::MIN), + ), @"-PT631107417600S"); + insta::assert_snapshot!(p( + 0.second() + .nanoseconds(b::SpanNanoseconds::MIN), + ), @"-PT9223372036.854775807S"); + + insta::assert_snapshot!(p( + 0.second() + .milliseconds(b::SpanMilliseconds::MIN) + .microseconds(999_999), + ), @"-PT631107417600.999999S"); + // This is 1 microsecond more than the maximum number of seconds + // representable in a span. + insta::assert_snapshot!(p( + 0.second() + .milliseconds(b::SpanMilliseconds::MIN) + .microseconds(1_000_000), + ), @"-PT631107417601S"); + insta::assert_snapshot!(p( + 0.second() + .milliseconds(b::SpanMilliseconds::MIN) + .microseconds(1_000_001), + ), @"-PT631107417601.000001S"); + // This is 1 nanosecond more than the maximum number of seconds + // representable in a span. + insta::assert_snapshot!(p( + 0.second() + .milliseconds(b::SpanMilliseconds::MIN) + .nanoseconds(1_000_000_000), + ), @"-PT631107417601S"); + insta::assert_snapshot!(p( + 0.second() + .milliseconds(b::SpanMilliseconds::MIN) + .nanoseconds(1_000_000_001), + ), @"-PT631107417601.000000001S"); + + // The max millis, micros and nanos, combined. + insta::assert_snapshot!(p( + 0.second() + .milliseconds(b::SpanMilliseconds::MIN) + .microseconds(b::SpanMicroseconds::MIN) + .nanoseconds(b::SpanNanoseconds::MIN), + ), @"-PT1271438207236.854775807S"); + // The max seconds, millis, micros and nanos, combined. + insta::assert_snapshot!(p( + Span::new() + .seconds(b::SpanSeconds::MIN) + .milliseconds(b::SpanMilliseconds::MIN) + .microseconds(b::SpanMicroseconds::MIN) + .nanoseconds(b::SpanNanoseconds::MIN), + ), @"-PT1902545624836.854775807S"); + } + + #[cfg(not(miri))] + #[test] + fn print_signed_duration() { + let p = |secs, nanos| -> String { + let dur = SignedDuration::new(secs, nanos); + let mut buf = String::new(); + SpanPrinter::new().print_signed_duration(&dur, &mut buf).unwrap(); + buf + }; + + insta::assert_snapshot!(p(0, 0), @"PT0S"); + insta::assert_snapshot!(p(0, 1), @"PT0.000000001S"); + insta::assert_snapshot!(p(1, 0), @"PT1S"); + insta::assert_snapshot!(p(59, 0), @"PT59S"); + insta::assert_snapshot!(p(60, 0), @"PT1M"); + insta::assert_snapshot!(p(60, 1), @"PT1M0.000000001S"); + insta::assert_snapshot!(p(61, 1), @"PT1M1.000000001S"); + insta::assert_snapshot!(p(3_600, 0), @"PT1H"); + insta::assert_snapshot!(p(3_600, 1), @"PT1H0.000000001S"); + insta::assert_snapshot!(p(3_660, 0), @"PT1H1M"); + insta::assert_snapshot!(p(3_660, 1), @"PT1H1M0.000000001S"); + insta::assert_snapshot!(p(3_661, 0), @"PT1H1M1S"); + insta::assert_snapshot!(p(3_661, 1), @"PT1H1M1.000000001S"); + + insta::assert_snapshot!(p(0, -1), @"-PT0.000000001S"); + insta::assert_snapshot!(p(-1, 0), @"-PT1S"); + insta::assert_snapshot!(p(-59, 0), @"-PT59S"); + insta::assert_snapshot!(p(-60, 0), @"-PT1M"); + insta::assert_snapshot!(p(-60, -1), @"-PT1M0.000000001S"); + insta::assert_snapshot!(p(-61, -1), @"-PT1M1.000000001S"); + insta::assert_snapshot!(p(-3_600, 0), @"-PT1H"); + insta::assert_snapshot!(p(-3_600, -1), @"-PT1H0.000000001S"); + insta::assert_snapshot!(p(-3_660, 0), @"-PT1H1M"); + insta::assert_snapshot!(p(-3_660, -1), @"-PT1H1M0.000000001S"); + insta::assert_snapshot!(p(-3_661, 0), @"-PT1H1M1S"); + insta::assert_snapshot!(p(-3_661, -1), @"-PT1H1M1.000000001S"); + + insta::assert_snapshot!( + p(i64::MIN, -999_999_999), + @"-PT2562047788015215H30M8.999999999S", + ); + insta::assert_snapshot!( + p(i64::MAX, 999_999_999), + @"PT2562047788015215H30M7.999999999S", + ); + } + + #[cfg(not(miri))] + #[test] + fn print_unsigned_duration() { + let p = |secs, nanos| -> String { + let dur = Duration::new(secs, nanos); + let mut buf = String::new(); + SpanPrinter::new() + .print_unsigned_duration(&dur, &mut buf) + .unwrap(); + buf + }; + + insta::assert_snapshot!(p(0, 0), @"PT0S"); + insta::assert_snapshot!(p(0, 1), @"PT0.000000001S"); + insta::assert_snapshot!(p(1, 0), @"PT1S"); + insta::assert_snapshot!(p(59, 0), @"PT59S"); + insta::assert_snapshot!(p(60, 0), @"PT1M"); + insta::assert_snapshot!(p(60, 1), @"PT1M0.000000001S"); + insta::assert_snapshot!(p(61, 1), @"PT1M1.000000001S"); + insta::assert_snapshot!(p(3_600, 0), @"PT1H"); + insta::assert_snapshot!(p(3_600, 1), @"PT1H0.000000001S"); + insta::assert_snapshot!(p(3_660, 0), @"PT1H1M"); + insta::assert_snapshot!(p(3_660, 1), @"PT1H1M0.000000001S"); + insta::assert_snapshot!(p(3_661, 0), @"PT1H1M1S"); + insta::assert_snapshot!(p(3_661, 1), @"PT1H1M1.000000001S"); + + insta::assert_snapshot!( + p(u64::MAX, 999_999_999), + @"PT5124095576030431H15.999999999S", + ); + } +}