mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-23 13:29:48 -04:00
wip: try out csvx instead
This commit is contained in:
parent
9fb467656b
commit
63d8c47c1f
1173
Cargo.lock
generated
1173
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
15
src/main.rs
15
src/main.rs
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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]
|
||||
}
|
||||
}
|
125
src/sheet/mod.rs
125
src/sheet/mod.rs
@ -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)),
|
||||
}
|
||||
})
|
||||
.collect::<Row>()
|
||||
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::<Vec<Row>>();
|
||||
.collect();
|
||||
Table::default().rows(rows)
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user