use std::cmp::Ordering;
use crate::common::SearchCriteria;
use crate::error::Error;
#[derive(Debug, Clone)]
pub struct SortedView<T: Clone> {
entries: Vec<Entry<T>>,
total_weight: u64,
min_item: Option<T>,
max_item: Option<T>,
}
#[derive(Debug, Clone)]
struct Entry<T> {
item: T,
cumulative_weight: u64,
}
impl<T: Clone + Ord> SortedView<T> {
fn from_sorted(mut entries: Vec<Entry<T>>, min_item: Option<T>, max_item: Option<T>) -> Self {
let mut total_weight = 0u64;
for entry in &mut entries {
total_weight += entry.cumulative_weight;
entry.cumulative_weight = total_weight;
}
Self {
entries,
total_weight,
min_item,
max_item,
}
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn total_weight(&self) -> u64 {
self.total_weight
}
pub fn rank(&self, item: &T, criteria: SearchCriteria) -> Option<f64> {
if self.is_empty() {
return None;
}
let index = if criteria == SearchCriteria::Inclusive {
upper_bound(&self.entries, item)
} else {
lower_bound(&self.entries, item)
};
if index == 0 {
return Some(0.0);
}
Some(self.entries[index - 1].cumulative_weight as f64 / self.total_weight as f64)
}
pub fn quantile(&self, rank: f64, criteria: SearchCriteria) -> Result<Option<T>, Error> {
if !(0.0..=1.0).contains(&rank) {
return Err(Error::invalid_argument(format!(
"rank must be in [0.0, 1.0], got {rank}"
)));
}
if self.is_empty() {
return Ok(None);
}
if rank == 0.0 {
return Ok(Some(self.min_item.as_ref().unwrap().clone()));
}
if rank == 1.0 {
return Ok(Some(self.max_item.as_ref().unwrap().clone()));
}
let weight = if criteria == SearchCriteria::Inclusive {
(rank * self.total_weight as f64).ceil() as u64
} else {
(rank * self.total_weight as f64) as u64
};
let index = if criteria == SearchCriteria::Inclusive {
lower_bound_by_weight(&self.entries, weight)
} else {
upper_bound_by_weight(&self.entries, weight)
};
Ok(Some(
self.entries[index.min(self.entries.len() - 1)].item.clone(),
))
}
pub fn quantiles(
&self,
ranks: &[f64],
criteria: SearchCriteria,
) -> Result<Option<Vec<T>>, Error> {
for &rank in ranks {
if !(0.0..=1.0).contains(&rank) {
return Err(Error::invalid_argument(format!(
"rank must be in [0.0, 1.0], got {rank}"
)));
}
}
if self.is_empty() {
return Ok(None);
}
let quantiles = ranks
.iter()
.map(|&rank| {
self.quantile(rank, criteria)
.map(|quantile| quantile.expect("checked non-empty view"))
})
.collect::<Result<_, _>>()?;
Ok(Some(quantiles))
}
pub fn cdf(
&self,
split_points: &[T],
criteria: SearchCriteria,
) -> Result<Option<Vec<f64>>, Error> {
check_split_points(split_points)?;
if self.is_empty() {
return Ok(None);
}
let mut ranks = Vec::with_capacity(split_points.len() + 1);
for item in split_points {
ranks.push(self.rank(item, criteria).expect("checked non-empty view"));
}
ranks.push(1.0);
Ok(Some(ranks))
}
pub fn pmf(
&self,
split_points: &[T],
criteria: SearchCriteria,
) -> Result<Option<Vec<f64>>, Error> {
let Some(mut buckets) = self.cdf(split_points, criteria)? else {
return Ok(None);
};
for index in (1..buckets.len()).rev() {
buckets[index] -= buckets[index - 1];
}
Ok(Some(buckets))
}
}
pub fn build_sorted_view<T: Clone + Ord>(
levels: &[Vec<T>],
is_level_zero_sorted: bool,
min_item: Option<T>,
max_item: Option<T>,
) -> SortedView<T> {
let mut runs = Vec::with_capacity(levels.len());
for (level_index, level) in levels.iter().enumerate() {
let weight = 1u64 << level_index;
let mut run: Vec<_> = level
.iter()
.cloned()
.map(|item| Entry {
item,
cumulative_weight: weight,
})
.collect();
if level_index == 0 && !is_level_zero_sorted {
run.sort_unstable_by(|left, right| left.item.cmp(&right.item));
}
if !run.is_empty() {
runs.push(run);
}
}
while runs.len() > 1 {
let mut merged_runs = Vec::with_capacity(runs.len().div_ceil(2));
let mut iter = runs.into_iter();
while let Some(left) = iter.next() {
if let Some(right) = iter.next() {
merged_runs.push(merge_sorted_entries(left, right));
} else {
merged_runs.push(left);
}
}
runs = merged_runs;
}
SortedView::from_sorted(runs.pop().unwrap_or_default(), min_item, max_item)
}
fn merge_sorted_entries<T: Ord>(left: Vec<Entry<T>>, right: Vec<Entry<T>>) -> Vec<Entry<T>> {
let mut merged = Vec::with_capacity(left.len() + right.len());
let mut left = left.into_iter().peekable();
let mut right = right.into_iter().peekable();
while let (Some(left_entry), Some(right_entry)) = (left.peek(), right.peek()) {
if left_entry.item.cmp(&right_entry.item) == Ordering::Greater {
merged.push(right.next().unwrap());
} else {
merged.push(left.next().unwrap());
}
}
merged.extend(left);
merged.extend(right);
merged
}
fn check_split_points<T: Ord>(split_points: &[T]) -> Result<(), Error> {
for (index, pair) in split_points.windows(2).enumerate() {
if pair[0].cmp(&pair[1]) != Ordering::Less {
return Err(Error::invalid_argument(format!(
"split points at indices {index} and {} must be strictly increasing",
index + 1
)));
}
}
Ok(())
}
fn lower_bound<T: Ord>(entries: &[Entry<T>], item: &T) -> usize {
entries.partition_point(|entry| entry.item.cmp(item) == Ordering::Less)
}
fn upper_bound<T: Ord>(entries: &[Entry<T>], item: &T) -> usize {
entries.partition_point(|entry| entry.item.cmp(item) != Ordering::Greater)
}
fn lower_bound_by_weight<T>(entries: &[Entry<T>], weight: u64) -> usize {
entries.partition_point(|entry| entry.cumulative_weight < weight)
}
fn upper_bound_by_weight<T>(entries: &[Entry<T>], weight: u64) -> usize {
entries.partition_point(|entry| entry.cumulative_weight <= weight)
}