Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rounding

An operation that can lose a digit takes a rounding mode as its last argument, so it never rounds unless asked to. A mode is one of nine zero-sized types in round, which the compiler resolves where the operation is inlined, or a Rounding, the same nine as a value chosen at run time.

The Nine Modes

ModeRounds
Floortoward −∞
Ceiltoward +∞
Trunctoward zero
Expandaway from zero
HalfFloorto the nearer step, and a tie toward −∞
HalfCeilto the nearer step, and a tie toward +∞
HalfTruncto the nearer step, and a tie toward zero
HalfExpandto the nearer step, and a tie away from zero
HalfEvento the nearer step, and a tie to the even step

Each takes six values to a whole number here:

fn each_mode_settles_the_digits_that_do_not_fit() {
    use ddust::round::Rounding;
    use ddust::{D64, dec};

    let values: [D64<2>; 6] = [dec!(-1.5), dec!(-1.25), dec!(-0.5), dec!(0.5), dec!(1.25), dec!(1.5)];
    let whole = |mode| values.map(|value| value.to_int(mode));
    assert_eq!(whole(Rounding::Floor),      [-2, -2, -1, 0, 1, 1]);
    assert_eq!(whole(Rounding::Ceil),       [-1, -1,  0, 1, 2, 2]);
    assert_eq!(whole(Rounding::Trunc),      [-1, -1,  0, 0, 1, 1]);
    assert_eq!(whole(Rounding::Expand),     [-2, -2, -1, 1, 2, 2]);
    assert_eq!(whole(Rounding::HalfFloor),  [-2, -1, -1, 0, 1, 1]);
    assert_eq!(whole(Rounding::HalfCeil),   [-1, -1,  0, 1, 1, 2]);
    assert_eq!(whole(Rounding::HalfTrunc),  [-1, -1,  0, 0, 1, 1]);
    assert_eq!(whole(Rounding::HalfExpand), [-2, -1, -1, 1, 1, 2]);
    assert_eq!(whole(Rounding::HalfEven),   [-2, -1,  0, 0, 1, 2]);
}

HalfExpand is the rounding taught at school, and HalfEven, banker’s rounding, is IEEE 754’s default and the one that does not drift up over many ties.

What Takes a Mode

MethodWhat it rounds
mul_rounda product, at this value’s scale
mul_round_toa product, at a scale named
div_rounda quotient by a decimal or a Divisor, at this value’s scale
div_round_toa quotient, at a scale named
div_int_roundone of n equal parts
mul_div_roundself × numerator / denominator, rounded once
round_toto a multiple of a step: a tick, a lot, a coin
roundto a whole number, at this value’s scale
to_intto a whole number, as an integer
rescale_roundto another scale
new_round, to_digits_rounddigits, to and from a decimal
from_ascii_roundtext with more decimals than the scale
from_f64a double

Each of the arithmetic ones has a checked_* twin that returns None past the range, or for a zero divisor. trunc, floor and ceil are the whole-number roundings with their modes named, and fract is what trunc leaves:

fn what_can_lose_a_digit_takes_a_mode() {
    use ddust::round::{Ceil, Floor, HalfEven, Trunc};
    use ddust::{D64, dec};

    let (amount, fee): (D64<2>, D64<4>) = (dec!(100.10), dec!(0.0025));
    assert_eq!(amount.mul_round(fee, Ceil), dec!(0.26), "0.25025, up to the cent");
    assert_eq!(amount.div_int_round(3, HalfEven), dec!(33.37), "a third, to the cent");
    assert_eq!(amount.div_round(dec!(0.07: D64<2>), Floor), dec!(1430));
    let share = amount.mul_div_round(dec!(2: D64<0>), dec!(3), HalfEven);
    assert_eq!(share, dec!(66.73), "two thirds, rounded once");
    assert_eq!(dec!(19.97: D64<2>).round_to(dec!(0.05), Floor), dec!(19.95), "to a step");
    assert_eq!(dec!(2.5: D64<1>).round(HalfEven), dec!(2), "to a whole number");
    assert_eq!(dec!(-2.5: D64<1>).to_int(Trunc), -2, "to an integer");
}

Every operation rounds once. Its exact result is computed in an integer wide enough to hold it, twice the decimal’s or 256 bits, and the mode settles it from the remainder: mul_div_round takes two thirds of 100.10 with no rounding between the multiplication and the division.

The compiler refuses an operation that can lose a digit without a mode:

//! An operation that can lose digits takes a rounding mode, so it never rounds unasked.

use ddust::{D64, dec};

