Compare commits

..

No commits in common. "a61ff8a4e1240c9e87381fc1813e9ea55b230a72" and "eb4fefe36370a89293c9c68944e4f0d0611f7ae7" have entirely different histories.

4 changed files with 1615 additions and 944 deletions

163
README.md
View File

@ -1,3 +1,4 @@
# <img align="center" alt="Rustframe" src=".github/rustframe_logo.png" height="50" /> rustframe
<!-- though the centre tag doesn't work as it would noramlly, it achieves the desired effect -->
@ -5,13 +6,12 @@
📚 [Docs](https://magnus167.github.io/rustframe/) | 🐙 [GitHub](https://github.com/Magnus167/rustframe) | 🌐 [Gitea mirror](https://gitea.nulltech.uk/Magnus167/rustframe) | 🦀 [Crates.io](https://crates.io/crates/rustframe) | 🔖 [docs.rs](https://docs.rs/rustframe/latest/rustframe/)
<!-- [![Last commit](https://img.shields.io/endpoint?url=https://magnus167.github.io/rustframe/rustframe/last-commit-date.json)](https://github.com/Magnus167/rustframe) -->
[![codecov](https://codecov.io/gh/Magnus167/rustframe/graph/badge.svg?token=J7ULJEFTVI)](https://codecov.io/gh/Magnus167/rustframe)
[![Coverage](https://img.shields.io/endpoint?url=https://magnus167.github.io/rustframe/docs/tarpaulin-badge.json)](https://magnus167.github.io/rustframe/docs/tarpaulin-report.html)
---
## Rustframe: _A lightweight dataframe & math toolkit for Rust_
## Rustframe: *A lightweight dataframe & math toolkit for Rust*
Rustframe provides intuitive dataframe, matrix, and series operations small-to-mid scale data analysis and manipulation.
@ -98,100 +98,101 @@ assert!(check);
```
---
## DataFrame Usage Example
```rust
use rustframe::dataframe::DataFrame;
use chrono::NaiveDate;
use std::collections::HashMap;
use std::any::TypeId; // Required for checking TypeId
use rustframe::{
dataframe::{DataFrame, TypedFrame, DataFrameColumn},
frame::{Frame, RowIndex},
matrix::Matrix,
};
// Helper for NaiveDate
fn d(y: i32, m: u32, d: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(y, m, d).unwrap()
// Helper to create a simple f64 TypedFrame (similar to test helpers)
fn create_f64_typed_frame(name: &str, data: Vec<f64>, index: Option<RowIndex>) -> TypedFrame {
let rows = data.len();
let matrix = Matrix::from_cols(vec![data]);
let frame_index = index.unwrap_or(RowIndex::Range(0..rows));
TypedFrame::F64(Frame::new(
matrix,
vec![name.to_string()],
Some(frame_index),
))
}
// Create a new DataFrame
let mut df = DataFrame::new();
// Helper to create a simple i64 TypedFrame
fn create_i64_typed_frame(name: &str, data: Vec<i64>, index: Option<RowIndex>) -> TypedFrame {
let rows = data.len();
let matrix = Matrix::from_cols(vec![data]);
let frame_index = index.unwrap_or(RowIndex::Range(0..rows));
TypedFrame::I64(Frame::new(
matrix,
vec![name.to_string()],
Some(frame_index),
))
}
// Add columns of different types
df.add_column("col_int1", vec![1, 2, 3, 4, 5]);
df.add_column("col_float1", vec![1.1, 2.2, 3.3, 4.4, 5.5]);
df.add_column("col_string", vec!["apple".to_string(), "banana".to_string(), "cherry".to_string(), "date".to_string(), "elderberry".to_string()]);
df.add_column("col_bool", vec![true, false, true, false, true]);
df.add_column("col_date", vec![d(2023,1,1), d(2023,1,2), d(2023,1,3), d(2023,1,4), d(2023,1,5)]);
// Helper to create a simple String TypedFrame
fn create_string_typed_frame(
name: &str,
data: Vec<String>,
index: Option<RowIndex>,
) -> TypedFrame {
let rows = data.len();
let matrix = Matrix::from_cols(vec![data]);
let frame_index = index.unwrap_or(RowIndex::Range(0..rows));
TypedFrame::String(Frame::new(
matrix,
vec![name.to_string()],
Some(frame_index),
))
}
println!("DataFrame after initial column additions:\n{}", df);
fn main() {
// 1. Create a DataFrame with different data types
let col_a = create_f64_typed_frame("A", vec![1.0, 2.0, 3.0], None);
let col_b = create_i64_typed_frame("B", vec![10, 20, 30], None);
let col_c = create_string_typed_frame(
"C",
vec!["apple".to_string(), "banana".to_string(), "cherry".to_string()],
None,
);
// Demonstrate frame re-use when adding columns of existing types
let initial_frames_count = df.num_internal_frames();
println!("\nInitial number of internal frames: {}", initial_frames_count);
let mut df = DataFrame::new(
vec![col_a, col_b, col_c],
vec!["A".to_string(), "B".to_string(), "C".to_string()],
None,
);
df.add_column("col_int2", vec![6, 7, 8, 9, 10]);
df.add_column("col_float2", vec![6.6, 7.7, 8.8, 9.9, 10.0]);
println!("Initial DataFrame:\n{:?}", df);
println!("Columns: {:?}", df.columns());
println!("Rows: {}", df.rows());
let frames_after_reuse = df.num_internal_frames();
println!("Number of internal frames after adding more columns of existing types: {}", frames_after_reuse);
assert_eq!(initial_frames_count, frames_after_reuse); // Should be equal, demonstrating re-use
// 2. Accessing columns
if let DataFrameColumn::F64(col_a_data) = df.column("A") {
println!("Column 'A' (f64): {:?}", col_a_data);
}
println!("\nDataFrame after adding more columns of existing types:\n{}", df);
if let DataFrameColumn::String(col_c_data) = df.column("C") {
println!("Column 'C' (String): {:?}", col_c_data);
}
// 3. Add a new column
let new_col_d = create_f64_typed_frame("D", vec![100.0, 200.0, 300.0], None);
df.add_column("D".to_string(), new_col_d);
println!("\nDataFrame after adding column 'D':\n{:?}", df);
println!("Columns after add: {:?}", df.columns());
// Get number of rows and columns
println!("Rows: {}", df.rows()); // Output: Rows: 5
println!("Columns: {}", df.cols()); // Output: Columns: 5
// 4. Rename a column
df.rename_column("A", "Alpha".to_string());
println!("\nDataFrame after renaming 'A' to 'Alpha':\n{:?}", df);
println!("Columns after rename: {:?}", df.columns());
// Get column names
println!("Column names: {:?}", df.get_column_names());
// Output: Column names: ["col_int", "col_float", "col_string", "col_bool", "col_date"]
// Get a specific column by name and type
let int_col = df.get_column::<i32>("col_int1").unwrap();
println!("Integer column (col_int1): {:?}", int_col); // Output: Integer column: [1, 2, 3, 4, 5]
let int_col2 = df.get_column::<i32>("col_int2").unwrap();
println!("Integer column (col_int2): {:?}", int_col2); // Output: Integer column: [6, 7, 8, 9, 10]
let float_col = df.get_column::<f64>("col_float1").unwrap();
println!("Float column (col_float1): {:?}", float_col); // Output: Float column: [1.1, 2.2, 3.3, 4.4, 5.5]
// Attempt to get a column with incorrect type (returns None)
let wrong_type_col = df.get_column::<bool>("col_int1");
println!("Wrong type column: {:?}", wrong_type_col); // Output: Wrong type column: None
// Get a row by index
let row_0 = df.get_row(0).unwrap();
println!("Row 0: {:?}", row_0);
// Output: Row 0: {"col_int1": "1", "col_float1": "1.1", "col_string": "apple", "col_bool": "true", "col_date": "2023-01-01", "col_int2": "6", "col_float2": "6.6"}
let row_2 = df.get_row(2).unwrap();
println!("Row 2: {:?}", row_2);
// Output: Row 2: {"col_int1": "3", "col_float1": "3.3", "col_string": "cherry", "col_bool": "true", "col_date": "2023-01-03", "col_int2": "8", "col_float2": "8.8"}
// Attempt to get an out-of-bounds row (returns None)
let row_out_of_bounds = df.get_row(10);
println!("Row out of bounds: {:?}", row_out_of_bounds); // Output: Row out of bounds: None
// Drop a column
df.drop_column("col_bool");
println!("\nDataFrame after dropping 'col_bool':\n{}", df);
println!("Columns after drop: {}", df.cols());
println!("Column names after drop: {:?}", df.get_column_names());
// Drop another column, ensuring the underlying Frame is removed if empty
df.drop_column("col_float1");
println!("\nDataFrame after dropping 'col_float1':\n{}", df);
println!("Columns after second drop: {}", df.cols());
println!("Column names after second drop: {:?}", df.get_column_names());
// Attempt to drop a non-existent column (will panic)
// df.drop_column("non_existent_col"); // Uncomment to see panic
// 5. Delete a column
let _deleted_col_b = df.delete_column("B");
println!("\nDataFrame after deleting column 'B':\n{:?}", df);
println!("Columns after delete: {:?}", df.columns());
}
```
### More examples
See the [examples](./examples/) directory for some demonstrations of Rustframe's syntax and functionality.

View File

@ -1,338 +0,0 @@
// src/gol.rs
use rand::{self, Rng};
use rustframe::matrix::{BoolMatrix, IntMatrix, Matrix};
/// Helper function to create a shifted version of the game board.
/// (Using the version provided by the user)
///
/// - `game`: The current state of the Game of Life as a `BoolMatrix`.
/// - `dr`: The row shift (delta row). Positive shifts down, negative shifts up.
/// - `dc`: The column shift (delta column). Positive shifts right, negative shifts left.
///
/// Returns an `IntMatrix` of the same dimensions as `game`.
/// - Cells in the shifted matrix get value `1` if the corresponding source cell in `game` was `true` (alive).
/// - Cells that would source from outside `game`'s bounds (due to the shift) get value `0`.
fn get_shifted_neighbor_layer(game: &BoolMatrix, dr: isize, dc: isize) -> IntMatrix {
let rows = game.rows();
let cols = game.cols();
if rows == 0 || cols == 0 {
// Handle 0x0 case, other 0-dim cases panic in Matrix::from_vec
return IntMatrix::from_vec(vec![], 0, 0);
}
// Initialize with a matrix of 0s using from_vec.
// This demonstrates creating an IntMatrix and then populating it.
let mut shifted_layer = IntMatrix::from_vec(vec![0i32; rows * cols], rows, cols);
for r_target in 0..rows {
// Iterate over cells in the *new* (target) shifted matrix
for c_target in 0..cols {
// Calculate where this target cell would have come from in the *original* game matrix
let r_source = r_target as isize - dr;
let c_source = c_target as isize - dc;
// Check if the source coordinates are within the bounds of the original game matrix
if r_source >= 0
&& r_source < rows as isize
&& c_source >= 0
&& c_source < cols as isize
{
// If the source cell in the original game was alive...
if game[(r_source as usize, c_source as usize)] {
// Demonstrates Index access on BoolMatrix
// ...then this cell in the shifted layer is 1.
shifted_layer[(r_target, c_target)] = 1; // Demonstrates IndexMut access on IntMatrix
}
}
// Else (source is out of bounds): it remains 0, as initialized.
}
}
shifted_layer // Return the constructed IntMatrix
}
/// Calculates the next generation of Conway's Game of Life.
///
/// This implementation uses a broadcast-like approach by creating shifted layers
/// for each neighbor and summing them up, then applying rules element-wise.
///
/// - `current_game`: A `&BoolMatrix` representing the current state (true=alive).
///
/// Returns: A new `BoolMatrix` for the next generation.
pub fn game_of_life_next_frame(current_game: &BoolMatrix) -> BoolMatrix {
let rows = current_game.rows();
let cols = current_game.cols();
if rows == 0 && cols == 0 {
return BoolMatrix::from_vec(vec![], 0, 0); // Return an empty BoolMatrix
}
// Assuming valid non-empty dimensions (e.g., 25x25) as per typical GOL.
// Your Matrix::from_vec would panic for other invalid 0-dim cases.
// Define the 8 neighbor offsets (row_delta, col_delta)
let neighbor_offsets: [(isize, isize); 8] = [
(-1, -1),
(-1, 0),
(-1, 1), // Top row (NW, N, NE)
(0, -1),
(0, 1), // Middle row (W, E)
(1, -1),
(1, 0),
(1, 1), // Bottom row (SW, S, SE)
];
// 1. Initialize `neighbor_counts` with the first shifted layer.
// This demonstrates creating an IntMatrix from a function and using it as a base.
let (first_dr, first_dc) = neighbor_offsets[0];
let mut neighbor_counts = get_shifted_neighbor_layer(current_game, first_dr, first_dc);
// 2. Add the remaining 7 neighbor layers.
// This demonstrates element-wise addition of matrices (`Matrix + Matrix`).
for i in 1..neighbor_offsets.len() {
let (dr, dc) = neighbor_offsets[i];
let next_neighbor_layer = get_shifted_neighbor_layer(current_game, dr, dc);
// `neighbor_counts` (owned IntMatrix) + `next_neighbor_layer` (owned IntMatrix)
// uses `impl Add for Matrix`, consumes both, returns new owned `IntMatrix`.
neighbor_counts = neighbor_counts + next_neighbor_layer;
}
// 3. Apply Game of Life rules using element-wise operations.
// Rule: Survival or Birth based on neighbor counts.
// A cell is alive in the next generation if:
// (it's currently alive AND has 2 or 3 neighbors) OR
// (it's currently dead AND has exactly 3 neighbors)
// `neighbor_counts.eq_elem(scalar)`:
// Demonstrates element-wise comparison of a Matrix with a scalar (broadcast).
// Returns an owned `BoolMatrix`.
let has_2_neighbors = neighbor_counts.eq_elem(2);
let has_3_neighbors = neighbor_counts.eq_elem(3); // This will be reused
// `has_2_neighbors | has_3_neighbors`:
// Demonstrates element-wise OR (`Matrix<bool> | Matrix<bool>`).
// Consumes both operands, returns an owned `BoolMatrix`.
let has_2_or_3_neighbors = has_2_neighbors | has_3_neighbors.clone(); // Clone has_3_neighbors as it's used again
// `current_game & &has_2_or_3_neighbors`:
// `current_game` is `&BoolMatrix`. `has_2_or_3_neighbors` is owned.
// Demonstrates element-wise AND (`&Matrix<bool> & &Matrix<bool>`).
// Borrows both operands, returns an owned `BoolMatrix`.
let survives = current_game & &has_2_or_3_neighbors;
// `!current_game`:
// Demonstrates element-wise NOT (`!&Matrix<bool>`).
// Borrows operand, returns an owned `BoolMatrix`.
let is_dead = !current_game;
// `is_dead & &has_3_neighbors`:
// `is_dead` is owned. `has_3_neighbors` is owned.
// Demonstrates element-wise AND (`Matrix<bool> & &Matrix<bool>`).
// Consumes `is_dead`, borrows `has_3_neighbors`, returns an owned `BoolMatrix`.
let births = is_dead & &has_3_neighbors;
// `survives | births`:
// Demonstrates element-wise OR (`Matrix<bool> | Matrix<bool>`).
// Consumes both operands, returns an owned `BoolMatrix`.
let next_frame_game = survives | births;
next_frame_game
}
pub fn generate_glider(board: &mut BoolMatrix, board_size: usize) {
// Initialize with a Glider pattern.
// It demonstrates how to set specific cells in the matrix.
// This demonstrates `IndexMut` for `current_board[(r, c)] = true;`.
let mut rng = rand::rng();
let r_offset = rng.random_range(0..(board_size - 3));
let c_offset = rng.random_range(0..(board_size - 3));
if board.rows() >= r_offset + 3 && board.cols() >= c_offset + 3 {
board[(r_offset + 0, c_offset + 1)] = true;
board[(r_offset + 1, c_offset + 2)] = true;
board[(r_offset + 2, c_offset + 0)] = true;
board[(r_offset + 2, c_offset + 1)] = true;
board[(r_offset + 2, c_offset + 2)] = true;
}
}
pub fn generate_pulsar(board: &mut BoolMatrix, board_size: usize) {
// Initialize with a Pulsar pattern.
// This demonstrates how to set specific cells in the matrix.
// This demonstrates `IndexMut` for `current_board[(r, c)] = true;`.
let mut rng = rand::rng();
let r_offset = rng.random_range(0..(board_size - 17));
let c_offset = rng.random_range(0..(board_size - 17));
if board.rows() >= r_offset + 17 && board.cols() >= c_offset + 17 {
let pulsar_coords = [
(2, 4),
(2, 5),
(2, 6),
(2, 10),
(2, 11),
(2, 12),
(4, 2),
(4, 7),
(4, 9),
(4, 14),
(5, 2),
(5, 7),
(5, 9),
(5, 14),
(6, 2),
(6, 7),
(6, 9),
(6, 14),
(7, 4),
(7, 5),
(7, 6),
(7, 10),
(7, 11),
(7, 12),
];
for &(dr, dc) in pulsar_coords.iter() {
board[(r_offset + dr, c_offset + dc)] = true;
}
}
}
pub fn detect_stable_state(
current_board: &BoolMatrix,
previous_board_state: &Option<BoolMatrix>,
) -> bool {
if let Some(ref prev_board) = previous_board_state {
// `*prev_board == current_board` demonstrates `PartialEq` for `Matrix`.
return *prev_board == *current_board;
}
false
}
pub fn hash_board(board: &BoolMatrix, primes: Vec<i32>) -> usize {
let board_ints_vec = board
.data()
.iter()
.map(|&cell| if cell { 1 } else { 0 })
.collect::<Vec<i32>>();
let ints_board = Matrix::from_vec(board_ints_vec, board.rows(), board.cols());
let primes_board = Matrix::from_vec(primes, ints_board.rows(), ints_board.cols());
let result = ints_board * primes_board;
let result: i32 = result.data().iter().sum();
result as usize
}
pub fn detect_repeating_state(board_hashes: &mut Vec<usize>) -> bool {
// so - detect alternating states. if 0==2, 1==3, 2==4, 3==5, 4==6, 5==7
if board_hashes.len() < 4 {
return false;
}
let mut result = false;
if (board_hashes[0] == board_hashes[2]) && (board_hashes[0] == board_hashes[2]) {
result = true;
}
// remove the 0th item
board_hashes.remove(0);
result
}
pub fn add_simulated_activity(current_board: &mut BoolMatrix, board_size: usize) {
for _ in 0..20 {
generate_glider(current_board, board_size);
}
// Generate a Pulsar pattern
for _ in 0..10 {
generate_pulsar(current_board, board_size);
}
}
// generate prime numbers
pub fn generate_primes(n: i32) -> Vec<i32> {
// I want to generate the first n primes
let mut primes = Vec::new();
let mut count = 0;
let mut num = 2; // Start checking for primes from 2
while count < n {
let mut is_prime = true;
for i in 2..=((num as f64).sqrt() as i32) {
if num % i == 0 {
is_prime = false;
break;
}
}
if is_prime {
primes.push(num);
count += 1;
}
num += 1;
}
primes
}
// --- Tests from previous example (can be kept or adapted) ---
#[cfg(test)]
mod tests {
use super::*;
use rustframe::matrix::{BoolMatrix, BoolOps}; // Assuming BoolOps is available for .count()
#[test]
fn test_blinker_oscillator() {
let initial_data = vec![false, true, false, false, true, false, false, true, false];
let game1 = BoolMatrix::from_vec(initial_data.clone(), 3, 3);
let expected_frame2_data = vec![false, false, false, true, true, true, false, false, false];
let expected_game2 = BoolMatrix::from_vec(expected_frame2_data, 3, 3);
let game2 = game_of_life_next_frame(&game1);
assert_eq!(
game2.data(),
expected_game2.data(),
"Frame 1 to Frame 2 failed for blinker"
);
let expected_game3 = BoolMatrix::from_vec(initial_data, 3, 3);
let game3 = game_of_life_next_frame(&game2);
assert_eq!(
game3.data(),
expected_game3.data(),
"Frame 2 to Frame 3 failed for blinker"
);
}
#[test]
fn test_empty_board_remains_empty() {
let board_3x3_all_false = BoolMatrix::from_vec(vec![false; 9], 3, 3);
let next_frame = game_of_life_next_frame(&board_3x3_all_false);
assert_eq!(
next_frame.count(),
0,
"All-false board should result in all-false"
);
}
#[test]
fn test_zero_size_board() {
let board_0x0 = BoolMatrix::from_vec(vec![], 0, 0);
let next_frame = game_of_life_next_frame(&board_0x0);
assert_eq!(next_frame.rows(), 0);
assert_eq!(next_frame.cols(), 0);
assert!(
next_frame.data().is_empty(),
"0x0 board should result in 0x0 board"
);
}
#[test]
fn test_still_life_block() {
let block_data = vec![
true, true, false, false, true, true, false, false, false, false, false, false, false,
false, false, false,
];
let game_block = BoolMatrix::from_vec(block_data.clone(), 4, 4);
let next_frame_block = game_of_life_next_frame(&game_block);
assert_eq!(
next_frame_block.data(),
game_block.data(),
"Block still life should remain unchanged"
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
//! This module provides the DataFrame structure for handling tabular data with mixed types.
pub mod df;
pub use df::{DataFrame, SubFrame};
pub use df::{DataFrame, DataFrameColumn, TypedFrame};