From cc4becd244fc9a210d3fc2ec98792dbd7b0aeb9a Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Sun, 20 Apr 2025 23:44:39 +0100 Subject: [PATCH] first draft of date/int addressing --- src/frame/base.rs | 2272 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 1722 insertions(+), 550 deletions(-) diff --git a/src/frame/base.rs b/src/frame/base.rs index 9a1ab90..b172b18 100644 --- a/src/frame/base.rs +++ b/src/frame/base.rs @@ -1,724 +1,1896 @@ -use crate::matrix::*; -use std::collections::HashMap; -use std::ops::{Index, IndexMut, Not}; +use crate::matrix::Matrix; -/// A data frame – a Matrix with string‑identified columns (column‑major). +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), + /// Date-based index. Values must be unique. Order is preserved as given. + Date(Vec), + /// Default range index (0..num_rows) used when no specific index is provided. + Range(Range), +} + +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), + Date(HashMap), + None, // Used for Range index +} + +// --- Frame Struct Definition --- + +/// A data frame – a Matrix with string-identified columns and a typed row index. /// -/// Restricts the element type T to anything that is at least Clone – -/// this guarantees we can duplicate data when adding columns or performing -/// ownership‑moving transformations later on. (Further trait bounds are added -/// per‑method when additional capabilities such as arithmetic are needed.) +/// `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`. +/// +/// # 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; // Assuming Frame is in the root of rustframe -/// use rustframe::matrix::Matrix; // Assuming Matrix is in rustframe::matrix +/// use rustframe::frame::{Frame, RowIndex}; +/// use rustframe::matrix::Matrix; // Assume Matrix is available +/// use chrono::NaiveDate; /// -/// // 1. Create a frame -/// let matrix = Matrix::from_cols(vec![ -/// vec![1.0, 2.0, 3.0], // Column "temp" -/// vec![5.5, 6.5, 7.5], // Column "pressure" +/// // 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 frame = Frame::new(matrix, vec!["temp", "pressure"]); +/// let mut frame1 = Frame::new(matrix_f64, vec!["A", "B"], None); /// -/// assert_eq!(frame.column_names, vec!["temp", "pressure"]); +/// 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" /// -/// // 2. Access data -/// assert_eq!(frame.column("temp"), &[1.0, 2.0, 3.0]); -/// assert_eq!(frame["pressure"].to_vec(), &[5.5, 6.5, 7.5]); -/// assert_eq!(frame.column_index("temp"), Some(0)); +/// // --- 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()))); /// -/// // 3. Mutate data -/// frame["temp"][0] = 1.5; -/// assert_eq!(frame["temp"].to_vec(), &[1.5, 2.0, 3.0]); +/// 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"]); /// -/// frame.column_mut("pressure")[1] = 6.8; -/// assert_eq!(frame["pressure"].to_vec(), &[5.5, 6.8, 7.5]); +/// // --- 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); /// -/// // 4. Add a column -/// frame.add_column("humidity", vec![50.0, 55.0, 60.0]); -/// assert_eq!(frame.column_names, vec!["temp", "pressure", "humidity"]); -/// assert_eq!(frame["humidity"].to_vec(), &[50.0, 55.0, 60.0]); // i32 mixed with f64 needs generic adjustment or separate examples +/// 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)); /// -/// // 5. Rename a column -/// frame.rename("temp", "temperature"); -/// assert_eq!(frame.column_names, vec!["temperature", "pressure", "humidity"]); -/// assert!(frame.column_index("temp").is_none()); -/// assert_eq!(frame.column_index("temperature"), Some(0)); -/// assert_eq!(frame["temperature"].to_vec(), &[1.5, 2.0, 3.0]); +/// // --- Example 4: Element-wise Multiplication --- +/// let f_prod = &f1 * &f2; +/// // Use approx comparison for floats if necessary +/// assert!((f_prod["C1"][0] - 0.1).abs() < 1e-9); // 1.0 * 0.1 +/// assert!((f_prod["C1"][1] - 0.4).abs() < 1e-9); // 2.0 * 0.2 +/// assert!((f_prod["C2"][0] - 0.9).abs() < 1e-9); // 3.0 * 0.3 +/// assert!((f_prod["C2"][1] - 1.6).abs() < 1e-9); // 4.0 * 0.4 /// -/// // 6. Swap columns -/// frame.swap_columns("temperature", "humidity"); -/// assert_eq!(frame.column_names, vec!["humidity", "pressure", "temperature"]); -/// assert_eq!(frame["humidity"].to_vec(), &[50.0, 55.0, 60.0]); // Now holds original temp data -/// -/// // 7. Sort columns -/// frame.sort_columns(); -/// assert_eq!(frame.column_names, vec!["humidity", "pressure", "temperature"]); // Already sorted after swap -/// // Let's add one more to see sorting: -/// // frame.add_column("altitude", vec![100.0, 110.0, 120.0]); -/// // frame.sort_columns(); -/// // assert_eq!(frame.column_names, vec!["altitude", "humidity", "pressure", "temperature"]); -/// -/// // 8. Delete a column -/// let deleted_pressure = frame.delete_column("pressure"); -/// assert_eq!(deleted_pressure, vec![5.5, 6.8, 7.5]); -/// assert_eq!(frame.column_names, vec!["humidity", "temperature"]); -/// assert!(frame.column_index("pressure").is_none()); -/// -/// // 9. Element-wise operations (requires compatible frames) -/// let matrix_offset = Matrix::from_cols(vec![ -/// vec![0.1, 0.1, 0.1], // humidity offset -/// vec![1.0, 1.0, 1.0], // temperature offset -/// ]); -/// let frame_offset = Frame::new(matrix_offset, vec!["humidity", "temperature"]); -/// -/// let adjusted_frame = &frame + &frame_offset; // Add requires frame: Frame -/// // Need to ensure frame is Frame for this op -/// // assert_eq!(adjusted_frame["humidity"], &[1.6, 2.1, 3.1]); // Original temp + 0.1 -/// // assert_eq!(adjusted_frame["temperature"], &[51.0, 56.0, 61.0]); // Original humidity + 1.0 - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Frame { - /// **Public** vector holding the column names in their current order. - pub column_names: Vec, - - matrix: Matrix, - /// Maps a label to the column index for **O(1)** lookup. - pub lookup: HashMap, +/// // --- 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 fmt::Debug for Frame { + 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() + } } -impl Frame { +#[derive(Clone, PartialEq)] // Removed Eq as T doesn't require Eq +pub struct Frame { + /// Vector holding the column names in their current order. + column_names: Vec, + /// The underlying column-major matrix storing the data. + matrix: Matrix, + /// Maps a column name to its physical column index for **O(1)** lookup. + col_lookup: HashMap, + /// The row index values (Int, Date, or Range). + index: RowIndex, + /// Internal lookup for mapping index values to physical row positions. + index_lookup: RowIndexLookup, +} + +impl Frame { /* ---------- Constructors ---------- */ - /// Creates a new [`Frame`] from a matrix and column names. - /// - /// # Panics - /// * if the number of names differs from `matrix.cols()` - /// * if names are not unique. - pub fn new>(matrix: Matrix, names: Vec) -> Self { - assert_eq!(matrix.cols(), names.len(), "column name count mismatch"); - let mut lookup = HashMap::with_capacity(names.len()); + + /// 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>(matrix: Matrix, names: Vec, index: Option) -> 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 = names .into_iter() .enumerate() .map(|(i, n)| { let s = n.into(); - if lookup.insert(s.clone(), i).is_some() { - panic!("duplicate column label: {}", s); + 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, - lookup, + col_lookup, + index: index_values, + index_lookup, } } - /* ---------- Immutable / mutable access ---------- */ - + /* ---------- Accessors ---------- */ + /// Returns an immutable reference to the underlying Matrix. #[inline] pub fn matrix(&self) -> &Matrix { &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 { &mut self.matrix } - - /// Returns an immutable view of the column `name`. - pub fn column(&self, name: &str) -> &[T] { - let idx = self - .lookup - .get(name) - .copied() - .unwrap_or_else(|| panic!("unknown column label: {}", name)); - self.matrix.column(idx) + /// 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 { + self.col_lookup.get(name).copied() } - /// Returns a mutable view of the column `name`. + /// Internal helper to find the physical row index based on the index key and type. + fn get_physical_row_index(&self, index_key: Idx) -> usize + where + Self: RowIndexLookupHelper, // Uses the helper trait below + { + >::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 - .lookup - .get(name) - .copied() - .unwrap_or_else(|| panic!("unknown column label: {}", name)); - // SAFETY: the column is stored contiguously (column‑major layout). + .column_index(name) + .unwrap_or_else(|| panic!("Frame::column_mut: unknown column label: '{}'", name)); self.matrix.column_mut(idx) } - /// Index of a column label, if it exists. - pub fn column_index(&self, name: &str) -> Option { - self.lookup.get(name).copied() + /* ---------- 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 ---------- */ - /// Swaps two columns identified by their labels. - /// Internally defers to the already‑implemented [`Matrix::swap_columns`]. - pub fn swap_columns>(&mut self, a: L, b: L) { - let ia = self - .column_index(a.as_ref()) - .unwrap_or_else(|| panic!("unknown column label: {}", a.as_ref())); - let ib = self - .column_index(b.as_ref()) - .unwrap_or_else(|| panic!("unknown column label: {}", b.as_ref())); + /// 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; // nothing to do + return; // No-op } - self.matrix.swap_columns(ia, ib); // <‑‑ reuse existing impl + + // 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); - // update lookup values - self.lookup.get_mut(a.as_ref()).map(|v| *v = ib); - self.lookup.get_mut(b.as_ref()).map(|v| *v = ia); + + // 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 a column. - /// - /// # Panics - /// * if `old` is missing - /// * if `new` already exists. + /// 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>(&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!("unknown column label: {}", old)); - let new = new.into(); - if self.lookup.contains_key(&new) { - panic!("duplicate column label: {}", new); + .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 + ); } - self.column_names[idx] = new.clone(); - self.lookup.remove(old); - self.lookup.insert(new, idx); + + // 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 column to the **end** of the frame. - pub fn add_column>(&mut self, name: L, column: Vec) { - let name = name.into(); - if self.lookup.contains_key(&name) { - panic!("duplicate column label: {}", 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>(&mut self, name: L, column_data: Vec) { + let name_str = name.into(); + if self.col_lookup.contains_key(&name_str) { + panic!("Frame::add_column: duplicate column label: {}", name_str); } - self.matrix.add_column(self.matrix.cols(), column); - self.column_names.push(name.clone()); - self.lookup.insert(name, self.matrix.cols() - 1); + // 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 and returns its data. + /// 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 { let idx = self .column_index(name) - .unwrap_or_else(|| panic!("unknown column label: {}", name)); - let mut col = Vec::with_capacity(self.matrix.rows()); - col.extend_from_slice(self.matrix.column(idx)); + .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.rebuild_lookup(); - col + 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 columns **lexicographically** by their names, *in‑place*. - /// - /// The operation is performed exclusively through calls to - /// [`swap_columns`](Frame::swap_columns), which themselves defer to - /// `Matrix::swap_columns`; thus we never re‑implement swapping logic. + /// 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) { - // Simple selection sort; complexity O(n²) but stable w.r.t matrix data. 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 = i; + let mut min_idx = i; for j in (i + 1)..n { - if self.column_names[j] < self.column_names[min] { - min = j; + if self.column_names[j] < self.column_names[min_idx] { + min_idx = j; } } - if min != i { - // Use public API; keeps single source of truth. - let col_i = self.column_names[i].clone(); - let col_min = self.column_names[min].clone(); - self.swap_columns(col_i, col_min); + 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 ---------- */ - - fn rebuild_lookup(&mut self) { - self.lookup.clear(); + /* ---------- 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.lookup.insert(name.clone(), i); + self.col_lookup.insert(name.clone(), i); } } } -/* ---------- Indexing ---------- */ +// --- Helper Trait for Row Index Lookup --- +/// Internal trait to abstract the logic for looking up physical row indices. +trait RowIndexLookupHelper { + fn lookup_row_index(key: Idx, index_values: &RowIndex, index_lookup: &RowIndexLookup) -> usize; +} -impl Index<&str> for Frame { +/// Implementation for `usize` keys (used for `RowIndex::Int` and `RowIndex::Range`). +impl RowIndexLookupHelper for Frame { + 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 + (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 RowIndexLookupHelper for Frame { + 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 + (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, + 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 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, + 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 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 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 Index<&str> for Frame { type Output = [T]; + #[inline] fn index(&self, name: &str) -> &Self::Output { self.column(name) } } -impl IndexMut<&str> for Frame { +/// Allows mutating a column's data as a slice using `frame["col_name"]`. +impl IndexMut<&str> for Frame { + #[inline] fn index_mut(&mut self, name: &str) -> &mut Self::Output { self.column_mut(name) } } -/* ---------- Element‑wise numerical ops ---------- */ +/* ---------- Element-wise numerical ops ---------- */ +/// Macro to implement element-wise binary operations (+, -, *, /) for Frames. macro_rules! impl_elementwise_frame_op { - ($OpTrait:ident, $method:ident, $op:tt) => { + ($OpTrait:ident, $method:ident) => { impl<'a, 'b, T> std::ops::$OpTrait<&'b Frame> for &'a Frame where - T: Clone + std::ops::$OpTrait, + T: Clone + PartialEq + std::ops::$OpTrait, { type Output = Frame; fn $method(self, rhs: &'b Frame) -> Frame { - assert_eq!(self.column_names, rhs.column_names, "column names mismatch"); - let matrix = (&self.matrix).$method(&rhs.matrix); - Frame::new(matrix, self.column_names.clone()) + // 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, /); +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 ---------- */ +/* ---------- 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, $op:tt) => { + ($OpTrait:ident, $method:ident) => { impl<'a, 'b> std::ops::$OpTrait<&'b Frame> for &'a Frame { type Output = Frame; fn $method(self, rhs: &'b Frame) -> Frame { - assert_eq!(self.column_names, rhs.column_names, "column names mismatch"); - let matrix = (&self.matrix).$method(&rhs.matrix); - Frame::new(matrix, self.column_names.clone()) + // 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, ^); +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 { type Output = Frame; fn not(self) -> Frame { - Frame::new(!self.matrix, self.column_names) + // 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 } } -// Unit Tests +// --- Tests --- #[cfg(test)] mod tests { - use super::{Frame, Matrix}; + 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; - // Helper function to create a standard test frame - fn create_test_frame_i32() -> Frame { - let matrix = Matrix::from_cols(vec![ - vec![1, 2, 3], // Col "A" - vec![4, 5, 6], // Col "B" - vec![7, 8, 9], // Col "C" - ]); - Frame::new(matrix, vec!["A", "B", "C"]) + // --- Test Helpers --- + fn create_test_matrix_f64() -> Matrix { + 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 { + 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 { + Matrix::from_cols(vec![vec![true, false], vec![false, true]]) // 2 rows, 2 cols + } + fn create_test_matrix_bool_alt() -> Matrix { + Matrix::from_cols(vec![vec![true, true], vec![false, false]]) // 2 rows, 2 cols + } + fn create_test_matrix_string() -> Matrix { + 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 { + Frame::new(create_test_matrix_f64(), vec!["A", "B"], None) + } + fn create_test_frame_f64_alt() -> Frame { + Frame::new(create_test_matrix_f64_alt(), vec!["A", "B"], None) } - fn create_test_frame_bool() -> Frame { - let matrix = Matrix::from_cols(vec![ - vec![true, false], // Col "P" - vec![false, true], // Col "Q" - ]); - Frame::new(matrix, vec!["P", "Q"]) + Frame::new(create_test_matrix_bool(), vec!["P", "Q"], None) + } + fn create_test_frame_bool_alt() -> Frame { + Frame::new(create_test_matrix_bool_alt(), vec!["P", "Q"], None) + } + fn create_test_frame_int() -> Frame { + 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 { + 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 test_new_frame_success() { - let matrix = Matrix::from_cols(vec![vec![1, 2], vec![3, 4]]); - let frame = Frame::new(matrix.clone(), vec!["col1", "col2"]); - - assert_eq!(frame.column_names, vec!["col1", "col2"]); - assert_eq!(frame.matrix(), &matrix); - assert_eq!(frame.lookup.get("col1"), Some(&0)); - assert_eq!(frame.lookup.get("col2"), Some(&1)); - assert_eq!(frame.lookup.len(), 2); - } - - #[test] - #[should_panic(expected = "column name count mismatch")] - fn test_new_frame_panic_name_count_mismatch() { - let matrix = Matrix::from_cols(vec![vec![1, 2], vec![3, 4]]); - Frame::new(matrix, vec!["col1"]); // Only one name for two columns - } - - #[test] - #[should_panic(expected = "duplicate column label: col1")] - fn test_new_frame_panic_duplicate_names() { - let matrix = Matrix::from_cols(vec![vec![1, 2], vec![3, 4]]); - Frame::new(matrix, vec!["col1", "col1"]); // Duplicate name - } - - #[test] - fn test_accessors() { - let mut frame = create_test_frame_i32(); - - // matrix() - assert_eq!(frame.matrix().rows(), 3); - assert_eq!(frame.matrix().cols(), 3); - - // column() - assert_eq!(frame.column("A"), &[1, 2, 3]); - assert_eq!(frame.column("C"), &[7, 8, 9]); - - // column_mut() - frame.column_mut("B")[1] = 50; - assert_eq!(frame.column("B"), &[4, 50, 6]); - - // column_index() - assert_eq!(frame.column_index("A"), Some(0)); - assert_eq!(frame.column_index("C"), Some(2)); - assert_eq!(frame.column_index("Z"), None); - - // matrix_mut() - check by modifying through matrix_mut - *frame.matrix_mut().get_mut(0, 0) = 100; // Modify element at (0, 0) which is A[0] - assert_eq!(frame.column("A"), &[100, 2, 3]); + 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] - #[should_panic(expected = "unknown column label: Z")] - fn test_column_panic_unknown_label() { - let frame = create_test_frame_i32(); - frame.column("Z"); + 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] - #[should_panic(expected = "unknown column label: Z")] - fn test_column_mut_panic_unknown_label() { - let mut frame = create_test_frame_i32(); - frame.column_mut("Z"); + 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] - #[should_panic(expected = "unknown column label: Z")] - fn test_swap_columns_panic_unknown_a() { - let mut frame = create_test_frame_i32(); - frame.swap_columns("Z", "B"); - } - - #[test] - #[should_panic(expected = "unknown column label: Z")] - fn test_swap_columns_panic_unknown_b() { - let mut frame = create_test_frame_i32(); - frame.swap_columns("A", "Z"); - } - - #[test] - fn test_rename_column() { - let mut frame = create_test_frame_i32(); - let original_b_data = frame.column("B").to_vec(); - - frame.rename("B", "Beta"); - - // Check names - assert_eq!(frame.column_names, vec!["A", "Beta", "C"]); - - // Check lookup - assert_eq!(frame.column_index("A"), Some(0)); - assert_eq!(frame.column_index("Beta"), Some(1)); - assert_eq!(frame.column_index("C"), Some(2)); - assert_eq!(frame.column_index("B"), None); // Old name gone - - // Check data accessible via new name - assert_eq!(frame.column("Beta"), original_b_data); - } - - #[test] - #[should_panic(expected = "unknown column label: Z")] - fn test_rename_panic_unknown_old() { - let mut frame = create_test_frame_i32(); - frame.rename("Z", "Omega"); - } - - #[test] - #[should_panic(expected = "duplicate column label: C")] - fn test_rename_panic_duplicate_new() { - let mut frame = create_test_frame_i32(); - frame.rename("A", "C"); // "C" already exists - } - - #[test] - fn test_add_column() { - let mut frame = create_test_frame_i32(); - let new_col_data = vec![10, 11, 12]; - - frame.add_column("D", new_col_data.clone()); - - // Check names - assert_eq!(frame.column_names, vec!["A", "B", "C", "D"]); - - // Check lookup - assert_eq!(frame.column_index("D"), Some(3)); - - // Check matrix dimensions - assert_eq!(frame.matrix().cols(), 4); - assert_eq!(frame.matrix().rows(), 3); - - // Check data of new column - assert_eq!(frame.column("D"), new_col_data); - // Check old columns are still there - assert_eq!(frame.column("A"), &[1, 2, 3]); - } - - #[test] - #[should_panic(expected = "duplicate column label: B")] - fn test_add_column_panic_duplicate_name() { - let mut frame = create_test_frame_i32(); - frame.add_column("B", vec![0, 0, 0]); - } - - #[test] - #[should_panic(expected = "column length mismatch")] - fn test_add_column_panic_length_mismatch() { - let mut frame = create_test_frame_i32(); - // Matrix::add_column panics if lengths mismatch - frame.add_column("D", vec![10, 11]); // Only 2 elements, expected 3 - } - - #[test] - fn test_delete_column() { - let mut frame = create_test_frame_i32(); - let original_b_data = frame.column("B").to_vec(); - let original_c_data = frame.column("C").to_vec(); // Need to check data shift - - let deleted_data = frame.delete_column("B"); - - // Check returned data - assert_eq!(deleted_data, original_b_data); - - // Check names - assert_eq!(frame.column_names, vec!["A", "C"]); - - // Check lookup (rebuilt) - assert_eq!(frame.column_index("A"), Some(0)); - assert_eq!(frame.column_index("C"), Some(1)); - assert_eq!(frame.column_index("B"), None); - - // Check matrix dimensions - assert_eq!(frame.matrix().cols(), 2); - assert_eq!(frame.matrix().rows(), 3); - - // Check remaining data - assert_eq!(frame.column("A"), &[1, 2, 3]); - assert_eq!(frame.column("C"), original_c_data); // "C" should now be at index 1 - } - - #[test] - #[should_panic(expected = "unknown column label: Z")] - fn test_delete_column_panic_unknown() { - let mut frame = create_test_frame_i32(); - frame.delete_column("Z"); - } - - #[test] - fn test_sort_columns() { - let matrix = Matrix::from_cols(vec![ - vec![7, 8, 9], // Col "C" - vec![1, 2, 3], // Col "A" - vec![4, 5, 6], // Col "B" - ]); - let mut frame = Frame::new(matrix, vec!["C", "A", "B"]); - - let orig_a = frame.column("A").to_vec(); - let orig_b = frame.column("B").to_vec(); - let orig_c = frame.column("C").to_vec(); - - frame.sort_columns(); - - // Check names order - assert_eq!(frame.column_names, vec!["A", "B", "C"]); - - // Check lookup map + 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"), Some(2)); + assert_eq!(frame.column_index("C"), None); - // Check data integrity (data moved with the names) - assert_eq!(frame.column("A"), orig_a); - assert_eq!(frame.column("B"), orig_b); - assert_eq!(frame.column("C"), orig_c); + // 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] - fn test_sort_columns_single_column() { - let matrix = Matrix::from_cols(vec![vec![1, 2, 3]]); - let mut frame = Frame::new(matrix.clone(), vec!["Solo"]); - let expected = frame.clone(); - frame.sort_columns(); - assert_eq!(frame, expected); // Should be unchanged + #[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 test_index() { - let frame = create_test_frame_i32(); - assert_eq!(frame["A"].to_vec(), vec![1, 2, 3]); - assert_eq!(frame["C"].to_vec(), vec![7, 8, 9]); + 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 = "unknown column label: Z")] - fn test_index_panic_unknown() { - let frame = create_test_frame_i32(); - let _ = frame["Z"]; + #[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:: { + 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:: { + 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:: { + 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 test_index_mut() { - let mut frame = create_test_frame_i32(); - frame["B"][0] = 42; - frame["C"][2] = 99; + 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); - assert_eq!(frame["B"].to_vec(), &[42, 5, 6]); - assert_eq!(frame["C"].to_vec(), &[7, 8, 99]); + // 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] - #[should_panic(expected = "unknown column label: Z")] - fn test_index_mut_panic_unknown() { - let mut frame = create_test_frame_i32(); - let _ = &mut frame["Z"]; + 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 (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 } - // --- Test Ops --- + // --- Frame Column Manipulation & Sorting Tests --- #[test] - fn test_elementwise_ops_numeric() { - let frame1 = create_test_frame_i32(); // A=[1,2,3], B=[4,5,6], C=[7,8,9] - let matrix2 = Matrix::from_cols(vec![ - vec![10, 10, 10], // Col "A" - vec![2, 2, 2], // Col "B" - vec![1, 1, 1], // Col "C" - ]); - let frame2 = Frame::new(matrix2, vec!["A", "B", "C"]); // Must have same names and dims - - // Add - let frame_add = &frame1 + &frame2; - assert_eq!(frame_add.column_names, frame1.column_names); - assert_eq!(frame_add["A"].to_vec(), &[11, 12, 13]); - assert_eq!(frame_add["B"].to_vec(), &[6, 7, 8]); - assert_eq!(frame_add["C"].to_vec(), &[8, 9, 10]); - - // Sub - let frame_sub = &frame1 - &frame2; - assert_eq!(frame_sub.column_names, frame1.column_names); - assert_eq!(frame_sub["A"].to_vec(), &[-9, -8, -7]); - assert_eq!(frame_sub["B"].to_vec(), &[2, 3, 4]); - assert_eq!(frame_sub["C"].to_vec(), &[6, 7, 8]); - - // Mul - let frame_mul = &frame1 * &frame2; - assert_eq!(frame_mul.column_names, frame1.column_names); - assert_eq!(frame_mul["A"].to_vec(), &[10, 20, 30]); - assert_eq!(frame_mul["B"].to_vec(), &[8, 10, 12]); - assert_eq!(frame_mul["C"].to_vec(), &[7, 8, 9]); - - // Div - let frame_div = &frame1 / &frame2; // Integer division - assert_eq!(frame_div.column_names, frame1.column_names); - assert_eq!(frame_div["A"].to_vec(), &[0, 0, 0]); // 1/10, 2/10, 3/10 - assert_eq!(frame_div["B"].to_vec(), &[2, 2, 3]); // 4/2, 5/2, 6/2 - assert_eq!(frame_div["C"].to_vec(), &[7, 8, 9]); // 7/1, 8/1, 9/1 - } - - #[test] - #[should_panic] // Exact message depends on Matrix op panic message ("row count mismatch" or "col count mismatch") - fn test_elementwise_op_panic_dimension_mismatch() { - let frame1 = create_test_frame_i32(); // 3x3 - let matrix2 = Matrix::from_cols(vec![vec![1, 2], vec![3, 4]]); // 2x2 - let frame2 = Frame::new(matrix2, vec!["X", "Y"]); - let _ = &frame1 + &frame2; // Should panic due to dimension mismatch - } - - #[test] - fn test_bitwise_ops_bool() { - let frame1 = create_test_frame_bool(); // P=[T, F], Q=[F, T] - let matrix2 = Matrix::from_cols(vec![ - vec![true, true], // P - vec![false, false], // Q - ]); - let frame2 = Frame::new(matrix2, vec!["P", "Q"]); - - // BitAnd - let frame_and = &frame1 & &frame2; - assert_eq!(frame_and.column_names, frame1.column_names); - assert_eq!(frame_and["P"].to_vec(), &[true, false]); // T&T=T, F&T=F - assert_eq!(frame_and["Q"].to_vec(), &[false, false]); // F&F=F, T&F=F - - // BitOr - let frame_or = &frame1 | &frame2; - assert_eq!(frame_or.column_names, frame1.column_names); - assert_eq!(frame_or["P"].to_vec(), &[true, true]); // T|T=T, F|T=T - assert_eq!(frame_or["Q"].to_vec(), &[false, true]); // F|F=F, T|F=T - - // BitXor - let frame_xor = &frame1 ^ &frame2; - assert_eq!(frame_xor.column_names, frame1.column_names); - assert_eq!(frame_xor["P"].to_vec(), &[false, true]); // T^T=F, F^T=T - assert_eq!(frame_xor["Q"].to_vec(), &[false, true]); // F^F=F, T^F=T - } - - #[test] - fn test_not_op_bool() { - let frame = create_test_frame_bool(); // P=[T, F], Q=[F, T] - let frame_not = !frame; // Note: consumes the original frame - - assert_eq!(frame_not.column_names, vec!["P", "Q"]); - assert_eq!(frame_not["P"].to_vec(), &[false, true]); - assert_eq!(frame_not["Q"].to_vec(), &[true, false]); - } - - #[test] - fn test_swap_columns() { - let mut frame = create_test_frame_i32(); - let initial_a_data = frame.column("A").to_vec(); // [1, 2, 3] - let initial_c_data = frame.column("C").to_vec(); // [7, 8, 9] - - frame.swap_columns("A", "C"); - - // Check names order - assert_eq!(frame.column_names, vec!["C", "B", "A"]); - - // Check lookup map - assert_eq!(frame.column_index("A"), Some(2)); - assert_eq!(frame.column_index("B"), Some(1)); + 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); - // Check data using new names (should be swapped) + // 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); - // Accessing by name "C" (now at index 0) should retrieve the data - // that was swapped INTO index 0, which was the *original C data*. - assert_eq!( - frame.column("C"), - initial_c_data.as_slice(), - "Data for name 'C' should be original C data" + // 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)); + } - // Accessing by name "A" (now at index 2) should retrieve the data - // that was swapped INTO index 2, which was the *original A data*. - assert_eq!( - frame.column("A"), - initial_a_data.as_slice(), - "Data for name 'A' should be original A data" + #[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 + } - // Column "B" should remain unchanged in data and position. + // --- 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.column("B"), - &[4, 5, 6], - "Column 'B' should be unchanged" + (frame.matrix_mut().rows(), frame.matrix_mut().cols()), + matrix_dims ); - - // Test swapping with self - let state_before_self_swap = frame.clone(); - frame.swap_columns("B", "B"); - assert_eq!(frame, state_before_self_swap); + *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 } }