1use std::collections::HashMap;
21
22use crate::column::{ColumnData, Value};
23use crate::error::{PondError, Result};
24use crate::grid::{Align, Keep, Materialize};
25use crate::par;
26use crate::reducer::{Agg, Reducer};
27use crate::rolling::{Rolling, RollingOver};
28use crate::rowwise::{Change, Fill, FillLimits};
29use crate::sequence::Sequence;
30use crate::series::{Column, ColumnKind, Key, Labels, TimeSeries};
31use crate::smooth::{Sample, Smooth};
32
33pub type PartitionKey = Vec<Option<Value>>;
35
36#[derive(Debug, Clone)]
59pub struct PartitionedSeries {
60 by: Vec<String>,
61 parts: Vec<(PartitionKey, TimeSeries)>,
64 run_order: Vec<usize>,
70 whole: Option<TimeSeries>,
73 empty: TimeSeries,
76}
77
78impl TimeSeries {
79 pub fn partition_by(&self, columns: &[&str]) -> Result<PartitionedSeries> {
84 const CALL: &str = "TimeSeries::partition_by";
85 if columns.is_empty() {
86 return Err(PondError::InvalidData {
87 call: CALL,
88 message: "needs at least one partition column".into(),
89 });
90 }
91 let by: Vec<usize> = columns
92 .iter()
93 .map(|c| self.column_index(CALL, c))
94 .collect::<Result<_>>()?;
95 refuse_array_keys(CALL, self, &by)?;
96 Ok(PartitionedSeries {
97 by: columns.iter().map(|c| c.to_string()).collect(),
98 run_order: Vec::new(),
99 parts: split(self, &by),
100 whole: Some(self.clone()),
101 empty: self.row_range(0..0),
102 })
103 }
104
105 pub fn group_by(&self, columns: &[&str]) -> Result<Vec<(PartitionKey, TimeSeries)>> {
109 Ok(self.partition_by(columns)?.into_groups())
110 }
111}
112
113impl PartitionedSeries {
114 pub fn by(&self) -> &[String] {
116 &self.by
117 }
118
119 pub fn len(&self) -> usize {
121 self.parts.len()
122 }
123
124 pub fn is_empty(&self) -> bool {
126 self.parts.is_empty()
127 }
128
129 pub fn iter(&self) -> impl ExactSizeIterator<Item = (&[Option<Value>], &TimeSeries)> {
132 self.parts.iter().map(|(k, s)| (k.as_slice(), s))
133 }
134
135 pub fn get(&self, key: &[Option<Value>]) -> Option<&TimeSeries> {
137 self.parts
138 .iter()
139 .find(|(k, _)| k.as_slice() == key)
140 .map(|(_, s)| s)
141 }
142
143 pub fn into_groups(self) -> Vec<(PartitionKey, TimeSeries)> {
145 self.parts
146 }
147
148 pub fn collect(&self) -> Result<TimeSeries> {
153 match &self.whole {
154 Some(whole) => Ok(whole.clone()),
155 None => {
156 let series: Vec<&TimeSeries> =
157 self.run_order.iter().map(|&i| &self.parts[i].1).collect();
158 merge(&series, &self.empty)
159 }
160 }
161 }
162
163 pub fn map<F>(&self, f: F) -> Result<PartitionedSeries>
172 where
173 F: Fn(&TimeSeries) -> Result<TimeSeries> + Sync + Send,
174 {
175 let (parts, empty) = self.run(&f)?;
176 let by_index: Vec<usize> = self
177 .by
178 .iter()
179 .map(|c| empty.column_index("PartitionedSeries", c))
180 .collect::<Result<_>>()?;
181 refuse_array_keys("PartitionedSeries", &empty, &by_index)?;
184 let unchanged =
187 !self.parts.is_empty() && parts.iter().all(|(key, s)| keeps_key(s, &by_index, key));
188 if !unchanged {
189 let whole = merge(&series_of(&parts), &empty)?;
192 return Ok(PartitionedSeries {
193 by: self.by.clone(),
194 parts: split(&whole, &by_index),
195 run_order: Vec::new(),
196 whole: Some(whole),
197 empty,
198 });
199 }
200 let mut ran: Vec<Option<(PartitionKey, TimeSeries)>> = parts
203 .into_iter()
204 .filter(|(_, s)| !s.is_empty())
205 .map(Some)
206 .collect();
207 let mut order: Vec<usize> = (0..ran.len()).collect();
208 order.sort_by_key(|&i| first_key(&ran[i].as_ref().expect("not taken yet").1));
209 let mut run_order = vec![0; ran.len()];
210 for (at, &i) in order.iter().enumerate() {
211 run_order[i] = at;
212 }
213 let parts = order
214 .iter()
215 .map(|&i| ran[i].take().expect("each taken once"))
216 .collect();
217 Ok(PartitionedSeries {
218 by: self.by.clone(),
219 parts,
220 run_order,
221 whole: None,
222 empty,
223 })
224 }
225
226 pub fn apply<F>(&self, f: F) -> Result<TimeSeries>
230 where
231 F: Fn(&TimeSeries) -> Result<TimeSeries> + Sync + Send,
232 {
233 let (parts, empty) = self.run(&f)?;
234 merge(&series_of(&parts), &empty)
235 }
236
237 pub fn diff(&self, columns: &[&str]) -> Result<PartitionedSeries> {
239 self.map(|g| g.diff(columns))
240 }
241
242 pub fn diff_with(&self, columns: &[&str], options: Change) -> Result<PartitionedSeries> {
244 self.map(|g| g.diff_with(columns, options.clone()))
245 }
246
247 pub fn rate(&self, columns: &[&str]) -> Result<PartitionedSeries> {
249 self.map(|g| g.rate(columns))
250 }
251
252 pub fn rate_with(&self, columns: &[&str], options: Change) -> Result<PartitionedSeries> {
254 self.map(|g| g.rate_with(columns, options.clone()))
255 }
256
257 pub fn pct_change(&self, columns: &[&str]) -> Result<PartitionedSeries> {
259 self.map(|g| g.pct_change(columns))
260 }
261
262 pub fn pct_change_with(&self, columns: &[&str], options: Change) -> Result<PartitionedSeries> {
264 self.map(|g| g.pct_change_with(columns, options.clone()))
265 }
266
267 pub fn shift(&self, columns: &[&str], n: i64) -> Result<PartitionedSeries> {
269 self.map(|g| g.shift(columns, n))
270 }
271
272 pub fn cumulative(&self, spec: &[(&str, Reducer)]) -> Result<PartitionedSeries> {
274 self.map(|g| g.cumulative(spec))
275 }
276
277 pub fn fill(&self, spec: &[(&str, Fill)]) -> Result<PartitionedSeries> {
279 self.map(|g| g.fill(spec))
280 }
281
282 pub fn fill_with(
284 &self,
285 spec: &[(&str, Fill)],
286 limits: &FillLimits,
287 ) -> Result<PartitionedSeries> {
288 self.map(|g| g.fill_with(spec, limits))
289 }
290
291 pub fn fill_all(&self, fill: Fill) -> Result<PartitionedSeries> {
293 self.map(|g| g.fill_all(fill.clone()))
294 }
295
296 pub fn fill_all_with(&self, fill: Fill, limits: &FillLimits) -> Result<PartitionedSeries> {
298 self.map(|g| g.fill_all_with(fill.clone(), limits))
299 }
300
301 pub fn rolling(&self, window: &str, aggs: &[Agg]) -> Result<PartitionedSeries> {
306 let aggs = self.with_partition_outputs(aggs);
307 self.map(|g| g.rolling(window, &aggs))
308 }
309
310 pub fn rolling_with(
317 &self,
318 window: &str,
319 aggs: &[Agg],
320 options: Rolling,
321 ) -> Result<PartitionedSeries> {
322 let aggs = self.rolling_outputs(aggs, options);
323 self.map(|g| g.rolling_with(window, &aggs, options))
324 }
325
326 pub fn rolling_rows(
329 &self,
330 rows: usize,
331 aggs: &[Agg],
332 options: Rolling,
333 ) -> Result<PartitionedSeries> {
334 let aggs = self.rolling_outputs(aggs, options);
335 self.map(|g| g.rolling_rows(rows, &aggs, options))
336 }
337
338 pub fn rolling_over(
343 &self,
344 sequence: &Sequence,
345 window: &str,
346 aggs: &[Agg],
347 options: RollingOver,
348 ) -> Result<PartitionedSeries> {
349 let aggs = self.with_partition_outputs(aggs);
350 self.map(|g| g.rolling_over(sequence, window, &aggs, options))
351 }
352
353 pub fn aggregate(&self, sequence: &Sequence, aggs: &[Agg]) -> Result<PartitionedSeries> {
358 let aggs = self.with_partition_outputs(aggs);
359 self.map(|g| g.aggregate(sequence, &aggs))
360 }
361
362 pub fn align(&self, sequence: &Sequence, options: Align) -> Result<PartitionedSeries> {
366 self.map(|g| g.align(sequence, options))
367 }
368
369 pub fn materialize(
373 &self,
374 sequence: &Sequence,
375 options: Materialize,
376 ) -> Result<PartitionedSeries> {
377 let by = self.by.clone();
378 self.map(move |g| {
379 let out = g.materialize(sequence, options)?;
380 if g.is_empty() {
381 return Ok(out);
382 }
383 let first = vec![Some(0); out.len()];
386 let mut columns = out.columns().to_vec();
387 for name in &by {
388 let at = out.column_index("materialize", name)?;
389 let source = g.column(name)?;
390 columns[at] = Column::from_data(name.clone(), source.data().take(&first), false);
391 }
392 Ok(out.with_value_columns(columns))
393 })
394 }
395
396 pub fn dedupe(&self, keep: Keep) -> Result<PartitionedSeries> {
399 self.map(|g| g.dedupe(keep.clone()))
400 }
401
402 pub fn smooth(&self, column: &str, smooth: &Smooth) -> Result<PartitionedSeries> {
405 self.map(|g| g.smooth(column, smooth))
406 }
407
408 pub fn baseline(
410 &self,
411 column: &str,
412 window: &str,
413 sigma: f64,
414 options: Rolling,
415 ) -> Result<PartitionedSeries> {
416 self.map(|g| g.baseline(column, window, sigma, options))
417 }
418
419 pub fn baseline_named(
421 &self,
422 column: &str,
423 window: &str,
424 sigma: f64,
425 options: Rolling,
426 names: [&str; 4],
427 ) -> Result<PartitionedSeries> {
428 self.map(|g| g.baseline_named(column, window, sigma, options, names))
429 }
430
431 pub fn outliers(
435 &self,
436 column: &str,
437 window: &str,
438 sigma: f64,
439 options: Rolling,
440 ) -> Result<PartitionedSeries> {
441 self.map(|g| g.outliers(column, window, sigma, options))
442 }
443
444 pub fn scan<A>(
449 &self,
450 column: &str,
451 init: A,
452 step: impl Fn(A, f64, usize) -> (A, f64) + Sync + Send,
453 ) -> Result<PartitionedSeries>
454 where
455 A: Clone + Sync + Send,
456 {
457 self.map(|g| g.scan(column, init.clone(), &step))
458 }
459
460 pub fn scan_to<A>(
463 &self,
464 column: &str,
465 output: &str,
466 init: A,
467 step: impl Fn(A, f64, usize) -> (A, f64) + Sync + Send,
468 ) -> Result<PartitionedSeries>
469 where
470 A: Clone + Sync + Send,
471 {
472 self.map(|g| g.scan_to(column, output, init.clone(), &step))
473 }
474
475 pub fn sample(&self, how: Sample) -> Result<PartitionedSeries> {
483 let Sample::Reservoir { size, seed } = how else {
484 return self.map(|g| g.sample(how));
485 };
486 let place: HashMap<usize, u64> = self
489 .parts
490 .iter()
491 .enumerate()
492 .map(|(i, (_, g))| (g as *const TimeSeries as usize, i as u64))
493 .collect();
494 self.map(|g| {
495 let i = place
496 .get(&(g as *const TimeSeries as usize))
497 .copied()
498 .unwrap_or(0);
499 let seed = seed ^ i.wrapping_mul(0x9E37_79B9_7F4A_7C15);
500 g.sample(Sample::Reservoir { size, seed })
501 })
502 }
503
504 fn rolling_outputs(&self, aggs: &[Agg], options: Rolling) -> Vec<Agg> {
508 if options.append {
509 aggs.to_vec()
510 } else {
511 self.with_partition_outputs(aggs)
512 }
513 }
514
515 fn with_partition_outputs(&self, aggs: &[Agg]) -> Vec<Agg> {
516 let mut out = aggs.to_vec();
517 for col in &self.by {
518 if !aggs.iter().any(|a| &a.output == col) && !out.iter().any(|a| &a.output == col) {
519 out.push(Agg::new(col.clone(), col.clone(), Reducer::First));
520 }
521 }
522 out
523 }
524
525 fn run<F>(&self, f: &F) -> Result<(Vec<(PartitionKey, TimeSeries)>, TimeSeries)>
528 where
529 F: Fn(&TimeSeries) -> Result<TimeSeries> + Sync + Send,
530 {
531 if self.parts.is_empty() {
532 let out = f(&self.empty)?;
533 let empty = out.row_range(0..0);
536 let parts = if out.is_empty() {
537 Vec::new()
538 } else {
539 vec![(Vec::new(), out)]
540 };
541 return Ok((parts, empty));
542 }
543 let results = par::map(&self.parts, |(_, s)| f(s));
544 let mut parts = Vec::with_capacity(results.len());
545 for ((key, _), out) in self.parts.iter().zip(results) {
546 parts.push((key.clone(), out?));
547 }
548 let empty = parts[0].1.row_range(0..0);
549 Ok((parts, empty))
550 }
551}
552
553fn refuse_array_keys(call: &'static str, series: &TimeSeries, by: &[usize]) -> Result<()> {
559 match by
560 .iter()
561 .find(|&&c| series.columns()[c].kind() == ColumnKind::Array)
562 {
563 Some(&c) => Err(PondError::WrongKind {
564 call,
565 column: series.columns()[c].name().to_string(),
566 kind: "array",
567 reason: "partitioning by an array column is not supported".into(),
568 }),
569 None => Ok(()),
570 }
571}
572
573fn split(series: &TimeSeries, by: &[usize]) -> Vec<(PartitionKey, TimeSeries)> {
574 let mut ids: Vec<u32> = Vec::new();
578 for (at, &c) in by.iter().enumerate() {
579 let next = value_ids(series.columns()[c].data());
580 ids = if at == 0 {
581 next
582 } else {
583 let mut pairs: HashMap<(u32, u32), u32> = HashMap::new();
584 ids.iter()
585 .zip(&next)
586 .map(|(&a, &b)| {
587 let id = pairs.len() as u32;
588 *pairs.entry((a, b)).or_insert(id)
589 })
590 .collect()
591 };
592 }
593 let mut part = vec![u32::MAX; ids.iter().max().map_or(0, |&m| m as usize + 1)];
596 let mut rows: Vec<Vec<usize>> = Vec::new();
597 for (i, &id) in ids.iter().enumerate() {
598 let slot = &mut part[id as usize];
599 if *slot == u32::MAX {
600 *slot = rows.len() as u32;
601 rows.push(Vec::new());
602 }
603 rows[*slot as usize].push(i);
604 }
605 par::map(&rows, |rows| {
606 let key = by
607 .iter()
608 .map(|&c| value_at(series.columns()[c].data(), rows[0]))
609 .collect();
610 (key, series.take_rows(rows))
611 })
612}
613
614fn value_ids(data: &ColumnData) -> Vec<u32> {
617 fn ids<K: std::hash::Hash + Eq>(values: impl Iterator<Item = Option<K>>) -> Vec<u32> {
618 let mut seen: HashMap<K, u32> = HashMap::new();
619 values
620 .map(|v| match v {
621 None => 0,
622 Some(v) => {
623 let next = seen.len() as u32 + 1;
624 *seen.entry(v).or_insert(next)
625 }
626 })
627 .collect()
628 }
629 match data {
630 ColumnData::Number(c) => ids(c.iter().map(|v| v.map(|v| (v + 0.0).to_bits()))),
632 ColumnData::Decimal(c) => ids(c.iter().map(|v| v.map(|d| d.value))),
633 ColumnData::String(c) => ids(c.iter()),
634 ColumnData::Boolean(c) => ids(c.iter()),
635 ColumnData::Array(_) => unreachable!("partition_by refuses array columns"),
636 }
637}
638
639fn value_at(data: &ColumnData, i: usize) -> Option<Value> {
640 match data {
641 ColumnData::Number(c) => c.get(i).map(|v| Value::Number(v + 0.0)),
642 ColumnData::Decimal(c) => c.get(i).map(Value::Decimal),
643 ColumnData::String(c) => c.get(i).map(|v| Value::String(v.to_string())),
644 ColumnData::Boolean(c) => c.get(i).map(Value::Boolean),
645 ColumnData::Array(_) => unreachable!("partition_by refuses array columns"),
646 }
647}
648
649fn keeps_key(series: &TimeSeries, by: &[usize], key: &[Option<Value>]) -> bool {
652 by.iter().zip(key).all(|(&c, want)| {
653 let n = series.len();
654 match (series.columns()[c].data(), want) {
655 (ColumnData::Number(c), Some(Value::Number(v))) => (0..n).all(|i| c.get(i) == Some(*v)),
657 (ColumnData::Decimal(c), Some(Value::Decimal(v))) => {
658 (0..n).all(|i| c.get(i) == Some(*v))
659 }
660 (ColumnData::String(c), Some(Value::String(v))) => {
661 (0..n).all(|i| c.get(i) == Some(v.as_str()))
662 }
663 (ColumnData::Boolean(c), Some(Value::Boolean(v))) => {
664 (0..n).all(|i| c.get(i) == Some(*v))
665 }
666 (data, None) => data.missing_count() == n,
667 _ => false,
668 }
669 })
670}
671
672fn first_key(series: &TimeSeries) -> (i64, i64) {
673 let key = series.key();
674 (key.begin()[0], key.end().map_or(0, |e| e[0]))
675}
676
677fn series_of(parts: &[(PartitionKey, TimeSeries)]) -> Vec<&TimeSeries> {
678 parts.iter().map(|(_, s)| s).collect()
679}
680
681fn merge(series: &[&TimeSeries], empty: &TimeSeries) -> Result<TimeSeries> {
684 let series: Vec<&TimeSeries> = series.iter().copied().filter(|s| !s.is_empty()).collect();
685 let Some(head) = series.first() else {
686 return Ok(empty.clone());
687 };
688 if series.len() == 1 {
689 return Ok((*head).clone());
690 }
691 if let Some(why) = mismatch(&series) {
692 return Err(PondError::InvalidData {
693 call: "PartitionedSeries",
694 message: format!(
695 "partitions came back with different columns ({why}); every partition must give the same columns"
696 ),
697 });
698 }
699 Ok(stack(head.name(), &series))
700}
701
702fn mismatch(series: &[&TimeSeries]) -> Option<String> {
705 let head = series[0];
706 for (i, s) in series.iter().enumerate().skip(1) {
707 if s.key_name() != head.key_name() || s.key().kind() != head.key().kind() {
708 return Some(format!(
709 "series {i} is keyed by '{}' ({:?}), series 0 by '{}' ({:?})",
710 s.key_name(),
711 s.key().kind(),
712 head.key_name(),
713 head.key().kind()
714 ));
715 }
716 if let (Key::Interval(a), Key::Interval(b)) = (head.key(), s.key())
717 && std::mem::discriminant(&a.label) != std::mem::discriminant(&b.label)
718 {
719 return Some(format!(
720 "series {i} has {} interval labels, series 0 {}",
721 label_kind(&b.label),
722 label_kind(&a.label)
723 ));
724 }
725 if s.columns().len() != head.columns().len() {
726 return Some(format!(
727 "series {i} has {} columns, series 0 has {}",
728 s.columns().len(),
729 head.columns().len()
730 ));
731 }
732 for (c, (a, b)) in head.columns().iter().zip(s.columns()).enumerate() {
733 if a.name() != b.name() || a.kind() != b.kind() {
734 return Some(format!(
735 "column {c} is '{}' ({:?}) in series {i} but '{}' ({:?}) in series 0",
736 b.name(),
737 b.kind(),
738 a.name(),
739 a.kind()
740 ));
741 }
742 if let (ColumnData::Decimal(x), ColumnData::Decimal(y)) = (a.data(), b.data())
743 && x.scale() != y.scale()
744 {
745 return Some(format!(
746 "decimal column '{}' has scale {} in series {i} but {} in series 0",
747 a.name(),
748 y.scale(),
749 x.scale()
750 ));
751 }
752 }
753 }
754 None
755}
756
757fn label_kind(labels: &Labels) -> &'static str {
758 match labels {
759 Labels::Number(_) => "number",
760 Labels::String(_) => "text",
761 }
762}
763
764fn stack(name: &str, series: &[&TimeSeries]) -> TimeSeries {
768 let head = series[0];
769 let picks = merge_order(series);
770 let keys: Vec<&Key> = series.iter().map(|s| s.key()).collect();
771 let key = Key::gather(&keys, &picks);
772 let columns: Vec<usize> = (0..head.columns().len()).collect();
773 let columns = par::map(&columns, |&c| {
774 let sources: Vec<&ColumnData> = series.iter().map(|s| s.columns()[c].data()).collect();
775 let required = series.iter().all(|s| s.columns()[c].is_required());
776 Column::from_data(
777 head.columns()[c].name().to_string(),
778 ColumnData::gather(&sources, &picks),
779 required,
780 )
781 });
782 TimeSeries::from_parts(name.to_string(), head.key_name().to_string(), key, columns)
783}
784
785impl TimeSeries {
786 pub fn concat(series: &[&TimeSeries]) -> Result<TimeSeries> {
806 const CALL: &str = "TimeSeries::concat";
807 let Some(head) = series.first() else {
808 return Err(PondError::InvalidData {
809 call: CALL,
810 message: "needs at least one series".into(),
811 });
812 };
813 if let Some(why) = mismatch(series) {
814 return Err(PondError::InvalidData {
815 call: CALL,
816 message: format!("the series do not match: {why}"),
817 });
818 }
819 if series.len() == 1 {
820 return Ok((*head).clone());
821 }
822 Ok(stack(head.name(), series))
823 }
824}
825
826fn merge_order(series: &[&TimeSeries]) -> Vec<(u32, u32)> {
832 let total: usize = series.iter().map(|s| s.len()).sum();
833 let mut picks: Vec<(u32, u32)> = Vec::with_capacity(total);
834 for (p, s) in series.iter().enumerate() {
835 picks.extend((0..s.len() as u32).map(|r| (p as u32, r)));
836 }
837 let flat = |part: &dyn Fn(&Key) -> Option<&[i64]>| -> Option<Vec<u64>> {
839 let mut out = Vec::with_capacity(total);
840 for s in series {
841 out.extend(part(s.key())?.iter().map(|&v| (v as u64) ^ (1 << 63)));
842 }
843 Some(out)
844 };
845 let mut order: Vec<u32> = (0..total as u32).collect();
846 if let Some(end) = flat(&|k| k.end()) {
848 radix_sort(&mut order, &end);
849 }
850 radix_sort(
851 &mut order,
852 &flat(&|k| Some(k.begin())).expect("every key has a begin"),
853 );
854 order.into_iter().map(|i| picks[i as usize]).collect()
855}
856
857fn radix_sort(order: &mut Vec<u32>, keys: &[u64]) {
860 let Some(&first) = keys.first() else {
861 return;
862 };
863 let varying = keys.iter().fold(0, |acc, &k| acc | (k ^ first));
864 let mut buf = vec![0u32; order.len()];
865 for shift in (0..64).step_by(16) {
866 if (varying >> shift) & 0xFFFF == 0 {
867 continue;
868 }
869 let digit = |i: u32| ((keys[i as usize] >> shift) & 0xFFFF) as usize;
870 let mut start = vec![0usize; 1 << 16];
871 for &i in order.iter() {
872 start[digit(i)] += 1;
873 }
874 let mut at = 0;
875 for slot in start.iter_mut() {
876 let count = *slot;
877 *slot = at;
878 at += count;
879 }
880 for &i in order.iter() {
881 let d = digit(i);
882 buf[start[d]] = i;
883 start[d] += 1;
884 }
885 std::mem::swap(order, &mut buf);
886 }
887}
888
889#[cfg(test)]
890mod tests {
891 use super::*;
892 use crate::series::{MAX_TIME_MS, TimeRangeKeys};
893
894 fn hosts() -> TimeSeries {
895 TimeSeries::new(
896 "h",
897 vec![0, 0, 1000, 1000, 2000],
898 vec![
899 Column::optional(
900 "host",
901 vec![Some("b"), Some("a"), Some("a"), Some("b"), None],
902 ),
903 Column::optional(
904 "v",
905 vec![Some(1.0), Some(10.0), Some(12.0), None, Some(5.0)],
906 ),
907 ],
908 )
909 .unwrap()
910 }
911
912 #[test]
913 fn splits_in_first_row_order_with_a_gap_partition() {
914 let p = hosts().partition_by(&["host"]).unwrap();
915 let keys: Vec<_> = p.iter().map(|(k, s)| (k.to_vec(), s.len())).collect();
916 assert_eq!(
917 keys,
918 vec![
919 (vec![Some(Value::String("b".into()))], 2),
920 (vec![Some(Value::String("a".into()))], 2),
921 (vec![None], 1),
922 ]
923 );
924 assert_eq!(p.collect().unwrap(), hosts());
926 }
927
928 #[test]
929 fn merge_puts_ties_in_partition_order() {
930 let p = hosts().partition_by(&["host"]).unwrap();
931 let out = p.map(|g| Ok(g.clone())).unwrap().collect().unwrap();
932 let hosts: Vec<_> = out
933 .column("host")
934 .unwrap()
935 .strings()
936 .unwrap()
937 .iter()
938 .collect();
939 assert_eq!(
941 hosts,
942 vec![Some("b"), Some("a"), Some("b"), Some("a"), None]
943 );
944 }
945
946 #[test]
947 fn merge_order_is_a_stable_sort() {
948 let mut x = 0x9e37_79b9_7f4a_7c15u64;
952 let mut next = move || {
953 x ^= x << 13;
954 x ^= x >> 7;
955 x ^= x << 17;
956 x
957 };
958 let pick = [
959 -MAX_TIME_MS,
960 -5_000_000_000_000,
961 -1,
962 0,
963 1,
964 7,
965 1 << 40,
966 MAX_TIME_MS - 2,
967 ];
968 for ranges in [false, true] {
969 let parts: Vec<TimeSeries> = (0..6)
970 .map(|_| {
971 let n = (next() % 40) as usize;
972 let mut keys: Vec<(i64, i64)> = (0..n)
973 .map(|_| {
974 let b = pick[(next() % 8) as usize];
975 let e = b.saturating_add((next() % 3) as i64);
976 (b, if ranges { e } else { b })
977 })
978 .collect();
979 keys.sort();
980 let (begin, end): (Vec<i64>, Vec<i64>) = keys.into_iter().unzip();
981 let cols = vec![Column::required("v", vec![0.0; begin.len()])];
982 if ranges {
983 TimeSeries::from_time_ranges("t", TimeRangeKeys { begin, end }, cols)
984 } else {
985 TimeSeries::new("t", begin, cols)
986 }
987 .unwrap()
988 })
989 .collect();
990 let refs: Vec<&TimeSeries> = parts.iter().collect();
991 let mut want: Vec<(u32, u32)> = Vec::new();
992 for (p, s) in parts.iter().enumerate() {
993 want.extend((0..s.len() as u32).map(|r| (p as u32, r)));
994 }
995 want.sort_by_key(|&(p, r)| {
996 let k = parts[p as usize].key();
997 (k.begin()[r as usize], k.end().map_or(0, |e| e[r as usize]))
998 });
999 assert_eq!(merge_order(&refs), want);
1000 }
1001 }
1002
1003 #[test]
1004 fn negative_zero_is_zero() {
1005 let s = TimeSeries::new(
1006 "z",
1007 vec![0, 1],
1008 vec![Column::required("k", vec![0.0, -0.0])],
1009 )
1010 .unwrap();
1011 assert_eq!(s.partition_by(&["k"]).unwrap().len(), 1);
1012 }
1013
1014 #[test]
1015 fn changed_partition_values_split_again() {
1016 let s = TimeSeries::new(
1017 "s",
1018 vec![0, 1, 2, 3],
1019 vec![Column::required("k", vec![1.0, 2.0, 1.0, 2.0])],
1020 )
1021 .unwrap();
1022 let p = s.partition_by(&["k"]).unwrap();
1023 let q = p.map(|g| g.map_numbers("k", |_| 7.0)).unwrap();
1025 assert_eq!(q.len(), 1);
1026 assert_eq!(q.iter().next().unwrap().0, &[Some(Value::Number(7.0))]);
1027 assert_eq!(q.collect().unwrap().len(), 4);
1028 }
1029
1030 #[test]
1031 fn errors() {
1032 let s = hosts();
1033 assert!(s.partition_by(&[]).is_err());
1034 assert!(matches!(
1035 s.partition_by(&["nope"]).unwrap_err(),
1036 PondError::UnknownColumn { .. }
1037 ));
1038 let p = s.partition_by(&["host"]).unwrap();
1039 assert!(p.map(|g| g.select(&["v"])).is_err());
1042 assert_eq!(p.apply(|g| g.select(&["v"])).unwrap().len(), 5);
1043 let err = p
1045 .map(|g| {
1046 if g.len() == 1 {
1047 g.select(&["zz"])
1048 } else {
1049 Ok(g.clone())
1050 }
1051 })
1052 .unwrap_err();
1053 assert!(matches!(err, PondError::UnknownColumn { .. }));
1054 }
1055
1056 #[test]
1057 fn different_columns_cannot_merge() {
1058 let p = hosts().partition_by(&["host"]).unwrap();
1059 let err = p
1060 .apply(|g| {
1061 if g.len() == 1 {
1062 g.select(&["v"])
1063 } else {
1064 g.select(&["host"])
1065 }
1066 })
1067 .unwrap_err();
1068 assert!(err.to_string().contains("different columns"), "{err}");
1069 }
1070
1071 #[test]
1072 fn empty_series() {
1073 let s = hosts().row_range(0..0);
1074 let p = s.partition_by(&["host"]).unwrap();
1075 assert!(p.is_empty());
1076 let out = p.diff(&["v"]).unwrap().collect().unwrap();
1077 assert!(out.is_empty());
1078 assert_eq!(out.columns().len(), 2);
1079 }
1080
1081 #[test]
1082 fn rows_made_from_an_empty_series_are_split() {
1083 let p = hosts().row_range(0..0).partition_by(&["host"]).unwrap();
1086 let made = p.map(|_| Ok(hosts())).unwrap();
1087 assert_eq!(made.len(), 3);
1088 let a = made.get(&[Some(Value::String("a".into()))]).unwrap();
1089 assert_eq!(a.len(), 2);
1090 let d = made.diff(&["v"]).unwrap();
1091 let a = d.get(&[Some(Value::String("a".into()))]).unwrap();
1092 assert_eq!(a.column("v").unwrap().numbers().unwrap().get(1), Some(2.0));
1093 }
1094
1095 #[test]
1096 fn group_by_gives_keys_and_series() {
1097 let groups = hosts().group_by(&["host"]).unwrap();
1098 assert_eq!(groups.len(), 3);
1099 assert_eq!(
1100 groups[1].1.column("v").unwrap().numbers().unwrap().get(1),
1101 Some(12.0)
1102 );
1103 }
1104
1105 #[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
1108 #[test]
1109 fn same_bits_on_one_and_many_threads() {
1110 let mut x = 0x9e37_79b9_7f4a_7c15u64;
1111 let mut next = || {
1112 x ^= x << 13;
1113 x ^= x >> 7;
1114 x ^= x << 17;
1115 x
1116 };
1117 let (symbols, bars) = (40, 300);
1118 let mut time = Vec::new();
1119 let mut sym = Vec::new();
1120 let mut v = Vec::new();
1121 for b in 0..bars {
1122 for s in 0..symbols {
1123 time.push(b as i64 * 60_000);
1124 sym.push(format!("s{s}"));
1125 let r = next();
1126 v.push((r % 13 != 0).then(|| (r % 100_003) as f64 / 7.0));
1127 }
1128 }
1129 let series = TimeSeries::new(
1130 "panel",
1131 time,
1132 vec![Column::required("sym", sym), Column::optional("v", v)],
1133 )
1134 .unwrap();
1135 let aggs = [
1136 Agg::of("v", Reducer::Avg),
1137 Agg::new("sd", "v", Reducer::Stdev),
1138 ];
1139 let run = |threads: usize| {
1140 rayon::ThreadPoolBuilder::new()
1141 .num_threads(threads)
1142 .build()
1143 .unwrap()
1144 .install(|| {
1145 let p = series.partition_by(&["sym"]).unwrap();
1146 let a = p.rolling("10m", &aggs).unwrap().collect().unwrap();
1147 let b = p
1148 .fill_all(Fill::Linear)
1149 .unwrap()
1150 .cumulative(&[("v", Reducer::Sum)])
1151 .unwrap()
1152 .collect()
1153 .unwrap();
1154 [a, b]
1155 .iter()
1156 .flat_map(|s| {
1157 s.columns()
1158 .iter()
1159 .filter_map(|c| c.numbers().ok())
1160 .flat_map(|c| c.values().iter().map(|v| v.to_bits()))
1161 .collect::<Vec<u64>>()
1162 })
1163 .collect::<Vec<u64>>()
1164 })
1165 };
1166 let one = run(1);
1167 assert!(one.len() > 30_000);
1168 assert_eq!(one, run(4));
1169 assert_eq!(one, run(7));
1170 }
1171
1172 #[test]
1173 fn reservoir_and_scan_run_per_partition() {
1174 let n = 40;
1176 let s = TimeSeries::new(
1177 "r",
1178 (0..n as i64).map(|i| i * 1000).collect(),
1179 vec![
1180 Column::required(
1181 "host",
1182 (0..n)
1183 .map(|i| if i % 2 == 0 { "a" } else { "b" })
1184 .collect::<Vec<_>>(),
1185 ),
1186 Column::required("v", (0..n).map(|i| i as f64).collect::<Vec<_>>()),
1187 ],
1188 )
1189 .unwrap();
1190 let p = s.partition_by(&["host"]).unwrap();
1191 let how = Sample::Reservoir { size: 5, seed: 3 };
1192 let picked = p.sample(how).unwrap();
1193 let v_of = |g: &TimeSeries| -> Vec<f64> {
1195 g.column("v")
1196 .unwrap()
1197 .numbers()
1198 .unwrap()
1199 .iter()
1200 .flatten()
1201 .collect()
1202 };
1203 let key = |h: &str| vec![Some(Value::String(h.into()))];
1204 let rows = [
1205 v_of(picked.get(&key("a")).unwrap()),
1206 v_of(picked.get(&key("b")).unwrap()),
1207 ];
1208 assert_eq!(picked.len(), 2);
1209 assert!(rows.iter().all(|r| r.len() == 5));
1213 assert!(rows[0].iter().all(|v| (*v as usize).is_multiple_of(2)));
1214 assert!(rows[1].iter().all(|v| *v as usize % 2 == 1));
1215 let positions = |r: &[f64]| r.iter().map(|v| (*v as usize) / 2).collect::<Vec<_>>();
1216 let alone = v_of(&p.get(&key("a")).unwrap().sample(how).unwrap());
1217 assert_eq!(rows[0], alone);
1218 assert_ne!(positions(&rows[0]), positions(&rows[1]));
1219 assert_eq!(p.sample(how).unwrap().collect(), picked.collect());
1220
1221 let counted = p
1223 .scan_to("v", "k", 100.0, |acc, _, i| (acc + 1.0, acc + i as f64))
1224 .unwrap();
1225 for (_, g) in counted.iter() {
1226 let k: Vec<f64> = g
1227 .column("k")
1228 .unwrap()
1229 .numbers()
1230 .unwrap()
1231 .iter()
1232 .flatten()
1233 .collect();
1234 assert_eq!(
1235 k,
1236 (0..20).map(|i| 100.0 + 2.0 * i as f64).collect::<Vec<_>>()
1237 );
1238 }
1239 }
1240}