Skip to content

More arithmetic functions for Duration #706

@max-heller

Description

@max-heller

Proposal

Problem statement

core::time::Duration is a numeric type, but lacks some of the common arithmetic functions provided by analogous types like unsigned integers uN. In particular, the only available Duration by Duration division functions return floats (div_duration_f64 and div_duration_f32), and Duration lacks some of the newer forms of integer arithmetic functions such as strict_{add,div,mul,sub} and (checked_)div_exact (whatever that family of functions will end up looking like when #![feature(exact_div)] is stabilized).

Motivating examples or use cases

In a codebase targeting embedded systems that run tasks at fixed periods ("control cycles"), I find myself manually implementing operations like Duration by Duration integer division (e.g., "how many control cycles make up a timeout?", "how many cycles should I wait between publishing data at a low rate to avoid overwhelming a consumer?") and performing various checked arithmetic operations and unwrapping them.

/// Some task runs every 10ms
const CYCLE_PERIOD: Duration = Duration::from_millis(10);

/// Number of cycles that make up a 2s timeout
// In practice, this needs to be converted to a smaller type such as u16,
// which will be easier when .try_into().unwrap() works in const
const TIMEOUT_CYCLES: u128 = Duration::from_secs(2).as_nanos().div_ceil(CYCLE_PERIOD.as_nanos());

/// The task publishes data to a slower task that runs every 500ms
const SLOW_TASK_CYCLE_PERIOD: Duration = Duration::from_millis(500);
/// To make sure the slow task receives data every cycle but avoid overwhelming it with high-rate data,
/// publish data much slower than the fast task's cycle but twice as fast as (at half the period of)
/// the slow task's cycle
const SLOW_DATA_DOWNSAMPLE_RATE: u128 =
    (SLOW_TASK_CYCLE_PERIOD.checked_div(2).unwrap()) // period at which to publish data
    .as_nanos() / CYCLE_PERIOD.as_nanos(); // data should be published every N "fast task" cycles

GitHub search shows several manual implementations of these operations:

Solution sketch

impl Duration {
    // Integer division alternatives to div_duration_{f32,f64}()
    // div_duration_floor() could be just div_duration(), but _floor seems clearer
    // to disambiguate from _{f32,f64}. 
    // Would parallel uN::div_floor() if that stabilizes (https://github.com/rust-lang/rust/issues/88581)
    const fn div_duration_floor(self, rhs: Self) -> u128
    const fn div_duration_ceil(self, rhs: Self) -> u128

    // uN::(checked_)div_exact() equivalents, mirroring whatever that API ends up looking like
    const fn div_duration_exact(self, rhs: Self) -> Option<u128>

    // Strict (explicitly panicking) Duration x u32 arithmetic
    const fn strict_mul(self, rhs: u32) -> Self
    const fn strict_div(self, rhs: u32) -> Self

    // Strict (explicitly panicking) Duration x Duration arithmetic
    const fn strict_add(self, rhs: Self) -> Self
    const fn strict_sub(self, rhs: Self) -> Self

    // Operations I have less immediate need for, but might be worth including for consistency?

    // Explicitly panicking and checked variants of div_duration_{floor,ceil}() on divide-by-zero
    const fn strict_div_duration_floor(self, rhs: Self) -> u128
    const fn strict_div_duration_ceil(self, rhs: Self) -> u128
    const fn checked_div_duration_floor(self, rhs: Self) -> Option<u128>
    const fn checked_div_duration_ceil(self, rhs: Self) -> Option<u128>

    // Parallels to uN::{is,(checked_)next}_multiple_of()
    // Seems natural to include these alongside exact division
    const fn is_multiple_of(self, rhs: Self) -> bool
    const fn next_multiple_of(self, rhs: Self) -> Self
    const fn checked_next_multiple_of(self, rhs: Self) -> Option<Self>
}

Alternatives

  • Don't add these APIs (all can be written using existing APIs), or implement them in a crates.io crate.
    • Relatively trivial functions like this aren't worth pulling in a dependency for (or, at least for me, even worth defining a small internal library for). Duration seems comparable to uN, for which the standard library has no shortage of technically redundant helpers for the sake of discoverability/clarity of code.
    • The ways to express Duration by Duration division APIs using existing APIs involve going through .as_nanos(). This requires knowing the precision that Duration provides for it to be clear to the reader that .as_nanos() is lossless.

Links and related work

What happens now?

This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.

Possible responses

The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):

  • We think this problem seems worth solving, and the standard library might be the right place to solve it.
  • We think that this probably doesn't belong in the standard library.

Second, if there's a concrete solution:

  • We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
  • We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions