Skip to main content

mathverse_core/
ops.rs

1//! Generic scalar operations over [`Real`].
2//!
3//! All functions in this module are generic over the [`Real`] trait and work
4//! with both `f32` and `f64`.
5
6use crate::constants::{DEG_TO_GRAD, GRAD_TO_DEG, GRAD_TO_RAD};
7use crate::traits::{FloatClass, Num, Real};
8use alloc::vec;
9use alloc::vec::Vec;
10use core::ops::Rem;
11
12/// Clamp `x` into `[lo, hi]`.
13///
14/// # Examples
15///
16/// ```
17/// use mathverse_core::ops::clamp;
18///
19/// assert_eq!(clamp(5.0, 0.0, 3.0), 3.0);
20/// assert_eq!(clamp(1.0, 0.0, 3.0), 1.0);
21/// ```
22#[must_use]
23#[inline]
24pub fn clamp<T: Real>(x: T, lo: T, hi: T) -> T {
25    x.max(lo).min(hi)
26}
27
28/// Linear interpolation between `a` and `b`.
29///
30/// `t = 0` returns `a`, `t = 1` returns `b`. Values of `t` outside `[0, 1]`
31/// extrapolate beyond the interval.
32///
33/// # Examples
34///
35/// ```
36/// use mathverse_core::ops::lerp;
37///
38/// assert_eq!(lerp(0.0, 10.0, 0.25), 2.5);
39/// assert_eq!(lerp(0.0, 10.0, 0.0), 0.0);
40/// assert_eq!(lerp(0.0, 10.0, 1.0), 10.0);
41/// ```
42#[must_use]
43#[inline]
44pub fn lerp<T: Real>(a: T, b: T, t: T) -> T {
45    a + (b - a) * t
46}
47
48/// Inverse linear interpolation: find `t` such that `lerp(a, b, t) == x`.
49///
50/// Returns `0` when `a == b`.
51///
52/// # Examples
53///
54/// ```
55/// use mathverse_core::ops::lerp_inv;
56///
57/// assert_eq!(lerp_inv(5.0, 0.0, 10.0), 0.5);
58/// assert_eq!(lerp_inv(0.0, 0.0, 10.0), 0.0);
59/// ```
60#[must_use]
61#[inline]
62pub fn lerp_inv<T: Real>(x: T, a: T, b: T) -> T {
63    let d = b - a;
64    if d == T::zero() {
65        T::zero()
66    } else {
67        (x - a) / d
68    }
69}
70
71/// Smoothstep: C1 interpolation with zero derivatives at both ends.
72///
73/// `x` assumed within `[0, 1]`. Formula: `3t^2 - 2t^3`.
74///
75/// # Examples
76///
77/// ```
78/// use mathverse_core::ops::smoothstep;
79///
80/// assert_eq!(smoothstep(0.0), 0.0);
81/// assert_eq!(smoothstep(0.5), 0.5);
82/// assert_eq!(smoothstep(1.0), 1.0);
83/// ```
84#[must_use]
85#[inline]
86pub fn smoothstep<T: Real>(x: T) -> T {
87    x * x * (T::from_f64(3.0) - T::from_f64(2.0) * x)
88}
89
90/// Smootherstep: C2 interpolation with zero derivatives at both ends.
91///
92/// `t` assumed within `[0, 1]`. Formula: `6t^5 - 15t^4 + 10t^3`.
93///
94/// # Examples
95///
96/// ```
97/// use mathverse_core::ops::smootherstep;
98///
99/// assert_eq!(smootherstep(0.0), 0.0);
100/// assert_eq!(smootherstep(0.5), 0.5);
101/// assert_eq!(smootherstep(1.0), 1.0);
102/// ```
103#[must_use]
104#[inline]
105pub fn smootherstep<T: Real>(x: T) -> T {
106    let x3 = x * x * x;
107    x3 * (x * x * T::from_f64(6.0) - x * T::from_f64(15.0) + T::from_f64(10.0))
108}
109
110/// Fractional part: `x - floor(x)`. Negative inputs give `[0, 1)`.
111///
112/// # Examples
113///
114/// ```
115/// use mathverse_core::ops::fract;
116///
117/// assert!((fract(2.75_f64) - 0.75).abs() < 1e-12);
118/// assert!((fract(-0.25_f64) - 0.75).abs() < 1e-12);
119/// ```
120#[must_use]
121#[inline]
122pub fn fract<T: Real>(x: T) -> T {
123    x - x.floor()
124}
125
126/// Integer part: `floor(x)`.
127///
128/// # Examples
129///
130/// ```
131/// use mathverse_core::ops::integer_part;
132///
133/// assert_eq!(integer_part(2.7), 2.0);
134/// assert_eq!(integer_part(-2.7), -3.0);
135/// ```
136#[must_use]
137#[inline]
138pub fn integer_part<T: Real>(x: T) -> T {
139    x.floor()
140}
141
142/// `n`-th root of `x` (any real `n`).
143///
144/// # Examples
145///
146/// ```
147/// use mathverse_core::ops::nth_root;
148///
149/// assert!((nth_root(27.0_f64, 3) - 3.0).abs() < 1e-12);
150/// assert!((nth_root(16.0_f64, 4) - 2.0).abs() < 1e-12);
151/// ```
152#[must_use]
153#[inline]
154#[allow(clippy::cast_lossless)] // From<i32> for f64 is unavailable in no_std
155pub fn nth_root<T: Real>(x: T, n: i32) -> T {
156    if n == 0 {
157        return T::one();
158    }
159    x.powf(T::from_f64(n as f64).recip())
160}
161
162/// Cube root of `x`.
163///
164/// # Examples
165///
166/// ```
167/// use mathverse_core::ops::cbrt;
168///
169/// assert!((cbrt(27.0_f64) - 3.0).abs() < 1e-12);
170/// assert!((cbrt(8.0_f64) - 2.0).abs() < 1e-12);
171/// ```
172#[must_use]
173#[inline]
174pub fn cbrt<T: Real>(x: T) -> T {
175    if x >= T::zero() {
176        x.powf(T::from_f64(1.0 / 3.0))
177    } else {
178        -(-x).powf(T::from_f64(1.0 / 3.0))
179    }
180}
181
182/// Overflow-safe `sqrt(a^2 + b^2)`, generic over reals.
183///
184/// # Examples
185///
186/// ```
187/// use mathverse_core::ops::hypot2;
188///
189/// assert_eq!(hypot2(3.0, 4.0), 5.0);
190/// assert_eq!(hypot2(0.0, 0.0), 0.0);
191/// ```
192#[must_use]
193pub fn hypot2<T: Real>(a: T, b: T) -> T {
194    let (a, b) = (a.abs(), b.abs());
195    let m = a.max(b);
196    if m == T::zero() {
197        T::zero()
198    } else {
199        m * (T::one() + (a.min(b) / m).powi(2)).sqrt()
200    }
201}
202
203/// Overflow-safe `sqrt(a^2 + b^2 + c^2)`, generic over reals.
204///
205/// # Examples
206///
207/// ```
208/// use mathverse_core::ops::hypot3;
209///
210/// assert_eq!(hypot3(3.0, 4.0, 12.0), 13.0);
211/// ```
212#[must_use]
213pub fn hypot3<T: Real>(a: T, b: T, c: T) -> T {
214    let m = a.abs().max(b.abs()).max(c.abs());
215    if m == T::zero() {
216        T::zero()
217    } else {
218        let (a, b, c) = (a / m, b / m, c / m);
219        m * (a * a + b * b + c * c).sqrt()
220    }
221}
222
223/// Sum of a slice. Returns `T::zero()` for an empty slice.
224///
225/// # Examples
226///
227/// ```
228/// use mathverse_core::ops::sum;
229///
230/// assert_eq!(sum(&[1, 2, 3, 4]), 10);
231/// assert_eq!(sum::<i32>(&[]), 0);
232/// ```
233#[must_use]
234#[inline]
235pub fn sum<T: Num>(xs: &[T]) -> T {
236    xs.iter().copied().fold(T::zero(), |acc, x| acc + x)
237}
238
239/// Product of a slice. Returns `T::one()` for an empty slice.
240///
241/// # Examples
242///
243/// ```
244/// use mathverse_core::ops::product;
245///
246/// assert_eq!(product(&[1, 2, 3, 4]), 24);
247/// assert_eq!(product::<f64>(&[]), 1.0);
248/// ```
249#[must_use]
250#[inline]
251pub fn product<T: Num>(xs: &[T]) -> T {
252    xs.iter().copied().fold(T::one(), |acc, x| acc * x)
253}
254
255/// Convert degrees to radians.
256///
257/// # Examples
258///
259/// ```
260/// use mathverse_core::ops::deg_to_rad;
261///
262/// let rad = deg_to_rad(180.0_f64);
263/// assert!((rad - core::f64::consts::PI).abs() < 1e-12);
264/// ```
265#[must_use]
266#[inline]
267pub fn deg_to_rad<T: Real>(d: T) -> T {
268    d * T::from_f64(crate::constants::DEG_TO_RAD)
269}
270
271/// Convert radians to degrees.
272///
273/// # Examples
274///
275/// ```
276/// use mathverse_core::ops::rad_to_deg;
277///
278/// let deg = rad_to_deg(core::f64::consts::PI);
279/// assert!((deg - 180.0).abs() < 1e-12);
280/// ```
281#[must_use]
282#[inline]
283pub fn rad_to_deg<T: Real>(r: T) -> T {
284    r * T::from_f64(crate::constants::RAD_TO_DEG)
285}
286
287/// Convert gradians to degrees (1 gradian = 0.9 degrees).
288///
289/// # Examples
290///
291/// ```
292/// use mathverse_core::ops::grad_to_deg;
293///
294/// assert_eq!(grad_to_deg(100.0), 90.0);
295/// ```
296#[must_use]
297#[inline]
298pub fn grad_to_deg<T: Real>(g: T) -> T {
299    g * T::from_f64(GRAD_TO_DEG)
300}
301
302/// Convert degrees to gradians.
303///
304/// # Examples
305///
306/// ```
307/// use mathverse_core::ops::deg_to_grad;
308///
309/// assert_eq!(deg_to_grad(90.0), 100.0);
310/// ```
311#[must_use]
312#[inline]
313pub fn deg_to_grad<T: Real>(d: T) -> T {
314    d * T::from_f64(DEG_TO_GRAD)
315}
316
317/// Convert gradians to radians.
318///
319/// # Examples
320///
321/// ```
322/// use mathverse_core::ops::grad_to_rad;
323///
324/// let rad = grad_to_rad(200.0);
325/// assert!((rad - core::f64::consts::PI).abs() < 1e-15);
326/// ```
327#[must_use]
328#[inline]
329pub fn grad_to_rad<T: Real>(g: T) -> T {
330    g * T::from_f64(GRAD_TO_RAD)
331}
332
333/// Convert radians to gradians.
334///
335/// # Examples
336///
337/// ```
338/// use mathverse_core::ops::rad_to_grad;
339///
340/// let grad = rad_to_grad(core::f64::consts::PI);
341/// assert!((grad - 200.0).abs() < 1e-10);
342/// ```
343#[must_use]
344#[inline]
345pub fn rad_to_grad<T: Real>(r: T) -> T {
346    r * T::from_f64(200.0 / core::f64::consts::PI)
347}
348
349/// Copy the sign of `b` to the magnitude of `a`.
350///
351/// # Examples
352///
353/// ```
354/// use mathverse_core::ops::copysign;
355///
356/// assert_eq!(copysign(3.0, -1.0), -3.0);
357/// assert_eq!(copysign(3.0, 1.0), 3.0);
358/// ```
359#[must_use]
360#[inline]
361pub fn copysign<T: Real>(a: T, b: T) -> T {
362    if b.is_negative() {
363        -a.abs()
364    } else {
365        a.abs()
366    }
367}
368
369/// Saturating subtraction: `max(0, a - b)`.
370///
371/// # Examples
372///
373/// ```
374/// use mathverse_core::ops::abs_sub;
375///
376/// assert_eq!(abs_sub(5.0, 3.0), 2.0);
377/// assert_eq!(abs_sub(3.0, 5.0), 0.0);
378/// ```
379#[must_use]
380#[inline]
381pub fn abs_sub<T: Real>(a: T, b: T) -> T {
382    (a - b).max(T::zero())
383}
384
385/// Map a value from one range to another.
386///
387/// `map_range(x, 0, 10, 0, 100)` maps 5 to 50.
388///
389/// # Examples
390///
391/// ```
392/// use mathverse_core::ops::map_range;
393///
394/// assert_eq!(map_range(5.0, 0.0, 10.0, 0.0, 100.0), 50.0);
395/// assert_eq!(map_range(0.0, 0.0, 10.0, 0.0, 100.0), 0.0);
396/// ```
397#[must_use]
398pub fn map_range<T: Real>(x: T, in_lo: T, in_hi: T, out_lo: T, out_hi: T) -> T {
399    let t = lerp_inv(x, in_lo, in_hi);
400    lerp(out_lo, out_hi, t)
401}
402
403/// Wrap `x` into the half-open interval `[lo, hi)`.
404///
405/// # Examples
406///
407/// ```
408/// use mathverse_core::ops::wrap;
409///
410/// assert_eq!(wrap(15.0, 0.0, 10.0), 5.0);
411/// assert_eq!(wrap(-3.0, 0.0, 10.0), 7.0);
412/// ```
413#[must_use]
414pub fn wrap<T: Real + FloatClass + Rem<Output = T>>(x: T, lo: T, hi: T) -> T {
415    if !x.is_finite() {
416        return x;
417    }
418    let range = hi - lo;
419    if range == T::zero() {
420        return lo;
421    }
422    let v = x - lo;
423    let r = v % range;
424    if r < T::zero() {
425        r + hi
426    } else {
427        r + lo
428    }
429}
430
431/// Ping-pong: triangle wave between 0 and `length`.
432///
433/// # Examples
434///
435/// ```
436/// use mathverse_core::ops::ping_pong;
437///
438/// assert_eq!(ping_pong(0.0, 10.0), 0.0);
439/// assert_eq!(ping_pong(7.5, 10.0), 7.5);
440/// assert_eq!(ping_pong(12.5, 10.0), 7.5);
441/// assert_eq!(ping_pong(20.0, 10.0), 0.0);
442/// ```
443#[must_use]
444#[inline]
445pub fn ping_pong<T: Real + FloatClass>(x: T, length: T) -> T {
446    let t = wrap(x, T::zero(), length * T::from_f64(2.0));
447    length - (t - length).abs()
448}
449
450/// Repeat `x` into `[0, length)`.
451///
452/// # Examples
453///
454/// ```
455/// use mathverse_core::ops::repeat;
456///
457/// assert_eq!(repeat(15.0, 10.0), 5.0);
458/// assert_eq!(repeat(3.0, 10.0), 3.0);
459/// ```
460#[must_use]
461#[inline]
462pub fn repeat<T: Real + FloatClass>(x: T, length: T) -> T {
463    wrap(x, T::zero(), length)
464}
465
466/// Normalize an angle in radians to `[-pi, pi]`.
467///
468/// # Examples
469///
470/// ```
471/// use mathverse_core::ops::wrap_angle;
472///
473/// let a = wrap_angle(core::f64::consts::PI + 0.1);
474/// assert!((a - (-core::f64::consts::PI + 0.1)).abs() < 1e-15);
475/// ```
476#[must_use]
477pub fn wrap_angle<T: Real + Rem<Output = T>>(x: T) -> T {
478    let pi = T::from_f64(core::f64::consts::PI);
479    let two_pi = T::from_f64(core::f64::consts::TAU);
480    let v = x % two_pi;
481    if v > pi {
482        v - two_pi
483    } else if v < -pi {
484        v + two_pi
485    } else {
486        v
487    }
488}
489
490/// Wrap angle in radians to `[0, 2pi)`.
491///
492/// # Examples
493///
494/// ```
495/// use mathverse_core::ops::wrap_angle_positive;
496///
497/// let a = wrap_angle_positive(-0.1);
498/// assert!((a - (2.0 * core::f64::consts::PI - 0.1)).abs() < 1e-15);
499/// ```
500#[must_use]
501pub fn wrap_angle_positive<T: Real + Rem<Output = T>>(x: T) -> T {
502    let two_pi = T::from_f64(core::f64::consts::TAU);
503    let v = x % two_pi;
504    if v < T::zero() {
505        v + two_pi
506    } else {
507        v
508    }
509}
510
511/// Distance between two values: `|a - b|`.
512///
513/// # Examples
514///
515/// ```
516/// use mathverse_core::ops::distance;
517///
518/// assert_eq!(distance(3.0, 7.0), 4.0);
519/// assert_eq!(distance(5.0, 5.0), 0.0);
520/// ```
521#[must_use]
522#[inline]
523pub fn distance<T: Real>(a: T, b: T) -> T {
524    (a - b).abs()
525}
526
527/// Normalize a vector (represented as a slice) to unit length.
528///
529/// Returns a zero vector if the norm is zero.
530///
531/// # Examples
532///
533/// ```
534/// use mathverse_core::ops::normalize;
535///
536/// let v = normalize(&[3.0_f64, 4.0]);
537/// assert!((v[0] - 0.6).abs() < 1e-12);
538/// assert!((v[1] - 0.8).abs() < 1e-12);
539/// ```
540#[must_use]
541pub fn normalize<T: Real>(xs: &[T]) -> Vec<T> {
542    let norm_sq: T = xs.iter().copied().fold(T::zero(), |acc, x| acc + x * x);
543    let norm = norm_sq.sqrt();
544    if norm == T::zero() {
545        return vec![T::zero(); xs.len()];
546    }
547    xs.iter().map(|&x| x / norm).collect()
548}
549
550/// Sign as integer: -1, 0, or 1.
551///
552/// # Examples
553///
554/// ```
555/// use mathverse_core::ops::sign;
556///
557/// assert_eq!(sign(5.0), 1);
558/// assert_eq!(sign(-5.0), -1);
559/// assert_eq!(sign(0.0), 0);
560/// ```
561#[must_use]
562#[inline]
563pub fn sign<T: Real>(x: T) -> i32 {
564    if x > T::zero() {
565        1
566    } else if x < T::zero() {
567        -1
568    } else {
569        0
570    }
571}
572
573/// Truncate toward zero.
574///
575/// # Examples
576///
577/// ```
578/// use mathverse_core::ops::trunc;
579///
580/// assert_eq!(trunc(2.7), 2.0);
581/// assert_eq!(trunc(-2.7), -2.0);
582/// ```
583#[must_use]
584#[inline]
585pub fn trunc<T: Real>(x: T) -> T {
586    if x >= T::zero() {
587        x.floor()
588    } else {
589        x.ceil()
590    }
591}
592
593/// Fractional part toward zero: `x - trunc(x)`.
594///
595/// # Examples
596///
597/// ```
598/// use mathverse_core::ops::frac;
599///
600/// assert!((frac(2.7_f64) - 0.7).abs() < 1e-12);
601/// assert!((frac(-2.7_f64) - (-0.7)).abs() < 1e-12);
602/// ```
603#[must_use]
604#[inline]
605pub fn frac<T: Real>(x: T) -> T {
606    x - trunc(x)
607}
608
609/// Heaviside step function: `0` if `x < edge`, `1` if `x >= edge`.
610///
611/// # Examples
612///
613/// ```
614/// use mathverse_core::ops::step;
615///
616/// assert_eq!(step(0.0, -1.0), 0.0);
617/// assert_eq!(step(0.0, 0.0), 1.0);
618/// assert_eq!(step(0.0, 1.0), 1.0);
619/// ```
620#[must_use]
621#[inline]
622pub fn step<T: Real>(edge: T, x: T) -> T {
623    if x < edge {
624        T::zero()
625    } else {
626        T::one()
627    }
628}
629
630/// Clamp `x` into `[0, 1]`.
631///
632/// # Examples
633///
634/// ```
635/// use mathverse_core::ops::clamp01;
636///
637/// assert_eq!(clamp01(0.5), 0.5);
638/// assert_eq!(clamp01(-1.0), 0.0);
639/// assert_eq!(clamp01(2.0), 1.0);
640/// ```
641#[must_use]
642#[inline]
643pub fn clamp01<T: Real>(x: T) -> T {
644    clamp(x, T::zero(), T::one())
645}
646
647/// Clamp `x` into `[-1, 1]`.
648///
649/// # Examples
650///
651/// ```
652/// use mathverse_core::ops::clamp11;
653///
654/// assert_eq!(clamp11(0.5), 0.5);
655/// assert_eq!(clamp11(-2.0), -1.0);
656/// assert_eq!(clamp11(3.0), 1.0);
657/// ```
658#[must_use]
659#[inline]
660pub fn clamp11<T: Real>(x: T) -> T {
661    clamp(x, T::zero() - T::one(), T::one())
662}
663
664/// Snap `x` to the nearest multiple of `step`.
665///
666/// # Examples
667///
668/// ```
669/// use mathverse_core::ops::snap;
670///
671/// assert_eq!(snap(7.0, 5.0), 5.0);
672/// assert_eq!(snap(8.0, 5.0), 10.0);
673/// assert_eq!(snap(0.0, 3.0), 0.0);
674/// ```
675#[must_use]
676pub fn snap<T: Real>(x: T, step_size: T) -> T {
677    if step_size == T::zero() {
678        return x;
679    }
680    (x / step_size).round() * step_size
681}
682
683/// Overflow-safe midpoint: `(a + b) / 2` without intermediate overflow.
684///
685/// # Examples
686///
687/// ```
688/// use mathverse_core::ops::midpoint;
689///
690/// assert_eq!(midpoint(0.0, 10.0), 5.0);
691/// assert_eq!(midpoint(-3.0, 3.0), 0.0);
692/// assert_eq!(midpoint(1e200, 1e200), 1e200);
693/// ```
694#[must_use]
695#[inline]
696pub fn midpoint<T: Real>(a: T, b: T) -> T {
697    let two = T::one() + T::one();
698    a + (b - a) / two
699}
700
701/// Shortest-path angular interpolation between `a` and `b` in radians.
702///
703/// `t = 0` returns `a`, `t = 1` returns `b`. Takes the shorter arc.
704///
705/// # Examples
706///
707/// ```
708/// use mathverse_core::ops::lerp_angle;
709///
710/// let result = lerp_angle(0.0, core::f64::consts::PI, 0.5);
711/// assert!((result - core::f64::consts::FRAC_PI_2).abs() < 1e-12);
712/// ```
713#[must_use]
714pub fn lerp_angle<T: Real + Rem<Output = T>>(a: T, b: T, t: T) -> T {
715    let pi = T::from_f64(core::f64::consts::PI);
716    let two_pi = T::from_f64(core::f64::consts::TAU);
717    let a = wrap_angle(a);
718    let b = wrap_angle(b);
719    let mut diff = (b - a) % two_pi;
720    if diff > pi {
721        diff = diff - two_pi;
722    } else if diff < -pi {
723        diff = diff + two_pi;
724    }
725    a + diff * t
726}
727
728/// Inverse linear interpolation: find `t` such that `lerp(a, b, t) == x`.
729///
730/// Alias for [`lerp_inv`].
731///
732/// # Examples
733///
734/// ```
735/// use mathverse_core::ops::inv_lerp;
736///
737/// assert_eq!(inv_lerp(5.0, 0.0, 10.0), 0.5);
738/// assert_eq!(inv_lerp(0.0, 0.0, 10.0), 0.0);
739/// ```
740#[must_use]
741#[inline]
742pub fn inv_lerp<T: Real>(x: T, a: T, b: T) -> T {
743    lerp_inv(x, a, b)
744}
745
746/// Remap `x` from range `[in_lo, in_hi]` to `[out_lo, out_hi]`.
747///
748/// Alias for [`map_range`] with clearer naming.
749///
750/// # Examples
751///
752/// ```
753/// use mathverse_core::ops::remap;
754///
755/// assert_eq!(remap(5.0, 0.0, 10.0, 0.0, 100.0), 50.0);
756/// assert_eq!(remap(0.0, 0.0, 10.0, 0.0, 100.0), 0.0);
757/// ```
758#[must_use]
759#[inline]
760pub fn remap<T: Real>(x: T, in_lo: T, in_hi: T, out_lo: T, out_hi: T) -> T {
761    map_range(x, in_lo, in_hi, out_lo, out_hi)
762}
763
764/// Returns `true` if `x` is within `[lo, hi]` (inclusive).
765///
766/// # Examples
767///
768/// ```
769/// use mathverse_core::ops::is_between;
770///
771/// assert!(is_between(5.0, 0.0, 10.0));
772/// assert!(!is_between(15.0, 0.0, 10.0));
773/// assert!(is_between(0.0, 0.0, 10.0));
774/// assert!(is_between(10.0, 0.0, 10.0));
775/// ```
776#[must_use]
777#[inline]
778pub fn is_between<T: Copy + PartialOrd>(x: T, lo: T, hi: T) -> bool {
779    x >= lo && x <= hi
780}
781
782/// Linear interpolation with `t` clamped to `[0, 1]`.
783///
784/// # Examples
785///
786/// ```
787/// use mathverse_core::ops::lerp_clamped;
788///
789/// assert_eq!(lerp_clamped(0.0, 10.0, 0.25), 2.5);
790/// assert_eq!(lerp_clamped(0.0, 10.0, -1.0), 0.0);
791/// assert_eq!(lerp_clamped(0.0, 10.0, 2.0), 10.0);
792/// ```
793#[must_use]
794#[inline]
795pub fn lerp_clamped<T: Real>(a: T, b: T, t: T) -> T {
796    lerp(a, b, clamp01(t))
797}
798
799/// Remap `x` from `[in_lo, in_hi]` to `[out_lo, out_hi]` with output clamped.
800///
801/// # Examples
802///
803/// ```
804/// use mathverse_core::ops::remap_clamped;
805///
806/// assert_eq!(remap_clamped(5.0, 0.0, 10.0, 0.0, 100.0), 50.0);
807/// assert_eq!(remap_clamped(-5.0, 0.0, 10.0, 0.0, 100.0), 0.0);
808/// assert_eq!(remap_clamped(15.0, 0.0, 10.0, 0.0, 100.0), 100.0);
809/// ```
810#[must_use]
811#[inline]
812pub fn remap_clamped<T: Real>(x: T, in_lo: T, in_hi: T, out_lo: T, out_hi: T) -> T {
813    lerp_clamped(out_lo, out_hi, lerp_inv(x, in_lo, in_hi))
814}
815
816/// Bilinear interpolation between four corner values.
817///
818/// `v00` at `(0,0)`, `v01` at `(0,1)`, `v10` at `(1,0)`, `v11` at `(1,1)`.
819/// `x` and `y` are assumed within `[0, 1]`.
820///
821/// # Examples
822///
823/// ```
824/// use mathverse_core::ops::bilinear;
825///
826/// assert_eq!(bilinear(0.0, 0.0, 0.0, 0.0, 0.0, 10.0), 0.0);
827/// assert_eq!(bilinear(1.0, 1.0, 0.0, 0.0, 0.0, 10.0), 10.0);
828/// assert_eq!(bilinear(0.5, 0.5, 0.0, 10.0, 10.0, 0.0), 5.0);
829/// ```
830#[must_use]
831pub fn bilinear<T: Real>(x: T, y: T, v00: T, v01: T, v10: T, v11: T) -> T {
832    let top = lerp(v00, v10, x);
833    let bottom = lerp(v01, v11, x);
834    lerp(top, bottom, y)
835}
836
837/// Check if a slice is sorted in non-decreasing order.
838///
839/// # Examples
840///
841/// ```
842/// use mathverse_core::ops::is_sorted;
843///
844/// assert!(is_sorted(&[1, 2, 3, 4]));
845/// assert!(is_sorted(&[1, 1, 2, 3]));
846/// assert!(!is_sorted(&[1, 3, 2, 4]));
847/// assert!(is_sorted::<i32>(&[]));
848/// assert!(is_sorted(&[42]));
849/// ```
850#[must_use]
851#[inline]
852pub fn is_sorted<T: PartialOrd>(xs: &[T]) -> bool {
853    xs.windows(2).all(|w| w[0] <= w[1])
854}
855
856/// Minimum value in a slice. Returns `None` for an empty slice.
857///
858/// # Examples
859///
860/// ```
861/// use mathverse_core::ops::min_value;
862///
863/// assert_eq!(min_value(&[3, 1, 4, 1, 5]), Some(&1));
864/// assert_eq!(min_value::<i32>(&[]), None);
865/// ```
866#[must_use]
867#[inline]
868pub fn min_value<T: PartialOrd>(xs: &[T]) -> Option<&T> {
869    xs.iter().min_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal))
870}
871
872/// Maximum value in a slice. Returns `None` for an empty slice.
873///
874/// # Examples
875///
876/// ```
877/// use mathverse_core::ops::max_value;
878///
879/// assert_eq!(max_value(&[3, 1, 4, 1, 5]), Some(&5));
880/// assert_eq!(max_value::<i32>(&[]), None);
881/// ```
882#[must_use]
883#[inline]
884pub fn max_value<T: PartialOrd>(xs: &[T]) -> Option<&T> {
885    xs.iter().max_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal))
886}
887
888/// Mean (average) of a slice. Returns `None` for an empty slice.
889///
890/// # Examples
891///
892/// ```
893/// use mathverse_core::ops::mean;
894///
895/// assert_eq!(mean(&[1.0, 2.0, 3.0, 4.0]), Some(2.5));
896/// assert_eq!(mean::<f64>(&[]), None);
897/// ```
898#[must_use]
899pub fn mean<T: Real>(xs: &[T]) -> Option<T> {
900    if xs.is_empty() {
901        return None;
902    }
903    let sum: T = xs.iter().copied().fold(T::zero(), |acc, x| acc + x);
904    Some(sum / T::from_f64(xs.len() as f64))
905}
906
907/// Cumulative sum of a slice. Returns an empty vector for an empty input.
908///
909/// # Examples
910///
911/// ```
912/// use mathverse_core::ops::cumsum;
913///
914/// assert_eq!(cumsum(&[1, 2, 3, 4]), vec![1, 3, 6, 10]);
915/// assert_eq!(cumsum::<i32>(&[]), vec![]);
916/// ```
917#[must_use]
918pub fn cumsum<T: Num + Copy + core::ops::Add<Output = T>>(xs: &[T]) -> Vec<T> {
919    let mut result = Vec::with_capacity(xs.len());
920    let mut acc = T::zero();
921    for &x in xs {
922        acc = acc + x;
923        result.push(acc);
924    }
925    result
926}
927
928/// Cumulative product of a slice. Returns an empty vector for an empty input.
929///
930/// # Examples
931///
932/// ```
933/// use mathverse_core::ops::cumprod;
934///
935/// assert_eq!(cumprod(&[1, 2, 3, 4]), vec![1, 2, 6, 24]);
936/// assert_eq!(cumprod::<i32>(&[]), vec![]);
937/// ```
938#[must_use]
939pub fn cumprod<T: Num + Copy + core::ops::Mul<Output = T>>(xs: &[T]) -> Vec<T> {
940    let mut result = Vec::with_capacity(xs.len());
941    let mut acc = T::one();
942    for &x in xs {
943        acc = acc * x;
944        result.push(acc);
945    }
946    result
947}
948
949/// Dot product of two slices. Returns `T::zero()` if either is empty.
950/// Truncates to the shorter length if they differ.
951///
952/// # Examples
953///
954/// ```
955/// use mathverse_core::ops::dot_product;
956///
957/// assert_eq!(dot_product(&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]), 32.0);
958/// assert_eq!(dot_product(&[2, 3], &[4, 5]), 23);
959/// ```
960#[must_use]
961#[inline]
962pub fn dot_product<T: Num + Copy + core::ops::Add<Output = T> + core::ops::Mul<Output = T>>(
963    a: &[T],
964    b: &[T],
965) -> T {
966    a.iter().zip(b.iter()).map(|(&x, &y)| x * y).fold(T::zero(), |acc, x| acc + x)
967}
968
969/// Parameterized smoothstep: C1 interpolation between `edge0` and `edge1`.
970///
971/// Returns `0` when `x <= edge0`, `1` when `x >= edge1`, and a smooth
972/// Hermite interpolation between them otherwise.
973/// Formula: `t = clamp((x - edge0) / (edge1 - edge0), 0, 1)`,
974/// result: `t^2 * (3 - 2t)`.
975///
976/// # Examples
977///
978/// ```
979/// use mathverse_core::ops::smoothstep_between;
980///
981/// assert_eq!(smoothstep_between(0.0, 1.0, 0.0), 0.0);
982/// assert_eq!(smoothstep_between(0.0, 1.0, 0.5), 0.5);
983/// assert_eq!(smoothstep_between(0.0, 1.0, 1.0), 1.0);
984/// assert_eq!(smoothstep_between(2.0, 5.0, 3.5), 0.5);
985/// assert_eq!(smoothstep_between(0.0, 1.0, -1.0), 0.0);
986/// assert_eq!(smoothstep_between(0.0, 1.0, 2.0), 1.0);
987/// ```
988#[must_use]
989pub fn smoothstep_between<T: Real>(edge0: T, edge1: T, x: T) -> T {
990    if edge0 == edge1 {
991        return if x >= edge0 { T::one() } else { T::zero() };
992    }
993    let t = clamp((x - edge0) / (edge1 - edge0), T::zero(), T::one());
994    t * t * (T::from_f64(3.0) - T::from_f64(2.0) * t)
995}
996
997#[cfg(test)]
998#[allow(clippy::suboptimal_flops)]
999mod tests {
1000    use super::*;
1001
1002    #[test]
1003    fn basic_ops() {
1004        assert_eq!(clamp(5.0, 0.0, 3.0), 3.0);
1005        assert_eq!(lerp(0.0, 10.0, 0.25), 2.5);
1006        assert_eq!(smoothstep(0.5), 0.5);
1007        assert_eq!(fract(2.75), 0.75);
1008        assert_eq!(fract(-0.25), 0.75);
1009        assert_eq!(nth_root(27.0, 3), 3.0);
1010    }
1011
1012    #[test]
1013    fn hypot_no_overflow() {
1014        let big = 1e150f64;
1015        assert_eq!(hypot2(big, big), big * 2.0f64.sqrt());
1016        assert_eq!(hypot2(0.0, 0.0), 0.0);
1017        assert_eq!(hypot3(3.0, 4.0, 12.0), 13.0);
1018    }
1019
1020    #[test]
1021    fn aggregates() {
1022        assert_eq!(sum(&[1, 2, 3, 4]), 10);
1023        assert_eq!(sum::<i32>(&[]), 0);
1024        assert_eq!(product(&[1, 2, 3, 4]), 24);
1025        assert_eq!(product::<f64>(&[]), 1.0);
1026    }
1027
1028    #[test]
1029    fn angle_conversions() {
1030        assert!((rad_to_deg(deg_to_rad(45.0f64)) - 45.0).abs() < 1e-12);
1031        assert_eq!(grad_to_deg(100.0), 90.0);
1032        assert_eq!(deg_to_grad(90.0), 100.0);
1033        assert!((rad_to_grad(3.14159265358979f64) - 200.0f64).abs() < 1e-10);
1034        assert!((grad_to_rad(200.0) - core::f64::consts::PI).abs() < 1e-15);
1035    }
1036
1037    #[test]
1038    fn new_ops() {
1039        assert_eq!(cbrt(27.0), 3.0);
1040        assert_eq!(copysign(3.0, -1.0), -3.0);
1041        assert_eq!(abs_sub(5.0, 3.0), 2.0);
1042        assert_eq!(abs_sub(3.0, 5.0), 0.0);
1043        assert_eq!(map_range(5.0, 0.0, 10.0, 0.0, 100.0), 50.0);
1044        assert_eq!(lerp_inv(5.0, 0.0, 10.0), 0.5);
1045        assert_eq!(smootherstep(0.5), 0.5);
1046        assert_eq!(sign(5.0), 1);
1047        assert_eq!(sign(-5.0), -1);
1048        assert_eq!(sign(0.0), 0);
1049        assert_eq!(trunc(2.7), 2.0);
1050        assert_eq!(trunc(-2.7), -2.0);
1051        assert!((frac(2.7f64) - 0.7f64).abs() < 1e-12);
1052        assert!((frac(-2.7f64) - (-0.7f64)).abs() < 1e-12);
1053        assert_eq!(distance(3.0, 7.0), 4.0);
1054    }
1055
1056#[test]
1057fn wrap_and_angle() {
1058    assert_eq!(wrap(15.0, 0.0, 10.0), 5.0);
1059    assert_eq!(wrap(-3.0, 0.0, 10.0), 7.0);
1060    assert_eq!(repeat(15.0, 10.0), 5.0);
1061    assert_eq!(ping_pong(0.0, 10.0), 0.0);
1062    assert_eq!(ping_pong(7.5, 10.0), 7.5);
1063    assert_eq!(ping_pong(12.5, 10.0), 7.5);
1064    assert_eq!(ping_pong(20.0, 10.0), 0.0);
1065    assert!((wrap_angle(core::f64::consts::PI + 0.1) - (-core::f64::consts::PI + 0.1)).abs() < 1e-15);
1066    assert!((wrap_angle_positive(-0.1) - (2.0 * core::f64::consts::PI - 0.1)).abs() < 1e-15);
1067    // NaN/Inf passthrough
1068    assert!(wrap(f64::NAN, 0.0, 1.0).is_nan());
1069    assert!(wrap(f64::INFINITY, 0.0, 1.0).is_infinite());
1070    assert!(wrap(f64::NEG_INFINITY, 0.0, 1.0).is_infinite());
1071}
1072
1073    #[test]
1074    fn lerp_angle_normalization() {
1075        // Both inputs outside [-pi, pi] should still take shortest path
1076        let a = core::f64::consts::PI * 3.0;  // equivalent to pi
1077        let b = core::f64::consts::PI * 5.0;  // equivalent to pi
1078        let result = lerp_angle(a, b, 0.5);
1079        // Both normalize to ~pi, so midpoint should be ~pi
1080        assert!((result - core::f64::consts::PI).abs() < 1e-10);
1081    }
1082
1083    #[test]
1084    fn normalize_vec() {
1085        let v = normalize(&[3.0, 4.0]);
1086        assert!((v[0] - 0.6f64).abs() < 1e-12);
1087        assert!((v[1] - 0.8f64).abs() < 1e-12);
1088        let zero = normalize(&[0.0, 0.0]);
1089        assert_eq!(zero, vec![0.0, 0.0]);
1090    }
1091
1092    #[test]
1093    fn new_ops_extra() {
1094        assert_eq!(step(0.0, -1.0), 0.0);
1095        assert_eq!(step(0.0, 0.0), 1.0);
1096        assert_eq!(step(0.0, 1.0), 1.0);
1097        assert_eq!(clamp01(0.5), 0.5);
1098        assert_eq!(clamp01(-1.0), 0.0);
1099        assert_eq!(clamp01(2.0), 1.0);
1100        assert_eq!(clamp11(0.5), 0.5);
1101        assert_eq!(clamp11(-2.0), -1.0);
1102        assert_eq!(clamp11(3.0), 1.0);
1103        assert_eq!(snap(7.0, 5.0), 5.0);
1104        assert_eq!(snap(8.0, 5.0), 10.0);
1105        assert_eq!(snap(0.0, 3.0), 0.0);
1106        assert_eq!(midpoint(0.0, 10.0), 5.0);
1107        assert_eq!(midpoint(-3.0, 3.0), 0.0);
1108        assert_eq!(midpoint(1e200, 1e200), 1e200);
1109        let result = lerp_angle(0.0, core::f64::consts::PI, 0.5);
1110        assert!((result - core::f64::consts::FRAC_PI_2).abs() < 1e-12);
1111        assert_eq!(inv_lerp(5.0, 0.0, 10.0), 0.5);
1112        assert_eq!(remap(5.0, 0.0, 10.0, 0.0, 100.0), 50.0);
1113    }
1114
1115    #[test]
1116    fn smoothstep_between_tests() {
1117        assert_eq!(smoothstep_between(0.0, 1.0, 0.0), 0.0);
1118        assert_eq!(smoothstep_between(0.0, 1.0, 0.5), 0.5);
1119        assert_eq!(smoothstep_between(0.0, 1.0, 1.0), 1.0);
1120        assert_eq!(smoothstep_between(2.0, 5.0, 3.5), 0.5);
1121        assert_eq!(smoothstep_between(0.0, 1.0, -1.0), 0.0);
1122        assert_eq!(smoothstep_between(0.0, 1.0, 2.0), 1.0);
1123        // edge0 == edge1: should not panic or return NaN
1124        assert_eq!(smoothstep_between(0.0, 0.0, 0.5), 1.0);
1125        assert_eq!(smoothstep_between(5.0, 5.0, 3.0), 0.0);
1126        assert_eq!(smoothstep_between(5.0, 5.0, 5.0), 1.0);
1127    }
1128
1129    #[test]
1130    fn is_between_tests() {
1131        assert!(is_between(5.0, 0.0, 10.0));
1132        assert!(!is_between(15.0, 0.0, 10.0));
1133        assert!(!is_between(-1.0, 0.0, 10.0));
1134        assert!(is_between(0.0, 0.0, 10.0));
1135        assert!(is_between(10.0, 0.0, 10.0));
1136    }
1137
1138    #[test]
1139    fn lerp_clamped_tests() {
1140        assert_eq!(lerp_clamped(0.0, 10.0, 0.25), 2.5);
1141        assert_eq!(lerp_clamped(0.0, 10.0, -1.0), 0.0);
1142        assert_eq!(lerp_clamped(0.0, 10.0, 2.0), 10.0);
1143    }
1144
1145    #[test]
1146    fn remap_clamped_tests() {
1147        assert_eq!(remap_clamped(5.0, 0.0, 10.0, 0.0, 100.0), 50.0);
1148        assert_eq!(remap_clamped(-5.0, 0.0, 10.0, 0.0, 100.0), 0.0);
1149        assert_eq!(remap_clamped(15.0, 0.0, 10.0, 0.0, 100.0), 100.0);
1150    }
1151
1152    #[test]
1153    fn bilinear_tests() {
1154        assert_eq!(bilinear(0.0, 0.0, 0.0, 0.0, 0.0, 10.0), 0.0);
1155        assert_eq!(bilinear(1.0, 1.0, 0.0, 0.0, 0.0, 10.0), 10.0);
1156        assert_eq!(bilinear(0.5, 0.5, 0.0, 10.0, 10.0, 0.0), 5.0);
1157        assert_eq!(bilinear(0.5, 0.0, 0.0, 10.0, 20.0, 30.0), 10.0);
1158    }
1159
1160    #[test]
1161    fn is_sorted_tests() {
1162        assert!(is_sorted(&[1, 2, 3, 4]));
1163        assert!(is_sorted(&[1, 1, 2, 3]));
1164        assert!(!is_sorted(&[1, 3, 2, 4]));
1165        assert!(is_sorted::<i32>(&[]));
1166        assert!(is_sorted(&[42]));
1167    }
1168
1169    #[test]
1170    fn min_max_value_tests() {
1171        assert_eq!(min_value(&[3, 1, 4, 1, 5]), Some(&1));
1172        assert_eq!(min_value::<i32>(&[]), None);
1173        assert_eq!(max_value(&[3, 1, 4, 1, 5]), Some(&5));
1174        assert_eq!(max_value::<i32>(&[]), None);
1175    }
1176
1177    #[test]
1178    fn mean_tests() {
1179        assert_eq!(mean(&[1.0, 2.0, 3.0, 4.0]), Some(2.5));
1180        assert_eq!(mean::<f64>(&[]), None);
1181        assert_eq!(mean(&[5.0]), Some(5.0));
1182    }
1183
1184    #[test]
1185    fn cumsum_tests() {
1186        assert_eq!(cumsum(&[1, 2, 3, 4]), vec![1, 3, 6, 10]);
1187        assert_eq!(cumsum::<i32>(&[]), vec![]);
1188        assert_eq!(cumsum(&[5]), vec![5]);
1189    }
1190
1191    #[test]
1192    fn cumprod_tests() {
1193        assert_eq!(cumprod(&[1, 2, 3, 4]), vec![1, 2, 6, 24]);
1194        assert_eq!(cumprod::<i32>(&[]), vec![]);
1195        assert_eq!(cumprod(&[5]), vec![5]);
1196    }
1197
1198    #[test]
1199    fn dot_product_tests() {
1200        assert_eq!(dot_product(&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]), 32.0);
1201        assert_eq!(dot_product(&[2, 3], &[4, 5]), 23);
1202        assert_eq!(dot_product::<f64>(&[], &[]), 0.0);
1203    }
1204}