1use core::ops::{Add, Div, Mul, Neg, Sub};
4
5use crate::saturation;
6
7const FRAC_BITS: u32 = 16;
9
10const ONE_RAW: i64 = 1 << FRAC_BITS;
12
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
42#[repr(transparent)]
43pub struct Fixed(i64);
44
45impl Fixed {
46 pub const ZERO: Self = Self(0);
48 pub const ONE: Self = Self(ONE_RAW);
50 pub const MIN: Self = Self(i64::MIN);
52 pub const MAX: Self = Self(i64::MAX);
54 pub const EPSILON: Self = Self(1);
56
57 #[must_use]
59 pub const fn from_bits(raw: i64) -> Self {
60 Self(raw)
61 }
62
63 #[must_use]
65 pub const fn to_bits(self) -> i64 {
66 self.0
67 }
68
69 #[must_use]
75 pub const fn from_int(value: i32) -> Self {
76 Self((value as i64) << FRAC_BITS)
77 }
78
79 #[must_use]
91 pub const fn from_ratio(numerator: i32, denominator: i32) -> Self {
92 assert!(
93 denominator != 0,
94 "Fixed::from_ratio needs a nonzero denominator"
95 );
96 let scaled = (numerator as i64) << FRAC_BITS;
97 let den = denominator as i64;
98 Self(round_div(scaled, den))
99 }
100
101 #[must_use]
103 pub const fn trunc_int(self) -> i64 {
104 self.0 / ONE_RAW
105 }
106
107 #[must_use]
109 pub const fn fract(self) -> Self {
110 Self(self.0 % ONE_RAW)
111 }
112
113 #[must_use]
115 pub fn abs(self) -> Self {
116 let Some(value) = self.0.checked_abs() else {
117 saturation::record();
118 return Self::MAX;
119 };
120 Self(value)
121 }
122
123 #[must_use]
125 pub const fn signum(self) -> Self {
126 Self(ONE_RAW * self.0.signum())
127 }
128
129 #[must_use]
131 pub const fn min(self, other: Self) -> Self {
132 if self.0 < other.0 { self } else { other }
133 }
134
135 #[must_use]
137 pub const fn max(self, other: Self) -> Self {
138 if self.0 > other.0 { self } else { other }
139 }
140
141 #[must_use]
148 pub const fn clamp(self, low: Self, high: Self) -> Self {
149 assert!(low.0 <= high.0, "Fixed::clamp needs low <= high");
150 self.max(low).min(high)
151 }
152
153 #[must_use]
166 pub fn saturating_mul(self, other: Self) -> Self {
167 let product = i128::from(self.0) * i128::from(other.0);
168 Self(narrow(round_shift(product)))
169 }
170
171 #[must_use]
179 pub fn saturating_div(self, other: Self) -> Self {
180 assert!(other.0 != 0, "Fixed division by zero");
181 let numerator = i128::from(self.0) << FRAC_BITS;
182 Self(narrow(round_div_i128(numerator, i128::from(other.0))))
183 }
184
185 #[must_use]
199 pub fn sqrt(self) -> Self {
200 assert!(self.0 >= 0, "Fixed::sqrt of a negative value");
201 self.checked_sqrt().unwrap_or(Self::ZERO)
204 }
205
206 #[must_use]
208 pub fn checked_sqrt(self) -> Option<Self> {
209 if self.0 < 0 {
210 return None;
211 }
212 #[expect(
217 clippy::cast_sign_loss,
218 clippy::cast_possible_truncation,
219 reason = "guarded by the sign check above and by the root's own magnitude"
220 )]
221 let root = ((self.0 as u128) << FRAC_BITS).isqrt() as i64;
222 Some(Self(root))
223 }
224
225 #[must_use]
227 pub const fn checked_add(self, other: Self) -> Option<Self> {
228 match self.0.checked_add(other.0) {
229 Some(sum) => Some(Self(sum)),
230 None => None,
231 }
232 }
233
234 #[must_use]
236 pub const fn checked_sub(self, other: Self) -> Option<Self> {
237 match self.0.checked_sub(other.0) {
238 Some(difference) => Some(Self(difference)),
239 None => None,
240 }
241 }
242
243 #[must_use]
252 pub const fn checked_div(self, other: Self) -> Option<Self> {
253 if other.0 == 0 {
254 return None;
255 }
256 let rounded = round_div_i128((self.0 as i128) << FRAC_BITS, other.0 as i128);
257 if rounded > i64::MAX as i128 || rounded < i64::MIN as i128 {
258 None
259 } else {
260 #[expect(
261 clippy::cast_possible_truncation,
262 reason = "the branches above establish the value is in range"
263 )]
264 let narrowed = rounded as i64;
265 Some(Self(narrowed))
266 }
267 }
268
269 #[must_use]
275 pub const fn checked_mul(self, other: Self) -> Option<Self> {
276 let rounded = round_shift(self.0 as i128 * other.0 as i128);
277 if rounded > i64::MAX as i128 || rounded < i64::MIN as i128 {
278 None
279 } else {
280 #[expect(
281 clippy::cast_possible_truncation,
282 reason = "the branches above establish the value is in range"
283 )]
284 let narrowed = rounded as i64;
285 Some(Self(narrowed))
286 }
287 }
288}
289
290const fn round_shift(product: i128) -> i128 {
293 let half = 1i128 << (FRAC_BITS - 1);
294 if product >= 0 {
295 (product + half) >> FRAC_BITS
296 } else {
297 -((-product + half) >> FRAC_BITS)
300 }
301}
302
303const fn round_div(numerator: i64, denominator: i64) -> i64 {
305 let (magnitude, negative) = match (numerator < 0, denominator < 0) {
306 (false, false) => (numerator / denominator, false),
307 (true, true) => ((-numerator) / (-denominator), false),
308 (true, false) => ((-numerator) / denominator, true),
309 (false, true) => (numerator / (-denominator), true),
310 };
311 let remainder = (numerator % denominator).abs();
312 let half = denominator.abs() / 2;
313 let rounded = if remainder * 2 >= denominator.abs() && half >= 0 {
314 magnitude + 1
315 } else {
316 magnitude
317 };
318 if negative { -rounded } else { rounded }
319}
320
321const fn round_div_i128(numerator: i128, denominator: i128) -> i128 {
323 let negative = (numerator < 0) != (denominator < 0);
324 let num = if numerator < 0 { -numerator } else { numerator };
325 let den = if denominator < 0 {
326 -denominator
327 } else {
328 denominator
329 };
330 let quotient = num / den;
331 let rounded = if (num % den) * 2 >= den {
332 quotient + 1
333 } else {
334 quotient
335 };
336 if negative { -rounded } else { rounded }
337}
338
339fn narrow(value: i128) -> i64 {
341 if value > i128::from(i64::MAX) {
342 saturation::record();
343 i64::MAX
344 } else if value < i128::from(i64::MIN) {
345 saturation::record();
346 i64::MIN
347 } else {
348 #[expect(
349 clippy::cast_possible_truncation,
350 reason = "the branches above establish the value is in range"
351 )]
352 let narrowed = value as i64;
353 narrowed
354 }
355}
356
357impl Add for Fixed {
358 type Output = Self;
359 fn add(self, other: Self) -> Self {
360 let Some(sum) = self.0.checked_add(other.0) else {
361 saturation::record();
362 return if self.0 > 0 { Self::MAX } else { Self::MIN };
363 };
364 Self(sum)
365 }
366}
367
368impl Sub for Fixed {
369 type Output = Self;
370 fn sub(self, other: Self) -> Self {
371 let Some(difference) = self.0.checked_sub(other.0) else {
372 saturation::record();
373 return if self.0 > 0 { Self::MAX } else { Self::MIN };
374 };
375 Self(difference)
376 }
377}
378
379impl Neg for Fixed {
380 type Output = Self;
381 fn neg(self) -> Self {
382 let Some(negated) = self.0.checked_neg() else {
383 saturation::record();
384 return Self::MAX;
385 };
386 Self(negated)
387 }
388}
389
390impl Mul for Fixed {
391 type Output = Self;
392 fn mul(self, other: Self) -> Self {
393 self.saturating_mul(other)
394 }
395}
396
397impl Div for Fixed {
398 type Output = Self;
399 fn div(self, other: Self) -> Self {
400 self.saturating_div(other)
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::{Fixed, round_div};
407 use crate::saturations;
408
409 #[test]
410 fn absolute_value_saturates_at_the_bottom_of_the_range() {
411 assert_eq!(Fixed::from_int(-3).abs(), Fixed::from_int(3));
412 assert_eq!(Fixed::from_int(3).abs(), Fixed::from_int(3));
413 assert_eq!(Fixed::ZERO.abs(), Fixed::ZERO);
414 let before = saturations();
417 assert_eq!(Fixed::MIN.abs(), Fixed::MAX);
418 assert_eq!(saturations().0, before.0 + 1, "the clamp must be counted");
419 }
420
421 #[test]
422 fn signum_reports_whole_units() {
423 assert_eq!(Fixed::from_int(-9).signum(), Fixed::from_int(-1));
424 assert_eq!(Fixed::ZERO.signum(), Fixed::ZERO);
425 assert_eq!(Fixed::from_ratio(1, 1000).signum(), Fixed::ONE);
426 }
427
428 #[test]
429 fn min_max_and_clamp_agree_with_the_ordering() {
430 let low = Fixed::from_int(-2);
431 let high = Fixed::from_int(5);
432 assert_eq!(low.min(high), low);
433 assert_eq!(low.max(high), high);
434 assert_eq!(Fixed::from_int(9).clamp(low, high), high);
435 assert_eq!(Fixed::from_int(-9).clamp(low, high), low);
436 assert_eq!(Fixed::from_int(1).clamp(low, high), Fixed::from_int(1));
437 }
438
439 #[test]
440 #[should_panic(expected = "Fixed::clamp needs low <= high")]
441 fn clamp_refuses_an_inverted_range() {
442 let _ = Fixed::ZERO.clamp(Fixed::ONE, Fixed::ZERO);
443 }
444
445 #[test]
446 #[should_panic(expected = "Fixed::from_ratio needs a nonzero denominator")]
447 fn a_ratio_over_zero_is_refused() {
448 let _ = Fixed::from_ratio(1, 0);
449 }
450
451 #[test]
452 #[should_panic(expected = "Fixed division by zero")]
453 fn division_by_zero_is_refused() {
454 let _ = Fixed::ONE.saturating_div(Fixed::ZERO);
455 }
456
457 #[test]
458 #[should_panic(expected = "Fixed::sqrt of a negative value")]
459 fn the_square_root_of_a_negative_is_refused() {
460 let _ = Fixed::from_int(-1).sqrt();
461 }
462
463 #[test]
466 fn the_checked_forms_report_rather_than_saturate() {
467 assert_eq!(Fixed::ONE.checked_add(Fixed::ONE), Some(Fixed::from_int(2)));
468 assert_eq!(Fixed::MAX.checked_add(Fixed::ONE), None);
469 assert_eq!(Fixed::ONE.checked_sub(Fixed::ONE), Some(Fixed::ZERO));
470 assert_eq!(Fixed::MIN.checked_sub(Fixed::ONE), None);
471 assert_eq!(
472 Fixed::from_int(3).checked_mul(Fixed::from_int(4)),
473 Some(Fixed::from_int(12))
474 );
475 assert_eq!(Fixed::MAX.checked_mul(Fixed::MAX), None);
476
477 let before = saturations();
480 let _ = Fixed::MAX.checked_add(Fixed::ONE);
481 let _ = Fixed::MAX.checked_mul(Fixed::MAX);
482 assert_eq!(saturations(), before);
483 }
484
485 #[test]
489 fn checked_division_answers_where_the_asserting_form_refuses() {
490 assert_eq!(
491 Fixed::from_int(6).checked_div(Fixed::from_int(3)),
492 Some(Fixed::from_int(2))
493 );
494 assert_eq!(Fixed::ONE.checked_div(Fixed::ZERO), None);
495 assert_eq!(Fixed::ZERO.checked_div(Fixed::ZERO), None);
496 assert_eq!(Fixed::MAX.checked_div(Fixed::EPSILON), None);
498 assert_eq!(
501 Fixed::from_int(7).checked_div(Fixed::from_int(2)),
502 Some(Fixed::from_int(7).saturating_div(Fixed::from_int(2)))
503 );
504 let before = saturations();
506 let _ = Fixed::MAX.checked_div(Fixed::EPSILON);
507 let _ = Fixed::ONE.checked_div(Fixed::ZERO);
508 assert_eq!(saturations(), before);
509 }
510
511 #[test]
512 fn subtraction_and_negation_saturate_at_both_ends() {
513 assert_eq!(Fixed::from_int(5) - Fixed::from_int(3), Fixed::from_int(2));
514 assert_eq!(-Fixed::from_int(3), Fixed::from_int(-3));
515 let before = saturations();
516 assert_eq!(Fixed::MAX - Fixed::MIN, Fixed::MAX);
517 assert_eq!(-Fixed::MIN, Fixed::MAX);
518 assert_eq!(saturations().0, before.0 + 2);
519 }
520
521 #[test]
525 fn the_parts_of_a_negative_value_carry_its_sign() {
526 let value = Fixed::from_ratio(-7, 2);
527 assert_eq!(value.trunc_int(), -3);
528 assert_eq!(value.fract(), Fixed::from_ratio(-1, 2));
529 }
530
531 #[test]
534 fn ratios_round_symmetrically() {
535 assert_eq!(Fixed::from_ratio(-981, 100), -Fixed::from_ratio(981, 100));
536 assert_eq!(Fixed::from_ratio(981, -100), -Fixed::from_ratio(981, 100));
537 assert_eq!(Fixed::from_ratio(1, 2), Fixed::from_bits(1 << 15));
538 }
539
540 #[test]
544 fn the_rounding_helper_is_symmetric_and_rounds_ties_away_from_zero() {
545 assert_eq!(round_div(7, 2), 4);
546 assert_eq!(round_div(-7, 2), -4);
547 assert_eq!(round_div(7, -2), -4);
548 assert_eq!(round_div(-7, -2), 4);
549 assert_eq!(round_div(5, 2), 3, "a tie rounds away from zero");
550 assert_eq!(round_div(-5, 2), -3, "and symmetrically");
551 assert_eq!(round_div(4, 2), 2, "an exact quotient is untouched");
552 }
553
554 #[test]
559 fn the_operators_delegate_to_the_named_forms() {
560 let a = Fixed::from_ratio(7, 3);
561 let b = Fixed::from_ratio(-11, 5);
562 assert_eq!(a * b, a.saturating_mul(b));
563 assert_eq!(a / b, a.saturating_div(b));
564 assert_eq!(a + b, Fixed::from_bits(a.to_bits() + b.to_bits()));
565 assert_eq!(a - b, Fixed::from_bits(a.to_bits() - b.to_bits()));
566 assert_eq!(Fixed::MAX * Fixed::MAX, Fixed::MAX);
568 }
569
570 #[test]
572 fn division_saturates_when_the_quotient_does_not_fit() {
573 let before = saturations();
574 assert_eq!(Fixed::MAX.saturating_div(Fixed::EPSILON), Fixed::MAX);
575 assert_eq!(saturations().0, before.0 + 1);
576 }
577}