fn main() {
    let (amount, rate): (D64<2>, D64<4>) = (dec!(100.10), dec!(0.0025));
    let _rounded = amount.mul_round(rate);
}
error[E0061]: this method takes 2 arguments but 1 argument was supplied
 --> tests/compile_fail/a_lossy_operation_takes_a_mode.rs:7:27
  |
7 |     let _rounded = amount.mul_round(rate);
  |                           ^^^^^^^^^------ argument #2 is missing
  |
note: method defined here
 --> src/rounded.rs
  |
  |     pub const fn mul_round<T, R>(self, rhs: Decimal<I, T>, mode: R) -> Self
  |                  ^^^^^^^^^
help: provide the argument
  |
7 |     let _rounded = amount.mul_round(rate, /* mode */);
  |                                         ++++++++++++

A Mode Chosen at Run Time

A venue’s rules, a configuration or a protocol may choose the mode. A Rounding is that choice as a value, and every operation takes it where it takes a mode type:

fn a_mode_read_at_run_time_is_a_rounding() {
    use ddust::round::{HalfExpand, Rounding};
    use ddust::{D64, Fixed, dec};

    let venue_rounds_half_up = true;
    let mode = if venue_rounds_half_up { Rounding::from(HalfExpand) } else { Rounding::HalfEven };
    let fee: D64<2> = dec!(0.125: D64<3>).rescale_round(Fixed, mode).expect("in range");
    assert_eq!(fee, dec!(0.13));
}

Each mode is a table of sixteen bits, one for each way a division can end: the result’s sign, whether its quotient is odd, and whether the remainder is zero, below half the divisor, at half or above it. Rounding reads one bit of the table, so a Rounding costs no match: a mode is data, whether it is a type, a constant or a value read at run time. The trait every mode implements, RoundingMode, is sealed, so the nine are all there are:

//! `RoundingMode` is sealed: a mode outside the nine would bypass their tables.

use ddust::round::RoundingMode;

#[derive(Clone, Copy)]
struct Nearest;

impl RoundingMode for Nearest {
    fn table(self) -> u16 {
        0
    }
}

fn main() {}
error[E0277]: the trait bound `Nearest: round::sealed::Sealed` is not satisfied
 --> tests/compile_fail/a_rounding_mode_is_one_of_the_nine.rs:8:23
  |
8 | impl RoundingMode for Nearest {
  |                       ^^^^^^^ unsatisfied trait bound
  |
help: the trait `round::sealed::Sealed` is not implemented for `Nearest`
 --> tests/compile_fail/a_rounding_mode_is_one_of_the_nine.rs:6:1
  |
6 | struct Nearest;
  | ^^^^^^^^^^^^^^
  = help: the following other types implement trait `round::sealed::Sealed`:
            Rounding
            ddust::round::Ceil
            ddust::round::Expand
            ddust::round::Floor
            ddust::round::HalfCeil
            ddust::round::HalfEven
            ddust::round::HalfExpand
            ddust::round::HalfFloor
          and $N others
note: required by a bound in `RoundingMode`
 --> src/round.rs
  |
  | pub const trait RoundingMode: sealed::Sealed + Copy {
  |                               ^^^^^^^^^^^^^^ required by this bound in `RoundingMode`
  = note: `RoundingMode` is a "sealed trait", because to implement it you also need to implement `ddust::round::sealed::Sealed`, which is not accessible; this is usually done to force you to use one of the provided types that already implement it
  = help: the following types implement the trait:
            ddust::round::Floor
            ddust::round::Ceil
            ddust::round::Trunc
            ddust::round::Expand
            ddust::round::HalfFloor
            ddust::round::HalfCeil
            ddust::round::HalfTrunc
            ddust::round::HalfExpand
          and $N others

Many Values by One Divisor

A Divisor is a divisor prepared once, for dividing many values by it: a book’s notionals by one price, or amounts by one rate. div_round and checked_div_round take it where they take a decimal, and give the same quotients:

fn a_divisor_prepared_once_divides_many_values() {
    use ddust::round::HalfEven;
    use ddust::{D64, Divisor, dec};

    let euros_per_dollar: D64<4> = dec!(0.9234);
    let rate = Divisor::new(euros_per_dollar).expect("not zero");
    let euros: [D64<2>; 3] = [dec!(100), dec!(250), dec!(19.99)];
    let dollars = euros.map(|amount| amount.div_round(rate, HalfEven));
    assert_eq!(dollars, [dec!(108.30), dec!(270.74), dec!(21.65)]);
    assert_eq!(dollars[2], euros[2].div_round(euros_per_dollar, HalfEven), "as by the decimal");
}

Preparing it costs a division; each quotient then costs two multiplications and a comparison. Performance measures both.