Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
args: --lib --all-features -- --test-threads=1
args: --all-features -- --test-threads=1

- name: Test Documentation
uses: actions-rs/cargo@v1
Expand Down
44 changes: 36 additions & 8 deletions src/duration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,14 +443,42 @@ pub struct Generic<T> {
scaling_factor: Fraction,
}

impl<T: TimeInt> Generic<T> {
/// Try to create a new, equivalent `Generic` with the given _scaling factor_
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to help make what is going on here a little clearer:

Suggested change
/// Try to create a new, equivalent `Generic` with the given _scaling factor_
/// Try to create a new, equivalent `Generic` with the given _scaling factor_.
///
/// To minimise errors due to truncation rounding, the new integer component is
/// calculated as `self.integer * (self.scaling_factor / scaling_factor)`.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have stopped contributing here and created fugit instead.

fn try_into_scaling_factor(self, scaling_factor: Fraction) -> Result<Self, ConversionError> {
let new_int = TimeInt::checked_mul_fraction(
&self.integer,
&self
.scaling_factor
.checked_div(&scaling_factor)
.ok_or(ConversionError::Overflow)?,
)
.ok_or(ConversionError::Overflow)?;

Ok(Self::new(new_int, scaling_factor))
}
}

impl<T: TimeInt> PartialOrd<Generic<T>> for Generic<T> {
/// See [Comparisons](trait.Duration.html#comparisons)
fn partial_cmp(&self, rhs: &Generic<T>) -> Option<core::cmp::Ordering> {
Some(
self.integer
.checked_mul_fraction(&self.scaling_factor)?
.cmp(&rhs.integer.checked_mul_fraction(&rhs.scaling_factor)?),
)
if self.scaling_factor == rhs.scaling_factor {
Some(self.integer.cmp(&rhs.integer))
} else if self.scaling_factor < rhs.scaling_factor {
// convert to the smaller scaling factor (rhs -> self)
// if conversion fails, we know self is less than rhs
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like this will be true most of the time, but I'm not entirely convinced that there aren't edge cases where rhs.scaling_factor / self.scaling_factor < self.integer / rhs.integer (so self > rhs), and rhs is close enough to the maximum bound on T that it will overflow. In particular, TimeInt::checked_mul_fraction is being called with rhs.integer and a fraction that is greater than 1, and is implemented by first multiplying by the numerator (which could overflow) and then dividing by the denominator (which if not for the overflow could bring the result back in range and below self.integer). It would be great to have tests that exercise these near-bounds edge cases.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have stopped contributing here and created fugit instead.

match rhs.try_into_scaling_factor(self.scaling_factor) {
Ok(converted_rhs) => Some(self.integer.cmp(&converted_rhs.integer)),
Err(_) => Some(core::cmp::Ordering::Less),
}
} else {
// convert to the smaller scaling factor (self -> rhs)
// if conversion fails, we know rhs is less than self
match self.try_into_scaling_factor(rhs.scaling_factor) {
Ok(converted_self) => Some(converted_self.integer.cmp(&rhs.integer)),
Err(_) => Some(core::cmp::Ordering::Greater),
}
}
}
}

Expand Down Expand Up @@ -884,9 +912,9 @@ pub mod units {
/// See [Comparisons](trait.Duration.html#comparisons)
fn partial_cmp(&self, rhs: &$big<RhsInt>) -> Option<core::cmp::Ordering> {
match Self::try_from(*rhs) {
Ok(rhs) => Some(self.integer().cmp(&rhs.integer())),
Err(_) => Some(core::cmp::Ordering::Less),
}
Ok(rhs) => Some(self.integer().cmp(&rhs.integer())),
Err(_) => Some(core::cmp::Ordering::Less),
}
}
}
)+
Expand Down
8 changes: 8 additions & 0 deletions src/time_int.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ pub trait TimeInt:
fn checked_div_fraction(&self, fraction: &Fraction) -> Option<Self> {
self.checked_mul_fraction(&fraction.recip())
}

/// Moves an integer into a comparable base for checking
fn checked_same_base(&self, fraction: &Fraction, rhs_fraction: &Fraction) -> Option<Self> {
let a_n = *fraction.numerator();
let b_d = *rhs_fraction.denominator();

self.checked_mul(&(b_d.into()))?.checked_mul(&(a_n.into()))
}
Comment on lines +35 to +42
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now unused in the PR after the refactor:

Suggested change
/// Moves an integer into a comparable base for checking
fn checked_same_base(&self, fraction: &Fraction, rhs_fraction: &Fraction) -> Option<Self> {
let a_n = *fraction.numerator();
let b_d = *rhs_fraction.denominator();
self.checked_mul(&(b_d.into()))?.checked_mul(&(a_n.into()))
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have stopped contributing here and created fugit instead.

}

impl TimeInt for u32 {}
Expand Down
47 changes: 47 additions & 0 deletions tests/duration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,53 @@ fn comparisons() {
Generic::new(1_000u32, Fraction::new(1, 1_000))
== Generic::new(2_000u32, Fraction::new(1, 2_000))
);

// Generic comparisons
assert!(
Generic::new(100u32, Fraction::new(1, 1000)) == Generic::new(10u32, Fraction::new(1, 100))
);
assert!(
Generic::new(100u32, Fraction::new(1, 1000)) <= Generic::new(10u32, Fraction::new(1, 100))
);
assert!(
Generic::new(100u32, Fraction::new(1, 1000)) <= Generic::new(11u32, Fraction::new(1, 100))
);
assert!(
Generic::new(200u32, Fraction::new(1, 1000)) >= Generic::new(10u32, Fraction::new(1, 100))
);
assert!(
Generic::new(200u32, Fraction::new(1, 1000)) >= Generic::new(10u32, Fraction::new(1, 100))
);
Comment on lines +70 to +75
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two test cases are identical. These should either be de-duplicated, one of these changed to cover a different part of the comparison space, or a comment added as to why the duplicate is present.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have stopped contributing here and created fugit instead.

assert!(
Generic::new(100u32, Fraction::new(1, 1000)) != Generic::new(100u32, Fraction::new(1, 100))
);
assert!(
Generic::new(101u32, Fraction::new(1, 1000)) > Generic::new(100u32, Fraction::new(1, 1000))
);
assert!(
Generic::new(u32::MAX, Fraction::new(1, 100))
> Generic::new(100u32, Fraction::new(1, 1000))
);

/* Generic comparison stress test:
loop {
let mut rands: [u32; 6] = [0; 6];
for rand in rands.iter_mut() {
unsafe {
loop {
core::arch::x86_64::_rdrand32_step(rand);
if *rand != 0 {
break;
}
}
}
}
println!("rands: {:#?}", rands);
assert!(
Generic::new(rands[0], Fraction::new(rands[1], rands[2]))
!= Generic::new(rands[3], Fraction::new(rands[4], rands[5]))
);
} */
Comment on lines +87 to +105
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be useful to rewrite this using the proptest crate, so you retain the "random checks" testing, while also having bounded runtimes for tests, and reductions of failures to simplified cases.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have stopped contributing here and created fugit instead.

}

#[test]
Expand Down