Rounding¶
to converts only when the result is exact. Where losing part of a step is the intent (a reading shown in coarser steps, Q15 narrowed to Q7), the conversion names how it rounds.
use exact_units::{Fixed, Rounding, prefix::{Deci, Milli}, units::Volt};
let v: Fixed<Volt, Milli, i16> = Fixed::new(1250);
assert_eq!(v.to::<Deci, i16>(), None); // 12.5 dV isn't whole
assert_eq!(v.to_rounded::<Deci, i16>(Rounding::NearestEven).map(|x| x.get()), Some(12));
assert_eq!(v.to_rounded::<Deci, i16>(Rounding::NearestAway).map(|x| x.get()), Some(13));
The five modes¶
Rounding |
Rule | 2.5 | −2.5 | 2.4 | Where you meet it |
|---|---|---|---|---|---|
Truncate |
\(\operatorname{trunc}(x)\), toward 0 | 2 | −2 | 2 | integer division in Rust and C |
Floor |
\(\lfloor x \rfloor\), toward \(-\infty\) | 2 | −3 | 2 | an arithmetic shift right |
Ceil |
\(\lceil x \rceil\), toward \(+\infty\) | 3 | −2 | 3 | buffer sizes |
NearestEven |
nearest, a tie to the even one | 2 | −2 | 2 | IEEE 754's default, Python's round |
NearestAway |
nearest, a tie away from 0 | 3 | −3 | 2 | C's round and lround |
The modes differ only where \(x\) is not whole, and the two nearest modes only at a tie, \(x = k + \tfrac12\):
Over a whole range each mode is a staircase. Here the crate's to_rounded turns tenths from −3 to 3 into whole numbers; the dots are the halves:
tools/book_plots with Fixed::to_rounded when the site is built.NearestEven has no bias: over many values its ties go up as often as down. NearestAway and Floor drift, which matters when values are summed.
Exact, once¶
Every mode rounds the true quotient once. For integer storages there is no float on the way.
A factor with \(\pi\), \(\ln 2\) or a root in it is allowed here, unlike in to. It is multiplied as a 64-bit fixed point within \(2^{-64}\) of the true factor, so the result is the correctly rounded one except within \(|x| \cdot 2^{-64}\) of a rounding step.
Thousandths of a degree to µrad is such a factor, \(\pi / 180\,000 \cdot 10^{6}\):
use exact_units::{Fixed, Rounding, prefix::Micro, units::Radian};
exact_units::scale! {
/// A thousandth of a degree, as a step of the radian: π / 180 000.
pub struct MilliDegree = "m°", Exact(1 / 180000, pi 1);
}
// 90°; to() would not compile: π is no fraction
let a: Fixed<Radian, MilliDegree, i32> = Fixed::new(90_000);
let r = a.to_rounded::<Micro, i32>(Rounding::NearestEven).unwrap();
assert_eq!(r.get(), 1_570_796); // π/2 rad in µrad
When the result doesn't fit¶
to_rounded returns None when the rounded result doesn't fit. Two more name another way: Overflow.
| Method | Too big for the storage |
|---|---|
to_rounded |
None |
to_saturating |
the nearest end |
to_wrapping |
modulo \(2^{\text{bits}}\) |
requantize(r, o) |
rounding and overflow chosen at run time |
Proven by the bounds¶
When the bounds prove that every rounded result fits, into_rounded returns the value itself, not an Option. It doesn't compile where the proof doesn't hold:
use exact_units::{Fixed, Range, Rounding, prefix::{Deci, Milli}, units::Volt};
type Rail = Fixed<Volt, Milli, i16, Range<0, 3300>>;
let dv: Fixed<Volt, Deci, u8> = Rail::bounded(1250).into_rounded(Rounding::NearestEven);
assert_eq!(dv.get(), 12); // 0 ..= 33 dV always fits a u8
At run time¶
A tool that reads scales from an API model has them as values, not types. Requantizer is the same conversion with the factor a Magnitude and the target a StorageKind, and gives the same number, bit for bit:
use exact_units::{Magnitude, Overflow, Repr, Requantizer, Rounding, StorageKind};
// mV to dV in an i8: the factor is 10⁻²
let q = Requantizer::new(
&Magnitude::pow10(-2),
StorageKind::Int { bits: 8, signed: true },
Rounding::NearestEven,
Overflow::Checked,
);
assert_eq!(q.apply(Repr::Int(1250)), Some(Repr::Int(12)));
WireWeaver's Python package and its C library run the same conversion with the same results.