Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow choice of rounding behavior for ties #12

Merged
merged 1 commit into from
Mar 17, 2024
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
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
62 changes: 49 additions & 13 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`).

This crate is does not need `std` or `alloc` (it’s always in `no_std` mode). No
features need to be enabled or disabled.
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)

### Example

```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,17 @@ 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
[`Roundable`]: https://docs.rs/roundable/0.1.1/roundable/trait.Roundable.html
[`try_round_to()`]: https://docs.rs/roundable/0.1.1/roundable/trait.Roundable.html#tymethod.try_round_to
[`round_to()`]: https://docs.rs/roundable/0.1.1/roundable/trait.Roundable.html#method.round_to
[`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