Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions crates/bindings-csharp/BSATN.Runtime/Builtins.cs
Original file line number Diff line number Diff line change
Expand Up @@ -357,10 +357,13 @@ public static Timestamp FromTimeSpanSinceUnixEpoch(TimeSpan timeSpan) =>
public readonly TimeSpan ToTimeSpanSinceUnixEpoch() => (TimeSpan)ToTimeDurationSinceUnixEpoch();

public readonly TimeDuration TimeDurationSince(Timestamp earlier) =>
new TimeDuration(MicrosecondsSinceUnixEpoch - earlier.MicrosecondsSinceUnixEpoch);
new TimeDuration(checked(MicrosecondsSinceUnixEpoch - earlier.MicrosecondsSinceUnixEpoch));

public static Timestamp operator +(Timestamp point, TimeDuration interval) =>
new Timestamp(point.MicrosecondsSinceUnixEpoch + interval.Microseconds);
new Timestamp(checked(point.MicrosecondsSinceUnixEpoch + interval.Microseconds));

public static Timestamp operator -(Timestamp point, TimeDuration interval) =>
new Timestamp(checked(point.MicrosecondsSinceUnixEpoch - interval.Microseconds));

public int CompareTo(Timestamp that)
{
Expand Down Expand Up @@ -430,6 +433,12 @@ public static implicit operator TimeSpan(TimeDuration d) =>
public static implicit operator TimeDuration(TimeSpan timeSpan) =>
new(timeSpan.Ticks / Util.TicksPerMicrosecond);

public static TimeDuration operator +(TimeDuration lhs, TimeDuration rhs) =>
new TimeDuration(checked(lhs.Microseconds + rhs.Microseconds));

public static TimeDuration operator -(TimeDuration lhs, TimeDuration rhs) =>
new TimeDuration(checked(lhs.Microseconds + rhs.Microseconds));

// For backwards-compatibility.
public readonly TimeSpan ToStd() => this;

Expand Down
79 changes: 79 additions & 0 deletions crates/sats/src/time_duration.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::timestamp::MICROSECONDS_PER_SECOND;
use crate::{de::Deserialize, impl_st, ser::Serialize, AlgebraicType, AlgebraicValue};
use std::fmt;
use std::ops::{Add, AddAssign, Sub, SubAssign};
use std::time::Duration;

#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize, Debug)]
Expand Down Expand Up @@ -69,6 +70,16 @@ impl TimeDuration {
.expect("Duration overflows i64 microseconds"),
)
}

/// Returns `Some(self + other)`, or `None` if that value would be out of bounds for [`TimeDuration`].
pub fn checked_add(self, other: Self) -> Option<Self> {
self.to_micros().checked_add(other.to_micros()).map(Self::from_micros)
}

/// Returns `Some(self - other)`, or `None` if that value would be out of bounds for [`TimeDuration`].
pub fn checked_sub(self, other: Self) -> Option<Self> {
self.to_micros().checked_sub(other.to_micros()).map(Self::from_micros)
}
}

impl From<Duration> for TimeDuration {
Expand Down Expand Up @@ -96,6 +107,40 @@ impl fmt::Display for TimeDuration {
}
}

impl Add for TimeDuration {
type Output = Self;

fn add(self, rhs: Self) -> Self::Output {
self.checked_add(rhs).unwrap()
}
}

impl Sub for TimeDuration {
type Output = Self;

fn sub(self, rhs: Self) -> Self::Output {
self.checked_sub(rhs).unwrap()
}
}

impl AddAssign for TimeDuration {
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs;
}
}

impl SubAssign for TimeDuration {
fn sub_assign(&mut self, rhs: Self) {
*self = *self - rhs;
}
}

// `std::time::Duration` has implementations of `Mul<u32>` and `Div<u32>`,
// plus checked methods and assign traits.
// It also has methods for division with floats,
// both `Duration -> Duration -> float` and `Duration -> float -> Duration`.
// We could provide some or all of these, but so far have not seen the need to.

impl From<TimeDuration> for AlgebraicValue {
fn from(value: TimeDuration) -> Self {
AlgebraicValue::product([value.to_micros().into()])
Expand Down Expand Up @@ -134,5 +179,39 @@ mod test {
prop_assert_eq!(time_duration_prime, time_duration);
prop_assert_eq!(time_duration_prime.to_micros(), micros);
}

#[test]
fn arithmetic_as_expected(lhs in any::<i64>(), rhs in any::<i64>()) {
let lhs_time_duration = TimeDuration::from_micros(lhs);
let rhs_time_duration = TimeDuration::from_micros(rhs);

if let Some(sum) = lhs.checked_add(rhs) {
let sum_time_duration = lhs_time_duration.checked_add(rhs_time_duration);
prop_assert!(sum_time_duration.is_some());
prop_assert_eq!(sum_time_duration.unwrap().to_micros(), sum);

prop_assert_eq!((lhs_time_duration + rhs_time_duration).to_micros(), sum);

let mut sum_assign = lhs_time_duration;
sum_assign += rhs_time_duration;
prop_assert_eq!(sum_assign.to_micros(), sum);
} else {
prop_assert!(lhs_time_duration.checked_add(rhs_time_duration).is_none());
}

if let Some(diff) = lhs.checked_sub(rhs) {
let diff_time_duration = lhs_time_duration.checked_sub(rhs_time_duration);
prop_assert!(diff_time_duration.is_some());
prop_assert_eq!(diff_time_duration.unwrap().to_micros(), diff);

prop_assert_eq!((lhs_time_duration - rhs_time_duration).to_micros(), diff);

let mut diff_assign = lhs_time_duration;
diff_assign -= rhs_time_duration;
prop_assert_eq!(diff_assign.to_micros(), diff);
} else {
prop_assert!(lhs_time_duration.checked_sub(rhs_time_duration).is_none());
}
}
}
}
124 changes: 115 additions & 9 deletions crates/sats/src/timestamp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use chrono::DateTime;

use crate::{de::Deserialize, impl_st, ser::Serialize, time_duration::TimeDuration, AlgebraicType, AlgebraicValue};
use std::fmt;
use std::ops::Add;
use std::ops::{Add, AddAssign, Sub, SubAssign};
use std::time::{Duration, SystemTime};

#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize, Debug)]
Expand Down Expand Up @@ -140,6 +140,37 @@ impl Timestamp {
.map(Timestamp::from_micros_since_unix_epoch)
}

/// Returns `Some(t)` where `t` is the time `self + duration` if `t` can be represented as a `Timestamp`,
/// i.e. a 64-bit signed number of microseconds before or after the Unix epoch.
pub fn checked_add(&self, duration: TimeDuration) -> Option<Self> {
self.__timestamp_micros_since_unix_epoch__
.checked_add(duration.to_micros())
.map(Timestamp::from_micros_since_unix_epoch)
}

/// Returns `Some(t)` where `t` is the time `self - duration` if `t` can be represented as a `Timestamp`,
/// i.e. a 64-bit signed number of microseconds before or after the Unix epoch.
pub fn checked_sub(&self, duration: TimeDuration) -> Option<Self> {
self.__timestamp_micros_since_unix_epoch__
.checked_sub(duration.to_micros())
.map(Timestamp::from_micros_since_unix_epoch)
}

/// Returns `Some(self + duration)`, or `None` if that value would be out-of-bounds for `Timestamp`.
///
/// Converts `duration` into a [`TimeDuration`] before the arithmetic.
/// Depending on the target platform's representation of [`Duration`], this may lose precision.
pub fn checked_add_duration(&self, duration: Duration) -> Option<Self> {
self.checked_add(TimeDuration::from_duration(duration))
}

