#![deny(clippy::all, clippy::cargo)]
#![forbid(const_err)]
#[macro_use]
mod const_panic;
#[cfg(feature = "serde")]
mod serde;
use core::convert::{Infallible, TryFrom};
use core::num::{ParseIntError, TryFromIntError};
pub type Precision = u8;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum Error<const P: Precision> {
#[error("failed to convert from integer: {0}")]
ConversionError(#[from] TryFromIntError),
#[error("units out of range (max {} bits)", FixedP::<P>::UNIT_BITS)]
OutOfRangeUnits,
#[error("fraction out of range (max {} bits, {} base-10 digits)", FixedP::<P>::FRAC_BITS, P)]
OutOfRangeFrac,
#[error("failed to parse fixed point value: {0}")]
ParserError(#[from] ParseIntError),
}
impl<const P: Precision> From<Infallible> for Error<P> {
fn from(i: Infallible) -> Self {
match i {}
}
}
#[repr(transparent)]
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FixedP<const P: Precision> {
n: u64,
}
impl<const P: Precision> FixedP<P> {
pub const MIN_UNIT_SCALE: u64 = 100_000_000_000;
const MIN_UNIT_SCALE_BITS: u32 =
(const_option_unwrap!(Self::MIN_UNIT_SCALE.checked_next_power_of_two()) - 1).count_ones();
const FRAC_SCALE: u64 = { 10u64.pow(const_int_as!(P, Precision, u32)) };
const BITMASK_FRAC: u64 =
const_option_unwrap!(Self::FRAC_SCALE.checked_next_power_of_two()) - 1;
const FRAC_BITS: u32 = Self::BITMASK_FRAC.count_ones();
const BITMASK_UNIT: u64 = !Self::BITMASK_FRAC;
const FRAC_MAX: u64 = const_option_unwrap!(Self::FRAC_SCALE.checked_sub(1));
const UNIT_MAX: u64 = const_option_unwrap!(Self::BITMASK_UNIT.checked_shr(Self::FRAC_BITS));
const MAX_VALUE: u64 = Self::BITMASK_UNIT | Self::FRAC_MAX;
const UNIT_BITS: u32 = {
let unit_bits = const_option_unwrap!(u64::BITS.checked_sub(Self::FRAC_BITS));
if unit_bits >= Self::MIN_UNIT_SCALE_BITS {
unit_bits
} else {
const_panic!()
}
};
const UNIT_OVERFLOW_MASK: u64 =
const_option_unwrap!(Self::BITMASK_FRAC.checked_shl(Self::UNIT_BITS));
pub const fn precision() -> Precision {
P
}
pub const fn unit_bits() -> u32 {
Self::UNIT_BITS
}
pub const fn frac_bits() -> u32 {
Self::FRAC_BITS
}
pub const fn unit_max() -> u64 {
Self::UNIT_MAX
}
pub const fn frac_max() -> u64 {
Self::FRAC_MAX
}
pub const fn zero() -> Self {
Self { n: 0 }
}
pub const fn min() -> Self {
Self::zero()
}
pub const fn max() -> Self {
Self { n: Self::MAX_VALUE }
}
pub const fn from_units_frac(units: u64, frac: u64) -> Result<Self, Error<P>> {
if units & Self::UNIT_OVERFLOW_MASK > 0 {
return Err(Error::OutOfRangeUnits);
}
if frac > Self::FRAC_MAX {
return Err(Error::OutOfRangeFrac);
}
Ok(Self {
n: (units << Self::FRAC_BITS) | frac,
})
}
pub const fn from_units(units: u64) -> Result<Self, Error<P>> {
Self::from_units_frac(units, 0)
}
pub const fn units(&self) -> u64 {
self.n >> Self::FRAC_BITS
}
pub const fn frac(&self) -> u64 {
self.n & Self::BITMASK_FRAC
}
pub const fn split(&self) -> (u64, u64) {
(self.units(), self.frac())
}
#[must_use = "this returns the result of the operation, without modifying the original"]
pub const fn checked_add(self, rhs: Self) -> Option<Self> {
let mut units = self.units() + rhs.units();
let mut frac = self.frac() + rhs.frac();
if frac > Self::FRAC_MAX {
frac -= Self::FRAC_SCALE;
units += 1; }
if units & Self::UNIT_OVERFLOW_MASK > 0 {
return None;
}
Some(Self {
n: (units << Self::FRAC_BITS) | frac,
})
}
#[must_use = "this returns the result of the operation, without modifying the original"]
pub const fn checked_sub(self, rhs: Self) -> Option<Self> {
let mut units = match self.units().checked_sub(rhs.units()) {
Some(units) => units,
_ => return None,
};
let mut frac = self.frac() as i64 - rhs.frac() as i64;
if frac.is_negative() {
frac += Self::FRAC_SCALE as i64;
units = match units.checked_sub(1) {
Some(units) => units,
_ => return None,
};
}
Some(Self {
n: (units << Self::FRAC_BITS) | frac as u64,
})
}
pub const fn try_from_fixed<const Q: Precision>(value: FixedP<Q>) -> Result<Self, Error<P>> {
let diff: i16 = P as i16 - Q as i16;
let exp = diff.abs() as u32;
let (units, frac) = if diff.is_positive() {
let factor = match 10u64.checked_pow(exp) {
Some(factor) => factor,
None => return Err(Error::OutOfRangeFrac),
};
let frac = match value.frac().checked_mul(factor) {
Some(frac) => frac,
None => return Err(Error::OutOfRangeFrac),
};
(value.units(), frac)
} else if diff.is_negative() {
let mut frac = value.frac();
if frac % exp as u64 == 0 {
let divisor = match 10u64.checked_pow(exp) {
Some(divisor) => divisor,
None => return Err(Error::OutOfRangeFrac),
};
frac /= divisor;
} else {
return Err(Error::OutOfRangeFrac);
}
(value.units(), frac)
} else {
(value.units(), value.frac())
};
Self::from_units_frac(units, frac)
}
}
impl<const P: Precision> Default for FixedP<P> {
fn default() -> Self {
Self::zero()
}
}
impl<const P: Precision> core::fmt::Display for FixedP<P> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{}.{:0width$}",
self.units(),
self.frac(),
width = (P as usize)
)
}
}
impl<const P: Precision> core::fmt::Debug for FixedP<P> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct(&format!("FixedP<{}>", P))
.field("units", &self.units())
.field("frac", &self.frac())
.field("unit_bits", &Self::UNIT_BITS)
.field("frac_bits", &Self::FRAC_BITS)
.field("unit_max", &Self::UNIT_MAX)
.field("frac_max", &Self::FRAC_MAX)
.field("bitvalue", &self.n)
.finish()
}
}
impl<const P: Precision> core::str::FromStr for FixedP<P> {
type Err = Error<P>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (units, frac) = match s.split_once('.') {
Some((units_s, frac_s)) => {
let mut frac = frac_s.parse()?;
if let Some(digits) = P.checked_sub(frac_s.len() as u8) {
if digits > 0 {
frac *= 10u64.pow(digits as u32);
}
} else {
return Err(Error::OutOfRangeFrac);
}
(units_s.parse()?, frac)
}
None => (s.parse()?, 0),
};
Self::from_units_frac(units, frac)
}
}
macro_rules! impl_integer_conversion {
( $($uint:ty),+ ) => {
$(
impl<const P: Precision> TryFrom<$uint> for FixedP<P> {
type Error = Error<P>;
fn try_from(value: $uint) -> Result<Self, Self::Error> {
Self::from_units(u64::try_from(value)?)
}
}
)+
};
}
impl_integer_conversion!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128);
impl<const P: Precision> core::ops::Add for FixedP<P> {
type Output = FixedP<P>;
fn add(self, rhs: Self) -> Self::Output {
self.checked_add(rhs).expect("overflow")
}
}
impl<const P: Precision> core::ops::AddAssign for FixedP<P> {
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs;
}
}
impl<const P: Precision> core::ops::Sub for FixedP<P> {
type Output = FixedP<P>;
fn sub(self, rhs: Self) -> Self::Output {
self.checked_sub(rhs).expect("underflow")
}
}
impl<const P: Precision> core::ops::SubAssign for FixedP<P> {
fn sub_assign(&mut self, rhs: Self) {
*self = *self - rhs;
}
}
#[cfg(test)]
mod test {
use super::*;
const P: Precision = 8;
const ZERO: FixedP<P> = FixedP::zero();
const MIN: FixedP<P> = FixedP::min();
const MAX: FixedP<P> = FixedP::max();
mod overflowing {
use super::*;
#[test]
#[should_panic(expected = "overflow")]
fn overflow_units() {
let one = FixedP::from_units(1).unwrap();
let _overflow = MAX + one;
}
#[test]
#[should_panic(expected = "underflow")]
fn underflow_units() {
let one = FixedP::from_units(1).unwrap();
let _underflow = MIN - one;
}
#[test]
#[should_panic(expected = "overflow")]
fn overflow_frac() {
let one = FixedP::from_units_frac(0, 1).unwrap();
let _overflow = MAX + one;
}
#[test]
#[should_panic(expected = "underflow")]
fn underflow_frac() {
let one = FixedP::from_units_frac(0, 1).unwrap();
let _underflow = MIN - one;
}
#[test]
#[should_panic(expected = "overflow")]
fn overflow_max() {
let _overflow = MAX + MAX;
}
#[test]
#[should_panic(expected = "underflow")]
fn underflow_max() {
let _underflow = MIN - MAX;
}
}
mod arithmetic {
use super::*;
#[test]
fn addition() -> Result<(), Error<P>> {
let a = FixedP::from_units_frac(2, 1)?;
let b = FixedP::from_units_frac(6, 4)?;
let res = a + b;
assert_eq!(8, res.units());
assert_eq!(5, res.frac());
Ok(())
}
#[test]
fn addition_assignment() -> Result<(), Error<P>> {
let mut a = FixedP::from_units_frac(2, 1)?;
let b = FixedP::from_units_frac(6, 4)?;
a += b;
assert_eq!(8, a.units());
assert_eq!(5, a.frac());
Ok(())
}
#[test]
fn subtraction() -> Result<(), Error<P>> {
let a = FixedP::from_units_frac(5, 2)?;
let b = FixedP::from_units_frac(2, 1)?;
let res = a - b;
assert_eq!(3, res.units());
assert_eq!(1, res.frac());
Ok(())
}
#[test]
fn subtraction_assignment() -> Result<(), Error<P>> {
let mut a = FixedP::from_units_frac(5, 2)?;
let b = FixedP::from_units_frac(2, 1)?;
a -= b;
assert_eq!(3, a.units());
assert_eq!(1, a.frac());
Ok(())
}
#[test]
fn addition_with_carry_over() -> Result<(), Error<P>> {
let almost_three = FixedP::from_units_frac(2, FixedP::<P>::FRAC_MAX)?;
let slightly_above_one = FixedP::from_units_frac(1, 2)?;
let res = almost_three + slightly_above_one;
assert_eq!(4, res.units());
assert_eq!(1, res.frac());
Ok(())
}
#[test]
fn subtraction_with_carry_over() -> Result<(), Error<P>> {
let just_above_two = FixedP::from_units_frac(2, 1)?;
let slightly_above_one = FixedP::from_units_frac(1, 2)?;
let res = just_above_two - slightly_above_one;
let (units, frac) = res.split();
assert_eq!(0, units);
assert_eq!(FixedP::<P>::FRAC_MAX, frac);
Ok(())
}
}
mod order {
use super::*;
#[test]
fn sorting() -> Result<(), Error<P>> {
let one = FixedP::from_units(1)?;
let ten = FixedP::from_units(10)?;
let mut v = vec![one, MAX, ZERO, ten];
v.sort();
assert_eq!(v.as_slice(), &[ZERO, one, ten, MAX]);
Ok(())
}
}
mod constructors {
use super::*;
#[test]
fn zero() -> Result<(), Error<P>> {
let zero_from_units = FixedP::from_units(0)?;
assert!(ZERO == zero_from_units);
assert_eq!(ZERO, zero_from_units);
Ok(())
}
#[test]
fn default_is_zero() -> Result<(), Error<P>> {
let zero_from_units = FixedP::from_units(0)?;
assert_eq!(FixedP::<P>::default(), zero_from_units);
Ok(())
}
#[test]
fn from_units_out_of_range_units() -> Result<(), Error<P>> {
let oor = FixedP::from_units(u64::MAX);
assert!(oor.is_err());
assert_eq!(oor.unwrap_err(), Error::<P>::OutOfRangeUnits);
Ok(())
}
#[test]
fn from_units_frac_out_of_range_frac() -> Result<(), Error<P>> {
let oor = FixedP::from_units_frac(0, FixedP::<P>::FRAC_SCALE);
assert!(oor.is_err());
assert_eq!(oor.unwrap_err(), Error::<P>::OutOfRangeFrac);
Ok(())
}
#[test]
fn try_from_conversion_error() -> Result<(), Error<P>> {
let converted = FixedP::try_from(u128::MAX);
assert!(matches!(converted, Err(Error::<P>::ConversionError(_))));
Ok(())
}
mod try_from_fixed {
use super::*;
#[test]
fn smaller() -> Result<(), Box<dyn std::error::Error>> {
let two_point_one = FixedP::<1>::from_units_frac(2, 1)?;
let two_point_one_hundred = FixedP::<3>::try_from_fixed(two_point_one);
assert!(two_point_one_hundred.is_ok());
assert_eq!(
two_point_one_hundred.unwrap(),
FixedP::<3>::from_units_frac(2, 100)?
);
Ok(())
}
#[test]
fn smaller_max_units() -> Result<(), Box<dyn std::error::Error>> {
let max_units_one_decimal = FixedP::<1>::from_units(FixedP::<1>::UNIT_MAX)?;
let three_decimal = FixedP::<3>::try_from_fixed(max_units_one_decimal);
assert!(three_decimal.is_err());
assert_eq!(three_decimal.unwrap_err(), Error::<3>::OutOfRangeUnits);
Ok(())
}
#[test]
fn bigger() -> Result<(), Box<dyn std::error::Error>> {
let two_point_one_hundred = FixedP::<3>::from_units_frac(2, 100)?;
let two_point_one = FixedP::<1>::try_from_fixed(two_point_one_hundred);
assert!(two_point_one.is_ok());
assert_eq!(two_point_one.unwrap(), FixedP::<1>::from_units_frac(2, 1)?);
Ok(())
}
#[test]
fn bigger_frac_one() -> Result<(), Box<dyn std::error::Error>> {
let two_point_one_thousandth = FixedP::<3>::from_units_frac(2, 1)?;
let one_decimal = FixedP::<1>::try_from_fixed(two_point_one_thousandth);
assert!(one_decimal.is_err());
assert_eq!(one_decimal.unwrap_err(), Error::<1>::OutOfRangeFrac);
Ok(())
}
#[test]
fn equal() -> Result<(), Box<dyn std::error::Error>> {
let two_point_one_hundred = FixedP::<3>::from_units_frac(2, 100)?;
let two_point_another_hundred = FixedP::<3>::try_from_fixed(two_point_one_hundred);
assert!(two_point_another_hundred.is_ok());
assert_eq!(two_point_another_hundred.unwrap(), two_point_one_hundred);
Ok(())
}
}
}
mod representation {
use super::*;
#[test]
fn display() -> Result<(), Error<P>> {
let val = FixedP::from_units_frac(12, 3)?;
assert_eq!(
&format!("{}", val),
&format!("12.{:0width$}", 3, width = (P as usize))
);
Ok(())
}
#[test]
fn display_frac_zero() -> Result<(), Error<P>> {
let val = FixedP::from_units(12)?;
assert_eq!(
&format!("{}", val),
&format!("12.{:0width$}", 0, width = (P as usize))
);
Ok(())
}
#[test]
fn debug() -> Result<(), Error<P>> {
let val = FixedP::from_units_frac(12, 3)?;
let debug_s = format!(
"FixedP<{P}> {{ \
units: {units}, \
frac: {frac}, \
unit_bits: {unit_bits}, \
frac_bits: {frac_bits}, \
unit_max: {unit_max}, \
frac_max: {frac_max}, \
bitvalue: {bitvalue} }}",
P = P,
units = &val.units(),
frac = &val.frac(),
unit_bits = &FixedP::<P>::UNIT_BITS,
frac_bits = &FixedP::<P>::FRAC_BITS,
unit_max = &FixedP::<P>::UNIT_MAX,
frac_max = &FixedP::<P>::FRAC_MAX,
bitvalue = &val.n
);
assert_eq!(&format!("{:?}", val), &debug_s);
Ok(())
}
}
mod parsing {
use super::*;
#[test]
fn integers() -> Result<(), Error<P>> {
let val = FixedP::from_units(12)?;
assert_eq!("12".parse::<FixedP<P>>()?, val);
assert_eq!("12.0".parse::<FixedP<P>>()?, val);
Ok(())
}
#[test]
#[allow(clippy::assertions_on_constants)]
fn frac_missing_right_zeroes() {
const P: Precision = 3;
assert!(P > 1);
let val = FixedP::from_units_frac(12, 3 * 10u64.pow((P - 1) as u32)).unwrap();
assert_eq!("12.3".parse::<FixedP<P>>().unwrap(), val);
}
#[test]
#[allow(clippy::assertions_on_constants)]
fn frac_including_left_zeroes() -> Result<(), Box<dyn std::error::Error>> {
const P: Precision = 3;
assert!(P > 1);
let val = FixedP::<P>::from_units_frac(12, 3 * 10u64.pow((P - 1 - 1) as u32))?;
let s = &format!("12.{:0width$}", 3, width = ((P - 1) as usize));
assert_eq!(s.parse::<FixedP<P>>()?, val);
Ok(())
}
#[test]
#[allow(clippy::assertions_on_constants)]
fn frac_out_of_range() -> Result<(), Box<dyn std::error::Error>> {
const P: Precision = 3;
assert!(P > 1);
let s = &format!("12.{:0width$}3", width = (P as usize));
let parsed = s.parse::<FixedP<P>>();
assert!(parsed.is_err());
assert!(matches!(parsed.unwrap_err(), Error::OutOfRangeFrac));
Ok(())
}
#[test]
fn units_out_of_range() -> Result<(), Box<dyn std::error::Error>> {
let s = &format!("{}", u64::MAX);
let parsed = s.parse::<FixedP<P>>();
assert!(parsed.is_err());
assert!(matches!(parsed.unwrap_err(), Error::OutOfRangeUnits));
Ok(())
}
}
mod ieee754_gotchas {
use super::*;
#[test]
#[allow(clippy::float_cmp)]
fn no_basic_rounding_error() -> Result<(), Box<dyn std::error::Error>> {
let zero_20 = FixedP::<2>::from_units_frac(0, 20)?;
let zero_15 = FixedP::<2>::from_units_frac(0, 15)?;
let zero_04 = FixedP::<2>::from_units_frac(0, 4)?;
let zero_09 = FixedP::<2>::from_units_frac(0, 9)?;
assert_ne!(0.20 - 0.15 + 0.04, 0.09);
assert_eq!(zero_20 - zero_15 + zero_04, zero_09);
Ok(())
}
#[test]
#[allow(clippy::float_cmp)]
fn no_edge_case_rounding_error_f32() -> Result<(), Box<dyn std::error::Error>> {
let f: f32 = 16777217 as f32;
assert_eq!(f, 16777217.0);
assert_eq!(f, 16777216 as f32);
assert_eq!(f, 16777216.0);
let fixed = FixedP::<2>::from_units_frac(16777217, 0)?;
assert_eq!(fixed.units(), 16777217);
assert_eq!(fixed.frac(), 0);
assert_ne!(fixed.units(), 16777216);
let parsed_f32 = format!("{}", fixed).parse::<f32>()?;
assert_eq!(parsed_f32, 16777217.0);
assert_ne!(format!("{}", parsed_f32).parse::<FixedP<2>>()?, fixed);
Ok(())
}
#[test]
#[allow(clippy::float_cmp)]
fn no_edge_case_rounding_error_f64() -> Result<(), Box<dyn std::error::Error>> {
let f: f64 = 9007199254740993u64 as f64;
assert_eq!(f, 9007199254740993.0);
assert_eq!(f, 9007199254740992u64 as f64);
assert_eq!(f, 9007199254740992.0);
let fixed = FixedP::<2>::from_units_frac(9007199254740993, 0)?;
assert_eq!(fixed.units(), 9007199254740993);
assert_eq!(fixed.frac(), 0);
assert_ne!(fixed.units(), 9007199254740992);
let parsed_f64 = format!("{}", fixed).parse::<f64>()?;
assert_eq!(parsed_f64, 9007199254740993.0);
assert_ne!(format!("{}", parsed_f64).parse::<FixedP<2>>()?, fixed);
Ok(())
}
#[test]
fn no_max_value_rounding_error() -> Result<(), Box<dyn std::error::Error>> {
let fixed = FixedP::<2>::from_units_frac(FixedP::<2>::UNIT_MAX, 0)?;
assert_eq!(fixed.units(), FixedP::<2>::UNIT_MAX);
assert_eq!(fixed.frac(), 0);
assert_ne!(fixed.units(), FixedP::<2>::UNIT_MAX - 1);
let fixed = FixedP::<2>::from_units_frac(FixedP::<2>::UNIT_MAX, FixedP::<2>::FRAC_MAX)?;
assert_eq!(fixed.units(), FixedP::<2>::UNIT_MAX);
assert_eq!(fixed.frac(), FixedP::<2>::FRAC_MAX);
assert_ne!(fixed.units(), FixedP::<2>::UNIT_MAX - 1);
assert_ne!(fixed.frac(), FixedP::<2>::FRAC_MAX - 1);
Ok(())
}
}
}