This commit is contained in:
2025-11-13 13:54:21 -07:00
parent 306ea9088d
commit 2f96492b31

View File

@@ -1,5 +1,9 @@
use std::{ use std::{
cmp::{max, min}, fmt::Display, fs, io::{Read, Write}, path::PathBuf cmp::{max, min},
fmt::Display,
fs,
io::{Read, Write},
path::PathBuf,
}; };
use evalexpr::*; use evalexpr::*;
@@ -68,14 +72,27 @@ impl Grid {
/// does not already have the extension. /// does not already have the extension.
pub fn save_to(&mut self, path: impl Into<PathBuf>) -> std::io::Result<()> { pub fn save_to(&mut self, path: impl Into<PathBuf>) -> std::io::Result<()> {
let mut path = path.into(); let mut path = path.into();
const CSV: &str = "csv";
const CUSTOM_EXT: &str = "nscim";
let resolve_values;
match path.extension() { match path.extension() {
Some(ext) => { Some(ext) => {
if ext != "csv" { match ext.to_str() {
path.add_extension("csv"); Some(CSV) => {
resolve_values = true;
}
_ => {
resolve_values = false;
path.add_extension(CUSTOM_EXT);
}
} }
} }
None => { None => {
path.add_extension("csv"); resolve_values = false;
path.add_extension(CUSTOM_EXT);
} }
} }
@@ -84,21 +101,18 @@ impl Grid {
for y in 0..=my { for y in 0..=my {
for x in 0..=mx { for x in 0..=mx {
let cell = &self.cells[x][y]; let cell = &self.cells[x][y];
let mut display = cell.as_ref().map(|f| f.to_string()).unwrap_or(String::new());
// escape quotes " -> "" let data = if let Some(cell) = cell {
let needs_escaping = if let Ok(val) = self.evaluate(&cell.to_string()) && resolve_values {
display.char_indices().filter(|f| f.1 == CSV_ESCAPE).map(|f| f.0).collect::<Vec<usize>>(); val.to_string()
for idx in needs_escaping.iter().rev() {
display.insert(*idx, CSV_ESCAPE);
}
// escape string of it has a comma
if display.contains(CSV_DELIMITER) {
write!(f, "\"{display}\"{CSV_DELIMITER}")?;
} else { } else {
write!(f, "{display}{CSV_DELIMITER}")?; cell.to_string_csv_escaped()
} }
} else {
CSV_DELIMITER.to_string()
};
write!(f, "{data}")?;
} }
write!(f, "\n")?; write!(f, "\n")?;
} }
@@ -317,7 +331,7 @@ impl Grid {
// At least half the arguments are gone // At least half the arguments are gone
if chars.len() == 0 || nums.len() == 0 { if chars.len() == 0 || nums.len() == 0 {
return None return None;
} }
// get the x index from the chars // get the x index from the chars
@@ -411,6 +425,23 @@ impl Into<CellType> for String {
} }
impl CellType { impl CellType {
fn to_string_csv_escaped(&self) -> String {
let mut display = self.to_string();
// escape quotes " -> ""
let needs_escaping = display.char_indices().filter(|f| f.1 == CSV_ESCAPE).map(|f| f.0).collect::<Vec<usize>>();
for idx in needs_escaping.iter().rev() {
display.insert(*idx, CSV_ESCAPE);
}
// escape string of it has a comma
if display.contains(CSV_DELIMITER) {
format!("\"{display}\"{CSV_DELIMITER}")
} else {
format!("{display}{CSV_DELIMITER}")
}
}
fn duck_type<'a>(value: impl Into<String>) -> Self { fn duck_type<'a>(value: impl Into<String>) -> Self {
let value = value.into(); let value = value.into();
@@ -674,29 +705,52 @@ fn xlookup_function() {
#[test] #[test]
fn parse_csv() { fn parse_csv() {
//standard parsing //standard parsing
assert_eq!(Grid::parse_csv_line("1,2,3"), vec![Some("1".to_string()), Some("2".to_string()), Some("3".to_string())]); assert_eq!(
Grid::parse_csv_line("1,2,3"),
vec![Some("1".to_string()), Some("2".to_string()), Some("3".to_string())]
);
// comma in a cell // comma in a cell
assert_eq!(Grid::parse_csv_line("1,\",\",3"), vec![Some("1".to_string()), Some(",".to_string()), Some("3".to_string())]); assert_eq!(
Grid::parse_csv_line("1,\",\",3"),
vec![Some("1".to_string()), Some(",".to_string()), Some("3".to_string())]
);
// quotes in a cell // quotes in a cell
assert_eq!(Grid::parse_csv_line("1,she said \"\"wow\"\",3"), vec![Some("1".to_string()), Some("she said \"wow\"".to_string()), Some("3".to_string())]); assert_eq!(
Grid::parse_csv_line("1,she said \"\"wow\"\",3"),
vec![Some("1".to_string()), Some("she said \"wow\"".to_string()), Some("3".to_string())]
);
// quotes and comma in cell // quotes and comma in cell
assert_eq!(Grid::parse_csv_line("1,\"she said \"\"hello, world\"\"\",3"), vec![Some("1".to_string()), Some("she said \"hello, world\"".to_string()), Some("3".to_string())]); assert_eq!(
Grid::parse_csv_line("1,\"she said \"\"hello, world\"\"\",3"),
vec![Some("1".to_string()), Some("she said \"hello, world\"".to_string()), Some("3".to_string())]
);
// ending with a quote // ending with a quote
assert_eq!(Grid::parse_csv_line("1,she said \"\"hello world\"\""), vec![Some("1".to_string()), Some("she said \"hello world\"".to_string())]); assert_eq!(
Grid::parse_csv_line("1,she said \"\"hello world\"\""),
vec![Some("1".to_string()), Some("she said \"hello world\"".to_string())]
);
// ending with a quote with a comma // ending with a quote with a comma
assert_eq!(Grid::parse_csv_line("1,\"she said \"\"hello, world\"\"\""), vec![Some("1".to_string()), Some("she said \"hello, world\"".to_string())]); assert_eq!(
Grid::parse_csv_line("1,\"she said \"\"hello, world\"\"\""),
vec![Some("1".to_string()), Some("she said \"hello, world\"".to_string())]
);
// starting with a quote // starting with a quote
assert_eq!(Grid::parse_csv_line("\"\"hello world\"\" is what she said,1"), vec![Some("\"hello world\" is what she said".to_string()), Some("1".to_string())]); assert_eq!(
Grid::parse_csv_line("\"\"hello world\"\" is what she said,1"),
vec![Some("\"hello world\" is what she said".to_string()), Some("1".to_string())]
);
// starting with a quote with a comma // starting with a quote with a comma
assert_eq!(Grid::parse_csv_line("\"\"\"hello, world\"\" is what she said\",1"), vec![Some("\"hello, world\" is what she said".to_string()), Some("1".to_string())]); assert_eq!(
Grid::parse_csv_line("\"\"\"hello, world\"\" is what she said\",1"),
vec![Some("\"hello, world\" is what she said".to_string()), Some("1".to_string())]
);
} }
#[test] #[test]