rolling_median/lib.rs
1// SPDX-License-Identifier: 0BSD
2// rolling-median
3// Copyright (C) 2025-2026 by LoRd_MuldeR <mulder2@gmx.de>
4
5#![allow(clippy::needless_doctest_main)]
6#![allow(clippy::unnecessary_map_or)]
7
8//! # rolling-median
9//!
10//! Computes the [**median**](https://en.wikipedia.org/wiki/Median) of a data set, using a “rolling” (online) algorithm.
11//!
12//! It uses two heaps (a “min” heap and a “max” heap) to efficiently keep track of the “middle” element.
13//!
14//! ## Complexity
15//!
16//! The `push()` operation has a complexity of: **`O(log(n))`**
17//!
18//! The `get()` operation has a complexity of: **`O(1)`**
19//!
20//! ## Usage
21//!
22//! Here is a simple example that demonstrates how to use it in your code:
23//!
24//! ```rust
25//! use rolling_median::Median;
26//!
27//! const VALUES: [f64; 6usize] = [3.27f64, 4.60f64, 5.95f64, 9.93f64, 7.79f64, 4.73f64];
28//!
29//! fn main() {
30//! let mut rolling_median = Median::new();
31//!
32//! for value in VALUES {
33//! rolling_median.push(value).expect("Invalid value!");
34//! println!("Median, so far: {}", rolling_median.get().expect("No result!"))
35//! }
36//!
37//! println!("Final median: {}", rolling_median.get().expect("No result!"))
38//! }
39//! ```
40//!
41//! 👉 Please see the [`Median`] struct for details!
42
43pub mod float_utils;
44
45use crate::float_utils::{FloatOrd, FloatType, InvalidValue};
46use std::{cmp::Reverse, collections::BinaryHeap};
47
48// --------------------------------------------------------------------------
49// Rolling median
50// --------------------------------------------------------------------------
51
52/// Computes the median of a data set, using a "rolling" (online) algorithm
53pub struct Median<T: FloatType> {
54 heap_lo: BinaryHeap<FloatOrd<T>>,
55 heap_hi: BinaryHeap<Reverse<FloatOrd<T>>>,
56}
57
58impl<T: FloatType> Median<T> {
59 /// Initializes a new rolling median computation
60 pub fn new() -> Self {
61 Median { heap_lo: BinaryHeap::new(), heap_hi: BinaryHeap::new() }
62 }
63
64 /// Insert the next value
65 ///
66 /// Returns `Ok(())`, if the value was inserted, or `Err(InvalidValue)`, if an attempt to insert `NaN` was made.
67 ///
68 /// This operation has a complexity of **`O(log(n))`**.
69 pub fn push(&mut self, value: T) -> Result<(), InvalidValue> {
70 let value = FloatOrd::new(value)?;
71
72 if self.heap_lo.peek().map_or(true, |peek| value <= *peek) {
73 self.heap_lo.push(value);
74 } else {
75 self.heap_hi.push(Reverse(value));
76 }
77
78 if self.heap_lo.len() > self.heap_hi.len().checked_add(1usize).unwrap() {
79 if let Some(value) = self.heap_lo.pop() {
80 self.heap_hi.push(Reverse(value));
81 }
82 } else if self.heap_hi.len() > self.heap_lo.len() {
83 if let Some(Reverse(value)) = self.heap_hi.pop() {
84 self.heap_lo.push(value);
85 }
86 }
87
88 Ok(())
89 }
90
91 /// Get the current median
92 ///
93 /// Returns `Some(median_value)`, if at least one value was inserted; otherwise `None`.
94 ///
95 /// This operation has a complexity of **`O(1)`**.
96 pub fn get(&self) -> Option<T> {
97 if self.heap_lo.is_empty() {
98 None
99 } else if self.heap_lo.len() == self.heap_hi.len() {
100 let lo_top = *self.heap_lo.peek().unwrap();
101 let hi_top = self.heap_hi.peek().unwrap().0;
102 Some(lo_top.midpoint(hi_top))
103 } else {
104 Some(self.heap_lo.peek().unwrap().into_inner())
105 }
106 }
107
108 /// Returns the number of values that have been inserted so far
109 pub fn len(&self) -> usize {
110 self.heap_lo.len().saturating_add(self.heap_hi.len())
111 }
112
113 /// Returns `true`, if **no** values have been inserted yet; otherwise `false`
114 pub fn is_empty(&self) -> bool {
115 self.heap_lo.is_empty() && self.heap_hi.is_empty()
116 }
117
118 /// Clear all values and restart the median computation from scratch
119 pub fn clear(&mut self) {
120 self.heap_lo.clear();
121 self.heap_hi.clear();
122 }
123}
124
125impl<T: FloatType> Default for Median<T> {
126 /// Initializes a new rolling median computation
127 fn default() -> Self {
128 Self::new()
129 }
130}