use core::ops::{Add, Div, Mul, Neg, Sub};
use crate::saturation;
const FRAC_BITS: u32 = 16;
const ONE_RAW: i64 = 1 << FRAC_BITS;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct Fixed(i64);
impl Fixed {
pub const ZERO: Self = Self(0);
pub const ONE: Self = Self(ONE_RAW);
pub const MIN: Self = Self(i64::MIN);
pub const MAX: Self = Self(i64::MAX);
pub const EPSILON: Self = Self(1);
#[must_use]
pub const fn from_bits(raw: i64) -> Self {
Self(raw)
}
#[must_use]
pub const fn to_bits(self) -> i64 {
self.0
}
#[must_use]
pub const fn from_int(value: i32) -> Self {
Self((value as i64) << FRAC_BITS)
}
#[must_use]
pub const fn from_ratio(numerator: i32, denominator: i32) -> Self {
assert!(
denominator != 0,
"Fixed::from_ratio needs a nonzero denominator"
);
let scaled = (numerator as i64) << FRAC_BITS;
let den = denominator as i64;
Self(round_div(scaled, den))
}
#[must_use]
pub const fn trunc_int(self) -> i64 {
self.0 / ONE_RAW
}
#[must_use]
pub const fn fract(self) -> Self {
Self(self.0 % ONE_RAW)
}
#[must_use]
pub fn abs(self) -> Self {
let Some(value) = self.0.checked_abs() else {
saturation::record();
return Self::MAX;
};
Self(value)
}
#[must_use]
pub const fn signum(self) -> Self {
Self(ONE_RAW * self.0.signum())
}
#[must_use]
pub const fn min(self, other: Self) -> Self {
if self.0 < other.0 { self } else { other }
}
#[must_use]
pub const fn max(self, other: Self) -> Self {
if self.0 > other.0 { self } else { other }
}
#[must_use]
pub const fn clamp(self, low: Self, high: Self) -> Self {
assert!(low.0 <= high.0, "Fixed::clamp needs low <= high");
self.max(low).min(high)
}
#[must_use]
pub fn saturating_mul(self, other: Self) -> Self {
let product = i128::from(self.0) * i128::from(other.0);
Self(narrow(round_shift(product)))
}
#[must_use]
pub fn saturating_div(self, other: Self) -> Self {
assert!(other.0 != 0, "Fixed division by zero");
let numerator = i128::from(self.0) << FRAC_BITS;
Self(narrow(round_div_i128(numerator, i128::from(other.0))))
}
#[must_use]
pub fn sqrt(self) -> Self {
assert!(self.0 >= 0, "Fixed::sqrt of a negative value");
self.checked_sqrt().unwrap_or(Self::ZERO)
}
#[must_use]
pub fn checked_sqrt(self) -> Option<Self> {
if self.0 < 0 {
return None;
}
#[expect(
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
reason = "guarded by the sign check above and by the root's own magnitude"
)]
let root = ((self.0 as u128) << FRAC_BITS).isqrt() as i64;
Some(Self(root))
}
#[must_use]
pub const fn checked_add(self, other: Self) -> Option<Self> {
match self.0.checked_add(other.0) {
Some(sum) => Some(Self(sum)),
None => None,
}
}
#[must_use]
pub const fn checked_sub(self, other: Self) -> Option<Self> {
match self.0.checked_sub(other.0) {
Some(difference) => Some(Self(difference)),
None => None,
}
}
#[must_use]
pub const fn checked_div(self, other: Self) -> Option<Self> {
if other.0 == 0 {
return None;
}
let rounded = round_div_i128((self.0 as i128) << FRAC_BITS, other.0 as i128);
if rounded > i64::MAX as i128 || rounded < i64::MIN as i128 {
None
} else {
#[expect(
clippy::cast_possible_truncation,
reason = "the branches above establish the value is in range"
)]
let narrowed = rounded as i64;
Some(Self(narrowed))
}
}
#[must_use]
pub const fn checked_mul(self, other: Self) -> Option<Self> {
let rounded = round_shift(self.0 as i128 * other.0 as i128);
if rounded > i64::MAX as i128 || rounded < i64::MIN as i128 {
None
} else {
#[expect(
clippy::cast_possible_truncation,
reason = "the branches above establish the value is in range"
)]
let narrowed = rounded as i64;
Some(Self(narrowed))
}
}
}
const fn round_shift(product: i128) -> i128 {
let half = 1i128 << (FRAC_BITS - 1);
if product >= 0 {
(product + half) >> FRAC_BITS
} else {
-((-product + half) >> FRAC_BITS)
}
}
const fn round_div(numerator: i64, denominator: i64) -> i64 {
let (magnitude, negative) = match (numerator < 0, denominator < 0) {
(false, false) => (numerator / denominator, false),
(true, true) => ((-numerator) / (-denominator), false),
(true, false) => ((-numerator) / denominator, true),
(false, true) => (numerator / (-denominator), true),
};
let remainder = (numerator % denominator).abs();
let half = denominator.abs() / 2;
let rounded = if remainder * 2 >= denominator.abs() && half >= 0 {
magnitude + 1
} else {
magnitude
};
if negative { -rounded } else { rounded }
}
const fn round_div_i128(numerator: i128, denominator: i128) -> i128 {
let negative = (numerator < 0) != (denominator < 0);
let num = if numerator < 0 { -numerator } else { numerator };
let den = if denominator < 0 {
-denominator
} else {
denominator
};
let quotient = num / den;
let rounded = if (num % den) * 2 >= den {
quotient + 1
} else {
quotient
};
if negative { -rounded } else { rounded }
}
fn narrow(value: i128) -> i64 {
if value > i128::from(i64::MAX) {
saturation::record();
i64::MAX
} else if value < i128::from(i64::MIN) {
saturation::record();
i64::MIN
} else {
#[expect(
clippy::cast_possible_truncation,
reason = "the branches above establish the value is in range"
)]
let narrowed = value as i64;
narrowed
}
}
impl Add for Fixed {
type Output = Self;
fn add(self, other: Self) -> Self {
let Some(sum) = self.0.checked_add(other.0) else {
saturation::record();
return if self.0 > 0 { Self::MAX } else { Self::MIN };
};
Self(sum)
}
}
impl Sub for Fixed {
type Output = Self;
fn sub(self, other: Self) -> Self {
let Some(difference) = self.0.checked_sub(other.0) else {
saturation::record();
return if self.0 > 0 { Self::MAX } else { Self::MIN };
};
Self(difference)
}
}
impl Neg for Fixed {
type Output = Self;
fn neg(self) -> Self {
let Some(negated) = self.0.checked_neg() else {
saturation::record();
return Self::MAX;
};
Self(negated)
}
}
impl Mul for Fixed {
type Output = Self;
fn mul(self, other: Self) -> Self {
self.saturating_mul(other)
}
}
impl Div for Fixed {
type Output = Self;
fn div(self, other: Self) -> Self {
self.saturating_div(other)
}
}
#[cfg(test)]
mod tests {
use super::{Fixed, round_div};
use crate::saturations;
#[test]
fn absolute_value_saturates_at_the_bottom_of_the_range() {
assert_eq!(Fixed::from_int(-3).abs(), Fixed::from_int(3));
assert_eq!(Fixed::from_int(3).abs(), Fixed::from_int(3));
assert_eq!(Fixed::ZERO.abs(), Fixed::ZERO);
let before = saturations();
assert_eq!(Fixed::MIN.abs(), Fixed::MAX);
assert_eq!(saturations().0, before.0 + 1, "the clamp must be counted");
}
#[test]
fn signum_reports_whole_units() {
assert_eq!(Fixed::from_int(-9).signum(), Fixed::from_int(-1));
assert_eq!(Fixed::ZERO.signum(), Fixed::ZERO);
assert_eq!(Fixed::from_ratio(1, 1000).signum(), Fixed::ONE);
}
#[test]
fn min_max_and_clamp_agree_with_the_ordering() {
let low = Fixed::from_int(-2);
let high = Fixed::from_int(5);
assert_eq!(low.min(high), low);
assert_eq!(low.max(high), high);
assert_eq!(Fixed::from_int(9).clamp(low, high), high);
assert_eq!(Fixed::from_int(-9).clamp(low, high), low);
assert_eq!(Fixed::from_int(1).clamp(low, high), Fixed::from_int(1));
}
#[test]
#[should_panic(expected = "Fixed::clamp needs low <= high")]
fn clamp_refuses_an_inverted_range() {
let _ = Fixed::ZERO.clamp(Fixed::ONE, Fixed::ZERO);
}
#[test]
#[should_panic(expected = "Fixed::from_ratio needs a nonzero denominator")]
fn a_ratio_over_zero_is_refused() {
let _ = Fixed::from_ratio(1, 0);
}
#[test]
#[should_panic(expected = "Fixed division by zero")]
fn division_by_zero_is_refused() {
let _ = Fixed::ONE.saturating_div(Fixed::ZERO);
}
#[test]
#[should_panic(expected = "Fixed::sqrt of a negative value")]
fn the_square_root_of_a_negative_is_refused() {
let _ = Fixed::from_int(-1).sqrt();
}
#[test]
fn the_checked_forms_report_rather_than_saturate() {
assert_eq!(Fixed::ONE.checked_add(Fixed::ONE), Some(Fixed::from_int(2)));
assert_eq!(Fixed::MAX.checked_add(Fixed::ONE), None);
assert_eq!(Fixed::ONE.checked_sub(Fixed::ONE), Some(Fixed::ZERO));
assert_eq!(Fixed::MIN.checked_sub(Fixed::ONE), None);
assert_eq!(
Fixed::from_int(3).checked_mul(Fixed::from_int(4)),
Some(Fixed::from_int(12))
);
assert_eq!(Fixed::MAX.checked_mul(Fixed::MAX), None);
let before = saturations();
let _ = Fixed::MAX.checked_add(Fixed::ONE);
let _ = Fixed::MAX.checked_mul(Fixed::MAX);
assert_eq!(saturations(), before);
}
#[test]
fn checked_division_answers_where_the_asserting_form_refuses() {
assert_eq!(
Fixed::from_int(6).checked_div(Fixed::from_int(3)),
Some(Fixed::from_int(2))
);
assert_eq!(Fixed::ONE.checked_div(Fixed::ZERO), None);
assert_eq!(Fixed::ZERO.checked_div(Fixed::ZERO), None);
assert_eq!(Fixed::MAX.checked_div(Fixed::EPSILON), None);
assert_eq!(
Fixed::from_int(7).checked_div(Fixed::from_int(2)),
Some(Fixed::from_int(7).saturating_div(Fixed::from_int(2)))
);
let before = saturations();
let _ = Fixed::MAX.checked_div(Fixed::EPSILON);
let _ = Fixed::ONE.checked_div(Fixed::ZERO);
assert_eq!(saturations(), before);
}
#[test]
fn subtraction_and_negation_saturate_at_both_ends() {
assert_eq!(Fixed::from_int(5) - Fixed::from_int(3), Fixed::from_int(2));
assert_eq!(-Fixed::from_int(3), Fixed::from_int(-3));
let before = saturations();
assert_eq!(Fixed::MAX - Fixed::MIN, Fixed::MAX);
assert_eq!(-Fixed::MIN, Fixed::MAX);
assert_eq!(saturations().0, before.0 + 2);
}
#[test]
fn the_parts_of_a_negative_value_carry_its_sign() {
let value = Fixed::from_ratio(-7, 2);
assert_eq!(value.trunc_int(), -3);
assert_eq!(value.fract(), Fixed::from_ratio(-1, 2));
}
#[test]
fn ratios_round_symmetrically() {
assert_eq!(Fixed::from_ratio(-981, 100), -Fixed::from_ratio(981, 100));
assert_eq!(Fixed::from_ratio(981, -100), -Fixed::from_ratio(981, 100));
assert_eq!(Fixed::from_ratio(1, 2), Fixed::from_bits(1 << 15));
}
#[test]
fn the_rounding_helper_is_symmetric_and_rounds_ties_away_from_zero() {
assert_eq!(round_div(7, 2), 4);
assert_eq!(round_div(-7, 2), -4);
assert_eq!(round_div(7, -2), -4);
assert_eq!(round_div(-7, -2), 4);
assert_eq!(round_div(5, 2), 3, "a tie rounds away from zero");
assert_eq!(round_div(-5, 2), -3, "and symmetrically");
assert_eq!(round_div(4, 2), 2, "an exact quotient is untouched");
}
#[test]
fn the_operators_delegate_to_the_named_forms() {
let a = Fixed::from_ratio(7, 3);
let b = Fixed::from_ratio(-11, 5);
assert_eq!(a * b, a.saturating_mul(b));
assert_eq!(a / b, a.saturating_div(b));
assert_eq!(a + b, Fixed::from_bits(a.to_bits() + b.to_bits()));
assert_eq!(a - b, Fixed::from_bits(a.to_bits() - b.to_bits()));
assert_eq!(Fixed::MAX * Fixed::MAX, Fixed::MAX);
}
#[test]
fn division_saturates_when_the_quotient_does_not_fit() {
let before = saturations();
assert_eq!(Fixed::MAX.saturating_div(Fixed::EPSILON), Fixed::MAX);
assert_eq!(saturations().0, before.0 + 1);
}
}