mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-23 05:19: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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
a1 = "1.0.1"
|
anyhow = { version = "1.0.91", features = ["backtrace"] }
|
||||||
clap = { version = "4.5.20", features = ["derive"] }
|
clap = { version = "4.5.20", features = ["derive"] }
|
||||||
crossterm = "0.28.1"
|
crossterm = "0.28.1"
|
||||||
formula = "0.1.0"
|
csvx = "0.1.17"
|
||||||
ratatui = "0.29.0"
|
ratatui = "0.29.0"
|
||||||
thiserror = "1.0.65"
|
thiserror = "1.0.65"
|
||||||
|
15
src/main.rs
15
src/main.rs
@ -1,5 +1,6 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
@ -8,7 +9,7 @@ use ratatui::{
|
|||||||
widgets::{Table, Tabs},
|
widgets::{Table, Tabs},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use sheet::{Address, Computable, Tbl};
|
use sheet::{Address, CellValue, Tbl};
|
||||||
|
|
||||||
mod sheet;
|
mod sheet;
|
||||||
|
|
||||||
@ -32,9 +33,15 @@ fn run(terminal: &mut ratatui::DefaultTerminal) -> std::io::Result<()> {
|
|||||||
|
|
||||||
fn generate_default_table<'a>() -> Table<'a> {
|
fn generate_default_table<'a>() -> Table<'a> {
|
||||||
let mut tbl = Tbl::new();
|
let mut tbl = Tbl::new();
|
||||||
tbl.update_entry(Address::new(5, 5), Computable::Text("loc: 5, 5".to_owned()));
|
tbl.update_entry(Address::new(5, 5), CellValue::text("5,5"))
|
||||||
tbl.update_entry(Address::new(10, 10), Computable::Number(10.10));
|
.context("Failed updating entry at 5,5")
|
||||||
tbl.update_entry(Address::new(0, 0), Computable::Formula("".to_owned()));
|
.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()
|
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
|
//! associations. From this we can compute the dimensions of a Tbl as well as render
|
||||||
//! them into a [Table] Widget.
|
//! them into a [Table] Widget.
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use csvx;
|
||||||
use ratatui::widgets::{Cell, Row, Table};
|
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].
|
/// The Address in a [Tbl].
|
||||||
#[derive(Default, Debug, PartialEq, PartialOrd, Ord, Eq)]
|
#[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.
|
/// A single table of addressable computable values.
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct Tbl {
|
pub struct Tbl {
|
||||||
addresses: BTreeMap<Address, Computable>,
|
csv: csvx::Table,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tbl {
|
impl Tbl {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self {
|
||||||
|
csv: csvx::Table::new("").unwrap(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dimensions(&self) -> (usize, usize) {
|
pub fn dimensions(&self) -> (usize, usize) {
|
||||||
let (mut row, mut col) = (0, 0);
|
let table = self.csv.get_raw_table();
|
||||||
for (addr, _) in &self.addresses {
|
let row_count = table.len();
|
||||||
row = std::cmp::max(row, addr.row);
|
if row_count > 0 {
|
||||||
col = std::cmp::max(col, addr.col);
|
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> {
|
pub fn from_str<S: Borrow<str>>(input: S) -> Result<Self> {
|
||||||
self.addresses.get(&Address::new(row, col))
|
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
|
// TODO(zaphar): At some point we'll need to store the graph of computation
|
||||||
// dependencies
|
let (row, col) = self.dimensions();
|
||||||
self.addresses.insert(address, computable);
|
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> {
|
impl<'t> From<Tbl> for Table<'t> {
|
||||||
fn from(value: Tbl) -> Self {
|
fn from(value: Tbl) -> Self {
|
||||||
let (row, col) = value.dimensions();
|
let rows: Vec<Row> = value
|
||||||
let rows = (0..=row)
|
.csv
|
||||||
.map(|ri| {
|
.get_calculated_table()
|
||||||
(0..=col)
|
.iter()
|
||||||
.map(|ci| {
|
.map(|r| {
|
||||||
match value.get_computable(ri, ci) {
|
let cells = r.iter().map(|v| Cell::new(format!("{}", v)));
|
||||||
// TODO(zaphar): Style information
|
Row::new(cells)
|
||||||
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>()
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<Row>>();
|
.collect();
|
||||||
Table::default().rows(rows)
|
Table::default().rows(rows)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,19 +3,11 @@ use super::*;
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_dimensions_calculation() {
|
fn test_dimensions_calculation() {
|
||||||
let mut tbl = Tbl::new();
|
let mut tbl = Tbl::new();
|
||||||
tbl.update_entry(Address::new(0, 0), Computable::Text(String::new()));
|
tbl.update_entry(Address::new(0, 0), CellValue::Text(String::new())).unwrap();
|
||||||
assert_eq!((0, 0), tbl.dimensions());
|
assert_eq!((1, 1), tbl.dimensions());
|
||||||
tbl.update_entry(Address::new(0, 10), Computable::Text(String::new()));
|
tbl.update_entry(Address::new(0, 10), CellValue::Text(String::new())).unwrap();
|
||||||
assert_eq!((0, 10), tbl.dimensions());
|
assert_eq!((1, 11), tbl.dimensions());
|
||||||
tbl.update_entry(Address::new(20, 5), Computable::Text(String::new()));
|
tbl.update_entry(Address::new(20, 5), CellValue::Text(String::new())).unwrap();
|
||||||
assert_eq!((20, 10), tbl.dimensions());
|
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