use std::borrow::Cow;
use crate::Error;
fn sanitize_formula_injection(s: &str) -> Cow<'_, str> {
if s.starts_with(['=', '+', '-', '@', '\t', '\r']) {
Cow::Owned(format!("\t{s}"))
} else {
Cow::Borrowed(s)
}
}
pub struct Table<T> {
size: (usize, usize),
cells: Vec<Option<T>>,
}
impl<T> Table<T> {
pub fn set(&mut self, row: usize, col: usize, item: T) {
assert!(
row < self.size.0,
"row index {row} out of bounds for table with {} rows",
self.size.0
);
assert!(
col < self.size.1,
"column index {col} out of bounds for table with {} columns",
self.size.1
);
self.cells[row * self.size.1 + col] = Some(item);
}
pub fn rows(&self) -> Vec<Vec<Option<&T>>> {
let mut rows = vec![];
for i in 0..self.size.0 {
let mut row = vec![];
for j in 0..self.size.1 {
row.push(self.cells[i * self.size.1 + j].as_ref());
}
rows.push(row);
}
rows
}
}
impl<T> Table<T>
where
T: Clone,
{
pub fn new(size: (usize, usize)) -> Self {
Self {
size,
cells: vec![None; size.0 * size.1],
}
}
pub fn map<T2>(&self, f: impl Fn(usize, usize, &T) -> T2) -> Table<T2>
where
T2: Clone,
{
map_table(self, f)
}
}
fn map_table<S, T, F>(table: &Table<T>, f: F) -> Table<S>
where
F: Fn(usize, usize, &T) -> S,
S: Clone,
{
let mut new_table = Table::new(table.size);
for i in 0..table.size.0 {
for j in 0..table.size.1 {
if let Some(item) = &table.cells[i * table.size.1 + j] {
new_table.set(i, j, f(i, j, item));
}
}
}
new_table
}
impl<T> Table<T>
where
T: std::fmt::Display,
{
pub fn write_csv(&self, writer: &mut impl std::io::Write) -> Result<(), Error> {
let mut buf: Vec<u8> = Vec::new();
{
let mut csv_writer = csv::Writer::from_writer(&mut buf);
for row in &self.rows() {
let mut record = csv::StringRecord::new();
for cell in row {
if let Some(item) = cell {
record.push_field(&sanitize_formula_injection(&item.to_string()));
} else {
record.push_field("");
}
}
csv_writer
.write_record(&record)
.map_err(|_| Error::FailedToConvertToCSV)?;
}
csv_writer
.flush()
.map_err(|_| Error::FailedToConvertToCSV)?;
}
writer
.write_all(&buf)
.map_err(|_| Error::FailedToConvertToCSV)
}
pub fn to_csv(&self) -> Result<String, Error> {
let mut buf = std::io::BufWriter::new(Vec::new());
self.write_csv(&mut buf)?;
let bytes = buf.into_inner().map_err(|_| Error::FailedToConvertToCSV)?;
String::from_utf8(bytes).map_err(|_| Error::FailedToConvertToCSV)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_updates_cell_when_index_is_in_bounds() {
let mut table = Table::new((2, 2));
table.set(1, 1, 42);
assert_eq!(table.rows()[1][1], Some(&42));
}
#[test]
#[should_panic(expected = "row index 2 out of bounds for table with 2 rows")]
fn set_panics_when_row_is_out_of_bounds() {
let mut table = Table::new((2, 2));
table.set(2, 0, 42);
}
#[test]
#[should_panic(expected = "column index 2 out of bounds for table with 2 columns")]
fn set_panics_when_column_is_out_of_bounds() {
let mut table = Table::new((2, 2));
table.set(0, 2, 42);
}
}