volas_core/dataframe.rs
1//! DataFrame: ordered, named columns sharing a single row index.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use crate::column::Column;
7use crate::error::{Result, VolasError};
8use crate::index::{Index, IndexKind};
9use crate::series::Series;
10
11/// Metadata for a materialized (cached) directive column: the directive that
12/// produced it, its lookback, and how many leading rows currently hold valid
13/// values. After an `append`, the new rows are stale (NaN) and `valid_rows` lags
14/// `height` until `fulfill` recomputes the tail.
15#[derive(Clone, Debug)]
16pub struct ComputedMeta {
17 /// The (canonical) directive string.
18 pub directive: String,
19 /// The directive's lookback (warm-up rows).
20 pub lookback: usize,
21 /// Rows `[0, valid_rows)` currently hold valid values.
22 pub valid_rows: usize,
23 /// Carried recursive state for an O(new-rows) append resume: a small,
24 /// fixed-size per-indicator vector capturing the internal recursive state as
25 /// of the last valid row (`valid_rows - 1`), so an `append`/`fulfill` can
26 /// continue the recursion over only the new rows, bit-identical to a fresh
27 /// full recompute. `None` when the directive has no resume implementation (it
28 /// then falls back to the correct full recompute) or the state is unknown
29 /// (e.g. after a slice that did not reach the parent's `valid_rows`).
30 pub state: Option<Vec<f64>>,
31 /// The original-frame row that THIS (possibly sliced) frame's row 0 maps to.
32 /// `0` for a freshly-computed column; a contiguous slice from `start` bumps it
33 /// by `start`. It lets an absolute-position indicator (the index family —
34 /// maxindex/minindex/minmaxindex) keep emitting ABSOLUTE positions after a
35 /// head-dropping slice: a sub-frame position `p` is original row `p + origin`,
36 /// matching the verbatim-carried (original-absolute) head. Recursive *value*
37 /// indicators ignore it (their state is offset-free).
38 pub origin: usize,
39}
40
41/// A 2-D, column-oriented, time-indexed table. All columns share one index and
42/// have equal length (`height`).
43#[derive(Clone, Debug)]
44pub struct DataFrame {
45 // Schema (names + lookup) is `Arc`-shared so a frame clone / same-schema
46 // derivation (slice / take / mask / astype) is an O(1) refcount bump, not a
47 // rebuild of the name strings + hash map (copy-on-write on mutation).
48 names: Arc<Vec<String>>,
49 columns: Vec<Column>,
50 name_to_idx: Arc<HashMap<String, usize>>,
51 index: Arc<Index>,
52 height: usize,
53 /// Column-name aliases (`alias -> source name`), resolved on lookup. Shared
54 /// via `Arc` (cheap clone) and carried through derived frames.
55 aliases: Arc<HashMap<String, String>>,
56 /// Materialized directive columns (name -> meta). Tracked so `fulfill` can
57 /// incrementally recompute their tail after an append. Carried through
58 /// `clone` / `append`; dropped by shape-changing ops (slice/select/…), where
59 /// the columns become plain data.
60 computed: HashMap<String, ComputedMeta>,
61}
62
63impl DataFrame {
64 /// Construct a frame from parallel `names` / `columns`, validating shape.
65 pub fn new(names: Vec<String>, columns: Vec<Column>, index: Option<Index>) -> Result<Self> {
66 if names.len() != columns.len() {
67 return Err(VolasError::Shape(format!(
68 "{} names but {} columns",
69 names.len(),
70 columns.len()
71 )));
72 }
73 let height = columns.first().map(|c| c.len()).unwrap_or(0);
74 for (n, c) in names.iter().zip(&columns) {
75 if c.len() != height {
76 return Err(VolasError::Shape(format!(
77 "column \"{}\" has length {} but frame height is {}",
78 n,
79 c.len(),
80 height
81 )));
82 }
83 }
84 let index = match index {
85 Some(ix) => {
86 if ix.len() != height {
87 return Err(VolasError::Shape(format!(
88 "index length {} != frame height {}",
89 ix.len(),
90 height
91 )));
92 }
93 ix
94 }
95 None => Index::range(height),
96 };
97 let mut name_to_idx = HashMap::with_capacity(names.len());
98 for (i, n) in names.iter().enumerate() {
99 name_to_idx.insert(n.clone(), i);
100 }
101 Ok(DataFrame {
102 names: Arc::new(names),
103 columns,
104 name_to_idx: Arc::new(name_to_idx),
105 index: Arc::new(index),
106 height,
107 aliases: Arc::new(HashMap::new()),
108 computed: HashMap::new(),
109 })
110 }
111
112 /// Number of rows.
113 pub fn height(&self) -> usize {
114 self.height
115 }
116
117 /// Number of columns.
118 pub fn width(&self) -> usize {
119 self.columns.len()
120 }
121
122 /// Column names in order.
123 pub fn names(&self) -> &[String] {
124 &self.names
125 }
126
127 /// The shared row index.
128 pub fn index(&self) -> &Arc<Index> {
129 &self.index
130 }
131
132 /// Columns in order.
133 pub fn columns(&self) -> &[Column] {
134 &self.columns
135 }
136
137 /// Resolve a name through the alias map (`alias -> source`, else itself).
138 fn resolve<'a>(&'a self, name: &'a str) -> &'a str {
139 self.aliases.get(name).map(String::as_str).unwrap_or(name)
140 }
141
142 /// Position of a column by name (alias-aware).
143 pub fn column_pos(&self, name: &str) -> Option<usize> {
144 self.name_to_idx.get(self.resolve(name)).copied()
145 }
146
147 /// Whether a column exists (alias-aware).
148 pub fn has_column(&self, name: &str) -> bool {
149 self.name_to_idx.contains_key(self.resolve(name))
150 }
151
152 /// Define a column alias (`as_name -> src_name`), returning a new frame.
153 /// Errors if `as_name` is already a real column, or `src_name` does not exist.
154 pub fn with_alias(&self, as_name: &str, src_name: &str) -> Result<DataFrame> {
155 if self.name_to_idx.contains_key(as_name) {
156 return Err(VolasError::Value(format!(
157 "column \"{as_name}\" already exists"
158 )));
159 }
160 if self.column_pos(src_name).is_none() {
161 return Err(VolasError::Value(format!(
162 "column \"{src_name}\" not exists"
163 )));
164 }
165 let mut aliases = (*self.aliases).clone();
166 aliases.insert(as_name.to_string(), src_name.to_string());
167 let mut df = self.clone();
168 df.aliases = Arc::new(aliases);
169 Ok(df)
170 }
171
172 /// Build a frame that **shares this frame's schema** (names + lookup +
173 /// aliases, all `Arc`-cloned) over freshly derived `columns` / `index` — for
174 /// the same-shape derivations (slice / take / mask / astype), with no
175 /// name-string or hash-map rebuild. Computed-column status is dropped; the
176 /// caller re-attaches it where the derivation preserves it (a contiguous slice).
177 fn same_schema(&self, columns: Vec<Column>, index: Index) -> DataFrame {
178 let height = columns.first().map_or(0, |c| c.len());
179 DataFrame {
180 names: Arc::clone(&self.names),
181 name_to_idx: Arc::clone(&self.name_to_idx),
182 columns,
183 index: Arc::new(index),
184 height,
185 aliases: Arc::clone(&self.aliases),
186 computed: HashMap::new(),
187 }
188 }
189
190 /// Gather rows by position into a new frame (carries aliases).
191 pub fn take(&self, positions: &[usize]) -> DataFrame {
192 let columns: Vec<Column> = self.columns.iter().map(|c| c.take(positions)).collect();
193 self.same_schema(columns, self.index.take(positions))
194 }
195
196 /// Borrow a column by name.
197 pub fn column(&self, name: &str) -> Result<&Column> {
198 self.column_pos(name)
199 .map(|i| &self.columns[i])
200 .ok_or_else(|| VolasError::ColumnNotFound(name.to_string()))
201 }
202
203 /// Extract a column as a [`Series`] sharing this frame's index.
204 pub fn series(&self, name: &str) -> Result<Series> {
205 let col = self.column(name)?.clone();
206 Ok(Series::new(
207 Some(name.to_string()),
208 col,
209 Arc::clone(&self.index),
210 ))
211 }
212
213 /// Add a new column or replace an existing one (must match `height`, unless
214 /// the frame currently has no columns).
215 pub fn set_column(&mut self, name: &str, col: Column) -> Result<()> {
216 if self.columns.is_empty() {
217 self.height = col.len();
218 if self.index.len() != self.height {
219 self.index = Arc::new(Index::range(self.height));
220 }
221 } else if col.len() != self.height {
222 return Err(VolasError::Shape(format!(
223 "new column \"{}\" has length {} but frame height is {}",
224 name,
225 col.len(),
226 self.height
227 )));
228 }
229 match self.column_pos(name) {
230 Some(i) => self.columns[i] = col,
231 None => {
232 Arc::make_mut(&mut self.name_to_idx).insert(name.to_string(), self.columns.len());
233 Arc::make_mut(&mut self.names).push(name.to_string());
234 self.columns.push(col);
235 }
236 }
237 Ok(())
238 }
239
240 /// Move a column out of the frame and use it as the row index (pandas
241 /// `set_index`). The column is removed; its values become the index
242 /// (datetime / int64 — see [`Index::from_column`]).
243 pub fn set_index(&self, name: &str) -> Result<DataFrame> {
244 let pos = self
245 .column_pos(name)
246 .ok_or_else(|| VolasError::ColumnNotFound(name.to_string()))?;
247 // Record the source column's name on the index (pandas keeps it, so
248 // `reset_index` can restore the original column label).
249 let index = Index::from_column(&self.columns[pos])?.with_name(Some(name.to_string()));
250 let mut names = (*self.names).clone();
251 let mut columns = self.columns.clone();
252 names.remove(pos);
253 columns.remove(pos);
254 let mut df = DataFrame::new(names, columns, Some(index))?;
255 df.aliases = Arc::clone(&self.aliases);
256 Ok(df)
257 }
258
259 /// Change the DatetimeIndex's **display / matching** timezone without moving
260 /// any instant (pandas `tz_convert`): stored UTC ns are unchanged; only how
261 /// they render and how bare-string `.loc` matches changes. Returns a new frame
262 /// (columns shared). Errors if the index is not a DatetimeIndex.
263 pub fn tz_convert(&self, tz: crate::tz::Tz) -> Result<DataFrame> {
264 match self.index.kind() {
265 IndexKind::Datetime(_, cur) => {
266 // A naive axis is an unanchored wall-clock — there is no source
267 // zone to convert FROM, so converting it would silently relabel
268 // wrong instants. Anchor with tz_localize first (pandas parity).
269 if !cur.is_aware() {
270 return Err(VolasError::DType(
271 "cannot tz_convert a tz-naive DatetimeIndex; use tz_localize to anchor it first"
272 .into(),
273 ));
274 }
275 let mut df = self.clone();
276 df.index = Arc::new((*self.index).clone().with_tz(tz));
277 Ok(df)
278 }
279 _ => Err(VolasError::DType(
280 "tz_convert requires a DatetimeIndex".into(),
281 )),
282 }
283 }
284
285 /// Tag the DatetimeIndex's zone directly, without the naive-axis guard of
286 /// [`Self::tz_convert`]. For importers (`from_pandas`) whose instants are
287 /// ALREADY true UTC and arrive carrying their zone — not a user-facing API.
288 pub fn set_index_tz(&self, tz: crate::tz::Tz) -> Result<DataFrame> {
289 match self.index.kind() {
290 IndexKind::Datetime(_, _) => {
291 let mut df = self.clone();
292 df.index = Arc::new((*self.index).clone().with_tz(tz));
293 Ok(df)
294 }
295 _ => Err(VolasError::DType(
296 "set_index_tz requires a DatetimeIndex".into(),
297 )),
298 }
299 }
300
301 /// Reinterpret the index's **wall-clock** as `tz` (pandas `tz_localize`): each
302 /// instant is recomputed so the displayed wall-clock is unchanged but now
303 /// correct for `tz`. Use this when data was ingested without a tz and you need
304 /// to attach the right one. Returns a new frame. Errors if the index is not a
305 /// DatetimeIndex or a wall-clock does not exist in `tz` (a DST spring-forward
306 /// gap).
307 pub fn tz_localize(&self, tz: crate::tz::Tz) -> Result<DataFrame> {
308 let (values, cur) = match self.index.kind() {
309 IndexKind::Datetime(v, cur) => (v.clone(), *cur),
310 _ => {
311 return Err(VolasError::DType(
312 "tz_localize requires a DatetimeIndex".into(),
313 ))
314 }
315 };
316 // Localize anchors an UNanchored wall-clock; an already-aware axis must
317 // use tz_convert (re-localizing would silently reinterpret instants).
318 if cur.is_aware() {
319 return Err(VolasError::DType(format!(
320 "index is already tz-aware ({}); use tz_convert",
321 cur.name()
322 )));
323 }
324 let mut shifted = Vec::with_capacity(values.len());
325 for ns in values {
326 let (y, mo, d, h, mi, s) = cur.civil_parts(ns);
327 let new = tz
328 .wall_to_utc_ns(y as i32, mo as u32, d as u32, h as u32, mi as u32, s as u32)
329 .ok_or_else(|| {
330 VolasError::Value(format!(
331 "wall-clock {y:04}-{mo:02}-{d:02} {h:02}:{mi:02}:{s:02} does not exist in {} (or is DST-ambiguous)",
332 tz.name()
333 ))
334 })?;
335 shifted.push(new);
336 }
337 let mut df = self.clone();
338 // tz_localize moves the instants but keeps the index identity (and name).
339 df.index = Arc::new(Index::datetime(shifted, tz).with_name(self.index.name().map(String::from)));
340 Ok(df)
341 }
342
343 /// Select a subset of columns into a new frame sharing this index.
344 pub fn select(&self, names: &[String]) -> Result<DataFrame> {
345 let mut columns = Vec::with_capacity(names.len());
346 for n in names {
347 columns.push(self.column(n)?.clone());
348 }
349 let mut name_to_idx = HashMap::with_capacity(names.len());
350 for (i, n) in names.iter().enumerate() {
351 name_to_idx.insert(n.clone(), i);
352 }
353 Ok(DataFrame {
354 names: Arc::new(names.to_vec()),
355 columns,
356 name_to_idx: Arc::new(name_to_idx),
357 index: Arc::clone(&self.index),
358 height: self.height,
359 aliases: Arc::clone(&self.aliases),
360 computed: HashMap::new(),
361 })
362 }
363
364 /// A `[start, end)` row slice.
365 ///
366 /// Deliberately a **value copy** (each column's window is copied), not a
367 /// zero-copy view into the parent buffer: a slice is an independent frame, so
368 /// slicing the recent tail of a long history does not pin the whole history
369 /// alive — the right default for a live system. (A view would be ~1.5x faster
370 /// here but would retain the parent's full buffer; we keep the safer copy.)
371 pub fn slice(&self, start: usize, end: usize) -> DataFrame {
372 let start = start.min(self.height);
373 let end = end.max(start).min(self.height);
374 let len = end - start;
375 let columns: Vec<Column> = self.columns.iter().map(|c| c.slice(start, end)).collect();
376 let mut df = self.same_schema(columns, self.index.slice(start, end));
377 // SP-9: carry cached-directive columns *as continuable computed columns*
378 // through a contiguous slice. The cached values are already correct (they
379 // were computed with full history) and are carried verbatim; we re-tag the
380 // `ComputedMeta` cursor so a later `append` refreshes the tail incrementally
381 // — re-deriving it from the retained raw columns over a `lookback` window,
382 // exactly as a non-sliced frame would (the engine re-warms from raw data,
383 // never from cached output, so composite recursive indicators continue
384 // correctly too). This is only sound when the slice keeps at least
385 // `lookback` warm-up rows; a shorter slice would re-warm from its own start
386 // (a seed that is *not* `lookback` rows back) and silently diverge, so there
387 // we drop the computed status and the column stays plain data (honest:
388 // values correct, but not continuable). Non-contiguous derivations
389 // (`take` / `filter_mask`) go through `DataFrame::new` and already drop it.
390 for (name, meta) in &self.computed {
391 if len >= meta.lookback {
392 let valid = meta.valid_rows.saturating_sub(start).min(len);
393 // Carry the recursive state only when this slice's END reaches the
394 // parent's `valid_rows`: the captured state is the internal state as
395 // of the parent row `valid_rows - 1`, which is THIS sub-frame's last
396 // valid row exactly when `start + len >= valid_rows` (so `valid` ==
397 // the parent's last-valid offset). A shorter slice (end before
398 // `valid_rows`) would leave the state attached to a row the sub-frame
399 // no longer ends on, so we drop it (the column stays correct via the
400 // full-recompute fallback, just not O(new-rows) continuable).
401 let carried = if end >= meta.valid_rows {
402 meta.state.clone()
403 } else {
404 None
405 };
406 df.computed.insert(
407 name.clone(),
408 ComputedMeta {
409 directive: meta.directive.clone(),
410 lookback: meta.lookback,
411 valid_rows: valid,
412 state: carried,
413 // This sub-frame's row 0 is the parent's row `start`, so its
414 // origin shifts by `start` (an absolute-index resume adds it
415 // back to stay original-absolute, matching the carried head).
416 origin: meta.origin + start,
417 },
418 );
419 }
420 }
421 df
422 }
423
424 /// Filter rows by a boolean mask.
425 pub fn filter_mask(&self, mask: &[bool]) -> Result<DataFrame> {
426 if mask.len() != self.height {
427 return Err(VolasError::Shape(format!(
428 "boolean mask length {} != frame height {}",
429 mask.len(),
430 self.height
431 )));
432 }
433 let idx: Vec<usize> = mask
434 .iter()
435 .enumerate()
436 .filter_map(|(i, &b)| if b { Some(i) } else { None })
437 .collect();
438 let columns: Vec<Column> = self.columns.iter().map(|c| c.take(&idx)).collect();
439 Ok(self.same_schema(columns, self.index.take(&idx)))
440 }
441
442 /// Append the rows of `other` (matched by column name) in place. Columns of
443 /// `self` absent from `other` are NaN-padded (so a frame with materialized
444 /// directive columns can take raw bars; `fulfill` then refreshes them).
445 /// Computed-column metadata is retained, leaving the new rows stale.
446 pub fn append(&mut self, other: &DataFrame) -> Result<()> {
447 let oh = other.height;
448 // Iterate by position to avoid cloning every column name and then
449 // re-hashing it back into this same frame on the live append path.
450 for pos in 0..self.names.len() {
451 let n = &self.names[pos];
452 if let Some(other_pos) = other.column_pos(n) {
453 self.columns[pos].append(&other.columns[other_pos])?;
454 } else {
455 // column `n` is missing from `other` — pad the new rows.
456 if self.computed.contains_key(n) {
457 // A cached directive (F64 indicator / Bool mask): a cheap stale
458 // placeholder (NaN / `false`); `fulfill` recomputes and overwrites
459 // the appended tail, so a dense placeholder keeps validity simple.
460 self.columns[pos].append_missing(oh)?;
461 } else {
462 // A plain column keeps its data semantics: pad with dtype-preserving
463 // NA (int / bool / str grow the validity bitmap; datetime -> NaT;
464 // float -> NaN), never upcasting the dtype or erroring.
465 self.columns[pos].append_na(oh);
466 }
467 }
468 }
469 Arc::make_mut(&mut self.index).extend(&other.index)?;
470 self.height += oh;
471 Ok(())
472 }
473
474 /// Assign `values` into column position `col` at the given row `positions`
475 /// (copy-on-write via [`Arc::make_mut`]). `values` is broadcast when it has
476 /// length 1, otherwise its length must equal `positions.len()`. This backs
477 /// `df.loc[...] = `, `df.iloc[...] = `, `df.at[...] = ` and `df.iat[...] = `.
478 ///
479 /// Dtype handling is delegated to [`Column::scatter`], the single assignment
480 /// primitive shared with the Series and boolean-mask surfaces: it **keeps the
481 /// target column's dtype** and updates its validity (a write into an existing NA
482 /// cell makes it present; a missing / `NaN` source marks the cell NA without
483 /// widening an int column to float; a present non-integral value into an int
484 /// column is a lossy error).
485 ///
486 /// A manual write into a cached directive column **drops its computed status**
487 /// (it becomes plain data) so a later `fulfill` can never silently clobber the
488 /// override.
489 pub fn assign_positions(
490 &mut self,
491 col: usize,
492 positions: &[usize],
493 values: &Column,
494 ) -> Result<()> {
495 if col >= self.columns.len() {
496 return Err(VolasError::Shape(format!(
497 "column position {col} is out of range (width {})",
498 self.columns.len()
499 )));
500 }
501 let n = positions.len();
502 if values.len() != 1 && values.len() != n {
503 return Err(VolasError::Shape(format!(
504 "cannot assign {} values to {n} selected rows",
505 values.len()
506 )));
507 }
508 for &p in positions {
509 if p >= self.height {
510 return Err(VolasError::Shape(format!(
511 "row position {p} is out of range (height {})",
512 self.height
513 )));
514 }
515 }
516 self.columns[col] = self.columns[col].scatter(positions, values)?;
517 self.invalidate_computed_on_write_at(col);
518 Ok(())
519 }
520
521 /// Rename columns (pandas `rename(columns=...)`), returning a new frame.
522 /// Names not in `mapping` are kept; columns and index are shared (cheap).
523 pub fn rename(&self, mapping: &HashMap<String, String>) -> Result<DataFrame> {
524 let names: Vec<String> = self
525 .names
526 .iter()
527 .map(|n| mapping.get(n).cloned().unwrap_or_else(|| n.clone()))
528 .collect();
529 let mut df = DataFrame::new(names, self.columns.clone(), Some((*self.index).clone()))?;
530 df.aliases = Arc::clone(&self.aliases);
531 Ok(df)
532 }
533
534 /// Cast the named columns to new dtypes (pandas `astype`), returning a new
535 /// frame. Untouched columns are shared (cheap).
536 pub fn astype(&self, mapping: &HashMap<String, crate::dtype::DType>) -> Result<DataFrame> {
537 let mut columns = self.columns.clone();
538 for (name, dtype) in mapping {
539 let pos = self
540 .column_pos(name)
541 .ok_or_else(|| VolasError::ColumnNotFound(name.clone()))?;
542 columns[pos] = self.columns[pos].cast(*dtype)?;
543 }
544 Ok(self.same_schema(columns, (*self.index).clone()))
545 }
546
547 /// Value equality (pandas `DataFrame.equals`): same column names + order,
548 /// same index *labels*, and value-equal columns (`NaN == NaN`). The index
549 /// *name* is metadata and is ignored, matching pandas (`.equals` ignores it).
550 pub fn equals(&self, other: &DataFrame) -> bool {
551 self.height == other.height
552 && self.names == other.names
553 && self.index.label_eq(&other.index)
554 && self
555 .columns
556 .iter()
557 .zip(&other.columns)
558 .all(|(a, b)| a.equals(b))
559 }
560
561 /// Flatten to a row-major (C-order) 2-D `f64` buffer for NumPy export,
562 /// returning `(data, height, width)`. Each column is materialized through the
563 /// validity-aware `to_f64_vec`, so a missing cell (int/bool NA, datetime NaT,
564 /// str) exports as `NaN` — not the raw placeholder — matching the 1-D
565 /// `Series` export and `pandas` `Int64.to_numpy()`.
566 pub fn to_row_major_f64(&self) -> (Vec<f64>, usize, usize) {
567 let h = self.height;
568 let w = self.columns.len();
569 let mut out = vec![0.0f64; h * w];
570 for (j, c) in self.columns.iter().enumerate() {
571 let col = c.to_f64_vec();
572 for i in 0..h {
573 out[i * w + j] = col[i];
574 }
575 }
576 (out, h, w)
577 }
578
579 /// Flatten to a row-major (C-order) 2-D `i64` buffer for an **exact** integer
580 /// (or `datetime64[ns]`) NumPy export, returning `(data, height, width)`. A
581 /// datetime column contributes its raw epoch-ns — so sub-2⁵³ ns and `NaT`
582 /// (which stays `i64::MIN`, the datetime64 sentinel) survive, unlike the
583 /// `to_row_major_f64` channel — and an `i64` column its exact value (no
584 /// float round-trip past 2⁵³). A float column truncates toward zero. A `str`
585 /// column has no integer meaning; the export boundary rejects it before
586 /// calling this, so it contributes a `0` placeholder it never reaches.
587 pub fn to_row_major_i64(&self) -> (Vec<i64>, usize, usize) {
588 let h = self.height;
589 let w = self.columns.len();
590 let mut out = vec![0i64; h * w];
591 for (j, c) in self.columns.iter().enumerate() {
592 for i in 0..h {
593 out[i * w + j] = match c {
594 Column::Datetime(v) => v[i],
595 Column::I64(v, _) => v[i],
596 Column::I32(v, _) => v[i] as i64,
597 Column::Bool(v, _) => v[i] as i64,
598 Column::F64(v) => v[i] as i64,
599 Column::F32(v) => v[i] as i64,
600 Column::Str(..) => 0,
601 };
602 }
603 }
604 (out, h, w)
605 }
606}
607
608mod computed;
609
610#[cfg(test)]
611mod tests;