use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub enum DataValue {
Number(f32),
Text(String),
Null,
}
impl DataValue {
#[must_use]
pub fn as_f32(&self) -> Option<f32> {
match self {
DataValue::Number(n) => Some(*n),
_ => None,
}
}
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
DataValue::Text(s) => Some(s.as_str()),
_ => None,
}
}
}
impl From<f32> for DataValue {
fn from(v: f32) -> Self {
DataValue::Number(v)
}
}
impl From<&str> for DataValue {
fn from(s: &str) -> Self {
DataValue::Text(s.to_string())
}
}
impl From<String> for DataValue {
fn from(s: String) -> Self {
DataValue::Text(s)
}
}
#[derive(Debug, Clone, Default)]
pub struct DataFrame {
columns: HashMap<String, Vec<DataValue>>,
n_rows: usize,
}
impl DataFrame {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_xy(x: &[f32], y: &[f32]) -> Self {
let n = x.len().min(y.len());
let mut df = Self::new();
df.add_column_f32("x", &x[..n]);
df.add_column_f32("y", &y[..n]);
df
}
#[must_use]
pub fn from_data(data: &[f32]) -> Self {
let mut df = Self::new();
df.add_column_f32("data", data);
df
}
pub fn add_column_f32(&mut self, name: &str, data: &[f32]) {
let values: Vec<DataValue> = data.iter().map(|&v| DataValue::Number(v)).collect();
self.n_rows = self.n_rows.max(values.len());
self.columns.insert(name.to_string(), values);
}
pub fn add_column_str(&mut self, name: &str, data: &[&str]) {
let values: Vec<DataValue> = data.iter().map(|&s| DataValue::Text(s.to_string())).collect();
self.n_rows = self.n_rows.max(values.len());
self.columns.insert(name.to_string(), values);
}
#[must_use]
pub fn get_f32(&self, name: &str) -> Option<Vec<f32>> {
self.columns.get(name).map(|col| col.iter().filter_map(DataValue::as_f32).collect())
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&[DataValue]> {
self.columns.get(name).map(std::vec::Vec::as_slice)
}
#[must_use]
pub fn nrow(&self) -> usize {
self.n_rows
}
#[must_use]
pub fn ncol(&self) -> usize {
self.columns.len()
}
#[must_use]
pub fn has_column(&self, name: &str) -> bool {
self.columns.contains_key(name)
}
#[must_use]
pub fn columns(&self) -> Vec<&str> {
self.columns.keys().map(std::string::String::as_str).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dataframe_from_xy() {
let df = DataFrame::from_xy(&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]);
assert_eq!(df.nrow(), 3);
assert_eq!(df.ncol(), 2);
assert!(df.has_column("x"));
assert!(df.has_column("y"));
}
#[test]
fn test_dataframe_get_f32() {
let df = DataFrame::from_xy(&[1.0, 2.0], &[3.0, 4.0]);
let x = df.get_f32("x").expect("operation should succeed");
assert_eq!(x, vec![1.0, 2.0]);
}
#[test]
fn test_data_value_conversions() {
let num: DataValue = 42.0f32.into();
assert_eq!(num.as_f32(), Some(42.0));
let text: DataValue = "hello".into();
assert_eq!(text.as_str(), Some("hello"));
}
#[test]
fn test_dataframe_from_data() {
let df = DataFrame::from_data(&[1.0, 2.0, 3.0, 4.0]);
assert_eq!(df.nrow(), 4);
assert!(df.has_column("data"));
let data = df.get_f32("data").expect("operation should succeed");
assert_eq!(data, vec![1.0, 2.0, 3.0, 4.0]);
}
#[test]
fn test_dataframe_add_column_str() {
let mut df = DataFrame::new();
df.add_column_str("names", &["Alice", "Bob", "Charlie"]);
assert_eq!(df.nrow(), 3);
assert!(df.has_column("names"));
}
#[test]
fn test_dataframe_get() {
let df = DataFrame::from_xy(&[1.0, 2.0], &[3.0, 4.0]);
let col = df.get("x").expect("key should exist");
assert_eq!(col.len(), 2);
assert_eq!(col[0].as_f32(), Some(1.0));
}
#[test]
fn test_dataframe_get_missing() {
let df = DataFrame::new();
assert!(df.get("missing").is_none());
assert!(df.get_f32("missing").is_none());
}
#[test]
fn test_dataframe_columns() {
let df = DataFrame::from_xy(&[1.0], &[2.0]);
let cols = df.columns();
assert_eq!(cols.len(), 2);
assert!(cols.contains(&"x"));
assert!(cols.contains(&"y"));
}
#[test]
fn test_dataframe_empty() {
let df = DataFrame::new();
assert_eq!(df.nrow(), 0);
assert_eq!(df.ncol(), 0);
assert!(!df.has_column("anything"));
}
#[test]
fn test_dataframe_from_xy_unequal() {
let df = DataFrame::from_xy(&[1.0, 2.0, 3.0], &[4.0, 5.0]);
let x = df.get_f32("x").expect("operation should succeed");
let y = df.get_f32("y").expect("operation should succeed");
assert_eq!(x.len(), 2);
assert_eq!(y.len(), 2);
}
#[test]
fn test_data_value_null() {
let null = DataValue::Null;
assert_eq!(null.as_f32(), None);
assert_eq!(null.as_str(), None);
}
#[test]
fn test_data_value_from_string() {
let text: DataValue = String::from("test").into();
assert_eq!(text.as_str(), Some("test"));
}
#[test]
fn test_data_value_as_f32_on_text() {
let text: DataValue = "hello".into();
assert_eq!(text.as_f32(), None);
}
#[test]
fn test_data_value_as_str_on_number() {
let num: DataValue = 42.0f32.into();
assert_eq!(num.as_str(), None);
}
#[test]
fn test_data_value_debug_clone_eq() {
let v1 = DataValue::Number(42.0);
let v2 = v1.clone();
assert_eq!(v1, v2);
let _ = format!("{v1:?}");
}
#[test]
fn test_dataframe_debug_clone() {
let df = DataFrame::from_xy(&[1.0], &[2.0]);
let df2 = df.clone();
assert_eq!(df2.nrow(), 1);
let _ = format!("{df2:?}");
}
#[test]
fn test_dataframe_default() {
let df = DataFrame::default();
assert_eq!(df.nrow(), 0);
}
}