use num_traits::{AsPrimitive as _, PrimInt as _, WrappingSub, Zero as _};
use zigzag::ZigZag;
const MIN_SAMPLE: usize = 512;
const MAX_SAMPLE: usize = 16_384;
const RLE_MIN_AVG_RUN_LENGTH: f64 = 2.0;
#[derive(Debug, Clone, Default)]
pub struct DataProfile {
avg_run_length: f64,
delta_avg_run_length: f64,
is_sorted: bool,
total_bits: u32,
delta_total_bits: u32,
}
impl DataProfile {
#[must_use]
#[expect(clippy::cast_precision_loss)]
pub(crate) fn profile<T>(sample: &[T::UInt]) -> Self
where
T: ZigZag,
<T as ZigZag>::UInt: WrappingSub,
{
if sample.is_empty() {
return Self::default();
}
let type_bits = T::zero().leading_zeros();
let mut runs: usize = 1;
let mut delta_runs: usize = 1;
let mut is_sorted_rising = true;
let mut is_sorted_falling = true;
let mut total_bits: u32 = type_bits - sample[0].leading_zeros();
let mut delta_total_bits: u32 = 0;
let mut prev = sample[0];
let mut prev_zz: T::UInt = T::UInt::zero();
for (i, &v) in sample[1..].iter().enumerate() {
if v != prev {
runs += 1;
}
if v < prev {
is_sorted_rising = false;
} else if prev < v {
is_sorted_falling = false;
}
let delta_bits: T::UInt = v.wrapping_sub(&prev);
let delta_signed: T = delta_bits.as_();
let zz = T::encode(delta_signed);
if i == 0 {
prev_zz = zz;
} else if zz != prev_zz {
delta_runs += 1;
prev_zz = zz;
}
total_bits += type_bits - v.leading_zeros();
delta_total_bits += type_bits - zz.leading_zeros();
prev = v;
}
let delta_len = sample.len().saturating_sub(1).max(1);
Self {
avg_run_length: sample.len() as f64 / runs as f64,
delta_avg_run_length: delta_len as f64 / delta_runs as f64,
is_sorted: is_sorted_rising || is_sorted_falling,
total_bits,
delta_total_bits,
}
}
pub(crate) fn take_sample<T>(values: &[T]) -> &[T] {
let len = (values.len() / 100).clamp(MIN_SAMPLE, MAX_SAMPLE);
if values.len() <= len {
values
} else {
let start = (values.len() / 2).saturating_sub(len / 2);
&values[start..start + len]
}
}
#[must_use]
pub(crate) fn rle_is_viable(&self) -> bool {
self.avg_run_length >= RLE_MIN_AVG_RUN_LENGTH
}
#[must_use]
pub(crate) fn delta_rle_is_viable(&self) -> bool {
self.delta_avg_run_length >= RLE_MIN_AVG_RUN_LENGTH
}
#[must_use]
pub(crate) fn delta_is_beneficial(&self) -> bool {
self.is_sorted || self.delta_total_bits < self.total_bits
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_profile_flags(profile: &DataProfile, delta: bool, rle: bool, delta_rle: bool) {
assert_eq!(profile.delta_is_beneficial(), delta, "delta");
assert_eq!(profile.rle_is_viable(), rle, "rle");
assert_eq!(profile.delta_rle_is_viable(), delta_rle, "delta_rle");
}
#[test]
fn profile_sequential_u32_uses_delta_rle_without_raw_rle() {
let data: Vec<u32> = (0..100).collect();
let profile = DataProfile::profile::<i32>(DataProfile::take_sample(&data));
assert!(profile.is_sorted);
assert_profile_flags(&profile, true, false, true);
}
#[test]
fn profile_constant_u32_uses_raw_and_delta_rle() {
let data = vec![1234u32; 500];
let profile = DataProfile::profile::<i32>(DataProfile::take_sample(&data));
assert!(profile.is_sorted);
assert_profile_flags(&profile, true, true, true);
}
#[test]
fn profile_sequential_u64_uses_delta_rle_without_raw_rle() {
let data: Vec<u64> = (0u64..500).collect();
let profile = DataProfile::profile::<i64>(DataProfile::take_sample(&data));
assert!(profile.is_sorted);
assert_profile_flags(&profile, true, false, true);
}
#[test]
fn profile_empty_has_no_optimizing_flags() {
let values: &[u32] = &[];
let profile = DataProfile::profile::<i32>(DataProfile::take_sample(values));
assert!(!profile.is_sorted);
assert_profile_flags(&profile, false, false, false);
}
}