wip: experiment with a combination of crates:

* a1 crate
* formula crate
This commit is contained in:
Jeremy Wall 2024-10-26 16:39:13 -04:00
parent 507fa630d5
commit 9fb467656b
5 changed files with 1448 additions and 14 deletions

1241
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -9,4 +9,6 @@ edition = "2021"
a1 = "1.0.1"
clap = { version = "4.5.20", features = ["derive"] }
crossterm = "0.28.1"
formula = "0.1.0"
ratatui = "0.29.0"
thiserror = "1.0.65"

202
src/sheet/formula.rs Normal file
View File

@ -0,0 +1,202 @@
//! 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

@ -10,6 +10,8 @@ use ratatui::widgets::{Cell, Row, Table};
use std::collections::BTreeMap;
mod formula;
/// The Address in a [Tbl].
#[derive(Default, Debug, PartialEq, PartialOrd, Ord, Eq)]
pub struct Address {
@ -23,6 +25,12 @@ 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 {

View File

@ -10,3 +10,12 @@ fn test_dimensions_calculation() {
tbl.update_entry(Address::new(20, 5), Computable::Text(String::new()));
assert_eq!((20, 10), 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");
}
}