From 0fb93c7be8cb054c08b16cf823d7c797857650ab Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Sun, 31 Dec 2023 07:36:54 -0800 Subject: [PATCH] ZeroToOne now checks when dividing Refs #120 --- CHANGELOG.md | 2 ++ src/animation.rs | 83 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff81df2..891b66c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 incorrectly. - A potential edge case where a `DynamicReader` would not return after being disconnected has been removed. +- [#120][120]: Dividing a `ZeroToOne` now properly checks for `NaN` and `0.`. ### Changed @@ -71,6 +72,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 basis using `RunningWindow::kludgine_id()` as the key. [99]: https://github.com/khonsulabs/cushy/issues/99 +[120]: https://github.com/khonsulabs/cushy/issues/120 ## v0.2.0 diff --git a/src/animation.rs b/src/animation.rs index e6dea74..db17b9a 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -1146,6 +1146,21 @@ impl ZeroToOne { pub fn one_minus(self) -> Self { Self(1. - self.0) } + + fn checked_div(&mut self, rhs: Float) { + let rhs = rhs.into(); + if Float::CAN_BE_NAN { + assert!(!rhs.is_nan()); + } + if rhs > 0. { + self.0 /= rhs; + } else if *self > 0. { + // The limit of f(x) -> x/0 is infinity, but the highest value we + // can represent is 1.0. + *self = Self::ONE; + } + // If both lhs and rhs are 0, this is a noop. + } } impl Zero for ZeroToOne { @@ -1171,12 +1186,28 @@ impl FromStr for ZeroToOne { } impl From for ZeroToOne { + /// Returns a `ZeroToOne` clamped between 0.0 and 1.0. + /// + /// # Panics + /// + /// This function panics if `value` is not a number. fn from(value: f32) -> Self { Self::new(value) } } +impl From for f32 { + fn from(value: ZeroToOne) -> Self { + value.0 + } +} + impl From for ZeroToOne { + /// Returns a `ZeroToOne` clamped between 0.0 and 1.0. + /// + /// # Panics + /// + /// This function panics if `value` is not a number. fn from(value: f64) -> Self { Self::new(value.cast()) } @@ -1258,25 +1289,55 @@ impl MulAssign for ZeroToOne { impl Div for ZeroToOne { type Output = Self; - fn div(self, rhs: Self) -> Self::Output { - self / rhs.0 + fn div(mut self, rhs: Self) -> Self::Output { + self /= rhs; + self } } impl DivAssign for ZeroToOne { fn div_assign(&mut self, rhs: Self) { - self.0 /= rhs.0; + self.checked_div(rhs); } } impl Div for ZeroToOne { type Output = Self; - fn div(self, rhs: f32) -> Self::Output { - Self(self.0 / rhs) + /// Divides `self` by `rhs`. + /// + /// # Panics + /// + /// This function panics if `rhs` is not a number. + fn div(mut self, rhs: f32) -> Self::Output { + self /= rhs; + self } } +impl DivAssign for ZeroToOne { + /// Divides `self` by `rhs`. + /// + /// # Panics + /// + /// This function panics if `rhs` is not a number. + fn div_assign(&mut self, rhs: f32) { + self.checked_div(rhs); + } +} + +trait PossiblyNaN: Into { + const CAN_BE_NAN: bool; +} + +impl PossiblyNaN for ZeroToOne { + const CAN_BE_NAN: bool = false; +} + +impl PossiblyNaN for f32 { + const CAN_BE_NAN: bool = true; +} + impl Ranged for ZeroToOne { const MAX: Self = Self::ONE; const MIN: Self = Self::ZERO; @@ -1305,6 +1366,18 @@ impl From for Component { } } +#[test] +fn zero_to_one_div() { + std::panic::catch_unwind(|| ZeroToOne::ONE / f32::NAN).expect_err("div by nan"); + let mut value = ZeroToOne::ONE; + std::panic::catch_unwind(move || value /= f32::NAN).expect_err("div by nan"); + + assert_eq!(ZeroToOne::ZERO / ZeroToOne::ZERO, ZeroToOne::ZERO); + assert_eq!(ZeroToOne::new(0.5) / ZeroToOne::ZERO, ZeroToOne::ONE); + assert_eq!(ZeroToOne::ZERO / 0., ZeroToOne::ZERO); + assert_eq!(ZeroToOne::new(0.5) / 0., ZeroToOne::ONE); +} + /// An easing function for customizing animations. #[derive(Debug, Clone)] pub enum EasingFunction {