1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[doc(inline)]
7pub use polynomial::Polynomial;
8#[doc(inline)]
9pub use roots::{linear_root, quadratic_roots};
10
11pub mod polynomial {
13 use core::ops::{Add, Div, Mul, Neg, Sub};
14
15 use crate::roots::{linear_root, quadratic_roots};
16
17 #[derive(Debug, Clone, PartialEq)]
24 pub struct Polynomial {
25 coefficients: Vec<f64>,
26 }
27
28 impl Polynomial {
29 #[must_use]
34 pub fn new(mut coefficients: Vec<f64>) -> Self {
35 while coefficients
36 .last()
37 .is_some_and(|coefficient| *coefficient == 0.0)
38 {
39 coefficients.pop();
40 }
41
42 Self { coefficients }
43 }
44
45 #[must_use]
47 pub fn constant(value: f64) -> Self {
48 Self::new(vec![value])
49 }
50
51 #[must_use]
53 pub fn zero() -> Self {
54 Self::new(Vec::new())
55 }
56
57 #[must_use]
59 pub fn one() -> Self {
60 Self::constant(1.0)
61 }
62
63 #[must_use]
65 pub fn coefficients(&self) -> &[f64] {
66 &self.coefficients
67 }
68
69 #[must_use]
71 pub const fn degree(&self) -> Option<usize> {
72 if self.coefficients.is_empty() {
73 None
74 } else {
75 Some(self.coefficients.len() - 1)
76 }
77 }
78
79 #[must_use]
81 pub fn leading_coefficient(&self) -> Option<f64> {
82 self.coefficients.last().copied()
83 }
84
85 #[must_use]
87 pub const fn is_zero(&self) -> bool {
88 self.coefficients.is_empty()
89 }
90
91 #[must_use]
93 pub fn evaluate(&self, x: f64) -> f64 {
94 self.coefficients
95 .iter()
96 .rev()
97 .fold(0.0, |accumulator, coefficient| {
98 accumulator * x + coefficient
99 })
100 }
101
102 #[must_use]
104 #[allow(clippy::cast_precision_loss)]
105 pub fn derivative(&self) -> Self {
106 if self.coefficients.len() < 2 {
107 return Self::zero();
108 }
109
110 let coefficients = self
111 .coefficients
112 .iter()
113 .enumerate()
114 .skip(1)
115 .map(|(index, coefficient)| *coefficient * index as f64)
116 .collect();
117
118 Self::new(coefficients)
119 }
120
121 #[must_use]
123 pub fn nth_derivative(&self, n: usize) -> Self {
124 if n == 0 {
125 return self.clone();
126 }
127
128 let mut derivative = self.clone();
129 for _ in 0..n {
130 derivative = derivative.derivative();
131 if derivative.is_zero() {
132 break;
133 }
134 }
135
136 derivative
137 }
138
139 #[must_use]
141 #[allow(clippy::cast_precision_loss)]
142 pub fn integral(&self, constant: f64) -> Self {
143 let mut coefficients = Vec::with_capacity(self.coefficients.len() + 1);
144 coefficients.push(constant);
145 coefficients.extend(
146 self.coefficients
147 .iter()
148 .enumerate()
149 .map(|(index, coefficient)| *coefficient / (index + 1) as f64),
150 );
151
152 Self::new(coefficients)
153 }
154
155 #[must_use]
160 pub fn real_roots_degree_1_or_2(&self) -> Option<Vec<f64>> {
161 match self.degree() {
162 None | Some(0) => Some(Vec::new()),
163 Some(1) => Some(
164 linear_root(self.coefficients[1], self.coefficients[0])
165 .into_iter()
166 .collect(),
167 ),
168 Some(2) => Some(quadratic_roots(
169 self.coefficients[2],
170 self.coefficients[1],
171 self.coefficients[0],
172 )),
173 Some(_) => None,
174 }
175 }
176 }
177
178 impl Add for Polynomial {
179 type Output = Self;
180
181 fn add(self, rhs: Self) -> Self::Output {
182 let max_len = self.coefficients.len().max(rhs.coefficients.len());
183 let coefficients = (0..max_len)
184 .map(|index| {
185 self.coefficients.get(index).copied().unwrap_or(0.0)
186 + rhs.coefficients.get(index).copied().unwrap_or(0.0)
187 })
188 .collect();
189
190 Self::new(coefficients)
191 }
192 }
193
194 impl Sub for Polynomial {
195 type Output = Self;
196
197 fn sub(self, rhs: Self) -> Self::Output {
198 let max_len = self.coefficients.len().max(rhs.coefficients.len());
199 let coefficients = (0..max_len)
200 .map(|index| {
201 self.coefficients.get(index).copied().unwrap_or(0.0)
202 - rhs.coefficients.get(index).copied().unwrap_or(0.0)
203 })
204 .collect();
205
206 Self::new(coefficients)
207 }
208 }
209
210 impl Mul for Polynomial {
211 type Output = Self;
212
213 fn mul(self, rhs: Self) -> Self::Output {
214 if self.is_zero() || rhs.is_zero() {
215 return Self::zero();
216 }
217
218 let mut coefficients = vec![0.0; self.coefficients.len() + rhs.coefficients.len() - 1];
219
220 for (left_index, left_coefficient) in self.coefficients.iter().enumerate() {
221 for (right_index, right_coefficient) in rhs.coefficients.iter().enumerate() {
222 coefficients[left_index + right_index] += left_coefficient * right_coefficient;
223 }
224 }
225
226 Self::new(coefficients)
227 }
228 }
229
230 impl Neg for Polynomial {
231 type Output = Self;
232
233 fn neg(self) -> Self::Output {
234 Self::new(
235 self.coefficients
236 .into_iter()
237 .map(|coefficient| -coefficient)
238 .collect(),
239 )
240 }
241 }
242
243 impl Mul<f64> for Polynomial {
244 type Output = Self;
245
246 fn mul(self, rhs: f64) -> Self::Output {
247 Self::new(
248 self.coefficients
249 .into_iter()
250 .map(|coefficient| coefficient * rhs)
251 .collect(),
252 )
253 }
254 }
255
256 impl Div<f64> for Polynomial {
257 type Output = Self;
258
259 fn div(self, rhs: f64) -> Self::Output {
260 Self::new(
261 self.coefficients
262 .into_iter()
263 .map(|coefficient| coefficient / rhs)
264 .collect(),
265 )
266 }
267 }
268}
269
270pub mod roots {
272 #[must_use]
276 pub fn linear_root(a: f64, b: f64) -> Option<f64> {
277 (a != 0.0).then_some(-b / a)
278 }
279
280 #[must_use]
285 pub fn quadratic_roots(a: f64, b: f64, c: f64) -> Vec<f64> {
286 if a == 0.0 {
287 return linear_root(b, c).into_iter().collect();
288 }
289
290 let discriminant = b.mul_add(b, -(4.0 * a * c));
291
292 if discriminant.is_nan() || discriminant < 0.0 {
293 return Vec::new();
294 }
295
296 if discriminant == 0.0 {
297 return vec![-b / (2.0 * a)];
298 }
299
300 let square_root = discriminant.sqrt();
301 let first = (-b - square_root) / (2.0 * a);
302 let second = (-b + square_root) / (2.0 * a);
303
304 if first <= second {
305 vec![first, second]
306 } else {
307 vec![second, first]
308 }
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::{Polynomial, linear_root, quadratic_roots};
315
316 #[test]
317 fn constructor_trims_trailing_zeros() {
318 let polynomial = Polynomial::new(vec![1.0, 2.0, 0.0, 0.0]);
319
320 assert_eq!(polynomial.coefficients(), &[1.0, 2.0]);
321 }
322
323 #[test]
324 fn zero_polynomial_uses_empty_representation() {
325 let zero = Polynomial::new(vec![0.0, 0.0]);
326
327 assert!(zero.coefficients().is_empty());
328 assert_eq!(zero.degree(), None);
329 assert_eq!(zero.leading_coefficient(), None);
330 assert!(zero.is_zero());
331 }
332
333 #[test]
334 fn constant_constructors_work() {
335 assert_eq!(Polynomial::constant(4.0).coefficients(), &[4.0]);
336 assert!(Polynomial::zero().is_zero());
337 assert_eq!(Polynomial::one().coefficients(), &[1.0]);
338 }
339
340 #[test]
341 fn reports_degree_and_leading_coefficient() {
342 let polynomial = Polynomial::new(vec![3.0, 2.0, 1.0]);
343
344 assert_eq!(polynomial.degree(), Some(2));
345 assert_eq!(polynomial.leading_coefficient(), Some(1.0));
346 }
347
348 #[test]
349 fn evaluates_with_horner_method() {
350 let polynomial = Polynomial::new(vec![3.0, 2.0, 1.0]);
351
352 assert!((polynomial.evaluate(2.0) - 11.0).abs() <= f64::EPSILON);
353 assert!(Polynomial::zero().evaluate(3.0).abs() <= f64::EPSILON);
354 }
355
356 #[test]
357 fn derivative_handles_zero_constant_linear_and_quadratic() {
358 assert_eq!(Polynomial::zero().derivative(), Polynomial::zero());
359 assert_eq!(Polynomial::constant(5.0).derivative(), Polynomial::zero());
360 assert_eq!(
361 Polynomial::new(vec![3.0, 2.0]).derivative(),
362 Polynomial::constant(2.0)
363 );
364 assert_eq!(
365 Polynomial::new(vec![3.0, 2.0, 1.0]).derivative(),
366 Polynomial::new(vec![2.0, 2.0])
367 );
368 }
369
370 #[test]
371 fn nth_derivative_behaves_as_expected() {
372 let polynomial = Polynomial::new(vec![1.0, 2.0, 3.0, 4.0]);
373
374 assert_eq!(
375 polynomial.nth_derivative(0),
376 Polynomial::new(vec![1.0, 2.0, 3.0, 4.0])
377 );
378 assert_eq!(
379 polynomial.nth_derivative(1),
380 Polynomial::new(vec![2.0, 6.0, 12.0])
381 );
382 assert_eq!(
383 polynomial.nth_derivative(2),
384 Polynomial::new(vec![6.0, 24.0])
385 );
386 assert_eq!(polynomial.nth_derivative(4), Polynomial::zero());
387 }
388
389 #[test]
390 fn computes_integral_with_constant() {
391 let polynomial = Polynomial::new(vec![2.0, 6.0]);
392
393 assert_eq!(
394 polynomial.integral(5.0),
395 Polynomial::new(vec![5.0, 2.0, 3.0])
396 );
397 }
398
399 #[test]
400 fn adds_polynomials() {
401 let left = Polynomial::new(vec![1.0, 2.0, 3.0]);
402 let right = Polynomial::new(vec![4.0, -2.0]);
403
404 assert_eq!(left + right, Polynomial::new(vec![5.0, 0.0, 3.0]));
405 }
406
407 #[test]
408 fn subtracts_polynomials() {
409 let left = Polynomial::new(vec![5.0, 1.0]);
410 let right = Polynomial::new(vec![2.0, 3.0, 4.0]);
411
412 assert_eq!(left - right, Polynomial::new(vec![3.0, -2.0, -4.0]));
413 }
414
415 #[test]
416 fn multiplies_polynomials() {
417 let left = Polynomial::new(vec![1.0, 1.0]);
418 let right = Polynomial::new(vec![1.0, -1.0]);
419
420 assert_eq!(left * right, Polynomial::new(vec![1.0, 0.0, -1.0]));
421 }
422
423 #[test]
424 fn negates_polynomials() {
425 let polynomial = Polynomial::new(vec![1.0, -2.0, 3.0]);
426
427 assert_eq!(-polynomial, Polynomial::new(vec![-1.0, 2.0, -3.0]));
428 }
429
430 #[test]
431 fn multiplies_by_scalar() {
432 let polynomial = Polynomial::new(vec![1.0, -2.0, 3.0]);
433
434 assert_eq!(polynomial * 2.0, Polynomial::new(vec![2.0, -4.0, 6.0]));
435 assert_eq!(Polynomial::new(vec![1.0, 2.0]) * 0.0, Polynomial::zero());
436 }
437
438 #[test]
439 fn divides_by_scalar() {
440 let polynomial = Polynomial::new(vec![2.0, -4.0, 6.0]);
441
442 assert_eq!(polynomial / 2.0, Polynomial::new(vec![1.0, -2.0, 3.0]));
443 }
444
445 #[test]
446 fn solves_linear_roots() {
447 assert_eq!(linear_root(2.0, -4.0), Some(2.0));
448 assert_eq!(linear_root(0.0, 3.0), None);
449 }
450
451 #[test]
452 fn solves_quadratic_roots_with_two_real_roots() {
453 assert_eq!(quadratic_roots(1.0, -3.0, 2.0), vec![1.0, 2.0]);
454 }
455
456 #[test]
457 fn solves_quadratic_roots_with_one_repeated_root() {
458 assert_eq!(quadratic_roots(1.0, -2.0, 1.0), vec![1.0]);
459 }
460
461 #[test]
462 fn solves_quadratic_roots_with_no_real_roots() {
463 assert!(quadratic_roots(1.0, 0.0, 1.0).is_empty());
464 }
465
466 #[test]
467 fn quadratic_roots_fall_back_to_linear_case() {
468 assert_eq!(quadratic_roots(0.0, 2.0, -4.0), vec![2.0]);
469 assert!(quadratic_roots(0.0, 0.0, 1.0).is_empty());
470 }
471
472 #[test]
473 fn low_degree_real_root_helper_handles_supported_cases() {
474 assert_eq!(Polynomial::zero().real_roots_degree_1_or_2(), Some(vec![]));
475 assert_eq!(
476 Polynomial::constant(5.0).real_roots_degree_1_or_2(),
477 Some(vec![])
478 );
479 assert_eq!(
480 Polynomial::new(vec![-4.0, 2.0]).real_roots_degree_1_or_2(),
481 Some(vec![2.0])
482 );
483 assert_eq!(
484 Polynomial::new(vec![2.0, -3.0, 1.0]).real_roots_degree_1_or_2(),
485 Some(vec![1.0, 2.0])
486 );
487 assert_eq!(
488 Polynomial::new(vec![1.0, 0.0, 0.0, 1.0]).real_roots_degree_1_or_2(),
489 None
490 );
491 }
492}