From 7a8adc58e2c2277a988ceabedfcd54b58b37fb94 Mon Sep 17 00:00:00 2001 From: Socialpranker <273312799+Socialpranker@users.noreply.github.com> Date: Sat, 5 Sep 2026 14:37:53 +0200 Subject: [PATCH 1/2] fix(relative): add the fraction of a negative relative second count `Relative::Seconds` holds a floor decomposition, so -0.25 seconds is `(-1, 750_000_000)`. A `jiff::Span` is sign-uniform, so combining those two fields directly subtracted the fraction instead of adding it and the result was off by `2 * (1 - fraction)` seconds. Rebalance the fraction onto the sign of the whole seconds before building the span. --- src/items/relative.rs | 20 +++++++++++++++++--- tests/time.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/items/relative.rs b/src/items/relative.rs index 5ed3888..3bc9722 100644 --- a/src/items/relative.rs +++ b/src/items/relative.rs @@ -59,9 +59,23 @@ impl TryFrom for jiff::Span { Relative::Days(days) => jiff::Span::new().try_days(days), Relative::Hours(hours) => jiff::Span::new().try_hours(hours), Relative::Minutes(minutes) => jiff::Span::new().try_minutes(minutes), - Relative::Seconds(seconds, nanoseconds) => jiff::Span::new() - .try_seconds(seconds) - .and_then(|span| span.try_nanoseconds(nanoseconds)), + Relative::Seconds(seconds, nanoseconds) => { + // `Relative::Seconds` is a floor decomposition: the value is + // `seconds + nanoseconds / 1e9` with a non-negative fraction, so + // -0.25 seconds is held as `(-1, 750_000_000)`. A `jiff::Span` + // is sign-uniform, so the fraction has to be rebalanced onto the + // sign of the whole seconds first; combining the fields as they + // are would subtract the fraction instead of adding it. + let (seconds, nanoseconds) = if seconds < 0 && nanoseconds > 0 { + (seconds + 1, -i64::from(1_000_000_000 - nanoseconds)) + } else { + (seconds, i64::from(nanoseconds)) + }; + + jiff::Span::new() + .try_seconds(seconds) + .and_then(|span| span.try_nanoseconds(nanoseconds)) + } } .map_err(|_| "relative value is invalid") } diff --git a/tests/time.rs b/tests/time.rs index 8c18d92..d9336db 100644 --- a/tests/time.rs +++ b/tests/time.rs @@ -280,3 +280,39 @@ fn test_time_seconds_ago_invalid(#[case] input: &str) { "Input string '{input}' did not produce an error when parsing" ); } + +// Fractional relative seconds, checked against GNU date 9.11 with +// TZ=UTC date --date="2026-08-27 12:00:00 " +"%H:%M:%S.%N" +#[rstest] +#[case::plus_half("+0.5 sec", "12:00:00.500000000")] +#[case::minus_half("-0.5 sec", "11:59:59.500000000")] +#[case::plus_one_and_a_half("+1.5 sec", "12:00:01.500000000")] +#[case::minus_one_and_a_half("-1.5 sec", "11:59:58.500000000")] +#[case::plus_quarter("+0.25 sec", "12:00:00.250000000")] +#[case::minus_quarter("-0.25 sec", "11:59:59.750000000")] +#[case::plus_one_and_three_quarters("+1.75 sec", "12:00:01.750000000")] +#[case::minus_one_and_three_quarters("-1.75 sec", "11:59:58.250000000")] +#[case::plus_two_and_an_eighth("+2.125 sec", "12:00:02.125000000")] +#[case::minus_two_and_an_eighth("-2.125 sec", "11:59:57.875000000")] +#[case::minus_nine_nanoseconds("-0.000000009 sec", "11:59:59.999999991")] +#[case::minus_almost_one("-0.999999999 sec", "11:59:59.000000001")] +#[case::plus_half_ago("+0.5 sec ago", "11:59:59.500000000")] +#[case::minus_half_ago("-0.5 sec ago", "12:00:00.500000000")] +#[case::plus_one_and_a_half_ago("+1.5 sec ago", "11:59:58.500000000")] +#[case::unsigned_half_ago("0.5 sec ago", "11:59:59.500000000")] +#[case::unsigned_half("0.5 sec", "12:00:00.500000000")] +#[case::plus_zero_fraction("+0.0 sec", "12:00:00.000000000")] +#[case::minus_zero_fraction("-0.0 sec", "12:00:00.000000000")] +#[case::minus_whole_with_fraction_zero("-1.0 sec", "11:59:59.000000000")] +#[case::minus_whole("-1 second ago", "12:00:01.000000000")] +#[case::plural_minus_half("-0.5 seconds", "11:59:59.500000000")] +#[case::abbreviated_minus_half("-0.5 secs", "11:59:59.500000000")] +fn test_relative_fractional_seconds(#[case] input: &str, #[case] expected: &str) { + let base = "2026-08-27 12:00:00" + .parse::() + .unwrap() + .to_zoned(TimeZone::UTC) + .unwrap(); + + check_time(input, expected, "%H:%M:%S.%N", Some(base)); +} From 8270fba719980ff359efefbf096230925629ddeb Mon Sep 17 00:00:00 2001 From: Socialpranker <273312799+Socialpranker@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:37:39 +0200 Subject: [PATCH 2/2] fix(relative): shorten the fraction comment, align the GNU version to 9.11 --- src/items/relative.rs | 8 ++------ tests/time.rs | 6 +++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/items/relative.rs b/src/items/relative.rs index 3bc9722..e16f314 100644 --- a/src/items/relative.rs +++ b/src/items/relative.rs @@ -60,12 +60,8 @@ impl TryFrom for jiff::Span { Relative::Hours(hours) => jiff::Span::new().try_hours(hours), Relative::Minutes(minutes) => jiff::Span::new().try_minutes(minutes), Relative::Seconds(seconds, nanoseconds) => { - // `Relative::Seconds` is a floor decomposition: the value is - // `seconds + nanoseconds / 1e9` with a non-negative fraction, so - // -0.25 seconds is held as `(-1, 750_000_000)`. A `jiff::Span` - // is sign-uniform, so the fraction has to be rebalanced onto the - // sign of the whole seconds first; combining the fields as they - // are would subtract the fraction instead of adding it. + // -0.25 is stored as (-1, 750_000_000), but a `jiff::Span` needs + // both fields with one sign, so move the fraction onto it. let (seconds, nanoseconds) = if seconds < 0 && nanoseconds > 0 { (seconds + 1, -i64::from(1_000_000_000 - nanoseconds)) } else { diff --git a/tests/time.rs b/tests/time.rs index d9336db..bd5aec7 100644 --- a/tests/time.rs +++ b/tests/time.rs @@ -5,7 +5,7 @@ use jiff::{civil::DateTime, tz::TimeZone, Zoned}; use parse_datetime::parse_datetime_at_date; use rstest::rstest; -// The expected values are produced by GNU date version 8.32 +// The expected values are produced by GNU date version 9.11 // export LC_TIME=en_US.UTF-8 // export TZ=UTC // date date --date="12:34:56+09:00" +"%H:%M:%S.%N" @@ -281,8 +281,8 @@ fn test_time_seconds_ago_invalid(#[case] input: &str) { ); } -// Fractional relative seconds, checked against GNU date 9.11 with -// TZ=UTC date --date="2026-08-27 12:00:00 " +"%H:%M:%S.%N" +// Fractional relative seconds, checked with +// TZ=UTC date --date="2026-08-27 12:00:00 +0000 " +"%H:%M:%S.%N" #[rstest] #[case::plus_half("+0.5 sec", "12:00:00.500000000")] #[case::minus_half("-0.5 sec", "11:59:59.500000000")]