wip: try out csvx instead

This commit is contained in:
Jeremy Wall 2024-10-29 09:08:36 -04:00
parent 9fb467656b
commit 63d8c47c1f
6 changed files with 145 additions and 1394 deletions

1173
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,9 +6,9 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
a1 = "1.0.1"
anyhow = { version = "1.0.91", features = ["backtrace"] }
clap = { version = "4.5.20", features = ["derive"] }
crossterm = "0.28.1"
formula = "0.1.0"
csvx = "0.1.17"
ratatui = "0.29.0"
thiserror = "1.0.65"

View File

@ -1,5 +1,6 @@
use std::path::PathBuf;
use anyhow::Context;
use clap::Parser;
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use ratatui::{
@ -8,7 +9,7 @@ use ratatui::{
widgets::{Table, Tabs},
Frame,
};
use sheet::{Address, Computable, Tbl};
use sheet::{Address, CellValue, Tbl};
mod sheet;
@ -32,9 +33,15 @@ fn run(terminal: &mut ratatui::DefaultTerminal) -> std::io::Result<()> {
fn generate_default_table<'a>() -> Table<'a> {
let mut tbl = Tbl::new();
tbl.update_entry(Address::new(5, 5), Computable::Text("loc: 5, 5".to_owned()));
tbl.update_entry(Address::new(10, 10), Computable::Number(10.10));
tbl.update_entry(Address::new(0, 0), Computable::Formula("".to_owned()));
tbl.update_entry(Address::new(5, 5), CellValue::text("5,5"))
.context("Failed updating entry at 5,5")
.unwrap();
tbl.update_entry(Address::new(10, 10), CellValue::float(10.10))
.context("Failed updating entry at 10,10")
.unwrap();
tbl.update_entry(Address::new(0, 0), CellValue::other("0.0"))
.context("Failed updating entry at 0,0")
.unwrap();
tbl.into()
}

View File

@ -1,202 +0,0 @@
//! Parser for a spreadsheet formula.
use a1::A1;
use std::iter::Iterator;
use std::ops::{RangeBounds, Index};
use std::slice::SliceIndex;
/// A segment of a Formula. A formula is segmented into either
/// [Placeholder](FormulaSegment::Placeholder) address lookups or partial
/// formula text.
#[derive(Debug)]
pub enum FormulaSegment<'source> {
Placeholder(A1),
Unparsed(&'source str),
}
/// A Parsed Formula AST
#[derive(Debug)]
pub struct PreParsed<'source> {
segments: Vec<FormulaSegment<'source>>,
}
#[derive(thiserror::Error, Debug)]
pub enum FormulaError {
#[error("Failed to Parse formula")]
ParseFailure,
}
impl<'source> PreParsed<'source> {
pub fn is_formula(candidate: &str) -> bool {
candidate.len() > 0 && candidate.bytes().nth(0).unwrap() == b'='
}
pub fn try_parse(candidate: &'source str) -> Result<Option<Self>, FormulaError> {
if !Self::is_formula(candidate) {
return Ok(None);
}
// TODO(zaphar) Gather up the references for this Formula
Ok(Some(PreParsed {
segments: parse_segments(candidate.into()),
}))
}
}
fn parse_segments<'source>(mut iter: StrIter<'source>) -> Vec<FormulaSegment<'source>> {
let mut segments = Vec::new();
let mut buffer = Vec::new();
loop {
if let Some((addr, i)) = try_parse_addr(iter.clone()) {
segments.push(FormulaSegment::Placeholder(addr));
buffer.clear();
iter = i;
} else {
if let Some(b) = iter.peek_next() {
buffer.push(b);
}
}
if let None = iter.next() {
break;
}
}
return segments;
}
pub fn try_parse_addr<'source>(iter: StrIter<'source>) -> Option<(a1::A1, StrIter<'source>)> {
let start = iter.clone();
if let Ok(addr) = a1::new(start.rest()) {
return Some((addr, start));
}
// Consume 1 capital
//if let Some(i) = consume_capital(iter.clone()) {
// iter = i;
//} else {
// // This isn't a capitable letter
// return None
//}
//// maybe Consume 2 capital letters
//if let Some(i) = consume_capital(iter.clone()) {
// iter = i;
//}
//if let Some(b':') = iter.peek_next() {
// iter.next();
//}
//// Consume 1 capital
//if let Some(i) = consume_capital(iter.clone()) {
// iter = i;
//} else {
// // This isn't a capitable letter
// return None
//}
//// maybe Consume 2 capital letters
//if let Some(i) = consume_capital(iter.clone()) {
// iter = i;
//}
return None;
}
fn consume_capital<'source>(mut iter: StrIter<'source>) -> Option<StrIter<'source>> {
if let Some(c) = iter.peek_next() {
match *c {
b'A' | b'B' | b'C' | b'D' | b'E' | b'F' | b'G' | b'H' | b'I' | b'J' | b'K'
| b'L' | b'M' | b'N' | b'O' | b'P' | b'Q' | b'R' | b'S' | b'T' | b'U'
| b'V' | b'W' | b'X' | b'Y' | b'Z' => {
iter.next();
return Some(iter);
}
_ => {
return None;
}
}
}
return None;
}
fn consume_ws<'source>(mut iter: StrIter<'source>) -> Option<StrIter<'source>> {
if let Some(c) = iter.peek_next() {
match *c {
b' ' | b'\t' | b'\r' | b'\n' => {
iter.next();
return Some(iter);
}
_ => {
return None;
}
}
}
return None;
}
/// Implements `InputIter` for any slice of T.
#[derive(Debug)]
pub struct StrIter<'a> {
source: &'a str,
pub offset: usize,
}
impl<'a> StrIter<'a> {
/// new constructs a StrIter from a Slice of T.
pub fn new(source: &'a str) -> Self {
StrIter { source, offset: 0 }
}
fn seek(&mut self, to: usize) -> usize {
let self_len = self.source.len();
let offset = if self_len > to { to } else { self_len };
self.offset = offset;
self.offset
}
fn peek_next(&self) -> Option<&'a u8> {
self.source.as_bytes().get(self.offset)
}
fn get_range<R: RangeBounds<usize> + SliceIndex<str, Output=str>>(&self, range: R) -> &'a str {
&self.source[range]
}
pub fn rest(&'a self) -> &'a str {
&self[self.offset..]
}
}
impl<'a> Iterator for StrIter<'a> {
type Item = &'a u8;
fn next(&mut self) -> Option<Self::Item> {
match self.source.as_bytes().get(self.offset) {
// TODO count lines and columns.
Some(item) => {
self.offset += 1;
Some(item)
}
None => None,
}
}
}
impl<'a> Clone for StrIter<'a> {
fn clone(&self) -> Self {
StrIter {
source: self.source,
offset: self.offset,
}
}
}
impl<'a> From<&'a str> for StrIter<'a> {
fn from(source: &'a str) -> Self {
Self::new(source)
}
}
impl<'a, Idx> Index<Idx> for StrIter<'a>
where Idx: RangeBounds<usize> + SliceIndex<str, Output=str>
{
type Output = Idx::Output;
fn index(&self, index: Idx) -> &'a Self::Output {
&self.source[index]
}
}

