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 feceb63..5a8a8b7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,14 +8,16 @@ 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, }; @@ -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 { @@ -227,7 +228,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); } @@ -293,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/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/expr/evaluator.rs b/src/expr/evaluator.rs new file mode 100644 index 0000000..e5d56af --- /dev/null +++ b/src/expr/evaluator.rs @@ -0,0 +1,128 @@ +use std::collections::HashMap; + +use super::parser::{Expr, Parser}; + +pub type Vars = HashMap; + +pub struct Evaluator {} + +#[derive(PartialEq, PartialOrd, Debug, Clone)] +pub enum Evalue { + String(String), + Number(f64), + Bool(bool), +} +impl Evalue { + fn to_bool(&self) -> bool { + match self { + Evalue::String(v) => v != "" && v != "0", + Evalue::Number(n) => *n != 0.0, + 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 { + pub fn new() -> Evaluator { + Evaluator {} + } + + 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()?; + 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()) + } + Evalue::Bool(b) => Ok(b), + } + } + + 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::String(s) => Ok(Evalue::String(s.clone())), + super::parser::Expr::Binary(lexpr, op, rexpr) => { + 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), + 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::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)), + }?; + Ok(Evalue::Bool(eval)) + } + 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)), + }, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_evaluate() { + let result = Evaluator::new().parse_and_evaluate("false", &HashMap::new()); + assert_eq!(false, result.unwrap()); + let result = Evaluator::new().parse_and_evaluate("20 > 10", &HashMap::new()); + + assert_eq!(true, result.unwrap()); + + let result = Evaluator::new().parse_and_evaluate("20 > 10 and false", &HashMap::new()); + + assert_eq!(false, result.unwrap()); + } + + #[test] + fn test_evaluate_params() { + 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()); + 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!(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 new file mode 100644 index 0000000..97846fc --- /dev/null +++ b/src/expr/lexer.rs @@ -0,0 +1,263 @@ +// distance > 10 AND distance < 20 +// type = Run +// pace > 06:00 +// average_speed > 10mph + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum TokenKind { + True, + String, + False, + Number, + Contains, + Unkown, + Colon, + GreaterThan, + GreaterThanEqual, + LessThanEqual, + LessThan, + Or, + And, + Equal, + FuzzyEqual, + NotEqual, + NotFuzzyEqual, + 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, +} + +pub 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(); + let t = 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 { + '!' => 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), + '=' => self.spawn_advance(TokenKind::Equal, 1), + ':' => 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, 1), + } + } + }; + t + } + + 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] { + "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), + "AND" => self.spawn_advance(TokenKind::And, length), + _ => self.spawn_advance(TokenKind::Name, length), + } + } + + 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, + 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); + 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] + 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_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"); + 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/expr/mod.rs b/src/expr/mod.rs new file mode 100644 index 0000000..6d219e3 --- /dev/null +++ b/src/expr/mod.rs @@ -0,0 +1,3 @@ +mod lexer; +pub mod parser; +pub mod evaluator; diff --git a/src/expr/parser.rs b/src/expr/parser.rs new file mode 100644 index 0000000..2d450cf --- /dev/null +++ b/src/expr/parser.rs @@ -0,0 +1,132 @@ +use super::lexer::{Lexer, Token, TokenKind}; + +#[derive(PartialEq, Debug, Clone)] +pub enum Expr { + Binary(Box, TokenKind, Box), + Number(f64), + Variable(String), + Boolean(bool), + String(String), +} + +pub 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) -> 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 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()), + }, + 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())) + } + _ => 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)); + } + + // infix parsing + while precedence < self.token_precedence(&next_t) { + let (right, new_t) = self.parse_expr(self.token_precedence(&next_t))?; + left = match &next_t.kind { + TokenKind::GreaterThan + | TokenKind::GreaterThanEqual + | TokenKind::And + | TokenKind::Or + | TokenKind::FuzzyEqual + | TokenKind::Equal + | TokenKind::NotFuzzyEqual + | TokenKind::NotEqual + | 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::Eol => 0, + _ => 100, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parse_expression() { + assert_eq!( + Expr::Variable("distance".to_string()), + Parser::new("distance").parse().unwrap() + ); + assert_eq!(Expr::Number(10.0), Parser::new("10").parse().unwrap()); + assert_eq!( + Expr::Binary( + Box::new(Expr::Number(10.0)), + TokenKind::GreaterThan, + Box::new(Expr::Number(20.0)) + ), + 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.0)) + )), + TokenKind::And, + Box::new(Expr::Binary( + Box::new(Expr::Number(10.0)), + TokenKind::LessThan, + Box::new(Expr::Number(30.0)) + )), + ), + Parser::new("variable > 20 and 10 < 30").parse().unwrap() + ); + } +} diff --git a/src/main.rs b/src/main.rs index 1db1292..4579f88 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ pub mod app; pub mod authenticator; +pub mod expr; pub mod client; pub mod component; pub mod config; diff --git a/src/store/activity.rs b/src/store/activity.rs index 3df14fb..0b2a679 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,23 @@ 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))), + ("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, + }) + .collect() + } } impl From> for Activities { 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