Skip to main content

pond_rs/
series.rs

1//! `TimeSeries`: a key column (points in time, time ranges, or labelled
2//! intervals) plus named value columns (numbers, text, true/false), all the
3//! same length.
4
5use crate::column::{
6    ArrayColumn, BooleanColumn, ColumnData, DecimalColumn, Float64Column, StringColumn,
7};
8use crate::error::{PondError, Result};
9use std::sync::Arc;
10
11/// What a schema column holds.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ColumnKind {
14    /// Key: a point in time, epoch milliseconds.
15    Time,
16    /// Key: a `[begin, end]` span, epoch milliseconds.
17    TimeRange,
18    /// Key: a labelled `[begin, end)` span, epoch milliseconds.
19    Interval,
20    /// Key of a [`ValueSeries`](crate::ValueSeries): a number axis
21    /// (distance, strike, frequency), not a time.
22    Value,
23    /// A value column of `f64`.
24    Number,
25    /// A value column of exact decimals (pond-rs only; pond-ts has none).
26    Decimal,
27    /// A value column of text.
28    String,
29    /// A value column of true/false values.
30    Boolean,
31    /// A value column of arrays of numbers, text and true/false values
32    /// ([`ArrayColumn`](crate::ArrayColumn)).
33    Array,
34}
35
36impl ColumnKind {
37    /// The name pond-ts uses in a schema (`"time"`, `"timeRange"`,
38    /// `"interval"`, `"value"`, `"number"`, `"string"`, `"boolean"`, `"array"`), and `"decimal"`,
39    /// which pond-ts does not have.
40    pub fn as_str(self) -> &'static str {
41        match self {
42            ColumnKind::Time => "time",
43            ColumnKind::TimeRange => "timeRange",
44            ColumnKind::Interval => "interval",
45            ColumnKind::Value => "value",
46            ColumnKind::Number => "number",
47            ColumnKind::Decimal => "decimal",
48            ColumnKind::String => "string",
49            ColumnKind::Boolean => "boolean",
50            ColumnKind::Array => "array",
51        }
52    }
53}
54
55/// One schema entry. `required: false` means the column may have gaps.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ColumnDef {
58    /// The column name.
59    pub name: String,
60    /// What the column holds.
61    pub kind: ColumnKind,
62    /// `false` when the column may have gaps.
63    pub required: bool,
64}
65
66impl ColumnDef {
67    /// A column that may not have gaps (pond-ts's default).
68    pub fn required(name: impl Into<String>, kind: ColumnKind) -> Self {
69        Self {
70            name: name.into(),
71            kind,
72            required: true,
73        }
74    }
75
76    /// A column that may have gaps (pond-ts `required: false`).
77    pub fn optional(name: impl Into<String>, kind: ColumnKind) -> Self {
78        Self {
79            name: name.into(),
80            kind,
81            required: false,
82        }
83    }
84}
85
86/// Time-range keys (pond-ts `timeRange`): row `i` spans `begin[i]` to
87/// `end[i]`, epoch milliseconds.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct TimeRangeKeys {
90    /// Starts, epoch milliseconds.
91    pub begin: Vec<i64>,
92    /// Ends, epoch milliseconds.
93    pub end: Vec<i64>,
94}
95
96/// Interval keys: row `i` covers `[begin[i], end[i])` and is labelled
97/// `label[i]` (for buckets made by a sequence, the label is the start).
98#[derive(Debug, Clone, PartialEq)]
99pub struct IntervalKeys {
100    /// One label per row.
101    pub label: Labels,
102    /// Starts, epoch milliseconds (inclusive).
103    pub begin: Vec<i64>,
104    /// Ends, epoch milliseconds (exclusive).
105    pub end: Vec<i64>,
106}
107
108/// Interval labels: numbers or text, one kind for the whole key, as in
109/// pond-ts. Every row has a label (no gaps). Build them with `.into()` from
110/// `Vec<f64>`, `Vec<i64>`, `Vec<&str>` or `Vec<String>`.
111#[derive(Debug, Clone, PartialEq)]
112pub enum Labels {
113    /// Number labels. `aggregate` labels each bucket with its start, in
114    /// epoch milliseconds. Must be finite.
115    Number(Vec<f64>),
116    /// Text labels.
117    String(StringColumn),
118}
119
120impl Labels {
121    /// Number of values.
122    pub fn len(&self) -> usize {
123        match self {
124            Labels::Number(v) => v.len(),
125            Labels::String(c) => c.len(),
126        }
127    }
128
129    /// `true` when there are no rows.
130    pub fn is_empty(&self) -> bool {
131        self.len() == 0
132    }
133
134    /// Number labels, or `None` for text labels.
135    pub fn numbers(&self) -> Option<&[f64]> {
136        match self {
137            Labels::Number(v) => Some(v),
138            Labels::String(_) => None,
139        }
140    }
141
142    /// Text labels, or `None` for number labels.
143    pub fn strings(&self) -> Option<&StringColumn> {
144        match self {
145            Labels::Number(_) => None,
146            Labels::String(c) => Some(c),
147        }
148    }
149}
150
151impl From<Vec<f64>> for Labels {
152    fn from(v: Vec<f64>) -> Self {
153        Labels::Number(v)
154    }
155}
156
157/// Exact for whole numbers up to 2^53 (every key time is): larger values
158/// round to the nearest `f64`.
159impl From<Vec<i64>> for Labels {
160    fn from(v: Vec<i64>) -> Self {
161        Labels::Number(v.into_iter().map(|x| x as f64).collect())
162    }
163}
164
165impl From<Vec<&str>> for Labels {
166    fn from(v: Vec<&str>) -> Self {
167        Labels::String(StringColumn::from_options(v.into_iter().map(Some)))
168    }
169}
170
171impl From<Vec<String>> for Labels {
172    fn from(v: Vec<String>) -> Self {
173        Labels::String(StringColumn::from_options(v.into_iter().map(Some)))
174    }
175}
176
177impl From<StringColumn> for Labels {
178    fn from(c: StringColumn) -> Self {
179        Labels::String(c)
180    }
181}
182
183/// The key column.
184#[derive(Debug, Clone, PartialEq)]
185pub enum Key {
186    /// Points in time, epoch milliseconds, non-decreasing.
187    Time(Vec<i64>),
188    /// Time ranges, ordered by begin, then end.
189    TimeRange(TimeRangeKeys),
190    /// Labelled intervals, ordered by begin, then end.
191    Interval(IntervalKeys),
192}
193
194impl Key {
195    /// Number of values.
196    pub fn len(&self) -> usize {
197        self.begin().len()
198    }
199
200    /// `true` when there are no rows.
201    pub fn is_empty(&self) -> bool {
202        self.len() == 0
203    }
204
205    /// Start of each row (the time itself for point keys).
206    pub fn begin(&self) -> &[i64] {
207        match self {
208            Key::Time(t) => t,
209            Key::TimeRange(k) => &k.begin,
210            Key::Interval(k) => &k.begin,
211        }
212    }
213
214    /// End of each row, or `None` for point keys.
215    pub fn end(&self) -> Option<&[i64]> {
216        match self {
217            Key::Time(_) => None,
218            Key::TimeRange(k) => Some(&k.end),
219            Key::Interval(k) => Some(&k.end),
220        }
221    }
222
223    /// The rows in `range`, copied.
224    pub(crate) fn rows(&self, range: std::ops::Range<usize>) -> Key {
225        match self {
226            Key::Time(t) => Key::Time(t[range].to_vec()),
227            Key::TimeRange(k) => Key::TimeRange(TimeRangeKeys {
228                begin: k.begin[range.clone()].to_vec(),
229                end: k.end[range].to_vec(),
230            }),
231            Key::Interval(k) => Key::Interval(IntervalKeys {
232                label: match &k.label {
233                    Labels::Number(v) => Labels::Number(v[range.clone()].to_vec()),
234                    Labels::String(s) => {
235                        let rows: Vec<Option<usize>> = range.clone().map(Some).collect();
236                        Labels::String(s.take(&rows))
237                    }
238                },
239                begin: k.begin[range.clone()].to_vec(),
240                end: k.end[range].to_vec(),
241            }),
242        }
243    }
244
245    /// Rows picked from several keys of this kind: `(source, row)` pairs,
246    /// in order.
247    pub(crate) fn gather(sources: &[&Key], picks: &[(u32, u32)]) -> Key {
248        let pick = |v: &dyn Fn(&Key) -> &[i64]| -> Vec<i64> {
249            let cols: Vec<&[i64]> = sources.iter().map(|k| v(k)).collect();
250            picks
251                .iter()
252                .map(|&(s, r)| cols[s as usize][r as usize])
253                .collect()
254        };
255        let begin = pick(&|k| k.begin());
256        match sources[0] {
257            Key::Time(_) => Key::Time(begin),
258            Key::TimeRange(_) => Key::TimeRange(TimeRangeKeys {
259                begin,
260                end: pick(&|k| k.end().expect("same kind")),
261            }),
262            Key::Interval(first) => {
263                let labels: Vec<&Labels> = sources
264                    .iter()
265                    .map(|k| match k {
266                        Key::Interval(i) => &i.label,
267                        _ => unreachable!("same kind"),
268                    })
269                    .collect();
270                let label = match &first.label {
271                    Labels::Number(_) => Labels::Number(
272                        picks
273                            .iter()
274                            .map(|&(s, r)| {
275                                labels[s as usize].numbers().expect("same kind")[r as usize]
276                            })
277                            .collect(),
278                    ),
279                    Labels::String(_) => {
280                        Labels::String(StringColumn::from_options(picks.iter().map(|&(s, r)| {
281                            labels[s as usize]
282                                .strings()
283                                .expect("same kind")
284                                .get(r as usize)
285                        })))
286                    }
287                };
288                Key::Interval(IntervalKeys {
289                    label,
290                    begin,
291                    end: pick(&|k| k.end().expect("same kind")),
292                })
293            }
294        }
295    }
296
297    /// The rows at `rows`, in that order, copied.
298    pub(crate) fn take(&self, rows: &[usize]) -> Key {
299        let pick = |v: &[i64]| rows.iter().map(|&i| v[i]).collect::<Vec<i64>>();
300        match self {
301            Key::Time(t) => Key::Time(pick(t)),
302            Key::TimeRange(k) => Key::TimeRange(TimeRangeKeys {
303                begin: pick(&k.begin),
304                end: pick(&k.end),
305            }),
306            Key::Interval(k) => Key::Interval(IntervalKeys {
307                label: match &k.label {
308                    Labels::Number(v) => Labels::Number(rows.iter().map(|&i| v[i]).collect()),
309                    Labels::String(s) => {
310                        let picks: Vec<Option<usize>> = rows.iter().map(|&i| Some(i)).collect();
311                        Labels::String(s.take(&picks))
312                    }
313                },
314                begin: pick(&k.begin),
315                end: pick(&k.end),
316            }),
317        }
318    }
319
320    pub(crate) fn kind(&self) -> ColumnKind {
321        match self {
322            Key::Time(_) => ColumnKind::Time,
323            Key::TimeRange(_) => ColumnKind::TimeRange,
324            Key::Interval(_) => ColumnKind::Interval,
325        }
326    }
327}
328
329/// A named value column: numbers, text or true/false values. Handed to
330/// [`TimeSeries::new`], and read back with [`TimeSeries::column`].
331///
332/// ```
333/// # use pond_rs::{Column, ColumnKind, TimeSeries};
334/// let s = TimeSeries::new("hosts", vec![0, 1000], vec![
335///     Column::required("cpu", vec![0.5, 0.7]),
336///     Column::optional("host", vec![Some("a"), None]),
337///     Column::required("up", vec![true, false]),
338/// ])?;
339/// assert_eq!(s.column("host")?.kind(), ColumnKind::String);
340/// assert_eq!(s.column("host")?.strings()?.get(0), Some("a"));
341/// assert!(s.column("host")?.numbers().is_err()); // not a number column
342/// # Ok::<(), pond_rs::PondError>(())
343/// ```
344#[derive(Debug, Clone, PartialEq)]
345pub struct Column {
346    pub(crate) name: String,
347    pub(crate) required: bool,
348    pub(crate) data: ColumnData,
349}
350
351impl Column {
352    /// A column with no gaps: `Vec<f64>`, `Vec<&str>`, `Vec<String>`,
353    /// `Vec<bool>` (or an array of them, or a built column). A gap (a
354    /// non-finite number, or a `None`) is rejected when the series is built.
355    pub fn required(name: impl Into<String>, values: impl Into<ColumnData>) -> Self {
356        Self {
357            name: name.into(),
358            required: true,
359            data: values.into(),
360        }
361    }
362
363    /// A column that may have gaps: as [`Column::required`], or with
364    /// `Option` values (`vec![Some(1.0), None]`, `[Some("a"), None]`) where
365    /// `None` is a gap. A non-finite number is a gap too.
366    pub fn optional(name: impl Into<String>, values: impl Into<ColumnData>) -> Self {
367        Self {
368            name: name.into(),
369            required: false,
370            data: values.into(),
371        }
372    }
373
374    /// A number column that may have gaps, given as raw `f64` where any
375    /// non-finite value (`NaN`, `±inf`) is a gap. The same as
376    /// [`Column::optional`] with a `Vec<f64>`.
377    pub fn optional_raw(name: impl Into<String>, values: Vec<f64>) -> Self {
378        Self::optional(name, values)
379    }
380
381    /// A column around existing storage, without copying.
382    pub(crate) fn from_data(name: String, data: impl Into<ColumnData>, required: bool) -> Self {
383        Self {
384            name,
385            required,
386            data: data.into(),
387        }
388    }
389
390    /// The column name.
391    pub fn name(&self) -> &str {
392        &self.name
393    }
394
395    /// `true` when the column may not have gaps.
396    pub fn is_required(&self) -> bool {
397        self.required
398    }
399
400    /// What the column holds: [`ColumnKind::Number`], `Decimal`, `String`,
401    /// `Boolean` or `Array`.
402    pub fn kind(&self) -> ColumnKind {
403        self.data.kind()
404    }
405
406    /// The values, whichever kind they are.
407    pub fn data(&self) -> &ColumnData {
408        &self.data
409    }
410
411    /// The values of a number column; an error for any other kind.
412    pub fn numbers(&self) -> Result<&Float64Column> {
413        match &self.data {
414            ColumnData::Number(c) => Ok(c),
415            _ => Err(self.wrong_kind("Column::numbers", "number")),
416        }
417    }
418
419    /// The values of a text column; an error for any other kind.
420    pub fn strings(&self) -> Result<&StringColumn> {
421        match &self.data {
422            ColumnData::String(c) => Ok(c),
423            _ => Err(self.wrong_kind("Column::strings", "string")),
424        }
425    }
426
427    /// The values of a decimal column; an error for any other kind.
428    pub fn decimals(&self) -> Result<&DecimalColumn> {
429        match &self.data {
430            ColumnData::Decimal(c) => Ok(c),
431            _ => Err(self.wrong_kind("Column::decimals", "decimal")),
432        }
433    }
434
435    /// The values of a true/false column; an error for any other kind.
436    pub fn booleans(&self) -> Result<&BooleanColumn> {
437        match &self.data {
438            ColumnData::Boolean(c) => Ok(c),
439            _ => Err(self.wrong_kind("Column::booleans", "boolean")),
440        }
441    }
442
443    /// The values of an array column; an error for any other kind.
444    pub fn arrays(&self) -> Result<&ArrayColumn> {
445        match &self.data {
446            ColumnData::Array(c) => Ok(c),
447            _ => Err(self.wrong_kind("Column::arrays", "array")),
448        }
449    }
450
451    fn wrong_kind(&self, call: &'static str, wanted: &str) -> PondError {
452        PondError::WrongKind {
453            call,
454            column: self.name.clone(),
455            kind: self.kind().as_str(),
456            reason: format!("this call needs a {wanted} column"),
457        }
458    }
459}
460
461/// A time series: a name, a key column and value columns of equal length.
462#[derive(Debug, Clone, PartialEq)]
463pub struct TimeSeries {
464    name: String,
465    key_name: String,
466    /// Shared: operations that keep the rows share the key instead of
467    /// copying it.
468    key: Arc<Key>,
469    columns: Vec<Column>,
470}
471
472/// Largest key time, in epoch milliseconds, either side of 1970: the range
473/// of a JavaScript `Date` (so of pond-ts), 100 million days. Keeping keys in
474/// it leaves room for any window or step arithmetic in `i64`. Checked on
475/// every series built from outside data; a derived series can go one bucket
476/// past it (`aggregate` of a row near the limit gives a bucket whose end is
477/// beyond it), as pond-ts's does.
478pub const MAX_TIME_MS: i64 = 8_640_000_000_000_000;
479
480impl TimeSeries {
481    /// Build a time-keyed series. The key column is named `"time"`; times are
482    /// epoch milliseconds within ±[`MAX_TIME_MS`] and must be non-decreasing
483    /// (repeats are allowed).
484    pub fn new(name: impl Into<String>, time: Vec<i64>, columns: Vec<Column>) -> Result<Self> {
485        Self::with_key(
486            "TimeSeries::new",
487            name.into(),
488            "time".into(),
489            Key::Time(time),
490            columns,
491        )
492    }
493
494    /// Build an interval-keyed series, the shape `aggregate` returns. The key
495    /// column is always named `"interval"` (pond-ts takes the name from the
496    /// schema; pond-rs has no way to choose it yet). Row `i` covers `[begin[i], end[i])` and
497    /// is labelled `label[i]`; rows must be ordered by `begin`, and the same
498    /// time limits as [`TimeSeries::new`] apply.
499    ///
500    /// ```
501    /// # use pond_rs::{Column, IntervalKeys, TimeSeries};
502    /// let days = TimeSeries::from_intervals(
503    ///     "days",
504    ///     IntervalKeys {
505    ///         label: vec!["mon", "tue"].into(),
506    ///         begin: vec![0, 86_400_000],
507    ///         end: vec![86_400_000, 172_800_000],
508    ///     },
509    ///     vec![Column::required("total", vec![12.0, 7.5])],
510    /// )?;
511    /// assert_eq!(days.key_name(), "interval");
512    /// # Ok::<(), pond_rs::PondError>(())
513    /// ```
514    pub fn from_intervals(
515        name: impl Into<String>,
516        keys: IntervalKeys,
517        columns: Vec<Column>,
518    ) -> Result<Self> {
519        Self::with_key(
520            "TimeSeries::from_intervals",
521            name.into(),
522            "interval".into(),
523            Key::Interval(keys),
524            columns,
525        )
526    }
527
528    /// Build a series keyed by time ranges (pond-ts `timeRange`). The key
529    /// column is named `"timeRange"`, pond-ts's default. Rows must be
530    /// ordered by `begin`, then `end`; each `end` must not be before its
531    /// `begin`, and the same time limits as [`TimeSeries::new`] apply.
532    ///
533    /// ```
534    /// # use pond_rs::{Column, TimeRangeKeys, TimeSeries};
535    /// let outages = TimeSeries::from_time_ranges(
536    ///     "outages",
537    ///     TimeRangeKeys { begin: vec![0, 5_000], end: vec![2_000, 5_500] },
538    ///     vec![Column::required("hosts", vec![3.0, 1.0])],
539    /// )?;
540    /// assert_eq!(outages.key_name(), "timeRange");
541    /// # Ok::<(), pond_rs::PondError>(())
542    /// ```
543    pub fn from_time_ranges(
544        name: impl Into<String>,
545        keys: TimeRangeKeys,
546        columns: Vec<Column>,
547    ) -> Result<Self> {
548        Self::with_key(
549            "TimeSeries::from_time_ranges",
550            name.into(),
551            "timeRange".into(),
552            Key::TimeRange(keys),
553            columns,
554        )
555    }
556
557    /// Build a series with any key, checking every rule.
558    pub(crate) fn with_key(
559        call: &'static str,
560        name: String,
561        key_name: String,
562        key: Key,
563        columns: Vec<Column>,
564    ) -> Result<Self> {
565        let invalid = |message: String| PondError::InvalidData { call, message };
566        let rows = key.len();
567        if let Key::Interval(k) = &key {
568            if k.label.len() != rows {
569                return Err(invalid(
570                    "interval labels and begins have different lengths".into(),
571                ));
572            }
573            match &k.label {
574                Labels::Number(v) => {
575                    if let Some(i) = v.iter().position(|l| !l.is_finite()) {
576                        return Err(invalid(format!(
577                            "interval label at row {i} is {}; number labels must be finite",
578                            v[i]
579                        )));
580                    }
581                }
582                Labels::String(c) => {
583                    if let Some(i) = c.iter().position(|l| l.is_none()) {
584                        return Err(invalid(format!(
585                            "interval label at row {i} is missing; every row needs a label"
586                        )));
587                    }
588                }
589            }
590        }
591        if let Some(end) = key.end() {
592            if end.len() != rows {
593                return Err(invalid("key begins and ends have different lengths".into()));
594            }
595            if let Some(i) = (0..rows).find(|&i| end[i] < key.begin()[i]) {
596                return Err(invalid(format!(
597                    "key at row {i} ends ({}) before it begins ({})",
598                    end[i],
599                    key.begin()[i]
600                )));
601            }
602        }
603        let out_of_range = |t: &i64| !(-MAX_TIME_MS..=MAX_TIME_MS).contains(t);
604        for edge in [key.begin(), key.end().unwrap_or(&[])] {
605            if let Some(i) = edge.iter().position(out_of_range) {
606                return Err(invalid(format!(
607                    "time {} ms at row {i} is outside the supported range ±{MAX_TIME_MS} ms (years -271821 to 275760, as JavaScript dates)",
608                    edge[i]
609                )));
610            }
611        }
612        // Ordered by begin, then (for ranges and intervals) by end, as in
613        // pond-ts.
614        let order = |i: usize| (key.begin()[i], key.end().map_or(0, |e| e[i]));
615        if let Some(i) = (1..rows).find(|&i| order(i) < order(i - 1)) {
616            let show = |i: usize| match key.end() {
617                None => format!("{}", key.begin()[i]),
618                Some(e) => format!("[{}, {}]", key.begin()[i], e[i]),
619            };
620            return Err(invalid(format!(
621                "key is not in order: row {i} ({}) sorts before row {} ({}); keys are ordered by begin, then end",
622                show(i),
623                i - 1,
624                show(i - 1)
625            )));
626        }
627        Self::check_columns(call, &key_name, rows, &columns)?;
628        Ok(Self {
629            name,
630            key_name,
631            key: Arc::new(key),
632            columns,
633        })
634    }
635
636    /// The series' name.
637    pub fn name(&self) -> &str {
638        &self.name
639    }
640
641    /// Number of rows.
642    pub fn len(&self) -> usize {
643        self.key.len()
644    }
645
646    /// `true` when there are no rows.
647    pub fn is_empty(&self) -> bool {
648        self.key.is_empty()
649    }
650
651    /// The key column: times, time ranges or intervals.
652    pub fn key(&self) -> &Key {
653        &self.key
654    }
655
656    /// The key column's name (`"time"`, `"interval"`, …).
657    pub fn key_name(&self) -> &str {
658        &self.key_name
659    }
660
661    /// The schema: the key first, then each value column in order.
662    pub fn schema(&self) -> Vec<ColumnDef> {
663        std::iter::once(ColumnDef {
664            name: self.key_name.clone(),
665            kind: self.key.kind(),
666            required: true,
667        })
668        .chain(self.columns.iter().map(|c| ColumnDef {
669            name: c.name.clone(),
670            kind: c.kind(),
671            required: c.required,
672        }))
673        .collect()
674    }
675
676    /// The value columns, in schema order.
677    pub fn columns(&self) -> &[Column] {
678        &self.columns
679    }
680
681    /// A value column by name; an error naming the columns there are when
682    /// there is none. Then [`Column::numbers`], [`Column::strings`] or
683    /// [`Column::booleans`] for its values.
684    pub fn column(&self, name: &str) -> Result<&Column> {
685        Ok(&self.columns[self.column_index("TimeSeries::column", name)?])
686    }
687
688    pub(crate) fn column_index(&self, call: &'static str, name: &str) -> Result<usize> {
689        self.columns
690            .iter()
691            .position(|c| c.name == name)
692            .ok_or_else(|| PondError::UnknownColumn {
693                call,
694                column: name.to_string(),
695                available: self.columns.iter().map(|c| c.name.clone()).collect(),
696            })
697    }
698
699    /// The value-column rules: names unique and not the key's, every
700    /// column `rows` long, no gap in a required column.
701    pub(crate) fn check_columns(
702        call: &'static str,
703        key_name: &str,
704        rows: usize,
705        columns: &[Column],
706    ) -> Result<()> {
707        let invalid = |message: String| PondError::InvalidData { call, message };
708        for (c, column) in columns.iter().enumerate() {
709            if column.name == key_name {
710                return Err(invalid(format!(
711                    "value column '{}' has the same name as the key",
712                    column.name
713                )));
714            }
715            if columns[..c].iter().any(|other| other.name == column.name) {
716                return Err(invalid(format!("column '{}' appears twice", column.name)));
717            }
718            if column.data.len() != rows {
719                return Err(invalid(format!(
720                    "column '{}' has {} values but the key has {rows}",
721                    column.name,
722                    column.data.len()
723                )));
724            }
725            if let ColumnData::Array(a) = &column.data
726                && let Some((r, e)) = a.first_bad_element()
727            {
728                return Err(invalid(format!(
729                    "column '{}' row {r}: array element {e} must be a finite number, string, or boolean",
730                    column.name
731                )));
732            }
733            if column.required && column.data.missing_count() > 0 {
734                let i = (0..rows)
735                    .find(|&i| !column.data.is_present(i))
736                    .expect("a gap exists");
737                return Err(invalid(format!(
738                    "required column '{}' has a gap at row {i}; use Column::optional if gaps are allowed",
739                    column.name
740                )));
741            }
742        }
743        Ok(())
744    }
745
746    pub(crate) fn from_parts(
747        name: String,
748        key_name: String,
749        key: impl Into<Arc<Key>>,
750        columns: Vec<Column>,
751    ) -> Self {
752        let key = key.into();
753        debug_assert!(columns.iter().all(|c| c.data.len() == key.len()));
754        Self {
755            name,
756            key_name,
757            key,
758            columns,
759        }
760    }
761
762    /// The key, shared rather than copied, for a result with the same rows.
763    pub(crate) fn shared_key(&self) -> Arc<Key> {
764        Arc::clone(&self.key)
765    }
766}
767
768#[cfg(test)]
769mod tests {
770    use super::*;
771
772    fn ranges(begin: Vec<i64>, end: Vec<i64>) -> Result<TimeSeries> {
773        TimeSeries::from_time_ranges("r", TimeRangeKeys { begin, end }, vec![])
774    }
775
776    fn intervals(label: Labels) -> Result<TimeSeries> {
777        let keys = IntervalKeys {
778            label,
779            begin: vec![0, 10],
780            end: vec![10, 20],
781        };
782        TimeSeries::from_intervals("i", keys, vec![])
783    }
784
785    #[test]
786    fn range_keys_are_checked_as_pond_ts_checks_them() {
787        // Same start: ordered by end. Zero length is allowed.
788        assert!(ranges(vec![0, 0, 5], vec![2, 9, 5]).is_ok());
789        let e = ranges(vec![0, 0], vec![9, 2]).unwrap_err().to_string();
790        assert!(
791            e.contains("row 1 ([0, 2]) sorts before row 0 ([0, 9])"),
792            "{e}"
793        );
794        let e = ranges(vec![5], vec![4]).unwrap_err().to_string();
795        assert!(e.contains("ends (4) before it begins (5)"), "{e}");
796        assert!(ranges(vec![0, 1], vec![1]).is_err());
797        assert!(ranges(vec![0], vec![MAX_TIME_MS + 1]).is_err());
798    }
799
800    #[test]
801    fn interval_labels_are_checked() {
802        // Repeats and fractions are fine.
803        assert!(intervals(vec![1.5, 1.5].into()).is_ok());
804        assert!(intervals(vec!["a", "a"].into()).is_ok());
805        let e = intervals(vec![1.0, f64::NAN].into())
806            .unwrap_err()
807            .to_string();
808        assert!(e.contains("label at row 1"), "{e}");
809        let gap = Labels::String(StringColumn::from_options([Some("a"), None]));
810        let e = intervals(gap).unwrap_err().to_string();
811        assert!(e.contains("label at row 1 is missing"), "{e}");
812        assert!(intervals(vec![1.0].into()).is_err());
813    }
814}