chore: reformat

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Edward Langley
2026-03-31 00:07:22 -07:00
parent 37584670eb
commit 183b2350f7
17 changed files with 1112 additions and 471 deletions

View File

@ -1,5 +1,5 @@
pub mod parser;
pub mod ast;
pub mod parser;
pub use ast::{AggFunc, BinOp, Expr, Formula};
pub use parser::parse_formula;

View File

@ -8,7 +8,9 @@ pub fn parse_formula(raw: &str, target_category: &str) -> Result<Formula> {
let raw = raw.trim();
// Split on first `=` to get target = expression
let eq_pos = raw.find('=').ok_or_else(|| anyhow!("Formula must contain '=': {raw}"))?;
let eq_pos = raw
.find('=')
.ok_or_else(|| anyhow!("Formula must contain '=': {raw}"))?;
let target = raw[..eq_pos].trim().to_string();
let rest = raw[eq_pos + 1..].trim();
@ -54,7 +56,9 @@ fn split_where(s: &str) -> (&str, Option<&str>) {
fn parse_where(s: &str) -> Result<Filter> {
// Format: Category = "Item" or Category = Item
let eq_pos = s.find('=').ok_or_else(|| anyhow!("WHERE clause must contain '=': {s}"))?;
let eq_pos = s
.find('=')
.ok_or_else(|| anyhow!("WHERE clause must contain '=': {s}"))?;
let category = s[..eq_pos].trim().to_string();
let item_raw = s[eq_pos + 1..].trim();
let item = item_raw.trim_matches('"').to_string();
@ -67,7 +71,10 @@ pub fn parse_expr(s: &str) -> Result<Expr> {
let mut pos = 0;
let expr = parse_add_sub(&tokens, &mut pos)?;
if pos < tokens.len() {
return Err(anyhow!("Unexpected token at position {pos}: {:?}", tokens[pos]));
return Err(anyhow!(
"Unexpected token at position {pos}: {:?}",
tokens[pos]
));
}
Ok(expr)
}
@ -101,20 +108,62 @@ fn tokenize(s: &str) -> Result<Vec<Token>> {
while i < chars.len() {
match chars[i] {
' ' | '\t' | '\n' => i += 1,
'+' => { tokens.push(Token::Plus); i += 1; }
'-' => { tokens.push(Token::Minus); i += 1; }
'*' => { tokens.push(Token::Star); i += 1; }
'/' => { tokens.push(Token::Slash); i += 1; }
'^' => { tokens.push(Token::Caret); i += 1; }
'(' => { tokens.push(Token::LParen); i += 1; }
')' => { tokens.push(Token::RParen); i += 1; }
',' => { tokens.push(Token::Comma); i += 1; }
'!' if chars.get(i+1) == Some(&'=') => { tokens.push(Token::Ne); i += 2; }
'<' if chars.get(i+1) == Some(&'=') => { tokens.push(Token::Le); i += 2; }
'>' if chars.get(i+1) == Some(&'=') => { tokens.push(Token::Ge); i += 2; }
'<' => { tokens.push(Token::Lt); i += 1; }
'>' => { tokens.push(Token::Gt); i += 1; }
'=' => { tokens.push(Token::Eq); i += 1; }
'+' => {
tokens.push(Token::Plus);
i += 1;
}
'-' => {
tokens.push(Token::Minus);
i += 1;
}
'*' => {
tokens.push(Token::Star);
i += 1;
}
'/' => {
tokens.push(Token::Slash);
i += 1;
}
'^' => {
tokens.push(Token::Caret);
i += 1;
}
'(' => {
tokens.push(Token::LParen);
i += 1;
}
')' => {
tokens.push(Token::RParen);
i += 1;
}
',' => {
tokens.push(Token::Comma);
i += 1;
}
'!' if chars.get(i + 1) == Some(&'=') => {
tokens.push(Token::Ne);
i += 2;
}
'<' if chars.get(i + 1) == Some(&'=') => {
tokens.push(Token::Le);
i += 2;
}
'>' if chars.get(i + 1) == Some(&'=') => {
tokens.push(Token::Ge);
i += 2;
}
'<' => {
tokens.push(Token::Lt);
i += 1;
}
'>' => {
tokens.push(Token::Gt);
i += 1;
}
'=' => {
tokens.push(Token::Eq);
i += 1;
}
'"' => {
i += 1;
let mut s = String::new();
@ -122,7 +171,9 @@ fn tokenize(s: &str) -> Result<Vec<Token>> {
s.push(chars[i]);
i += 1;
}
if i < chars.len() { i += 1; }
if i < chars.len() {
i += 1;
}
tokens.push(Token::Str(s));
}
c if c.is_ascii_digit() || c == '.' => {
@ -135,13 +186,25 @@ fn tokenize(s: &str) -> Result<Vec<Token>> {
}
c if c.is_alphabetic() || c == '_' => {
let mut ident = String::new();
while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == ' ') {
while i < chars.len()
&& (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == ' ')
{
// Don't consume trailing spaces if next non-space is operator
if chars[i] == ' ' {
// Peek ahead
let j = i + 1;
let next_nonspace = chars[j..].iter().find(|&&c| c != ' ');
if matches!(next_nonspace, Some('+') | Some('-') | Some('*') | Some('/') | Some('^') | Some(')') | Some(',') | None) {
if matches!(
next_nonspace,
Some('+')
| Some('-')
| Some('*')
| Some('/')
| Some('^')
| Some(')')
| Some(',')
| None
) {
break;
}
}
@ -161,7 +224,7 @@ fn parse_add_sub(tokens: &[Token], pos: &mut usize) -> Result<Expr> {
let mut left = parse_mul_div(tokens, pos)?;
while *pos < tokens.len() {
let op = match &tokens[*pos] {
Token::Plus => BinOp::Add,
Token::Plus => BinOp::Add,
Token::Minus => BinOp::Sub,
_ => break,
};
@ -176,7 +239,7 @@ fn parse_mul_div(tokens: &[Token], pos: &mut usize) -> Result<Expr> {
let mut left = parse_pow(tokens, pos)?;
while *pos < tokens.len() {
let op = match &tokens[*pos] {
Token::Star => BinOp::Mul,
Token::Star => BinOp::Mul,
Token::Slash => BinOp::Div,
_ => break,
};
@ -239,19 +302,42 @@ fn parse_primary(tokens: &[Token], pos: &mut usize) -> Result<Expr> {
if kw.to_ascii_uppercase() == "WHERE" {
*pos += 1;
let cat = match &tokens[*pos] {
Token::Ident(s) => { let s = s.clone(); *pos += 1; s }
t => return Err(anyhow!("Expected category name, got {t:?}")),
Token::Ident(s) => {
let s = s.clone();
*pos += 1;
s
}
t => {
return Err(anyhow!(
"Expected category name, got {t:?}"
))
}
};
// expect =
if *pos < tokens.len() && tokens[*pos] == Token::Eq { *pos += 1; }
if *pos < tokens.len() && tokens[*pos] == Token::Eq {
*pos += 1;
}
let item = match &tokens[*pos] {
Token::Str(s) | Token::Ident(s) => { let s = s.clone(); *pos += 1; s }
Token::Str(s) | Token::Ident(s) => {
let s = s.clone();
*pos += 1;
s
}
t => return Err(anyhow!("Expected item name, got {t:?}")),
};
Some(Filter { category: cat, item })
} else { None }
} else { None }
} else { None };
Some(Filter {
category: cat,
item,
})
} else {
None
}
} else {
None
}
} else {
None
};
// expect )
if *pos < tokens.len() && tokens[*pos] == Token::RParen {
*pos += 1;
@ -266,9 +352,13 @@ fn parse_primary(tokens: &[Token], pos: &mut usize) -> Result<Expr> {
if *pos < tokens.len() && tokens[*pos] == Token::LParen {
*pos += 1;
let cond = parse_comparison(tokens, pos)?;
if *pos < tokens.len() && tokens[*pos] == Token::Comma { *pos += 1; }
if *pos < tokens.len() && tokens[*pos] == Token::Comma {
*pos += 1;
}
let then = parse_add_sub(tokens, pos)?;
if *pos < tokens.len() && tokens[*pos] == Token::Comma { *pos += 1; }
if *pos < tokens.len() && tokens[*pos] == Token::Comma {
*pos += 1;
}
let else_ = parse_add_sub(tokens, pos)?;
if *pos < tokens.len() && tokens[*pos] == Token::RParen {
*pos += 1;
@ -296,7 +386,9 @@ fn parse_primary(tokens: &[Token], pos: &mut usize) -> Result<Expr> {
fn parse_comparison(tokens: &[Token], pos: &mut usize) -> Result<Expr> {
let left = parse_add_sub(tokens, pos)?;
if *pos >= tokens.len() { return Ok(left); }
if *pos >= tokens.len() {
return Ok(left);
}
let op = match &tokens[*pos] {
Token::Eq => BinOp::Eq,
Token::Ne => BinOp::Ne,