reduction_factor/
lib.rs

1#![no_std]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5use core::num::NonZeroUsize;
6use core::ops::{Add, Deref, DerefMut, Mul, MulAssign, Sub};
7
8use num_traits::{One, Zero};
9
10/// A newtype representing a reduction factor of `(1 - T)`.
11///
12/// See the [module-level documentation](self) for more information.
13///
14/// Note that this type does not implement the [`num_traits::One`](https://docs.rs/num-traits/latest/num_traits/identities/trait.One.html) trait.
15/// This is intentional as the trait requires `one()` to return the multiplicative identity, which for `Reduction` is [`Reduction::none()`].
16/// However, having `Reduction::one()` return a `Reduction(T::zero())` would be highly confusing.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18#[repr(transparent)]
19pub struct Reduction<T>(pub T);
20
21impl<T: fmt::Display> fmt::Display for Reduction<T> {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        write!(f, "(1 - {})", self.0)
24    }
25}
26
27impl<T> Reduction<T> {
28    /// Creates a new `Reduction` from the given inner value.
29    #[inline]
30    #[must_use]
31    pub const fn new(value: T) -> Self {
32        Self(value)
33    }
34
35    /// Creates a `Reduction` representing no reduction (0%).
36    ///
37    /// This is the multiplicative identity for composition: any reduction composed
38    /// with `none` remains unchanged.
39    ///
40    /// Its multiplier is 1.
41    #[must_use]
42    pub fn none() -> Self
43    where
44        T: Zero,
45    {
46        Self(T::zero())
47    }
48
49    /// Creates a `Reduction` representing a full reduction (100%).
50    ///
51    /// This is the absorbing element for composition: any reduction composed
52    /// with `full` results in `full`.
53    ///
54    /// Note that for floats, this property may not hold exactly.
55    ///
56    /// Its multiplier is 0.
57    #[must_use]
58    pub fn full() -> Self
59    where
60        T: One,
61    {
62        Self(T::one())
63    }
64
65    /// Consumes the `Reduction` and returns the inner value `x`.
66    #[inline]
67    pub fn inner(self) -> T {
68        self.0
69    }
70}
71impl<T: One + Sub<Output = T>> Reduction<T> {
72    /// Calculates the multiplicative factor, `1 - self.0`.
73    ///
74    /// This is the value that is multiplied with a base value when the reduction is applied.
75    ///
76    /// # Example
77    /// ```
78    /// use reduction_factor::Reduction;
79    /// let r = Reduction(0.25f32);
80    /// assert_eq!(r.multiplier(), 0.75);
81    /// ```
82    pub fn multiplier(self) -> T {
83        T::one() - self.0
84    }
85
86    /// Applies the reduction to a given value.
87    ///
88    /// This is equivalent to `value * self.multiplier()`.
89    ///
90    /// This operation is also available through multiplication: `reduction * value`.
91    ///
92    /// # Example
93    /// ```
94    /// use reduction_factor::Reduction;
95    /// let r = Reduction(0.25f32);
96    /// assert_eq!(r.reduce(100.0), 75.0);
97    /// assert_eq!(r * 100.0, 75.0);
98    /// ```
99    #[doc(alias = "apply")]
100    pub fn reduce(self, value: T) -> T {
101        value * self.multiplier()
102    }
103
104    /// Returns the complement of the reduction.
105    ///
106    /// The complement of `Reduction(x)` is `Reduction(1 - x)`.
107    /// For example, the complement of a 25% reduction is an 75% reduction.
108    ///
109    /// # Example
110    /// ```
111    /// use reduction_factor::Reduction;
112    /// let r = Reduction(0.25f32);
113    /// let complement = r.complement();
114    /// assert_eq!(*complement, 0.75);
115    /// ```
116    #[doc(alias = "invert")]
117    pub fn complement(self) -> Self {
118        Self(self.multiplier())
119    }
120}
121impl<T: Add<Output = T> + Sub<Output = T> + Mul<Output = T> + Clone> Reduction<T> {
122    /// Composes this reduction with another, returning a new `Reduction`.
123    ///
124    /// This is equivalent to applying one reduction and then the other.
125    ///
126    /// The formula for the new inner value is `x + y - xy` where
127    /// `x` and `y` are the inner values of the two reductions.
128    ///
129    /// This operation is also available through multiplication: `r1 * r2`.
130    ///
131    /// # Example
132    /// ```
133    /// use reduction_factor::Reduction;
134    /// let r1 = Reduction(0.20f32);
135    /// let r2 = Reduction(0.10f32);
136    /// let stacked = r1.compose(r2);
137    /// assert_eq!(*stacked, 0.28);
138    /// assert_eq!(r1 * r2, stacked); // Equivalent
139    /// ```
140    #[doc(alias = "stack")]
141    #[doc(alias = "combine")]
142    pub fn compose(self, other: Self) -> Self {
143        // (1 - x) * (1 - y) = 1 - x - y + xy = 1 - (x + y - xy)
144        Self(self.0.clone() + other.0.clone() - self.0 * other.0)
145    }
146
147    /// In-place version of [`compose`](Self::compose).
148    #[doc(alias = "stack_inplace")]
149    #[doc(alias = "combine_inplace")]
150    pub fn compose_inplace(&mut self, other: Self)
151    where
152        // `Default` bound is added because it's not worth dealing with the trouble of temporarily taking `T` out of a `&mut T`
153        T: Default,
154    {
155        let value = core::mem::take(&mut self.0);
156        self.0 = value.clone() + other.0.clone() - value * other.0;
157    }
158
159    /// Composes the reduction with itself `exponent` times.
160    ///
161    /// `pow(0)` returns `Reduction(0)` the identity reduction, [`Reduction::none()`].
162    ///
163    /// # Example
164    /// ```
165    /// use reduction_factor::Reduction;
166    /// let r = Reduction(0.5f32);
167    /// // Applying a 50% reduction twice results in a 75% reduction.
168    /// let r2 = r.pow(2);
169    /// assert_eq!(*r2, 0.75);
170    /// assert_eq!(r * r, r2);
171    /// ```
172    ///
173    /// Zero exponent returns the identity reduction.
174    /// ```
175    /// use reduction_factor::Reduction;
176    /// let r = Reduction(0.5f32);
177    ///
178    /// let r0 = r.pow(0);
179    /// assert_eq!(*r0, 0.0);
180    /// assert_eq!(r0, Reduction::none());
181    /// ```
182    #[doc(alias = "repeat")]
183    pub fn pow(&self, exponent: usize) -> Self
184    where
185        T: Zero,
186    {
187        if exponent == 0 {
188            Self::none()
189        } else if exponent % 2 == 0 {
190            let tmp = self.pow(exponent / 2);
191            tmp.clone().compose(tmp)
192        } else {
193            self.clone().compose(self.pow(exponent - 1))
194        }
195    }
196
197    /// Composes the reduction with itself `exponent` times for a non-zero exponent.
198    ///
199    /// The main difference from [`Reduction::pow()`] is that `T: Zero` isn't required.
200    ///
201    /// # Example
202    /// ```
203    /// use reduction_factor::Reduction;
204    /// use core::num::NonZeroUsize;
205    ///
206    /// let r = Reduction(0.5f32);
207    /// // Applying a 50% reduction twice results in a 75% reduction.
208    /// let r2 = r.pow_nonzero(NonZeroUsize::new(2).unwrap());
209    /// assert_eq!(*r2, 0.75);
210    /// assert_eq!(r * r, r2);
211    /// ```
212    #[doc(alias = "repeat_nonzero")]
213    pub fn pow_nonzero(&self, exponent: NonZeroUsize) -> Self {
214        if exponent.get() == 1 {
215            self.clone()
216        } else if exponent.get() % 2 == 0 {
217            // SAFETY: Since exponent can't be 1, it won't go to 0
218            let exponent = unsafe { NonZeroUsize::new_unchecked(exponent.get() / 2) };
219            let tmp = self.pow_nonzero(exponent);
220            tmp.clone().compose(tmp)
221        } else {
222            // SAFETY: Since exponent can't be 1, it won't go to 0
223            let exponent = unsafe { NonZeroUsize::new_unchecked(exponent.get() - 1) };
224            self.clone().compose(self.pow_nonzero(exponent))
225        }
226    }
227}
228
229impl<T> From<T> for Reduction<T> {
230    #[inline]
231    fn from(value: T) -> Self {
232        Self::new(value)
233    }
234}
235
236impl<T> Deref for Reduction<T> {
237    type Target = T;
238
239    #[inline]
240    fn deref(&self) -> &Self::Target {
241        &self.0
242    }
243}
244impl<T> DerefMut for Reduction<T> {
245    #[inline]
246    fn deref_mut(&mut self) -> &mut Self::Target {
247        &mut self.0
248    }
249}
250impl<T> AsRef<T> for Reduction<T> {
251    #[inline]
252    fn as_ref(&self) -> &T {
253        &self.0
254    }
255}
256impl<T> AsMut<T> for Reduction<T> {
257    #[inline]
258    fn as_mut(&mut self) -> &mut T {
259        &mut self.0
260    }
261}
262
263impl<T: Zero> Default for Reduction<T> {
264    #[inline]
265    fn default() -> Self {
266        Self::none()
267    }
268}
269
270impl<T: Add<Output = T> + Sub<Output = T> + Mul<Output = T> + Clone> Mul for Reduction<T> {
271    type Output = Self;
272
273    /// The multiplication operator is overloaded for two `Reduction`s to perform [`compose`](Self::compose).
274    ///
275    /// This is the idiomatic way to compose two reductions.
276    #[inline]
277    fn mul(self, rhs: Self) -> Self::Output {
278        self.compose(rhs)
279    }
280}
281impl<T: Default + Add<Output = T> + Sub<Output = T> + Mul<Output = T> + Clone> MulAssign
282    for Reduction<T>
283{
284    /// The `*=` operator is overloaded for two `Reduction`s to perform [`compose_inplace`](Self::compose_inplace).
285    #[inline]
286    fn mul_assign(&mut self, rhs: Self) {
287        self.compose_inplace(rhs)
288    }
289}
290impl<T: One + Sub<Output = T>> Mul<T> for Reduction<T> {
291    type Output = T;
292
293    /// The multiplication operator is overloaded to [`reduce`](Self::reduce) the reduction to a value of type `T`.
294    #[inline]
295    fn mul(self, rhs: T) -> Self::Output {
296        self.reduce(rhs)
297    }
298}
299
300// Does not implement `One` because `Reduction(1)` is not the multiplicative identity
301// (that's how `num_trait` defined its `One`).
302// The multiplicative identity is `Reduction(0)` or `Reduction::none()`.
303// But if `Reduction::one() == Reduction::none()`, that is way too confusing.
304
305// Cannot implement `Zero` because addition between `Reduction`s is not defined.
306// Even if it could be implemented, the same confusion would make it not worth it.
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311    use core::num::NonZeroUsize;
312
313    const EPSILON: f32 = 1e-6;
314
315    #[test]
316    fn test_apply_reduction() {
317        let price = 100.0f32;
318        let discount = Reduction(0.25);
319        assert!((discount * price - 75.0).abs() < EPSILON);
320    }
321
322    #[test]
323    fn test_compose() {
324        let r1 = Reduction(0.20f32);
325        let r2 = Reduction(0.10f32);
326        let combined = r1 * r2;
327        assert!((combined.inner() - 0.28).abs() < EPSILON);
328    }
329
330    #[test]
331    fn test_identity_and_default() {
332        let r1 = Reduction(0.3f32);
333        assert_eq!(r1 * Reduction::none(), r1);
334        assert_eq!(Reduction::none() * r1, r1);
335        assert_eq!(Reduction::<f32>::default(), Reduction::none());
336    }
337
338    #[test]
339    fn test_full_reduction() {
340        let r1 = Reduction(0.3f32);
341        let full = Reduction::full();
342        assert!((*(r1 * full) - *full).abs() < EPSILON);
343        assert!((*(full * r1) - *full).abs() < EPSILON);
344        assert!((full * 100.0f32).abs() < EPSILON);
345    }
346
347    #[test]
348    fn test_pow() {
349        let r = Reduction(0.5f32);
350        assert_eq!(r.pow(0), Reduction::none());
351        assert_eq!(r.pow(1), r);
352        assert!((r.pow(2).inner() - 0.75).abs() < EPSILON); // 0.5 + 0.5 - 0.25 = 0.75
353        assert!((r.pow(3).inner() - 0.875).abs() < EPSILON);
354    }
355
356    #[test]
357    fn test_pow_nonzero() {
358        let r = Reduction(0.5f32);
359        assert_eq!(r.pow_nonzero(NonZeroUsize::new(1).unwrap()), r);
360        assert!((r.pow_nonzero(NonZeroUsize::new(2).unwrap()).inner() - 0.75).abs() < EPSILON);
361    }
362
363    #[test]
364    fn test_complement() {
365        let r = Reduction(0.2f32);
366        let c = r.complement();
367        assert!((c.inner() - 0.8).abs() < EPSILON);
368        assert!((c.complement().inner() - 0.2).abs() < EPSILON);
369    }
370}