Skip to main content

use_equation/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Small equation-solving primitives for `RustUse`.
5
6#[doc(inline)]
7pub use linear::{LinearEquation, solve_linear};
8#[cfg(feature = "polynomial")]
9#[doc(inline)]
10pub use polynomial::solve_polynomial_degree_1_or_2;
11#[doc(inline)]
12pub use quadratic::{QuadraticEquation, solve_quadratic};
13#[doc(inline)]
14pub use root::{RootSolver, Roots};
15#[doc(inline)]
16pub use system::{LinearSystem2, solve_2x2};
17
18/// Root result values and shared solving traits.
19pub mod root {
20    /// Real-root outcomes for the small equation helpers in this crate.
21    #[derive(Debug, Clone, PartialEq)]
22    pub enum Roots {
23        /// The equation has no finite real roots.
24        None,
25        /// The equation has exactly one finite real root.
26        One(f64),
27        /// The equation has exactly two distinct finite real roots.
28        Two(f64, f64),
29        /// The equation is satisfied by every real value.
30        Infinite,
31    }
32
33    /// Shared trait for equation types that can solve themselves.
34    pub trait RootSolver {
35        /// The solved output type for the equation.
36        type Output;
37
38        /// Solves the equation and returns its concrete output.
39        fn solve(&self) -> Self::Output;
40    }
41}
42
43/// Linear equation helpers for equations of the form `ax + b = 0`.
44pub mod linear {
45    use crate::root::{RootSolver, Roots};
46
47    /// A linear equation of the form `ax + b = 0`.
48    #[derive(Debug, Clone, Copy, PartialEq)]
49    pub struct LinearEquation {
50        /// The coefficient for `x`.
51        pub a: f64,
52        /// The constant term.
53        pub b: f64,
54    }
55
56    impl LinearEquation {
57        /// Creates a linear equation of the form `ax + b = 0`.
58        #[must_use]
59        pub const fn new(a: f64, b: f64) -> Self {
60            Self { a, b }
61        }
62    }
63
64    impl RootSolver for LinearEquation {
65        type Output = Roots;
66
67        fn solve(&self) -> Self::Output {
68            solve_linear(self.a, self.b)
69        }
70    }
71
72    /// Solves the linear equation `ax + b = 0`.
73    ///
74    /// Returns [`Roots::Infinite`] when both coefficients are zero, and
75    /// [`Roots::None`] when the equation has no finite real root.
76    #[must_use]
77    pub fn solve_linear(a: f64, b: f64) -> Roots {
78        if a == 0.0 {
79            if b == 0.0 {
80                Roots::Infinite
81            } else {
82                Roots::None
83            }
84        } else {
85            let root = -b / a;
86
87            if root.is_finite() {
88                Roots::One(root)
89            } else {
90                Roots::None
91            }
92        }
93    }
94}
95
96/// Quadratic equation helpers for equations of the form `ax^2 + bx + c = 0`.
97pub mod quadratic {
98    use crate::{
99        linear::solve_linear,
100        root::{RootSolver, Roots},
101    };
102
103    /// A quadratic equation of the form `ax^2 + bx + c = 0`.
104    #[derive(Debug, Clone, Copy, PartialEq)]
105    pub struct QuadraticEquation {
106        /// The coefficient for `x^2`.
107        pub a: f64,
108        /// The coefficient for `x`.
109        pub b: f64,
110        /// The constant term.
111        pub c: f64,
112    }
113
114    impl QuadraticEquation {
115        /// Creates a quadratic equation of the form `ax^2 + bx + c = 0`.
116        #[must_use]
117        pub const fn new(a: f64, b: f64, c: f64) -> Self {
118            Self { a, b, c }
119        }
120    }
121
122    impl RootSolver for QuadraticEquation {
123        type Output = Roots;
124
125        fn solve(&self) -> Self::Output {
126            solve_quadratic(self.a, self.b, self.c)
127        }
128    }
129
130    /// Solves the quadratic equation `ax^2 + bx + c = 0` over the real numbers.
131    ///
132    /// When `a == 0.0`, this falls back to solving `bx + c = 0` with
133    /// [`solve_linear`]. Roots are returned in ascending order when two
134    /// distinct real roots exist.
135    #[must_use]
136    pub fn solve_quadratic(a: f64, b: f64, c: f64) -> Roots {
137        if a == 0.0 {
138            return solve_linear(b, c);
139        }
140
141        let discriminant = b.mul_add(b, -4.0 * a * c);
142        if !discriminant.is_finite() {
143            return Roots::None;
144        }
145
146        if discriminant < 0.0 {
147            return Roots::None;
148        }
149
150        if discriminant == 0.0 {
151            let root = -b / (2.0 * a);
152            return if root.is_finite() {
153                Roots::One(root)
154            } else {
155                Roots::None
156            };
157        }
158
159        let sqrt_discriminant = discriminant.sqrt();
160        let first_root = (-b - sqrt_discriminant) / (2.0 * a);
161        let second_root = (-b + sqrt_discriminant) / (2.0 * a);
162
163        if !first_root.is_finite() || !second_root.is_finite() {
164            return Roots::None;
165        }
166
167        if first_root <= second_root {
168            Roots::Two(first_root, second_root)
169        } else {
170            Roots::Two(second_root, first_root)
171        }
172    }
173}
174
175/// Small `2x2` linear-system helpers.
176pub mod system {
177    use crate::root::RootSolver;
178
179    /// A `2x2` linear system of the form:
180    ///
181    /// `a11*x + a12*y = b1`
182    ///
183    /// `a21*x + a22*y = b2`
184    #[derive(Debug, Clone, Copy, PartialEq)]
185    pub struct LinearSystem2 {
186        /// Row 1, column 1 coefficient.
187        pub a11: f64,
188        /// Row 1, column 2 coefficient.
189        pub a12: f64,
190        /// Row 1 right-hand side.
191        pub b1: f64,
192        /// Row 2, column 1 coefficient.
193        pub a21: f64,
194        /// Row 2, column 2 coefficient.
195        pub a22: f64,
196        /// Row 2 right-hand side.
197        pub b2: f64,
198    }
199
200    impl LinearSystem2 {
201        /// Creates a `2x2` linear system.
202        #[must_use]
203        pub const fn new(a11: f64, a12: f64, b1: f64, a21: f64, a22: f64, b2: f64) -> Self {
204            Self {
205                a11,
206                a12,
207                b1,
208                a21,
209                a22,
210                b2,
211            }
212        }
213    }
214
215    impl RootSolver for LinearSystem2 {
216        type Output = Option<(f64, f64)>;
217
218        fn solve(&self) -> Self::Output {
219            solve_2x2(self.a11, self.a12, self.b1, self.a21, self.a22, self.b2)
220        }
221    }
222
223    /// Solves a `2x2` linear system with Cramer's rule.
224    ///
225    /// Returns `None` when the determinant is zero, non-finite, or when the
226    /// computed solution is not finite.
227    #[must_use]
228    pub fn solve_2x2(
229        a11: f64,
230        a12: f64,
231        b1: f64,
232        a21: f64,
233        a22: f64,
234        b2: f64,
235    ) -> Option<(f64, f64)> {
236        let determinant = a11 * a22 - a12 * a21;
237        if determinant == 0.0 || !determinant.is_finite() {
238            return None;
239        }
240
241        let x = (b1 * a22 - a12 * b2) / determinant;
242        let y = (a11 * b2 - b1 * a21) / determinant;
243
244        if x.is_finite() && y.is_finite() {
245            Some((x, y))
246        } else {
247            None
248        }
249    }
250}
251
252#[cfg(feature = "polynomial")]
253/// Low-degree polynomial bridge helpers.
254pub mod polynomial {
255    use crate::{linear::solve_linear, quadratic::solve_quadratic, root::Roots};
256    use use_polynomial::Polynomial;
257
258    /// Solves a polynomial when its degree is 0, 1, or 2.
259    ///
260    /// Returns `Some(Roots)` for low-degree polynomials and `None` for higher
261    /// degrees. The zero polynomial returns [`Roots::Infinite`].
262    #[must_use]
263    pub fn solve_polynomial_degree_1_or_2(polynomial: &Polynomial) -> Option<Roots> {
264        match polynomial.degree() {
265            None => Some(Roots::Infinite),
266            Some(0) => {
267                let constant = polynomial.coefficients().first().copied().unwrap_or(0.0);
268                Some(if constant == 0.0 {
269                    Roots::Infinite
270                } else {
271                    Roots::None
272                })
273            },
274            Some(1) => Some(solve_linear(
275                polynomial.coefficients()[1],
276                polynomial.coefficients()[0],
277            )),
278            Some(2) => Some(solve_quadratic(
279                polynomial.coefficients()[2],
280                polynomial.coefficients()[1],
281                polynomial.coefficients()[0],
282            )),
283            Some(_) => None,
284        }
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::{
291        LinearEquation, LinearSystem2, QuadraticEquation, RootSolver, Roots, solve_2x2,
292        solve_linear, solve_quadratic,
293    };
294
295    #[test]
296    fn solves_linear_equation_with_one_root() {
297        assert_eq!(solve_linear(2.0, -4.0), Roots::One(2.0));
298    }
299
300    #[test]
301    fn solves_linear_equation_with_no_roots() {
302        assert_eq!(solve_linear(0.0, 5.0), Roots::None);
303    }
304
305    #[test]
306    fn solves_linear_equation_with_infinite_roots() {
307        assert_eq!(solve_linear(0.0, 0.0), Roots::Infinite);
308    }
309
310    #[test]
311    fn solves_quadratic_equation_with_two_real_roots() {
312        assert_eq!(solve_quadratic(1.0, -3.0, 2.0), Roots::Two(1.0, 2.0));
313    }
314
315    #[test]
316    fn solves_quadratic_equation_with_one_repeated_root() {
317        assert_eq!(solve_quadratic(1.0, -2.0, 1.0), Roots::One(1.0));
318    }
319
320    #[test]
321    fn solves_quadratic_equation_with_no_real_roots() {
322        assert_eq!(solve_quadratic(1.0, 0.0, 1.0), Roots::None);
323    }
324
325    #[test]
326    fn quadratic_falls_back_to_linear_solving() {
327        assert_eq!(solve_quadratic(0.0, 2.0, -4.0), Roots::One(2.0));
328    }
329
330    #[test]
331    fn solves_2x2_system_with_one_solution() {
332        assert_eq!(solve_2x2(2.0, 1.0, 5.0, 1.0, -1.0, 1.0), Some((2.0, 1.0)));
333    }
334
335    #[test]
336    fn rejects_singular_2x2_system() {
337        assert_eq!(solve_2x2(1.0, 2.0, 3.0, 2.0, 4.0, 6.0), None);
338    }
339
340    #[test]
341    fn rejects_non_finite_determinant() {
342        assert_eq!(solve_2x2(f64::INFINITY, 0.0, 1.0, 0.0, 1.0, 2.0), None);
343    }
344
345    #[test]
346    fn linear_equation_struct_solves() {
347        let equation = LinearEquation::new(2.0, -4.0);
348
349        assert_eq!(equation.solve(), Roots::One(2.0));
350    }
351
352    #[test]
353    fn quadratic_equation_struct_solves() {
354        let equation = QuadraticEquation::new(1.0, -3.0, 2.0);
355
356        assert_eq!(equation.solve(), Roots::Two(1.0, 2.0));
357    }
358
359    #[test]
360    fn linear_system_struct_solves() {
361        let system = LinearSystem2::new(2.0, 1.0, 5.0, 1.0, -1.0, 1.0);
362
363        assert_eq!(system.solve(), Some((2.0, 1.0)));
364    }
365
366    #[cfg(feature = "polynomial")]
367    #[test]
368    fn solves_supported_polynomials() {
369        use super::solve_polynomial_degree_1_or_2;
370        use use_polynomial::Polynomial;
371
372        assert_eq!(
373            solve_polynomial_degree_1_or_2(&Polynomial::new(vec![2.0, -3.0, 1.0])),
374            Some(Roots::Two(1.0, 2.0))
375        );
376        assert_eq!(
377            solve_polynomial_degree_1_or_2(&Polynomial::zero()),
378            Some(Roots::Infinite)
379        );
380        assert_eq!(
381            solve_polynomial_degree_1_or_2(&Polynomial::new(vec![1.0, 0.0, 0.0, 1.0])),
382            None
383        );
384    }
385}