View File

@ -6,11 +6,45 @@
//! associations. From this we can compute the dimensions of a Tbl as well as render
//! them into a [Table] Widget.
use anyhow::{anyhow, Result};
use csvx;
use ratatui::widgets::{Cell, Row, Table};
use std::collections::BTreeMap;
use std::borrow::Borrow;
mod formula;
pub enum CellValue {
Text(String),
Float(f64),
Integer(i64),
Other(String),
}
impl CellValue {
pub fn to_csv_value(&self) -> String {
match self {
CellValue::Text(v) => format!("\"{}\"", v),
CellValue::Float(v) => format!("{}", v),
CellValue::Integer(v) => format!("{}", v),
CellValue::Other(v) => format!("{}", v),
}
}
pub fn text<S: Into<String>>(value: S) -> CellValue {
CellValue::Text(Into::<String>::into(value))
}
pub fn other<S: Into<String>>(value: S) -> CellValue {
CellValue::Other(Into::<String>::into(value))
}
pub fn float(value: f64) -> CellValue {
CellValue::Float(value)
}
pub fn int(value: i64) -> CellValue {
CellValue::Integer(value)
}
}
/// The Address in a [Tbl].
#[derive(Default, Debug, PartialEq, PartialOrd, Ord, Eq)]
@ -25,75 +59,66 @@ impl Address {
}
}
impl From<(usize, usize)> for Address {
fn from((row, col): (usize, usize)) -> Self {
Address::new(row, col)
}
}
/// The computable value located at an [Address].
#[derive(Debug)]
pub enum Computable {
Text(String),
Number(f64),
Formula(String),
}
impl Default for Computable {
fn default() -> Self {
Self::Text("".to_owned())
}
}
/// A single table of addressable computable values.
#[derive(Default, Debug)]
pub struct Tbl {
addresses: BTreeMap<Address, Computable>,
csv: csvx::Table,
}
impl Tbl {
pub fn new() -> Self {
Self::default()
Self {
csv: csvx::Table::new("").unwrap(),
}
}
pub fn dimensions(&self) -> (usize, usize) {
let (mut row, mut col) = (0, 0);
for (addr, _) in &self.addresses {
row = std::cmp::max(row, addr.row);
col = std::cmp::max(col, addr.col);
let table = self.csv.get_raw_table();
let row_count = table.len();
if row_count > 0 {
let col_count = table.first().unwrap().len();
return (row_count, col_count);
}
(row, col)
return (0, 0);
}
pub fn get_computable(&self, row: usize, col: usize) -> Option<&Computable> {
self.addresses.get(&Address::new(row, col))
pub fn from_str<S: Borrow<str>>(input: S) -> Result<Self> {
Ok(Self {
csv: csvx::Table::new(input)
.map_err(|e| anyhow!("Error parsing table from csv text: {}", e))?,
})
}
pub fn update_entry(&mut self, address: Address, computable: Computable) {
pub fn update_entry(&mut self, address: Address, value: CellValue) -> Result<()> {
// TODO(zaphar): At some point we'll need to store the graph of computation
// dependencies
self.addresses.insert(address, computable);
let (row, col) = self.dimensions();
if address.row >= row {
// then we need to add rows.
for r in row..=address.row {
self.csv.insert_y(r);
}
}
if address.col >= col {
for c in col..=address.col {
self.csv.insert_x(c);
}
}
Ok(self
.csv
.update(address.col, address.row, value.to_csv_value())?)
}
}
impl<'t> From<Tbl> for Table<'t> {
fn from(value: Tbl) -> Self {
let (row, col) = value.dimensions();
let rows = (0..=row)
.map(|ri| {
(0..=col)
.map(|ci| {
match value.get_computable(ri, ci) {
// TODO(zaphar): Style information
Some(Computable::Text(s)) => Cell::new(format!(" {}", s)),
Some(Computable::Number(f)) => Cell::new(format!(" {}", f)),
Some(Computable::Formula(_expr)) => Cell::new(format!(" .formula. ")),
None => Cell::new(format!(" {}:{} ", ri, ci)),
}
let rows: Vec<Row> = value
.csv
.get_calculated_table()
.iter()
.map(|r| {
let cells = r.iter().map(|v| Cell::new(format!("{}", v)));
Row::new(cells)
})
.collect::<Row>()
})
.collect::<Vec<Row>>();
.collect();
Table::default().rows(rows)
}
}

View File

@ -3,19 +3,11 @@ use super::*;
#[test]
fn test_dimensions_calculation() {
let mut tbl = Tbl::new();
tbl.update_entry(Address::new(0, 0), Computable::Text(String::new()));
assert_eq!((0, 0), tbl.dimensions());
tbl.update_entry(Address::new(0, 10), Computable::Text(String::new()));
assert_eq!((0, 10), tbl.dimensions());
tbl.update_entry(Address::new(20, 5), Computable::Text(String::new()));
assert_eq!((20, 10), tbl.dimensions());
tbl.update_entry(Address::new(0, 0), CellValue::Text(String::new())).unwrap();
assert_eq!((1, 1), tbl.dimensions());
tbl.update_entry(Address::new(0, 10), CellValue::Text(String::new())).unwrap();
assert_eq!((1, 11), tbl.dimensions());
tbl.update_entry(Address::new(20, 5), CellValue::Text(String::new())).unwrap();
assert_eq!((21, 11), tbl.dimensions());
}
#[test]
fn test_address_parse() {
if let Some((a1, iter)) = formula::try_parse_addr("A1:A2 foo bar".into()) {
assert_eq!("A1:A2", a1.to_string());
assert_eq!(&iter[0..], " foo bar");
}
}