From 06dbcc08dd75cdcb790e3f41180114cbcddb8bc8 Mon Sep 17 00:00:00 2001 From: Kjetil Orbekk Date: Fri, 7 Feb 2020 21:27:11 -0500 Subject: Oh dear --- src/error.rs | 2 + src/template.rs | 203 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 198 insertions(+), 7 deletions(-) diff --git a/src/error.rs b/src/error.rs index 166005d..c58083e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -81,6 +81,7 @@ pub enum Error { CommunicationError(reqwest::Error), ParseError(serde_json::error::Error), ParseTimeError(chrono::format::ParseError), + TemplateError(String, Value), StravaApiError(StravaApiError), UnexpectedJson(Value), AlreadyExists, @@ -96,6 +97,7 @@ impl fmt::Display for Error { Error::CommunicationError(ref e) => e.fmt(f), Error::ParseError(ref e) => e.fmt(f), Error::ParseTimeError(ref e) => e.fmt(f), + Error::TemplateError(ref s, _) => f.write_str(&s), Error::UnexpectedJson(_) => f.write_str("UnexpectedJson"), Error::StravaApiError(ref e) => e.fmt(f), Error::AlreadyExists => f.write_str("AlreadyExists"), diff --git a/src/template.rs b/src/template.rs index 21defa3..d469394 100644 --- a/src/template.rs +++ b/src/template.rs @@ -1,31 +1,34 @@ +use crate::error::Error; use serde::Deserialize; use serde::Serialize; +use serde_json::to_value; +use serde_json::Value as Json; pub fn running_template() -> TemplateSpec { TemplateSpec::Table(vec![ Column { - display_name: Some("Date".to_string()), + display_name: "Date".to_string(), field: FieldSpec::App( Function::DisplayUnit(Unit::Timestamp, "date".to_string()), vec![FieldSpec::Field("start_time".to_string())], ), }, Column { - display_name: Some("Time".to_string()), + display_name: "Time".to_string(), field: FieldSpec::App( Function::DisplayUnit(Unit::Seconds, "".to_string()), vec![FieldSpec::Field("moving_time".to_string())], ), }, Column { - display_name: Some("Distance".to_string()), + display_name: "Distance".to_string(), field: FieldSpec::App( Function::DisplayUnit(Unit::Meters, "miles".to_string()), vec![FieldSpec::Field("distance".to_string())], ), }, Column { - display_name: Some("Pace".to_string()), + display_name: "Pace".to_string(), field: FieldSpec::App( Function::DisplayUnit(Unit::Pace, "".to_string()), vec![FieldSpec::App( @@ -40,7 +43,7 @@ pub fn running_template() -> TemplateSpec { ]) } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Copy, Clone)] pub enum Unit { Timestamp, // As Rfc3339 Meters, @@ -51,7 +54,6 @@ pub enum Unit { #[derive(Debug, Serialize, Deserialize, Clone)] pub enum Function { - TimestampToDate, DisplayUnit(Unit, String), Div, } @@ -64,7 +66,7 @@ pub enum FieldSpec { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Column { - display_name: Option, + display_name: String, field: FieldSpec, } @@ -78,3 +80,190 @@ pub struct Template { pub entry_type: String, pub spec: TemplateSpec, } + +mod function { + use super::*; + + const METERS_PER_MILE: f64 = 1609.344; + + fn really_f64(d: &Json) -> Result { + d.to_string() + .parse::() + .map_err(|_| Error::TemplateError("convert to f64".to_string(), d.clone())) + } + + fn format_pace(d: &Json) -> Result { + let pace = really_f64(d)?; + let seconds_per_mile = (pace * METERS_PER_MILE).round() as i64; + let mut hours = "".to_string(); + if seconds_per_mile > 3600 { + hours = format!("{}", seconds_per_mile / 3600); + } + Ok(json!(format!( + "{}{:02}:{:02}", + hours, + seconds_per_mile / 60, + seconds_per_mile % 60 + ))) + } + + fn format_distance(d: &Json, opts: &str) -> Result { + Ok(json!(match opts { + "miles" => format!("{:.1}", really_f64(d)? / METERS_PER_MILE), + _ => format!("{} m", really_f64(d)?) + })) + } + + fn display_unit(u: Unit, opts: &str, params: &Vec, d: &Json) -> Result { + if params.len() != 1 { + Err(Error::TemplateError( + format!("DisplayUnit expects 1 parameter, got {:?}", params), + d.clone(), + ))?; + } + let d = eval(¶ms[0], &d)?; + Ok(match u { + Unit::Timestamp => json!(format!("as_timestamp({:?})", d)), + Unit::Meters => format_distance(&d, &opts)?, + Unit::Seconds => json!(format!("as_duration({:?})", d)), + Unit::Speed => json!(format!("as_speed({:?})", d)), + Unit::Pace => format_pace(&d)?, + }) + } + + fn div(params: &Vec, d: &Json) -> Result { + if params.len() != 2 { + Err(Error::TemplateError( + format!("DisplayUnit expects 1 parameter, got {:?}", params), + d.clone(), + ))?; + } + let p1 = really_f64(&eval(¶ms[0], &d)?)?; + let p2 = really_f64(&eval(¶ms[1], &d)?)?; + Ok(json!(p1 / p2)) + } + + fn app(f: &Function, params: &Vec, d: &Json) -> Result { + match f { + Function::DisplayUnit(ref u, ref opts) => display_unit(*u, opts, params, d), + Function::Div => div(params, d), + } + } + + pub fn eval(s: &FieldSpec, d: &Json) -> Result { + Ok(match s { + FieldSpec::Field(ref s) => to_value(&d[s])?, + FieldSpec::App(ref f, ref params) => app(f, params, d)?, + }) + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn eval_field() { + let d = json!({"x": 2}); + + assert_eq!( + json!(2), + eval(&FieldSpec::Field("x".to_string()), &d).unwrap() + ); + } + + #[test] + fn eval_fn1() { + let d = json!({"x": 0.260976}); + + let app1 = |f| { + println!("{:?}", f); + eval( + &FieldSpec::App(f, vec![FieldSpec::Field("x".to_string())]), + &d, + ) + .unwrap() + }; + assert_eq!( + json!("07:00"), + app1(Function::DisplayUnit(Unit::Pace, "".to_string())) + ); + } + + #[test] + fn eval_fn2() { + let distance = 2.0 * 1609.344; + let time = 60 * 14; + let d = json!({"distance": distance, + "time": time}); + + assert_eq!( + eval( + &FieldSpec::App(Function::Div, + vec!(FieldSpec::Field("time".to_string()), + FieldSpec::Field("distance".to_string()))), + &d).unwrap(), + json!((time as f64) / distance) + ) + } + } +} + +mod table { + use super::*; + + #[derive(Debug, Serialize, Deserialize, Clone)] + pub struct DisplayTable { + pub headings: Vec, + pub rows: Vec>, + } + + pub fn apply(columns: &Vec, d: &Json) -> Result { + let rows = d.as_array() + .ok_or_else(|| Error::TemplateError("expected array".to_string(), d.clone()))?; + + Ok(DisplayTable { + headings: columns.iter().map(|c| c.display_name.clone()).collect(), + rows: rows.iter().map(|d| { + columns.iter().map(|c| { + function::eval(&c.field, d) + }).collect::, _>>() + }).collect::, _>>()?, + }) + } +} + +pub fn apply(t: &TemplateSpec, d: &Json) -> Result { + Ok(match t { + &TemplateSpec::Table(ref c) => to_value(table::apply(c, d)?)?, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn table_test() { + let d = json!([{ + "name": "Nick", + "distance": 3218.688 + }]); + let columns = vec![ + Column { + display_name: "Name".to_string(), + field: FieldSpec::Field("name".to_string()), + }, + Column { + display_name: "Distance".to_string(), + field: FieldSpec::App( + Function::DisplayUnit(Unit::Meters, "miles".to_string()), + vec![FieldSpec::Field("distance".to_string())], + ), + }, + ]; + + let table = table::apply(&columns, &d).unwrap(); + assert_eq!(table.headings, vec!("Name", "Distance")); + assert_eq!(table.rows, vec!(vec!(json!("Nick"), json!("2.0")))); + } +} -- cgit v1.2.3