1use std::ops::Range;
7
8use arrow_array::UInt32Array;
9
10use crate::column::ColumnData;
11use crate::error::{PondError, Result};
12use crate::reducer::{Agg, check_source_kind};
13use crate::rolling::{Alignment, Frame, Span, check_magnitude_in};
14use crate::series::{Column, Key, TimeSeries};
15
16#[derive(Debug, Clone, PartialEq)]
19pub enum Bins {
20 Width {
25 width: f64,
27 origin: f64,
29 },
30 Edges {
37 edges: Vec<f64>,
39 upper_inclusive: bool,
41 },
42}
43
44impl Bins {
45 pub fn width(width: f64) -> Self {
47 Bins::Width { width, origin: 0.0 }
48 }
49
50 pub fn width_from(width: f64, origin: f64) -> Self {
52 Bins::Width { width, origin }
53 }
54
55 pub fn edges(edges: impl Into<Vec<f64>>) -> Self {
57 Bins::Edges {
58 edges: edges.into(),
59 upper_inclusive: false,
60 }
61 }
62
63 pub fn edges_upper_inclusive(edges: impl Into<Vec<f64>>) -> Self {
66 Bins::Edges {
67 edges: edges.into(),
68 upper_inclusive: true,
69 }
70 }
71}
72
73#[derive(Debug, Clone)]
77pub struct Table {
78 columns: Vec<Column>,
79 rows: usize,
80}
81
82impl Table {
83 pub fn len(&self) -> usize {
85 self.rows
86 }
87
88 pub fn is_empty(&self) -> bool {
90 self.rows == 0
91 }
92
93 pub fn columns(&self) -> &[Column] {
95 &self.columns
96 }
97
98 pub fn column(&self, name: &str) -> Result<&Column> {
100 self.columns
101 .iter()
102 .find(|c| c.name() == name)
103 .ok_or_else(|| PondError::UnknownColumn {
104 call: "Table::column",
105 column: name.to_string(),
106 available: self.columns.iter().map(|c| c.name().to_string()).collect(),
107 })
108 }
109}
110
111const MAX_WIDTH_BINS: f64 = 1_000_000.0;
113
114const MAX_SAFE: f64 = 9_007_199_254_740_991.0;
117
118impl TimeSeries {
119 pub fn by_column(&self, column: &str, bins: &Bins, aggs: &[Agg]) -> Result<Table> {
152 const CALL: &str = "by_column";
153 let (_, values) = self.number_source(CALL, column)?;
154 let plan = self.value_axis_plan(CALL, aggs, &["start", "end"])?;
155 const NONE: u32 = u32::MAX;
157 let (bin_of, first, count): (Vec<u32>, f64, usize) = match bins {
158 Bins::Edges {
159 edges,
160 upper_inclusive,
161 } => {
162 check_edges(CALL, edges)?;
163 let last = edges.len() - 1;
164 let bin_of = values
165 .iter()
166 .map(|&v| {
167 if !v.is_finite() {
168 return NONE;
169 }
170 edge_bin(edges, v, *upper_inclusive).map_or(NONE, |b| b as u32)
171 })
172 .collect();
173 (bin_of, 0.0, last)
174 }
175 &Bins::Width { width, origin } => {
176 if !width.is_finite() || width <= 0.0 {
177 return Err(invalid(CALL, "width must be a positive finite number"));
178 }
179 if !origin.is_finite() {
180 return Err(invalid(CALL, "origin must be finite"));
181 }
182 let raw: Vec<f64> = values
183 .iter()
184 .map(|&v| {
185 if v.is_finite() {
186 width_bin(v, width, origin)
187 } else {
188 f64::NAN
189 }
190 })
191 .collect();
192 let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
195 for &b in raw.iter().filter(|b| !b.is_nan()) {
196 if b < lo {
197 lo = b;
198 }
199 if b > hi {
200 hi = b;
201 }
202 }
203 if lo > hi {
204 (vec![NONE; values.len()], 0.0, 0)
205 } else {
206 if !(lo.abs() <= MAX_SAFE && hi.abs() <= MAX_SAFE) {
207 return Err(invalid(
208 CALL,
209 "the data range and width give a bin number past 2^53; use a larger width",
210 ));
211 }
212 let count = hi - lo + 1.0;
213 if count > MAX_WIDTH_BINS {
214 return Err(invalid(
215 CALL,
216 &format!(
217 "width gives {count} bins (more than {MAX_WIDTH_BINS}); use a larger width or edges"
218 ),
219 ));
220 }
221 let bin_of = raw
222 .iter()
223 .map(|&b| if b.is_nan() { NONE } else { (b - lo) as u32 })
224 .collect();
225 (bin_of, lo, count as usize)
226 }
227 }
228 };
229 let mut starts = Vec::with_capacity(count);
231 let mut ends = Vec::with_capacity(count);
232 for b in 0..count {
233 let (start, end) = match bins {
234 Bins::Edges { edges, .. } => (edges[b], edges[b + 1]),
235 &Bins::Width { width, origin } => {
236 let i = first + b as f64;
237 (origin + i * width, origin + (i + 1.0) * width)
238 }
239 };
240 if !start.is_finite() || !end.is_finite() || end <= start {
241 return Err(invalid(
242 CALL,
243 &format!(
244 "bin [{start}, {end}) is not a range f64 can hold; use a larger width or edges"
245 ),
246 ));
247 }
248 starts.push(start);
249 ends.push(end);
250 }
251 let mut first_row = vec![0u32; count + 1];
254 for &b in bin_of.iter().filter(|&&b| b != NONE) {
255 first_row[b as usize + 1] += 1;
256 }
257 for b in 0..count {
258 first_row[b + 1] += first_row[b];
259 }
260 let mut next = first_row.clone();
261 let mut order = vec![0u32; first_row[count] as usize];
262 for (row, &b) in bin_of.iter().enumerate() {
263 if b != NONE {
264 order[next[b as usize] as usize] = row as u32;
265 next[b as usize] += 1;
266 }
267 }
268 let ranges: Vec<Range<usize>> = (0..count)
269 .map(|b| first_row[b] as usize..first_row[b + 1] as usize)
270 .collect();
271 let (holder, plan) = self.holder(&plan, &order);
272 let plan: Vec<(&Agg, usize)> = plan.iter().map(|&(s, a)| (a, s)).collect();
273 let reduced = holder.reduce_columns(CALL, &plan, &ranges)?;
274 let mut columns = vec![
275 Column::required("start", starts),
276 Column::required("end", ends),
277 ];
278 columns.extend(reduced);
279 Ok(Table {
280 columns,
281 rows: count,
282 })
283 }
284
285 pub fn rolling_by_column(&self, column: &str, radius: f64, aggs: &[Agg]) -> Result<Table> {
313 self.roll_by_column(column, radius, None, aggs)
314 }
315
316 pub fn rolling_by_column_at(
323 &self,
324 column: &str,
325 radius: f64,
326 centers: &[f64],
327 aggs: &[Agg],
328 ) -> Result<Table> {
329 self.roll_by_column(column, radius, Some(centers), aggs)
330 }
331
332 fn roll_by_column(
333 &self,
334 column: &str,
335 radius: f64,
336 centers: Option<&[f64]>,
337 aggs: &[Agg],
338 ) -> Result<Table> {
339 const CALL: &str = "rolling_by_column";
340 let (_, values) = self.number_source(CALL, column)?;
341 if !radius.is_finite() || radius <= 0.0 {
342 return Err(invalid(CALL, "radius must be a positive finite number"));
343 }
344 let plan = self.value_axis_plan(CALL, aggs, &[])?;
345 let mut rows = Vec::with_capacity(values.len());
347 let mut axis = Vec::with_capacity(values.len());
348 for (row, &v) in values.iter().enumerate() {
349 if !v.is_finite() {
350 continue;
351 }
352 if let Some(&prev) = axis.last()
353 && v < prev
354 {
355 return Err(invalid(
356 CALL,
357 &format!("column '{column}' must not go down; row {row} ({v}) is below {prev}"),
358 ));
359 }
360 rows.push(row as u32);
361 axis.push(v);
362 }
363 if let Some(centers) = centers {
364 for (k, &c) in centers.iter().enumerate() {
365 if !c.is_finite() {
366 return Err(invalid(CALL, &format!("center {k} ({c}) is not finite")));
367 }
368 if k > 0 && c < centers[k - 1] {
369 return Err(invalid(CALL, "centers must not go down"));
370 }
371 }
372 }
373 let mut windows = Vec::with_capacity(centers.map_or(axis.len(), <[f64]>::len));
377 let (mut lo, mut hi) = (0usize, 0usize);
378 let mut place = |c: f64| {
379 let (from, to) = (c - radius, c + radius);
380 while hi < axis.len() && axis[hi] <= to {
381 hi += 1;
382 }
383 while lo < hi && axis[lo] < from {
384 lo += 1;
385 }
386 lo..hi
387 };
388 match centers {
389 Some(centers) => windows.extend(centers.iter().map(|&c| place(c))),
390 None => windows.extend(axis.iter().map(|&c| place(c))),
391 }
392 rows.truncate(windows.last().map_or(0, |w| w.end));
395 for &(source, agg) in &plan {
396 check_magnitude_in(CALL, &self.columns()[source], agg.reducer, Some(&rows))?;
397 }
398 let (holder, plan) = self.holder(&plan, &rows);
399 let rolled = holder.roll_columns(CALL, &windows_frame(&windows), &plan)?;
400 let columns = if centers.is_none() && rows.len() < values.len() {
401 let nothing = [Range { start: 0, end: 0 }];
404 let empty = holder.roll_columns(CALL, &windows_frame(¬hing), &plan)?;
405 let mut placed = 0u32;
406 let picks: Vec<(u32, u32)> = values
407 .iter()
408 .map(|v| {
409 if v.is_finite() {
410 placed += 1;
411 (0, placed - 1)
412 } else {
413 (1, 0)
414 }
415 })
416 .collect();
417 rolled
418 .iter()
419 .zip(&empty)
420 .map(|(r, e)| {
421 let data = ColumnData::gather(&[r.data(), e.data()], &picks);
422 Column::from_data(r.name().to_string(), data, false)
423 })
424 .collect()
425 } else {
426 rolled
427 };
428 let rows = match centers {
429 Some(_) => windows.len(),
430 None => values.len(),
431 };
432 Ok(Table { columns, rows })
433 }
434
435 fn value_axis_plan<'a>(
439 &self,
440 call: &'static str,
441 aggs: &'a [Agg],
442 reserved: &[&str],
443 ) -> Result<Vec<(usize, &'a Agg)>> {
444 let mut plan = Vec::with_capacity(aggs.len());
445 for (i, agg) in aggs.iter().enumerate() {
446 let source = self.column_index(call, &agg.source)?;
447 agg.reducer.check(call)?;
448 if reserved.contains(&agg.output.as_str())
449 || aggs[..i].iter().any(|a| a.output == agg.output)
450 {
451 return Err(PondError::DuplicateOutput {
452 call,
453 column: agg.output.clone(),
454 });
455 }
456 check_source_kind(call, &self.columns()[source], agg.reducer)?;
457 plan.push((source, agg));
458 }
459 Ok(plan)
460 }
461
462 fn holder<'a>(
465 &self,
466 plan: &[(usize, &'a Agg)],
467 rows: &[u32],
468 ) -> (TimeSeries, Vec<(usize, &'a Agg)>) {
469 let indices = UInt32Array::from(rows.to_vec());
470 let mut sources: Vec<usize> = Vec::new();
471 let plan = plan
472 .iter()
473 .map(|&(source, agg)| {
474 let at = sources
475 .iter()
476 .position(|&s| s == source)
477 .unwrap_or_else(|| {
478 sources.push(source);
479 sources.len() - 1
480 });
481 (at, agg)
482 })
483 .collect();
484 let columns = sources
485 .iter()
486 .map(|&s| {
487 let c = &self.columns()[s];
488 Column::from_data(c.name().to_string(), c.data().take_indices(&indices), false)
489 })
490 .collect();
491 let holder = TimeSeries::from_parts(
492 self.name().to_string(),
493 "time".to_string(),
494 Key::Time(vec![0; rows.len()]),
495 columns,
496 );
497 (holder, plan)
498 }
499}
500
501fn windows_frame(windows: &[Range<usize>]) -> Frame<'_> {
503 Frame {
504 times: &[],
505 span: Span::Windows(windows),
506 alignment: Alignment::default(),
507 min_samples: 0,
508 }
509}
510
511fn invalid(call: &'static str, message: &str) -> PondError {
512 PondError::InvalidData {
513 call,
514 message: message.to_string(),
515 }
516}
517
518fn check_edges(call: &'static str, edges: &[f64]) -> Result<()> {
519 if edges.len() < 2 {
520 return Err(invalid(call, "edges must have at least 2 entries"));
521 }
522 for (i, &e) in edges.iter().enumerate() {
523 if !e.is_finite() {
524 return Err(invalid(call, &format!("edge {i} ({e}) is not finite")));
525 }
526 if i > 0 && e <= edges[i - 1] {
527 return Err(invalid(call, "edges must be strictly rising"));
528 }
529 }
530 Ok(())
531}
532
533fn edge_bin(edges: &[f64], v: f64, upper_inclusive: bool) -> Option<usize> {
536 let last = edges.len() - 1;
537 let (mut lo, mut hi) = (0usize, last);
538 if upper_inclusive {
539 if v < edges[0] || v > edges[last] {
540 return None;
541 }
542 while lo < hi {
545 let mid = (lo + hi).div_ceil(2);
546 if edges[mid] < v {
547 lo = mid
548 } else {
549 hi = mid - 1
550 }
551 }
552 } else {
553 if v < edges[0] || v >= edges[last] {
554 return None;
555 }
556 while lo < hi {
557 let mid = (lo + hi).div_ceil(2);
558 if edges[mid] <= v {
559 lo = mid
560 } else {
561 hi = mid - 1
562 }
563 }
564 }
565 Some(lo)
566}
567
568fn width_bin(v: f64, width: f64, origin: f64) -> f64 {
573 let mut bin = ((v - origin) / width).floor();
574 let mut guard = 0;
575 while v < origin + bin * width && guard < 4 {
576 guard += 1;
577 bin -= 1.0;
578 }
579 while v >= origin + (bin + 1.0) * width && guard < 4 {
581 guard += 1;
582 bin += 1.0;
583 }
584 bin
585}
586
587#[cfg(test)]
588mod tests {
589 use super::*;
590 use crate::Reducer;
591
592 fn track() -> TimeSeries {
593 TimeSeries::new(
594 "t",
595 vec![0, 1000, 2000, 3000],
596 vec![
597 Column::required("d", vec![0.0, 1.0, 2.0, 3.0]),
598 Column::optional("v", [Some(1.0), Some(2e300), None, Some(4.0)]),
599 ],
600 )
601 .unwrap()
602 }
603
604 #[test]
607 fn refuses_what_pond_ts_refuses() {
608 let s = track();
609 let count = [Agg::new("n", "d", Reducer::Count)];
610 for bins in [
611 Bins::width(f64::INFINITY),
612 Bins::width(f64::NAN),
613 Bins::width_from(1.0, f64::NAN),
614 Bins::width_from(1.0, f64::NEG_INFINITY),
615 Bins::edges([0.0, f64::INFINITY]),
616 Bins::edges([f64::NAN, 1.0]),
617 Bins::edges_upper_inclusive([0.0, 0.0]),
618 ] {
619 assert!(s.by_column("d", &bins, &count).is_err(), "{bins:?}");
620 }
621 for radius in [f64::NAN, f64::INFINITY] {
622 assert!(s.rolling_by_column("d", radius, &count).is_err());
623 }
624 for centers in [[0.0, f64::NAN], [f64::NEG_INFINITY, 0.0]] {
625 assert!(s.rolling_by_column_at("d", 1.0, ¢ers, &count).is_err());
626 }
627 let twice = [
628 Agg::new("n", "d", Reducer::Count),
629 Agg::new("n", "v", Reducer::Count),
630 ];
631 assert!(matches!(
632 s.by_column("d", &Bins::width(1.0), &twice),
633 Err(PondError::DuplicateOutput { .. })
634 ));
635 assert!(matches!(
636 s.rolling_by_column("d", 1.0, &twice),
637 Err(PondError::DuplicateOutput { .. })
638 ));
639 let end = [Agg::new("end", "d", Reducer::Count)];
640 assert!(s.by_column("d", &Bins::width(1.0), &end).is_err());
641 assert!(s.rolling_by_column("d", 1.0, &end).is_ok());
643 }
644
645 #[test]
646 fn table_reads_columns_by_name() {
647 let s = track();
648 let t = s
649 .by_column(
650 "d",
651 &Bins::width(2.0),
652 &[Agg::new("n", "v", Reducer::Count)],
653 )
654 .unwrap();
655 assert_eq!((t.len(), t.is_empty()), (2, false));
656 let names: Vec<&str> = t.columns().iter().map(|c| c.name()).collect();
657 assert_eq!(names, ["start", "end", "n"]);
658 assert_eq!(
659 t.column("n").unwrap().numbers().unwrap().values(),
660 &[2.0, 1.0]
661 );
662 assert!(matches!(
663 t.column("nope"),
664 Err(PondError::UnknownColumn { .. })
665 ));
666 }
667
668 #[test]
671 fn huge_values_refused_only_when_read() {
672 let s = track();
673 let sum = [Agg::new("s", "v", Reducer::Sum)];
674 let err = s.rolling_by_column("d", 0.5, &sum).unwrap_err().to_string();
675 assert!(err.contains("row 1"), "{err}");
676 assert!(s.rolling_by_column_at("d", 0.5, &[3.0], &sum).is_err());
679 let t = s.rolling_by_column_at("d", 0.5, &[0.0], &sum).unwrap();
681 assert_eq!(t.column("s").unwrap().numbers().unwrap().values(), &[1.0]);
682 }
683}