about summary refs log tree commit diff stats
path: root/crates/core/src/numerics/rational.rs
blob: e814dbdbf1d7aa70aa3ec626831236c70e961d6c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
//! Rational numbers.

//TODO: implement overloads for `Rational`.
//TODO: reduce rational numbers before construction is finished.

use crate::numerics::Integer;

#[cfg(feature = "core-fmt")]
use core::fmt;

#[cfg(feature = "core-error")]
use core::error;

/// Rational number.
///
/// All operations
#[derive(Eq, PartialEq, Copy, Clone)]
#[cfg_attr(feature = "core-fmt", derive(Debug))]
pub struct Rational<T> {
    numerator: T,
    denominator: T,
}

impl<T> Rational<T>
where
    T: Integer,
{
    /// Constructs a new `Rational<T>`.
    ///
    /// # Panics
    ///
    /// Panics if the denominator is zero.
    #[cfg(feature = "core-fmt")]
    pub fn new(numerator: T, denominator: T) -> Self {
        //PANIC: this is acceptable as the panic is mentioned above and it is
        //       used to assert an invariant.
        #[allow(clippy::expect_used)]
        Self::try_new(numerator, denominator)
            .expect("denominator should not be zero")
    }

    /// Constructs a new `Rational<T>`.
    ///
    /// # Errors
    ///
    /// Returns an error if the denominator is zero.
    pub fn try_new(numerator: T, denominator: T) -> Result<Self, Error> {
        if denominator == T::ZERO {
            return Err(Error::ZeroDenominator);
        }
        let mut rational = Self {
            numerator,
            denominator,
        };
        rational.reduce();
        Ok(rational)
    }

    /// Converts the rational into its components.
    #[inline]
    pub fn into_parts(self) -> (T, T) {
        (self.numerator, self.denominator)
    }

    /// Returns both the numerator and denominator of the rational.
    #[inline]
    pub fn as_parts(&self) -> (&T, &T) {
        (&self.numerator, &self.denominator)
    }

    /// Returns the numerator of the rational.
    #[inline]
    pub fn numerator(&self) -> &T {
        &self.numerator
    }

    /// Returns the denominator of the rational.
    #[inline]
    pub fn denominator(&self) -> &T {
        &self.denominator
    }

    /// Reduces the rational so that the numerator and denominator have no
    /// common factors and the denominator is greater than the zero element.
    fn reduce(&mut self) {
        if self.numerator == T::ZERO {
            self.denominator = T::ONE;
            return;
        }

        if self.numerator == self.denominator {
            self.numerator = T::ONE;
            self.denominator = T::ONE;
            return;
        }

        let gcd = self.numerator.gcd(&self.denominator);

        self.numerator /= gcd;
        self.denominator /= gcd;

        if self.denominator < T::ZERO {
            self.numerator = T::ZERO - self.numerator;
            self.denominator = T::ZERO - self.denominator;
        }
    }
}

/// Representation of an error that occurred within [`Rational`].
#[non_exhaustive]
#[derive(Eq, PartialEq)]
#[cfg_attr(feature = "core-fmt", derive(Debug))]
pub enum Error {
    /// Denominator was equal to zero.
    ZeroDenominator,
}

#[cfg(feature = "core-fmt")]
impl fmt::Display for Error {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        fmt.write_str("rational number construction failed")?;
        let reason = match self {
            Error::ZeroDenominator => " because the denominator was zero",
        };
        fmt.write_str(reason)
    }
}

#[cfg(feature = "core-error")]
impl error::Error for Error {}

#[cfg(all(test, feature = "core-error"))]
mod test {
    #![allow(clippy::expect_used)]

    use super::*;

    #[test]
    fn rational_sanity() {
        assert_eq!(
            Rational::try_new(6, 3)
                .expect("unable to construct a rational number")
                .into_parts(),
            (2, 1)
        );
    }
}