From 69567db4864486e482fa440b23e973550c61b82d Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 30 Jan 2024 10:08:10 -0500 Subject: [PATCH 1/5] Introduce a structure for formatting and parsing Weight values --- fitnesstrax/app/src/components/day.rs | 6 +- fitnesstrax/app/src/components/mod.rs | 4 +- fitnesstrax/app/src/components/steps.rs | 2 +- fitnesstrax/app/src/components/text_entry.rs | 6 +- fitnesstrax/app/src/components/weight.rs | 25 ++++---- fitnesstrax/app/src/types.rs | 61 ++++++++++++++++++- fitnesstrax/app/src/view_models/day_detail.rs | 15 ++--- 7 files changed, 86 insertions(+), 33 deletions(-) diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index b4b130d..4e92180 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with Fit // use chrono::NaiveDate; // use ft_core::TraxRecord; use crate::{ - components::{steps_editor, weight_editor, ActionGroup, Steps, Weight}, + components::{steps_editor, weight_editor, ActionGroup, Steps, WeightLabel}, view_models::DayDetailViewModel, }; use glib::Object; @@ -93,7 +93,7 @@ impl DaySummary { .css_classes(["day-summary__weight"]) .build(); if let Some(s) = view_model.steps() { - label.set_label(&format!("{} steps", s.to_string())); + label.set_label(&format!("{} steps", s)); } row.append(&label); @@ -162,7 +162,7 @@ impl DayDetail { let top_row = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .build(); - let weight_view = Weight::new(view_model.weight()); + let weight_view = WeightLabel::new(view_model.weight()); top_row.append(&weight_view.widget()); let steps_view = Steps::new(view_model.steps()); diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index 880664e..8b2fff5 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -27,13 +27,13 @@ mod steps; pub use steps::{steps_editor, Steps}; mod text_entry; -pub use text_entry::{ParseError, TextEntry}; +pub use text_entry::TextEntry; mod time_distance; pub use time_distance::TimeDistanceView; mod weight; -pub use weight::{weight_editor, Weight}; +pub use weight::{weight_editor, WeightLabel}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; diff --git a/fitnesstrax/app/src/components/steps.rs b/fitnesstrax/app/src/components/steps.rs index e81c840..3860ca9 100644 --- a/fitnesstrax/app/src/components/steps.rs +++ b/fitnesstrax/app/src/components/steps.rs @@ -14,7 +14,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . */ -use crate::components::{ParseError, TextEntry}; +use crate::{components::TextEntry, types::ParseError}; use gtk::prelude::*; #[derive(Default)] diff --git a/fitnesstrax/app/src/components/text_entry.rs b/fitnesstrax/app/src/components/text_entry.rs index 12c0a6c..53702cb 100644 --- a/fitnesstrax/app/src/components/text_entry.rs +++ b/fitnesstrax/app/src/components/text_entry.rs @@ -1,5 +1,5 @@ /* -Copyright 2023, Savanni D'Gerinel +Copyright 2023-2024, Savanni D'Gerinel This file is part of FitnessTrax. @@ -14,12 +14,10 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . */ +use crate::types::ParseError; use gtk::prelude::*; use std::{cell::RefCell, rc::Rc}; -#[derive(Clone, Debug)] -pub struct ParseError; - type Renderer = dyn Fn(&T) -> String; type Parser = dyn Fn(&str) -> Result; diff --git a/fitnesstrax/app/src/components/weight.rs b/fitnesstrax/app/src/components/weight.rs index 059e903..76036c0 100644 --- a/fitnesstrax/app/src/components/weight.rs +++ b/fitnesstrax/app/src/components/weight.rs @@ -14,23 +14,25 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . */ -use crate::components::{ParseError, TextEntry}; -use dimensioned::si; +use crate::{ + components::TextEntry, + types::{FormatOption, Weight}, +}; use gtk::prelude::*; -pub struct Weight { +pub struct WeightLabel { label: gtk::Label, } -impl Weight { - pub fn new(weight: Option>) -> Self { +impl WeightLabel { + pub fn new(weight: Option) -> Self { let label = gtk::Label::builder() .css_classes(["card", "weight-view"]) .can_focus(true) .build(); match weight { - Some(w) => label.set_text(&format!("{:?}", w)), + Some(w) => label.set_text(&w.format(FormatOption::Abbreviated)), None => label.set_text("No weight recorded"), } @@ -42,19 +44,16 @@ impl Weight { } } -pub fn weight_editor( - weight: Option>, - on_update: OnUpdate, -) -> TextEntry> +pub fn weight_editor(weight: Option, on_update: OnUpdate) -> TextEntry where - OnUpdate: Fn(si::Kilogram) + 'static, + OnUpdate: Fn(Weight) + 'static, { TextEntry::new( "0 kg", weight, - |val: &si::Kilogram| val.to_string(), + |val: &Weight| val.format(FormatOption::Abbreviated), move |v: &str| { - let new_weight = v.parse::().map(|w| w * si::KG).map_err(|_| ParseError); + let new_weight = Weight::parse(v); match new_weight { Ok(w) => { on_update(w); diff --git a/fitnesstrax/app/src/types.rs b/fitnesstrax/app/src/types.rs index 799f2d8..ac9a5da 100644 --- a/fitnesstrax/app/src/types.rs +++ b/fitnesstrax/app/src/types.rs @@ -1,4 +1,8 @@ -use chrono::{Duration, Local, NaiveDate}; +use chrono::{Local, NaiveDate}; +use dimensioned::si; + +#[derive(Clone, Debug)] +pub struct ParseError; // This interval doesn't feel right, either. The idea that I have a specific interval type for just // NaiveDate is odd. This should be genericized, as should the iterator. Also, it shouldn't live @@ -12,7 +16,7 @@ pub struct DayInterval { impl Default for DayInterval { fn default() -> Self { Self { - start: (Local::now() - Duration::days(7)).date_naive(), + start: (Local::now() - chrono::Duration::days(7)).date_naive(), end: Local::now().date_naive(), } } @@ -38,10 +42,61 @@ impl Iterator for DayIterator { fn next(&mut self) -> Option { if self.current <= self.end { let val = self.current; - self.current += Duration::days(1); + self.current += chrono::Duration::days(1); Some(val) } else { None } } } + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FormatOption { + Abbreviated, + Full, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] +pub struct Weight(si::Kilogram); + +impl Weight { + pub fn format(&self, option: FormatOption) -> String { + match option { + FormatOption::Abbreviated => format!("{} kg", self.0.value_unsafe), + FormatOption::Full => format!("{} kilograms", self.0.value_unsafe), + } + } + + pub fn parse(s: &str) -> Result { + s.parse::() + .map(|w| Weight(w * si::KG)) + .map_err(|_| ParseError) + } +} + +impl std::ops::Add for Weight { + type Output = Weight; + fn add(self, rside: Self) -> Self::Output { + Self::Output::from(self.0 + rside.0) + } +} + +impl std::ops::Sub for Weight { + type Output = Weight; + fn sub(self, rside: Self) -> Self::Output { + Self::Output::from(self.0 - rside.0) + } +} + +impl std::ops::Deref for Weight { + type Target = si::Kilogram; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From> for Weight { + fn from(value: si::Kilogram) -> Self { + Self(value) + } +} diff --git a/fitnesstrax/app/src/view_models/day_detail.rs b/fitnesstrax/app/src/view_models/day_detail.rs index f32e75f..98ed4fb 100644 --- a/fitnesstrax/app/src/view_models/day_detail.rs +++ b/fitnesstrax/app/src/view_models/day_detail.rs @@ -14,8 +14,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . */ -use crate::app::App; -use dimensioned::si; +use crate::{app::App, types::Weight}; use emseries::{Record, RecordId, Recordable}; use ft_core::TraxRecord; use std::{ @@ -125,20 +124,22 @@ impl DayDetailViewModel { } } - pub fn weight(&self) -> Option> { - (*self.weight.read().unwrap()).as_ref().map(|w| w.weight) + pub fn weight(&self) -> Option { + (*self.weight.read().unwrap()) + .as_ref() + .map(|w| Weight::from(w.weight)) } - pub fn set_weight(&self, new_weight: si::Kilogram) { + pub fn set_weight(&self, new_weight: Weight) { let mut record = self.weight.write().unwrap(); let new_record = match *record { Some(ref rstate) => rstate.clone().with_value(ft_core::Weight { date: self.date, - weight: new_weight, + weight: *new_weight, }), None => RecordState::New(ft_core::Weight { date: self.date, - weight: new_weight, + weight: *new_weight, }), }; *record = Some(new_record); -- 2.44.1 From dcd6301bb99bec54aca4b9b568989d253ca36671 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 30 Jan 2024 10:09:50 -0500 Subject: [PATCH 2/5] Introduce structures for formatting and parsing Duration and Distance values These aren't in use yet. --- fitnesstrax/app/src/types.rs | 117 +++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/fitnesstrax/app/src/types.rs b/fitnesstrax/app/src/types.rs index ac9a5da..4af25c1 100644 --- a/fitnesstrax/app/src/types.rs +++ b/fitnesstrax/app/src/types.rs @@ -100,3 +100,120 @@ impl From> for Weight { Self(value) } } + +#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] +pub struct Distance { + value: si::Meter, +} + +impl Distance { + pub fn format(&self, option: FormatOption) -> String { + match option { + FormatOption::Abbreviated => format!("{} km", self.value.value_unsafe / 1000.), + FormatOption::Full => format!("{} kilometers", self.value.value_unsafe / 1000.), + } + } + + pub fn parse(s: &str) -> Result { + let digits = take_digits(s.to_owned()); + let value = digits.parse::().map_err(|_| ParseError)?; + Ok(Distance { + value: value * 1000. * si::M, + }) + } +} + +impl std::ops::Add for Distance { + type Output = Distance; + fn add(self, rside: Self) -> Self::Output { + Self::Output::from(self.value + rside.value) + } +} + +impl std::ops::Sub for Distance { + type Output = Distance; + fn sub(self, rside: Self) -> Self::Output { + Self::Output::from(self.value - rside.value) + } +} + +impl std::ops::Deref for Distance { + type Target = si::Meter; + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl From> for Distance { + fn from(value: si::Meter) -> Self { + Self { value } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] +pub struct Duration { + value: si::Second, +} + +impl Duration { + pub fn format(&self, option: FormatOption) -> String { + let (hours, minutes) = self.hours_and_minutes(); + let (h, m) = match option { + FormatOption::Abbreviated => ("h", "m"), + FormatOption::Full => ("hours", "minutes"), + }; + if hours > 0 { + format!("{}{} {}{}", hours, h, minutes, m) + } else { + format!("{}{}", minutes, m) + } + } + + pub fn parse(s: &str) -> Result { + let digits = take_digits(s.to_owned()); + let value = digits.parse::().map_err(|_| ParseError)?; + Ok(Duration { + value: value * 60. * si::S, + }) + } + + fn hours_and_minutes(&self) -> (i64, i64) { + let minutes: i64 = (self.value.value_unsafe / 60.).round() as i64; + let hours: i64 = minutes / 60; + let minutes = minutes - (hours * 60); + (hours, minutes) + } +} + +impl std::ops::Add for Duration { + type Output = Duration; + fn add(self, rside: Self) -> Self::Output { + Self::Output::from(self.value + rside.value) + } +} + +impl std::ops::Sub for Duration { + type Output = Duration; + fn sub(self, rside: Self) -> Self::Output { + Self::Output::from(self.value - rside.value) + } +} + +impl std::ops::Deref for Duration { + type Target = si::Second; + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl From> for Duration { + fn from(value: si::Second) -> Self { + Self { value } + } +} + +fn take_digits(s: String) -> String { + s.chars() + .take_while(|t| t.is_ascii_digit()) + .collect::() +} -- 2.44.1 From 7d14308def0db5fc95adf7e7cc22a3b25584341c Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Wed, 31 Jan 2024 08:10:46 -0500 Subject: [PATCH 3/5] Create automated testing for weight, duration, and distance formatters --- fitnesstrax/app/src/types.rs | 110 ++++++++++++++++++++++++++--------- 1 file changed, 81 insertions(+), 29 deletions(-) diff --git a/fitnesstrax/app/src/types.rs b/fitnesstrax/app/src/types.rs index 4af25c1..f017596 100644 --- a/fitnesstrax/app/src/types.rs +++ b/fitnesstrax/app/src/types.rs @@ -1,7 +1,7 @@ use chrono::{Local, NaiveDate}; use dimensioned::si; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct ParseError; // This interval doesn't feel right, either. The idea that I have a specific interval type for just @@ -102,65 +102,58 @@ impl From> for Weight { } #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] -pub struct Distance { - value: si::Meter, -} +pub struct Distance(si::Meter); impl Distance { pub fn format(&self, option: FormatOption) -> String { match option { - FormatOption::Abbreviated => format!("{} km", self.value.value_unsafe / 1000.), - FormatOption::Full => format!("{} kilometers", self.value.value_unsafe / 1000.), + FormatOption::Abbreviated => format!("{} km", self.0.value_unsafe / 1000.), + FormatOption::Full => format!("{} kilometers", self.0.value_unsafe / 1000.), } } pub fn parse(s: &str) -> Result { - let digits = take_digits(s.to_owned()); - let value = digits.parse::().map_err(|_| ParseError)?; - Ok(Distance { - value: value * 1000. * si::M, - }) + let value = s.parse::().map_err(|_| ParseError)?; + Ok(Distance(value * 1000. * si::M)) } } impl std::ops::Add for Distance { type Output = Distance; fn add(self, rside: Self) -> Self::Output { - Self::Output::from(self.value + rside.value) + Self::Output::from(self.0 + rside.0) } } impl std::ops::Sub for Distance { type Output = Distance; fn sub(self, rside: Self) -> Self::Output { - Self::Output::from(self.value - rside.value) + Self::Output::from(self.0 - rside.0) } } impl std::ops::Deref for Distance { type Target = si::Meter; fn deref(&self) -> &Self::Target { - &self.value + &self.0 } } impl From> for Distance { fn from(value: si::Meter) -> Self { - Self { value } + Self(value) } } #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] -pub struct Duration { - value: si::Second, -} +pub struct Duration(si::Second); impl Duration { pub fn format(&self, option: FormatOption) -> String { let (hours, minutes) = self.hours_and_minutes(); let (h, m) = match option { FormatOption::Abbreviated => ("h", "m"), - FormatOption::Full => ("hours", "minutes"), + FormatOption::Full => (" hours", " minutes"), }; if hours > 0 { format!("{}{} {}{}", hours, h, minutes, m) @@ -170,15 +163,12 @@ impl Duration { } pub fn parse(s: &str) -> Result { - let digits = take_digits(s.to_owned()); - let value = digits.parse::().map_err(|_| ParseError)?; - Ok(Duration { - value: value * 60. * si::S, - }) + let value = s.parse::().map_err(|_| ParseError)?; + Ok(Duration(value * 60. * si::S)) } fn hours_and_minutes(&self) -> (i64, i64) { - let minutes: i64 = (self.value.value_unsafe / 60.).round() as i64; + let minutes: i64 = (self.0.value_unsafe / 60.).round() as i64; let hours: i64 = minutes / 60; let minutes = minutes - (hours * 60); (hours, minutes) @@ -188,32 +178,94 @@ impl Duration { impl std::ops::Add for Duration { type Output = Duration; fn add(self, rside: Self) -> Self::Output { - Self::Output::from(self.value + rside.value) + Self::Output::from(self.0 + rside.0) } } impl std::ops::Sub for Duration { type Output = Duration; fn sub(self, rside: Self) -> Self::Output { - Self::Output::from(self.value - rside.value) + Self::Output::from(self.0 - rside.0) } } impl std::ops::Deref for Duration { type Target = si::Second; fn deref(&self) -> &Self::Target { - &self.value + &self.0 } } impl From> for Duration { fn from(value: si::Second) -> Self { - Self { value } + Self(value) } } +/* fn take_digits(s: String) -> String { s.chars() .take_while(|t| t.is_ascii_digit()) .collect::() } +*/ + +#[cfg(test)] +mod test { + use super::*; + use dimensioned::si; + + #[test] + fn it_parses_weight_values() { + assert_eq!(Weight::parse("15.3"), Ok(Weight(15.3 * si::KG))); + assert_eq!(Weight::parse("15.ab"), Err(ParseError)); + } + + #[test] + fn it_formats_weight_values() { + assert_eq!( + Weight::from(15.3 * si::KG).format(FormatOption::Abbreviated), + "15.3 kg" + ); + assert_eq!( + Weight::from(15.3 * si::KG).format(FormatOption::Full), + "15.3 kilograms" + ); + } + + #[test] + fn it_parses_distance_values() { + assert_eq!(Distance::parse("70"), Ok(Distance(70000. * si::M))); + assert_eq!(Distance::parse("15.ab"), Err(ParseError)); + } + + #[test] + fn it_formats_distance_values() { + assert_eq!( + Distance::from(70000. * si::M).format(FormatOption::Abbreviated), + "70 km" + ); + assert_eq!( + Distance::from(70000. * si::M).format(FormatOption::Full), + "70 kilometers" + ); + } + + #[test] + fn it_parses_duration_values() { + assert_eq!(Duration::parse("70"), Ok(Duration(4200. * si::S))); + assert_eq!(Duration::parse("15.ab"), Err(ParseError)); + } + + #[test] + fn it_formats_duration_values() { + assert_eq!( + Duration::from(4200. * si::S).format(FormatOption::Abbreviated), + "1h 10m" + ); + assert_eq!( + Duration::from(4200. * si::S).format(FormatOption::Full), + "1 hours 10 minutes" + ); + } +} -- 2.44.1 From 2cbd539bf48811ebd118ad909b18c52a30df710f Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Wed, 31 Jan 2024 08:27:42 -0500 Subject: [PATCH 4/5] Add a type for managing Time of Day values --- fitnesstrax/app/src/types.rs | 83 ++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/fitnesstrax/app/src/types.rs b/fitnesstrax/app/src/types.rs index f017596..09a7c71 100644 --- a/fitnesstrax/app/src/types.rs +++ b/fitnesstrax/app/src/types.rs @@ -56,6 +56,66 @@ pub enum FormatOption { Full, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Time(chrono::NaiveTime); + +impl Time { + fn format(&self, option: FormatOption) -> String { + match option { + FormatOption::Abbreviated => self.0.format("%H:%M"), + FormatOption::Full => self.0.format("%H:%M:%S"), + } + .to_string() + } + + fn parse(s: &str) -> Result { + let parts = s + .split(':') + .map(|part| part.parse::().map_err(|_| ParseError)) + .collect::, ParseError>>()?; + match parts.len() { + 0 => Err(ParseError), + 1 => Err(ParseError), + 2 => Ok(Time( + chrono::NaiveTime::from_hms_opt(parts[0], parts[1], 0).unwrap(), + )), + 3 => Ok(Time( + chrono::NaiveTime::from_hms_opt(parts[0], parts[1], parts[2]).unwrap(), + )), + _ => Err(ParseError), + } + } +} + +/* +impl std::ops::Add for Time { + type Output = Time; + fn add(self, rside: chrono::Duration) -> Self::Output { + Self::Output::from(self.0 + rside) + } +} + +impl std::ops::Sub for Time { + type Output = Time; + fn sub(self, rside: chrono::Duration) -> Self::Output { + Self::Output::from(self.0 - rside) + } +} +*/ + +impl std::ops::Deref for Time { + type Target = chrono::NaiveTime; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Time { + fn from(value: chrono::NaiveTime) -> Self { + Self(value) + } +} + #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] pub struct Weight(si::Kilogram); @@ -268,4 +328,27 @@ mod test { "1 hours 10 minutes" ); } + + #[test] + fn it_parses_time_values() { + assert_eq!( + Time::parse("13:25"), + Ok(Time::from( + chrono::NaiveTime::from_hms_opt(13, 25, 0).unwrap() + )), + ); + assert_eq!( + Time::parse("13:25:50"), + Ok(Time::from( + chrono::NaiveTime::from_hms_opt(13, 25, 50).unwrap() + )), + ); + } + + #[test] + fn it_formats_time_values() { + let time = Time::from(chrono::NaiveTime::from_hms_opt(13, 25, 50).unwrap()); + assert_eq!(time.format(FormatOption::Abbreviated), "13:25".to_owned()); + assert_eq!(time.format(FormatOption::Full), "13:25:50".to_owned()); + } } -- 2.44.1 From 55c1a6372fd35a466872c3ae2f0d36f2fe66d556 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Wed, 31 Jan 2024 08:38:17 -0500 Subject: [PATCH 5/5] Rename the formatters --- fitnesstrax/app/src/components/weight.rs | 17 ++- fitnesstrax/app/src/types.rs | 133 +++++++++--------- fitnesstrax/app/src/view_models/day_detail.rs | 8 +- 3 files changed, 77 insertions(+), 81 deletions(-) diff --git a/fitnesstrax/app/src/components/weight.rs b/fitnesstrax/app/src/components/weight.rs index 76036c0..2afc259 100644 --- a/fitnesstrax/app/src/components/weight.rs +++ b/fitnesstrax/app/src/components/weight.rs @@ -1,5 +1,5 @@ /* -Copyright 2023, Savanni D'Gerinel +Copyright 2023-2024, Savanni D'Gerinel This file is part of FitnessTrax. @@ -16,7 +16,7 @@ You should have received a copy of the GNU General Public License along with Fit use crate::{ components::TextEntry, - types::{FormatOption, Weight}, + types::{FormatOption, WeightFormatter}, }; use gtk::prelude::*; @@ -25,7 +25,7 @@ pub struct WeightLabel { } impl WeightLabel { - pub fn new(weight: Option) -> Self { + pub fn new(weight: Option) -> Self { let label = gtk::Label::builder() .css_classes(["card", "weight-view"]) .can_focus(true) @@ -44,16 +44,19 @@ impl WeightLabel { } } -pub fn weight_editor(weight: Option, on_update: OnUpdate) -> TextEntry +pub fn weight_editor( + weight: Option, + on_update: OnUpdate, +) -> TextEntry where - OnUpdate: Fn(Weight) + 'static, + OnUpdate: Fn(WeightFormatter) + 'static, { TextEntry::new( "0 kg", weight, - |val: &Weight| val.format(FormatOption::Abbreviated), + |val: &WeightFormatter| val.format(FormatOption::Abbreviated), move |v: &str| { - let new_weight = Weight::parse(v); + let new_weight = WeightFormatter::parse(v); match new_weight { Ok(w) => { on_update(w); diff --git a/fitnesstrax/app/src/types.rs b/fitnesstrax/app/src/types.rs index 09a7c71..0411596 100644 --- a/fitnesstrax/app/src/types.rs +++ b/fitnesstrax/app/src/types.rs @@ -57,9 +57,9 @@ pub enum FormatOption { } #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Time(chrono::NaiveTime); +pub struct TimeFormatter(chrono::NaiveTime); -impl Time { +impl TimeFormatter { fn format(&self, option: FormatOption) -> String { match option { FormatOption::Abbreviated => self.0.format("%H:%M"), @@ -68,7 +68,7 @@ impl Time { .to_string() } - fn parse(s: &str) -> Result { + fn parse(s: &str) -> Result { let parts = s .split(':') .map(|part| part.parse::().map_err(|_| ParseError)) @@ -76,10 +76,10 @@ impl Time { match parts.len() { 0 => Err(ParseError), 1 => Err(ParseError), - 2 => Ok(Time( + 2 => Ok(TimeFormatter( chrono::NaiveTime::from_hms_opt(parts[0], parts[1], 0).unwrap(), )), - 3 => Ok(Time( + 3 => Ok(TimeFormatter( chrono::NaiveTime::from_hms_opt(parts[0], parts[1], parts[2]).unwrap(), )), _ => Err(ParseError), @@ -87,39 +87,23 @@ impl Time { } } -/* -impl std::ops::Add for Time { - type Output = Time; - fn add(self, rside: chrono::Duration) -> Self::Output { - Self::Output::from(self.0 + rside) - } -} - -impl std::ops::Sub for Time { - type Output = Time; - fn sub(self, rside: chrono::Duration) -> Self::Output { - Self::Output::from(self.0 - rside) - } -} -*/ - -impl std::ops::Deref for Time { +impl std::ops::Deref for TimeFormatter { type Target = chrono::NaiveTime; fn deref(&self) -> &Self::Target { &self.0 } } -impl From for Time { +impl From for TimeFormatter { fn from(value: chrono::NaiveTime) -> Self { Self(value) } } #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] -pub struct Weight(si::Kilogram); +pub struct WeightFormatter(si::Kilogram); -impl Weight { +impl WeightFormatter { pub fn format(&self, option: FormatOption) -> String { match option { FormatOption::Abbreviated => format!("{} kg", self.0.value_unsafe), @@ -127,44 +111,44 @@ impl Weight { } } - pub fn parse(s: &str) -> Result { + pub fn parse(s: &str) -> Result { s.parse::() - .map(|w| Weight(w * si::KG)) + .map(|w| WeightFormatter(w * si::KG)) .map_err(|_| ParseError) } } -impl std::ops::Add for Weight { - type Output = Weight; +impl std::ops::Add for WeightFormatter { + type Output = WeightFormatter; fn add(self, rside: Self) -> Self::Output { Self::Output::from(self.0 + rside.0) } } -impl std::ops::Sub for Weight { - type Output = Weight; +impl std::ops::Sub for WeightFormatter { + type Output = WeightFormatter; fn sub(self, rside: Self) -> Self::Output { Self::Output::from(self.0 - rside.0) } } -impl std::ops::Deref for Weight { +impl std::ops::Deref for WeightFormatter { type Target = si::Kilogram; fn deref(&self) -> &Self::Target { &self.0 } } -impl From> for Weight { +impl From> for WeightFormatter { fn from(value: si::Kilogram) -> Self { Self(value) } } #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] -pub struct Distance(si::Meter); +pub struct DistanceFormatter(si::Meter); -impl Distance { +impl DistanceFormatter { pub fn format(&self, option: FormatOption) -> String { match option { FormatOption::Abbreviated => format!("{} km", self.0.value_unsafe / 1000.), @@ -172,43 +156,43 @@ impl Distance { } } - pub fn parse(s: &str) -> Result { + pub fn parse(s: &str) -> Result { let value = s.parse::().map_err(|_| ParseError)?; - Ok(Distance(value * 1000. * si::M)) + Ok(DistanceFormatter(value * 1000. * si::M)) } } -impl std::ops::Add for Distance { - type Output = Distance; +impl std::ops::Add for DistanceFormatter { + type Output = DistanceFormatter; fn add(self, rside: Self) -> Self::Output { Self::Output::from(self.0 + rside.0) } } -impl std::ops::Sub for Distance { - type Output = Distance; +impl std::ops::Sub for DistanceFormatter { + type Output = DistanceFormatter; fn sub(self, rside: Self) -> Self::Output { Self::Output::from(self.0 - rside.0) } } -impl std::ops::Deref for Distance { +impl std::ops::Deref for DistanceFormatter { type Target = si::Meter; fn deref(&self) -> &Self::Target { &self.0 } } -impl From> for Distance { +impl From> for DistanceFormatter { fn from(value: si::Meter) -> Self { Self(value) } } #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] -pub struct Duration(si::Second); +pub struct DurationFormatter(si::Second); -impl Duration { +impl DurationFormatter { pub fn format(&self, option: FormatOption) -> String { let (hours, minutes) = self.hours_and_minutes(); let (h, m) = match option { @@ -222,9 +206,9 @@ impl Duration { } } - pub fn parse(s: &str) -> Result { + pub fn parse(s: &str) -> Result { let value = s.parse::().map_err(|_| ParseError)?; - Ok(Duration(value * 60. * si::S)) + Ok(DurationFormatter(value * 60. * si::S)) } fn hours_and_minutes(&self) -> (i64, i64) { @@ -235,28 +219,28 @@ impl Duration { } } -impl std::ops::Add for Duration { - type Output = Duration; +impl std::ops::Add for DurationFormatter { + type Output = DurationFormatter; fn add(self, rside: Self) -> Self::Output { Self::Output::from(self.0 + rside.0) } } -impl std::ops::Sub for Duration { - type Output = Duration; +impl std::ops::Sub for DurationFormatter { + type Output = DurationFormatter; fn sub(self, rside: Self) -> Self::Output { Self::Output::from(self.0 - rside.0) } } -impl std::ops::Deref for Duration { +impl std::ops::Deref for DurationFormatter { type Target = si::Second; fn deref(&self) -> &Self::Target { &self.0 } } -impl From> for Duration { +impl From> for DurationFormatter { fn from(value: si::Second) -> Self { Self(value) } @@ -277,54 +261,63 @@ mod test { #[test] fn it_parses_weight_values() { - assert_eq!(Weight::parse("15.3"), Ok(Weight(15.3 * si::KG))); - assert_eq!(Weight::parse("15.ab"), Err(ParseError)); + assert_eq!( + WeightFormatter::parse("15.3"), + Ok(WeightFormatter(15.3 * si::KG)) + ); + assert_eq!(WeightFormatter::parse("15.ab"), Err(ParseError)); } #[test] fn it_formats_weight_values() { assert_eq!( - Weight::from(15.3 * si::KG).format(FormatOption::Abbreviated), + WeightFormatter::from(15.3 * si::KG).format(FormatOption::Abbreviated), "15.3 kg" ); assert_eq!( - Weight::from(15.3 * si::KG).format(FormatOption::Full), + WeightFormatter::from(15.3 * si::KG).format(FormatOption::Full), "15.3 kilograms" ); } #[test] fn it_parses_distance_values() { - assert_eq!(Distance::parse("70"), Ok(Distance(70000. * si::M))); - assert_eq!(Distance::parse("15.ab"), Err(ParseError)); + assert_eq!( + DistanceFormatter::parse("70"), + Ok(DistanceFormatter(70000. * si::M)) + ); + assert_eq!(DistanceFormatter::parse("15.ab"), Err(ParseError)); } #[test] fn it_formats_distance_values() { assert_eq!( - Distance::from(70000. * si::M).format(FormatOption::Abbreviated), + DistanceFormatter::from(70000. * si::M).format(FormatOption::Abbreviated), "70 km" ); assert_eq!( - Distance::from(70000. * si::M).format(FormatOption::Full), + DistanceFormatter::from(70000. * si::M).format(FormatOption::Full), "70 kilometers" ); } #[test] fn it_parses_duration_values() { - assert_eq!(Duration::parse("70"), Ok(Duration(4200. * si::S))); - assert_eq!(Duration::parse("15.ab"), Err(ParseError)); + assert_eq!( + DurationFormatter::parse("70"), + Ok(DurationFormatter(4200. * si::S)) + ); + assert_eq!(DurationFormatter::parse("15.ab"), Err(ParseError)); } #[test] fn it_formats_duration_values() { assert_eq!( - Duration::from(4200. * si::S).format(FormatOption::Abbreviated), + DurationFormatter::from(4200. * si::S).format(FormatOption::Abbreviated), "1h 10m" ); assert_eq!( - Duration::from(4200. * si::S).format(FormatOption::Full), + DurationFormatter::from(4200. * si::S).format(FormatOption::Full), "1 hours 10 minutes" ); } @@ -332,14 +325,14 @@ mod test { #[test] fn it_parses_time_values() { assert_eq!( - Time::parse("13:25"), - Ok(Time::from( + TimeFormatter::parse("13:25"), + Ok(TimeFormatter::from( chrono::NaiveTime::from_hms_opt(13, 25, 0).unwrap() )), ); assert_eq!( - Time::parse("13:25:50"), - Ok(Time::from( + TimeFormatter::parse("13:25:50"), + Ok(TimeFormatter::from( chrono::NaiveTime::from_hms_opt(13, 25, 50).unwrap() )), ); @@ -347,7 +340,7 @@ mod test { #[test] fn it_formats_time_values() { - let time = Time::from(chrono::NaiveTime::from_hms_opt(13, 25, 50).unwrap()); + let time = TimeFormatter::from(chrono::NaiveTime::from_hms_opt(13, 25, 50).unwrap()); assert_eq!(time.format(FormatOption::Abbreviated), "13:25".to_owned()); assert_eq!(time.format(FormatOption::Full), "13:25:50".to_owned()); } diff --git a/fitnesstrax/app/src/view_models/day_detail.rs b/fitnesstrax/app/src/view_models/day_detail.rs index 98ed4fb..cf8ac0e 100644 --- a/fitnesstrax/app/src/view_models/day_detail.rs +++ b/fitnesstrax/app/src/view_models/day_detail.rs @@ -14,7 +14,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . */ -use crate::{app::App, types::Weight}; +use crate::{app::App, types::WeightFormatter}; use emseries::{Record, RecordId, Recordable}; use ft_core::TraxRecord; use std::{ @@ -124,13 +124,13 @@ impl DayDetailViewModel { } } - pub fn weight(&self) -> Option { + pub fn weight(&self) -> Option { (*self.weight.read().unwrap()) .as_ref() - .map(|w| Weight::from(w.weight)) + .map(|w| WeightFormatter::from(w.weight)) } - pub fn set_weight(&self, new_weight: Weight) { + pub fn set_weight(&self, new_weight: WeightFormatter) { let mut record = self.weight.write().unwrap(); let new_record = match *record { Some(ref rstate) => rstate.clone().with_value(ft_core::Weight { -- 2.44.1