From 2929067ba24675212b3d1884196527c696b106a2 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Thu, 8 Aug 2024 19:15:36 -0400 Subject: [PATCH] api: add new top-level SignedDuration type It is meant to be as close of a mimic of `std::time::Duration` as possible, but signed instead of unsigned. In most cases, it's a drop-in replacement. This type fits more naturally with Jiff's signed `Span` type. We call methods using this type "jiff_duration," which is a bit of a mouthful. In `jiff 0.2`, we'll plan to drop the `jiff_` prefix and remove the corresponding `std::time::Duration` methods. Instead, callers can use `SignedDuration` and then convert to-and-from `std::time::Duration` from there. --- CHANGELOG.md | 36 + src/fmt/temporal/mod.rs | 91 +- src/fmt/temporal/parser.rs | 334 ++++++- src/fmt/temporal/printer.rs | 97 +- src/lib.rs | 2 + src/signed_duration.rs | 1823 +++++++++++++++++++++++++++++++++++ src/span.rs | 270 +++++- src/timestamp.rs | 294 ++++-- 8 files changed, 2848 insertions(+), 99 deletions(-) create mode 100644 src/signed_duration.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b2f955..c7c91a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,39 @@ +0.1.6 (TBD) +=========== +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. + +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. + +This release also includes a few deprecations. + +Deprecations: + +* `Timestamp::as_duration`: to be replaced with `SignedDuration` in `jiff 0.2`. +* `Timestamp::from_duration`: to be replaced with `SignedDuration` in +`jiff 0.2`. +* `Timestamp::from_signed_duration`: to be replaced with `SignedDuration` in +`jiff 0.2`. +* `Span::to_duration`: to be replaced with `SignedDuration` in `jiff 0.2`. + +Basically, all of the above APIs either accept or return a +`std::time::Duration`. To avoid breaking chnages 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: + +* [#XXX](https://github.com/BurntSushi/jiff/issues/XXX): +Add new top-level `SignedDuration` type. + + 0.1.5 (2024-08-09) ================== This release includes some improvements and bug fixes, particularly for Jiff's diff --git a/src/fmt/temporal/mod.rs b/src/fmt/temporal/mod.rs index 165352b..2b26b64 100644 --- a/src/fmt/temporal/mod.rs +++ b/src/fmt/temporal/mod.rs @@ -146,7 +146,7 @@ use crate::{ fmt::Write, span::Span, tz::{Disambiguation, OffsetConflict, TimeZoneDatabase}, - Timestamp, Zoned, + SignedDuration, Timestamp, Zoned, }; mod parser; @@ -1102,6 +1102,54 @@ impl SpanParser { let span = parsed.into_full()?; Ok(span) } + + /// Parse an ISO 8601 duration string into a [`SignedDuration`] value. + /// + /// # Errors + /// + /// This returns an error if the span string given is invalid or if it is + /// valid but can't be converted to a `SignedDuration`. This can occur + /// when the parsed time exceeds the minimum and maximum `SignedDuration` + /// values, or if there are any non-zero units greater than hours. + /// + /// # Example + /// + /// This shows a basic example of using this routine. + /// + /// ``` + /// use jiff::{fmt::temporal::SpanParser, SignedDuration}; + /// + /// static PARSER: SpanParser = SpanParser::new(); + /// + /// let duration = PARSER.parse_duration(b"PT48m")?; + /// assert_eq!(duration, SignedDuration::from_mins(48)); + /// + /// # Ok::<(), Box>(()) + /// ``` + /// + /// Note that unless you need to parse a span from a byte string, + /// at time of writing, there is no other advantage to using this + /// parser directly. It is likely more convenient to just use + /// the [`FromStr`](std::str::FromStr) trait implementation on + /// [`SignedDuration`]: + /// + /// ```ignore + /// use jiff::SignedDuration; + /// + /// let duration = "PT48m".parse::()?; + /// assert_eq!(duration, SignedDuration::from_mins(48)); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn parse_duration>( + &self, + input: I, + ) -> Result { + let input = input.as_ref(); + let parsed = self.p.parse_signed_duration(input)?; + let dur = parsed.into_full()?; + Ok(dur) + } } /// A printer for Temporal durations. @@ -1203,6 +1251,47 @@ impl SpanPrinter { ) -> Result<(), Error> { self.p.print_span(span, wtr) } + + /// Print a `SignedDuration` to the given writer. + /// + /// 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::temporal::SpanPrinter, SignedDuration}; + /// + /// const 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, "PT24h2m5.123000789s"); + /// + /// // Negative durations are supported. + /// buf.clear(); + /// PRINTER.print_duration(&-dur, &mut buf).unwrap(); + /// assert_eq!(buf, "-PT24h2m5.123000789s"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn print_duration( + &self, + duration: &SignedDuration, + wtr: W, + ) -> Result<(), Error> { + self.p.print_duration(duration, wtr) + } } #[cfg(test)] diff --git a/src/fmt/temporal/parser.rs b/src/fmt/temporal/parser.rs index 76bf38c..6120ff6 100644 --- a/src/fmt/temporal/parser.rs +++ b/src/fmt/temporal/parser.rs @@ -10,7 +10,7 @@ use crate::{ span::Span, tz::{AmbiguousZoned, Disambiguation, OffsetConflict, TimeZoneDatabase}, util::{escape, parse, rangeint::RFrom, t}, - Timestamp, Unit, Zoned, + SignedDuration, Timestamp, Unit, Zoned, }; /// The datetime components parsed from a string. @@ -912,6 +912,17 @@ impl SpanParser { ) } + #[inline(always)] + pub(super) fn parse_signed_duration<'i>( + &self, + input: &'i [u8], + ) -> Result, Error> { + self.parse_duration(input).context( + "failed to parse ISO 8601 \ + duration string into `SignedDuration`", + ) + } + #[inline(always)] fn parse_span<'i>( &self, @@ -949,6 +960,27 @@ impl SpanParser { Ok(Parsed { value: span, input }) } + #[inline(always)] + fn parse_duration<'i>( + &self, + input: &'i [u8], + ) -> Result, Error> { + let Parsed { value: sign, input } = self.parse_sign(input); + let Parsed { input, .. } = self.parse_duration_designator(input)?; + let Parsed { value: has_time, input } = + self.parse_time_designator(input); + if !has_time { + return Err(err!( + "parsing ISO 8601 duration into SignedDuration requires \ + that the duration contain a time component and no \ + components of days or greater", + )); + } + let Parsed { value: dur, input } = + self.parse_time_units_duration(input, sign == -1)?; + Ok(Parsed { value: dur, input }) + } + /// Parses consecutive date units from an ISO 8601 duration string into the /// span given. /// @@ -1076,6 +1108,121 @@ impl SpanParser { Ok(Parsed { value: (span, parsed_any), input }) } + /// Parses consecutive time units from an ISO 8601 duration string into + /// a Jiff signed duration. + /// + /// If no time units are found, then this returns an error. + #[inline(always)] + fn parse_time_units_duration<'i>( + &self, + mut input: &'i [u8], + negative: bool, + ) -> Result, Error> { + let mut parsed_any = false; + let mut prev_unit: Option = None; + let mut dur = SignedDuration::ZERO; + + loop { + let parsed = self.parse_unit_value(input)?; + input = parsed.input; + let Some(value) = parsed.value else { break }; + + let parsed = parse_temporal_fraction(input)?; + input = parsed.input; + let fraction = parsed.value; + + let parsed = self.parse_unit_time_designator(input)?; + input = parsed.input; + let unit = parsed.value; + + if let Some(prev_unit) = prev_unit { + if prev_unit <= unit { + return Err(err!( + "found value {value:?} with unit {unit} \ + after unit {prev_unit}, but units must be \ + written from largest to smallest \ + (and they can't be repeated)", + unit = unit.singular(), + prev_unit = prev_unit.singular(), + )); + } + } + prev_unit = Some(unit); + parsed_any = true; + + // Convert our parsed unit into a number of seconds. + let unit_secs = match unit { + Unit::Second => value.get(), + Unit::Minute => { + let mins = value.get(); + mins.checked_mul(60).ok_or_else(|| { + err!( + "minute units {mins} overflowed i64 when \ + converted to seconds" + ) + })? + } + Unit::Hour => { + let hours = value.get(); + hours.checked_mul(3_600).ok_or_else(|| { + err!( + "hour units {hours} overflowed i64 when \ + converted to seconds" + ) + })? + } + // Guaranteed not to be here since `parse_unit_time_designator` + // always returns hours, minutes or seconds. + _ => unreachable!(), + }; + // Never panics since nanos==0. + let unit_dur = SignedDuration::new(unit_secs, 0); + // And now try to add it to our existing duration. + let result = if negative { + dur.checked_sub(unit_dur) + } else { + dur.checked_add(unit_dur) + }; + dur = result.ok_or_else(|| { + err!( + "adding value {value} from unit {unit} overflowed \ + signed duration {dur:?}", + unit = unit.singular(), + ) + })?; + + if let Some(fraction) = fraction { + // span = span_fractional_time(unit, value, fraction, span)?; + let fraction_dur = duration_fractional_time(unit, fraction); + let result = if negative { + dur.checked_sub(fraction_dur) + } else { + dur.checked_add(fraction_dur) + }; + dur = result.ok_or_else(|| { + err!( + "adding fractional duration {fraction_dur:?} \ + from unit {unit} to {dur:?} overflowed \ + signed duration limits", + unit = unit.singular(), + ) + })?; + // Once we see a fraction, we are done. We don't permit parsing + // any more units. That is, a fraction can only occur on the + // lowest unit of time. + break; + } + } + if !parsed_any { + return Err(err!( + "expected at least one unit of time (hours, minutes or \ + seconds) in ISO 8601 duration when parsing into a \ + `SignedDuration`", + )); + } + Ok(Parsed { value: dur, input }) + } + #[inline(always)] fn parse_unit_value<'i>( &self, @@ -1167,13 +1314,13 @@ impl SpanParser { ) -> Result, Error> { if input.is_empty() { return Err(err!( - "expected to find span beginning with 'P' or 'p', \ + "expected to find duration beginning with 'P' or 'p', \ but found end of input", )); } if !matches!(input[0], b'P' | b'p') { return Err(err!( - "expected 'P' or 'p' prefix to begin span, \ + "expected 'P' or 'p' prefix to begin duration, \ but found {found:?} instead", found = escape::Byte(input[0]), )); @@ -1328,10 +1475,191 @@ fn span_fractional_time( Ok(span) } +/// Like `span_fractional_time`, but just converts the fraction of the given +/// unit to a signed duration. +/// +/// Since a signed duration doesn't keep track of individual units, there is +/// no loss of fidelity between it and ISO 8601 durations like there is for +/// `Span`. +/// +/// Note that `fraction` can be a fractional hour, minute or second (even +/// though its type suggests its only a fraction of a second). +/// +/// # Panics +/// +/// This panics if `unit` is not `Hour`, `Minute` or `Second`. +fn duration_fractional_time( + unit: Unit, + fraction: t::SubsecNanosecond, +) -> SignedDuration { + let fraction = t::NoUnits::rfrom(fraction); + let nanos = match unit { + Unit::Hour => fraction * t::SECONDS_PER_HOUR, + Unit::Minute => fraction * t::SECONDS_PER_MINUTE, + Unit::Second => fraction, + _ => unreachable!("unsupported unit: {unit:?}"), + }; + SignedDuration::from_nanos(nanos.get()) +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn ok_signed_duration() { + let p = + |input| SpanParser::new().parse_signed_duration(input).unwrap(); + + insta::assert_debug_snapshot!(p(b"PT0s"), @r###" + Parsed { + value: 0s, + input: "", + } + "###); + insta::assert_debug_snapshot!(p(b"PT0.000000001s"), @r###" + Parsed { + value: 1ns, + input: "", + } + "###); + insta::assert_debug_snapshot!(p(b"PT1s"), @r###" + Parsed { + value: 1s, + input: "", + } + "###); + insta::assert_debug_snapshot!(p(b"PT59s"), @r###" + Parsed { + value: 59s, + input: "", + } + "###); + insta::assert_debug_snapshot!(p(b"PT60s"), @r###" + Parsed { + value: 60s, + input: "", + } + "###); + insta::assert_debug_snapshot!(p(b"PT1m"), @r###" + Parsed { + value: 60s, + input: "", + } + "###); + insta::assert_debug_snapshot!(p(b"PT1m0.000000001s"), @r###" + Parsed { + value: 60s1ns, + input: "", + } + "###); + insta::assert_debug_snapshot!(p(b"PT1.25m"), @r###" + Parsed { + value: 75s, + input: "", + } + "###); + insta::assert_debug_snapshot!(p(b"PT1h"), @r###" + Parsed { + value: 3600s, + input: "", + } + "###); + insta::assert_debug_snapshot!(p(b"PT1h0.000000001s"), @r###" + Parsed { + value: 3600s1ns, + input: "", + } + "###); + insta::assert_debug_snapshot!(p(b"PT1.25h"), @r###" + Parsed { + value: 4500s, + input: "", + } + "###); + + insta::assert_debug_snapshot!(p(b"-PT2562047788015215h30m8.999999999s"), @r###" + Parsed { + value: -9223372036854775808s999999999ns, + input: "", + } + "###); + insta::assert_debug_snapshot!(p(b"PT2562047788015215h30m7.999999999s"), @r###" + Parsed { + value: 9223372036854775807s999999999ns, + input: "", + } + "###); + } + + #[test] + fn err_signed_duration() { + let p = |input| { + SpanParser::new().parse_signed_duration(input).unwrap_err() + }; + + insta::assert_snapshot!( + p(b"P0d"), + @"failed to parse ISO 8601 duration string into `SignedDuration`: parsing ISO 8601 duration into SignedDuration requires that the duration contain a time component and no components of days or greater", + ); + insta::assert_snapshot!( + p(b"PT0d"), + @r###"failed to parse ISO 8601 duration string into `SignedDuration`: expected to find time unit designator suffix (H, M or S), but found "d" instead"###, + ); + insta::assert_snapshot!( + p(b"P0dT1s"), + @"failed to parse ISO 8601 duration string into `SignedDuration`: parsing ISO 8601 duration into SignedDuration requires that the duration contain a time component and no components of days or greater", + ); + + insta::assert_snapshot!( + p(b""), + @"failed to parse ISO 8601 duration string into `SignedDuration`: expected to find duration beginning with 'P' or 'p', but found end of input", + ); + insta::assert_snapshot!( + p(b"P"), + @"failed to parse ISO 8601 duration string into `SignedDuration`: parsing ISO 8601 duration into SignedDuration requires that the duration contain a time component and no components of days or greater", + ); + insta::assert_snapshot!( + p(b"PT"), + @"failed to parse ISO 8601 duration string into `SignedDuration`: expected at least one unit of time (hours, minutes or seconds) in ISO 8601 duration when parsing into a `SignedDuration`", + ); + insta::assert_snapshot!( + p(b"PTs"), + @"failed to parse ISO 8601 duration string into `SignedDuration`: expected at least one unit of time (hours, minutes or seconds) in ISO 8601 duration when parsing into a `SignedDuration`", + ); + + insta::assert_snapshot!( + p(b"PT1s1m"), + @"failed to parse ISO 8601 duration string into `SignedDuration`: found value 1 with unit minute after unit second, but units must be written from largest to smallest (and they can't be repeated)", + ); + insta::assert_snapshot!( + p(b"PT1s1h"), + @"failed to parse ISO 8601 duration string into `SignedDuration`: found value 1 with unit hour after unit second, but units must be written from largest to smallest (and they can't be repeated)", + ); + insta::assert_snapshot!( + p(b"PT1m1h"), + @"failed to parse ISO 8601 duration string into `SignedDuration`: found value 1 with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)", + ); + + insta::assert_snapshot!( + p(b"-PT9223372036854775809s"), + @r###"failed to parse ISO 8601 duration string into `SignedDuration`: failed to parse "9223372036854775809" as 64-bit signed integer: number '9223372036854775809' too big to parse into 64-bit integer"###, + ); + insta::assert_snapshot!( + p(b"PT9223372036854775808s"), + @r###"failed to parse ISO 8601 duration string into `SignedDuration`: failed to parse "9223372036854775808" as 64-bit signed integer: number '9223372036854775808' too big to parse into 64-bit integer"###, + ); + + insta::assert_snapshot!( + p(b"PT1m9223372036854775807s"), + @"failed to parse ISO 8601 duration string into `SignedDuration`: adding value 9223372036854775807 from unit second overflowed signed duration 60s", + ); + insta::assert_snapshot!( + p(b"PT2562047788015215.6h"), + @"failed to parse ISO 8601 duration string into `SignedDuration`: adding fractional duration 2160s from unit hour to 9223372036854774000s overflowed signed duration limits", + ); + } + #[test] fn ok_temporal_duration_basic() { let p = diff --git a/src/fmt/temporal/printer.rs b/src/fmt/temporal/printer.rs index 35557a4..76fa0ca 100644 --- a/src/fmt/temporal/printer.rs +++ b/src/fmt/temporal/printer.rs @@ -8,7 +8,7 @@ use crate::{ span::Span, tz::{Offset, TimeZone}, util::{rangeint::RFrom, t}, - Timestamp, Zoned, + SignedDuration, Timestamp, Zoned, }; #[derive(Clone, Debug)] @@ -317,6 +317,55 @@ impl SpanPrinter { } Ok(()) } + + /// 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_duration( + &self, + dur: &SignedDuration, + mut wtr: W, + ) -> Result<(), Error> { + static FMT_INT: DecimalFormatter = DecimalFormatter::new(); + static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new(); + + let mut non_zero_greater_than_second = false; + if dur.is_negative() { + wtr.write_str("-")?; + } + wtr.write_str("PT")?; + + let mut secs = dur.as_secs(); + // OK because subsec_nanos -999_999_999<=nanos<=999_999_999. + let nanos = dur.subsec_nanos().abs(); + // OK because guaranteed to be bigger than i64::MIN. + let hours = (secs / (60 * 60)).abs(); + secs %= 60 * 60; + // OK because guaranteed to be bigger than i64::MIN. + let minutes = (secs / 60).abs(); + // OK because guaranteed to be bigger than i64::MIN. + secs = (secs % 60).abs(); + if hours != 0 { + wtr.write_int(&FMT_INT, hours)?; + wtr.write_str("h")?; + non_zero_greater_than_second = true; + } + if minutes != 0 { + wtr.write_int(&FMT_INT, minutes)?; + wtr.write_str("m")?; + non_zero_greater_than_second = true; + } + if (secs != 0 || !non_zero_greater_than_second) && nanos == 0 { + wtr.write_int(&FMT_INT, secs)?; + wtr.write_str("s")?; + } else if nanos != 0 { + wtr.write_int(&FMT_INT, secs)?; + wtr.write_str(".")?; + wtr.write_fraction(&FMT_FRACTION, nanos)?; + wtr.write_str("s")?; + } + Ok(()) + } } #[cfg(test)] @@ -548,4 +597,50 @@ mod tests { .nanoseconds(t::SpanNanoseconds::MIN_REPR), ), @"-PT1902545624836.854775807s"); } + + #[test] + fn print_duration() { + let p = |secs, nanos| -> String { + let dur = SignedDuration::new(secs, nanos); + let mut buf = String::new(); + SpanPrinter::new().print_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", + ); + } } diff --git a/src/lib.rs b/src/lib.rs index 299837a..90319e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -643,6 +643,7 @@ extern crate alloc; pub use crate::{ error::Error, + signed_duration::SignedDuration, span::{ Span, SpanArithmetic, SpanCompare, SpanRelativeTo, SpanRound, SpanTotal, ToSpan, Unit, @@ -662,6 +663,7 @@ mod error; pub mod fmt; #[cfg(feature = "std")] mod now; +mod signed_duration; mod span; mod timestamp; pub mod tz; diff --git a/src/signed_duration.rs b/src/signed_duration.rs new file mode 100644 index 0000000..f8a011f --- /dev/null +++ b/src/signed_duration.rs @@ -0,0 +1,1823 @@ +use core::time::Duration; + +use crate::{ + error::{err, ErrorContext}, + Error, +}; + +#[cfg(not(feature = "std"))] +use crate::util::libm::Float; + +// BREADCRUMBS: Update relevant portions of crate documentation too. + +/// A signed duration of time represented as a 96-bit integer of nanoseconds. +/// +/// Each duration is made up of a 64-bit integer of whole seconds and a +/// 32-bit integer of fractional nanoseconds less than 1 whole second. Unlike +/// [`std::time::Duration`], this duration is signed. The sign applies +/// to the entire duration. That is, either _both_ the seconds and the +/// fractional nanoseconds are negative or _neither_ are. Stated differently, +/// it is guaranteed that the signs of [`SignedDuration::as_secs`] and +/// [`SignedDuration::subsec_nanos`] are always the same. +/// +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct SignedDuration { + secs: i64, + nanos: i32, +} + +const NANOS_PER_SEC: i32 = 1_000_000_000; +const NANOS_PER_MILLI: i32 = 1_000_000; +const NANOS_PER_MICRO: i32 = 1_000; +const MILLIS_PER_SEC: i64 = 1_000; +const MICROS_PER_SEC: i64 = 1_000_000; +const SECS_PER_MINUTE: i64 = 60; +const MINS_PER_HOUR: i64 = 60; + +impl SignedDuration { + /// A duration of zero time. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::ZERO; + /// assert!(duration.is_zero()); + /// assert_eq!(duration.as_secs(), 0); + /// assert_eq!(duration.subsec_nanos(), 0); + /// ``` + pub const ZERO: SignedDuration = SignedDuration { secs: 0, nanos: 0 }; + + /// The minimum possible duration. Or the "most negative" duration. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::MIN; + /// assert_eq!(duration.as_secs(), i64::MIN); + /// assert_eq!(duration.subsec_nanos(), -999_999_999); + /// ``` + pub const MIN: SignedDuration = + SignedDuration { secs: i64::MIN, nanos: -(NANOS_PER_SEC - 1) }; + + /// The maximum possible duration. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::MAX; + /// assert_eq!(duration.as_secs(), i64::MAX); + /// assert_eq!(duration.subsec_nanos(), 999_999_999); + /// ``` + pub const MAX: SignedDuration = + SignedDuration { secs: i64::MAX, nanos: NANOS_PER_SEC - 1 }; + + /// Creates a new `SignedDuration` from the given number of whole seconds + /// and additional nanoseconds. + /// + /// If the absolute value of the nanoseconds is greater than or equal to + /// 1 second, then the excess balances into the number of whole seconds. + /// + /// # Panics + /// + /// When the absolute value of the nanoseconds is greater than or equal + /// to 1 second and the excess that carries over to the number of whole + /// seconds overflows `i64`. + /// + /// This never panics when `nanos` is less than `1_000_000_000`. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 0); + /// assert_eq!(duration.as_secs(), 12); + /// assert_eq!(duration.subsec_nanos(), 0); + /// + /// let duration = SignedDuration::new(12, -1); + /// assert_eq!(duration.as_secs(), 11); + /// assert_eq!(duration.subsec_nanos(), 999_999_999); + /// + /// let duration = SignedDuration::new(12, 1_000_000_000); + /// assert_eq!(duration.as_secs(), 13); + /// assert_eq!(duration.subsec_nanos(), 0); + /// ``` + #[inline] + pub const fn new(mut secs: i64, mut nanos: i32) -> SignedDuration { + // When |nanos| exceeds 1 second, we balance the excess up to seconds. + if !(-NANOS_PER_SEC < nanos && nanos < NANOS_PER_SEC) { + // Never wraps or panics because NANOS_PER_SEC!={0,-1}. + let addsecs = nanos / NANOS_PER_SEC; + secs = match secs.checked_add(addsecs as i64) { + Some(secs) => secs, + None => panic!( + "nanoseconds overflowed seconds in SignedDuration::new" + ), + }; + // Never wraps or panics because NANOS_PER_SEC!={0,-1}. + nanos = nanos % NANOS_PER_SEC; + } + // At this point, we're done if either unit is zero or if they have the + // same sign. + if nanos == 0 || secs == 0 || secs.signum() == (nanos.signum() as i64) + { + return SignedDuration { secs, nanos }; + } + // Otherwise, the only work we have to do is to balance negative nanos + // into positive seconds, or positive nanos into negative seconds. + if secs < 0 { + debug_assert!(nanos > 0); + // Never wraps because adding +1 to a negative i64 never overflows. + // + // MSRV(1.79): Consider using `unchecked_add` here. + secs += 1; + // Never wraps because subtracting +1_000_000_000 from a positive + // i32 never overflows. + // + // MSRV(1.79): Consider using `unchecked_sub` here. + nanos -= NANOS_PER_SEC; + } else { + debug_assert!(secs > 0); + debug_assert!(nanos < 0); + // Never wraps because subtracting +1 from a positive i64 never + // overflows. + // + // MSRV(1.79): Consider using `unchecked_add` here. + secs -= 1; + // Never wraps because adding +1_000_000_000 to a negative i32 + // never overflows. + // + // MSRV(1.79): Consider using `unchecked_add` here. + nanos += NANOS_PER_SEC; + } + SignedDuration { secs, nanos } + } + + /// Creates a new `SignedDuration` from the given number of whole seconds. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::from_secs(12); + /// assert_eq!(duration.as_secs(), 12); + /// assert_eq!(duration.subsec_nanos(), 0); + /// ``` + #[inline] + pub const fn from_secs(secs: i64) -> SignedDuration { + SignedDuration { secs, nanos: 0 } + } + + /// Creates a new `SignedDuration` from the given number of whole + /// milliseconds. + /// + /// Note that since this accepts an `i64`, this method cannot be used + /// to construct the full range of possible signed duration values. In + /// particular, [`SignedDuration::as_millis`] returns an `i128`, and this + /// may be a value that would otherwise overflow an `i64`. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::from_millis(12_456); + /// assert_eq!(duration.as_secs(), 12); + /// assert_eq!(duration.subsec_nanos(), 456_000_000); + /// + /// let duration = SignedDuration::from_millis(-12_456); + /// assert_eq!(duration.as_secs(), -12); + /// assert_eq!(duration.subsec_nanos(), -456_000_000); + /// ``` + #[inline] + pub const fn from_millis(millis: i64) -> SignedDuration { + // OK because MILLIS_PER_SEC!={-1,0}. + let secs = millis / MILLIS_PER_SEC; + // OK because MILLIS_PER_SEC!={-1,0} and because + // millis % MILLIS_PER_SEC can be at most 999, and 999 * 1_000_000 + // never overflows i32. + let nanos = (millis % MILLIS_PER_SEC) as i32 * NANOS_PER_MILLI; + SignedDuration { secs, nanos } + } + + /// Creates a new `SignedDuration` from the given number of whole + /// microseconds. + /// + /// Note that since this accepts an `i64`, this method cannot be used + /// to construct the full range of possible signed duration values. In + /// particular, [`SignedDuration::as_micros`] returns an `i128`, and this + /// may be a value that would otherwise overflow an `i64`. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::from_micros(12_000_456); + /// assert_eq!(duration.as_secs(), 12); + /// assert_eq!(duration.subsec_nanos(), 456_000); + /// + /// let duration = SignedDuration::from_micros(-12_000_456); + /// assert_eq!(duration.as_secs(), -12); + /// assert_eq!(duration.subsec_nanos(), -456_000); + /// ``` + #[inline] + pub const fn from_micros(micros: i64) -> SignedDuration { + // OK because MICROS_PER_SEC!={-1,0}. + let secs = micros / MICROS_PER_SEC; + // OK because MICROS_PER_SEC!={-1,0} and because + // millis % MICROS_PER_SEC can be at most 999, and 999 * 1_000_000 + // never overflows i32. + let nanos = (micros % MICROS_PER_SEC) as i32 * NANOS_PER_MICRO; + SignedDuration { secs, nanos } + } + + /// Creates a new `SignedDuration` from the given number of whole + /// nanoseconds. + /// + /// Note that since this accepts an `i64`, this method cannot be used + /// to construct the full range of possible signed duration values. In + /// particular, [`SignedDuration::as_nanos`] returns an `i128`, and this + /// may be a value that would otherwise overflow an `i64`. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::from_nanos(12_000_000_456); + /// assert_eq!(duration.as_secs(), 12); + /// assert_eq!(duration.subsec_nanos(), 456); + /// + /// let duration = SignedDuration::from_nanos(-12_000_000_456); + /// assert_eq!(duration.as_secs(), -12); + /// assert_eq!(duration.subsec_nanos(), -456); + /// ``` + #[inline] + pub const fn from_nanos(nanos: i64) -> SignedDuration { + // OK because NANOS_PER_SEC!={-1,0}. + let secs = nanos / (NANOS_PER_SEC as i64); + // OK because NANOS_PER_SEC!={-1,0}. + let nanos = (nanos % (NANOS_PER_SEC as i64)) as i32; + SignedDuration { secs, nanos } + } + + /// Creates a new `SignedDuration` from the given number of hours. Every + /// hour is exactly `3,600` seconds. + /// + /// # Panics + /// + /// Panics if the number of hours, after being converted to nanoseconds, + /// overflows the minimum or maximum `SignedDuration` values. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::from_hours(24); + /// assert_eq!(duration.as_secs(), 86_400); + /// assert_eq!(duration.subsec_nanos(), 0); + /// + /// let duration = SignedDuration::from_hours(-24); + /// assert_eq!(duration.as_secs(), -86_400); + /// assert_eq!(duration.subsec_nanos(), 0); + /// ``` + #[inline] + pub const fn from_hours(hours: i64) -> SignedDuration { + // OK because (SECS_PER_MINUTE*MINS_PER_HOUR)!={-1,0}. + const MIN_HOUR: i64 = i64::MIN / (SECS_PER_MINUTE * MINS_PER_HOUR); + // OK because (SECS_PER_MINUTE*MINS_PER_HOUR)!={-1,0}. + const MAX_HOUR: i64 = i64::MAX / (SECS_PER_MINUTE * MINS_PER_HOUR); + // OK because (SECS_PER_MINUTE*MINS_PER_HOUR)!={-1,0}. + if hours < MIN_HOUR { + panic!("hours overflowed minimum number of SignedDuration seconds") + } + // OK because (SECS_PER_MINUTE*MINS_PER_HOUR)!={-1,0}. + if hours > MAX_HOUR { + panic!("hours overflowed maximum number of SignedDuration seconds") + } + SignedDuration::from_secs(hours * MINS_PER_HOUR * SECS_PER_MINUTE) + } + + /// Creates a new `SignedDuration` from the given number of minutes. Every + /// minute is exactly `60` seconds. + /// + /// # Panics + /// + /// Panics if the number of minutes, after being converted to nanoseconds, + /// overflows the minimum or maximum `SignedDuration` values. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::from_mins(1_440); + /// assert_eq!(duration.as_secs(), 86_400); + /// assert_eq!(duration.subsec_nanos(), 0); + /// + /// let duration = SignedDuration::from_mins(-1_440); + /// assert_eq!(duration.as_secs(), -86_400); + /// assert_eq!(duration.subsec_nanos(), 0); + /// ``` + #[inline] + pub const fn from_mins(minutes: i64) -> SignedDuration { + // OK because SECS_PER_MINUTE!={-1,0}. + const MIN_MINUTE: i64 = i64::MIN / SECS_PER_MINUTE; + // OK because SECS_PER_MINUTE!={-1,0}. + const MAX_MINUTE: i64 = i64::MAX / SECS_PER_MINUTE; + // OK because SECS_PER_MINUTE!={-1,0}. + if minutes < MIN_MINUTE { + panic!( + "minutes overflowed minimum number of SignedDuration seconds" + ) + } + // OK because SECS_PER_MINUTE!={-1,0}. + if minutes > MAX_MINUTE { + panic!( + "minutes overflowed maximum number of SignedDuration seconds" + ) + } + SignedDuration::from_secs(minutes * SECS_PER_MINUTE) + } + + /// Returns true if this duration spans no time. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// assert!(SignedDuration::ZERO.is_zero()); + /// assert!(!SignedDuration::MIN.is_zero()); + /// assert!(!SignedDuration::MAX.is_zero()); + /// ``` + #[inline] + pub const fn is_zero(&self) -> bool { + self.secs == 0 && self.nanos == 0 + } + + /// Returns the number of whole seconds in this duration. + /// + /// The value returned is negative when the duration is negative. + /// + /// This does not include any fractional component corresponding to units + /// less than a second. To access those, use one of the `subsec` methods + /// such as [`SignedDuration::subsec_nanos`]. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 999_999_999); + /// assert_eq!(duration.as_secs(), 12); + /// + /// let duration = SignedDuration::new(-12, -999_999_999); + /// assert_eq!(duration.as_secs(), -12); + /// ``` + #[inline] + pub const fn as_secs(&self) -> i64 { + self.secs + } + + /// Returns the fractional part of this duration in whole milliseconds. + /// + /// The value returned is negative when the duration is negative. It is + /// guaranteed that the range of the value returned is in the inclusive + /// range `-999..=999`. + /// + /// To get the length of the total duration represented in milliseconds, + /// use [`SignedDuration::as_millis`]. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 123_456_789); + /// assert_eq!(duration.subsec_millis(), 123); + /// + /// let duration = SignedDuration::new(-12, -123_456_789); + /// assert_eq!(duration.subsec_millis(), -123); + /// ``` + #[inline] + pub const fn subsec_millis(&self) -> i32 { + // OK because NANOS_PER_MILLI!={-1,0}. + self.nanos / NANOS_PER_MILLI + } + + /// Returns the fractional part of this duration in whole microseconds. + /// + /// The value returned is negative when the duration is negative. It is + /// guaranteed that the range of the value returned is in the inclusive + /// range `-999_999..=999_999`. + /// + /// To get the length of the total duration represented in microseconds, + /// use [`SignedDuration::as_micros`]. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 123_456_789); + /// assert_eq!(duration.subsec_micros(), 123_456); + /// + /// let duration = SignedDuration::new(-12, -123_456_789); + /// assert_eq!(duration.subsec_micros(), -123_456); + /// ``` + #[inline] + pub const fn subsec_micros(&self) -> i32 { + // OK because NANOS_PER_MICRO!={-1,0}. + self.nanos / NANOS_PER_MICRO + } + + /// Returns the fractional part of this duration in whole nanoseconds. + /// + /// The value returned is negative when the duration is negative. It is + /// guaranteed that the range of the value returned is in the inclusive + /// range `-999_999_999..=999_999_999`. + /// + /// To get the length of the total duration represented in nanoseconds, + /// use [`SignedDuration::as_nanos`]. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 123_456_789); + /// assert_eq!(duration.subsec_nanos(), 123_456_789); + /// + /// let duration = SignedDuration::new(-12, -123_456_789); + /// assert_eq!(duration.subsec_nanos(), -123_456_789); + /// ``` + #[inline] + pub const fn subsec_nanos(&self) -> i32 { + self.nanos + } + + /// Returns the total duration in units of whole milliseconds. + /// + /// The value returned is negative when the duration is negative. + /// + /// To get only the fractional component of this duration in units of + /// whole milliseconds, use [`SignedDuration::subsec_millis`]. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 123_456_789); + /// assert_eq!(duration.as_millis(), 12_123); + /// + /// let duration = SignedDuration::new(-12, -123_456_789); + /// assert_eq!(duration.as_millis(), -12_123); + /// ``` + #[inline] + pub const fn as_millis(&self) -> i128 { + // OK because 1_000 times any i64 will never overflow i128. + let millis = (self.secs as i128) * (MILLIS_PER_SEC as i128); + // OK because NANOS_PER_MILLI!={-1,0}. + let subsec_millis = (self.nanos / NANOS_PER_MILLI) as i128; + // OK because subsec_millis maxes out at 999, and adding that to + // i64::MAX*1_000 will never overflow a i128. + millis + subsec_millis + } + + /// Returns the total duration in units of whole microseconds. + /// + /// The value returned is negative when the duration is negative. + /// + /// To get only the fractional component of this duration in units of + /// whole microseconds, use [`SignedDuration::subsec_micros`]. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 123_456_789); + /// assert_eq!(duration.as_micros(), 12_123_456); + /// + /// let duration = SignedDuration::new(-12, -123_456_789); + /// assert_eq!(duration.as_micros(), -12_123_456); + /// ``` + #[inline] + pub const fn as_micros(&self) -> i128 { + // OK because 1_000_000 times any i64 will never overflow i128. + let micros = (self.secs as i128) * (MICROS_PER_SEC as i128); + // OK because NANOS_PER_MICRO!={-1,0}. + let subsec_micros = (self.nanos / NANOS_PER_MICRO) as i128; + // OK because subsec_micros maxes out at 999_999, and adding that to + // i64::MAX*1_000_000 will never overflow a i128. + micros + subsec_micros + } + + /// Returns the total duration in units of whole nanoseconds. + /// + /// The value returned is negative when the duration is negative. + /// + /// To get only the fractional component of this duration in units of + /// whole nanoseconds, use [`SignedDuration::subsec_nanos`]. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 123_456_789); + /// assert_eq!(duration.as_nanos(), 12_123_456_789); + /// + /// let duration = SignedDuration::new(-12, -123_456_789); + /// assert_eq!(duration.as_nanos(), -12_123_456_789); + /// ``` + #[inline] + pub const fn as_nanos(&self) -> i128 { + // OK because 1_000_000_000 times any i64 will never overflow i128. + let nanos = (self.secs as i128) * (NANOS_PER_SEC as i128); + // OK because subsec_nanos maxes out at 999_999_999, and adding that to + // i64::MAX*1_000_000_000 will never overflow a i128. + nanos + (self.nanos as i128) + } + + // NOTE: We don't provide `abs_diff` here because we can't represent the + // difference between all possible durations. For example, + // `abs_diff(SignedDuration::MAX, SignedDuration::MIN)`. It therefore seems + // like we should actually return a `std::time::Duration` here, but I'm + // trying to be conservative when divering from std. + + /// Add two signed durations together. If overflow occurs, then `None` is + /// returned. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration1 = SignedDuration::new(12, 500_000_000); + /// let duration2 = SignedDuration::new(0, 500_000_000); + /// assert_eq!( + /// duration1.checked_add(duration2), + /// Some(SignedDuration::new(13, 0)), + /// ); + /// + /// let duration1 = SignedDuration::MAX; + /// let duration2 = SignedDuration::new(0, 1); + /// assert_eq!(duration1.checked_add(duration2), None); + /// ``` + #[inline] + pub const fn checked_add( + self, + rhs: SignedDuration, + ) -> Option { + let Some(mut secs) = self.secs.checked_add(rhs.secs) else { + return None; + }; + // OK because `-999_999_999 <= nanos <= 999_999_999`, and so adding + // them together will never overflow an i32. + let mut nanos = self.nanos + rhs.nanos; + // The below is effectively SignedDuration::new, but with checked + // arithmetic. My suspicion is that there is probably a better way + // to do this. The main complexity here is that 1) `|nanos|` might + // now exceed 1 second and 2) the signs of `secs` and `nanos` might + // not be the same. The other difference from SignedDuration::new is + // that we know that `-1_999_999_998 <= nanos <= 1_999_999_998` since + // `|SignedDuration::nanos|` is guaranteed to be less than 1 second. So + // we can skip the div and modulus operations. + + // When |nanos| exceeds 1 second, we balance the excess up to seconds. + if nanos >= NANOS_PER_SEC { + nanos -= NANOS_PER_SEC; + secs = match secs.checked_add(1) { + None => return None, + Some(secs) => secs, + }; + } else if nanos <= -NANOS_PER_SEC { + nanos += NANOS_PER_SEC; + secs = match secs.checked_sub(1) { + None => return None, + Some(secs) => secs, + }; + } + if secs != 0 && nanos != 0 && secs.signum() != (nanos.signum() as i64) + { + if secs < 0 { + debug_assert!(nanos > 0); + // OK because secs<0. + secs += 1; + // OK because nanos>0. + nanos -= NANOS_PER_SEC; + } else { + debug_assert!(secs > 0); + debug_assert!(nanos < 0); + // OK because secs>0. + secs -= 1; + // OK because nanos<0. + nanos += NANOS_PER_SEC; + } + } + Some(SignedDuration { secs, nanos }) + } + + /// Add two signed durations together. If overflow occurs, then arithmetic + /// saturates. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration1 = SignedDuration::MAX; + /// let duration2 = SignedDuration::new(0, 1); + /// assert_eq!(duration1.saturating_add(duration2), SignedDuration::MAX); + /// + /// let duration1 = SignedDuration::MIN; + /// let duration2 = SignedDuration::new(0, -1); + /// assert_eq!(duration1.saturating_add(duration2), SignedDuration::MIN); + /// ``` + #[inline] + pub const fn saturating_add(self, rhs: SignedDuration) -> SignedDuration { + let Some(sum) = self.checked_add(rhs) else { + return if rhs.is_negative() { + SignedDuration::MIN + } else { + SignedDuration::MAX + }; + }; + sum + } + + /// Subtract one signed duration from another. If overflow occurs, then + /// `None` is returned. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration1 = SignedDuration::new(12, 500_000_000); + /// let duration2 = SignedDuration::new(0, 500_000_000); + /// assert_eq!( + /// duration1.checked_sub(duration2), + /// Some(SignedDuration::new(12, 0)), + /// ); + /// + /// let duration1 = SignedDuration::MIN; + /// let duration2 = SignedDuration::new(0, 1); + /// assert_eq!(duration1.checked_sub(duration2), None); + /// ``` + #[inline] + pub const fn checked_sub( + self, + rhs: SignedDuration, + ) -> Option { + let Some(rhs) = rhs.checked_neg() else { return None }; + self.checked_add(rhs) + } + + /// Add two signed durations together. If overflow occurs, then arithmetic + /// saturates. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration1 = SignedDuration::MAX; + /// let duration2 = SignedDuration::new(0, -1); + /// assert_eq!(duration1.saturating_sub(duration2), SignedDuration::MAX); + /// + /// let duration1 = SignedDuration::MIN; + /// let duration2 = SignedDuration::new(0, 1); + /// assert_eq!(duration1.saturating_sub(duration2), SignedDuration::MIN); + /// ``` + #[inline] + pub const fn saturating_sub(self, rhs: SignedDuration) -> SignedDuration { + let Some(diff) = self.checked_sub(rhs) else { + return if rhs.is_positive() { + SignedDuration::MIN + } else { + SignedDuration::MAX + }; + }; + diff + } + + /// Multiply this signed duration by an integer. If the multiplication + /// overflows, then `None` is returned. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 500_000_000); + /// assert_eq!( + /// duration.checked_mul(2), + /// Some(SignedDuration::new(25, 0)), + /// ); + /// ``` + #[inline] + pub const fn checked_mul(self, rhs: i32) -> Option { + let rhs = rhs as i64; + // Multiplying any two i32 values never overflows an i64. + let nanos = (self.nanos as i64) * rhs; + // OK since NANOS_PER_SEC!={-1,0}. + let addsecs = nanos / (NANOS_PER_SEC as i64); + // OK since NANOS_PER_SEC!={-1,0}. + let nanos = (nanos % (NANOS_PER_SEC as i64)) as i32; + let Some(secs) = self.secs.checked_mul(rhs) else { return None }; + let Some(secs) = secs.checked_add(addsecs) else { return None }; + Some(SignedDuration { secs, nanos }) + } + + /// Multiply this signed duration by an integer. If the multiplication + /// overflows, then the result saturates to either the minimum or maximum + /// duration depending on the sign of the product. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(i64::MAX, 0); + /// assert_eq!(duration.saturating_mul(2), SignedDuration::MAX); + /// assert_eq!(duration.saturating_mul(-2), SignedDuration::MIN); + /// + /// let duration = SignedDuration::new(i64::MIN, 0); + /// assert_eq!(duration.saturating_mul(2), SignedDuration::MIN); + /// assert_eq!(duration.saturating_mul(-2), SignedDuration::MAX); + /// ``` + #[inline] + pub const fn saturating_mul(self, rhs: i32) -> SignedDuration { + let Some(product) = self.checked_mul(rhs) else { + let sign = (self.signum() as i64) * (rhs as i64).signum(); + return if sign.is_negative() { + SignedDuration::MIN + } else { + SignedDuration::MAX + }; + }; + product + } + + /// Divide this duration by an integer. If the division overflows, then + /// `None` is returned. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 500_000_000); + /// assert_eq!( + /// duration.checked_div(2), + /// Some(SignedDuration::new(6, 250_000_000)), + /// ); + /// assert_eq!( + /// duration.checked_div(-2), + /// Some(SignedDuration::new(-6, -250_000_000)), + /// ); + /// + /// let duration = SignedDuration::new(-12, -500_000_000); + /// assert_eq!( + /// duration.checked_div(2), + /// Some(SignedDuration::new(-6, -250_000_000)), + /// ); + /// assert_eq!( + /// duration.checked_div(-2), + /// Some(SignedDuration::new(6, 250_000_000)), + /// ); + /// ``` + #[inline] + pub const fn checked_div(self, rhs: i32) -> Option { + if rhs == 0 || (self.secs == i64::MIN && rhs == -1) { + return None; + } + // OK since rhs!={-1,0}. + let secs = self.secs / (rhs as i64); + // OK since rhs!={-1,0}. + let addsecs = self.secs % (rhs as i64); + // OK since rhs!=0 and self.nanos>i32::MIN. + let mut nanos = self.nanos / rhs; + // OK since rhs!=0 and self.nanos>i32::MIN. + let addnanos = self.nanos % rhs; + let leftover_nanos = + (addsecs * (NANOS_PER_SEC as i64)) + (addnanos as i64); + nanos += (leftover_nanos / (rhs as i64)) as i32; + debug_assert!(nanos < NANOS_PER_SEC); + Some(SignedDuration { secs, nanos }) + } + + /// Returns the number of seconds, with a possible fractional nanosecond + /// component, represented by this signed duration as a 64-bit float. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 123_456_789); + /// assert_eq!(duration.as_secs_f64(), 12.123456789); + /// + /// let duration = SignedDuration::new(-12, -123_456_789); + /// assert_eq!(duration.as_secs_f64(), -12.123456789); + /// ``` + #[inline] + pub fn as_secs_f64(&self) -> f64 { + (self.secs as f64) + ((self.nanos as f64) / (NANOS_PER_SEC as f64)) + } + + /// Returns the number of seconds, with a possible fractional nanosecond + /// component, represented by this signed duration as a 32-bit float. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 123_456_789); + /// assert_eq!(duration.as_secs_f32(), 12.123456789); + /// + /// let duration = SignedDuration::new(-12, -123_456_789); + /// assert_eq!(duration.as_secs_f32(), -12.123456789); + /// ``` + #[inline] + pub fn as_secs_f32(&self) -> f32 { + (self.secs as f32) + ((self.nanos as f32) / (NANOS_PER_SEC as f32)) + } + + /// Returns the number of milliseconds, with a possible fractional + /// nanosecond component, represented by this signed duration as a 64-bit + /// float. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 123_456_789); + /// assert_eq!(duration.as_millis_f64(), 12123.456789); + /// + /// let duration = SignedDuration::new(-12, -123_456_789); + /// assert_eq!(duration.as_millis_f64(), -12123.456789); + /// ``` + #[inline] + pub fn as_millis_f64(&self) -> f64 { + ((self.secs as f64) * (MILLIS_PER_SEC as f64)) + + ((self.nanos as f64) / (NANOS_PER_MILLI as f64)) + } + + /// Returns the number of milliseconds, with a possible fractional + /// nanosecond component, represented by this signed duration as a 32-bit + /// float. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 123_456_789); + /// assert_eq!(duration.as_millis_f32(), 12123.456789); + /// + /// let duration = SignedDuration::new(-12, -123_456_789); + /// assert_eq!(duration.as_millis_f32(), -12123.456789); + /// ``` + #[inline] + pub fn as_millis_f32(&self) -> f32 { + ((self.secs as f32) * (MILLIS_PER_SEC as f32)) + + ((self.nanos as f32) / (NANOS_PER_MILLI as f32)) + } + + /// Returns a signed duration corresponding to the number of seconds + /// represented as a 64-bit float. The number given may have a fractional + /// nanosecond component. + /// + /// # Panics + /// + /// If the given float overflows the minimum or maximum signed duration + /// values, then this panics. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::from_secs_f64(12.123456789); + /// assert_eq!(duration.as_secs(), 12); + /// assert_eq!(duration.subsec_nanos(), 123_456_789); + /// + /// let duration = SignedDuration::from_secs_f64(-12.123456789); + /// assert_eq!(duration.as_secs(), -12); + /// assert_eq!(duration.subsec_nanos(), -123_456_789); + /// + /// # Ok::<(), Box>(()) + /// ``` + #[inline] + pub fn from_secs_f64(secs: f64) -> SignedDuration { + SignedDuration::try_from_secs_f64(secs) + .expect("finite and in-bounds f64") + } + + /// Returns a signed duration corresponding to the number of seconds + /// represented as a 32-bit float. The number given may have a fractional + /// nanosecond component. + /// + /// # Panics + /// + /// If the given float overflows the minimum or maximum signed duration + /// values, then this panics. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::from_secs_f32(12.123456789); + /// assert_eq!(duration.as_secs(), 12); + /// // loss of precision! + /// assert_eq!(duration.subsec_nanos(), 123_456_952); + /// + /// let duration = SignedDuration::from_secs_f32(-12.123456789); + /// assert_eq!(duration.as_secs(), -12); + /// // loss of precision! + /// assert_eq!(duration.subsec_nanos(), -123_456_952); + /// + /// # Ok::<(), Box>(()) + /// ``` + #[inline] + pub fn from_secs_f32(secs: f32) -> SignedDuration { + SignedDuration::try_from_secs_f32(secs) + .expect("finite and in-bounds f32") + } + + /// Returns a signed duration corresponding to the number of seconds + /// represented as a 64-bit float. The number given may have a fractional + /// nanosecond component. + /// + /// If the given float overflows the minimum or maximum signed duration + /// values, then an error is returned. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::try_from_secs_f64(12.123456789)?; + /// assert_eq!(duration.as_secs(), 12); + /// assert_eq!(duration.subsec_nanos(), 123_456_789); + /// + /// let duration = SignedDuration::try_from_secs_f64(-12.123456789)?; + /// assert_eq!(duration.as_secs(), -12); + /// assert_eq!(duration.subsec_nanos(), -123_456_789); + /// + /// assert!(SignedDuration::try_from_secs_f64(f64::NAN).is_err()); + /// assert!(SignedDuration::try_from_secs_f64(f64::INFINITY).is_err()); + /// assert!(SignedDuration::try_from_secs_f64(f64::NEG_INFINITY).is_err()); + /// assert!(SignedDuration::try_from_secs_f64(f64::MIN).is_err()); + /// assert!(SignedDuration::try_from_secs_f64(f64::MAX).is_err()); + /// + /// # Ok::<(), Box>(()) + /// ``` + #[inline] + pub fn try_from_secs_f64(secs: f64) -> Result { + if !secs.is_finite() { + return Err(err!( + "could not convert non-finite seconds \ + {secs} to signed duration", + )); + } + if secs < (i64::MIN as f64) { + return Err(err!( + "floating point seconds {secs} overflows signed duration \ + minimum value of {:?}", + SignedDuration::MIN, + )); + } + if secs > (i64::MAX as f64) { + return Err(err!( + "floating point seconds {secs} overflows signed duration \ + maximum value of {:?}", + SignedDuration::MAX, + )); + } + let nanos = (secs.fract() * (NANOS_PER_SEC as f64)).round() as i32; + let secs = secs.trunc() as i64; + Ok(SignedDuration { secs, nanos }) + } + + /// Returns a signed duration corresponding to the number of seconds + /// represented as a 32-bit float. The number given may have a fractional + /// nanosecond component. + /// + /// If the given float overflows the minimum or maximum signed duration + /// values, then an error is returned. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::try_from_secs_f32(12.123456789)?; + /// assert_eq!(duration.as_secs(), 12); + /// // loss of precision! + /// assert_eq!(duration.subsec_nanos(), 123_456_952); + /// + /// let duration = SignedDuration::try_from_secs_f32(-12.123456789)?; + /// assert_eq!(duration.as_secs(), -12); + /// // loss of precision! + /// assert_eq!(duration.subsec_nanos(), -123_456_952); + /// + /// assert!(SignedDuration::try_from_secs_f32(f32::NAN).is_err()); + /// assert!(SignedDuration::try_from_secs_f32(f32::INFINITY).is_err()); + /// assert!(SignedDuration::try_from_secs_f32(f32::NEG_INFINITY).is_err()); + /// assert!(SignedDuration::try_from_secs_f32(f32::MIN).is_err()); + /// assert!(SignedDuration::try_from_secs_f32(f32::MAX).is_err()); + /// + /// # Ok::<(), Box>(()) + /// ``` + #[inline] + pub fn try_from_secs_f32(secs: f32) -> Result { + if !secs.is_finite() { + return Err(err!( + "could not convert non-finite seconds \ + {secs} to signed duration", + )); + } + if secs < (i64::MIN as f32) { + return Err(err!( + "floating point seconds {secs} overflows signed duration \ + minimum value of {:?}", + SignedDuration::MIN, + )); + } + if secs > (i64::MAX as f32) { + return Err(err!( + "floating point seconds {secs} overflows signed duration \ + maximum value of {:?}", + SignedDuration::MAX, + )); + } + let nanos = (secs.fract() * (NANOS_PER_SEC as f32)).round() as i32; + let secs = secs.trunc() as i64; + Ok(SignedDuration { secs, nanos }) + } + + /// Returns the result of multiplying this duration by the given 64-bit + /// float. + /// + /// # Panics + /// + /// This panics if the result is not finite or overflows a + /// `SignedDuration`. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 300_000_000); + /// assert_eq!( + /// duration.mul_f64(2.0), + /// SignedDuration::new(24, 600_000_000), + /// ); + /// assert_eq!( + /// duration.mul_f64(-2.0), + /// SignedDuration::new(-24, -600_000_000), + /// ); + /// ``` + #[inline] + pub fn mul_f64(self, rhs: f64) -> SignedDuration { + SignedDuration::from_secs_f64(rhs * self.as_secs_f64()) + } + + /// Returns the result of multiplying this duration by the given 32-bit + /// float. + /// + /// # Panics + /// + /// This panics if the result is not finite or overflows a + /// `SignedDuration`. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 300_000_000); + /// assert_eq!( + /// duration.mul_f32(2.0), + /// // loss of precision! + /// SignedDuration::new(24, 600_000_384), + /// ); + /// assert_eq!( + /// duration.mul_f32(-2.0), + /// // loss of precision! + /// SignedDuration::new(-24, -600_000_384), + /// ); + /// ``` + #[inline] + pub fn mul_f32(self, rhs: f32) -> SignedDuration { + SignedDuration::from_secs_f32(rhs * self.as_secs_f32()) + } + + /// Returns the result of dividing this duration by the given 64-bit + /// float. + /// + /// # Panics + /// + /// This panics if the result is not finite or overflows a + /// `SignedDuration`. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 300_000_000); + /// assert_eq!( + /// duration.div_f64(2.0), + /// SignedDuration::new(6, 150_000_000), + /// ); + /// assert_eq!( + /// duration.div_f64(-2.0), + /// SignedDuration::new(-6, -150_000_000), + /// ); + /// ``` + #[inline] + pub fn div_f64(self, rhs: f64) -> SignedDuration { + SignedDuration::from_secs_f64(self.as_secs_f64() / rhs) + } + + /// Returns the result of dividing this duration by the given 32-bit + /// float. + /// + /// # Panics + /// + /// This panics if the result is not finite or overflows a + /// `SignedDuration`. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 300_000_000); + /// assert_eq!( + /// duration.div_f32(2.0), + /// // loss of precision! + /// SignedDuration::new(6, 150_000_096), + /// ); + /// assert_eq!( + /// duration.div_f32(-2.0), + /// // loss of precision! + /// SignedDuration::new(-6, -150_000_096), + /// ); + /// ``` + #[inline] + pub fn div_f32(self, rhs: f32) -> SignedDuration { + SignedDuration::from_secs_f32(self.as_secs_f32() / rhs) + } + + /// Divides this signed duration by another signed duration and returns the + /// corresponding 64-bit float result. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration1 = SignedDuration::new(12, 600_000_000); + /// let duration2 = SignedDuration::new(6, 300_000_000); + /// assert_eq!(duration1.div_duration_f64(duration2), 2.0); + /// + /// let duration1 = SignedDuration::new(-12, -600_000_000); + /// let duration2 = SignedDuration::new(6, 300_000_000); + /// assert_eq!(duration1.div_duration_f64(duration2), -2.0); + /// + /// let duration1 = SignedDuration::new(-12, -600_000_000); + /// let duration2 = SignedDuration::new(-6, -300_000_000); + /// assert_eq!(duration1.div_duration_f64(duration2), 2.0); + /// ``` + #[inline] + pub fn div_duration_f64(self, rhs: SignedDuration) -> f64 { + let lhs_nanos = + (self.secs as f64) * (NANOS_PER_SEC as f64) + (self.nanos as f64); + let rhs_nanos = + (rhs.secs as f64) * (NANOS_PER_SEC as f64) + (rhs.nanos as f64); + lhs_nanos / rhs_nanos + } + + /// Divides this signed duration by another signed duration and returns the + /// corresponding 32-bit float result. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration1 = SignedDuration::new(12, 600_000_000); + /// let duration2 = SignedDuration::new(6, 300_000_000); + /// assert_eq!(duration1.div_duration_f32(duration2), 2.0); + /// + /// let duration1 = SignedDuration::new(-12, -600_000_000); + /// let duration2 = SignedDuration::new(6, 300_000_000); + /// assert_eq!(duration1.div_duration_f32(duration2), -2.0); + /// + /// let duration1 = SignedDuration::new(-12, -600_000_000); + /// let duration2 = SignedDuration::new(-6, -300_000_000); + /// assert_eq!(duration1.div_duration_f32(duration2), 2.0); + /// ``` + #[inline] + pub fn div_duration_f32(self, rhs: SignedDuration) -> f32 { + let lhs_nanos = + (self.secs as f32) * (NANOS_PER_SEC as f32) + (self.nanos as f32); + let rhs_nanos = + (rhs.secs as f32) * (NANOS_PER_SEC as f32) + (rhs.nanos as f32); + lhs_nanos / rhs_nanos + } +} + +/// Additional APIs not found in the standard library. +/// +/// In most cases, these APIs exist as a result of the fact that this duration +/// is signed. +impl SignedDuration { + /// Returns the duration from `time1` until `time2`. + /// + /// # Errors + /// + /// This returns an error if the difference between the two time values + /// overflows the signed duration limits. + /// + /// # Example + /// + /// ``` + /// use std::time::{Duration, SystemTime}; + /// use jiff::SignedDuration; + /// + /// let time1 = SystemTime::UNIX_EPOCH; + /// let time2 = time1.checked_add(Duration::from_secs(86_400)).unwrap(); + /// assert_eq!( + /// SignedDuration::until(time1, time2)?, + /// SignedDuration::from_hours(24), + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + #[cfg(feature = "std")] + #[inline] + pub fn until( + time1: std::time::SystemTime, + time2: std::time::SystemTime, + ) -> Result { + match time2.duration_since(time1) { + Ok(dur) => SignedDuration::try_from(dur).with_context(|| { + err!( + "unsigned duration {dur:?} for system time since \ + Unix epoch overflowed signed duration" + ) + }), + Err(err) => { + let dur = err.duration(); + let dur = + SignedDuration::try_from(dur).with_context(|| { + err!( + "unsigned duration {dur:?} for system time before \ + Unix epoch overflowed signed duration" + ) + })?; + dur.checked_neg().ok_or_else(|| { + err!("negating duration {dur:?} from before the Unix epoch \ + overflowed signed duration") + }) + } + } + } + + /// Returns the number of whole hours in this duration. + /// + /// The value returned is negative when the duration is negative. + /// + /// This does not include any fractional component corresponding to units + /// less than an hour. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(86_400, 999_999_999); + /// assert_eq!(duration.as_hours(), 24); + /// + /// let duration = SignedDuration::new(-86_400, -999_999_999); + /// assert_eq!(duration.as_hours(), -24); + /// ``` + #[inline] + pub const fn as_hours(&self) -> i64 { + self.as_secs() / (MINS_PER_HOUR * SECS_PER_MINUTE) + } + + /// Returns the number of whole minutes in this duration. + /// + /// The value returned is negative when the duration is negative. + /// + /// This does not include any fractional component corresponding to units + /// less than a minute. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(3_600, 999_999_999); + /// assert_eq!(duration.as_mins(), 60); + /// + /// let duration = SignedDuration::new(-3_600, -999_999_999); + /// assert_eq!(duration.as_mins(), -60); + /// ``` + #[inline] + pub const fn as_mins(&self) -> i64 { + self.as_secs() / SECS_PER_MINUTE + } + + /// Returns the absolute value of this signed duration. + /// + /// If this duration isn't negative, then this returns the original + /// duration unchanged. + /// + /// # Panics + /// + /// This panics when the seconds component of this signed duration is + /// equal to `i64::MIN`. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(1, -1_999_999_999); + /// assert_eq!(duration.abs(), SignedDuration::new(0, 999_999_999)); + /// ``` + #[inline] + pub const fn abs(self) -> SignedDuration { + SignedDuration { secs: self.secs.abs(), nanos: self.nanos.abs() } + } + + /// Returns the absolute value of this signed duration as a + /// [`std::time::Duration`]. More specifically, this routine cannot + /// panic because the absolute value of `SignedDuration::MIN` is + /// representable in a `std::time::Duration`. + /// + /// # Example + /// + /// ``` + /// use std::time::Duration; + /// + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::MIN; + /// assert_eq!( + /// duration.unsigned_abs(), + /// Duration::new(i64::MIN.unsigned_abs(), 999_999_999), + /// ); + /// ``` + #[inline] + pub const fn unsigned_abs(self) -> Duration { + Duration::new(self.secs.unsigned_abs(), self.nanos.unsigned_abs()) + } + + /// Returns this duration with its sign flipped. + /// + /// If this duration is zero, then this returns the duration unchanged. + /// + /// This returns none if the negation does not exist. This occurs in + /// precisely the cases when [`SignedDuration::as_secs`] is equal to + /// `i64::MIN`. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(12, 123_456_789); + /// assert_eq!( + /// duration.checked_neg(), + /// Some(SignedDuration::new(-12, -123_456_789)), + /// ); + /// + /// let duration = SignedDuration::new(-12, -123_456_789); + /// assert_eq!( + /// duration.checked_neg(), + /// Some(SignedDuration::new(12, 123_456_789)), + /// ); + /// + /// // Negating the minimum seconds isn't possible. + /// assert_eq!(SignedDuration::MIN.checked_neg(), None); + /// ``` + #[inline] + pub const fn checked_neg(self) -> Option { + let Some(secs) = self.secs.checked_neg() else { return None }; + Some(SignedDuration { + secs, + // Always OK because `-999_999_999 <= self.nanos <= 999_999_999`. + nanos: -self.nanos, + }) + } + + /// Returns a number that represents the sign of this duration. + /// + /// * When [`SignedDuration::is_zero`] is true, this returns `0`. + /// * When [`SignedDuration::is_positive`] is true, this returns `1`. + /// * When [`SignedDuration::is_negative`] is true, this returns `-1`. + /// + /// The above cases are mutually exclusive. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// assert_eq!(0, SignedDuration::ZERO.signum()); + /// ``` + #[inline] + pub const fn signum(self) -> i8 { + if self.is_zero() { + 0 + } else if self.is_positive() { + 1 + } else { + debug_assert!(self.is_negative()); + -1 + } + } + + /// Returns true when this duration is positive. That is, greater than + /// [`SignedDuration::ZERO`]. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(0, 1); + /// assert!(duration.is_positive()); + /// ``` + #[inline] + pub const fn is_positive(&self) -> bool { + self.secs.is_positive() || self.nanos.is_positive() + } + + /// Returns true when this duration is negative. That is, less than + /// [`SignedDuration::ZERO`]. + /// + /// # Example + /// + /// ``` + /// use jiff::SignedDuration; + /// + /// let duration = SignedDuration::new(0, -1); + /// assert!(duration.is_negative()); + /// ``` + #[inline] + pub const fn is_negative(&self) -> bool { + self.secs.is_negative() || self.nanos.is_negative() + } +} + +impl core::fmt::Display for SignedDuration { + #[inline] + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + use crate::fmt::{temporal::DEFAULT_SPAN_PRINTER, StdFmtWrite}; + + DEFAULT_SPAN_PRINTER + .print_duration(self, StdFmtWrite(f)) + .map_err(|_| core::fmt::Error) + } +} + +impl core::fmt::Debug for SignedDuration { + #[inline] + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + // TODO: Replace with `jiff::fmt::friendly` printer when it exists. + if self.nanos == 0 { + write!(f, "{}s", self.secs) + } else if self.secs == 0 { + write!(f, "{}ns", self.nanos) + } else { + write!(f, "{}s{}ns", self.secs, self.nanos.abs()) + } + } +} + +impl TryFrom for SignedDuration { + type Error = Error; + + fn try_from(d: Duration) -> Result { + let secs = i64::try_from(d.as_secs()).map_err(|_| { + err!("seconds in unsigned duration {d:?} overflowed i64") + })?; + // Guaranteed to succeed since 0<=nanos<=999,999,999. + let nanos = i32::try_from(d.subsec_nanos()).unwrap(); + Ok(SignedDuration { secs, nanos }) + } +} + +impl TryFrom for Duration { + type Error = Error; + + fn try_from(sd: SignedDuration) -> Result { + let secs = u64::try_from(sd.as_secs()).map_err(|_| { + err!("seconds in signed duration {sd:?} overflowed u64") + })?; + // Guaranteed to succeed because the above only succeeds + // when `sd` is non-negative. And when `sd` is non-negative, + // we are guaranteed that 0<=nanos<=999,999,999. + let nanos = u32::try_from(sd.subsec_nanos()).unwrap(); + Ok(Duration::new(secs, nanos)) + } +} + +impl core::str::FromStr for SignedDuration { + type Err = Error; + + #[inline] + fn from_str(string: &str) -> Result { + use crate::fmt::temporal::DEFAULT_SPAN_PARSER; + + DEFAULT_SPAN_PARSER.parse_duration(string) + } +} + +impl core::ops::Neg for SignedDuration { + type Output = SignedDuration; + + #[inline] + fn neg(self) -> SignedDuration { + self.checked_neg().expect("overflow when negating signed duration") + } +} + +impl core::ops::Add for SignedDuration { + type Output = SignedDuration; + + #[inline] + fn add(self, rhs: SignedDuration) -> SignedDuration { + self.checked_add(rhs).expect("overflow when adding signed durations") + } +} + +impl core::ops::AddAssign for SignedDuration { + #[inline] + fn add_assign(&mut self, rhs: SignedDuration) { + *self = *self + rhs; + } +} + +impl core::ops::Sub for SignedDuration { + type Output = SignedDuration; + + #[inline] + fn sub(self, rhs: SignedDuration) -> SignedDuration { + self.checked_sub(rhs) + .expect("overflow when subtracting signed durations") + } +} + +impl core::ops::SubAssign for SignedDuration { + #[inline] + fn sub_assign(&mut self, rhs: SignedDuration) { + *self = *self - rhs; + } +} + +impl core::ops::Mul for SignedDuration { + type Output = SignedDuration; + + #[inline] + fn mul(self, rhs: i32) -> SignedDuration { + self.checked_mul(rhs) + .expect("overflow when multiplying signed duration by scalar") + } +} + +impl core::ops::Mul for i32 { + type Output = SignedDuration; + + #[inline] + fn mul(self, rhs: SignedDuration) -> SignedDuration { + rhs * self + } +} + +impl core::ops::MulAssign for SignedDuration { + #[inline] + fn mul_assign(&mut self, rhs: i32) { + *self = *self * rhs; + } +} + +impl core::ops::Div for SignedDuration { + type Output = SignedDuration; + + #[inline] + fn div(self, rhs: i32) -> SignedDuration { + self.checked_div(rhs) + .expect("overflow when dividing signed duration by scalar") + } +} + +impl core::ops::DivAssign for SignedDuration { + #[inline] + fn div_assign(&mut self, rhs: i32) { + *self = *self / rhs; + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for SignedDuration { + #[inline] + fn serialize( + &self, + serializer: S, + ) -> Result { + serializer.collect_str(self) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for SignedDuration { + #[inline] + fn deserialize>( + deserializer: D, + ) -> Result { + use serde::de; + + struct SignedDurationVisitor; + + impl<'de> de::Visitor<'de> for SignedDurationVisitor { + type Value = SignedDuration; + + fn expecting( + &self, + f: &mut core::fmt::Formatter, + ) -> core::fmt::Result { + f.write_str("a signed duration string") + } + + #[inline] + fn visit_bytes( + self, + value: &[u8], + ) -> Result { + use crate::fmt::temporal::DEFAULT_SPAN_PARSER; + + DEFAULT_SPAN_PARSER + .parse_duration(value) + .map_err(de::Error::custom) + } + + #[inline] + fn visit_str( + self, + value: &str, + ) -> Result { + self.visit_bytes(value.as_bytes()) + } + } + + deserializer.deserialize_bytes(SignedDurationVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new() { + let d = SignedDuration::new(12, i32::MAX); + assert_eq!(d.as_secs(), 14); + assert_eq!(d.subsec_nanos(), 147_483_647); + + let d = SignedDuration::new(-12, i32::MIN); + assert_eq!(d.as_secs(), -14); + assert_eq!(d.subsec_nanos(), -147_483_648); + + let d = SignedDuration::new(i64::MAX, i32::MIN); + assert_eq!(d.as_secs(), i64::MAX - 3); + assert_eq!(d.subsec_nanos(), 852_516_352); + + let d = SignedDuration::new(i64::MIN, i32::MAX); + assert_eq!(d.as_secs(), i64::MIN + 3); + assert_eq!(d.subsec_nanos(), -852_516_353); + } + + #[test] + #[should_panic] + fn new_fail_positive() { + SignedDuration::new(i64::MAX, 1_000_000_000); + } + + #[test] + #[should_panic] + fn new_fail_negative() { + SignedDuration::new(i64::MIN, -1_000_000_000); + } + + #[test] + fn from_hours_limits() { + let d = SignedDuration::from_hours(2_562_047_788_015_215); + assert_eq!(d.as_secs(), 9223372036854774000); + + let d = SignedDuration::from_hours(-2_562_047_788_015_215); + assert_eq!(d.as_secs(), -9223372036854774000); + } + + #[test] + #[should_panic] + fn from_hours_fail_positive() { + SignedDuration::from_hours(2_562_047_788_015_216); + } + + #[test] + #[should_panic] + fn from_hours_fail_negative() { + SignedDuration::from_hours(-2_562_047_788_015_216); + } + + #[test] + fn from_minutes_limits() { + let d = SignedDuration::from_mins(153_722_867_280_912_930); + assert_eq!(d.as_secs(), 9223372036854775800); + + let d = SignedDuration::from_mins(-153_722_867_280_912_930); + assert_eq!(d.as_secs(), -9223372036854775800); + } + + #[test] + #[should_panic] + fn from_minutes_fail_positive() { + SignedDuration::from_mins(153_722_867_280_912_931); + } + + #[test] + #[should_panic] + fn from_minutes_fail_negative() { + SignedDuration::from_mins(-153_722_867_280_912_931); + } + + #[test] + fn add() { + let add = |(secs1, nanos1): (i64, i32), + (secs2, nanos2): (i64, i32)| + -> (i64, i32) { + let d1 = SignedDuration::new(secs1, nanos1); + let d2 = SignedDuration::new(secs2, nanos2); + let sum = d1.checked_add(d2).unwrap(); + (sum.as_secs(), sum.subsec_nanos()) + }; + + assert_eq!(add((1, 1), (1, 1)), (2, 2)); + assert_eq!(add((1, 1), (-1, -1)), (0, 0)); + assert_eq!(add((-1, -1), (1, 1)), (0, 0)); + assert_eq!(add((-1, -1), (-1, -1)), (-2, -2)); + + assert_eq!(add((1, 500_000_000), (1, 500_000_000)), (3, 0)); + assert_eq!(add((-1, -500_000_000), (-1, -500_000_000)), (-3, 0)); + assert_eq!( + add((5, 200_000_000), (-1, -500_000_000)), + (3, 700_000_000) + ); + assert_eq!( + add((-5, -200_000_000), (1, 500_000_000)), + (-3, -700_000_000) + ); + } + + #[test] + fn add_overflow() { + let add = |(secs1, nanos1): (i64, i32), + (secs2, nanos2): (i64, i32)| + -> Option<(i64, i32)> { + let d1 = SignedDuration::new(secs1, nanos1); + let d2 = SignedDuration::new(secs2, nanos2); + d1.checked_add(d2).map(|d| (d.as_secs(), d.subsec_nanos())) + }; + assert_eq!(None, add((i64::MAX, 0), (1, 0))); + assert_eq!(None, add((i64::MIN, 0), (-1, 0))); + assert_eq!(None, add((i64::MAX, 1), (0, 999_999_999))); + assert_eq!(None, add((i64::MIN, -1), (0, -999_999_999))); + } +} diff --git a/src/span.rs b/src/span.rs index b8f3f84..91229fe 100644 --- a/src/span.rs +++ b/src/span.rs @@ -12,7 +12,7 @@ use crate::{ round::increment, t::{self, Constant, NoUnits, NoUnits128, Sign, C}, }, - RoundMode, Timestamp, Zoned, + RoundMode, SignedDuration, Timestamp, Zoned, }; /// A span of time represented via a mixture of calendar and clock units. @@ -2023,6 +2023,138 @@ impl Span { options.round(self) } + /// Converts a `Span` to a [`SignedDuration`] relative to the date given. + /// + /// In most cases, it is unlikely that you'll need to use this routine to + /// convert a `Span` to a `SignedDuration`. Namely, every Jiff routine for + /// computing a `Span` between datetimes ([`Zoned`], [`Timestamp`], + /// [`DateTime`], etc.) will return spans with uniform units by default. + /// That is, _by default_: + /// + /// * [`Zoned::until`] guarantees that the biggest non-zero unit is hours. + /// * [`Timestamp::until`] guarantees that the biggest non-zero unit is + /// seconds. + /// * [`DateTime::until`] guarantees that the biggest non-zero unit is + /// days. + /// * [`Date::until`] guarantees that the biggest non-zero unit is days. + /// * [`Time::until`] guarantees that the biggest non-zero unit is hours. + /// + /// Of course, this can be changed by asking, for example, `Zoned::until` + /// to return units up to years. But by default, in every case above, + /// converting the resulting `Span` to a `SignedDuration` can be done + /// _correctly_ without providing a relative date. This conversion is done + /// with the `TryFrom for SignedDuration` trait implementation. + /// (Which will error when given a span with non-zero units bigger than + /// days.) + /// + /// # Planned breaking change + /// + /// It is planned to rename this routine to `Span::to_duration` in `jiff + /// 0.2`. The current `Span::to_duration` routine, which returns a + /// `std::time::Duration`, will be removed. If callers need to convert + /// a `Span` to a `std::time::Duration`, then they should first convert + /// it to a `SignedDuration`, and then to a `std::time::Duration` via + /// `TryFrom for Duration`. + /// + /// # Errors + /// + /// This returns an error if adding this span to the date given results in + /// overflow. + /// + /// # Example: converting a span with calendar units to a `SignedDuration` + /// + /// This compares the number of seconds in a non-leap year with a leap + /// year: + /// + /// ``` + /// use jiff::{civil::date, SignedDuration, Span, ToSpan}; + /// + /// let span = 1.year(); + /// + /// let duration = span.to_jiff_duration(date(2024, 1, 1))?; + /// assert_eq!(duration, SignedDuration::from_secs(31_622_400)); + /// let duration = span.to_jiff_duration(date(2023, 1, 1))?; + /// assert_eq!(duration, SignedDuration::from_secs(31_536_000)); + /// + /// # Ok::<(), Box>(()) + /// ``` + #[inline] + pub fn to_jiff_duration<'a>( + &self, + relative: impl Into>, + ) -> Result { + let max_unit = self.largest_unit(); + let relative: SpanRelativeTo<'a> = relative.into(); + if !relative.is_variable(max_unit) { + return Ok(self.to_jiff_duration_invariant()); + } + let relspan = relative + .to_relative() + .and_then(|r| r.into_relative_span(Unit::Second, *self)) + .with_context(|| { + err!( + "could not compute normalized relative span \ + from datetime {relative} and span {self}", + relative = relative.kind, + ) + })?; + debug_assert!(relspan.span.largest_unit() <= Unit::Second); + Ok(relspan.span.to_jiff_duration_invariant()) + } + + /// Converts an entirely invariant span to a `SignedDuration`. + /// + /// Callers must ensure that this span has no units greater than days. If + /// it does have non-zero units of days, then every day is considered 24 + /// hours. + #[inline] + fn to_jiff_duration_invariant(&self) -> SignedDuration { + // This guarantees, at compile time, that a maximal invariant Span + // (that is, all units are days or lower and all units are set to their + // maximum values) will still balance out to a number of seconds that + // fits into a `i64`. This in turn implies that a `SignedDuration` can + // represent all possible invariant positive spans. + const _FITS_IN_U64: () = { + debug_assert!( + i64::MAX as i128 + > ((t::SpanDays::MAX * t::SECONDS_PER_CIVIL_DAY.bound()) + + (t::SpanHours::MAX * t::SECONDS_PER_HOUR.bound()) + + (t::SpanMinutes::MAX + * t::SECONDS_PER_MINUTE.bound()) + + t::SpanSeconds::MAX + + (t::SpanMilliseconds::MAX + / t::MILLIS_PER_SECOND.bound()) + + (t::SpanMicroseconds::MAX + / t::MICROS_PER_SECOND.bound()) + + (t::SpanNanoseconds::MAX + / t::NANOS_PER_SECOND.bound())), + ); + () + }; + + let nanos = self.to_invariant_nanoseconds(); + debug_assert!( + self.largest_unit() <= Unit::Day, + "units must be days or lower" + ); + + let seconds = nanos / t::NANOS_PER_SECOND; + let seconds = i64::from(seconds); + let subsec_nanos = nanos % t::NANOS_PER_SECOND; + // OK because % 1_000_000_000 above guarantees that the result fits + // in a i32. + let subsec_nanos = i32::try_from(subsec_nanos).unwrap(); + + // SignedDuration::new can panic if |subsec_nanos| >= 1_000_000_000 + // and seconds == {i64::MIN,i64::MAX}. But this can never happen + // because we guaranteed by construction above that |subsec_nanos| < + // 1_000_000_000. + SignedDuration::new(seconds, subsec_nanos) + } +} + +/// Deprecated APIs on `Span`. +impl Span { /// Converts a non-negative `Span` to an unsigned [`std::time::Duration`] /// relative to the date given. /// @@ -2095,6 +2227,7 @@ impl Span { /// /// # Ok::<(), Box>(()) /// ``` + #[deprecated(since = "0.1.5", note = "use Span::to_jiff_duration instead")] #[inline] pub fn to_duration<'a>( &self, @@ -2941,7 +3074,7 @@ impl core::ops::Mul for i64 { /// a reference date. /// /// This can never result in overflow because a `Duration` can represent a -/// bigger span of time than `Span` limits to units of days or lower. +/// bigger span of time than `Span` when limited to units of days or lower. /// /// If you need to convert a `Span` to a `Duration` that has non-zero units /// bigger than days (or a `Span` with days of non-uniform length), then please @@ -3098,6 +3231,139 @@ impl TryFrom for Span { } } +/// Converts a `Span` to a [`SignedDuration`]. +/// +/// Note that this assumes that days are always 24 hours long. +/// +/// # Errors +/// +/// This can fail for only when the span has any non-zero units greater than +/// days. This is an error because it's impossible to determine the length of, +/// e.g., a month without a reference date. +/// +/// This can never result in overflow because a `SignedDuration` can represent +/// a bigger span of time than `Span` when limited to units of days or lower. +/// +/// If you need to convert a `Span` to a `SignedDuration` that has non-zero +/// units bigger than days (or a `Span` with days of non-uniform length), then +/// please use [`Span::to_jiff_duration`] with a corresponding relative date. +/// +/// # Example: maximal span +/// +/// This example shows the maximum possible span using units of days or +/// smaller, and the corresponding `SignedDuration` value: +/// +/// ``` +/// use jiff::{SignedDuration, Span}; +/// +/// let sp = Span::new() +/// .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_417_600_000_000i64) +/// .nanoseconds(9_223_372_036_854_775_807i64); +/// let duration = SignedDuration::try_from(sp)?; +/// assert_eq!(duration, SignedDuration::new(3_795_867_877_636, 854_775_807)); +/// +/// # Ok::<(), Box>(()) +/// ``` +impl TryFrom for SignedDuration { + type Error = Error; + + #[inline] + fn try_from(sp: Span) -> Result { + if sp.largest_unit() > Unit::Day { + return Err(err!( + "cannot convert span with non-zero {unit}, \ + must use Span::to_duration with a relative date \ + instead", + unit = sp.largest_unit().plural(), + )); + } + Ok(sp.to_jiff_duration_invariant()) + } +} + +/// Converts a [`SignedDuration`] to a `Span`. +/// +/// The span returned from this conversion will only ever have non-zero units +/// of seconds or smaller. +/// +/// # Errors +/// +/// This only fails when the given `SignedDuration` overflows the maximum +/// number of seconds representable by a `Span`. +/// +/// # Example +/// +/// This shows a basic conversion: +/// +/// ``` +/// use jiff::{SignedDuration, Span, ToSpan}; +/// +/// let duration = SignedDuration::new(86_400, 123_456_789); +/// let span = Span::try_from(duration)?; +/// // A duration-to-span conversion always results in a span with +/// // non-zero units no bigger than seconds. +/// assert_eq!( +/// span, +/// 86_400.seconds().milliseconds(123).microseconds(456).nanoseconds(789), +/// ); +/// +/// # Ok::<(), Box>(()) +/// ``` +/// +/// # Example: rounding +/// +/// This example shows how to convert a `SignedDuration` to a `Span`, and then +/// round it up to bigger units given a relative date: +/// +/// ``` +/// use jiff::{civil::date, SignedDuration, Span, SpanRound, ToSpan, Unit}; +/// +/// let duration = SignedDuration::new(450 * 86_401, 0); +/// let span = Span::try_from(duration)?; +/// // We get back a simple span of just seconds: +/// assert_eq!(span, Span::new().seconds(450 * 86_401)); +/// // But we can balance it up to bigger units: +/// let options = SpanRound::new() +/// .largest(Unit::Year) +/// .relative(date(2024, 1, 1)); +/// assert_eq!( +/// span.round(options)?, +/// 1.year().months(2).days(25).minutes(7).seconds(30), +/// ); +/// +/// # Ok::<(), Box>(()) +/// ``` +impl TryFrom for Span { + type Error = Error; + + #[inline] + fn try_from(d: SignedDuration) -> Result { + let seconds = d.as_secs(); + let nanoseconds = i64::from(d.subsec_nanos()); + let milliseconds = nanoseconds / t::NANOS_PER_MILLI.value(); + let microseconds = (nanoseconds % t::NANOS_PER_MILLI.value()) + / t::NANOS_PER_MICRO.value(); + let nanoseconds = nanoseconds % t::NANOS_PER_MICRO.value(); + + let span = Span::new().try_seconds(seconds).with_context(|| { + err!("signed duration {d:?} overflows limits of a Jiff `Span`") + })?; + // These are all OK because `|SignedDuration::subsec_nanos|` is + // guaranteed to return less than 1_000_000_000 nanoseconds. And + // splitting that up into millis, micros and nano components is + // guaranteed to fit into the limits of a `Span`. + Ok(span + .milliseconds(milliseconds) + .microseconds(microseconds) + .nanoseconds(nanoseconds)) + } +} + #[cfg(feature = "serde")] impl serde::Serialize for Span { #[inline] diff --git a/src/timestamp.rs b/src/timestamp.rs index 5d13ec7..d686190 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -14,7 +14,7 @@ use crate::{ }, }, zoned::Zoned, - RoundMode, Span, SpanRound, Unit, + RoundMode, SignedDuration, Span, SpanRound, Unit, }; /// An instant in time represented as the number of nanoseconds since the Unix @@ -649,48 +649,21 @@ impl Timestamp { Ok(Timestamp::from_nanosecond_ranged(nanosecond)) } - /// Creates a new instant from a `Duration` since the Unix epoch. - /// - /// A `Duration` is always positive. If you need to construct - /// a timestamp before the Unix epoch with a `Duration`, use - /// [`Timestamp::from_signed_duration`]. - /// - /// # Errors - /// - /// This returns an error if the given duration corresponds to a timestamp - /// greater than [`Timestamp::MAX`]. - /// - /// # Example - /// - /// How one might construct a `Timestamp` from a `SystemTime` if one can - /// assume the time is after the Unix epoch: - /// - /// ``` - /// use std::time::SystemTime; - /// use jiff::Timestamp; - /// - /// let elapsed = SystemTime::UNIX_EPOCH.elapsed()?; - /// assert!(Timestamp::from_duration(elapsed).is_ok()); - /// - /// # Ok::<(), Box>(()) - /// ``` - /// - /// Of course, one should just use [`Timestamp::try_from`] for this - /// instead. - #[cfg(feature = "std")] - #[inline] - pub fn from_duration( - duration: std::time::Duration, - ) -> Result { - Timestamp::from_signed_duration(1, duration) - } - /// Creates a new timestamp from a `Duration` with the given sign since the /// Unix epoch. /// /// Positive durations result in a timestamp after the Unix epoch. Negative /// durations result in a timestamp before the Unix epoch. /// + /// # Planned breaking change + /// + /// It is planned to rename this routine to `Timestamp::from_duration` in + /// `jiff 0.2`. The current `Timestamp::from_duration` routine, which + /// accepts a `std::time::Duration`, will be removed. If callers need to + /// build a `Timestamp` from a `std::time::Duration`, then they should + /// first convert it to a `SignedDuration` via + /// `TryFrom for SignedDuration`. + /// /// # Errors /// /// This returns an error if the given duration corresponds to a timestamp @@ -732,27 +705,11 @@ impl Timestamp { /// /// # Ok::<(), Box>(()) /// ``` - #[cfg(feature = "std")] #[inline] - pub fn from_signed_duration( - sign: i8, - duration: std::time::Duration, + pub fn from_jiff_duration( + duration: SignedDuration, ) -> Result { - let sign = sign.signum(); - let seconds = i64::try_from(duration.as_secs()).map_err(|_| { - Error::unsigned( - "duration seconds", - duration.as_secs(), - UnixSeconds::MIN_REPR, - UnixSeconds::MAX_REPR, - ) - })?; - let nanos = i32::try_from(duration.subsec_nanos()) - .expect("nanoseconds in duration are less than 1,000,000,000"); - // NOTE: Can multiplication actually fail here? I think if `seconds` is - // `i64::MIN`? But, no, I don't think so. Since `duration` is always - // positive. - Timestamp::new(seconds * i64::from(sign), nanos * i32::from(sign)) + Timestamp::new(duration.as_secs(), duration.subsec_nanos()) } /// Returns this timestamp as a number of seconds since the Unix epoch. @@ -962,45 +919,44 @@ impl Timestamp { self.subsec_nanosecond_ranged().get() } - /// Returns this timestamp as a standard library - /// [`Duration`](std::time::Duration) since the Unix epoch. + /// Returns this timestamp as a [`SignedDuration`] since the Unix epoch. /// - /// Since a `Duration` is unsigned and a `Timestamp` is signed, this - /// also returns the sign of this timestamp (`-1`, `0` or `1`) along with - /// the unsigned `Duration`. A negative sign means the duration should be - /// subtracted from the Unix epoch. A positive sign means the duration - /// should be added to the Unix epoch. A zero sign means the duration is - /// the same precise instant as the Unix epoch. + /// # Planned breaking change + /// + /// It is planned to rename this routine to `Timestamp::as_duration` in + /// `jiff 0.2`. The current `Timestamp::as_duration` routine, which returns + /// a `std::time::Duration`, will be removed. If callers need a + /// `std::time::Duration` from a `Timestamp`, then they should call this + /// routine and then use `TryFrom for Duration` to convert + /// the result. /// /// # Example /// /// ``` - /// use std::time::Duration; - /// use jiff::Timestamp; + /// use jiff::{SignedDuration, Timestamp}; /// /// assert_eq!( - /// Timestamp::UNIX_EPOCH.as_duration(), - /// (0, Duration::ZERO), + /// Timestamp::UNIX_EPOCH.as_jiff_duration(), + /// SignedDuration::ZERO, /// ); /// assert_eq!( - /// Timestamp::new(5, 123_456_789)?.as_duration(), - /// (1, Duration::new(5, 123_456_789)), + /// Timestamp::new(5, 123_456_789)?.as_jiff_duration(), + /// SignedDuration::new(5, 123_456_789), /// ); /// assert_eq!( - /// Timestamp::new(-5, -123_456_789)?.as_duration(), - /// (-1, Duration::new(5, 123_456_789)), + /// Timestamp::new(-5, -123_456_789)?.as_jiff_duration(), + /// SignedDuration::new(-5, -123_456_789), /// ); /// /// # Ok::<(), Box>(()) /// ``` - #[cfg(feature = "std")] #[inline] - pub fn as_duration(self) -> (i8, std::time::Duration) { - let second = u64::try_from(self.as_second().abs()) - .expect("absolute value of seconds fits in u64"); - let nanosecond = u32::try_from(self.subsec_nanosecond().abs()) - .expect("nanosecond always fit in a u32"); - (self.signum(), std::time::Duration::new(second, nanosecond)) + pub fn as_jiff_duration(self) -> SignedDuration { + let second = i64::try_from(self.as_second()) + .expect("value of seconds fits in i64"); + let nanosecond = i32::try_from(self.subsec_nanosecond()) + .expect("nanosecond always fit in a i32"); + SignedDuration::new(second, nanosecond) } /// Returns the sign of this timestamp. @@ -1988,6 +1944,166 @@ impl Timestamp { } } +/// Deprecated APIs on `Timestamp`. +impl Timestamp { + /// Creates a new instant from a `Duration` since the Unix epoch. + /// + /// A `Duration` is always positive. If you need to construct + /// a timestamp before the Unix epoch with a `Duration`, use + /// [`Timestamp::from_signed_duration`]. + /// + /// # Errors + /// + /// This returns an error if the given duration corresponds to a timestamp + /// greater than [`Timestamp::MAX`]. + /// + /// # Example + /// + /// How one might construct a `Timestamp` from a `SystemTime` if one can + /// assume the time is after the Unix epoch: + /// + /// ``` + /// use std::time::SystemTime; + /// use jiff::Timestamp; + /// + /// let elapsed = SystemTime::UNIX_EPOCH.elapsed()?; + /// assert!(Timestamp::from_duration(elapsed).is_ok()); + /// + /// # Ok::<(), Box>(()) + /// ``` + /// + /// Of course, one should just use [`Timestamp::try_from`] for this + /// instead. + #[deprecated( + since = "0.1.5", + note = "use Timestamp::from_jiff_duration instead" + )] + #[inline] + pub fn from_duration( + duration: core::time::Duration, + ) -> Result { + #[allow(deprecated)] + Timestamp::from_signed_duration(1, duration) + } + + /// Creates a new timestamp from a `Duration` with the given sign since the + /// Unix epoch. + /// + /// Positive durations result in a timestamp after the Unix epoch. Negative + /// durations result in a timestamp before the Unix epoch. + /// + /// # Errors + /// + /// This returns an error if the given duration corresponds to a timestamp + /// outside of the [`Timestamp::MIN`] and [`Timestamp::MAX`] boundaries. + /// + /// # Example + /// + /// How one might construct a `Timestamp` from a `SystemTime`: + /// + /// ``` + /// use std::time::SystemTime; + /// use jiff::Timestamp; + /// + /// let unix_epoch = SystemTime::UNIX_EPOCH; + /// let now = SystemTime::now(); + /// let (duration, sign) = match now.duration_since(unix_epoch) { + /// Ok(duration) => (duration, 1), + /// Err(err) => (err.duration(), -1), + /// }; + /// + /// let ts = Timestamp::from_signed_duration(sign, duration)?; + /// assert!(ts > Timestamp::UNIX_EPOCH); + /// + /// # Ok::<(), Box>(()) + /// ``` + /// + /// Of course, one should just use [`Timestamp::try_from`] for this + /// instead. Indeed, the above example is copied almost exactly from the + /// `TryFrom` implementation. + /// + /// # Example: a sign of 0 always results in `Timestamp::UNIX_EPOCH` + /// + /// ``` + /// use jiff::Timestamp; + /// + /// let duration = std::time::Duration::new(5, 123_456_789); + /// let ts = Timestamp::from_signed_duration(0, duration)?; + /// assert_eq!(ts, Timestamp::UNIX_EPOCH); + /// + /// # Ok::<(), Box>(()) + /// ``` + #[deprecated( + since = "0.1.5", + note = "use Timestamp::from_jiff_duration instead" + )] + #[inline] + pub fn from_signed_duration( + sign: i8, + duration: core::time::Duration, + ) -> Result { + let sign = sign.signum(); + let seconds = i64::try_from(duration.as_secs()).map_err(|_| { + Error::unsigned( + "duration seconds", + duration.as_secs(), + UnixSeconds::MIN_REPR, + UnixSeconds::MAX_REPR, + ) + })?; + let nanos = i32::try_from(duration.subsec_nanos()) + .expect("nanoseconds in duration are less than 1,000,000,000"); + // NOTE: Can multiplication actually fail here? I think if `seconds` is + // `i64::MIN`? But, no, I don't think so. Since `duration` is always + // positive. + Timestamp::new(seconds * i64::from(sign), nanos * i32::from(sign)) + } + + /// Returns this timestamp as a standard library + /// [`Duration`](std::time::Duration) since the Unix epoch. + /// + /// Since a `Duration` is unsigned and a `Timestamp` is signed, this + /// also returns the sign of this timestamp (`-1`, `0` or `1`) along with + /// the unsigned `Duration`. A negative sign means the duration should be + /// subtracted from the Unix epoch. A positive sign means the duration + /// should be added to the Unix epoch. A zero sign means the duration is + /// the same precise instant as the Unix epoch. + /// + /// # Example + /// + /// ``` + /// use std::time::Duration; + /// use jiff::Timestamp; + /// + /// assert_eq!( + /// Timestamp::UNIX_EPOCH.as_duration(), + /// (0, Duration::ZERO), + /// ); + /// assert_eq!( + /// Timestamp::new(5, 123_456_789)?.as_duration(), + /// (1, Duration::new(5, 123_456_789)), + /// ); + /// assert_eq!( + /// Timestamp::new(-5, -123_456_789)?.as_duration(), + /// (-1, Duration::new(5, 123_456_789)), + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + #[deprecated( + since = "0.1.5", + note = "use Timestamp::as_signed_duration instead" + )] + #[inline] + pub fn as_duration(self) -> (i8, core::time::Duration) { + let second = u64::try_from(self.as_second().abs()) + .expect("absolute value of seconds fits in u64"); + let nanosecond = u32::try_from(self.subsec_nanosecond().abs()) + .expect("nanosecond always fit in a u32"); + (self.signum(), std::time::Duration::new(second, nanosecond)) + } +} + /// Internal APIs using Jiff ranged integers. impl Timestamp { #[inline] @@ -2308,18 +2424,15 @@ impl From for std::time::SystemTime { #[inline] fn from(time: Timestamp) -> std::time::SystemTime { let unix_epoch = std::time::SystemTime::UNIX_EPOCH; - let (sign, duration) = time.as_duration(); + let sdur = time.as_jiff_duration(); + let dur = sdur.unsigned_abs(); // These are guaranteed to succeed because we assume that SystemTime // uses at least 64 bits for the time, and our durations are capped via // the range on UnixSeconds. - if sign >= 0 { - unix_epoch - .checked_add(duration) - .expect("duration too big (positive)") + if sdur.is_negative() { + unix_epoch.checked_sub(dur).expect("duration too big (negative)") } else { - unix_epoch - .checked_sub(duration) - .expect("duration too big (negative)") + unix_epoch.checked_add(dur).expect("duration too big (positive)") } } } @@ -2333,11 +2446,8 @@ impl TryFrom for Timestamp { system_time: std::time::SystemTime, ) -> Result { let unix_epoch = std::time::SystemTime::UNIX_EPOCH; - let (duration, sign) = match system_time.duration_since(unix_epoch) { - Ok(duration) => (duration, 1), - Err(err) => (err.duration(), -1), - }; - Timestamp::from_signed_duration(sign, duration) + let dur = SignedDuration::until(unix_epoch, system_time)?; + Timestamp::from_jiff_duration(dur) } }