use std::time::Duration;
use arrow_schema::{DataType, Schema, TimeUnit};
use crate::filter::resolve;
use crate::value::{Kind, Value, ValueFn};
use crate::view::{Rows, SortColumn, ViewState};
use crate::DataError;
#[derive(Debug, Clone, PartialEq)]
pub struct Window {
column: String,
span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Span {
Duration(Duration),
Value(f64),
}
impl Window {
#[must_use]
pub fn duration(column: impl Into<String>, span: Duration) -> Self {
Self {
column: column.into(),
span: Span::Duration(span),
}
}
#[must_use]
pub fn span(column: impl Into<String>, span: f64) -> Self {
Self {
column: column.into(),
span: Span::Value(span),
}
}
pub fn column(&self) -> &str {
&self.column
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Reach {
Whole(i128),
Float(f64),
}
#[derive(Debug, Clone)]
pub(crate) struct WindowState {
spec: Window,
reach: Reach,
pub(crate) rows: ViewState,
column: usize,
read: ValueFn,
}
impl WindowState {
pub(crate) fn compile(spec: &Window, schema: &Schema) -> Result<Self, DataError> {
let (column, kind, read) = resolve(schema, &spec.column)?;
let mismatch = || DataError::WindowSpan(spec.column.clone());
let reach = match (spec.span, time_unit(schema.field(column).data_type())) {
(Span::Duration(span), Some(unit)) => {
Reach::Whole((span.as_nanos() / unit).cast_signed())
}
(Span::Value(span), None) if span >= 0.0 => match kind {
Kind::Int | Kind::UInt => Reach::Whole(whole(span)),
Kind::Decimal { scale } => Reach::Whole(whole(span * 10f64.powi(scale.into()))),
Kind::Float => Reach::Float(span),
Kind::Bool | Kind::Str => return Err(mismatch()),
},
_ => return Err(mismatch()),
};
Ok(Self {
spec: spec.clone(),
reach,
rows: ViewState::sorted(SortColumn::ascending(column, read)),
column,
read,
})
}
pub(crate) fn spec(&self) -> &Window {
&self.spec
}
pub(crate) fn expired(&self, rows: Rows<'_>) -> usize {
let value = |id| {
let at = rows.at(id);
(self.read)(rows.batches[at.batch].column(self.column).as_ref(), at.row)
};
let below = self.rows.partition_point(|id| below_infinity(value(id)));
let Some(newest) = below.checked_sub(1).and_then(|i| self.rows.get(i)) else {
return 0;
};
let newest = value(newest);
self.rows
.partition_point(|id| older(value(id), newest, self.reach))
}
}
const NANOS_PER_DAY: u128 = 86_400 * 1_000_000_000;
fn time_unit(data_type: &DataType) -> Option<u128> {
let unit = match data_type {
DataType::Timestamp(unit, _)
| DataType::Time32(unit)
| DataType::Time64(unit)
| DataType::Duration(unit) => unit,
DataType::Date32 => return Some(NANOS_PER_DAY),
DataType::Date64 => &TimeUnit::Millisecond,
_ => return None,
};
Some(match unit {
TimeUnit::Second => 1_000_000_000,
TimeUnit::Millisecond => 1_000_000,
TimeUnit::Microsecond => 1_000,
TimeUnit::Nanosecond => 1,
})
}
fn whole(span: f64) -> i128 {
let nearest = span.round();
let span = match (span - nearest).abs() <= NEAR * nearest.abs() {
true => nearest,
false => span.floor(),
};
span as i128
}
const NEAR: f64 = 1e-9;
fn below_infinity(value: Value<'_>) -> bool {
match value {
Value::Int(_) | Value::UInt(_) | Value::Decimal(_) => true,
Value::Float(v) => v < f64::INFINITY,
Value::Null | Value::Bool(_) | Value::Str(_) => false,
}
}
fn older(value: Value<'_>, newest: Value<'_>, reach: Reach) -> bool {
let whole = |v: Value<'_>| match v {
Value::Int(v) => Some(i128::from(v)),
Value::UInt(v) => Some(i128::from(v)),
Value::Decimal(v) => Some(v),
_ => None,
};
match (reach, value, newest) {
(Reach::Float(reach), Value::Float(value), Value::Float(newest)) => newest - value > reach,
(Reach::Whole(reach), value, newest) => match (whole(value), whole(newest)) {
(Some(value), Some(newest)) => newest.saturating_sub(value) > reach,
_ => false,
},
_ => false,
}
}