From d8137b53ebf87913e9b75015c4206438db3384c5 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 9 Nov 2024 14:43:51 +0000 Subject: [PATCH 01/12] Added Filter DSL Lexer --- src/component/activity_view.rs | 8 +- src/component/polyline.rs | 6 +- src/filter/lexer.rs | 218 +++++++++++++++++++++++++++++++++ src/filter/mod.rs | 2 + src/main.rs | 1 + src/util/geotool.rs | 1 - 6 files changed, 229 insertions(+), 7 deletions(-) create mode 100644 src/filter/lexer.rs create mode 100644 src/filter/mod.rs delete mode 100644 src/util/geotool.rs diff --git a/src/component/activity_view.rs b/src/component/activity_view.rs index 05b88af..6968734 100644 --- a/src/component/activity_view.rs +++ b/src/component/activity_view.rs @@ -1,7 +1,7 @@ use tui::{ layout::{Constraint, Direction, Layout, Margin}, prelude::Buffer, - widgets::{Block, Borders, Widget, Paragraph}, + widgets::{Block, Borders, Widget}, }; use crate::{ @@ -78,15 +78,13 @@ impl View for ActivityView { fn draw(&mut self, app: &mut App, f: &mut Buffer, area: tui::layout::Rect) { let rows = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Min(1), Constraint::Length(2)].as_ref()) + .constraints([Constraint::Length(4), Constraint::Length(2)].as_ref()) .split(area); if let Some(activity) = &app.activity { { let a = Activities::from(activity.clone()); activity_list_table(app, &a).render(rows[0], f); - let desc = Paragraph::new(activity.description.as_str()); - desc.render(rows[1], f); } } @@ -100,7 +98,7 @@ impl View for ActivityView { ] .as_ref(), ) - .split(rows[2]); + .split(rows[1]); let col1 = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) diff --git a/src/component/polyline.rs b/src/component/polyline.rs index e46fada..f657bfd 100644 --- a/src/component/polyline.rs +++ b/src/component/polyline.rs @@ -29,7 +29,11 @@ pub fn draw( } if let Ok(decoded) = activity.polyline() { - let mapped_polyline = ActivityMap::from_polyline(decoded, area.width - 4, area.height - 4); + let mapped_polyline = ActivityMap::from_polyline( + decoded, + area.width.saturating_add(4), + area.height.saturating_sub(4) + ); let length_per_split = mapped_polyline.length() / ((activity.distance / 1000.0) * KILOMETER_TO_MILE); diff --git a/src/filter/lexer.rs b/src/filter/lexer.rs new file mode 100644 index 0000000..a95302f --- /dev/null +++ b/src/filter/lexer.rs @@ -0,0 +1,218 @@ +// distance > 10 AND distance < 20 +// type = Run +// pace > 06:00 +// average_speed > 10mph + +#[derive(PartialEq, Eq, Debug)] +pub enum TokenKind { + Number, + Contains, + Unkown, + Colon, + GreaterThan, + GreaterThanEqual, + LessThanEqual, + LessThan, + Or, + And, + Equal, + Name, + Eol, +} + +fn is_number(c: char) -> bool { + match c { + '0'..='9' => true, + _ => false, + } +} +fn is_name(c: char) -> bool { + match c { + 'a'..='z' => true, + 'A'..='Z' => true, + _ => false, + } +} + +#[derive(Debug)] +pub struct Token { + pub kind: TokenKind, + pub start: usize, + pub length: usize, +} + +struct Lexer<'a> { + pub pos: usize, + pub expr: &'a str, +} + +impl Lexer<'_> { + pub fn new<'a>(expr: &'a str) -> Lexer<'_> { + Lexer { expr, pos: 0 } + } + pub fn next(&mut self) -> Token { + self.skip_whitespace(); + let c = self.current(); + match c { + '\0' => self.spawn_token(TokenKind::Eol, self.pos), + _ => { + if is_number(c) { + return self.parse_number(); + } + + if is_name(c) { + return self.parse_name(); + } + + match c { + ':' => self.spawn_advance(TokenKind::Colon, 1), + '>' => match self.peek(1) { + '=' => self.spawn_advance(TokenKind::GreaterThanEqual, 2), + _ => self.spawn_advance(TokenKind::GreaterThan, 1), + }, + '<' => match self.peek(1) { + '=' => self.spawn_advance(TokenKind::LessThanEqual, 2), + _ => self.spawn_advance(TokenKind::LessThan, 1), + }, + _ => self.spawn_advance(TokenKind::Unkown, 0), + } + } + } + } + + fn advance(&mut self) { + self.pos += 1; + } + + fn current(&self) -> char { + match self.expr.chars().nth(self.pos) { + Some(s) => s, + None => '\0', + } + } + + fn peek(&self, amount: usize) -> char { + match self.expr.chars().nth(self.pos + amount) { + Some(s) => s, + None => '\0', + } + } + + fn parse_number(&mut self) -> Token { + let start = self.pos; + while is_number(self.current()) { + self.advance() + } + + self.spawn_token(TokenKind::Number, start) + } + + fn parse_name(&mut self) -> Token { + let mut length = 0; + while is_name(self.peek(length)) { + length += 1; + } + + match &self.expr[self.pos..self.pos + length] { + "or" => self.spawn_advance(TokenKind::Or, length), + "and" => self.spawn_advance(TokenKind::And, length), + "OR" => self.spawn_advance(TokenKind::Or, length), + "AND" => self.spawn_advance(TokenKind::And, length), + _ => self.spawn_advance(TokenKind::Name, length), + } + } + + fn spawn_token(&self, number: TokenKind, start: usize) -> Token { + Token { + kind: number, + start, + length: self.pos - start, + } + } + + fn skip_whitespace(&mut self) { + while ' ' == self.current() { + self.advance(); + } + } + + pub fn token_value(&self, token: Token) -> &str { + &self.expr[token.start..token.start + token.length] + } + + fn spawn_advance(&mut self, kind: TokenKind, length: usize) -> Token { + let t = Token { + kind, + start: self.pos, + length, + }; + self.pos += length; + return t; + } + +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + pub fn lex_int() { + assert_eq!(TokenKind::Number, Lexer::new("10").next().kind); + assert_eq!(2, Lexer::new("10").next().length); + assert_eq!(0, Lexer::new("10").next().start); + } + + #[test] + pub fn lex_skip_whitespace() { + let mut l = Lexer::new(" 10"); + let t = l.next(); + assert_eq!(TokenKind::Number, t.kind); + assert_eq!("10", l.token_value(t)) + } + + #[test] + pub fn lex_eof() { + let mut l = Lexer::new(" 10"); + assert_eq!(TokenKind::Number, l.next().kind); + assert_eq!(TokenKind::Eol, l.next().kind); + } + #[test] + pub fn lex_symbols() { + let mut l = Lexer::new(" :"); + assert_eq!(TokenKind::Colon, l.next().kind); + } + + #[test] + pub fn lex_comparators() { + assert_eq!(TokenKind::GreaterThanEqual, Lexer::new(">=").next().kind); + assert_eq!(TokenKind::GreaterThan, Lexer::new(">").next().kind); + assert_eq!(TokenKind::LessThanEqual, Lexer::new("<=").next().kind); + assert_eq!(TokenKind::LessThan, Lexer::new("<").next().kind); + } + + #[test] + pub fn lex_logical_operators() { + assert_eq!(TokenKind::Or, Lexer::new("or").next().kind); + assert_eq!(TokenKind::And, Lexer::new("and").next().kind); + assert_eq!(TokenKind::Or, Lexer::new("OR").next().kind); + assert_eq!(TokenKind::And, Lexer::new("AND").next().kind); + } + + #[test] + pub fn lex_expression() { + let mut l = Lexer::new("distance > 10m"); + let t = l.next(); + assert_eq!(TokenKind::Name, t.kind); + assert_eq!("distance", l.token_value(t)); + let t = l.next(); + assert_eq!(TokenKind::GreaterThan, t.kind); + assert_eq!(">", l.token_value(t)); + let t = l.next(); + assert_eq!(TokenKind::Number, t.kind); + assert_eq!("10", l.token_value(t)); + let t = l.next(); + assert_eq!(TokenKind::Name, t.kind); + assert_eq!("m", l.token_value(t)); + } +} diff --git a/src/filter/mod.rs b/src/filter/mod.rs new file mode 100644 index 0000000..4963a65 --- /dev/null +++ b/src/filter/mod.rs @@ -0,0 +1,2 @@ +#[allow(dead_code)] +mod lexer; diff --git a/src/main.rs b/src/main.rs index 1db1292..f161aff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ pub mod app; pub mod authenticator; +pub mod filter; pub mod client; pub mod component; pub mod config; diff --git a/src/util/geotool.rs b/src/util/geotool.rs deleted file mode 100644 index 00608f2..0000000 --- a/src/util/geotool.rs +++ /dev/null @@ -1 +0,0 @@ -pub fn dis From 7166444bb8ace5237dd096b5f61666a3cb3bc3ed Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 9 Nov 2024 15:30:11 +0000 Subject: [PATCH 02/12] Failing parser --- src/filter/lexer.rs | 2 +- src/filter/mod.rs | 3 +++ src/filter/parser.rs | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/filter/parser.rs diff --git a/src/filter/lexer.rs b/src/filter/lexer.rs index a95302f..8f7f8cf 100644 --- a/src/filter/lexer.rs +++ b/src/filter/lexer.rs @@ -41,7 +41,7 @@ pub struct Token { pub length: usize, } -struct Lexer<'a> { +pub struct Lexer<'a> { pub pos: usize, pub expr: &'a str, } diff --git a/src/filter/mod.rs b/src/filter/mod.rs index 4963a65..5ae68b7 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -1,2 +1,5 @@ #[allow(dead_code)] mod lexer; + +#[allow(dead_code)] +mod parser; diff --git a/src/filter/parser.rs b/src/filter/parser.rs new file mode 100644 index 0000000..dd14901 --- /dev/null +++ b/src/filter/parser.rs @@ -0,0 +1,38 @@ +use super::lexer::{Lexer, TokenKind}; + +trait Expr {} + +struct BinaryOp { + left: Box, + operand: TokenKind, + right: Box, +} + +struct Parser<'a> { + lexer: Lexer<'a>, +} + +impl Parser<'_> { + pub fn new<'a>(expr: &'a str) -> Parser<'a> { + let lexer = Lexer::new(expr); + Parser { lexer } + } + + pub fn parse(&mut self) -> Box { + let token = self.lexer.next(); + + match token.kind { + _ => panic!("unexpected token: {:?}", token), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parse_expression() { + Parser::new("distance > 10m").parse(); + } +} From d8840391cacdf50e97ed8f37e2ff86669180472b Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 9 Nov 2024 15:59:03 +0000 Subject: [PATCH 03/12] Parser variable and number --- src/filter/parser.rs | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/filter/parser.rs b/src/filter/parser.rs index dd14901..11748ca 100644 --- a/src/filter/parser.rs +++ b/src/filter/parser.rs @@ -1,11 +1,10 @@ use super::lexer::{Lexer, TokenKind}; -trait Expr {} - -struct BinaryOp { - left: Box, - operand: TokenKind, - right: Box, +#[derive(PartialEq, Eq, Debug)] +pub enum Expr { + Binary(Box, TokenKind, Box), + Number(u16), + Variable(String), } struct Parser<'a> { @@ -18,12 +17,21 @@ impl Parser<'_> { Parser { lexer } } - pub fn parse(&mut self) -> Box { + pub fn parse(&mut self) -> Result { let token = self.lexer.next(); - - match token.kind { - _ => panic!("unexpected token: {:?}", token), - } + let left: Result = match token.kind { + TokenKind::Number => match self.lexer.token_value(token).parse::() { + Ok(v) => Ok(Expr::Number(v)), + Err(_) => Err("Could not number"), + }, + TokenKind::Name => { + let value = self.lexer.token_value(token); + Ok(Expr::Variable(value.to_string())) + } + _ => Err("foo"), + }; + + left } } @@ -33,6 +41,7 @@ mod test { #[test] fn parse_expression() { - Parser::new("distance > 10m").parse(); + assert_eq!(Expr::Variable("distance".to_string()), Parser::new("distance").parse().unwrap()); + assert_eq!(Expr::Number(10), Parser::new("10").parse().unwrap()); } } From 49349fac95b93bd4e48fbedb3d7397907c52f909 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 9 Nov 2024 18:24:47 +0000 Subject: [PATCH 04/12] Parse distance --- src/filter/lexer.rs | 19 ++++--- src/filter/parser.rs | 127 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 127 insertions(+), 19 deletions(-) diff --git a/src/filter/lexer.rs b/src/filter/lexer.rs index 8f7f8cf..3147e74 100644 --- a/src/filter/lexer.rs +++ b/src/filter/lexer.rs @@ -3,7 +3,7 @@ // pace > 06:00 // average_speed > 10mph -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Clone)] pub enum TokenKind { Number, Contains, @@ -53,7 +53,7 @@ impl Lexer<'_> { pub fn next(&mut self) -> Token { self.skip_whitespace(); let c = self.current(); - match c { + let t = match c { '\0' => self.spawn_token(TokenKind::Eol, self.pos), _ => { if is_number(c) { @@ -77,7 +77,8 @@ impl Lexer<'_> { _ => self.spawn_advance(TokenKind::Unkown, 0), } } - } + }; + t } fn advance(&mut self) { @@ -136,7 +137,7 @@ impl Lexer<'_> { } } - pub fn token_value(&self, token: Token) -> &str { + pub fn token_value(&self, token: &Token) -> &str { &self.expr[token.start..token.start + token.length] } @@ -168,7 +169,7 @@ mod test { let mut l = Lexer::new(" 10"); let t = l.next(); assert_eq!(TokenKind::Number, t.kind); - assert_eq!("10", l.token_value(t)) + assert_eq!("10", l.token_value(&t)) } #[test] @@ -204,15 +205,15 @@ mod test { let mut l = Lexer::new("distance > 10m"); let t = l.next(); assert_eq!(TokenKind::Name, t.kind); - assert_eq!("distance", l.token_value(t)); + assert_eq!("distance", l.token_value(&t)); let t = l.next(); assert_eq!(TokenKind::GreaterThan, t.kind); - assert_eq!(">", l.token_value(t)); + assert_eq!(">", l.token_value(&t)); let t = l.next(); assert_eq!(TokenKind::Number, t.kind); - assert_eq!("10", l.token_value(t)); + assert_eq!("10", l.token_value(&t)); let t = l.next(); assert_eq!(TokenKind::Name, t.kind); - assert_eq!("m", l.token_value(t)); + assert_eq!("m", l.token_value(&t)); } } diff --git a/src/filter/parser.rs b/src/filter/parser.rs index 11748ca..cd81d04 100644 --- a/src/filter/parser.rs +++ b/src/filter/parser.rs @@ -1,10 +1,17 @@ -use super::lexer::{Lexer, TokenKind}; +use super::lexer::{Lexer, Token, TokenKind}; -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Clone)] pub enum Expr { Binary(Box, TokenKind, Box), Number(u16), Variable(String), + Quantity(Box, QuantityUnit) +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum QuantityUnit { + Miles, + Kilometers, } struct Parser<'a> { @@ -17,21 +24,90 @@ impl Parser<'_> { Parser { lexer } } - pub fn parse(&mut self) -> Result { + pub fn parse(&mut self) -> Result { + match self.parse_expr(0) { + Ok((expr, _)) => Ok(expr), + Err(e) => Err(e), + } + } + + fn parse_expr(&mut self, precedence: usize) -> Result<(Expr, Token), String> { let token = self.lexer.next(); - let left: Result = match token.kind { - TokenKind::Number => match self.lexer.token_value(token).parse::() { + let mut left: Expr = match token.kind { + TokenKind::Number => match self.lexer.token_value(&token).parse::() { Ok(v) => Ok(Expr::Number(v)), - Err(_) => Err("Could not number"), + Err(_) => Err("Could not parse number".to_string()), }, TokenKind::Name => { - let value = self.lexer.token_value(token); + let value = self.lexer.token_value(&token); Ok(Expr::Variable(value.to_string())) } - _ => Err("foo"), + _ => Err(format!("unknown left token: {:?} at {}", token.kind, token.start)), + }?; + + let mut next_t = self.lexer.next(); + if next_t.kind == TokenKind::Eol { + return Ok((left, next_t)); + } + + // suffix parsing + let suffix = match &next_t.kind { + TokenKind::Name => match self.lexer.token_value(&next_t) { + "m" => Some(Expr::Quantity(Box::new(left.clone()), QuantityUnit::Miles)), + "km" => Some(Expr::Quantity(Box::new(left.clone()), QuantityUnit::Kilometers)), + _ => None, + }, + _ => None, + }; + (next_t, left) = match suffix { + Some(suffix) => { + (self.lexer.next(), suffix) + }, + None => (next_t, left), }; - left + // infix parsing + while precedence < self.token_precedence(&next_t) { + let (right, new_t) = self.parse_expr(self.token_precedence(&next_t)).unwrap(); + left = match &next_t.kind { + TokenKind::GreaterThan + | TokenKind::GreaterThanEqual + | TokenKind::And + | TokenKind::Or + | TokenKind::Equal + | TokenKind::LessThanEqual + | TokenKind::LessThan => Ok(Expr::Binary( + Box::new(left), + next_t.kind.clone(), + Box::new(right), + )), + _ => Err(format!( + "unknown infix token: {:?} at {}", + &next_t.kind, &next_t.start + )), + }?; + next_t = new_t; + } + + Ok((left, next_t)) + } + + fn token_precedence(&self, token: &super::lexer::Token) -> usize { + match token.kind { + TokenKind::Or => 10, + TokenKind::And => 10, + TokenKind::GreaterThan => 20, + TokenKind::GreaterThanEqual => 20, + TokenKind::LessThanEqual => 20, + TokenKind::LessThan => 20, + TokenKind::Equal => 20, + TokenKind::Contains => 20, + TokenKind::Number => 100, + TokenKind::Unkown => 100, + TokenKind::Colon => 100, + TokenKind::Name => 100, + TokenKind::Eol => 0, + } } } @@ -41,7 +117,38 @@ mod test { #[test] fn parse_expression() { - assert_eq!(Expr::Variable("distance".to_string()), Parser::new("distance").parse().unwrap()); + assert_eq!( + Expr::Variable("distance".to_string()), + Parser::new("distance").parse().unwrap() + ); assert_eq!(Expr::Number(10), Parser::new("10").parse().unwrap()); + assert_eq!( + Expr::Binary( + Box::new(Expr::Number(10)), + TokenKind::GreaterThan, + Box::new(Expr::Number(20)) + ), + Parser::new("10 > 20").parse().unwrap() + ); + assert_eq!( + Expr::Binary( + Box::new(Expr::Binary( + Box::new(Expr::Variable("variable".to_string())), + TokenKind::GreaterThan, + Box::new(Expr::Number(20)) + )), + TokenKind::And, + Box::new(Expr::Binary( + Box::new(Expr::Number(10)), + TokenKind::LessThan, + Box::new(Expr::Number(30)) + )), + ), + Parser::new("variable > 20 and 10 < 30").parse().unwrap() + ); + assert_eq!( + Expr::Quantity(Box::new(Expr::Number(10)), QuantityUnit::Miles), + Parser::new("10m").parse().unwrap() + ); } } From 24344ae4c58d1852f8eb0c5264a0b3c802a47f96 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 10 Nov 2024 10:17:39 +0000 Subject: [PATCH 05/12] Boolean --- src/filter/evaluator.rs | 103 ++++++++++++++++++++++++++++++++++++++++ src/filter/lexer.rs | 4 ++ src/filter/mod.rs | 3 ++ src/filter/parser.rs | 37 ++------------- 4 files changed, 115 insertions(+), 32 deletions(-) create mode 100644 src/filter/evaluator.rs diff --git a/src/filter/evaluator.rs b/src/filter/evaluator.rs new file mode 100644 index 0000000..577a9af --- /dev/null +++ b/src/filter/evaluator.rs @@ -0,0 +1,103 @@ +use super::parser::Parser; + +struct Evaluator<'a, TSubject> { + parser: Parser<'a>, + subject: TSubject, +} + +#[derive(PartialEq, Eq, PartialOrd, Debug)] +enum Evalue { + String(String), + Number(u16), + Bool(bool), +} +impl Evalue { + fn to_bool(&self) -> bool { + match self { + Evalue::String(v) => v != "" && v != "0", + Evalue::Number(n) => *n != 0, + Evalue::Bool(b) => *b, + } + } +} + +impl Evaluator<'_, T> { + pub fn new<'a, TSubject>(expr: &'a str, subject: TSubject) -> Evaluator { + Evaluator:: { + parser: Parser::new(expr), + subject, + } + } + + pub fn evaluate(&mut self) -> Result { + let expr = self.parser.parse()?; + match self.evaluate_expr(expr.clone())? { + Evalue::String(_)| + Evalue::Number(_) => Err( + format!( + "expression must evluate to a boolean, got: {:?}", + expr + ).to_string() + ), + Evalue::Bool(b) => Ok(b), + } + } + + fn evaluate_expr<>(&self, expr: super::parser::Expr) -> Result { + match expr { + super::parser::Expr::Boolean(b) => Ok(Evalue::Bool(b)), + super::parser::Expr::Binary( + lexpr, + op, + rexpr, + ) => { + let lval = self.evaluate_expr(*lexpr)?; + let rval = self.evaluate_expr(*rexpr)?; + let eval = match op { + super::lexer::TokenKind::GreaterThan => Ok(lval > rval), + super::lexer::TokenKind::GreaterThanEqual => Ok(lval >= rval), + super::lexer::TokenKind::LessThanEqual => Ok(lval <= rval), + super::lexer::TokenKind::LessThan => Ok(lval < rval), + super::lexer::TokenKind::Equal => Ok(lval == rval), + super::lexer::TokenKind::Or => Ok(lval.to_bool() || rval.to_bool()), + super::lexer::TokenKind::And => Ok(lval.to_bool() && rval.to_bool()), + _ => Err(format!("unknown operator: {:?}", op)) + }?; + Ok(Evalue::Bool(eval)) + }, + super::parser::Expr::Number(n) => Ok(Evalue::Number(n)), + super::parser::Expr::Variable(_) => Ok(Evalue::Number(1)), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + struct TestSubject { + distance: u64, + } + + #[test] + fn test_evaluate() { + let result = Evaluator::::new( + "false", + TestSubject { distance: 100 }, + ).evaluate(); + assert_eq!(false, result.unwrap()); + let result = Evaluator::::new( + "20 > 10", + TestSubject { distance: 100 }, + ).evaluate(); + + assert_eq!(true, result.unwrap()); + + let result = Evaluator::::new( + "20 > 10 and false", + TestSubject { distance: 100 }, + ).evaluate(); + + assert_eq!(false, result.unwrap()); + } +} diff --git a/src/filter/lexer.rs b/src/filter/lexer.rs index 3147e74..609c363 100644 --- a/src/filter/lexer.rs +++ b/src/filter/lexer.rs @@ -5,6 +5,8 @@ #[derive(PartialEq, Eq, Debug, Clone)] pub enum TokenKind { + True, + False, Number, Contains, Unkown, @@ -115,6 +117,8 @@ impl Lexer<'_> { } match &self.expr[self.pos..self.pos + length] { + "true" => self.spawn_advance(TokenKind::True, length), + "false" => self.spawn_advance(TokenKind::False, length), "or" => self.spawn_advance(TokenKind::Or, length), "and" => self.spawn_advance(TokenKind::And, length), "OR" => self.spawn_advance(TokenKind::Or, length), diff --git a/src/filter/mod.rs b/src/filter/mod.rs index 5ae68b7..31bc965 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -3,3 +3,6 @@ mod lexer; #[allow(dead_code)] mod parser; + +#[allow(dead_code)] +mod evaluator; diff --git a/src/filter/parser.rs b/src/filter/parser.rs index cd81d04..efb68a5 100644 --- a/src/filter/parser.rs +++ b/src/filter/parser.rs @@ -5,16 +5,10 @@ pub enum Expr { Binary(Box, TokenKind, Box), Number(u16), Variable(String), - Quantity(Box, QuantityUnit) + Boolean(bool), } -#[derive(PartialEq, Eq, Debug, Clone)] -pub enum QuantityUnit { - Miles, - Kilometers, -} - -struct Parser<'a> { +pub struct Parser<'a> { lexer: Lexer<'a>, } @@ -34,6 +28,8 @@ impl Parser<'_> { fn parse_expr(&mut self, precedence: usize) -> Result<(Expr, Token), String> { let token = self.lexer.next(); let mut left: Expr = match token.kind { + TokenKind::True => Ok(Expr::Boolean(true)), + TokenKind::False => Ok(Expr::Boolean(false)), TokenKind::Number => match self.lexer.token_value(&token).parse::() { Ok(v) => Ok(Expr::Number(v)), Err(_) => Err("Could not parse number".to_string()), @@ -50,22 +46,6 @@ impl Parser<'_> { return Ok((left, next_t)); } - // suffix parsing - let suffix = match &next_t.kind { - TokenKind::Name => match self.lexer.token_value(&next_t) { - "m" => Some(Expr::Quantity(Box::new(left.clone()), QuantityUnit::Miles)), - "km" => Some(Expr::Quantity(Box::new(left.clone()), QuantityUnit::Kilometers)), - _ => None, - }, - _ => None, - }; - (next_t, left) = match suffix { - Some(suffix) => { - (self.lexer.next(), suffix) - }, - None => (next_t, left), - }; - // infix parsing while precedence < self.token_precedence(&next_t) { let (right, new_t) = self.parse_expr(self.token_precedence(&next_t)).unwrap(); @@ -102,11 +82,8 @@ impl Parser<'_> { TokenKind::LessThan => 20, TokenKind::Equal => 20, TokenKind::Contains => 20, - TokenKind::Number => 100, - TokenKind::Unkown => 100, - TokenKind::Colon => 100, - TokenKind::Name => 100, TokenKind::Eol => 0, + _ => 100, } } } @@ -146,9 +123,5 @@ mod test { ), Parser::new("variable > 20 and 10 < 30").parse().unwrap() ); - assert_eq!( - Expr::Quantity(Box::new(Expr::Number(10)), QuantityUnit::Miles), - Parser::new("10m").parse().unwrap() - ); } } From 2da5cebec241c960f0fda96faec572223b75015b Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 10 Nov 2024 10:33:48 +0000 Subject: [PATCH 06/12] Evaluate variables --- src/filter/evaluator.rs | 69 ++++++++++++++++++++--------------------- src/filter/lexer.rs | 1 + 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/filter/evaluator.rs b/src/filter/evaluator.rs index 577a9af..afa984f 100644 --- a/src/filter/evaluator.rs +++ b/src/filter/evaluator.rs @@ -1,11 +1,13 @@ +use std::collections::HashMap; + use super::parser::Parser; -struct Evaluator<'a, TSubject> { +struct Evaluator<'a, 'b> { parser: Parser<'a>, - subject: TSubject, + params: &'b HashMap, } -#[derive(PartialEq, Eq, PartialOrd, Debug)] +#[derive(PartialEq, Eq, PartialOrd, Debug, Clone)] enum Evalue { String(String), Number(u16), @@ -21,36 +23,28 @@ impl Evalue { } } -impl Evaluator<'_, T> { - pub fn new<'a, TSubject>(expr: &'a str, subject: TSubject) -> Evaluator { - Evaluator:: { +impl Evaluator<'_, '_> { + pub fn new<'a, 'b>(expr: &'a str, params: &'b HashMap) -> Evaluator<'a, 'b> { + Evaluator { parser: Parser::new(expr), - subject, + params, } } pub fn evaluate(&mut self) -> Result { let expr = self.parser.parse()?; match self.evaluate_expr(expr.clone())? { - Evalue::String(_)| - Evalue::Number(_) => Err( - format!( - "expression must evluate to a boolean, got: {:?}", - expr - ).to_string() - ), + Evalue::String(_) | Evalue::Number(_) => { + Err(format!("expression must evluate to a boolean, got: {:?}", expr).to_string()) + } Evalue::Bool(b) => Ok(b), } } - fn evaluate_expr<>(&self, expr: super::parser::Expr) -> Result { + fn evaluate_expr(&self, expr: super::parser::Expr) -> Result { match expr { super::parser::Expr::Boolean(b) => Ok(Evalue::Bool(b)), - super::parser::Expr::Binary( - lexpr, - op, - rexpr, - ) => { + super::parser::Expr::Binary(lexpr, op, rexpr) => { let lval = self.evaluate_expr(*lexpr)?; let rval = self.evaluate_expr(*rexpr)?; let eval = match op { @@ -61,12 +55,15 @@ impl Evaluator<'_, T> { super::lexer::TokenKind::Equal => Ok(lval == rval), super::lexer::TokenKind::Or => Ok(lval.to_bool() || rval.to_bool()), super::lexer::TokenKind::And => Ok(lval.to_bool() && rval.to_bool()), - _ => Err(format!("unknown operator: {:?}", op)) + _ => Err(format!("unknown operator: {:?}", op)), }?; Ok(Evalue::Bool(eval)) - }, + } super::parser::Expr::Number(n) => Ok(Evalue::Number(n)), - super::parser::Expr::Variable(_) => Ok(Evalue::Number(1)), + super::parser::Expr::Variable(v) => match self.params.get(&v) { + Some(v) => Ok(v.clone()), + None => Err(format!("Unknown variable `{}`", v)), + }, } } } @@ -81,23 +78,25 @@ mod test { #[test] fn test_evaluate() { - let result = Evaluator::::new( - "false", - TestSubject { distance: 100 }, - ).evaluate(); + let result = Evaluator::new("false", &HashMap::new()).evaluate(); assert_eq!(false, result.unwrap()); - let result = Evaluator::::new( - "20 > 10", - TestSubject { distance: 100 }, - ).evaluate(); + let result = Evaluator::new("20 > 10", &HashMap::new()).evaluate(); assert_eq!(true, result.unwrap()); - let result = Evaluator::::new( - "20 > 10 and false", - TestSubject { distance: 100 }, - ).evaluate(); + let result = Evaluator::new("20 > 10 and false", &HashMap::new()).evaluate(); assert_eq!(false, result.unwrap()); } + + #[test] + fn test_evaluate_params() { + let map = HashMap::from([("distance".to_string(), Evalue::Number(10))]); + let result = Evaluator::new("distance > 5", &map).evaluate(); + assert_eq!(true, result.unwrap()); + let result = Evaluator::new("distance < 5", &map).evaluate(); + assert_eq!(false, result.unwrap()); + let result = Evaluator::new("distance = 10", &map).evaluate(); + assert_eq!(true, result.unwrap()); + } } diff --git a/src/filter/lexer.rs b/src/filter/lexer.rs index 609c363..6344f95 100644 --- a/src/filter/lexer.rs +++ b/src/filter/lexer.rs @@ -67,6 +67,7 @@ impl Lexer<'_> { } match c { + '=' => self.spawn_advance(TokenKind::Equal, 1), ':' => self.spawn_advance(TokenKind::Colon, 1), '>' => match self.peek(1) { '=' => self.spawn_advance(TokenKind::GreaterThanEqual, 2), From 6d55f17d45c24511062711a29d885e4bc235aabc Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 10 Nov 2024 10:34:56 +0000 Subject: [PATCH 07/12] Renamed mod --- src/{filter => expr}/evaluator.rs | 8 ++++---- src/{filter => expr}/lexer.rs | 0 src/{filter => expr}/mod.rs | 0 src/{filter => expr}/parser.rs | 0 src/main.rs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename src/{filter => expr}/evaluator.rs (93%) rename src/{filter => expr}/lexer.rs (100%) rename src/{filter => expr}/mod.rs (100%) rename src/{filter => expr}/parser.rs (100%) diff --git a/src/filter/evaluator.rs b/src/expr/evaluator.rs similarity index 93% rename from src/filter/evaluator.rs rename to src/expr/evaluator.rs index afa984f..ff7e154 100644 --- a/src/filter/evaluator.rs +++ b/src/expr/evaluator.rs @@ -4,7 +4,7 @@ use super::parser::Parser; struct Evaluator<'a, 'b> { parser: Parser<'a>, - params: &'b HashMap, + vars: &'b HashMap, } #[derive(PartialEq, Eq, PartialOrd, Debug, Clone)] @@ -24,10 +24,10 @@ impl Evalue { } impl Evaluator<'_, '_> { - pub fn new<'a, 'b>(expr: &'a str, params: &'b HashMap) -> Evaluator<'a, 'b> { + pub fn new<'a, 'b>(expr: &'a str, vars: &'b HashMap) -> Evaluator<'a, 'b> { Evaluator { parser: Parser::new(expr), - params, + vars, } } @@ -60,7 +60,7 @@ impl Evaluator<'_, '_> { Ok(Evalue::Bool(eval)) } super::parser::Expr::Number(n) => Ok(Evalue::Number(n)), - super::parser::Expr::Variable(v) => match self.params.get(&v) { + super::parser::Expr::Variable(v) => match self.vars.get(&v) { Some(v) => Ok(v.clone()), None => Err(format!("Unknown variable `{}`", v)), }, diff --git a/src/filter/lexer.rs b/src/expr/lexer.rs similarity index 100% rename from src/filter/lexer.rs rename to src/expr/lexer.rs diff --git a/src/filter/mod.rs b/src/expr/mod.rs similarity index 100% rename from src/filter/mod.rs rename to src/expr/mod.rs diff --git a/src/filter/parser.rs b/src/expr/parser.rs similarity index 100% rename from src/filter/parser.rs rename to src/expr/parser.rs diff --git a/src/main.rs b/src/main.rs index f161aff..4579f88 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ pub mod app; pub mod authenticator; -pub mod filter; +pub mod expr; pub mod client; pub mod component; pub mod config; From 74496da6cebe61eda601e7af56cf550fcdf7afae Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 10 Nov 2024 15:31:52 +0000 Subject: [PATCH 08/12] Separate responsiblities --- src/expr/evaluator.rs | 47 ++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/expr/evaluator.rs b/src/expr/evaluator.rs index ff7e154..6593e64 100644 --- a/src/expr/evaluator.rs +++ b/src/expr/evaluator.rs @@ -1,10 +1,10 @@ use std::collections::HashMap; -use super::parser::Parser; +use super::parser::{Parser, Expr}; -struct Evaluator<'a, 'b> { - parser: Parser<'a>, - vars: &'b HashMap, +type Vars = HashMap; + +struct Evaluator { } #[derive(PartialEq, Eq, PartialOrd, Debug, Clone)] @@ -23,17 +23,18 @@ impl Evalue { } } -impl Evaluator<'_, '_> { - pub fn new<'a, 'b>(expr: &'a str, vars: &'b HashMap) -> Evaluator<'a, 'b> { - Evaluator { - parser: Parser::new(expr), - vars, - } +impl Evaluator { + pub fn new() -> Evaluator { + Evaluator { } + } + + pub fn parse(&mut self, expr: String) -> Result { + Parser::new(&expr).parse() } - pub fn evaluate(&mut self) -> Result { - let expr = self.parser.parse()?; - match self.evaluate_expr(expr.clone())? { + pub fn parse_and_evaluate(&mut self, expr: &str, vars: &Vars) -> Result { + let expr = Parser::new(expr).parse()?; + match self.evaluate(expr.clone(), vars)? { Evalue::String(_) | Evalue::Number(_) => { Err(format!("expression must evluate to a boolean, got: {:?}", expr).to_string()) } @@ -41,12 +42,12 @@ impl Evaluator<'_, '_> { } } - fn evaluate_expr(&self, expr: super::parser::Expr) -> Result { + pub fn evaluate(&self, expr: super::parser::Expr, vars: &Vars) -> Result { match expr { super::parser::Expr::Boolean(b) => Ok(Evalue::Bool(b)), super::parser::Expr::Binary(lexpr, op, rexpr) => { - let lval = self.evaluate_expr(*lexpr)?; - let rval = self.evaluate_expr(*rexpr)?; + let lval = self.evaluate(*lexpr, vars)?; + let rval = self.evaluate(*rexpr, vars)?; let eval = match op { super::lexer::TokenKind::GreaterThan => Ok(lval > rval), super::lexer::TokenKind::GreaterThanEqual => Ok(lval >= rval), @@ -60,7 +61,7 @@ impl Evaluator<'_, '_> { Ok(Evalue::Bool(eval)) } super::parser::Expr::Number(n) => Ok(Evalue::Number(n)), - super::parser::Expr::Variable(v) => match self.vars.get(&v) { + super::parser::Expr::Variable(v) => match vars.get(&v) { Some(v) => Ok(v.clone()), None => Err(format!("Unknown variable `{}`", v)), }, @@ -78,13 +79,13 @@ mod test { #[test] fn test_evaluate() { - let result = Evaluator::new("false", &HashMap::new()).evaluate(); + let result = Evaluator::new().parse_and_evaluate("false", &HashMap::new()); assert_eq!(false, result.unwrap()); - let result = Evaluator::new("20 > 10", &HashMap::new()).evaluate(); + let result = Evaluator::new().parse_and_evaluate("20 > 10", &HashMap::new()); assert_eq!(true, result.unwrap()); - let result = Evaluator::new("20 > 10 and false", &HashMap::new()).evaluate(); + let result = Evaluator::new().parse_and_evaluate("20 > 10 and false", &HashMap::new()); assert_eq!(false, result.unwrap()); } @@ -92,11 +93,11 @@ mod test { #[test] fn test_evaluate_params() { let map = HashMap::from([("distance".to_string(), Evalue::Number(10))]); - let result = Evaluator::new("distance > 5", &map).evaluate(); + let result = Evaluator::new().parse_and_evaluate("distance > 5", &map); assert_eq!(true, result.unwrap()); - let result = Evaluator::new("distance < 5", &map).evaluate(); + let result = Evaluator::new().parse_and_evaluate("distance < 5", &map); assert_eq!(false, result.unwrap()); - let result = Evaluator::new("distance = 10", &map).evaluate(); + let result = Evaluator::new().parse_and_evaluate("distance = 10", &map); assert_eq!(true, result.unwrap()); } } From 03595da8a99f24e6a2dc5178ef795a8af1448668 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 10 Nov 2024 16:05:19 +0000 Subject: [PATCH 09/12] Filtering works --- src/app.rs | 10 +++++++-- src/expr/evaluator.rs | 51 ++++++++++++++++++++++++------------------- src/expr/lexer.rs | 26 ++++++++++++++++++++++ src/expr/mod.rs | 9 ++------ src/expr/parser.rs | 24 +++++++++++--------- src/store/activity.rs | 16 ++++++++++++++ 6 files changed, 94 insertions(+), 42 deletions(-) diff --git a/src/app.rs b/src/app.rs index feceb63..1765791 100644 --- a/src/app.rs +++ b/src/app.rs @@ -17,7 +17,7 @@ use crate::{ component::{activity_list, unit_formatter::UnitFormatter, log_view::LogView}, event::keymap::KeyMap, store::activity::Activity, - ui, + ui, expr::{self, evaluator::{Evaluator, Vars}}, }; use crate::{ component::{ @@ -227,7 +227,13 @@ impl App<'_> { pub async fn reload(&mut self) { let mut activities = self.store.activities().await; - activities = activities.where_title_contains(self.filters.filter.as_str()); + + let mut evaluator = Evaluator::new(); + activities = match evaluator.parse(self.filters.filter.as_str()) { + Ok(expr) => activities.by_expr(&evaluator, &expr), + Err(_) => activities.where_title_contains(self.filters.filter.as_str()), + }; + if let Some(activity_type) = self.activity_type.clone() { activities = activities.having_activity_type(activity_type); } diff --git a/src/expr/evaluator.rs b/src/expr/evaluator.rs index 6593e64..5d856c5 100644 --- a/src/expr/evaluator.rs +++ b/src/expr/evaluator.rs @@ -1,23 +1,22 @@ use std::collections::HashMap; -use super::parser::{Parser, Expr}; +use super::parser::{Expr, Parser}; -type Vars = HashMap; +pub type Vars = HashMap; -struct Evaluator { -} +pub struct Evaluator {} -#[derive(PartialEq, Eq, PartialOrd, Debug, Clone)] -enum Evalue { +#[derive(PartialEq, PartialOrd, Debug, Clone)] +pub enum Evalue { String(String), - Number(u16), + Number(f64), Bool(bool), } impl Evalue { fn to_bool(&self) -> bool { match self { Evalue::String(v) => v != "" && v != "0", - Evalue::Number(n) => *n != 0, + Evalue::Number(n) => *n != 0.0, Evalue::Bool(b) => *b, } } @@ -25,16 +24,20 @@ impl Evalue { impl Evaluator { pub fn new() -> Evaluator { - Evaluator { } + Evaluator {} } - pub fn parse(&mut self, expr: String) -> Result { - Parser::new(&expr).parse() + pub fn parse(&mut self, expr: &str) -> Result { + Parser::new(expr).parse() } pub fn parse_and_evaluate(&mut self, expr: &str, vars: &Vars) -> Result { let expr = Parser::new(expr).parse()?; - match self.evaluate(expr.clone(), vars)? { + self.evaluate(&expr, vars) + } + + pub fn evaluate(&self, expr: &Expr, vars: &Vars) -> Result { + match self.evaluate_expr(&expr, vars)? { Evalue::String(_) | Evalue::Number(_) => { Err(format!("expression must evluate to a boolean, got: {:?}", expr).to_string()) } @@ -42,12 +45,13 @@ impl Evaluator { } } - pub fn evaluate(&self, expr: super::parser::Expr, vars: &Vars) -> Result { + fn evaluate_expr(&self, expr: &super::parser::Expr, vars: &Vars) -> Result { match expr { - super::parser::Expr::Boolean(b) => Ok(Evalue::Bool(b)), + super::parser::Expr::Boolean(b) => Ok(Evalue::Bool(*b)), + super::parser::Expr::String(s) => Ok(Evalue::String(s.clone())), super::parser::Expr::Binary(lexpr, op, rexpr) => { - let lval = self.evaluate(*lexpr, vars)?; - let rval = self.evaluate(*rexpr, vars)?; + let lval = self.evaluate_expr(lexpr, vars)?; + let rval = self.evaluate_expr(rexpr, vars)?; let eval = match op { super::lexer::TokenKind::GreaterThan => Ok(lval > rval), super::lexer::TokenKind::GreaterThanEqual => Ok(lval >= rval), @@ -60,8 +64,8 @@ impl Evaluator { }?; Ok(Evalue::Bool(eval)) } - super::parser::Expr::Number(n) => Ok(Evalue::Number(n)), - super::parser::Expr::Variable(v) => match vars.get(&v) { + super::parser::Expr::Number(n) => Ok(Evalue::Number(*n)), + super::parser::Expr::Variable(v) => match vars.get(v) { Some(v) => Ok(v.clone()), None => Err(format!("Unknown variable `{}`", v)), }, @@ -73,10 +77,6 @@ impl Evaluator { mod test { use super::*; - struct TestSubject { - distance: u64, - } - #[test] fn test_evaluate() { let result = Evaluator::new().parse_and_evaluate("false", &HashMap::new()); @@ -92,12 +92,17 @@ mod test { #[test] fn test_evaluate_params() { - let map = HashMap::from([("distance".to_string(), Evalue::Number(10))]); + let map = HashMap::from([ + ("distance".to_string(), Evalue::Number(10.0)), + ("type".to_string(), Evalue::String("Run".to_string())), + ]); let result = Evaluator::new().parse_and_evaluate("distance > 5", &map); assert_eq!(true, result.unwrap()); let result = Evaluator::new().parse_and_evaluate("distance < 5", &map); assert_eq!(false, result.unwrap()); let result = Evaluator::new().parse_and_evaluate("distance = 10", &map); assert_eq!(true, result.unwrap()); + let result = Evaluator::new().parse_and_evaluate("type = 'Run'", &map); + assert_eq!(true, result.unwrap()); } } diff --git a/src/expr/lexer.rs b/src/expr/lexer.rs index 6344f95..30f233b 100644 --- a/src/expr/lexer.rs +++ b/src/expr/lexer.rs @@ -6,6 +6,7 @@ #[derive(PartialEq, Eq, Debug, Clone)] pub enum TokenKind { True, + String, False, Number, Contains, @@ -67,6 +68,8 @@ impl Lexer<'_> { } match c { + '"' => self.parse_string(), + '\'' => self.parse_string(), '=' => self.spawn_advance(TokenKind::Equal, 1), ':' => self.spawn_advance(TokenKind::Colon, 1), '>' => match self.peek(1) { @@ -128,6 +131,20 @@ impl Lexer<'_> { } } + fn parse_string(&mut self) -> Token { + // move past opening quote + self.advance(); + + let mut length = 1; + while self.peek(length) != '\'' && self.peek(length) != '"' && self.peek(length) != '\0' { + length += 1; + } + + let val = self.spawn_advance(TokenKind::String, length); + self.advance(); + val + } + fn spawn_token(&self, number: TokenKind, start: usize) -> Token { Token { kind: number, @@ -205,6 +222,15 @@ mod test { assert_eq!(TokenKind::And, Lexer::new("AND").next().kind); } + #[test] + pub fn lex_string_literal() { + assert_eq!(TokenKind::String, Lexer::new("\"or\"").next().kind); + assert_eq!(TokenKind::String, Lexer::new("'or'").next().kind); + let mut l = Lexer::new("'or'"); + let t = l.next(); + assert_eq!("or", l.token_value(&t)); + } + #[test] pub fn lex_expression() { let mut l = Lexer::new("distance > 10m"); diff --git a/src/expr/mod.rs b/src/expr/mod.rs index 31bc965..6d219e3 100644 --- a/src/expr/mod.rs +++ b/src/expr/mod.rs @@ -1,8 +1,3 @@ -#[allow(dead_code)] mod lexer; - -#[allow(dead_code)] -mod parser; - -#[allow(dead_code)] -mod evaluator; +pub mod parser; +pub mod evaluator; diff --git a/src/expr/parser.rs b/src/expr/parser.rs index efb68a5..360d095 100644 --- a/src/expr/parser.rs +++ b/src/expr/parser.rs @@ -1,11 +1,14 @@ +use std::ascii::AsciiExt; + use super::lexer::{Lexer, Token, TokenKind}; -#[derive(PartialEq, Eq, Debug, Clone)] +#[derive(PartialEq, Debug, Clone)] pub enum Expr { Binary(Box, TokenKind, Box), - Number(u16), + Number(f64), Variable(String), Boolean(bool), + String(String), } pub struct Parser<'a> { @@ -30,10 +33,11 @@ impl Parser<'_> { let mut left: Expr = match token.kind { TokenKind::True => Ok(Expr::Boolean(true)), TokenKind::False => Ok(Expr::Boolean(false)), - TokenKind::Number => match self.lexer.token_value(&token).parse::() { + TokenKind::Number => match self.lexer.token_value(&token).parse::() { Ok(v) => Ok(Expr::Number(v)), Err(_) => Err("Could not parse number".to_string()), }, + TokenKind::String => Ok(Expr::String(self.lexer.token_value(&token).to_string())), TokenKind::Name => { let value = self.lexer.token_value(&token); Ok(Expr::Variable(value.to_string())) @@ -48,7 +52,7 @@ impl Parser<'_> { // infix parsing while precedence < self.token_precedence(&next_t) { - let (right, new_t) = self.parse_expr(self.token_precedence(&next_t)).unwrap(); + let (right, new_t) = self.parse_expr(self.token_precedence(&next_t))?; left = match &next_t.kind { TokenKind::GreaterThan | TokenKind::GreaterThanEqual @@ -98,12 +102,12 @@ mod test { Expr::Variable("distance".to_string()), Parser::new("distance").parse().unwrap() ); - assert_eq!(Expr::Number(10), Parser::new("10").parse().unwrap()); + assert_eq!(Expr::Number(10.0), Parser::new("10").parse().unwrap()); assert_eq!( Expr::Binary( - Box::new(Expr::Number(10)), + Box::new(Expr::Number(10.0)), TokenKind::GreaterThan, - Box::new(Expr::Number(20)) + Box::new(Expr::Number(20.0)) ), Parser::new("10 > 20").parse().unwrap() ); @@ -112,13 +116,13 @@ mod test { Box::new(Expr::Binary( Box::new(Expr::Variable("variable".to_string())), TokenKind::GreaterThan, - Box::new(Expr::Number(20)) + Box::new(Expr::Number(20.0)) )), TokenKind::And, Box::new(Expr::Binary( - Box::new(Expr::Number(10)), + Box::new(Expr::Number(10.0)), TokenKind::LessThan, - Box::new(Expr::Number(30)) + Box::new(Expr::Number(30.0)) )), ), Parser::new("variable > 20 and 10 < 30").parse().unwrap() diff --git a/src/store/activity.rs b/src/store/activity.rs index 3df14fb..3321b79 100644 --- a/src/store/activity.rs +++ b/src/store/activity.rs @@ -7,6 +7,8 @@ use serde::{Deserialize, Serialize}; use sqlx::{FromRow, SqlitePool}; use strum::EnumIter; +use crate::expr::{parser::Expr, evaluator::{Evaluator, Vars, Evalue}}; + use super::polyline_compare::compare; #[derive(EnumIter)] @@ -203,6 +205,20 @@ impl Activities { pub fn to_vec(&self) -> Vec { self.activities.clone() } + + pub(crate) fn by_expr(&self, evaluator: &Evaluator, expr: &Expr) -> Activities { + self.activities.clone() + .into_iter() + .filter(|a| match evaluator.evaluate(expr, &Vars::from([ + ("distance".to_string(), Evalue::Number(a.distance)), + ("type".to_string(), Evalue::String(a.activity_type.to_string())), + ("heartrate".to_string(), Evalue::Number(a.average_heartrate.unwrap_or(0.0))), + ])) { + Ok(v) => v, + Err(_) => false, + }) + .collect() + } } impl From> for Activities { From ea93baef7fbe63c0a4a0d55c419fb55000a16145 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 10 Nov 2024 16:54:49 +0000 Subject: [PATCH 10/12] Fuzzy match operator --- src/expr/evaluator.rs | 16 ++++++++++++++++ src/expr/lexer.rs | 4 ++++ src/expr/parser.rs | 1 + src/store/activity.rs | 1 + 4 files changed, 22 insertions(+) diff --git a/src/expr/evaluator.rs b/src/expr/evaluator.rs index 5d856c5..db79d04 100644 --- a/src/expr/evaluator.rs +++ b/src/expr/evaluator.rs @@ -20,6 +20,17 @@ impl Evalue { Evalue::Bool(b) => *b, } } + + fn to_string(&self) -> String { + match self { + Evalue::String(v) => v.clone(), + Evalue::Number(n) => format!("{}", *n), + Evalue::Bool(b) => match b { + true => "true".to_string(), + false => "false".to_string(), + }, + } + } } impl Evaluator { @@ -58,6 +69,7 @@ impl Evaluator { super::lexer::TokenKind::LessThanEqual => Ok(lval <= rval), super::lexer::TokenKind::LessThan => Ok(lval < rval), super::lexer::TokenKind::Equal => Ok(lval == rval), + super::lexer::TokenKind::FuzzyEqual => Ok(lval.to_string().contains(rval.to_string().as_str())), super::lexer::TokenKind::Or => Ok(lval.to_bool() || rval.to_bool()), super::lexer::TokenKind::And => Ok(lval.to_bool() && rval.to_bool()), _ => Err(format!("unknown operator: {:?}", op)), @@ -104,5 +116,9 @@ mod test { assert_eq!(true, result.unwrap()); let result = Evaluator::new().parse_and_evaluate("type = 'Run'", &map); assert_eq!(true, result.unwrap()); + let result = Evaluator::new().parse_and_evaluate("type ~ 'Ru'", &map); + assert_eq!(true, result.unwrap()); + let result = Evaluator::new().parse_and_evaluate("type ~ 'Rup'", &map); + assert_eq!(false, result.unwrap()); } } diff --git a/src/expr/lexer.rs b/src/expr/lexer.rs index 30f233b..bc0385d 100644 --- a/src/expr/lexer.rs +++ b/src/expr/lexer.rs @@ -19,6 +19,7 @@ pub enum TokenKind { Or, And, Equal, + FuzzyEqual, Name, Eol, } @@ -70,6 +71,7 @@ impl Lexer<'_> { match c { '"' => self.parse_string(), '\'' => self.parse_string(), + '~' => self.spawn_advance(TokenKind::FuzzyEqual, 1), '=' => self.spawn_advance(TokenKind::Equal, 1), ':' => self.spawn_advance(TokenKind::Colon, 1), '>' => match self.peek(1) { @@ -212,6 +214,8 @@ mod test { assert_eq!(TokenKind::GreaterThan, Lexer::new(">").next().kind); assert_eq!(TokenKind::LessThanEqual, Lexer::new("<=").next().kind); assert_eq!(TokenKind::LessThan, Lexer::new("<").next().kind); + assert_eq!(TokenKind::Equal, Lexer::new("=").next().kind); + assert_eq!(TokenKind::FuzzyEqual, Lexer::new("~").next().kind); } #[test] diff --git a/src/expr/parser.rs b/src/expr/parser.rs index 360d095..856f87f 100644 --- a/src/expr/parser.rs +++ b/src/expr/parser.rs @@ -58,6 +58,7 @@ impl Parser<'_> { | TokenKind::GreaterThanEqual | TokenKind::And | TokenKind::Or + | TokenKind::FuzzyEqual | TokenKind::Equal | TokenKind::LessThanEqual | TokenKind::LessThan => Ok(Expr::Binary( diff --git a/src/store/activity.rs b/src/store/activity.rs index 3321b79..5c8ee1c 100644 --- a/src/store/activity.rs +++ b/src/store/activity.rs @@ -213,6 +213,7 @@ impl Activities { ("distance".to_string(), Evalue::Number(a.distance)), ("type".to_string(), Evalue::String(a.activity_type.to_string())), ("heartrate".to_string(), Evalue::Number(a.average_heartrate.unwrap_or(0.0))), + ("title".to_string(), Evalue::String(a.title.clone())), ])) { Ok(v) => v, Err(_) => false, From 91042b073ad9782457149068064ada179c01a820 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 10 Nov 2024 17:06:24 +0000 Subject: [PATCH 11/12] Negation --- src/expr/evaluator.rs | 6 +++++- src/expr/lexer.rs | 11 ++++++++++- src/expr/parser.rs | 2 ++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/expr/evaluator.rs b/src/expr/evaluator.rs index db79d04..e5d56af 100644 --- a/src/expr/evaluator.rs +++ b/src/expr/evaluator.rs @@ -70,6 +70,8 @@ impl Evaluator { super::lexer::TokenKind::LessThan => Ok(lval < rval), super::lexer::TokenKind::Equal => Ok(lval == rval), super::lexer::TokenKind::FuzzyEqual => Ok(lval.to_string().contains(rval.to_string().as_str())), + super::lexer::TokenKind::NotEqual => Ok(lval != rval), + super::lexer::TokenKind::NotFuzzyEqual => Ok(!lval.to_string().contains(rval.to_string().as_str())), super::lexer::TokenKind::Or => Ok(lval.to_bool() || rval.to_bool()), super::lexer::TokenKind::And => Ok(lval.to_bool() && rval.to_bool()), _ => Err(format!("unknown operator: {:?}", op)), @@ -118,7 +120,9 @@ mod test { assert_eq!(true, result.unwrap()); let result = Evaluator::new().parse_and_evaluate("type ~ 'Ru'", &map); assert_eq!(true, result.unwrap()); - let result = Evaluator::new().parse_and_evaluate("type ~ 'Rup'", &map); + let result = Evaluator::new().parse_and_evaluate("type !~ 'Rup'", &map); + assert_eq!(true, result.unwrap()); + let result = Evaluator::new().parse_and_evaluate("type != 'Run'", &map); assert_eq!(false, result.unwrap()); } } diff --git a/src/expr/lexer.rs b/src/expr/lexer.rs index bc0385d..97846fc 100644 --- a/src/expr/lexer.rs +++ b/src/expr/lexer.rs @@ -20,6 +20,8 @@ pub enum TokenKind { And, Equal, FuzzyEqual, + NotEqual, + NotFuzzyEqual, Name, Eol, } @@ -69,6 +71,11 @@ impl Lexer<'_> { } match c { + '!' => match self.peek(1) { + '=' => self.spawn_advance(TokenKind::NotEqual, 2), + '~' => self.spawn_advance(TokenKind::NotFuzzyEqual, 2), + _ => self.spawn_advance(TokenKind::Unkown, 1), + }, '"' => self.parse_string(), '\'' => self.parse_string(), '~' => self.spawn_advance(TokenKind::FuzzyEqual, 1), @@ -82,7 +89,7 @@ impl Lexer<'_> { '=' => self.spawn_advance(TokenKind::LessThanEqual, 2), _ => self.spawn_advance(TokenKind::LessThan, 1), }, - _ => self.spawn_advance(TokenKind::Unkown, 0), + _ => self.spawn_advance(TokenKind::Unkown, 1), } } }; @@ -216,6 +223,8 @@ mod test { assert_eq!(TokenKind::LessThan, Lexer::new("<").next().kind); assert_eq!(TokenKind::Equal, Lexer::new("=").next().kind); assert_eq!(TokenKind::FuzzyEqual, Lexer::new("~").next().kind); + assert_eq!(TokenKind::NotEqual, Lexer::new("!=").next().kind); + assert_eq!(TokenKind::NotFuzzyEqual, Lexer::new("!~").next().kind); } #[test] diff --git a/src/expr/parser.rs b/src/expr/parser.rs index 856f87f..b490d94 100644 --- a/src/expr/parser.rs +++ b/src/expr/parser.rs @@ -60,6 +60,8 @@ impl Parser<'_> { | TokenKind::Or | TokenKind::FuzzyEqual | TokenKind::Equal + | TokenKind::NotFuzzyEqual + | TokenKind::NotEqual | TokenKind::LessThanEqual | TokenKind::LessThan => Ok(Expr::Binary( Box::new(left), From 39d81c8b3cdfb8567a2feb03936daae75ce1771e Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 10 Nov 2024 17:34:51 +0000 Subject: [PATCH 12/12] Updatre README --- README.md | 67 +++++++++++++++++++++++++++++++++++++------ src/app.rs | 23 ++++++++------- src/expr/parser.rs | 2 -- src/store/activity.rs | 2 ++ 4 files changed, 72 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 3e323e0..2802d8c 100644 --- a/README.md +++ b/README.md @@ -6,24 +6,73 @@ Strava TUI written in Rust! This is an experimental TUI for Strava. Features: - List activities in a comparable way -- Filter activites by name +- Filter activites by with expressions - Sort listed activities - Display the route - Show laps - Race predictions - Filter by route similarity ("anchoring") +## Screenshots + ### List activities -![image](https://github.com/dantleech/strava-rs/assets/530801/7187befb-65e2-4fbc-b5b4-8710510c5e1a) -*Numbers* +![image](https://github.com/user-attachments/assets/f13ed611-d764-4941-a3df-c95db8636ba7) + +### Acivity View + +![image](https://github.com/user-attachments/assets/88c9b34a-7cee-409d-9d01-39bd22ef8259) + +## Key Map + +- `q`: **Quit**: quit! +- `k`: **Up** - select previous activity +- `j`: **Down** - select next activity +- `n`: **Next** - (in activity view) next split +- `p`: **Previous** - (in activity view) previous split +- `o`: **ToggleSortOrder** - switch between ascending and descending order +- `u`: **ToggleUnitSystem** - switch between imperial and metric units +- `s`: **Sort** - show sort dialog +- `S`: **Rank** - choose ranking +- `f`: **Filter** - filter (see filter section below) +- `r`: **Refresh** - reload activities +- `a`: **Anchor** - show activities with similar routes +- `+`: **IncreaseTolerance** - incease the anchor tolerance +- `-`: **DecreaseTolerance** - descrease the ancor tolerance +- `0`: **ToggleLogView** - toggle log view + +## Filter + +Press `f` on the activity list view to open the filter input. + +### Examples + +Show all runs that are of a half marathon distance or more: + +``` +type = "Run" and distance > 21000 +``` + +Show all runs with "Park" in the title: + +``` +type = "Run" and title ~ "Park" +``` -### Filter activities +### Fields -![image](https://github.com/dantleech/strava-rs/assets/530801/42a5a2e2-0925-4d1f-a780-e1a5d11b0ab1) -*Chronological* +- `distance`: Distance (in meters) +- `type`: `Run`, `Ride` etc. +- `heartrate`: Heart rate in BPM. +- `title`: Activity title +- `elevation`: Elevation (in meters) +- `time`: Time (in seconds, 3600 = 1 hour) -### Details +### Operators -![image](https://github.com/dantleech/strava-rs/assets/530801/633ea4ff-12c8-4ead-817b-80db8efcf61a) -*Detailed Maps* +- `>`, `<`: Greater than, Less than (e.g. `distance > 21000`) +- `and`, `or`: Logical operators (e.g. `type = "Run" and time > 0`) +- `=`: Equal to +- `~`: String contains +- `!=`: Not equal to (e.g. `type != "Run"`) +- `!~`: String does not contain (e.g. `title ~ "Parkrun"`) diff --git a/src/app.rs b/src/app.rs index 1765791..5a8a8b7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,16 +8,18 @@ use log::info; use tokio::sync::mpsc::{Receiver, Sender}; use tui::{ backend::{Backend, CrosstermBackend}, - widgets::TableState, Terminal, + widgets::TableState, + Terminal, }; use tui_input::Input; use tui_logger::TuiWidgetState; use crate::{ - component::{activity_list, unit_formatter::UnitFormatter, log_view::LogView}, + component::{activity_list, log_view::LogView, unit_formatter::UnitFormatter}, event::keymap::KeyMap, + expr::evaluator::Evaluator, store::activity::Activity, - ui, expr::{self, evaluator::{Evaluator, Vars}}, + ui, }; use crate::{ component::{ @@ -138,7 +140,8 @@ impl App<'_> { pace_table_state: TableState::default(), selected_split: None, }, - log_view_state: TuiWidgetState::default().set_default_display_level(log::LevelFilter::Debug), + log_view_state: TuiWidgetState::default() + .set_default_display_level(log::LevelFilter::Debug), filters: ActivityFilters { sort_by: SortBy::Date, sort_order: SortOrder::Desc, @@ -176,8 +179,8 @@ impl App<'_> { let mut view: Box = match self.active_page { ActivePage::ActivityList => Box::new(ActivityList::new()), - ActivePage::Activity => Box::new(ActivityView{}), - ActivePage::LogView => Box::new(LogView::new()) + ActivePage::Activity => Box::new(ActivityView {}), + ActivePage::LogView => Box::new(LogView::new()), }; if let Some(message) = &self.info_message { @@ -194,9 +197,7 @@ impl App<'_> { while self.event_queue.len() > 1 { let event = self.event_queue.pop().unwrap(); info!("Sending event: {:?}", event); - self.event_sender - .send(event) - .await?; + self.event_sender.send(event).await?; } if let Some(event) = self.event_receiver.recv().await { @@ -233,7 +234,7 @@ impl App<'_> { Ok(expr) => activities.by_expr(&evaluator, &expr), Err(_) => activities.where_title_contains(self.filters.filter.as_str()), }; - + if let Some(activity_type) = self.activity_type.clone() { activities = activities.having_activity_type(activity_type); } @@ -299,7 +300,7 @@ impl App<'_> { fn render( &mut self, terminal: &mut Terminal>, - view: &mut dyn View + view: &mut dyn View, ) -> Result<(), anyhow::Error> { let area = terminal.size().expect("Could not determine terminal size'"); terminal.autoresize()?; diff --git a/src/expr/parser.rs b/src/expr/parser.rs index b490d94..2d450cf 100644 --- a/src/expr/parser.rs +++ b/src/expr/parser.rs @@ -1,5 +1,3 @@ -use std::ascii::AsciiExt; - use super::lexer::{Lexer, Token, TokenKind}; #[derive(PartialEq, Debug, Clone)] diff --git a/src/store/activity.rs b/src/store/activity.rs index 5c8ee1c..0b2a679 100644 --- a/src/store/activity.rs +++ b/src/store/activity.rs @@ -214,6 +214,8 @@ impl Activities { ("type".to_string(), Evalue::String(a.activity_type.to_string())), ("heartrate".to_string(), Evalue::Number(a.average_heartrate.unwrap_or(0.0))), ("title".to_string(), Evalue::String(a.title.clone())), + ("elevation".to_string(), Evalue::Number(a.total_elevation_gain)), + ("time".to_string(), Evalue::Number(a.moving_time as f64)), ])) { Ok(v) => v, Err(_) => false,