use crate::data_frame_list::DataFrameList;
use crate::utils::{csv_header, is_hashmap_dataframe, remove_trailing_comma};
use std::collections::HashMap;
use std::convert::From;
use std::convert::TryFrom;
use std::fmt;
use term_table::row::Row;
use term_table::table_cell::{Alignment, TableCell};
use term_table::{Table, TableStyle};
#[derive(Debug)]
pub enum DataFrameErrors {
DifferrentNumberRows,
EmptyDataFrame,
ColumnDoesNotExist,
RowDoesNotExist,
CouldNotOpenFile,
}
pub struct CSVDataFrame {
data: HashMap<String, Vec<String>>,
}
impl Default for CSVDataFrame {
fn default() -> Self {
Self::new()
}
}
impl<const N: usize> From<[(String, Vec<String>); N]> for CSVDataFrame {
fn from(array: [(String, Vec<String>); N]) -> Self {
CSVDataFrame {
data: HashMap::from(array),
}
}
}
impl From<DataFrameList> for CSVDataFrame {
fn from(data_frames: DataFrameList) -> Self {
CSVDataFrame {
data: data_frames
.iter()
.fold(HashMap::new(), |all_dataframes, next_dataframe| {
all_dataframes
.into_iter()
.chain(next_dataframe.data.clone())
.collect()
}),
}
}
}
impl TryFrom<HashMap<String, Vec<String>>> for CSVDataFrame {
type Error = DataFrameErrors;
fn try_from(mut data_frame: HashMap<String, Vec<String>>) -> Result<Self, DataFrameErrors> {
match is_hashmap_dataframe(&mut data_frame) {
true => Ok(CSVDataFrame { data: data_frame }),
false => Err(DataFrameErrors::DifferrentNumberRows),
}
}
}
impl CSVDataFrame {
pub fn new() -> Self {
CSVDataFrame {
data: HashMap::new(),
}
}
pub fn n_cols(&self) -> usize {
self.data.len()
}
pub fn n_rows(&self) -> usize {
if self.n_cols() == 0 {
0
} else {
self.data.iter().next().unwrap().1.len()
}
}
fn get_column_names(&self) -> Vec<String> {
self.data
.iter()
.map(|(column_name, _)| column_name.to_string())
.collect()
}
fn get_sorted_columns_names(&self) -> Vec<String> {
let mut columns = self.get_column_names();
columns.sort();
columns
}
#[allow(dead_code)]
fn get_column(&self, column_name: &str) -> Result<&Vec<String>, DataFrameErrors> {
if self.data.contains_key(column_name) {
Ok(self.data.get_key_value(column_name).unwrap().1)
} else {
Err(DataFrameErrors::ColumnDoesNotExist)
}
}
fn get_row(&self, row_idx: usize) -> Result<HashMap<String, String>, DataFrameErrors> {
if self.n_rows() <= row_idx {
Err(DataFrameErrors::RowDoesNotExist)
} else {
Ok(self
.data
.iter()
.map(|(column_name, column_values)| {
(column_name.clone(), column_values[row_idx].clone())
})
.collect())
}
}
pub fn to_csv(&self) -> String {
csv_header(self.get_sorted_columns_names())
+ &self.csv_body(self.get_sorted_columns_names())
}
fn csv_body(&self, columns: Vec<String>) -> String {
(0..self.n_rows())
.map(|row_idx| self.csv_row(&columns, row_idx))
.collect::<String>()
}
fn csv_row(&self, columns: &Vec<String>, row_idx: usize) -> String {
remove_trailing_comma(
columns
.iter()
.map(|column_name| {
self.get_row(row_idx)
.unwrap()
.get(column_name)
.unwrap()
.to_owned()
+ ","
})
.collect::<String>()
+ "\n",
)
}
}
impl fmt::Display for CSVDataFrame {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = Table::new();
table.style = TableStyle::simple();
let mut columns = self.get_column_names();
columns.sort();
table.add_row(Row::new(columns.iter().map(|column| {
TableCell::new_with_alignment(column, 2, Alignment::Center)
})));
for row_idx in 0..self.n_rows() {
table.add_row(Row::new(columns.iter().map(|column_name| {
TableCell::new_with_alignment(
self.get_row(row_idx).unwrap().get(column_name).unwrap(),
2,
Alignment::Center,
)
})));
}
write!(f, "{}", table.render())
}
}
#[cfg(test)]
mod tests {
use super::*;
mod test_from_dataframe_list {
use super::*;
#[test]
fn from_simple_dataframe_list() {
let mut my_test_dataframe_list = DataFrameList::new();
let _ = my_test_dataframe_list.push(
CSVDataFrame::try_from(HashMap::from([(
String::from("column-0-list-0"),
vec![String::from("value-0-0"), String::from("value-0-1")],
)]))
.unwrap(),
);
let _ = my_test_dataframe_list.push(
CSVDataFrame::try_from(HashMap::from([(
String::from("column-0-list-1"),
vec![String::from("value-0-0"), String::from("value-0-1")],
)]))
.unwrap(),
);
let my_test_dataframe = CSVDataFrame::from(my_test_dataframe_list);
assert_eq!(
my_test_dataframe.data,
HashMap::from([
(
String::from("column-0-list-0"),
vec![String::from("value-0-0"), String::from("value-0-1")],
),
(
String::from("column-0-list-1"),
vec![String::from("value-0-0"), String::from("value-0-1")],
),
])
);
}
}
mod test_from_tuple_list {
use super::*;
#[test]
fn sucess_from_list() {
let my_test_dataframe = CSVDataFrame::try_from(HashMap::from([
(
String::from("column-0-list-0"),
vec![String::from("value-0-0"), String::from("value-0-1")],
),
(
String::from("column-0-list-1"),
vec![String::from("value-0-0"), String::from("value-0-1")],
),
]))
.unwrap();
assert_eq!(
my_test_dataframe.data,
HashMap::from([
(
String::from("column-0-list-0"),
vec![String::from("value-0-0"), String::from("value-0-1")],
),
(
String::from("column-0-list-1"),
vec![String::from("value-0-0"), String::from("value-0-1")],
),
])
);
}
#[test]
fn failure_from_list() {
let error = CSVDataFrame::try_from(HashMap::from([
(
String::from("column-0-list-0"),
vec![
String::from("value-0-0"),
String::from("value-0-1"),
String::from("value-0-1"),
],
),
(
String::from("column-0-list-1"),
vec![String::from("value-0-0"), String::from("value-0-1")],
),
]));
assert!(matches!(
error.err().unwrap(),
DataFrameErrors::DifferrentNumberRows
));
}
}
mod test_column_names {
use super::*;
#[test]
fn get_three_column_names() {
assert_eq!(
test_utils::get_simple_dataframe().get_column_names().sort(),
vec![
String::from("column-1"),
String::from("column-2"),
String::from("column-3"),
]
.sort()
)
}
#[test]
fn get_no_column_names() {
assert_eq!(CSVDataFrame::new().get_column_names(), Vec::<String>::new());
}
}
mod test_get_column {
use super::*;
#[test]
fn get_three_columns() {
let my_data = test_utils::get_simple_dataframe();
assert_eq!(
my_data.get_column("column-0").unwrap(),
&vec![String::from("value-0-0"), String::from("value-0-1")]
);
assert_eq!(
my_data.get_column("column-1").unwrap(),
&vec![String::from("value-1-0"), String::from("value-1-1")]
);
assert_eq!(
my_data.get_column("column-2").unwrap(),
&vec![String::from("value-2-0"), String::from("value-2-1")]
);
}
#[test]
fn get_nonexisting_column() {
assert!(matches!(
CSVDataFrame::new().get_column("column_name").err().unwrap(),
DataFrameErrors::ColumnDoesNotExist
));
}
}
mod test_get_row {
use super::*;
#[test]
fn get_existing_rows() {
let my_data = test_utils::get_simple_dataframe();
assert_eq!(
my_data.get_row(0).unwrap(),
test_utils::get_hash_map(vec![
(String::from("column-0"), String::from("value-0-0")),
(String::from("column-1"), String::from("value-1-0")),
(String::from("column-2"), String::from("value-2-0"))
])
);
assert_eq!(
my_data.get_row(1).unwrap(),
test_utils::get_hash_map(vec![
(String::from("column-0"), String::from("value-0-1")),
(String::from("column-1"), String::from("value-1-1")),
(String::from("column-2"), String::from("value-2-1"))
])
);
}
#[test]
fn get_non_existing_row() {
assert!(matches!(
test_utils::get_simple_dataframe().get_row(2).err().unwrap(),
DataFrameErrors::RowDoesNotExist
));
}
#[test]
fn get_row_empty_dataframe() {
assert!(matches!(
CSVDataFrame::new().get_row(0).err().unwrap(),
DataFrameErrors::RowDoesNotExist
));
}
}
mod test_nrows {
use super::*;
#[test]
fn two_rows() {
assert_eq!(test_utils::get_simple_dataframe().n_rows(), 2);
}
#[test]
fn empty_dataframe() {
assert_eq!(CSVDataFrame::new().n_rows(), 0);
}
}
mod test_ncols {
use super::*;
#[test]
fn three_columns() {
assert_eq!(test_utils::get_simple_dataframe().n_cols(), 3);
}
#[test]
fn empty_dataframe() {
assert_eq!(CSVDataFrame::new().n_cols(), 0);
}
}
mod test_format {
use super::*;
#[test]
fn test_simple_dataframe() {
assert_eq!(
"+------------+------------+------------+\n\
| column-0 | column-1 | column-2 |\n\
+------------+------------+------------+\n\
| value-0-0 | value-1-0 | value-2-0 |\n\
+------------+------------+------------+\n\
| value-0-1 | value-1-1 | value-2-1 |\n\
+------------+------------+------------+\n",
format!("{}", test_utils::get_simple_dataframe())
)
}
}
mod test_to_csv {
use super::*;
#[test]
fn test_simple_dataframe() {
assert_eq!(
"column-0,column-1,column-2\n\
value-0-0,value-1-0,value-2-0\n\
value-0-1,value-1-1,value-2-1\n",
test_utils::get_simple_dataframe().to_csv()
)
}
}
mod test_csv_row {
use super::*;
#[test]
fn simple_data_test_row_1_all_cols() {
assert_eq!(
String::from("value-0-0,value-1-0,value-2-0\n"),
test_utils::get_simple_dataframe().csv_row(
&vec![
String::from("column-0"),
String::from("column-1"),
String::from("column-2")
],
0
)
)
}
#[test]
fn simple_data_test_row_1_two_cols() {
assert_eq!(
String::from("value-0-0,value-1-0\n"),
test_utils::get_simple_dataframe().csv_row(
&vec![String::from("column-0"), String::from("column-1"),],
0
)
)
}
#[test]
fn simple_data_test_row_2_all_cols() {
assert_eq!(
String::from("value-0-1,value-1-1,value-2-1\n"),
test_utils::get_simple_dataframe().csv_row(
&vec![
String::from("column-0"),
String::from("column-1"),
String::from("column-2")
],
1
)
)
}
}
mod test_csv_body {
use super::*;
#[test]
fn simple_data_test() {
assert_eq!(
String::from("value-0-0,value-1-0,value-2-0\nvalue-0-1,value-1-1,value-2-1\n"),
test_utils::get_simple_dataframe().csv_body(vec![
String::from("column-0"),
String::from("column-1"),
String::from("column-2")
],)
)
}
}
mod test_utils {
use super::*;
pub fn get_simple_dataframe() -> CSVDataFrame {
CSVDataFrame::try_from(HashMap::from([
(
String::from("column-0"),
vec![String::from("value-0-0"), String::from("value-0-1")],
),
(
String::from("column-1"),
vec![String::from("value-1-0"), String::from("value-1-1")],
),
(
String::from("column-2"),
vec![String::from("value-2-0"), String::from("value-2-1")],
),
]))
.unwrap()
}
pub fn get_hash_map<T>(elems: Vec<(T, T)>) -> HashMap<T, T>
where
T: std::cmp::Eq + std::hash::Hash,
{
let mut hashmap = HashMap::new();
for (key, value) in elems {
hashmap.insert(key, value);
}
hashmap
}
}
}