Skip to main content

antecedent_data/
table.rs

1//! [`TableView`] trait — public causal table API (ADR 0004).
2//!
3//! SPDX-License-Identifier: MIT OR Apache-2.0
4
5use antecedent_core::{CausalSchema, VariableId};
6
7use crate::column::ColumnView;
8use crate::error::DataError;
9
10/// Lossy `i64` → `f64` for the float analysis path (mantissa cannot hold all `i64`).
11fn analysis_f64_from_i64(v: i64) -> f64 {
12    #[allow(clippy::cast_precision_loss)]
13    {
14        v as f64
15    }
16}
17
18/// Borrowed table access used by algorithms.
19pub trait TableView {
20    /// Immutable causal schema.
21    fn schema(&self) -> &CausalSchema;
22
23    /// Number of rows.
24    fn row_count(&self) -> usize;
25
26    /// Column view for `id`.
27    ///
28    /// # Errors
29    ///
30    /// Unknown variable or type issues.
31    fn column(&self, id: VariableId) -> Result<ColumnView<'_>, DataError>;
32
33    /// Borrow a native `Float64` column as a contiguous slice (no allocation).
34    ///
35    /// Unlike [`TableView::float64_values`], no coercion is attempted: `Int64`
36    /// and `Boolean` columns error so callers stay on the copying path for
37    /// them. Use [`TableView::float64_cow`] to get borrowed-when-possible
38    /// semantics with the same values as `float64_values`.
39    ///
40    /// # Errors
41    ///
42    /// Unknown variable or non-`Float64` column type.
43    fn float64_slice(&self, id: VariableId) -> Result<&[f64], DataError> {
44        match self.column(id)? {
45            ColumnView::Float64(c) => Ok(c.values.as_slice()),
46            _ => Err(DataError::TypeMismatch { id, expected: "native float64 (borrowed slice)" }),
47        }
48    }
49
50    /// Column values as `f64`, borrowed when the column is native `Float64`.
51    ///
52    /// Yields exactly the same values as [`TableView::float64_values`]
53    /// (including the `Int64`/`Boolean` coercions), but avoids the copy for
54    /// native `Float64` columns.
55    ///
56    /// # Errors
57    ///
58    /// Unknown variable or unsupported column type.
59    fn float64_cow(&self, id: VariableId) -> Result<std::borrow::Cow<'_, [f64]>, DataError> {
60        match self.float64_slice(id) {
61            Ok(s) => Ok(std::borrow::Cow::Borrowed(s)),
62            Err(_) => self.float64_values(id).map(std::borrow::Cow::Owned),
63        }
64    }
65
66    /// Copy a column into an owned `f64` buffer.
67    ///
68    /// Native `Float64` columns are copied as-is. `Int64` and `Boolean` columns
69    /// are coerced to `f64` (`true` → `1.0`, `false` → `0.0`); invalid rows become
70    /// `NaN`. Other column kinds (categorical, timestamp, fixed vector) error.
71    ///
72    /// # Errors
73    ///
74    /// Unknown variable or unsupported column type.
75    fn float64_values(&self, id: VariableId) -> Result<Vec<f64>, DataError> {
76        match self.column(id)? {
77            ColumnView::Float64(c) => Ok(c.values.to_vec()),
78            ColumnView::Int64(c) => {
79                let mut out = Vec::with_capacity(c.values.len());
80                for (i, &v) in c.values.iter().enumerate() {
81                    out.push(if c.validity.is_valid(i) {
82                        analysis_f64_from_i64(v)
83                    } else {
84                        f64::NAN
85                    });
86                }
87                Ok(out)
88            }
89            ColumnView::Boolean(c) => {
90                let mut out = Vec::with_capacity(c.values.len());
91                for (i, &v) in c.values.iter().enumerate() {
92                    out.push(if c.validity.is_valid(i) { f64::from(v) } else { f64::NAN });
93                }
94                Ok(out)
95            }
96            _ => Err(DataError::TypeMismatch {
97                id,
98                expected: "float64 (or coercible int64/boolean)",
99            }),
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use std::sync::Arc;
107
108    use antecedent_core::{
109        CausalSchemaBuilder, MeasurementSpec, RoleHint, SmallRoleSet, ValueType, VariableId,
110    };
111
112    use super::*;
113    use crate::column::{BooleanColumn, Float64Column, Int64Column, OwnedColumn, ValidityBitmap};
114    use crate::dataset::TabularData;
115    use crate::storage::OwnedColumnarStorage;
116
117    fn schema_n(n: usize) -> antecedent_core::CausalSchema {
118        let mut b = CausalSchemaBuilder::new();
119        for i in 0..n {
120            b.add_variable(
121                format!("v{i}"),
122                ValueType::Continuous,
123                SmallRoleSet::from_hint(RoleHint::Context),
124                None,
125                None,
126                MeasurementSpec::default(),
127            )
128            .unwrap();
129        }
130        b.build().unwrap()
131    }
132
133    #[test]
134    fn float64_values_coerces_int64_and_boolean() {
135        let schema = schema_n(3);
136        let cols = vec![
137            OwnedColumn::Float64(
138                Float64Column::new(
139                    VariableId::from_raw(0),
140                    Arc::from([1.5_f64, 2.5]),
141                    ValidityBitmap::all_valid(2),
142                )
143                .unwrap(),
144            ),
145            OwnedColumn::Int64(
146                Int64Column::new(
147                    VariableId::from_raw(1),
148                    Arc::<[i64]>::from([3_i64, 4]),
149                    ValidityBitmap::all_valid(2),
150                )
151                .unwrap(),
152            ),
153            OwnedColumn::Boolean(
154                BooleanColumn::new(
155                    VariableId::from_raw(2),
156                    Arc::<[u8]>::from([1_u8, 0]),
157                    ValidityBitmap::all_valid(2),
158                )
159                .unwrap(),
160            ),
161        ];
162        let storage = OwnedColumnarStorage::try_new(schema, cols, None, None).unwrap();
163        let data = TabularData::new(storage);
164        assert_eq!(data.float64_values(VariableId::from_raw(0)).unwrap(), vec![1.5, 2.5]);
165        assert_eq!(data.float64_values(VariableId::from_raw(1)).unwrap(), vec![3.0, 4.0]);
166        assert_eq!(data.float64_values(VariableId::from_raw(2)).unwrap(), vec![1.0, 0.0]);
167    }
168}