mirror of
https://github.com/Magnus167/rustframe.git
synced 2025-08-20 04:19:59 +00:00
1899 lines
80 KiB
Rust
1899 lines
80 KiB
Rust
use crate::matrix::Matrix;
|
||
|
||
use chrono::NaiveDate;
|
||
use std::collections::HashMap;
|
||
|
||
use std::fmt;
|
||
use std::ops::{Index, IndexMut, Not, Range};
|
||
|
||
// --- Helper Enums and Structs for Indexing ---
|
||
|
||
/// Represents the different types of row indices a Frame can have.
|
||
#[derive(Debug, Clone, PartialEq, Eq)] // Added Eq for HashMaps etc.
|
||
pub enum RowIndex {
|
||
/// Integer-based index (e.g., 0, 1, 2, ...). Values must be unique.
|
||
Int(Vec<usize>),
|
||
/// Date-based index. Values must be unique. Order is preserved as given.
|
||
Date(Vec<NaiveDate>),
|
||
/// Default range index (0..num_rows) used when no specific index is provided.
|
||
Range(Range<usize>),
|
||
}
|
||
|
||
impl RowIndex {
|
||
/// Returns the number of elements in the index.
|
||
///
|
||
/// # Examples
|
||
/// ```
|
||
/// # use rustframe::frame::RowIndex;
|
||
/// # use chrono::NaiveDate;
|
||
/// # fn d(y: i32, m: u32, d: u32) -> NaiveDate { NaiveDate::from_ymd_opt(y,m,d).unwrap() }
|
||
/// let idx_int = RowIndex::Int(vec![10, 20, 5]);
|
||
/// assert_eq!(idx_int.len(), 3);
|
||
///
|
||
/// let idx_date = RowIndex::Date(vec![d(2024,1,1), d(2024,1,2)]);
|
||
/// assert_eq!(idx_date.len(), 2);
|
||
///
|
||
/// let idx_range = RowIndex::Range(0..5);
|
||
/// assert_eq!(idx_range.len(), 5);
|
||
///
|
||
/// let idx_empty_int = RowIndex::Int(vec![]);
|
||
/// assert_eq!(idx_empty_int.len(), 0);
|
||
/// ```
|
||
pub fn len(&self) -> usize {
|
||
match self {
|
||
RowIndex::Int(v) => v.len(),
|
||
RowIndex::Date(v) => v.len(),
|
||
RowIndex::Range(r) => r.end.saturating_sub(r.start),
|
||
}
|
||
}
|
||
|
||
/// Checks if the index is empty.
|
||
///
|
||
/// # Examples
|
||
/// ```
|
||
/// # use rustframe::frame::RowIndex;
|
||
/// # use chrono::NaiveDate;
|
||
/// let idx_int = RowIndex::Int(vec![10, 20, 5]);
|
||
/// assert!(!idx_int.is_empty());
|
||
///
|
||
/// let idx_range = RowIndex::Range(0..0);
|
||
/// assert!(idx_range.is_empty());
|
||
///
|
||
/// let idx_empty_date = RowIndex::Date(vec![]);
|
||
/// assert!(idx_empty_date.is_empty());
|
||
/// ```
|
||
pub fn is_empty(&self) -> bool {
|
||
self.len() == 0
|
||
}
|
||
}
|
||
|
||
/// Internal helper for fast lookups from index value to physical row position.
|
||
#[derive(Debug, Clone, PartialEq, Eq)] // Added Eq
|
||
enum RowIndexLookup {
|
||
Int(HashMap<usize, usize>),
|
||
Date(HashMap<NaiveDate, usize>),
|
||
None, // Used for Range index
|
||
}
|
||
|
||
// --- Frame Struct Definition ---
|
||
|
||
/// A data frame – a Matrix with string-identified columns and a typed row index.
|
||
///
|
||
/// `Frame` extends the concept of a `Matrix` by adding named columns
|
||
/// and an index for rows, which can be integers, dates, or a default range.
|
||
/// It allows accessing data by column name (using `[]`) and by row index value
|
||
/// (using `get_row`, `get_row_mut`, `get_row_date`, `get_row_date_mut` methods).
|
||
///
|
||
/// Direct row indexing with `frame[row_key]` is not supported due to Rust's
|
||
/// lifetime and ownership rules clashing with the `std::ops::Index` trait when
|
||
/// returning row views or temporary copies. Use the explicit `get_row*` methods instead.
|
||
///
|
||
/// The internal data is stored column-major in a `Matrix<T>`.
|
||
///
|
||
/// # Type Parameters
|
||
///
|
||
/// * `T`: The data type of the elements within the frame. Must be `Clone + PartialEq`.
|
||
/// Numerical/Boolean traits required for specific operations (e.g., `Add`, `BitAnd`).
|
||
///
|
||
/// # Examples
|
||
///
|
||
/// ```
|
||
/// use rustframe::frame::{Frame, RowIndex};
|
||
/// use rustframe::matrix::Matrix; // Assume Matrix is available
|
||
/// use chrono::NaiveDate;
|
||
///
|
||
/// // Helper fn for dates
|
||
/// fn d(y: i32, m: u32, d: u32) -> NaiveDate { NaiveDate::from_ymd_opt(y,m,d).unwrap() }
|
||
///
|
||
/// // --- Example 1: Basic Creation and Access ---
|
||
/// let matrix_f64 = Matrix::from_cols(vec![
|
||
/// vec![1.0, 2.0, 3.0], // Column "A"
|
||
/// vec![4.0, 5.0, 6.0], // Column "B"
|
||
/// ]);
|
||
/// let mut frame1 = Frame::new(matrix_f64, vec!["A", "B"], None);
|
||
///
|
||
/// assert_eq!(frame1.columns(), &["A", "B"]);
|
||
/// assert_eq!(frame1["A"], vec![1.0, 2.0, 3.0]); // Compare with slice literal
|
||
/// assert_eq!(frame1.index(), &RowIndex::Range(0..3));
|
||
/// let row0 = frame1.get_row(0);
|
||
/// assert_eq!(row0["A"], 1.0);
|
||
/// assert_eq!(row0[1], 4.0); // Column "B"
|
||
///
|
||
/// // --- Example 2: Date Index and Mutation ---
|
||
/// let dates = vec![d(2024, 1, 1), d(2024, 1, 2)];
|
||
/// let matrix_string = Matrix::from_cols(vec![ vec!["X".to_string(), "Y".to_string()], ]);
|
||
/// let mut frame2 = Frame::new(matrix_string, vec!["Label"], Some(RowIndex::Date(dates.clone())));
|
||
///
|
||
/// assert_eq!(frame2.index(), &RowIndex::Date(dates));
|
||
/// assert_eq!(frame2.get_row_date(d(2024, 1, 2))["Label"], "Y");
|
||
/// frame2.get_row_date_mut(d(2024, 1, 1)).set_by_index(0, "Z".to_string());
|
||
/// assert_eq!(frame2["Label"], vec!["Z", "Y"]);
|
||
///
|
||
/// // --- Example 3: Element-wise Addition ---
|
||
/// let m1 = Matrix::from_cols(vec![ vec![1.0, 2.0], vec![3.0, 4.0] ]);
|
||
/// let f1 = Frame::new(m1, vec!["C1", "C2"], None);
|
||
/// let m2 = Matrix::from_cols(vec![ vec![0.1, 0.2], vec![0.3, 0.4] ]);
|
||
/// let f2 = Frame::new(m2, vec!["C1", "C2"], None);
|
||
///
|
||
/// let f_sum = &f1 + &f2;
|
||
/// assert_eq!(f_sum["C1"], vec![1.1, 2.2]);
|
||
/// assert_eq!(f_sum["C2"], vec![3.3, 4.4]);
|
||
/// assert_eq!(f_sum.index(), &RowIndex::Range(0..2));
|
||
///
|
||
/// // --- Example 4: Element-wise Multiplication ---
|
||
/// let f_prod = &f1 * &f2;
|
||
/// // Use approx comparison for floats if necessary
|
||
/// assert!((f_prod["C1"][0] - 0.1_f64).abs() < 1e-9); // 1.0 * 0.1
|
||
/// assert!((f_prod["C1"][1] - 0.4_f64).abs() < 1e-9); // 2.0 * 0.2
|
||
/// assert!((f_prod["C2"][0] - 0.9_f64).abs() < 1e-9); // 3.0 * 0.3
|
||
/// assert!((f_prod["C2"][1] - 1.6_f64).abs() < 1e-9); // 4.0 * 0.4
|
||
///
|
||
/// // --- Example 5: Column Manipulation and Sorting ---
|
||
/// let mut frame_manip = Frame::new(
|
||
/// Matrix::from_cols(vec![ vec![1, 2], vec![3, 4] ]), // Example uses i32
|
||
/// vec!["DataC", "DataA"], // Column names (out of order)
|
||
/// None
|
||
/// );
|
||
/// assert_eq!(frame_manip["DataC"], vec![1, 2]);
|
||
/// assert_eq!(frame_manip["DataA"], vec![3, 4]);
|
||
/// frame_manip.add_column("DataB", vec![5, 6]);
|
||
/// assert_eq!(frame_manip.columns(), &["DataC", "DataA", "DataB"]);
|
||
/// frame_manip.rename("DataA", "DataX"); // Rename A -> X
|
||
/// assert_eq!(frame_manip.columns(), &["DataC", "DataX", "DataB"]);
|
||
/// assert_eq!(frame_manip["DataX"], vec![3, 4]); // X has A's original data
|
||
/// let deleted_c = frame_manip.delete_column("DataC");
|
||
/// assert_eq!(deleted_c, vec![1, 2]);
|
||
/// assert_eq!(frame_manip.columns(), &["DataX", "DataB"]); // Order after delete
|
||
/// frame_manip.sort_columns(); // Sorts ["DataX", "DataB"] -> ["DataB", "DataX"]
|
||
/// assert_eq!(frame_manip.columns(), &["DataB", "DataX"]);
|
||
/// assert_eq!(frame_manip["DataB"], vec![5, 6]); // B keeps its data
|
||
/// assert_eq!(frame_manip["DataX"], vec![3, 4]); // X keeps its data (originally A's)
|
||
/// ```
|
||
// Implement Debug manually for Frame
|
||
impl<T: Clone + PartialEq + fmt::Debug> fmt::Debug for Frame<T> {
|
||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
f.debug_struct("Frame")
|
||
.field("column_names", &self.column_names)
|
||
.field("index", &self.index)
|
||
.field("matrix_dims", &(self.matrix.rows(), self.matrix.cols()))
|
||
.field("col_lookup", &self.col_lookup)
|
||
.field("index_lookup", &self.index_lookup)
|
||
// Optionally hide matrix data in Debug unless specifically requested
|
||
// .field("matrix", &self.matrix)
|
||
.finish()
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, PartialEq)] // Removed Eq as T doesn't require Eq
|
||
pub struct Frame<T: Clone + PartialEq> {
|
||
/// Vector holding the column names in their current order.
|
||
column_names: Vec<String>,
|
||
/// The underlying column-major matrix storing the data.
|
||
matrix: Matrix<T>,
|
||
/// Maps a column name to its physical column index for **O(1)** lookup.
|
||
col_lookup: HashMap<String, usize>,
|
||
/// The row index values (Int, Date, or Range).
|
||
index: RowIndex,
|
||
/// Internal lookup for mapping index values to physical row positions.
|
||
index_lookup: RowIndexLookup,
|
||
}
|
||
|
||
impl<T: Clone + PartialEq> Frame<T> {
|
||
/* ---------- Constructors ---------- */
|
||
|
||
/// Creates a new [`Frame`] from a matrix, column names, and an optional row index.
|
||
/// Panics if the underlying `Matrix` requirements are not met (e.g., rows>0, cols>0 for standard constructors),
|
||
/// or if column name/index constraints are violated.
|
||
pub fn new<L: Into<String>>(matrix: Matrix<T>, names: Vec<L>, index: Option<RowIndex>) -> Self {
|
||
// --- Column Validation ---
|
||
if matrix.cols() != names.len() {
|
||
panic!(
|
||
"Frame::new: column name count mismatch (names: {}, matrix: {})",
|
||
names.len(),
|
||
matrix.cols()
|
||
);
|
||
}
|
||
// Note: Matrix constructors enforce rows > 0 and cols > 0 based on provided code.
|
||
// Therefore, we don't need to explicitly handle 0-row/0-col cases here,
|
||
// as they would have already panicked during Matrix creation.
|
||
|
||
let mut col_lookup = HashMap::with_capacity(names.len());
|
||
let column_names: Vec<String> = names
|
||
.into_iter()
|
||
.enumerate()
|
||
.map(|(i, n)| {
|
||
let s = n.into();
|
||
if col_lookup.insert(s.clone(), i).is_some() {
|
||
panic!("Frame::new: duplicate column label: {}", s);
|
||
}
|
||
s
|
||
})
|
||
.collect();
|
||
|
||
// --- Index Validation and Processing ---
|
||
let num_rows = matrix.rows();
|
||
let (index_values, index_lookup) = match index {
|
||
Some(RowIndex::Int(vals)) => {
|
||
if vals.len() != num_rows {
|
||
panic!(
|
||
"Frame::new: Int index length ({}) mismatch matrix rows ({})",
|
||
vals.len(),
|
||
num_rows
|
||
);
|
||
}
|
||
let mut lookup = HashMap::with_capacity(num_rows);
|
||
for (physical_row, index_val) in vals.iter().enumerate() {
|
||
if lookup.insert(*index_val, physical_row).is_some() {
|
||
panic!("Frame::new: duplicate Int index value: {}", index_val);
|
||
}
|
||
}
|
||
(RowIndex::Int(vals), RowIndexLookup::Int(lookup))
|
||
}
|
||
Some(RowIndex::Date(vals)) => {
|
||
if vals.len() != num_rows {
|
||
panic!(
|
||
"Frame::new: Date index length ({}) mismatch matrix rows ({})",
|
||
vals.len(),
|
||
num_rows
|
||
);
|
||
}
|
||
let mut lookup = HashMap::with_capacity(num_rows);
|
||
for (physical_row, index_val) in vals.iter().enumerate() {
|
||
if lookup.insert(*index_val, physical_row).is_some() {
|
||
panic!("Frame::new: duplicate Date index value: {}", index_val);
|
||
}
|
||
}
|
||
(RowIndex::Date(vals), RowIndexLookup::Date(lookup))
|
||
}
|
||
Some(RowIndex::Range(_)) => {
|
||
panic!(
|
||
"Frame::new: Cannot explicitly provide a Range index. Use None for default range."
|
||
);
|
||
}
|
||
None => (RowIndex::Range(0..num_rows), RowIndexLookup::None),
|
||
};
|
||
|
||
Self {
|
||
matrix,
|
||
column_names,
|
||
col_lookup,
|
||
index: index_values,
|
||
index_lookup,
|
||
}
|
||
}
|
||
|
||
/* ---------- Accessors ---------- */
|
||
/// Returns an immutable reference to the underlying Matrix.
|
||
#[inline]
|
||
pub fn matrix(&self) -> &Matrix<T> {
|
||
&self.matrix
|
||
}
|
||
/// Returns a mutable reference to the underlying Matrix.
|
||
/// Use with caution, as direct matrix manipulation bypasses Frame's name/index tracking.
|
||
#[inline]
|
||
pub fn matrix_mut(&mut self) -> &mut Matrix<T> {
|
||
&mut self.matrix
|
||
}
|
||
/// Returns a slice containing the current column names in order.
|
||
#[inline]
|
||
pub fn columns(&self) -> &[String] {
|
||
&self.column_names
|
||
}
|
||
/// Returns a reference to the `RowIndex`.
|
||
#[inline]
|
||
pub fn index(&self) -> &RowIndex {
|
||
&self.index
|
||
}
|
||
/// Returns the number of rows in the frame.
|
||
#[inline]
|
||
pub fn rows(&self) -> usize {
|
||
self.matrix.rows()
|
||
}
|
||
/// Returns the number of columns in the frame.
|
||
#[inline]
|
||
pub fn cols(&self) -> usize {
|
||
self.matrix.cols()
|
||
}
|
||
/// Returns the physical column index for a given column name, if it exists.
|
||
#[inline]
|
||
pub fn column_index(&self, name: &str) -> Option<usize> {
|
||
self.col_lookup.get(name).copied()
|
||
}
|
||
|
||
/// Internal helper to find the physical row index based on the index key and type.
|
||
fn get_physical_row_index<Idx>(&self, index_key: Idx) -> usize
|
||
where
|
||
Self: RowIndexLookupHelper<Idx>, // Uses the helper trait below
|
||
{
|
||
<Self as RowIndexLookupHelper<Idx>>::lookup_row_index(
|
||
index_key,
|
||
&self.index,
|
||
&self.index_lookup,
|
||
)
|
||
}
|
||
|
||
/// Returns an immutable slice representing the data in the specified column.
|
||
/// Panics if the column name does not exist.
|
||
pub fn column(&self, name: &str) -> &[T] {
|
||
let idx = self
|
||
.column_index(name)
|
||
.unwrap_or_else(|| panic!("Frame::column: unknown column label: '{}'", name));
|
||
self.matrix.column(idx)
|
||
}
|
||
/// Returns a mutable slice representing the data in the specified column.
|
||
/// Panics if the column name does not exist.
|
||
pub fn column_mut(&mut self, name: &str) -> &mut [T] {
|
||
let idx = self
|
||
.column_index(name)
|
||
.unwrap_or_else(|| panic!("Frame::column_mut: unknown column label: '{}'", name));
|
||
self.matrix.column_mut(idx)
|
||
}
|
||
|
||
/* ---------- Row Access Methods ---------- */
|
||
/// Returns an immutable view of the row corresponding to the given integer index key.
|
||
/// Panics if the key is not found in the `RowIndex::Int` or `RowIndex::Range`.
|
||
/// Panics if the index is `RowIndex::Date`.
|
||
pub fn get_row(&self, index_key: usize) -> FrameRowView<'_, T> {
|
||
let idx = self.get_physical_row_index(index_key);
|
||
FrameRowView {
|
||
frame: self,
|
||
physical_row_idx: idx,
|
||
}
|
||
}
|
||
/// Returns a mutable view of the row corresponding to the given integer index key.
|
||
/// Panics if the key is not found in the `RowIndex::Int` or `RowIndex::Range`.
|
||
/// Panics if the index is `RowIndex::Date`.
|
||
pub fn get_row_mut(&mut self, index_key: usize) -> FrameRowViewMut<'_, T> {
|
||
let idx = self.get_physical_row_index(index_key);
|
||
FrameRowViewMut {
|
||
frame: self,
|
||
physical_row_idx: idx,
|
||
}
|
||
}
|
||
/// Returns an immutable view of the row corresponding to the given date index key.
|
||
/// Panics if the key is not found in the `RowIndex::Date`.
|
||
/// Panics if the index is `RowIndex::Int` or `RowIndex::Range`.
|
||
pub fn get_row_date(&self, index_key: NaiveDate) -> FrameRowView<'_, T> {
|
||
let idx = self.get_physical_row_index(index_key);
|
||
FrameRowView {
|
||
frame: self,
|
||
physical_row_idx: idx,
|
||
}
|
||
}
|
||
/// Returns a mutable view of the row corresponding to the given date index key.
|
||
/// Panics if the key is not found in the `RowIndex::Date`.
|
||
/// Panics if the index is `RowIndex::Int` or `RowIndex::Range`.
|
||
pub fn get_row_date_mut(&mut self, index_key: NaiveDate) -> FrameRowViewMut<'_, T> {
|
||
let idx = self.get_physical_row_index(index_key);
|
||
FrameRowViewMut {
|
||
frame: self,
|
||
physical_row_idx: idx,
|
||
}
|
||
}
|
||
|
||
/* ---------- Column manipulation ---------- */
|
||
|
||
/// Internal helper to swap two columns. Updates matrix, column names, and lookup map.
|
||
/// This is not intended for direct user consumption. Use `sort_columns`.
|
||
fn _swap_columns_internal(&mut self, a: &str, b: &str) {
|
||
// Avoid cloning strings if possible by getting indices first
|
||
let maybe_ia = self.column_index(a);
|
||
let maybe_ib = self.column_index(b);
|
||
|
||
let ia = maybe_ia.unwrap_or_else(|| {
|
||
panic!("Frame::_swap_columns_internal: unknown column label: {}", a)
|
||
});
|
||
let ib = maybe_ib.unwrap_or_else(|| {
|
||
panic!("Frame::_swap_columns_internal: unknown column label: {}", b)
|
||
});
|
||
|
||
if ia == ib {
|
||
return; // No-op
|
||
}
|
||
|
||
// 1. Swap data in the underlying matrix
|
||
self.matrix.swap_columns(ia, ib);
|
||
|
||
// 2. Swap names in the ordered list
|
||
self.column_names.swap(ia, ib);
|
||
|
||
// 3. Update the lookup map to reflect the new physical indices
|
||
// The column originally named 'a' is now at physical index 'ib'
|
||
self.col_lookup.insert(a.to_string(), ib);
|
||
// The column originally named 'b' is now at physical index 'ia'
|
||
self.col_lookup.insert(b.to_string(), ia);
|
||
}
|
||
|
||
/// Renames an existing column.
|
||
/// Panics if the old name doesn't exist, or the new name already exists or is the same as the old name.
|
||
pub fn rename<L: Into<String>>(&mut self, old: &str, new: L) {
|
||
let new_name = new.into();
|
||
if old == new_name {
|
||
panic!(
|
||
"Frame::rename: new name '{}' cannot be the same as the old name",
|
||
new_name
|
||
);
|
||
}
|
||
let idx = self
|
||
.column_index(old)
|
||
.unwrap_or_else(|| panic!("Frame::rename: unknown column label: '{}'", old));
|
||
if self.col_lookup.contains_key(&new_name) {
|
||
panic!(
|
||
"Frame::rename: new column name '{}' already exists",
|
||
new_name
|
||
);
|
||
}
|
||
|
||
// Update lookup map
|
||
self.col_lookup.remove(old);
|
||
self.col_lookup.insert(new_name.clone(), idx);
|
||
|
||
// Update ordered name list
|
||
self.column_names[idx] = new_name;
|
||
}
|
||
|
||
/// Adds a new column to the end of the frame.
|
||
/// Panics if the column name already exists or if the data length doesn't match the number of rows.
|
||
pub fn add_column<L: Into<String>>(&mut self, name: L, column_data: Vec<T>) {
|
||
let name_str = name.into();
|
||
if self.col_lookup.contains_key(&name_str) {
|
||
panic!("Frame::add_column: duplicate column label: {}", name_str);
|
||
}
|
||
// Matrix::add_column checks length against self.rows().
|
||
// This assumes self.rows() > 0 because Matrix constructors enforce this.
|
||
let new_col_idx = self.matrix.cols();
|
||
self.matrix.add_column(new_col_idx, column_data); // Add to end
|
||
|
||
// Update frame metadata
|
||
self.column_names.push(name_str.clone());
|
||
self.col_lookup.insert(name_str, new_col_idx);
|
||
}
|
||
|
||
/// Deletes a column from the frame by name.
|
||
/// Returns the data of the deleted column.
|
||
/// Panics if the column name does not exist.
|
||
pub fn delete_column(&mut self, name: &str) -> Vec<T> {
|
||
let idx = self
|
||
.column_index(name)
|
||
.unwrap_or_else(|| panic!("Frame::delete_column: unknown column label: '{}'", name));
|
||
|
||
// Retrieve data before deleting. Requires Clone.
|
||
// Note: Matrix::delete_column might be more efficient if it returned the data.
|
||
let deleted_data = self.matrix.column(idx).to_vec();
|
||
|
||
// Delete from matrix
|
||
self.matrix.delete_column(idx);
|
||
|
||
// Remove from metadata
|
||
self.column_names.remove(idx);
|
||
self.col_lookup.remove(name); // Remove the specific entry
|
||
|
||
// Update indices in the lookup map for columns that shifted
|
||
// This is necessary because deleting column `idx` shifts all columns > `idx` one position to the left.
|
||
self.rebuild_col_lookup(); // Rebuild is simpler than manually adjusting indices
|
||
|
||
// Handle index if last column removed
|
||
// Since Matrix must have cols >= 1, delete_column cannot result in 0 cols
|
||
// unless the matrix started with 1 col. Matrix must also have rows >= 1.
|
||
// So, after delete_column, the matrix will still have rows >= 1, but potentially 0 cols.
|
||
// This state (rows > 0, cols = 0) might be invalid depending on Matrix guarantees.
|
||
// Assuming Matrix *allows* this state after delete_column:
|
||
if self.cols() == 0 {
|
||
// Ensure index matches row count.
|
||
if let RowIndex::Range(_) = self.index {
|
||
// The range should reflect the number of rows remaining.
|
||
self.index = RowIndex::Range(0..self.rows());
|
||
self.index_lookup = RowIndexLookup::None;
|
||
}
|
||
// If the index was Int or Date, it remains unchanged as it tracks rows.
|
||
}
|
||
|
||
deleted_data
|
||
}
|
||
|
||
/// Sorts the columns of the Frame alphabetically by name in place.
|
||
/// The data remains associated with its original column name.
|
||
pub fn sort_columns(&mut self) {
|
||
let n = self.column_names.len();
|
||
if n <= 1 {
|
||
return; // Already sorted (or empty)
|
||
}
|
||
|
||
// Simple selection sort based on column names.
|
||
for i in 0..n {
|
||
let mut min_idx = i;
|
||
for j in (i + 1)..n {
|
||
if self.column_names[j] < self.column_names[min_idx] {
|
||
min_idx = j;
|
||
}
|
||
}
|
||
if min_idx != i {
|
||
// Get names at current physical positions i and min_idx before swapping
|
||
let col_i_name = self.column_names[i].clone();
|
||
let col_min_name = self.column_names[min_idx].clone();
|
||
|
||
// Use the internal swap function which handles matrix, names, and lookup
|
||
self._swap_columns_internal(&col_i_name, &col_min_name);
|
||
// After swap, col_i_name is at min_idx, col_min_name is at i.
|
||
// The name list `self.column_names` is updated by the swap.
|
||
}
|
||
}
|
||
|
||
// Final check for internal consistency (optional, for debugging)
|
||
#[cfg(debug_assertions)]
|
||
{
|
||
let mut temp_lookup = HashMap::with_capacity(self.cols());
|
||
for (idx, name) in self.column_names.iter().enumerate() {
|
||
temp_lookup.insert(name.clone(), idx);
|
||
}
|
||
assert_eq!(
|
||
self.col_lookup, temp_lookup,
|
||
"Internal col_lookup inconsistent after sort_columns"
|
||
);
|
||
}
|
||
}
|
||
|
||
/* ---------- Helpers ---------- */
|
||
/// Rebuilds the `col_lookup` map based on the current `column_names` order.
|
||
/// Used internally after operations that change multiple column indices (like delete).
|
||
fn rebuild_col_lookup(&mut self) {
|
||
self.col_lookup.clear();
|
||
for (i, name) in self.column_names.iter().enumerate() {
|
||
self.col_lookup.insert(name.clone(), i);
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Helper Trait for Row Index Lookup ---
|
||
/// Internal trait to abstract the logic for looking up physical row indices.
|
||
trait RowIndexLookupHelper<Idx> {
|
||
fn lookup_row_index(key: Idx, index_values: &RowIndex, index_lookup: &RowIndexLookup) -> usize;
|
||
}
|
||
|
||
/// Implementation for `usize` keys (used for `RowIndex::Int` and `RowIndex::Range`).
|
||
impl<T: Clone + PartialEq> RowIndexLookupHelper<usize> for Frame<T> {
|
||
fn lookup_row_index(
|
||
key: usize,
|
||
index_values: &RowIndex,
|
||
index_lookup: &RowIndexLookup,
|
||
) -> usize {
|
||
match (index_values, index_lookup) {
|
||
(RowIndex::Int(_), RowIndexLookup::Int(lookup)) => {
|
||
// Use the HashMap for O(1) average lookup
|
||
*lookup.get(&key).unwrap_or_else(|| {
|
||
panic!("Frame index: integer key {} not found in Int index", key)
|
||
})
|
||
}
|
||
(RowIndex::Range(range), RowIndexLookup::None) => {
|
||
// Direct mapping for Range, but check bounds
|
||
if range.contains(&key) {
|
||
// For a range S..E, the key `k` corresponds to physical row `k - S`.
|
||
// Frame::new ensures Range is always 0..N, so S=0.
|
||
debug_assert_eq!(range.start, 0, "Range index expected to start at 0");
|
||
key // physical row = key - 0
|
||
} else {
|
||
panic!(
|
||
"Frame index: integer key {} out of bounds for Range index {:?}",
|
||
key, range
|
||
);
|
||
}
|
||
}
|
||
(RowIndex::Date(_), _) => {
|
||
panic!("Frame index: incompatible key type usize for Date index")
|
||
}
|
||
// Ensure state consistency
|
||
#[allow(unreachable_patterns)]
|
||
(RowIndex::Int(_), RowIndexLookup::None)
|
||
| (RowIndex::Int(_), RowIndexLookup::Date(_))
|
||
| (RowIndex::Date(_), RowIndexLookup::Int(_))
|
||
| (RowIndex::Date(_), RowIndexLookup::None)
|
||
| (RowIndex::Range(_), RowIndexLookup::Int(_))
|
||
| (RowIndex::Range(_), RowIndexLookup::Date(_)) => {
|
||
panic!(
|
||
"Frame index: inconsistent internal index state (lookup type mismatch for index type with usize key)"
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Implementation for `NaiveDate` keys (used for `RowIndex::Date`).
|
||
impl<T: Clone + PartialEq> RowIndexLookupHelper<NaiveDate> for Frame<T> {
|
||
fn lookup_row_index(
|
||
key: NaiveDate,
|
||
index_values: &RowIndex,
|
||
index_lookup: &RowIndexLookup,
|
||
) -> usize {
|
||
match (index_values, index_lookup) {
|
||
(RowIndex::Date(_), RowIndexLookup::Date(lookup)) => {
|
||
// Use the HashMap for O(1) average lookup
|
||
*lookup.get(&key).unwrap_or_else(|| {
|
||
panic!("Frame index: date key {} not found in Date index", key)
|
||
})
|
||
}
|
||
(RowIndex::Int(_), _) | (RowIndex::Range(_), _) => {
|
||
panic!("Frame index: incompatible key type NaiveDate for Int or Range index")
|
||
}
|
||
// Ensure state consistency
|
||
#[allow(unreachable_patterns)]
|
||
(RowIndex::Date(_), RowIndexLookup::None)
|
||
| (RowIndex::Date(_), RowIndexLookup::Int(_))
|
||
| (RowIndex::Int(_), RowIndexLookup::Date(_))
|
||
| (RowIndex::Range(_), RowIndexLookup::Date(_)) => {
|
||
panic!(
|
||
"Frame index: inconsistent internal index state (lookup type mismatch for index type with NaiveDate key)"
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Row View Structs ---
|
||
|
||
/// An immutable view of a single row in a `Frame`. Allows access via `[]`.
|
||
pub struct FrameRowView<'a, T: Clone + PartialEq> {
|
||
frame: &'a Frame<T>,
|
||
physical_row_idx: usize,
|
||
}
|
||
|
||
impl<'a, T: Clone + PartialEq + fmt::Debug> fmt::Debug for FrameRowView<'a, T> {
|
||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
// Collect references to the data for display
|
||
let row_data: Vec<&T> = (0..self.frame.cols())
|
||
.map(|c| &self.frame.matrix[(self.physical_row_idx, c)])
|
||
.collect();
|
||
f.debug_struct("FrameRowView")
|
||
.field("physical_row_idx", &self.physical_row_idx)
|
||
.field("columns", &self.frame.column_names)
|
||
.field("data", &row_data)
|
||
.finish()
|
||
}
|
||
}
|
||
|
||
impl<'a, T: Clone + PartialEq> FrameRowView<'a, T> {
|
||
/// Get an element by its physical column index. Panics if out of bounds (delegated to Matrix).
|
||
pub fn get_by_index(&self, col_idx: usize) -> &T {
|
||
// Add check here for better error message if desired
|
||
if col_idx >= self.frame.cols() {
|
||
panic!(
|
||
"FrameRowView::get_by_index: column index {} out of bounds for frame with {} columns",
|
||
col_idx,
|
||
self.frame.cols()
|
||
);
|
||
}
|
||
&self.frame.matrix[(self.physical_row_idx, col_idx)]
|
||
}
|
||
/// Get an element by its column name. Panics if name not found.
|
||
pub fn get(&self, col_name: &str) -> &T {
|
||
let idx = self
|
||
.frame
|
||
.column_index(col_name)
|
||
.unwrap_or_else(|| panic!("FrameRowView::get: column name '{}' not found", col_name));
|
||
self.get_by_index(idx)
|
||
}
|
||
}
|
||
// Indexing by column name (&str)
|
||
impl<'a, T: Clone + PartialEq> Index<&str> for FrameRowView<'a, T> {
|
||
type Output = T;
|
||
#[inline]
|
||
fn index(&self, col_name: &str) -> &Self::Output {
|
||
self.get(col_name)
|
||
}
|
||
}
|
||
// Indexing by physical column index (usize)
|
||
impl<'a, T: Clone + PartialEq> Index<usize> for FrameRowView<'a, T> {
|
||
type Output = T;
|
||
#[inline]
|
||
fn index(&self, col_idx: usize) -> &Self::Output {
|
||
self.get_by_index(col_idx)
|
||
}
|
||
}
|
||
|
||
/// A mutable view of a single row in a `Frame`. Allows access/mutation via `[]` or methods.
|
||
pub struct FrameRowViewMut<'a, T: Clone + PartialEq> {
|
||
frame: &'a mut Frame<T>,
|
||
physical_row_idx: usize,
|
||
}
|
||
|
||
impl<'a, T: Clone + PartialEq + fmt::Debug> fmt::Debug for FrameRowViewMut<'a, T> {
|
||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
// Avoid borrowing frame immutably while already borrowed mutably
|
||
f.debug_struct("FrameRowViewMut")
|
||
.field("physical_row_idx", &self.physical_row_idx)
|
||
.field("columns", &self.frame.column_names) // Reading column names is fine
|
||
// Cannot easily display data without another borrow
|
||
.finish()
|
||
}
|
||
}
|
||
|
||
impl<'a, T: Clone + PartialEq> FrameRowViewMut<'a, T> {
|
||
/// Get a mutable reference to an element by its physical column index. Panics if out of bounds (delegated to Matrix).
|
||
pub fn get_by_index_mut(&mut self, col_idx: usize) -> &mut T {
|
||
// Add check here for better error message if desired
|
||
let num_cols = self.frame.cols(); // Borrow checker friendly
|
||
if col_idx >= num_cols {
|
||
panic!(
|
||
"FrameRowViewMut::get_by_index_mut: column index {} out of bounds for frame with {} columns",
|
||
col_idx, num_cols
|
||
);
|
||
}
|
||
&mut self.frame.matrix[(self.physical_row_idx, col_idx)]
|
||
}
|
||
/// Get a mutable reference to an element by its column name. Panics if name not found.
|
||
pub fn get_mut(&mut self, col_name: &str) -> &mut T {
|
||
// Need to get index first, as borrow checker prevents simultaneous borrow for index lookup and mutable access
|
||
// Clone name to avoid borrow conflict if name comes from frame itself (unlikely but possible)
|
||
let col_name_owned = col_name.to_string();
|
||
let idx = self.frame.column_index(&col_name_owned).unwrap_or_else(|| {
|
||
panic!(
|
||
"FrameRowViewMut::get_mut: column name '{}' not found",
|
||
col_name_owned
|
||
)
|
||
});
|
||
self.get_by_index_mut(idx)
|
||
}
|
||
/// Set the value of an element by its physical column index. Panics if out of bounds.
|
||
pub fn set_by_index(&mut self, col_idx: usize, value: T) {
|
||
// get_by_index_mut already checks bounds
|
||
*self.get_by_index_mut(col_idx) = value;
|
||
}
|
||
/// Set the value of an element by its column name. Panics if name not found.
|
||
pub fn set(&mut self, col_name: &str, value: T) {
|
||
// get_mut already finds index and calls get_by_index_mut (which checks bounds)
|
||
*self.get_mut(col_name) = value;
|
||
}
|
||
|
||
// --- Read-only access needed for Index trait ---
|
||
/// Internal helper for immutable access by index (needed for Index impl).
|
||
fn get_by_index_ref(&self, col_idx: usize) -> &T {
|
||
// Add check here for better error message if desired
|
||
if col_idx >= self.frame.cols() {
|
||
panic!(
|
||
"FrameRowViewMut::get_by_index_ref: column index {} out of bounds for frame with {} columns",
|
||
col_idx,
|
||
self.frame.cols()
|
||
);
|
||
}
|
||
&self.frame.matrix[(self.physical_row_idx, col_idx)]
|
||
}
|
||
/// Internal helper for immutable access by name (needed for Index impl).
|
||
fn get_ref(&self, col_name: &str) -> &T {
|
||
let idx = self.frame.column_index(col_name).unwrap_or_else(|| {
|
||
panic!(
|
||
"FrameRowViewMut::get_ref: column name '{}' not found",
|
||
col_name
|
||
)
|
||
});
|
||
self.get_by_index_ref(idx)
|
||
}
|
||
}
|
||
// Read-only indexing by column name (&str)
|
||
impl<'a, T: Clone + PartialEq> Index<&str> for FrameRowViewMut<'a, T> {
|
||
type Output = T;
|
||
#[inline]
|
||
fn index(&self, col_name: &str) -> &Self::Output {
|
||
// Must use the read-only helper due to borrow rules
|
||
self.get_ref(col_name)
|
||
}
|
||
}
|
||
// Read-only indexing by physical column index (usize)
|
||
impl<'a, T: Clone + PartialEq> Index<usize> for FrameRowViewMut<'a, T> {
|
||
type Output = T;
|
||
#[inline]
|
||
fn index(&self, col_idx: usize) -> &Self::Output {
|
||
// Must use the read-only helper due to borrow rules
|
||
self.get_by_index_ref(col_idx)
|
||
}
|
||
}
|
||
// Mutable indexing by column name (&str)
|
||
impl<'a, T: Clone + PartialEq> IndexMut<&str> for FrameRowViewMut<'a, T> {
|
||
#[inline]
|
||
fn index_mut(&mut self, col_name: &str) -> &mut Self::Output {
|
||
self.get_mut(col_name)
|
||
}
|
||
}
|
||
// Mutable indexing by physical column index (usize)
|
||
impl<'a, T: Clone + PartialEq> IndexMut<usize> for FrameRowViewMut<'a, T> {
|
||
#[inline]
|
||
fn index_mut(&mut self, col_idx: usize) -> &mut Self::Output {
|
||
self.get_by_index_mut(col_idx)
|
||
}
|
||
}
|
||
|
||
/* ---------- Frame Indexing Implementation ---------- */
|
||
/// Allows accessing a column's data as a slice using `frame["col_name"]`.
|
||
impl<T: Clone + PartialEq> Index<&str> for Frame<T> {
|
||
type Output = [T];
|
||
#[inline]
|
||
fn index(&self, name: &str) -> &Self::Output {
|
||
self.column(name)
|
||
}
|
||
}
|
||
/// Allows mutating a column's data as a slice using `frame["col_name"]`.
|
||
impl<T: Clone + PartialEq> IndexMut<&str> for Frame<T> {
|
||
#[inline]
|
||
fn index_mut(&mut self, name: &str) -> &mut Self::Output {
|
||
self.column_mut(name)
|
||
}
|
||
}
|
||
|
||
/* ---------- Element-wise numerical ops ---------- */
|
||
/// Macro to implement element-wise binary operations (+, -, *, /) for Frames.
|
||
macro_rules! impl_elementwise_frame_op {
|
||
($OpTrait:ident, $method:ident) => {
|
||
impl<'a, 'b, T> std::ops::$OpTrait<&'b Frame<T>> for &'a Frame<T>
|
||
where
|
||
T: Clone + PartialEq + std::ops::$OpTrait<Output = T>,
|
||
{
|
||
type Output = Frame<T>;
|
||
fn $method(self, rhs: &'b Frame<T>) -> Frame<T> {
|
||
// 1. Check for compatibility
|
||
if self.column_names != rhs.column_names {
|
||
panic!(
|
||
"Element-wise op ({}): column names mismatch. Left: {:?}, Right: {:?}",
|
||
stringify!($method),
|
||
self.column_names,
|
||
rhs.column_names
|
||
);
|
||
}
|
||
if self.index != rhs.index {
|
||
panic!(
|
||
"Element-wise op ({}): row indices mismatch. Left: {:?}, Right: {:?}",
|
||
stringify!($method),
|
||
self.index,
|
||
rhs.index
|
||
);
|
||
}
|
||
|
||
// 2. Perform operation on underlying matrices
|
||
let result_matrix = (&self.matrix).$method(&rhs.matrix);
|
||
|
||
// 3. Construct the new Frame
|
||
// Clone index unless it's Range, then pass None to use default construction
|
||
let new_index = match self.index {
|
||
RowIndex::Range(_) => None, // Frame::new handles None correctly
|
||
_ => Some(self.index.clone()), // Clone Int or Date index
|
||
};
|
||
Frame::new(result_matrix, self.column_names.clone(), new_index)
|
||
}
|
||
}
|
||
};
|
||
}
|
||
impl_elementwise_frame_op!(Add, add);
|
||
impl_elementwise_frame_op!(Sub, sub);
|
||
impl_elementwise_frame_op!(Mul, mul);
|
||
impl_elementwise_frame_op!(Div, div);
|
||
|
||
/* ---------- Boolean-specific bitwise ops ---------- */
|
||
/// Macro to implement element-wise binary bitwise operations (&, |, ^) for Frames of bool.
|
||
macro_rules! impl_bitwise_frame_op {
|
||
($OpTrait:ident, $method:ident) => {
|
||
impl<'a, 'b> std::ops::$OpTrait<&'b Frame<bool>> for &'a Frame<bool> {
|
||
type Output = Frame<bool>;
|
||
fn $method(self, rhs: &'b Frame<bool>) -> Frame<bool> {
|
||
// 1. Check for compatibility
|
||
if self.column_names != rhs.column_names {
|
||
panic!(
|
||
"Bitwise op ({}): column names mismatch. Left: {:?}, Right: {:?}",
|
||
stringify!($method),
|
||
self.column_names,
|
||
rhs.column_names
|
||
);
|
||
}
|
||
if self.index != rhs.index {
|
||
panic!(
|
||
"Bitwise op ({}): row indices mismatch. Left: {:?}, Right: {:?}",
|
||
stringify!($method),
|
||
self.index,
|
||
rhs.index
|
||
);
|
||
}
|
||
|
||
// 2. Perform operation on underlying matrices
|
||
let result_matrix = (&self.matrix).$method(&rhs.matrix);
|
||
|
||
// 3. Construct the new Frame
|
||
let new_index = match self.index {
|
||
RowIndex::Range(_) => None,
|
||
_ => Some(self.index.clone()),
|
||
};
|
||
Frame::new(result_matrix, self.column_names.clone(), new_index)
|
||
}
|
||
}
|
||
};
|
||
}
|
||
impl_bitwise_frame_op!(BitAnd, bitand);
|
||
impl_bitwise_frame_op!(BitOr, bitor);
|
||
impl_bitwise_frame_op!(BitXor, bitxor);
|
||
|
||
/// Implements element-wise logical NOT (!) for Frames of bool. Consumes the frame.
|
||
impl Not for Frame<bool> {
|
||
type Output = Frame<bool>;
|
||
fn not(self) -> Frame<bool> {
|
||
// Perform operation on underlying matrix (Matrix::not consumes the matrix)
|
||
let result_matrix = !self.matrix;
|
||
|
||
// Construct the new Frame (index can be moved as self is consumed)
|
||
let new_index = match self.index {
|
||
RowIndex::Range(_) => None,
|
||
_ => Some(self.index), // Move index
|
||
};
|
||
Frame::new(result_matrix, self.column_names, new_index) // Move column names
|
||
}
|
||
}
|
||
|
||
// --- Tests ---
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
// Assume Matrix is available from crate::matrix or similar
|
||
use crate::matrix::Matrix;
|
||
use chrono::NaiveDate;
|
||
// HashMap needed for direct inspection in tests if required
|
||
use std::collections::HashMap;
|
||
// Use a fixed tolerance for float comparisons
|
||
const FLOAT_TOLERANCE: f64 = 1e-9;
|
||
|
||
// --- Test Helpers ---
|
||
fn create_test_matrix_f64() -> Matrix<f64> {
|
||
Matrix::from_cols(vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]]) // 3 rows, 2 cols
|
||
}
|
||
fn create_test_matrix_f64_alt() -> Matrix<f64> {
|
||
Matrix::from_cols(vec![vec![0.1, 0.2, 0.3], vec![0.4, 0.5, 0.6]]) // 3 rows, 2 cols
|
||
}
|
||
fn create_test_matrix_bool() -> Matrix<bool> {
|
||
Matrix::from_cols(vec![vec![true, false], vec![false, true]]) // 2 rows, 2 cols
|
||
}
|
||
fn create_test_matrix_bool_alt() -> Matrix<bool> {
|
||
Matrix::from_cols(vec![vec![true, true], vec![false, false]]) // 2 rows, 2 cols
|
||
}
|
||
fn create_test_matrix_string() -> Matrix<String> {
|
||
Matrix::from_cols(vec![
|
||
vec!["r0c0".to_string(), "r1c0".to_string()], // Col 0
|
||
vec!["r0c1".to_string(), "r1c1".to_string()], // Col 1
|
||
]) // 2 rows, 2 cols
|
||
}
|
||
fn d(y: i32, m: u32, d: u32) -> NaiveDate {
|
||
NaiveDate::from_ymd_opt(y, m, d).unwrap()
|
||
}
|
||
fn create_test_frame_f64() -> Frame<f64> {
|
||
Frame::new(create_test_matrix_f64(), vec!["A", "B"], None)
|
||
}
|
||
fn create_test_frame_f64_alt() -> Frame<f64> {
|
||
Frame::new(create_test_matrix_f64_alt(), vec!["A", "B"], None)
|
||
}
|
||
fn create_test_frame_bool() -> Frame<bool> {
|
||
Frame::new(create_test_matrix_bool(), vec!["P", "Q"], None)
|
||
}
|
||
fn create_test_frame_bool_alt() -> Frame<bool> {
|
||
Frame::new(create_test_matrix_bool_alt(), vec!["P", "Q"], None)
|
||
}
|
||
fn create_test_frame_int() -> Frame<i32> {
|
||
Frame::new(
|
||
Matrix::from_cols(vec![vec![1, -2], vec![3, -4]]), // 2 rows, 2 cols
|
||
vec!["X", "Y"],
|
||
None,
|
||
)
|
||
}
|
||
fn create_test_frame_int_alt() -> Frame<i32> {
|
||
Frame::new(
|
||
Matrix::from_cols(vec![vec![10, 20], vec![30, 40]]), // 2 rows, 2 cols
|
||
vec!["X", "Y"],
|
||
None,
|
||
)
|
||
}
|
||
|
||
// --- Frame::new Tests ---
|
||
#[test]
|
||
fn frame_new_default_index() {
|
||
let frame = create_test_frame_f64(); // 3 rows, 2 cols
|
||
assert_eq!(frame.rows(), 3);
|
||
assert_eq!(frame.cols(), 2);
|
||
assert_eq!(frame.columns(), &["A", "B"]);
|
||
assert_eq!(frame.index(), &RowIndex::Range(0..3));
|
||
assert_eq!(frame.col_lookup.len(), 2);
|
||
assert_eq!(frame.col_lookup["A"], 0);
|
||
assert_eq!(frame.col_lookup["B"], 1);
|
||
assert_eq!(frame.index_lookup, RowIndexLookup::None);
|
||
assert_eq!(frame["A"], vec![1.0, 2.0, 3.0]);
|
||
assert_eq!(frame["B"], vec![4.0, 5.0, 6.0]);
|
||
}
|
||
#[test]
|
||
fn frame_new_int_index() {
|
||
let matrix = create_test_matrix_f64(); // 3 rows
|
||
let index_vec = vec![10, 20, 5];
|
||
let index = RowIndex::Int(index_vec.clone());
|
||
let frame = Frame::new(matrix, vec!["A", "B"], Some(index.clone()));
|
||
assert_eq!(frame.index(), &index);
|
||
assert!(matches!(frame.index_lookup, RowIndexLookup::Int(_)));
|
||
if let RowIndexLookup::Int(lookup) = &frame.index_lookup {
|
||
assert_eq!(lookup.len(), 3);
|
||
assert_eq!(lookup[&10], 0); // value 10 -> physical row 0
|
||
assert_eq!(lookup[&20], 1); // value 20 -> physical row 1
|
||
assert_eq!(lookup[&5], 2); // value 5 -> physical row 2
|
||
}
|
||
assert_eq!(frame.get_row(10)["A"], 1.0); // Access by index value
|
||
assert_eq!(frame.get_row(20)["A"], 2.0);
|
||
assert_eq!(frame.get_row(5)["A"], 3.0);
|
||
}
|
||
#[test]
|
||
fn frame_new_date_index() {
|
||
let matrix = create_test_matrix_string(); // 2 rows
|
||
let dates = vec![d(2024, 1, 10), d(2024, 1, 5)]; // Order preserved
|
||
let index = RowIndex::Date(dates.clone());
|
||
let frame = Frame::new(matrix, vec!["X", "Y"], Some(index.clone()));
|
||
assert_eq!(frame.rows(), 2);
|
||
assert_eq!(frame.cols(), 2);
|
||
assert_eq!(frame.index(), &index);
|
||
assert!(matches!(frame.index_lookup, RowIndexLookup::Date(_)));
|
||
if let RowIndexLookup::Date(lookup) = &frame.index_lookup {
|
||
assert_eq!(lookup.len(), 2);
|
||
assert_eq!(lookup[&d(2024, 1, 10)], 0); // date -> physical row 0
|
||
assert_eq!(lookup[&d(2024, 1, 5)], 1); // date -> physical row 1
|
||
}
|
||
assert_eq!(frame["X"], vec!["r0c0", "r1c0"]);
|
||
assert_eq!(frame.get_row_date(d(2024, 1, 10))["X"], "r0c0");
|
||
assert_eq!(frame.get_row_date(d(2024, 1, 5))["X"], "r1c0");
|
||
}
|
||
#[test]
|
||
fn frame_new_one_by_one() {
|
||
let matrix = Matrix::from_cols(vec![vec![100]]); // 1 row, 1 col
|
||
let frame = Frame::new(matrix, vec!["Single"], None);
|
||
assert_eq!(frame.rows(), 1);
|
||
assert_eq!(frame.cols(), 1);
|
||
assert_eq!(frame.columns(), &["Single"]);
|
||
assert_eq!(frame.index(), &RowIndex::Range(0..1));
|
||
assert_eq!(frame["Single"], vec![100]);
|
||
assert_eq!(frame.get_row(0)[0], 100);
|
||
assert_eq!(frame.get_row(0)["Single"], 100);
|
||
}
|
||
// Removed test frame_new_zero_rows_zero_cols as Matrix constructors prevent it
|
||
|
||
// --- Frame::new Panic Tests ---
|
||
#[test]
|
||
#[should_panic(expected = "Frame::new: column name count mismatch (names: 1, matrix: 2)")]
|
||
fn frame_new_panic_col_count() {
|
||
let matrix = create_test_matrix_f64();
|
||
Frame::new(matrix, vec!["A"], None);
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "duplicate column label: A")]
|
||
fn frame_new_panic_duplicate_col() {
|
||
let matrix = create_test_matrix_f64();
|
||
Frame::new(matrix, vec!["A", "A"], None);
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "Int index length (2) mismatch matrix rows (3)")]
|
||
fn frame_new_panic_index_len() {
|
||
let matrix = create_test_matrix_f64(); // 3 rows
|
||
let index = RowIndex::Int(vec![10, 20]); // Only 2 index values
|
||
Frame::new(matrix, vec!["A", "B"], Some(index));
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "Date index length (1) mismatch matrix rows (2)")]
|
||
fn frame_new_panic_date_index_len() {
|
||
let matrix = create_test_matrix_string(); // 2 rows
|
||
let index = RowIndex::Date(vec![d(2024, 1, 1)]); // Only 1 index value
|
||
Frame::new(matrix, vec!["X", "Y"], Some(index));
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "duplicate Int index value: 10")]
|
||
fn frame_new_panic_duplicate_int_index() {
|
||
let matrix = create_test_matrix_f64(); // 3 rows
|
||
let index = RowIndex::Int(vec![10, 20, 10]); // Duplicate 10
|
||
Frame::new(matrix, vec!["A", "B"], Some(index));
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "duplicate Date index value: 2024-01-10")]
|
||
fn frame_new_panic_duplicate_date_index() {
|
||
let matrix = create_test_matrix_string(); // 2 rows
|
||
let index = RowIndex::Date(vec![d(2024, 1, 10), d(2024, 1, 10)]); // Duplicate date
|
||
Frame::new(matrix, vec!["X", "Y"], Some(index));
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "Cannot explicitly provide a Range index")]
|
||
fn frame_new_panic_explicit_range() {
|
||
let matrix = create_test_matrix_f64();
|
||
let index = RowIndex::Range(0..3); // User cannot provide Range directly
|
||
Frame::new(matrix, vec!["A", "B"], Some(index));
|
||
}
|
||
|
||
// --- RowIndex Method Tests ---
|
||
#[test]
|
||
fn test_row_index_methods() {
|
||
let idx_int = RowIndex::Int(vec![10, 20, 5]);
|
||
assert_eq!(idx_int.len(), 3);
|
||
assert!(!idx_int.is_empty());
|
||
let idx_date = RowIndex::Date(vec![d(2024, 1, 1), d(2024, 1, 2)]);
|
||
assert_eq!(idx_date.len(), 2);
|
||
assert!(!idx_date.is_empty());
|
||
let idx_range = RowIndex::Range(0..5);
|
||
assert_eq!(idx_range.len(), 5);
|
||
assert!(!idx_range.is_empty());
|
||
let idx_empty_int = RowIndex::Int(vec![]);
|
||
assert_eq!(idx_empty_int.len(), 0);
|
||
assert!(idx_empty_int.is_empty());
|
||
let idx_empty_date = RowIndex::Date(vec![]);
|
||
assert_eq!(idx_empty_date.len(), 0);
|
||
assert!(idx_empty_date.is_empty());
|
||
let idx_empty_range = RowIndex::Range(3..3);
|
||
assert_eq!(idx_empty_range.len(), 0);
|
||
assert!(idx_empty_range.is_empty());
|
||
let idx_range_zero = RowIndex::Range(0..0);
|
||
assert_eq!(idx_range_zero.len(), 0);
|
||
assert!(idx_range_zero.is_empty());
|
||
}
|
||
|
||
// --- Frame Accessor Tests ---
|
||
#[test]
|
||
fn frame_column_access() {
|
||
let mut frame = create_test_frame_f64(); // A=[1,2,3], B=[4,5,6]
|
||
assert_eq!(frame.column("A"), &[1.0, 2.0, 3.0]);
|
||
assert_eq!(frame["B"], vec![4.0, 5.0, 6.0]); // Index trait
|
||
assert_eq!(frame.column_index("A"), Some(0));
|
||
assert_eq!(frame.column_index("B"), Some(1));
|
||
assert_eq!(frame.column_index("C"), None);
|
||
|
||
// Mutation
|
||
frame.column_mut("A")[1] = 2.5;
|
||
assert_eq!(frame["A"], vec![1.0, 2.5, 3.0]);
|
||
frame["B"][0] = 4.1; // IndexMut trait
|
||
assert_eq!(frame["B"], vec![4.1, 5.0, 6.0]);
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "unknown column label: 'C'")]
|
||
fn frame_column_access_panic() {
|
||
let frame = create_test_frame_f64();
|
||
let _ = frame.column("C");
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "unknown column label: 'C'")]
|
||
fn frame_column_access_mut_panic() {
|
||
let mut frame = create_test_frame_f64();
|
||
let _ = frame.column_mut("C");
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "unknown column label: 'C'")]
|
||
fn frame_column_index_panic() {
|
||
let frame = create_test_frame_f64();
|
||
let _ = frame["C"]; // Panics when Index calls column internally
|
||
}
|
||
|
||
#[test]
|
||
#[should_panic(expected = "unknown column label: 'C'")]
|
||
fn frame_column_index_mut_panic() {
|
||
let mut frame = create_test_frame_f64();
|
||
frame["C"][0] = 0.0; // Panics when IndexMut calls column_mut internally
|
||
}
|
||
|
||
#[test]
|
||
fn frame_row_access_default_index() {
|
||
let frame = create_test_frame_f64(); // Index 0..3
|
||
let row1 = frame.get_row(1); // Get row for index value 1 (physical row 1)
|
||
assert_eq!(row1.get("A"), &2.0);
|
||
assert_eq!(row1.get_by_index(1), &5.0); // Access by physical column index
|
||
assert_eq!(row1["A"], 2.0); // Index by name
|
||
assert_eq!(row1[1], 5.0); // Index by physical column index
|
||
assert_eq!(frame.get_row(0)["B"], 4.0); // Index value 0 -> physical row 0
|
||
}
|
||
#[test]
|
||
fn frame_row_access_int_index() {
|
||
let matrix = create_test_matrix_f64(); // 3 rows
|
||
let index = RowIndex::Int(vec![100, 50, 200]);
|
||
let frame = Frame::new(matrix, vec!["A", "B"], Some(index));
|
||
let row50 = frame.get_row(50); // Access by index value 50 (physical row 1)
|
||
assert_eq!(row50["A"], 2.0);
|
||
assert_eq!(row50[1], 5.0); // Column B (physical index 1)
|
||
assert_eq!(frame.get_row(200)["A"], 3.0); // Index value 200 -> physical row 2
|
||
}
|
||
#[test]
|
||
fn frame_row_access_date_index() {
|
||
let matrix = create_test_matrix_string(); // 2 rows
|
||
let index = RowIndex::Date(vec![d(2023, 5, 1), d(2023, 5, 10)]);
|
||
let frame = Frame::new(matrix, vec!["X", "Y"], Some(index));
|
||
let row_may10 = frame.get_row_date(d(2023, 5, 10)); // Access by date (physical row 1)
|
||
assert_eq!(row_may10["X"], "r1c0");
|
||
assert_eq!(row_may10[1], "r1c1"); // Column Y (physical index 1)
|
||
assert_eq!(frame.get_row_date(d(2023, 5, 1))["Y"], "r0c1"); // Date -> physical row 0
|
||
}
|
||
|
||
#[test]
|
||
#[should_panic(expected = "integer key 99 not found in Int index")]
|
||
fn frame_row_access_int_index_panic_not_found() {
|
||
let matrix = create_test_matrix_f64();
|
||
let index = RowIndex::Int(vec![100, 50, 200]);
|
||
let frame = Frame::new(matrix, vec!["A", "B"], Some(index));
|
||
frame.get_row(99); // 99 is not in the index values
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "integer key 99 not found in Int index")]
|
||
fn frame_row_access_int_index_mut_panic_not_found() {
|
||
let matrix = create_test_matrix_f64();
|
||
let index = RowIndex::Int(vec![100, 50, 200]);
|
||
let mut frame = Frame::new(matrix, vec!["A", "B"], Some(index));
|
||
frame.get_row_mut(99); // 99 is not in the index values
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "integer key 3 out of bounds for Range index 0..3")]
|
||
fn frame_row_access_default_index_panic_out_of_bounds() {
|
||
let frame = create_test_frame_f64(); // Index 0..3
|
||
frame.get_row(3); // 3 is not in the range [0, 3)
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "integer key 3 out of bounds for Range index 0..3")]
|
||
fn frame_row_access_default_index_mut_panic_out_of_bounds() {
|
||
let mut frame = create_test_frame_f64(); // Index 0..3
|
||
frame.get_row_mut(3); // 3 is not in the range [0, 3)
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "date key 2023-05-02 not found in Date index")]
|
||
fn frame_row_access_date_index_panic_not_found() {
|
||
let matrix = create_test_matrix_string();
|
||
let index = RowIndex::Date(vec![d(2023, 5, 1), d(2023, 5, 10)]);
|
||
let frame = Frame::new(matrix, vec!["X", "Y"], Some(index));
|
||
frame.get_row_date(d(2023, 5, 2)); // Date not in index
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "date key 2023-05-02 not found in Date index")]
|
||
fn frame_row_access_date_index_mut_panic_not_found() {
|
||
let matrix = create_test_matrix_string();
|
||
let index = RowIndex::Date(vec![d(2023, 5, 1), d(2023, 5, 10)]);
|
||
let mut frame = Frame::new(matrix, vec!["X", "Y"], Some(index));
|
||
frame.get_row_date_mut(d(2023, 5, 2)); // Date not in index
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "incompatible key type usize for Date index")]
|
||
fn frame_row_access_type_mismatch_panic_usize_on_date() {
|
||
let matrix = create_test_matrix_string();
|
||
let index = RowIndex::Date(vec![d(2023, 5, 1), d(2023, 5, 10)]);
|
||
let frame = Frame::new(matrix, vec!["X", "Y"], Some(index));
|
||
frame.get_row(0); // Using usize key with Date index
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "incompatible key type usize for Date index")]
|
||
fn frame_row_access_type_mismatch_mut_panic_usize_on_date() {
|
||
let matrix = create_test_matrix_string();
|
||
let index = RowIndex::Date(vec![d(2023, 5, 1), d(2023, 5, 10)]);
|
||
let mut frame = Frame::new(matrix, vec!["X", "Y"], Some(index));
|
||
frame.get_row_mut(0); // Using usize key with Date index
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "incompatible key type NaiveDate for Int or Range index")]
|
||
fn frame_row_access_type_mismatch_panic_date_on_int() {
|
||
let matrix = create_test_matrix_f64();
|
||
let index = RowIndex::Int(vec![100, 50, 200]);
|
||
let frame = Frame::new(matrix, vec!["A", "B"], Some(index));
|
||
frame.get_row_date(d(2023, 5, 1)); // Using Date key with Int index
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "incompatible key type NaiveDate for Int or Range index")]
|
||
fn frame_row_access_type_mismatch_mut_panic_date_on_int() {
|
||
let matrix = create_test_matrix_f64();
|
||
let index = RowIndex::Int(vec![100, 50, 200]);
|
||
let mut frame = Frame::new(matrix, vec!["A", "B"], Some(index));
|
||
frame.get_row_date_mut(d(2023, 5, 1)); // Using Date key with Int index
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "incompatible key type NaiveDate for Int or Range index")]
|
||
fn frame_row_access_type_mismatch_panic_date_on_range() {
|
||
let frame = create_test_frame_f64(); // Range index
|
||
frame.get_row_date(d(2023, 5, 1)); // Using Date key with Range index
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "incompatible key type NaiveDate for Int or Range index")]
|
||
fn frame_row_access_type_mismatch_mut_panic_date_on_range() {
|
||
let mut frame = create_test_frame_f64(); // Range index
|
||
frame.get_row_date_mut(d(2023, 5, 1)); // Using Date key with Range index
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "inconsistent internal index state")]
|
||
fn frame_row_access_inconsistent_state_int_none() {
|
||
// Manually create inconsistent state (Int index, None lookup)
|
||
let frame = Frame::<i32> {
|
||
matrix: Matrix::from_cols(vec![vec![1]]),
|
||
column_names: vec!["A".to_string()],
|
||
col_lookup: HashMap::from([("A".to_string(), 0)]),
|
||
index: RowIndex::Int(vec![10]),
|
||
index_lookup: RowIndexLookup::None, // Inconsistent
|
||
};
|
||
frame.get_row(10); // Should panic due to inconsistency
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "inconsistent internal index state")]
|
||
fn frame_row_access_inconsistent_state_date_none() {
|
||
// Manually create inconsistent state (Date index, None lookup)
|
||
let frame = Frame::<i32> {
|
||
matrix: Matrix::from_cols(vec![vec![1]]),
|
||
column_names: vec!["A".to_string()],
|
||
col_lookup: HashMap::from([("A".to_string(), 0)]),
|
||
index: RowIndex::Date(vec![d(2024, 1, 1)]),
|
||
index_lookup: RowIndexLookup::None, // Inconsistent
|
||
};
|
||
frame.get_row_date(d(2024, 1, 1)); // Should panic due to inconsistency
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "inconsistent internal index state")]
|
||
fn frame_row_access_inconsistent_state_range_int() {
|
||
// Manually create inconsistent state (Range index, Int lookup)
|
||
let frame = Frame::<i32> {
|
||
matrix: Matrix::from_cols(vec![vec![1]]),
|
||
column_names: vec!["A".to_string()],
|
||
col_lookup: HashMap::from([("A".to_string(), 0)]),
|
||
index: RowIndex::Range(0..1),
|
||
index_lookup: RowIndexLookup::Int(HashMap::new()), // Inconsistent
|
||
};
|
||
frame.get_row(0); // Should panic due to inconsistency
|
||
}
|
||
|
||
// --- Frame Row Mutation Tests ---
|
||
#[test]
|
||
fn frame_row_mutate_default_index() {
|
||
let mut frame = create_test_frame_f64(); // Index 0..3, A=[1,2,3], B=[4,5,6]
|
||
// Mutate using set("col_name", value)
|
||
frame.get_row_mut(1).set("A", 2.9); // Mutate row index 1, col A
|
||
assert_eq!(frame["A"], vec![1.0, 2.9, 3.0]);
|
||
// Mutate using IndexMut by physical column index
|
||
frame.get_row_mut(0)[1] = 4.9; // Mutate row index 0, col B (index 1)
|
||
assert_eq!(frame["B"], vec![4.9, 5.0, 6.0]);
|
||
// Mutate using IndexMut by column name
|
||
frame.get_row_mut(2)["A"] = 3.9; // Mutate row index 2, col A
|
||
assert_eq!(frame["A"], vec![1.0, 2.9, 3.9]);
|
||
}
|
||
#[test]
|
||
fn frame_row_mutate_date_index() {
|
||
let matrix = create_test_matrix_string(); // r0=["r0c0","r0c1"], r1=["r1c0","r1c1"]
|
||
let index = RowIndex::Date(vec![d(2023, 5, 1), d(2023, 5, 10)]); // r0=May1, r1=May10
|
||
let mut frame = Frame::new(matrix, vec!["X", "Y"], Some(index));
|
||
let key_may10 = d(2023, 5, 10);
|
||
let key_may1 = d(2023, 5, 1);
|
||
|
||
// Mutate using set_by_index(col_idx, value)
|
||
frame
|
||
.get_row_date_mut(key_may10) // Get row for May 10 (physical row 1)
|
||
.set_by_index(0, "r1c0_mod".to_string()); // Set col X (index 0)
|
||
assert_eq!(frame["X"], vec!["r0c0", "r1c0_mod"]);
|
||
|
||
// Mutate using IndexMut by column name
|
||
frame.get_row_date_mut(key_may1)["Y"] = "r0c1_mod".to_string(); // Row May 1, col Y
|
||
assert_eq!(frame["Y"], vec!["r0c1_mod", "r1c1"]);
|
||
|
||
// Mutate using IndexMut by physical column index
|
||
frame.get_row_date_mut(key_may10)[1] = "r1c1_mod2".to_string(); // Row May 10, col Y (index 1)
|
||
assert_eq!(frame["Y"], vec!["r0c1_mod", "r1c1_mod2"]);
|
||
}
|
||
|
||
// --- FrameRowView / FrameRowViewMut Indexing Tests ---
|
||
#[test]
|
||
fn test_row_view_mut_readonly_index() {
|
||
// Test that read-only indexing still works on a mutable view
|
||
let mut frame = create_test_frame_f64(); // A=[1,2,3], B=[4,5,6]
|
||
let row_mut = frame.get_row_mut(1); // Get mutable view of row index 1
|
||
assert_eq!(row_mut["A"], 2.0); // Read via Index<&str>
|
||
assert_eq!(row_mut[1], 5.0); // Read via Index<usize> (col B)
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "column index 2 out of bounds")] // Expect more specific message now
|
||
fn test_row_view_index_panic() {
|
||
let frame = create_test_frame_f64(); // 2 cols (0, 1)
|
||
let row_view = frame.get_row(0);
|
||
let _ = row_view[2]; // Access column index 2 (out of bounds)
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "column name 'C' not found")]
|
||
fn test_row_view_name_panic() {
|
||
let frame = create_test_frame_f64();
|
||
let row_view = frame.get_row(0);
|
||
let _ = row_view["C"]; // Access non-existent column name
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "column index 3 out of bounds")] // Check specific message
|
||
fn test_row_view_get_by_index_panic() {
|
||
let frame = create_test_frame_f64(); // 2 cols (0, 1)
|
||
let row_view = frame.get_row(0);
|
||
let _ = row_view.get_by_index(3);
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "column index 2 out of bounds")] // Expect more specific message now
|
||
fn test_row_view_mut_index_panic() {
|
||
let mut frame = create_test_frame_f64(); // 2 cols (0, 1)
|
||
let mut row_view_mut = frame.get_row_mut(0);
|
||
row_view_mut[2] = 0.0; // Access column index 2 (out of bounds)
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "column name 'C' not found")]
|
||
fn test_row_view_mut_name_panic() {
|
||
let mut frame = create_test_frame_f64();
|
||
let mut row_view_mut = frame.get_row_mut(0);
|
||
row_view_mut["C"] = 0.0; // Access non-existent column name
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "column index 3 out of bounds")] // Check specific message
|
||
fn test_row_view_mut_get_by_index_mut_panic() {
|
||
let mut frame = create_test_frame_f64(); // 2 cols (0, 1)
|
||
let mut row_view_mut = frame.get_row_mut(0);
|
||
let _ = row_view_mut.get_by_index_mut(3);
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "column index 3 out of bounds")] // Check specific message
|
||
fn test_row_view_mut_set_by_index_panic() {
|
||
let mut frame = create_test_frame_f64(); // 2 cols (0, 1)
|
||
let mut row_view_mut = frame.get_row_mut(0);
|
||
row_view_mut.set_by_index(3, 0.0);
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "column name 'C' not found")] // Panic from view set -> get_mut
|
||
fn test_row_view_mut_set_panic() {
|
||
let mut frame = create_test_frame_f64();
|
||
let mut row_view_mut = frame.get_row_mut(0);
|
||
row_view_mut.set("C", 0.0); // Access non-existent column name
|
||
}
|
||
|
||
// --- Frame Column Manipulation & Sorting Tests ---
|
||
#[test]
|
||
fn frame_column_manipulation_and_sort() {
|
||
// Initial: C=[1,2], A=[3,4] (names out of alphabetical order)
|
||
let mut frame = Frame::new(
|
||
Matrix::from_cols(vec![vec![1, 2], vec![3, 4]]), // 2 rows, 2 cols
|
||
vec!["C", "A"],
|
||
None,
|
||
);
|
||
assert_eq!(frame.columns(), &["C", "A"]);
|
||
assert_eq!(frame["C"], vec![1, 2]);
|
||
assert_eq!(frame["A"], vec![3, 4]);
|
||
assert_eq!(frame.column_index("C"), Some(0));
|
||
assert_eq!(frame.column_index("A"), Some(1));
|
||
assert_eq!(frame.col_lookup.len(), 2);
|
||
|
||
// Add B=[5,6]: C=[1,2], A=[3,4], B=[5,6]
|
||
frame.add_column("B", vec![5, 6]);
|
||
assert_eq!(frame.columns(), &["C", "A", "B"]);
|
||
assert_eq!(frame["B"], vec![5, 6]);
|
||
assert_eq!(frame.column_index("C"), Some(0));
|
||
assert_eq!(frame.column_index("A"), Some(1));
|
||
assert_eq!(frame.column_index("B"), Some(2)); // B added at the end
|
||
assert_eq!(frame.col_lookup.len(), 3);
|
||
|
||
// Rename C -> X: X=[1,2], A=[3,4], B=[5,6]
|
||
frame.rename("C", "X");
|
||
assert_eq!(frame.columns(), &["X", "A", "B"]); // Name 'C' at index 0 replaced by 'X'
|
||
assert_eq!(frame["X"], vec![1, 2]); // Data remains
|
||
assert_eq!(frame.column_index("X"), Some(0));
|
||
assert_eq!(frame.column_index("A"), Some(1));
|
||
assert_eq!(frame.column_index("B"), Some(2));
|
||
assert!(frame.column_index("C").is_none()); // Old name gone
|
||
assert_eq!(frame.col_lookup.len(), 3);
|
||
|
||
// Delete A: X=[1,2], B=[5,6]
|
||
let deleted_a = frame.delete_column("A");
|
||
assert_eq!(deleted_a, vec![3, 4]);
|
||
// Deleting col A (at physical index 1) shifts B left
|
||
assert_eq!(frame.columns(), &["X", "B"]); // Remaining columns in physical order
|
||
assert_eq!(frame.rows(), 2);
|
||
assert_eq!(frame.cols(), 2);
|
||
assert_eq!(frame["X"], vec![1, 2]); // X data unchanged
|
||
assert_eq!(frame["B"], vec![5, 6]); // B data unchanged
|
||
// Check internal state after delete + rebuild_col_lookup
|
||
assert_eq!(frame.column_index("X"), Some(0)); // X is now physical col 0
|
||
assert_eq!(frame.column_index("B"), Some(1)); // B is now physical col 1
|
||
assert!(frame.column_index("A").is_none());
|
||
assert_eq!(frame.col_lookup.len(), 2);
|
||
|
||
// Sort Columns [X, B] -> [B, X]
|
||
frame.sort_columns();
|
||
assert_eq!(frame.columns(), &["B", "X"]); // Alphabetical order of names
|
||
// Verify data remained with the correct logical column after sort
|
||
assert_eq!(frame["B"], vec![5, 6], "Data in B after sort"); // B should still have [5, 6]
|
||
assert_eq!(frame["X"], vec![1, 2], "Data in X after sort"); // X should still have [1, 2]
|
||
// Verify internal lookup map is correct after sort
|
||
assert_eq!(frame.column_index("B"), Some(0), "Index of B after sort"); // B is now physical col 0
|
||
assert_eq!(frame.column_index("X"), Some(1), "Index of X after sort"); // X is now physical col 1
|
||
assert_eq!(frame.col_lookup.len(), 2);
|
||
assert_eq!(*frame.col_lookup.get("B").unwrap(), 0);
|
||
assert_eq!(*frame.col_lookup.get("X").unwrap(), 1);
|
||
}
|
||
|
||
// Tests specific to the old public swap_columns API are removed as it's now internal.
|
||
// Test internal swap via sort_columns edge cases.
|
||
#[test]
|
||
fn test_sort_columns_already_sorted() {
|
||
let mut frame = create_test_frame_f64(); // A, B (already sorted)
|
||
let original_frame = frame.clone();
|
||
frame.sort_columns();
|
||
assert_eq!(frame.columns(), &["A", "B"]);
|
||
assert_eq!(frame["A"], original_frame["A"]);
|
||
assert_eq!(frame["B"], original_frame["B"]);
|
||
assert_eq!(frame.col_lookup, original_frame.col_lookup);
|
||
}
|
||
#[test]
|
||
fn test_sort_columns_reverse_sorted() {
|
||
let mut frame = Frame::new(
|
||
Matrix::from_cols(vec![vec![1, 2], vec![3, 4]]),
|
||
vec!["Z", "A"], // Z, A (reverse sorted)
|
||
None,
|
||
);
|
||
frame.sort_columns();
|
||
assert_eq!(frame.columns(), &["A", "Z"]);
|
||
assert_eq!(frame["A"], vec![3, 4]); // A keeps its original data
|
||
assert_eq!(frame["Z"], vec![1, 2]); // Z keeps its original data
|
||
assert_eq!(frame.column_index("A"), Some(0));
|
||
assert_eq!(frame.column_index("Z"), Some(1));
|
||
}
|
||
|
||
#[test]
|
||
#[should_panic(expected = "new column name 'B' already exists")]
|
||
fn test_rename_to_existing() {
|
||
let mut frame = create_test_frame_f64(); // Has cols "A", "B"
|
||
frame.rename("A", "B"); // Try renaming A to B (which exists)
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "new name 'A' cannot be the same as the old name")]
|
||
fn test_rename_to_self() {
|
||
let mut frame = create_test_frame_f64();
|
||
frame.rename("A", "A");
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "unknown column label: 'Z'")]
|
||
fn test_rename_panic_unknown() {
|
||
let mut frame = create_test_frame_f64();
|
||
frame.rename("Z", "Y"); // Try renaming non-existent column Z
|
||
}
|
||
|
||
#[test]
|
||
#[should_panic(expected = "duplicate column label: A")]
|
||
fn test_add_column_panic_duplicate() {
|
||
let mut frame = create_test_frame_f64(); // Has col "A"
|
||
frame.add_column("A", vec![0.0, 0.0, 0.0]); // Try adding "A" again
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "column length mismatch")] // Panic comes from Matrix::add_column
|
||
fn test_add_column_panic_len_mismatch() {
|
||
let mut frame = create_test_frame_f64(); // Expects len 3
|
||
frame.add_column("C", vec![0.0, 0.0]); // Provide len 2
|
||
}
|
||
// Removed tests for adding columns to 0-row frames as Matrix constructors prevent 0-row frames.
|
||
|
||
#[test]
|
||
fn test_delete_last_column_range_index() {
|
||
// Start with a 1-column frame (since Matrix requires cols >= 1)
|
||
let matrix = Matrix::from_cols(vec![vec![1, 2]]); // 2 rows, 1 col
|
||
let mut frame = Frame::new(matrix, vec!["Single"], None); // Range index 0..2
|
||
assert_eq!(frame.cols(), 1);
|
||
assert_eq!(frame.rows(), 2);
|
||
|
||
let deleted_data = frame.delete_column("Single");
|
||
assert_eq!(deleted_data, vec![1, 2]);
|
||
|
||
// Assuming Matrix allows 0 columns after deletion, but rows remain
|
||
assert_eq!(frame.cols(), 0);
|
||
assert!(frame.columns().is_empty());
|
||
assert!(frame.col_lookup.is_empty());
|
||
assert_eq!(frame.rows(), 2); // Rows remain
|
||
assert_eq!(frame.index(), &RowIndex::Range(0..2)); // Range index should still reflect rows
|
||
}
|
||
#[test]
|
||
fn test_delete_last_column_int_index() {
|
||
let matrix = Matrix::from_cols(vec![vec![1, 2]]); // 2 rows, 1 col
|
||
let index = RowIndex::Int(vec![10, 20]);
|
||
let mut frame = Frame::new(matrix, vec!["Single"], Some(index.clone()));
|
||
assert_eq!(frame.cols(), 1);
|
||
assert_eq!(frame.rows(), 2);
|
||
|
||
let deleted_data = frame.delete_column("Single");
|
||
assert_eq!(deleted_data, vec![1, 2]);
|
||
|
||
assert_eq!(frame.cols(), 0);
|
||
assert!(frame.columns().is_empty());
|
||
assert!(frame.col_lookup.is_empty());
|
||
assert_eq!(frame.rows(), 2);
|
||
assert_eq!(frame.index(), &index); // Int index remains unchanged
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "unknown column label: 'Z'")]
|
||
fn test_delete_column_panic_unknown() {
|
||
let mut frame = create_test_frame_f64();
|
||
frame.delete_column("Z"); // Try deleting non-existent column Z
|
||
}
|
||
|
||
#[test]
|
||
fn test_sort_columns_empty_and_single() {
|
||
// Test sorting an empty frame (0 cols) - Create via delete
|
||
let mut frame0 = Frame::new(Matrix::from_cols(vec![vec![1]]), vec!["A"], None);
|
||
frame0.delete_column("A");
|
||
assert_eq!(frame0.cols(), 0);
|
||
assert_eq!(frame0.rows(), 1); // Row remains
|
||
let frame0_clone = frame0.clone();
|
||
frame0.sort_columns(); // Should be a no-op
|
||
assert_eq!(frame0, frame0_clone);
|
||
assert_eq!(frame0.columns(), &[] as &[String]); // Ensure columns are empty
|
||
|
||
// Test sorting a frame with a single column
|
||
let mut frame1 = Frame::new(Matrix::from_cols(vec![vec![1.0]]), vec!["Z"], None); // 1x1
|
||
assert_eq!(frame1.cols(), 1);
|
||
let frame1_clone = frame1.clone();
|
||
frame1.sort_columns(); // Should be a no-op
|
||
assert_eq!(frame1, frame1_clone);
|
||
assert_eq!(frame1.columns(), &["Z"]);
|
||
}
|
||
|
||
// --- Element-wise Arithmetic Ops Tests ---
|
||
#[test]
|
||
fn test_frame_arithmetic_ops_f64() {
|
||
let f1 = create_test_frame_f64(); // A=[1,2,3], B=[4,5,6]
|
||
let f2 = create_test_frame_f64_alt(); // A=[0.1,0.2,0.3], B=[0.4,0.5,0.6]
|
||
|
||
// Addition
|
||
let f_add = &f1 + &f2;
|
||
assert_eq!(f_add.columns(), f1.columns());
|
||
assert_eq!(f_add.index(), f1.index());
|
||
assert!((f_add["A"][0] - 1.1).abs() < FLOAT_TOLERANCE);
|
||
assert!((f_add["A"][1] - 2.2).abs() < FLOAT_TOLERANCE);
|
||
assert!((f_add["A"][2] - 3.3).abs() < FLOAT_TOLERANCE);
|
||
assert!((f_add["B"][0] - 4.4).abs() < FLOAT_TOLERANCE);
|
||
assert!((f_add["B"][1] - 5.5).abs() < FLOAT_TOLERANCE);
|
||
assert!((f_add["B"][2] - 6.6).abs() < FLOAT_TOLERANCE);
|
||
|
||
// Subtraction
|
||
let f_sub = &f1 - &f2;
|
||
assert_eq!(f_sub.columns(), f1.columns());
|
||
assert_eq!(f_sub.index(), f1.index());
|
||
assert!((f_sub["A"][0] - 0.9).abs() < FLOAT_TOLERANCE);
|
||
assert!((f_sub["A"][1] - 1.8).abs() < FLOAT_TOLERANCE);
|
||
assert!((f_sub["A"][2] - 2.7).abs() < FLOAT_TOLERANCE);
|
||
assert!((f_sub["B"][0] - 3.6).abs() < FLOAT_TOLERANCE);
|
||
assert!((f_sub["B"][1] - 4.5).abs() < FLOAT_TOLERANCE);
|
||
assert!((f_sub["B"][2] - 5.4).abs() < FLOAT_TOLERANCE);
|
||
|
||
// Multiplication
|
||
let f_mul = &f1 * &f2;
|
||
assert_eq!(f_mul.columns(), f1.columns());
|
||
assert_eq!(f_mul.index(), f1.index());
|
||
assert!((f_mul["A"][0] - 0.1).abs() < FLOAT_TOLERANCE); // 1.0 * 0.1
|
||
assert!((f_mul["A"][1] - 0.4).abs() < FLOAT_TOLERANCE); // 2.0 * 0.2
|
||
assert!((f_mul["A"][2] - 0.9).abs() < FLOAT_TOLERANCE); // 3.0 * 0.3
|
||
assert!((f_mul["B"][0] - 1.6).abs() < FLOAT_TOLERANCE); // 4.0 * 0.4
|
||
assert!((f_mul["B"][1] - 2.5).abs() < FLOAT_TOLERANCE); // 5.0 * 0.5
|
||
assert!(
|
||
(f_mul["B"][2] - 3.6).abs() < FLOAT_TOLERANCE,
|
||
"Check B[2] multiplication"
|
||
); // 6.0 * 0.6
|
||
|
||
// Division
|
||
let f_div = &f1 / &f2;
|
||
assert_eq!(f_div.columns(), f1.columns());
|
||
assert_eq!(f_div.index(), f1.index());
|
||
assert!((f_div["A"][0] - 10.0).abs() < FLOAT_TOLERANCE); // 1.0 / 0.1
|
||
assert!((f_div["A"][1] - 10.0).abs() < FLOAT_TOLERANCE); // 2.0 / 0.2
|
||
assert!((f_div["A"][2] - 10.0).abs() < FLOAT_TOLERANCE); // 3.0 / 0.3
|
||
assert!((f_div["B"][0] - 10.0).abs() < FLOAT_TOLERANCE); // 4.0 / 0.4
|
||
assert!((f_div["B"][1] - 10.0).abs() < FLOAT_TOLERANCE); // 5.0 / 0.5
|
||
assert!((f_div["B"][2] - 10.0).abs() < FLOAT_TOLERANCE); // 6.0 / 0.6
|
||
}
|
||
|
||
#[test]
|
||
fn test_frame_arithmetic_ops_int() {
|
||
let frame1 = create_test_frame_int(); // X=[1,-2], Y=[3,-4]
|
||
let frame2 = create_test_frame_int_alt(); // X=[10,20], Y=[30,40]
|
||
|
||
let frame_add = &frame1 + &frame2; // X=[11,18], Y=[33,36]
|
||
assert_eq!(frame_add.columns(), frame1.columns());
|
||
assert_eq!(frame_add.index(), frame1.index());
|
||
assert_eq!(frame_add["X"], vec![11, 18]);
|
||
assert_eq!(frame_add["Y"], vec![33, 36]);
|
||
|
||
let frame_sub = &frame1 - &frame2; // X=[-9,-22], Y=[-27,-44]
|
||
assert_eq!(frame_sub.columns(), frame1.columns());
|
||
assert_eq!(frame_sub.index(), frame1.index());
|
||
assert_eq!(frame_sub["X"], vec![-9, -22]);
|
||
assert_eq!(frame_sub["Y"], vec![-27, -44]);
|
||
|
||
let frame_mul = &frame1 * &frame2; // X=[10,-40], Y=[90,-160]
|
||
assert_eq!(frame_mul.columns(), frame1.columns());
|
||
assert_eq!(frame_mul.index(), frame1.index());
|
||
assert_eq!(frame_mul["X"], vec![10, -40]);
|
||
assert_eq!(frame_mul["Y"], vec![90, -160]);
|
||
|
||
// Integer division (truncates)
|
||
let frame_div = &frame2 / &frame1; // X=[10/1, 20/-2]=[10,-10], Y=[30/3, 40/-4]=[10,-10]
|
||
assert_eq!(frame_div.columns(), frame1.columns());
|
||
assert_eq!(frame_div.index(), frame1.index());
|
||
assert_eq!(frame_div["X"], vec![10, -10]);
|
||
assert_eq!(frame_div["Y"], vec![10, -10]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_frame_arithmetic_ops_date_index() {
|
||
let dates = vec![d(2024, 1, 1), d(2024, 1, 2)];
|
||
let index = Some(RowIndex::Date(dates));
|
||
let m1 = Matrix::from_cols(vec![vec![1, 2], vec![10, 20]]);
|
||
let m2 = Matrix::from_cols(vec![vec![3, 4], vec![30, 40]]);
|
||
let f1 = Frame::new(m1, vec!["A", "B"], index.clone());
|
||
let f2 = Frame::new(m2, vec!["A", "B"], index.clone());
|
||
|
||
let f_add = &f1 + &f2;
|
||
assert_eq!(f_add.columns(), f1.columns());
|
||
assert_eq!(f_add.index(), f1.index());
|
||
assert_eq!(f_add["A"], vec![4, 6]);
|
||
assert_eq!(f_add["B"], vec![40, 60]);
|
||
assert_eq!(f_add.get_row_date(d(2024, 1, 1))["A"], 4);
|
||
assert_eq!(f_add.get_row_date(d(2024, 1, 2))["B"], 60);
|
||
}
|
||
|
||
#[test]
|
||
fn test_bitwise_ops_and_not() {
|
||
let frame_a = create_test_frame_bool(); // P=[T,F], Q=[F,T]
|
||
let frame_b = create_test_frame_bool_alt(); // P=[T,T], Q=[F,F]
|
||
|
||
// Bitwise AND
|
||
let frame_and = &frame_a & &frame_b; // P=[T&T, F&T]->[T,F], Q=[F&F, T&F]->[F,F]
|
||
assert_eq!(frame_and.columns(), frame_a.columns());
|
||
assert_eq!(frame_and.index(), frame_a.index());
|
||
assert_eq!(frame_and["P"], vec![true, false]);
|
||
assert_eq!(frame_and["Q"], vec![false, false]);
|
||
|
||
// Logical NOT (takes ownership)
|
||
let frame_a_clone = frame_a.clone(); // Clone frame_a as Not consumes it
|
||
let frame_not = !frame_a_clone;
|
||
assert_eq!(frame_not.columns(), frame_a.columns());
|
||
assert_eq!(frame_not.index(), frame_a.index());
|
||
assert_eq!(frame_not["P"], vec![false, true]);
|
||
assert_eq!(frame_not["Q"], vec![true, false]);
|
||
|
||
// Check original frame_a is unchanged
|
||
assert_eq!(frame_a["P"], vec![true, false]);
|
||
}
|
||
#[test]
|
||
fn test_bitwise_ops_or_xor() {
|
||
let frame_a = create_test_frame_bool(); // P=[T,F], Q=[F,T]
|
||
let frame_b = create_test_frame_bool_alt(); // P=[T,T], Q=[F,F]
|
||
|
||
// Bitwise OR
|
||
let frame_or = &frame_a | &frame_b; // P=[T|T, F|T]->[T,T], Q=[F|F, T|F]->[F,T]
|
||
assert_eq!(frame_or.columns(), frame_a.columns());
|
||
assert_eq!(frame_or.index(), frame_a.index());
|
||
assert_eq!(frame_or["P"], vec![true, true]);
|
||
assert_eq!(frame_or["Q"], vec![false, true]);
|
||
|
||
// Bitwise XOR
|
||
let frame_xor = &frame_a ^ &frame_b; // P=[T^T, F^T]->[F,T], Q=[F^F, T^F]->[F,T]
|
||
assert_eq!(frame_xor.columns(), frame_a.columns());
|
||
assert_eq!(frame_xor.index(), frame_a.index());
|
||
assert_eq!(frame_xor["P"], vec![false, true]);
|
||
assert_eq!(frame_xor["Q"], vec![false, true]);
|
||
}
|
||
|
||
#[test]
|
||
#[should_panic(expected = "row indices mismatch")]
|
||
fn frame_elementwise_ops_panic_index() {
|
||
let frame1 = create_test_frame_f64(); // Range index 0..3
|
||
let matrix2 = create_test_matrix_f64_alt(); // 3 rows
|
||
let index2 = RowIndex::Int(vec![10, 20, 30]); // Different index type/values
|
||
let frame2 = Frame::new(matrix2, vec!["A", "B"], Some(index2));
|
||
let _ = &frame1 + &frame2; // Should panic due to index mismatch
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "column names mismatch")]
|
||
fn frame_elementwise_ops_panic_cols() {
|
||
let frame1 = create_test_frame_f64(); // Columns ["A", "B"]
|
||
let matrix2 = create_test_matrix_f64_alt();
|
||
let frame2 = Frame::new(matrix2, vec!["X", "Y"], None); // Different column names
|
||
let _ = &frame1 + &frame2; // Should panic due to column name mismatch
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "row indices mismatch")]
|
||
fn frame_bitwise_ops_panic_index() {
|
||
let frame1 = create_test_frame_bool(); // Range index 0..2
|
||
let matrix2 = create_test_matrix_bool_alt(); // 2 rows
|
||
let index2 = RowIndex::Int(vec![10, 20]); // Different index
|
||
let frame2 = Frame::new(matrix2, vec!["P", "Q"], Some(index2));
|
||
let _ = &frame1 & &frame2;
|
||
}
|
||
#[test]
|
||
#[should_panic(expected = "column names mismatch")]
|
||
fn frame_bitwise_ops_panic_cols() {
|
||
let frame1 = create_test_frame_bool(); // Cols P, Q
|
||
let matrix2 = create_test_matrix_bool_alt();
|
||
let frame2 = Frame::new(matrix2, vec!["X", "Y"], None); // Cols X, Y
|
||
let _ = &frame1 | &frame2;
|
||
}
|
||
|
||
// --- Debug Format Tests ---
|
||
#[test]
|
||
fn test_frame_debug_format() {
|
||
let frame = create_test_frame_f64();
|
||
let debug_str = format!("{:?}", frame);
|
||
println!("Frame Debug: {}", debug_str); // Print for manual inspection
|
||
assert!(debug_str.starts_with("Frame"));
|
||
assert!(debug_str.contains("column_names: [\"A\", \"B\"]"));
|
||
assert!(debug_str.contains("index: Range(0..3)"));
|
||
assert!(debug_str.contains("matrix_dims: (3, 2)"));
|
||
// Check for key-value pairs independently of order
|
||
assert!(debug_str.contains("\"A\": 0"));
|
||
assert!(debug_str.contains("\"B\": 1"));
|
||
assert!(debug_str.contains("index_lookup: None"));
|
||
}
|
||
#[test]
|
||
fn test_frame_debug_format_date_index() {
|
||
let matrix = create_test_matrix_string();
|
||
let index = RowIndex::Date(vec![d(2023, 5, 1), d(2023, 5, 10)]);
|
||
let frame = Frame::new(matrix, vec!["X", "Y"], Some(index));
|
||
let debug_str = format!("{:?}", frame);
|
||
println!("Frame Debug Date Index: {}", debug_str);
|
||
assert!(debug_str.starts_with("Frame"));
|
||
assert!(debug_str.contains("column_names: [\"X\", \"Y\"]"));
|
||
assert!(debug_str.contains("index: Date([2023-05-01, 2023-05-10])"));
|
||
assert!(debug_str.contains("matrix_dims: (2, 2)"));
|
||
assert!(debug_str.contains("\"X\": 0"));
|
||
assert!(debug_str.contains("\"Y\": 1"));
|
||
assert!(
|
||
debug_str.contains("index_lookup: Date({2023-05-10: 1, 2023-05-01: 0})")
|
||
|| debug_str.contains("index_lookup: Date({2023-05-01: 0, 2023-05-10: 1})")
|
||
);
|
||
}
|
||
#[test]
|
||
fn test_row_view_debug_format() {
|
||
let frame = create_test_frame_f64();
|
||
let row_view = frame.get_row(1); // Physical row 1
|
||
let debug_str = format!("{:?}", row_view);
|
||
println!("RowView Debug: {}", debug_str); // Print for manual inspection
|
||
assert!(debug_str.starts_with("FrameRowView"));
|
||
assert!(debug_str.contains("physical_row_idx: 1"));
|
||
assert!(debug_str.contains("columns: [\"A\", \"B\"]"));
|
||
assert!(debug_str.contains("data: [2.0, 5.0]")); // Data from row 1
|
||
}
|
||
#[test]
|
||
fn test_row_view_mut_debug_format() {
|
||
let mut frame = create_test_frame_f64();
|
||
let row_view_mut = frame.get_row_mut(0); // Physical row 0
|
||
let debug_str = format!("{:?}", row_view_mut);
|
||
println!("RowViewMut Debug: {}", debug_str); // Print for manual inspection
|
||
assert!(debug_str.starts_with("FrameRowViewMut"));
|
||
assert!(debug_str.contains("physical_row_idx: 0"));
|
||
assert!(debug_str.contains("columns: [\"A\", \"B\"]"));
|
||
// Debug format doesn't show data for Mut view to avoid borrow issues
|
||
}
|
||
|
||
// --- Miscellaneous Tests ---
|
||
#[test]
|
||
fn test_simple_accessors() {
|
||
let matrix = create_test_matrix_f64(); // 3 rows, 2 cols
|
||
let matrix_dims = (matrix.rows(), matrix.cols());
|
||
let mut frame = Frame::new(matrix.clone(), vec!["A", "B"], None);
|
||
assert_eq!(frame.rows(), 3);
|
||
assert_eq!(frame.cols(), 2);
|
||
assert_eq!(frame.columns(), &["A", "B"]);
|
||
assert_eq!(frame.index(), &RowIndex::Range(0..3));
|
||
// Check matrix accessors
|
||
assert_eq!((frame.matrix().rows(), frame.matrix().cols()), matrix_dims);
|
||
assert_eq!(frame.matrix().get(0, 0), matrix.get(0, 0)); // Get (0,0) via matrix ref
|
||
assert_eq!(frame.matrix().get(0, 0), &1.0);
|
||
// Check mutable matrix accessor (use with caution)
|
||
assert_eq!(
|
||
(frame.matrix_mut().rows(), frame.matrix_mut().cols()),
|
||
matrix_dims
|
||
);
|
||
*frame.matrix_mut().get_mut(0, 0) = 99.0; // Mutate matrix directly
|
||
assert_eq!(frame.matrix().get(0, 0), &99.0); // Verify change via matrix ref
|
||
assert_eq!(frame["A"][0], 99.0); // Verify change via Frame column access
|
||
}
|
||
}
|