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
| Mode | Rounds |
|---|---|
Floor | toward −∞ |
Ceil | toward +∞ |
Trunc | toward zero |
Expand | away from zero |
HalfFloor | to the nearer step, and a tie toward −∞ |
HalfCeil | to the nearer step, and a tie toward +∞ |
HalfTrunc | to the nearer step, and a tie toward zero |
HalfExpand | to the nearer step, and a tie away from zero |
HalfEven | to 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
| Method | What it rounds |
|---|---|
mul_round | a product, at this value’s scale |
mul_round_to | a product, at a scale named |
div_round | a quotient by a decimal or a Divisor, at this value’s scale |
div_round_to | a quotient, at a scale named |
div_int_round | one of n equal parts |
mul_div_round | self × numerator / denominator, rounded once |
round_to | to a multiple of a step: a tick, a lot, a coin |
round | to a whole number, at this value’s scale |
to_int | to a whole number, as an integer |
rescale_round | to another scale |
new_round, to_digits_round | digits, to and from a decimal |
from_ascii_round | text with more decimals than the scale |
from_f64 | a 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.