From 755dbc70f12f5e0e86684d023cd8702a37ad7696 Mon Sep 17 00:00:00 2001 From: Rushmore75 Date: Thu, 13 Nov 2025 14:29:40 -0700 Subject: [PATCH] organize --- src/app/app.rs | 2 +- src/app/clipboard.rs | 47 +----- src/app/logic/calc.rs | 331 +++++++++++++++++------------------------- src/app/logic/cell.rs | 104 +++++++++++++ src/app/logic/ctx.rs | 8 +- src/app/logic/mod.rs | 3 +- 6 files changed, 249 insertions(+), 246 deletions(-) create mode 100644 src/app/logic/cell.rs diff --git a/src/app/app.rs b/src/app/app.rs index c21a70c..406d476 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -7,7 +7,7 @@ use ratatui::{ }; use crate::app::{ - clipboard::Clipboard, error_msg::StatusMessage, logic::calc::{CellType, Grid}, mode::Mode, screen::ScreenSpace + clipboard::Clipboard, error_msg::StatusMessage, logic::{calc::Grid, cell::CellType}, mode::Mode, screen::ScreenSpace }; pub struct App { diff --git a/src/app/clipboard.rs b/src/app/clipboard.rs index 4eab934..95838ad 100644 --- a/src/app/clipboard.rs +++ b/src/app/clipboard.rs @@ -1,11 +1,6 @@ -use std::cmp::min; - use evalexpr::eval_with_context; -use crate::app::logic::{ - calc::{CellType, Grid}, - ctx::ExtractionContext, -}; +use crate::app::logic::{calc::Grid, cell::CellType, ctx::ExtractionContext}; #[cfg(test)] use crate::app::{ @@ -62,7 +57,7 @@ impl Clipboard { if translate { if let Some(cell) = cell { - let trans = Clipboard::translate_cell(cell, self.source_cell, into.cursor()); + let trans = cell.translate_cell(self.source_cell, into.cursor()); into.set_cell_raw(idx, Some(trans)); } else { // cell doesn't exist, no need to translate @@ -80,44 +75,6 @@ impl Clipboard { self.last_paste_cell = (cx, cy); } - fn translate_cell(cell: &CellType, from: (usize, usize), to: (usize, usize)) -> CellType { - match cell { - // don't translate non-equations - CellType::Number(_) | CellType::String(_) => return cell.clone(), - CellType::Equation(eq) => { - // extract all the variables - let ctx = ExtractionContext::new(); - let _ = eval_with_context(eq, &ctx); - - let mut rolling = eq.clone(); - // translate standard vars A0 -> A1 - for old_var in ctx.dump_vars() { - if let Some((src_x, src_y)) = Grid::parse_to_idx(&old_var) { - let (x1, y1) = from; - let x1 = x1 as i32; - let y1 = y1 as i32; - let (x2, y2) = to; - let x2 = x2 as i32; - let y2 = y2 as i32; - - let dest_x = (src_x as i32 + (x2 - x1)) as usize; - let dest_y = (src_y as i32 + (y2 - y1)) as usize; - - let alpha = Grid::num_to_char(dest_x); - let alpha = alpha.trim(); - let new_var = format!("{alpha}{dest_y}"); - - // swap out vars - rolling = rolling.replace(&old_var, &new_var); - } else { - // why you coping invalid stuff, nerd? - } - } - return rolling.into(); - } - } - } - /// Clones data from Grid into self. /// Start and end don't have to be sorted in any sort of way. The function works with /// any two points. diff --git a/src/app/logic/calc.rs b/src/app/logic/calc.rs index 7a5403d..ba47d85 100644 --- a/src/app/logic/calc.rs +++ b/src/app/logic/calc.rs @@ -8,7 +8,10 @@ use std::{ use evalexpr::*; -use crate::app::logic::ctx; +use crate::app::logic::{ + cell::{CSV_DELIMITER, CellType}, + ctx, +}; #[cfg(test)] use crate::app::app::App; @@ -34,135 +37,22 @@ impl std::fmt::Debug for Grid { } } -const CSV_DELIMITER: char = ','; -const CSV_ESCAPE: char = '"'; - impl Grid { - pub fn cursor(&self) -> (usize, usize) { - self.selected_cell - } - - pub fn apply_momentum(&mut self, (x, y): (i32, i32)) { - let (cx, cy) = self.cursor(); - - assert_eq!(0i32.saturating_add(-1), -1); - let x = (cx as i32).saturating_add(x); - let y = (cy as i32).saturating_add(y); - - // make it positive - let x = max(x, 0) as usize; - let y = max(y, 0) as usize; - - // keep it in the grid - let x = min(x, LEN - 1); - let y = min(y, LEN - 1); - - self.mv_cursor_to(x, y); - } - - pub fn mv_cursor_to(&mut self, x: usize, y: usize) { - self.selected_cell = (x, y) - } - - pub fn needs_to_be_saved(&self) -> bool { - self.dirty - } - - /// Save file to `path` as a csv. Path with have `csv` appended to it if it - /// does not already have the extension. - pub fn save_to(&mut self, path: impl Into) -> std::io::Result<()> { - let mut path = path.into(); - - const CSV: &str = "csv"; - const CUSTOM_EXT: &str = "nscim"; - - let resolve_values; - - match path.extension() { - Some(ext) => { - match ext.to_str() { - Some(CSV) => { - resolve_values = true; - } - _ => { - resolve_values = false; - path.add_extension(CUSTOM_EXT); - } - } - } - None => { - resolve_values = false; - path.add_extension(CUSTOM_EXT); + pub fn new() -> Self { + let mut a = Vec::with_capacity(LEN); + for _ in 0..LEN { + let mut b = Vec::with_capacity(LEN); + for _ in 0..LEN { + b.push(None) } + a.push(b) } - let mut f = fs::OpenOptions::new().write(true).append(false).truncate(true).create(true).open(path)?; - let (mx, my) = self.max(); - for y in 0..=my { - for x in 0..=mx { - let cell = &self.cells[x][y]; - - let data = if let Some(cell) = cell { - if let Ok(val) = self.evaluate(&cell.to_string()) && resolve_values { - val.to_string() - } else { - cell.to_string_csv_escaped() - } - } else { - CSV_DELIMITER.to_string() - }; - - write!(f, "{data}")?; - } - write!(f, "\n")?; + Self { + cells: a, + selected_cell: (0, 0), + dirty: false, } - f.flush()?; - - self.dirty = false; - Ok(()) - } - - #[must_use] - pub fn max_y_at_x(&self, x: usize) -> usize { - let mut max_y = 0; - if let Some(col) = self.cells.get(x) { - // we could do fancy things like .take_while(not null) but then - // we would have to deal with empty cells and stuff, which sounds - // boring. This will be fast "enough", considering the grid is - // probably only like 1k cells - for (yi, cell) in col.iter().enumerate() { - if cell.is_some() { - if yi > max_y { - max_y = yi - } - } - } - } - - max_y - } - - /// Iterate over the entire grid and see where - /// the farthest modified cell is. - #[must_use] - fn max(&self) -> (usize, usize) { - let mut max_x = 0; - let mut max_y = 0; - - for (xi, x) in self.cells.iter().enumerate() { - for (yi, cell) in x.iter().enumerate() { - if cell.is_some() { - if yi > max_y { - max_y = yi - } - if xi > max_x { - max_x = xi - } - } - } - } - - (max_x, max_y) } pub fn new_from_file(path: impl Into) -> std::io::Result { @@ -186,6 +76,64 @@ impl Grid { Ok(grid) } + /// Save file to `path` as a csv. Path with have `csv` appended to it if it + /// does not already have the extension. + pub fn save_to(&mut self, path: impl Into) -> std::io::Result<()> { + let mut path = path.into(); + + const CSV: &str = "csv"; + const CUSTOM_EXT: &str = "nscim"; + + let resolve_values; + + match path.extension() { + Some(ext) => match ext.to_str() { + Some(CSV) => { + resolve_values = true; + } + _ => { + resolve_values = false; + path.add_extension(CUSTOM_EXT); + } + }, + None => { + resolve_values = false; + path.add_extension(CUSTOM_EXT); + } + } + + let mut f = fs::OpenOptions::new().write(true).append(false).truncate(true).create(true).open(path)?; + let (mx, my) = self.max(); + for y in 0..=my { + for x in 0..=mx { + let cell = &self.cells[x][y]; + + let data = if let Some(cell) = cell { + if let Ok(val) = self.evaluate(&cell.to_string()) + && resolve_values + { + val.to_string() + } else { + cell.to_string_csv_escaped() + } + } else { + CSV_DELIMITER.to_string() + }; + + write!(f, "{data}")?; + } + write!(f, "\n")?; + } + f.flush()?; + + self.dirty = false; + Ok(()) + } + + pub fn needs_to_be_saved(&self) -> bool { + self.dirty + } + fn parse_csv_line(line: &str) -> Vec> { let mut iter = line.as_bytes().iter().map(|f| *f as char).peekable(); let mut cells = Vec::new(); @@ -261,21 +209,73 @@ impl Grid { cells } - pub fn new() -> Self { - let mut a = Vec::with_capacity(LEN); - for _ in 0..LEN { - let mut b = Vec::with_capacity(LEN); - for _ in 0..LEN { - b.push(None) + pub fn cursor(&self) -> (usize, usize) { + self.selected_cell + } + + pub fn mv_cursor_to(&mut self, x: usize, y: usize) { + self.selected_cell = (x, y) + } + + pub fn apply_momentum(&mut self, (x, y): (i32, i32)) { + let (cx, cy) = self.cursor(); + + assert_eq!(0i32.saturating_add(-1), -1); + let x = (cx as i32).saturating_add(x); + let y = (cy as i32).saturating_add(y); + + // make it positive + let x = max(x, 0) as usize; + let y = max(y, 0) as usize; + + // keep it in the grid + let x = min(x, LEN - 1); + let y = min(y, LEN - 1); + + self.mv_cursor_to(x, y); + } + + /// Iterate over the entire grid and see where + /// the farthest modified cell is. + #[must_use] + fn max(&self) -> (usize, usize) { + let mut max_x = 0; + let mut max_y = 0; + + for (xi, x) in self.cells.iter().enumerate() { + for (yi, cell) in x.iter().enumerate() { + if cell.is_some() { + if yi > max_y { + max_y = yi + } + if xi > max_x { + max_x = xi + } + } } - a.push(b) } - Self { - cells: a, - selected_cell: (0, 0), - dirty: false, + (max_x, max_y) + } + + #[must_use] + pub fn max_y_at_x(&self, x: usize) -> usize { + let mut max_y = 0; + if let Some(col) = self.cells.get(x) { + // we could do fancy things like .take_while(not null) but then + // we would have to deal with empty cells and stuff, which sounds + // boring. This will be fast "enough", considering the grid is + // probably only like 1k cells + for (yi, cell) in col.iter().enumerate() { + if cell.is_some() { + if yi > max_y { + max_y = yi + } + } + } } + + max_y } /// Only evaluates equations, such as `=10` or `=A1/C2`, not @@ -405,65 +405,6 @@ impl Default for Grid { } } -#[derive(Debug, Clone)] -pub enum CellType { - Number(f64), - String(String), - Equation(String), -} - -impl Into for f64 { - fn into(self) -> CellType { - CellType::duck_type(self.to_string()) - } -} - -impl Into for String { - fn into(self) -> CellType { - CellType::duck_type(self) - } -} - -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::>(); - 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) -> Self { - let value = value.into(); - - if let Ok(parse) = value.parse::() { - Self::Number(parse) - } else { - if value.starts_with('=') { Self::Equation(value) } else { Self::String(value) } - } - } -} - -impl Display for CellType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let d = match self { - CellType::Number(n) => n.to_string(), - CellType::String(n) => n.to_owned(), - CellType::Equation(r) => r.to_owned(), - }; - write!(f, "{d}") - } -} - // Do cells hold strings? #[test] fn cell_strings() { diff --git a/src/app/logic/cell.rs b/src/app/logic/cell.rs new file mode 100644 index 0000000..99ef761 --- /dev/null +++ b/src/app/logic/cell.rs @@ -0,0 +1,104 @@ +use std::fmt::Display; + +use evalexpr::eval_with_context; + +use crate::app::logic::{calc::Grid, ctx::ExtractionContext}; + +#[derive(Debug, Clone)] +pub enum CellType { + Number(f64), + String(String), + Equation(String), +} + +impl Into for f64 { + fn into(self) -> CellType { + CellType::duck_type(self.to_string()) + } +} + +impl Into for String { + fn into(self) -> CellType { + CellType::duck_type(self) + } +} + +pub const CSV_DELIMITER: char = ','; +const CSV_ESCAPE: char = '"'; + +impl CellType { + pub 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::>(); + 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) -> Self { + let value = value.into(); + + if let Ok(parse) = value.parse::() { + Self::Number(parse) + } else { + if value.starts_with('=') { Self::Equation(value) } else { Self::String(value) } + } + } + pub fn translate_cell(&self, from: (usize, usize), to: (usize, usize)) -> CellType { + match self { + // don't translate non-equations + CellType::Number(_) | CellType::String(_) => return self.clone(), + CellType::Equation(eq) => { + // extract all the variables + let ctx = ExtractionContext::new(); + let _ = eval_with_context(eq, &ctx); + + let mut rolling = eq.clone(); + // translate standard vars A0 -> A1 + for old_var in ctx.dump_vars() { + if let Some((src_x, src_y)) = Grid::parse_to_idx(&old_var) { + let (x1, y1) = from; + let x1 = x1 as i32; + let y1 = y1 as i32; + let (x2, y2) = to; + let x2 = x2 as i32; + let y2 = y2 as i32; + + let dest_x = (src_x as i32 + (x2 - x1)) as usize; + let dest_y = (src_y as i32 + (y2 - y1)) as usize; + + let alpha = Grid::num_to_char(dest_x); + let alpha = alpha.trim(); + let new_var = format!("{alpha}{dest_y}"); + + // swap out vars + rolling = rolling.replace(&old_var, &new_var); + } else { + // why you coping invalid stuff, nerd? + } + } + return rolling.into(); + } + } + } +} + +impl Display for CellType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let d = match self { + CellType::Number(n) => n.to_string(), + CellType::String(n) => n.to_owned(), + CellType::Equation(r) => r.to_owned(), + }; + write!(f, "{d}") + } +} diff --git a/src/app/logic/ctx.rs b/src/app/logic/ctx.rs index 301fdcd..fd92352 100644 --- a/src/app/logic/ctx.rs +++ b/src/app/logic/ctx.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::RwLock}; use evalexpr::{error::EvalexprResultValue, *}; -use crate::app::logic::calc::{CellType, Grid}; +use crate::app::logic::{calc::Grid, cell::CellType}; pub struct CallbackContext<'a> { variables: &'a Grid, @@ -155,9 +155,9 @@ impl<'a> Context for CallbackContext<'a> { if let Some(v) = self.variables.get_cell(identifier) { match v { - super::calc::CellType::Number(n) => return Some(Value::Float(n.to_owned())), - super::calc::CellType::String(s) => return Some(Value::String(s.to_owned())), - super::calc::CellType::Equation(eq) => { + CellType::Number(n) => return Some(Value::Float(n.to_owned())), + CellType::String(s) => return Some(Value::String(s.to_owned())), + CellType::Equation(eq) => { if let Ok(mut depth) = self.eval_depth.write() { *depth += 1; if *depth > RECURSION_DEPTH_LIMIT { diff --git a/src/app/logic/mod.rs b/src/app/logic/mod.rs index 4832215..41b7d57 100644 --- a/src/app/logic/mod.rs +++ b/src/app/logic/mod.rs @@ -1,2 +1,3 @@ pub mod calc; -pub mod ctx; \ No newline at end of file +pub mod ctx; +pub mod cell; \ No newline at end of file