use std::fmt;
use std::sync::Arc;
use crate::enums::error::MinarrowError;
use crate::enums::shape_dim::ShapeDim;
use crate::structs::buffer::Buffer;
use crate::traits::{concatenate::Concatenate, shape::Shape};
use crate::{Array, Field, FieldArray, FloatArray, NumericArray, Table, Vec64};
#[cfg(feature = "views")]
use crate::{SharedBuffer, TableV};
#[repr(C, align(64))]
#[derive(Clone, PartialEq)]
pub struct Matrix {
pub n_rows: usize,
pub n_cols: usize,
pub stride: usize,
pub data: Buffer<f64>,
pub name: Option<String>,
}
const ALIGN_ELEMS: usize = 64 / std::mem::size_of::<f64>();
#[inline]
const fn aligned_stride(n_rows: usize) -> usize {
(n_rows + ALIGN_ELEMS - 1) & !(ALIGN_ELEMS - 1)
}
impl Matrix {
pub fn new(n_rows: usize, n_cols: usize, name: Option<String>) -> Self {
let stride = aligned_stride(n_rows);
let len = stride * n_cols;
let mut vec = Vec64::with_capacity(len);
vec.resize(len, 0.0);
let data = Buffer::from_vec64(vec);
Matrix { n_rows, n_cols, stride, data, name }
}
pub fn from_f64_aligned(data: Vec64<f64>, n_rows: usize, n_cols: usize, name: Option<String>) -> Self {
let stride = aligned_stride(n_rows);
assert_eq!(
data.len(),
stride * n_cols,
"Matrix: padded buffer length does not match stride * n_cols"
);
Matrix { n_rows, n_cols, stride, data: Buffer::from_vec64(data), name }
}
pub fn from_f64_unaligned(src: &[f64], n_rows: usize, n_cols: usize, name: Option<String>) -> Self {
assert_eq!(
src.len(),
n_rows * n_cols,
"Matrix shape does not match buffer length"
);
let stride = aligned_stride(n_rows);
if stride == n_rows {
let vec = Vec64::from(src);
return Matrix { n_rows, n_cols, stride, data: Buffer::from_vec64(vec), name };
}
let mut vec = Vec64::with_capacity(stride * n_cols);
vec.resize(stride * n_cols, 0.0);
for col in 0..n_cols {
let src_start = col * n_rows;
let dst_start = col * stride;
vec.as_mut_slice()[dst_start..dst_start + n_rows]
.copy_from_slice(&src[src_start..src_start + n_rows]);
}
Matrix { n_rows, n_cols, stride, data: Buffer::from_vec64(vec), name }
}
pub fn try_from_cols(
cols: impl AsRef<[FloatArray<f64>]>,
name: Option<String>,
) -> Result<Self, MinarrowError> {
let columns = cols.as_ref();
if columns.is_empty() {
return Err(MinarrowError::ShapeError {
message: "Matrix::try_from_cols requires at least one column".into(),
});
}
let n_rows = columns[0].data.len();
for (i, col) in columns.iter().enumerate() {
if col.data.len() != n_rows {
return Err(MinarrowError::ColumnLengthMismatch {
col: i,
expected: n_rows,
found: col.data.len(),
});
}
if col.null_mask.as_ref().map_or(false, |m| m.has_nulls()) {
return Err(MinarrowError::NullError {
message: Some(format!(
"Matrix::try_from_cols: column {i} contains null values; Matrix requires dense data"
)),
});
}
}
let n_cols = columns.len();
let stride = aligned_stride(n_rows);
let pad = stride - n_rows;
let mut vec = Vec64::with_capacity(stride * n_cols);
for col in columns {
let col_slice: &[f64] = col.data.as_slice();
vec.extend_from_slice(col_slice);
if pad > 0 {
vec.extend_from_slice(&[0.0; ALIGN_ELEMS][..pad]);
}
}
Ok(Matrix { n_rows, n_cols, stride, data: Buffer::from_vec64(vec), name })
}
#[inline]
pub fn get(&self, row: usize, col: usize) -> f64 {
debug_assert!(row < self.n_rows, "Row out of bounds");
debug_assert!(col < self.n_cols, "Col out of bounds");
self.data.as_slice()[col * self.stride + row]
}
#[inline]
pub fn set(&mut self, row: usize, col: usize, value: f64) {
debug_assert!(row < self.n_rows, "Row out of bounds");
debug_assert!(col < self.n_cols, "Col out of bounds");
self.data.as_mut_slice()[col * self.stride + row] = value;
}
#[inline]
pub fn is_empty(&self) -> bool {
self.n_rows == 0 || self.n_cols == 0
}
#[inline]
pub fn len(&self) -> usize {
self.n_rows * self.n_cols
}
#[inline]
pub fn as_slice(&self) -> &[f64] {
self.data.as_slice()
}
#[inline]
pub fn as_mut_slice(&mut self) -> &mut [f64] {
self.data.as_mut_slice()
}
pub fn columns(&self) -> Vec<&[f64]> {
let slice = self.data.as_slice();
(0..self.n_cols)
.map(|col| &slice[(col * self.stride)..(col * self.stride + self.n_rows)])
.collect()
}
pub fn columns_mut(&mut self) -> Vec<&mut [f64]> {
let n_rows = self.n_rows;
let stride = self.stride;
let n_cols = self.n_cols;
assert!(
stride >= n_rows,
"Matrix::columns_mut: stride ({stride}) < n_rows ({n_rows}); column slices would alias"
);
let total = stride.checked_mul(n_cols).expect("Matrix::columns_mut: stride * n_cols overflow");
assert!(
self.data.len() >= total,
"Matrix::columns_mut: data buffer shorter than stride * n_cols"
);
let ptr = self.data.as_mut_slice().as_mut_ptr();
let mut result = Vec::with_capacity(n_cols);
for col in 0..n_cols {
let start = col * stride;
unsafe {
let col_ptr = ptr.add(start);
let slice = std::slice::from_raw_parts_mut(col_ptr, n_rows);
result.push(slice);
}
}
result
}
#[inline]
pub fn col(&self, col: usize) -> &[f64] {
assert!(col < self.n_cols, "Col out of bounds");
let slice = self.data.as_slice();
&slice[(col * self.stride)..(col * self.stride + self.n_rows)]
}
#[inline]
pub fn col_mut(&mut self, col: usize) -> &mut [f64] {
assert!(col < self.n_cols, "Col out of bounds");
let start = col * self.stride;
&mut self.data.as_mut_slice()[start..start + self.n_rows]
}
#[inline]
pub fn row(&self, row: usize) -> Vec<f64> {
debug_assert!(row < self.n_rows, "Row out of bounds");
let slice = self.data.as_slice();
(0..self.n_cols).map(|col| slice[col * self.stride + row]).collect()
}
pub fn transpose(&self) -> Self {
let mut dst = Matrix::new(self.n_cols, self.n_rows, self.name.clone());
let src = self.data.as_slice();
let dst_stride = dst.stride;
let dst_slice = dst.data.as_mut_slice();
for j in 0..self.n_cols {
for i in 0..self.n_rows {
dst_slice[i * dst_stride + j] = src[j * self.stride + i];
}
}
dst
}
pub fn extract_rows(&self, indices: &[usize]) -> Self {
let n_new = indices.len();
let mut dst = Matrix::new(n_new, self.n_cols, self.name.clone());
for j in 0..self.n_cols {
let src_col = self.col(j);
let dst_col = dst.col_mut(j);
for (k, &idx) in indices.iter().enumerate() {
dst_col[k] = src_col[idx];
}
}
dst
}
#[inline]
pub fn set_name(&mut self, name: impl Into<String>) {
self.name = Some(name.into());
}
pub fn n_cols(&self) -> usize {
self.n_cols
}
#[inline]
pub fn n_rows(&self) -> usize {
self.n_rows
}
#[inline]
pub fn m(&self) -> i32 {
self.n_rows as i32
}
#[inline]
pub fn n(&self) -> i32 {
self.n_cols as i32
}
#[inline]
pub fn lda(&self) -> i32 {
self.stride as i32
}
#[inline]
pub fn as_strided(&self) -> (&[f64], i32) {
(self.data.as_slice(), self.stride as i32)
}
#[inline]
pub fn as_mut_strided(&mut self) -> (&mut [f64], i32) {
let lda = self.stride as i32;
(self.data.as_mut_slice(), lda)
}
pub fn to_table(self, fields: Vec<Field>) -> Result<Table, MinarrowError> {
if fields.len() != self.n_cols {
return Err(MinarrowError::ShapeError {
message: format!(
"to_table: expected {} fields for {} columns, got {}",
self.n_cols, self.n_cols, fields.len()
),
});
}
let n_rows = self.n_rows;
let n_cols = self.n_cols;
let stride = self.stride;
let name = self.name;
let (shared, base_offset, _len) = unsafe { self.data.into_shared_parts() };
let mut cols = Vec::with_capacity(n_cols);
for (i, field) in fields.into_iter().enumerate() {
let col_offset = base_offset + i * stride;
let buf: Buffer<f64> = Buffer::from_shared_column(shared.clone(), col_offset, n_rows);
let float_arr = FloatArray::new(buf, None);
let array = Array::NumericArray(NumericArray::Float64(Arc::new(float_arr)));
cols.push(FieldArray::new(field, array));
}
Ok(Table::new(name.unwrap_or_default(), Some(cols)))
}
pub fn to_table_gen(self) -> Table {
let n_cols = self.n_cols;
let fields: Vec<Field> = (0..n_cols)
.map(|i| Field::new(format!("col_{}", i), crate::ffi::arrow_dtype::ArrowType::Float64, false, None))
.collect();
self.to_table(fields).unwrap()
}
}
#[cfg(feature = "views")]
impl TableV {
pub fn try_as_matrix_zc(&self) -> Result<Matrix, MinarrowError> {
let n_rows = self.n_rows();
let n_cols = self.n_cols();
if n_cols == 0 {
return Err(MinarrowError::ShapeError {
message: "try_as_matrix_zc: TableV has no active columns".into(),
});
}
let stride = aligned_stride(n_rows);
let active = self.active_col_indices();
let mut base_owner: Option<SharedBuffer> = None;
let mut base_offset: usize = 0;
let mut owner_elem_len: usize = 0;
for (pos, &raw_idx) in active.iter().enumerate() {
let arrayv = &self.cols[raw_idx];
let fa = arrayv.array.num_ref()?.f64_ref()?;
if arrayv.null_count() > 0 {
return Err(MinarrowError::NullError {
message: Some(format!(
"try_as_matrix_zc: column {raw_idx} contains null values"
)),
});
}
let (owner, buf_offset, _) = fa.data.shared_parts().ok_or_else(|| {
MinarrowError::ShapeError {
message: format!(
"try_as_matrix_zc: column {raw_idx} is owned, not a shared view"
),
}
})?;
let col_elem_offset = buf_offset + arrayv.offset;
match &base_owner {
None => {
base_owner = Some(owner.clone());
base_offset = col_elem_offset;
owner_elem_len = owner.len() / std::mem::size_of::<f64>();
}
Some(base) => {
if !SharedBuffer::ptr_eq(base, owner) {
return Err(MinarrowError::ShapeError {
message: format!(
"try_as_matrix_zc: column {raw_idx} lives in a different allocation"
),
});
}
let expected = base_offset + pos * stride;
if col_elem_offset != expected {
return Err(MinarrowError::ShapeError {
message: format!(
"try_as_matrix_zc: column {raw_idx} offset {col_elem_offset} does \
not match expected stride layout {expected}"
),
});
}
}
}
}
let total_elems = stride * n_cols;
if base_offset + total_elems > owner_elem_len {
return Err(MinarrowError::ShapeError {
message: format!(
"try_as_matrix_zc: shared allocation has {owner_elem_len} elements, \
needs {} for column-major layout of {n_rows}x{n_cols} at stride {stride}",
base_offset + total_elems
),
});
}
let data = Buffer::from_shared_column(base_owner.unwrap(), base_offset, total_elems);
let name = if self.name().is_empty() { None } else { Some(self.name().to_string()) };
Ok(Matrix { n_rows, n_cols, stride, data, name })
}
}
impl Shape for Matrix {
fn shape(&self) -> ShapeDim {
ShapeDim::Rank2 {
rows: self.n_rows(),
cols: self.n_cols(),
}
}
}
impl Concatenate for Matrix {
fn concat(self, other: Self) -> Result<Self, MinarrowError> {
if self.n_cols != other.n_cols {
return Err(MinarrowError::IncompatibleTypeError {
from: "Matrix",
to: "Matrix",
message: Some(format!(
"Cannot concatenate matrices with different column counts: {} vs {}",
self.n_cols, other.n_cols
)),
});
}
if self.is_empty() && other.is_empty() {
return Ok(Matrix::new(
0,
0,
None,
));
}
let result_n_rows = self.n_rows + other.n_rows;
let result_n_cols = self.n_cols;
let result_stride = aligned_stride(result_n_rows);
let pad = result_stride - result_n_rows;
let mut result_vec = Vec64::with_capacity(result_stride * result_n_cols);
for col in 0..result_n_cols {
result_vec.extend_from_slice(self.col(col));
result_vec.extend_from_slice(other.col(col));
if pad > 0 {
result_vec.extend_from_slice(&[0.0; ALIGN_ELEMS][..pad]);
}
}
Ok(Matrix {
n_rows: result_n_rows,
n_cols: result_n_cols,
stride: result_stride,
data: Buffer::from_vec64(result_vec),
name: None,
})
}
}
impl fmt::Debug for Matrix {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Matrix{}: {} × {} [col-major]",
self.name.as_deref().map_or(String::new(), |n| format!(" '{}'", n)),
self.n_rows, self.n_cols
)?;
for row in 0..self.n_rows.min(6) {
write!(f, "\n[")?;
for col in 0..self.n_cols.min(8) {
write!(f, " {:8.4}", self.get(row, col))?;
if col != self.n_cols - 1 {
write!(f, ",")?;
}
}
if self.n_cols > 8 {
write!(f, " ...")?;
}
write!(f, " ]")?;
}
if self.n_rows > 6 {
write!(f, "\n...")?;
}
Ok(())
}
}
impl From<Vec<FloatArray<f64>>> for Matrix {
fn from(columns: Vec<FloatArray<f64>>) -> Self {
let n_cols = columns.len();
let n_rows = columns.first().map(|c| c.data.len()).unwrap_or(0);
let stride = aligned_stride(n_rows);
let pad = stride - n_rows;
for col in &columns {
assert_eq!(col.data.len(), n_rows, "Column length mismatch");
}
let mut vec = Vec64::with_capacity(stride * n_cols);
for col in &columns {
vec.extend_from_slice(&col.data);
if pad > 0 {
vec.extend_from_slice(&[0.0; ALIGN_ELEMS][..pad]);
}
}
Matrix { n_rows, n_cols, stride, data: Buffer::from_vec64(vec), name: None }
}
}
impl From<(Vec<FloatArray<f64>>, String)> for Matrix {
fn from((columns, name): (Vec<FloatArray<f64>>, String)) -> Self {
let mut mat = Matrix::from(columns);
mat.name = Some(name);
mat
}
}
impl From<&[FloatArray<f64>]> for Matrix {
fn from(columns: &[FloatArray<f64>]) -> Self {
let n_cols = columns.len();
let n_rows = columns.first().map(|c| c.data.len()).unwrap_or(0);
let stride = aligned_stride(n_rows);
let pad = stride - n_rows;
for col in columns {
assert_eq!(col.data.len(), n_rows, "Column length mismatch");
}
let mut vec = Vec64::with_capacity(stride * n_cols);
for col in columns {
vec.extend_from_slice(&col.data);
if pad > 0 {
vec.extend_from_slice(&[0.0; ALIGN_ELEMS][..pad]);
}
}
Matrix { n_rows, n_cols, stride, data: Buffer::from_vec64(vec), name: None }
}
}
impl TryFrom<&Table> for Matrix {
type Error = MinarrowError;
fn try_from(table: &Table) -> Result<Self, Self::Error> {
let name = if table.name.is_empty() { None } else { Some(table.name.clone()) };
let n_cols = table.n_cols();
let n_rows = table.n_rows;
let stride = aligned_stride(n_rows);
let pad = stride - n_rows;
let mut vec = Vec64::with_capacity(stride * n_cols);
for (col_idx, fa) in table.cols.iter().enumerate() {
let numeric = fa.array.num_ref().map_err(|_| MinarrowError::TypeError {
from: "non-numeric",
to: "Float64",
message: Some(format!("column {} is not numeric", col_idx)),
})?;
let f64_arr = numeric.clone().f64()?;
if f64_arr.data.len() != n_rows {
return Err(MinarrowError::ColumnLengthMismatch {
col: col_idx,
expected: n_rows,
found: f64_arr.data.len(),
});
}
vec.extend_from_slice(f64_arr.data.as_slice());
if pad > 0 {
vec.extend_from_slice(&[0.0; ALIGN_ELEMS][..pad]);
}
}
Ok(Matrix { n_rows, n_cols, stride, data: Buffer::from_vec64(vec), name })
}
}
impl TryFrom<Table> for Matrix {
type Error = MinarrowError;
fn try_from(table: Table) -> Result<Self, Self::Error> {
Matrix::try_from(&table)
}
}
impl From<&[Vec<f64>]> for Matrix {
fn from(columns: &[Vec<f64>]) -> Self {
let n_cols = columns.len();
let n_rows = columns.first().map(|c| c.len()).unwrap_or(0);
let stride = aligned_stride(n_rows);
let pad = stride - n_rows;
for col in columns {
assert_eq!(col.len(), n_rows, "Column length mismatch");
}
let mut vec = Vec64::with_capacity(stride * n_cols);
for col in columns {
vec.extend_from_slice(col);
if pad > 0 {
vec.extend_from_slice(&[0.0; ALIGN_ELEMS][..pad]);
}
}
Matrix { n_rows, n_cols, stride, data: Buffer::from_vec64(vec), name: None }
}
}
impl<'a> From<(&'a [f64], usize, usize, Option<String>)> for Matrix {
fn from((slice, n_rows, n_cols, name): (&'a [f64], usize, usize, Option<String>)) -> Self {
assert_eq!(slice.len(), n_rows * n_cols, "Slice shape mismatch");
Matrix::from_f64_unaligned(slice, n_rows, n_cols, name)
}
}
#[cfg(feature = "views")]
impl TryFrom<&TableV> for Matrix {
type Error = MinarrowError;
fn try_from(view: &TableV) -> Result<Self, Self::Error> {
let name = if view.name().is_empty() { None } else { Some(view.name().to_string()) };
let n_rows = view.n_rows();
let active = view.active_col_indices();
let n_cols = active.len();
let stride = aligned_stride(n_rows);
let pad = stride - n_rows;
let mut vec = Vec64::with_capacity(stride * n_cols);
for &col_idx in &active {
let (array, offset, len) = view.cols[col_idx].as_tuple_ref();
let fa = array.num_ref()?.f64_ref()?;
vec.extend_from_slice(&fa.data.as_slice()[offset..offset + len]);
if pad > 0 {
vec.extend_from_slice(&[0.0; ALIGN_ELEMS][..pad]);
}
}
Ok(Matrix { n_rows, n_cols, stride, data: Buffer::from_vec64(vec), name })
}
}
#[cfg(feature = "views")]
impl TableV {
pub fn try_as_matrix(&self) -> Result<Matrix, MinarrowError> {
Matrix::try_from(self)
}
}
impl<'a> IntoIterator for &'a Matrix {
type Item = &'a f64;
type IntoIter = std::slice::Iter<'a, f64>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.data.as_slice().iter()
}
}
impl<'a> IntoIterator for &'a mut Matrix {
type Item = &'a mut f64;
type IntoIter = std::slice::IterMut<'a, f64>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.data.as_mut_slice().iter_mut()
}
}
impl IntoIterator for Matrix {
type Item = f64;
type IntoIter = <Vec64<f64> as IntoIterator>::IntoIter;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.data.into_iter()
}
}
#[cfg(all(test, feature = "views"))]
mod try_as_matrix_zc_tests {
use super::*;
#[test]
fn round_trip_zero_copy() {
let src = Matrix::try_from_cols(&[
FloatArray::from_slice(&[1.0_f64, 2.0, 3.0, 4.0, 5.0]),
FloatArray::from_slice(&[6.0_f64, 7.0, 8.0, 9.0, 10.0]),
FloatArray::from_slice(&[11.0_f64, 12.0, 13.0, 14.0, 15.0]),
], Some("m".into())).unwrap();
let expected: Vec<f64> = src.data.as_slice().to_vec();
let stride_src = src.stride;
let table = src.to_table_gen();
let view = TableV::from_table(table, 0, 5);
let zc = view.try_as_matrix_zc().expect("zero-copy should succeed");
assert_eq!(zc.n_rows, 5);
assert_eq!(zc.n_cols, 3);
assert_eq!(zc.stride, stride_src);
assert_eq!(zc.data.as_slice(), expected.as_slice());
assert!(zc.data.is_shared(), "ZC result must reuse the shared allocation");
}
#[test]
fn cow_on_mutate_preserves_source() {
let src = Matrix::try_from_cols(&[
FloatArray::from_slice(&[1.0_f64, 2.0, 3.0]),
FloatArray::from_slice(&[4.0_f64, 5.0, 6.0]),
], None).unwrap();
let table = src.to_table_gen();
let view = TableV::from_table(table.clone(), 0, 3);
let mut zc = view.try_as_matrix_zc().unwrap();
zc.set(0, 0, 99.0);
assert_eq!(zc.get(0, 0), 99.0);
assert!(!zc.data.is_shared(), "mutation must trigger COW");
let original_table_first = {
let tv = TableV::from_table(table, 0, 3);
let zc2 = tv.try_as_matrix_zc().unwrap();
zc2.get(0, 0)
};
assert_eq!(original_table_first, 1.0);
}
#[test]
fn rejects_owned_buffer_columns() {
let a = crate::fa_f64!("a", 1.0, 2.0, 3.0);
let b = crate::fa_f64!("b", 4.0, 5.0, 6.0);
let table = Table::new("t".into(), Some(vec![a, b]));
let view = TableV::from_table(table, 0, 3);
let err = view.try_as_matrix_zc().expect_err("owned columns cannot be zero-copy");
assert!(
matches!(err, MinarrowError::ShapeError { .. }),
"expected ShapeError, got {err:?}"
);
}
#[test]
fn rejects_null_masked_columns() {
let src = Matrix::try_from_cols(&[
FloatArray::from_slice(&[1.0_f64, 2.0, 3.0]),
FloatArray::from_slice(&[4.0_f64, 5.0, 6.0]),
], None).unwrap();
let mut table = src.to_table_gen();
if let crate::Array::NumericArray(crate::NumericArray::Float64(arc_fa)) =
&mut table.cols[0].array
{
let mut fa = (**arc_fa).clone();
let mut mask = crate::Bitmask::new_set_all(3, true);
mask.set(0, false);
fa.null_mask = Some(mask);
*arc_fa = std::sync::Arc::new(fa);
} else {
panic!("expected Float64 column");
}
let view = TableV::from_table(table, 0, 3);
let err = view.try_as_matrix_zc().expect_err("column with nulls must reject");
assert!(
matches!(err, MinarrowError::NullError { .. }),
"expected NullError, got {err:?}"
);
}
#[test]
fn rejects_empty_view() {
let table = Table::new("t".into(), Some(Vec::new()));
let view = TableV::from_table(table, 0, 0);
let err = view.try_as_matrix_zc().expect_err("empty view must reject");
assert!(matches!(err, MinarrowError::ShapeError { .. }));
}
#[test]
fn rejects_non_f64_columns() {
let a = crate::fa_i32!("a", 1, 2, 3);
let table = Table::new("t".into(), Some(vec![a]));
let view = TableV::from_table(table, 0, 3);
let err = view.try_as_matrix_zc().expect_err("non-f64 column must reject");
assert!(matches!(err, MinarrowError::TypeError { .. }));
}
#[test]
fn rejects_column_order_mismatch() {
let m_a = Matrix::try_from_cols(&[
FloatArray::from_slice(&[1.0_f64, 2.0, 3.0]),
FloatArray::from_slice(&[4.0_f64, 5.0, 6.0]),
], None).unwrap();
let m_b = Matrix::try_from_cols(&[
FloatArray::from_slice(&[10.0_f64, 20.0, 30.0]),
FloatArray::from_slice(&[40.0_f64, 50.0, 60.0]),
], None).unwrap();
let mut table_a = m_a.to_table_gen();
let table_b = m_b.to_table_gen();
table_a.cols[1] = table_b.cols[0].clone();
table_a.n_rows = 3;
let view = TableV::from_table(table_a, 0, 3);
let err = view.try_as_matrix_zc().expect_err("mixed allocations must reject");
assert!(matches!(err, MinarrowError::ShapeError { .. }));
}
#[test]
fn rejects_column_offset_misalignment() {
let src = Matrix::try_from_cols(&[
FloatArray::from_slice(&[1.0_f64, 2.0, 3.0]),
FloatArray::from_slice(&[4.0_f64, 5.0, 6.0]),
FloatArray::from_slice(&[7.0_f64, 8.0, 9.0]),
], None).unwrap();
let mut table = src.to_table_gen();
table.cols.swap(0, 2);
let view = TableV::from_table(table, 0, 3);
let err = view.try_as_matrix_zc().expect_err("reordered columns must reject");
assert!(matches!(err, MinarrowError::ShapeError { .. }));
}
#[test]
fn try_as_matrix_copies_owned_columns() {
let a = crate::fa_f64!("a", 1.0, 2.0, 3.0);
let b = crate::fa_f64!("b", 4.0, 5.0, 6.0);
let table = Table::new("t".into(), Some(vec![a, b]));
let view = TableV::from_table(table, 0, 3);
let m = view.try_as_matrix().expect("copy path always works for f64 columns");
assert_eq!(m.n_rows, 3);
assert_eq!(m.n_cols, 2);
assert_eq!(m.col(0), &[1.0, 2.0, 3.0]);
assert_eq!(m.col(1), &[4.0, 5.0, 6.0]);
assert!(!m.data.is_shared(), "try_as_matrix produces owned data");
}
#[test]
fn try_as_matrix_respects_window_offset() {
let a = crate::fa_f64!("a", 10.0, 11.0, 12.0, 13.0, 14.0);
let b = crate::fa_f64!("b", 20.0, 21.0, 22.0, 23.0, 24.0);
let table = Table::new("t".into(), Some(vec![a, b]));
let view = TableV::from_table(table, 1, 3);
let m = view.try_as_matrix().unwrap();
assert_eq!(m.n_rows, 3);
assert_eq!(m.col(0), &[11.0, 12.0, 13.0]);
assert_eq!(m.col(1), &[21.0, 22.0, 23.0]);
}
#[test]
fn try_as_matrix_rejects_non_f64() {
let a = crate::fa_i32!("a", 1, 2, 3);
let table = Table::new("t".into(), Some(vec![a]));
let view = TableV::from_table(table, 0, 3);
let err = view.try_as_matrix().expect_err("non-f64 column must reject");
assert!(matches!(err, MinarrowError::TypeError { .. }));
}
}