/// Returns `Some(self - duration)`, or `None` if that value would be out-of-bounds for `Timestamp`.
///
/// Converts `duration` into a [`TimeDuration`] before the arithmetic.
/// Depending on the target platform's representation of [`Duration`], this may lose precision.
pub fn checked_sub_duration(&self, duration: Duration) -> Option<Self> {
self.checked_sub(TimeDuration::from_duration(duration))
}
/// Returns an RFC 3339 and ISO 8601 date and time string such as `1996-12-19T16:39:57-08:00`.
pub fn to_rfc3339(&self) -> anyhow::Result<String> {
DateTime::from_timestamp_micros(self.to_micros_since_unix_epoch())
Expand All @@ -153,7 +184,55 @@ impl Add<TimeDuration> for Timestamp {
type Output = Self;

fn add(self, other: TimeDuration) -> Self::Output {
Timestamp::from_micros_since_unix_epoch(self.to_micros_since_unix_epoch() + other.to_micros())
self.checked_add(other).unwrap()
}
}

impl Add<Duration> for Timestamp {
type Output = Self;

fn add(self, other: Duration) -> Self::Output {
self.checked_add_duration(other).unwrap()
}
}

impl Sub<TimeDuration> for Timestamp {
type Output = Self;

fn sub(self, other: TimeDuration) -> Self::Output {
self.checked_sub(other).unwrap()
}
}

impl Sub<Duration> for Timestamp {
type Output = Self;

fn sub(self, other: Duration) -> Self::Output {
self.checked_sub_duration(other).unwrap()
}
}

impl AddAssign<TimeDuration> for Timestamp {
fn add_assign(&mut self, other: TimeDuration) {
*self = *self + other;
}
}

impl AddAssign<Duration> for Timestamp {
fn add_assign(&mut self, other: Duration) {
*self = *self + other;
}
}

impl SubAssign<TimeDuration> for Timestamp {
fn sub_assign(&mut self, rhs: TimeDuration) {
*self = *self - rhs;
}
}

impl SubAssign<Duration> for Timestamp {
fn sub_assign(&mut self, rhs: Duration) {
*self = *self - rhs;
}
}

Expand Down Expand Up @@ -221,13 +300,40 @@ mod test {
}

#[test]
fn add_duration(since_epoch in any::<i64>().prop_map(|n| n.abs()), duration in any::<i64>()) {
prop_assume!(since_epoch.checked_add(duration).is_some());

let timestamp = Timestamp::from_micros_since_unix_epoch(since_epoch);
let time_duration = TimeDuration::from_micros(duration);
let result = timestamp + time_duration;
prop_assert_eq!(result.to_micros_since_unix_epoch(), since_epoch + duration);
fn arithmetic_with_timeduration(lhs in any::<i64>(), rhs in any::<i64>()) {
let lhs_timestamp = Timestamp::from_micros_since_unix_epoch(lhs);
let rhs_time_duration = TimeDuration::from_micros(rhs);

if let Some(sum) = lhs.checked_add(rhs) {
let sum_timestamp = lhs_timestamp.checked_add(rhs_time_duration);
prop_assert!(sum_timestamp.is_some());
prop_assert_eq!(sum_timestamp.unwrap().to_micros_since_unix_epoch(), sum);

prop_assert_eq!((lhs_timestamp + rhs_time_duration).to_micros_since_unix_epoch(), sum);

let mut sum_assign = lhs_timestamp;
sum_assign += rhs_time_duration;
prop_assert_eq!(sum_assign.to_micros_since_unix_epoch(), sum);
} else {
prop_assert!(lhs_timestamp.checked_add(rhs_time_duration).is_none());
}

if let Some(diff) = lhs.checked_sub(rhs) {
let diff_timestamp = lhs_timestamp.checked_sub(rhs_time_duration);
prop_assert!(diff_timestamp.is_some());
prop_assert_eq!(diff_timestamp.unwrap().to_micros_since_unix_epoch(), diff);

prop_assert_eq!((lhs_timestamp - rhs_time_duration).to_micros_since_unix_epoch(), diff);

let mut diff_assign = lhs_timestamp;
diff_assign -= rhs_time_duration;
prop_assert_eq!(diff_assign.to_micros_since_unix_epoch(), diff);
} else {
prop_assert!(lhs_timestamp.checked_sub(rhs_time_duration).is_none());
}
}

// TODO: determine what guarantees we provide for arithmetic with `Duration`,
// then write tests that we uphold said guarantees.
}
}
Loading