diff --git a/Cargo.lock b/Cargo.lock index 8e8c8c8..d1f8cb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -287,7 +287,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.12.1", "lazy_static", "lazycell", "proc-macro2", @@ -484,6 +484,7 @@ dependencies = [ "axum", "chrono", "image 0.25.2", + "interp", "mysql", "mysql_common", "plotters", @@ -1527,7 +1528,7 @@ dependencies = [ "approx", "getrandom", "image 0.25.2", - "itertools", + "itertools 0.12.1", "nalgebra", "num", "rand", @@ -1551,6 +1552,16 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "interp" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf8add5dd9442596f3c5f504fd4fbbdf6266ff2337a51f6430dfeff6fd3a4de" +dependencies = [ + "itertools 0.13.0", + "num-traits", +] + [[package]] name = "interpolate_name" version = "0.2.4" @@ -1580,6 +1591,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -2497,7 +2517,7 @@ dependencies = [ "built", "cfg-if", "interpolate_name", - "itertools", + "itertools 0.12.1", "libc", "libfuzzer-sys", "log", diff --git a/server/Cargo.toml b/server/Cargo.toml index a93549c..4558905 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -18,3 +18,4 @@ time = "0.3.36" tokio = { version = "1.39.2", features = ["macros", "rt-multi-thread"] } tower-http = { version = "0.5.2", features = ["fs"] } shared = { path = "../shared", features = ["sql"] } +interp = "2.0.1" diff --git a/server/src/main.rs b/server/src/main.rs index 7b2e6d3..d39f1dc 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -19,9 +19,7 @@ use mysql::*; use std::sync::Arc; mod math; mod my_structs; -use my_structs::MyError; -use my_structs::Purchase; -use my_structs::Weight; +use my_structs::{MyError, Purchase, Running, Weight}; mod plotting; // use crate::plotting::Plotter; use std::{thread, time}; @@ -147,12 +145,14 @@ async fn svg_weight_loss(State(state): State>) -> Result, purchases: Vec, weight: Vec, + running: Vec, ) -> plotting::SVGPlotFun { let (mindate, calorie_days) = calculate_calories_per_day(nutrition_map, purchases); - let calories_burned_per_day: f32 = 2200.; + let calories_burned_per_day: f32 = 2140.; + + // Based on the Caloie Burned by Distance Calculator + // https://www.calculator.net/calories-burned-calculator.html + // if running between 5 minutes per km, and 7 minutes per km, + // we get the approximate formula: kcal = (0.94*kg + 10.01)*km + let weight_dates: Vec = weight + .iter() + .map(|w| (w.datetime.date() - mindate).whole_days() as f32) + .collect(); + let weight_values: Vec = weight.iter().map(|w| w.weight).collect(); + let weight_days = interp_slice( + &weight_dates, + &weight_values, + &(0..calorie_days.len()) + .map(|x| x as f32) + .collect::>(), + &InterpMode::FirstLast, + ); + let mut running_days: Vec = vec![0.; calorie_days.len()]; + running.into_iter().for_each(|r| { + let idx: usize = (r.datetime.date() - mindate) + .whole_days() + .try_into() + .unwrap(); + running_days[idx] += (0.94 * weight_days[idx] + 10.01) * r.distance; + }); + let deficit_days: Vec = calorie_days .iter() - .scan(0., |deficit, &calories| { - *deficit += calories - calories_burned_per_day; + .enumerate() + .scan(0., |deficit, (i, &calories)| { + *deficit += calories - calories_burned_per_day - running_days[i]; Some(*deficit) }) .collect(); @@ -230,7 +260,9 @@ pub fn plot_weight_loss( mindate_chrono .checked_add_days(chrono::Days::new(i.try_into().unwrap())) .unwrap(), - *cal / 7000. + weight[0].weight + 1., + *cal / 7000. + (weight[0].weight + 1.), // the non-calories values is an + // offset to make the two plotted + // lines fit ) }), RED.stroke_width(3), diff --git a/server/src/my_structs.rs b/server/src/my_structs.rs index 9067c05..f5fae1d 100644 --- a/server/src/my_structs.rs +++ b/server/src/my_structs.rs @@ -1,6 +1,8 @@ pub use self::myerror::MyError; pub use self::purchases::Purchase; +pub use self::running::Running; pub use self::weight::Weight; mod myerror; mod purchases; +mod running; mod weight; diff --git a/server/src/my_structs/running.rs b/server/src/my_structs/running.rs new file mode 100644 index 0000000..6590cdc --- /dev/null +++ b/server/src/my_structs/running.rs @@ -0,0 +1,56 @@ +use mysql::prelude::*; +use mysql::*; +use mysql_common::frunk::{hlist_pat, HList}; +use struct_field_names_as_array::FieldNamesAsArray; +use time::{PrimitiveDateTime, Time}; + +#[allow(non_snake_case)] +#[derive(Debug, PartialEq, FieldNamesAsArray)] +pub struct Running { + id: u32, + pub distance: f32, + pub time: Time, + pub datetime: PrimitiveDateTime, +} + +type RowType = HList!(u32, f32, Time, PrimitiveDateTime); + +impl Running { + fn get_sql_fields() -> String { + Running::FIELD_NAMES_AS_ARRAY + .iter() + .cloned() + .intersperse(", ") + .collect() + } + + fn query_map_helper() -> (String, impl Fn(RowType) -> Running) { + let sql_query = format!("SELECT {} from running", Self::get_sql_fields()); + println!("{}", sql_query); + + let construction_closure = |row: RowType| { + let hlist_pat![id, distance, time, datetime] = row; + Running { + id, + distance, + time, + datetime, + } + }; + + (sql_query, construction_closure) + } + + pub async fn get_running_rows(sql_pool: Pool) -> Result> { + let mut conn = sql_pool + .get_conn() + .expect("Cannot establish database connection"); + + let (running_query, running_closure) = Self::query_map_helper(); + let running_val: Vec = conn + .query_map(running_query, running_closure) + .expect("Data in database doesn't match the Running class"); + + Ok(running_val) + } +}