diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs
index 3143302..e78d829 100644
--- a/fitnesstrax/app/src/components/day.rs
+++ b/fitnesstrax/app/src/components/day.rs
@@ -17,10 +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, text_entry::distance_field, time_distance_summary, weight_field, ActionGroup,
- Steps, Weight,
- },
+ components::{steps_editor, time_distance_summary, weight_field, ActionGroup, Steps, Weight},
view_models::DayDetailViewModel,
};
use emseries::{Record, RecordId};
@@ -419,7 +416,7 @@ where
biking_button.connect_clicked({
let view_model = view_model.clone();
move |_| {
- let workout = view_model.new_record(RecordType::Walk);
+ let workout = view_model.new_record(RecordType::BikeRide);
add_row(workout);
}
});
diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs
index 3a5c8c1..cfa64f4 100644
--- a/fitnesstrax/app/src/components/mod.rs
+++ b/fitnesstrax/app/src/components/mod.rs
@@ -27,9 +27,7 @@ mod steps;
pub use steps::{steps_editor, Steps};
mod text_entry;
-pub use text_entry::{
- distance_field, duration_field, time_field, weight_field, ParseError, TextEntry,
-};
+pub use text_entry::{distance_field, duration_field, time_field, weight_field, TextEntry};
mod time_distance;
pub use time_distance::{time_distance_detail, time_distance_summary};
diff --git a/fitnesstrax/app/src/components/steps.rs b/fitnesstrax/app/src/components/steps.rs
index f86da50..11ba591 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::{text_entry::OnUpdate, 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 b014b33..91cafb8 100644
--- a/fitnesstrax/app/src/components/text_entry.rs
+++ b/fitnesstrax/app/src/components/text_entry.rs
@@ -14,13 +14,11 @@ 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::{Distance, Duration, FormatOption, ParseError};
use dimensioned::si;
use gtk::prelude::*;
use std::{cell::RefCell, rc::Rc};
-#[derive(Clone, Debug)]
-pub struct ParseError;
-
pub type Renderer = dyn Fn(&T) -> String;
pub type Parser = dyn Fn(&str) -> Result;
pub type OnUpdate = dyn Fn(Option);
@@ -153,47 +151,28 @@ where
)
}
-pub fn distance_field(
- value: Option>,
- on_update: OnUpdate,
-) -> TextEntry>
+pub fn distance_field(value: Option, on_update: OnUpdate) -> TextEntry
where
- OnUpdate: Fn(Option>) + 'static,
+ OnUpdate: Fn(Option) + 'static,
{
TextEntry::new(
"0 km",
value,
|v| format!("{} km", v.value_unsafe / 1000.),
- |s| {
- let digits = take_digits(s.to_owned());
- let value = digits.parse::().map_err(|_| ParseError)?;
- println!("value: {}", value);
- Ok(value * 1000. * si::M)
- },
+ Distance::parse,
on_update,
)
}
-pub fn duration_field(
- value: Option>,
- on_update: OnUpdate,
-) -> TextEntry>
+pub fn duration_field(value: Option, on_update: OnUpdate) -> TextEntry
where
- OnUpdate: Fn(Option>) + 'static,
+ OnUpdate: Fn(Option) + 'static,
{
TextEntry::new(
"0 minutes",
value,
- |v| format!("{} minutes", v.value_unsafe / 1000.),
- |s| {
- let digits = take_digits(s.to_owned());
- let value = digits.parse::().map_err(|_| ParseError)?;
- Ok(value * 60. * si::S)
- },
+ |v| v.format(FormatOption::Abbreviated),
+ |s| Duration::parse(s),
on_update,
)
}
-
-fn take_digits(s: String) -> String {
- s.chars().take_while(|t| t.is_digit(10)).collect::()
-}
diff --git a/fitnesstrax/app/src/components/time_distance.rs b/fitnesstrax/app/src/components/time_distance.rs
index f164540..0467ad0 100644
--- a/fitnesstrax/app/src/components/time_distance.rs
+++ b/fitnesstrax/app/src/components/time_distance.rs
@@ -17,28 +17,25 @@ You should have received a copy of the GNU General Public License along with Fit
// use crate::components::{EditView, ParseError, TextEntry};
// use chrono::{Local, NaiveDate};
// use dimensioned::si;
-use crate::components::{distance_field, duration_field, time_field};
+use crate::{
+ components::{distance_field, duration_field, time_field},
+ types::{Distance, Duration, FormatOption},
+};
use dimensioned::si;
use ft_core::{RecordType, TimeDistance};
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use std::{cell::RefCell, rc::Rc};
-pub fn time_distance_summary(
- distance: si::Meter,
- duration: si::Second,
-) -> Option {
- let text = match (distance > si::M, duration > si::S) {
+pub fn time_distance_summary(distance: Distance, duration: Duration) -> Option {
+ let text = match (*distance > si::M, *duration > si::S) {
(true, true) => Some(format!(
- "{} kilometers of biking in {} minutes",
- distance.value_unsafe / 1000.,
- duration.value_unsafe / 60.
+ "{} of biking in {}",
+ distance.format(FormatOption::Full),
+ duration.format(FormatOption::Full)
)),
- (true, false) => Some(format!(
- "{} kilometers of biking",
- distance.value_unsafe / 1000.
- )),
- (false, true) => Some(format!("{} seconds of biking", duration.value_unsafe / 60.)),
+ (true, false) => Some(format!("{} of biking", distance.format(FormatOption::Full))),
+ (false, true) => Some(format!("{} of biking", duration.format(FormatOption::Full))),
(false, false) => None,
};
@@ -72,7 +69,7 @@ pub fn time_distance_detail(type_: ft_core::RecordType, record: ft_core::TimeDis
.label(
record
.distance
- .map(|dist| format!("{}", dist))
+ .map(|dist| Distance::from(dist).format(FormatOption::Abbreviated))
.unwrap_or("".to_owned()),
)
.build(),
@@ -84,7 +81,7 @@ pub fn time_distance_detail(type_: ft_core::RecordType, record: ft_core::TimeDis
.label(
record
.duration
- .map(|duration| format!("{}", duration))
+ .map(|duration| Duration::from(duration).format(FormatOption::Abbreviated))
.unwrap_or("".to_owned()),
)
.build(),
@@ -173,16 +170,16 @@ impl TimeDistanceEdit {
.widget(),
);
details_row.append(
- &distance_field(workout.distance, {
+ &distance_field(workout.distance.map(Distance::from), {
let s = s.clone();
- move |d| s.update_distance(d)
+ move |d| s.update_distance(d.map(|d| *d))
})
.widget(),
);
details_row.append(
- &duration_field(workout.duration, {
+ &duration_field(workout.duration.map(Duration::from), {
let s = s.clone();
- move |d| s.update_duration(d)
+ move |d| s.update_duration(d.map(|d| *d))
})
.widget(),
);
diff --git a/fitnesstrax/app/src/types.rs b/fitnesstrax/app/src/types.rs
index 799f2d8..f48cddc 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,132 @@ 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 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)?;
+ println!("value: {}", value);
+ 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) as i64;
+ 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_digit(10)).collect::()
+}
diff --git a/fitnesstrax/app/src/view_models/day_detail.rs b/fitnesstrax/app/src/view_models/day_detail.rs
index 9681ba6..fe1bd1d 100644
--- a/fitnesstrax/app/src/view_models/day_detail.rs
+++ b/fitnesstrax/app/src/view_models/day_detail.rs
@@ -14,7 +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::app::App;
+use crate::{
+ app::App,
+ types::{Distance, Duration},
+};
use dimensioned::si;
use emseries::{Record, RecordId, Recordable};
use ft_core::{RecordType, TimeDistance, TraxRecord};
@@ -164,9 +167,9 @@ impl DayDetailViewModel {
*record = Some(new_record);
}
- pub fn biking_summary(&self) -> (si::Meter, si::Second) {
+ pub fn biking_summary(&self) -> (Distance, Duration) {
self.records.read().unwrap().iter().fold(
- (0. * si::M, 0. * si::S),
+ (Distance::default(), Duration::default()),
|(acc_distance, acc_duration), (_, record)| match record.data() {
Some(Record {
data:
@@ -176,11 +179,11 @@ impl DayDetailViewModel {
..
}) => (
distance
- .map(|distance| acc_distance + distance)
+ .map(|distance| acc_distance + Distance::from(distance))
.unwrap_or(acc_distance),
- (duration
- .map(|duration| acc_duration + duration)
- .unwrap_or(acc_duration)),
+ duration
+ .map(|duration| acc_duration + Duration::from(duration))
+ .unwrap_or(acc_duration),
),
_ => (acc_distance, acc_duration),