Skip to content

Commit

Permalink
Allow choice of rounding behavior for ties
Browse files Browse the repository at this point in the history
Traditionally, rounding a value that is exactly halfway between to round
values results in the higher round value:

    10 == 5.round_to(10, Tie::Up)

This adds a parameter and `enum` to allow other rules for dealing with
“ties” — values that are halfway between two round numbers. The rule
mentioned above is represented by `Tie::Up` — ties choose the higher
number. The complete list of options is:

  * `Tie::Up`
  * `Tie::Down`
  * `Tie::TowardZero`
  * `Tie::AwayFromZero`
  * `Tie::TowardEven`
  * `Tie::TowardOdd`

Fixes #3 — Support other types of rounding.
  • Loading branch information
danielparks committed Mar 17, 2024
1 parent 0fc436d commit cebd5d4
Show file tree
Hide file tree
Showing 5 changed files with 809 additions and 197 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.

## main branch

* Allow selecting the rule for dealing with values that are exactly halfway
between two round numbers. For example, what does `5.round_to(10)` return?

## Release 0.1.1 (2024-03-14)

* Make `no_std` by default.
Expand Down
55 changes: 44 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,61 @@
![Rust version 1.56.1+](https://img.shields.io/badge/Rust%20version-1.56.1%2B-success)

This provides an implementation of rounding for various values, including the
native number types and [`core::time::Duration`][`Duration`] (or
native number types and [`core::time::Duration`][`Duration`] (also known as
`std::time::Duration`).

The [`Roundable`] trait adds the following functions to roundable values:

* [`Roundable::try_round_to(factor, tie_strategy)`][try_round_to] (returns
`None` on overflow)
* [`Roundable::round_to(factor, tie_strategy)`][round_to] (panics on overflow)

This crate is does not need `std` or `alloc` (it’s always in `no_std` mode). No
features need to be enabled or disabled.

```rust
use roundable::Roundable;
use roundable::{Roundable, Tie};

assert!(310 == 314.round_to(10));
assert!(300.0 == 314.1.round_to(100.0));
assert!(310 == 314.round_to(10, Tie::Up));
assert!(300.0 == 314.1.round_to(100.0, Tie::Up));

// To avoid panicking on overflow:
assert!(Some(260) == 255.try_round_to(10));
assert!(None == 255u8.try_round_to(10));
assert!(Some(260) == 255.try_round_to(10, Tie::Up));
assert!(None == 255u8.try_round_to(10, Tie::Up));
```

### Tie strategies

“Ties” are numbers exactly halfway between two round numbers, e.g. 0.5 when
rounding to the nearest whole number. Traditionally, ties are resolved by
picking the higher number, but there are other strategies. `Roundable` supports
the following rules:

* [`Tie::Up`]: Round ties up (what most people consider correct).
* [`Tie::Down`]: Round ties down.
* [`Tie::TowardZero`]: Round ties toward zero.
* [`Tie::AwayFromZero`]: Round ties away from zero.
* [`Tie::TowardEven`]: Round ties toward the “even” number (see docs).
* [`Tie::TowardOdd`]: Round ties toward the “odd” number (see docs).

### Rounding `Duration`

See [the list of constants][constants] for a list of time units that make
rounding [`Duration`][] easier.

```rust
use roundable::{SECOND, MINUTE, Roundable};
use roundable::{SECOND, MINUTE, Roundable, Tie};
use std::time::Duration;

assert!(Duration::ZERO == Duration::from_millis(314).round_to(SECOND));
assert!(MINUTE == Duration::from_millis(59_500).round_to(SECOND));
assert!(Duration::ZERO == Duration::from_millis(314).round_to(SECOND, Tie::Up));
assert!(MINUTE == Duration::from_millis(59_500).round_to(SECOND, Tie::Up));
```

## `#![no_std]` by default

You can use this crate with or without `std` and `alloc`. You do not need to
enable or disable features either way.

## ⚠️ Development status

This is in active development. The API may be entirely rewritten. I am open to
Expand All @@ -57,8 +84,14 @@ Unless you explicitly state otherwise, any contribution you submit as defined
in the Apache 2.0 license shall be dual licensed as above, without any
additional terms or conditions.

[docs.rs]: https://docs.rs/roundable/latest/roundable/
[docs.rs]: https://docs.rs/roundable/0.1.1/roundable/
[crates.io]: https://crates.io/crates/roundable
[issues]: https://github.com/danielparks/roundable/issues
[`Duration`]: https://doc.rust-lang.org/core/time/struct.Duration.html
[Constants]: https://docs.rs/roundable/latest/roundable/#constants
[`Tie::Up`]: https://docs.rs/roundable/0.1.1/roundable/enum.Tie.html#variant.Up
[`Tie::Down`]: https://docs.rs/roundable/0.1.1/roundable/enum.Tie.html#variant.Down
[`Tie::TowardZero`]: https://docs.rs/roundable/0.1.1/roundable/enum.Tie.html#variant.TowardZero
[`Tie::AwayFromZero`]: https://docs.rs/roundable/0.1.1/roundable/enum.Tie.html#variant.AwayFromZero
[`Tie::TowardEven`]: https://docs.rs/roundable/0.1.1/roundable/enum.Tie.html#variant.TowardEven
[`Tie::TowardOdd`]: https://docs.rs/roundable/0.1.1/roundable/enum.Tie.html#variant.TowardOdd
[Constants]: https://docs.rs/roundable/0.1.1/roundable/#constants
5 changes: 5 additions & 0 deletions release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ awk-in-place Cargo.toml '
}
{ print }'

awk-in-place README.md '{
sub(/https:\/\/docs\.rs\/roundable\/[0-9]+.[0-9]+.[0-9]+\//, "https://docs.rs/roundable/'$version'/")
print
}'

cargo check --quiet

cargo semver-checks check-release
Expand Down
78 changes: 39 additions & 39 deletions src/duration.rs
Original file line number Diff line number Diff line change
@@ -1,63 +1,63 @@
//! Functions, constants, etc. related to Duration.

use crate::Roundable;
use crate::{Roundable, Tie};
use core::time::Duration;

/// A microsecond. Useful for rounding [`Duration`].
///
/// ```rust
/// use roundable::{MICROSECOND, Roundable};
/// use roundable::{MICROSECOND, Roundable, Tie};
/// use std::time::Duration;
///
/// assert!(MICROSECOND == Duration::from_nanos(500).round_to(MICROSECOND));
/// assert!(MICROSECOND == Duration::from_nanos(500).round_to(MICROSECOND, Tie::Up));
/// ```
pub const MICROSECOND: Duration = Duration::from_micros(1);

/// A millisecond. Useful for rounding [`Duration`].
///
/// ```rust
/// use roundable::{MILLISECOND, Roundable};
/// use roundable::{MILLISECOND, Roundable, Tie};
/// use std::time::Duration;
///
/// assert!(MILLISECOND == Duration::from_micros(500).round_to(MILLISECOND));
/// assert!(MILLISECOND == Duration::from_micros(500).round_to(MILLISECOND, Tie::Up));
/// ```
pub const MILLISECOND: Duration = Duration::from_millis(1);

/// A second. Useful for rounding [`Duration`].
///
/// ```rust
/// use roundable::{SECOND, Roundable};
/// use roundable::{SECOND, Roundable, Tie};
/// use std::time::Duration;
///
/// assert!(SECOND == Duration::from_millis(500).round_to(SECOND));
/// assert!(SECOND == Duration::from_millis(500).round_to(SECOND, Tie::Up));
/// ```
pub const SECOND: Duration = Duration::from_secs(1);

/// A minute. Useful for rounding [`Duration`].
///
/// ```rust
/// use roundable::{MINUTE, Roundable};
/// use roundable::{MINUTE, Roundable, Tie};
/// use std::time::Duration;
///
/// assert!(MINUTE == Duration::from_secs(30).round_to(MINUTE));
/// assert!(MINUTE == Duration::from_secs(30).round_to(MINUTE, Tie::Up));
/// ```
pub const MINUTE: Duration = Duration::from_secs(60);

/// An hour. Useful for rounding [`Duration`].
///
/// ```rust
/// use roundable::{HOUR, Roundable};
/// use roundable::{HOUR, Roundable, Tie};
/// use std::time::Duration;
///
/// assert!(HOUR == Duration::from_secs(30*60).round_to(HOUR));
/// assert!(HOUR == Duration::from_secs(30*60).round_to(HOUR, Tie::Up));
/// ```
pub const HOUR: Duration = Duration::from_secs(60 * 60);

impl Roundable for Duration {
fn try_round_to(self, factor: Self) -> Option<Self> {
fn try_round_to(self, factor: Self, tie: Tie) -> Option<Self> {
// Duration will always fit into u128 as nanoseconds.
self.as_nanos()
.try_round_to(factor.as_nanos())
.try_round_to(factor.as_nanos(), tie)
.map(nanos_to_duration)
}
}
Expand Down Expand Up @@ -109,53 +109,53 @@ mod tests {

#[test]
fn round_millisecond_to_nearest_millisecond() {
check!(ms(10) == ms(10).round_to(MILLISECOND));
check!(ms(10) == ms(10).round_to(MILLISECOND, Tie::Up));

check!(ms(10) == ms(10).round_to(ms(2)));
check!(ms(10) == ms(9).round_to(ms(2)));
check!(ms(10) == ms(10).round_to(ms(2), Tie::Up));
check!(ms(10) == ms(9).round_to(ms(2), Tie::Up));

check!(ms(9) == ms(9).round_to(ms(3)));
check!(ms(9) == ms(10).round_to(ms(3)));
check!(ms(12) == ms(11).round_to(ms(3)));
check!(ms(12) == ms(12).round_to(ms(3)));
check!(ms(9) == ms(9).round_to(ms(3), Tie::Up));
check!(ms(9) == ms(10).round_to(ms(3), Tie::Up));
check!(ms(12) == ms(11).round_to(ms(3), Tie::Up));
check!(ms(12) == ms(12).round_to(ms(3), Tie::Up));
}

#[test]
fn round_second_to_nearest_millisecond() {
check!(ms(1_010) == ms(1_010).round_to(MILLISECOND));
check!(ms(1_010) == ms(1_010).round_to(MILLISECOND, Tie::Up));

check!(ms(1_010) == ms(1_010).round_to(ms(2)));
check!(ms(1_010) == ms(1_009).round_to(ms(2)));
check!(ms(1_010) == ms(1_010).round_to(ms(2), Tie::Up));
check!(ms(1_010) == ms(1_009).round_to(ms(2), Tie::Up));

check!(ms(1_008) == ms(1_008).round_to(ms(3)));
check!(ms(1_008) == ms(1_009).round_to(ms(3)));
check!(ms(1_011) == ms(1_010).round_to(ms(3)));
check!(ms(1_011) == ms(1_011).round_to(ms(3)));
check!(ms(1_008) == ms(1_008).round_to(ms(3), Tie::Up));
check!(ms(1_008) == ms(1_009).round_to(ms(3), Tie::Up));
check!(ms(1_011) == ms(1_010).round_to(ms(3), Tie::Up));
check!(ms(1_011) == ms(1_011).round_to(ms(3), Tie::Up));
}

#[test]
fn round_second_to_nearest_second() {
check!(ms(0) == ms(499).round_to(SECOND));
check!(SECOND == ms(500).round_to(SECOND));
check!(SECOND == ms(1_010).round_to(SECOND));
check!(SECOND == ms(1_499).round_to(SECOND));
check!(ms(2_000) == ms(1_500).round_to(SECOND));

check!(ms(1_001) == ms(1_000).round_to(ms(1_001)));
check!(ms(1_001) == ms(1_001).round_to(ms(1_001)));
check!(ms(1_001) == ms(1_002).round_to(ms(1_001)));
check!(ms(0) == ms(499).round_to(SECOND, Tie::Up));
check!(SECOND == ms(500).round_to(SECOND, Tie::Up));
check!(SECOND == ms(1_010).round_to(SECOND, Tie::Up));
check!(SECOND == ms(1_499).round_to(SECOND, Tie::Up));
check!(ms(2_000) == ms(1_500).round_to(SECOND, Tie::Up));

check!(ms(1_001) == ms(1_000).round_to(ms(1_001), Tie::Up));
check!(ms(1_001) == ms(1_001).round_to(ms(1_001), Tie::Up));
check!(ms(1_001) == ms(1_002).round_to(ms(1_001), Tie::Up));
}

#[test]
fn round_to_giant_factor() {
check!(ms(0) == ms(1_000_000).round_to(Duration::MAX));
check!(Duration::MAX == Duration::MAX.round_to(Duration::MAX));
check!(ms(0) == ms(1_000_000).round_to(Duration::MAX, Tie::Up));
check!(Duration::MAX == Duration::MAX.round_to(Duration::MAX, Tie::Up));
}

#[test]
#[should_panic(expected = "try_round_to() requires positive factor")]
fn round_to_zero_factor() {
let _ = ms(10).round_to(ms(0));
let _ = ms(10).round_to(ms(0), Tie::Up);
}

/// Theoretical maximum Duration as nanoseconds (based on u64 for seconds).
Expand Down
Loading

0 comments on commit cebd5d4

Please sign in to comment.