diff --git a/src/items/relative.rs b/src/items/relative.rs index 5ed3888..e16f314 100644 --- a/src/items/relative.rs +++ b/src/items/relative.rs @@ -59,9 +59,19 @@ 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) => { + // -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 { + (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..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" @@ -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 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")] +#[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)); +}