Invert the TraxRecord #182

Merged
savanni merged 1 commits from fitnesstrax/invert-record-structure into main 2024-02-08 23:58:37 +00:00
4 changed files with 109 additions and 138 deletions
Showing only changes of commit 2fb8728856 - Show all commits

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2023, Savanni D'Gerinel <savanni@luminescent-dreams.com> Copyright 2023-2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of FitnessTrax. This file is part of FitnessTrax.
@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with Fit
// use crate::components::{EditView, ParseError, TextEntry}; // use crate::components::{EditView, ParseError, TextEntry};
// use chrono::{Local, NaiveDate}; // use chrono::{Local, NaiveDate};
// use dimensioned::si; // use dimensioned::si;
use ft_core::{RecordType, TimeDistance}; use ft_core::TimeDistance;
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
use std::cell::RefCell; use std::cell::RefCell;
@ -44,7 +44,7 @@ glib::wrapper! {
} }
impl TimeDistanceView { impl TimeDistanceView {
pub fn new(type_: RecordType, record: TimeDistance) -> Self { pub fn new(record: TimeDistance) -> Self {
let s: Self = Object::builder().build(); let s: Self = Object::builder().build();
s.set_orientation(gtk::Orientation::Vertical); s.set_orientation(gtk::Orientation::Vertical);
s.set_hexpand(true); s.set_hexpand(true);
@ -58,12 +58,14 @@ impl TimeDistanceView {
.build(), .build(),
); );
/*
first_row.append( first_row.append(
&gtk::Label::builder() &gtk::Label::builder()
.halign(gtk::Align::Start) .halign(gtk::Align::Start)
.label(format!("{:?}", type_)) .label(format!("{:?}", type_))
.build(), .build(),
); );
*/
first_row.append( first_row.append(
&gtk::Label::builder() &gtk::Label::builder()

View File

@ -20,7 +20,7 @@ use crate::app::{ReadError, RecordProvider, WriteError};
use chrono::NaiveDate; use chrono::NaiveDate;
use dimensioned::si; use dimensioned::si;
use emseries::{Record, RecordId, Recordable}; use emseries::{Record, RecordId, Recordable};
use ft_core::{TimeDistance, TimeDistanceWorkoutType, TraxRecord}; use ft_core::{TimeDistance, TimeDistanceActivity, TraxRecord};
use std::{ use std::{
collections::HashMap, collections::HashMap,
ops::Deref, ops::Deref,
@ -48,20 +48,34 @@ impl<T: Clone + emseries::Recordable> RecordState<T> {
fn exists(&self) -> bool { fn exists(&self) -> bool {
match self { match self {
RecordState::Original(ref _r) => true, RecordState::Original(_) => true,
RecordState::New(ref _r) => true, RecordState::New(_) => true,
RecordState::Updated(ref _r) => true, RecordState::Updated(_) => true,
RecordState::Deleted(ref _r) => false, RecordState::Deleted(_) => false,
} }
} }
fn with_value(self, value: T) -> RecordState<T> { fn set_value(&mut self, value: T) {
match self { *self = match self {
RecordState::Original(r) => RecordState::Updated(Record { data: value, ..r }), RecordState::Original(r) => RecordState::Updated(Record {
id: r.id.clone(),
data: value,
}),
RecordState::New(_) => RecordState::New(value), RecordState::New(_) => RecordState::New(value),
RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..r }), RecordState::Updated(r) => RecordState::Updated(Record {
RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..r }), id: r.id.clone(),
data: value,
}),
RecordState::Deleted(r) => RecordState::Updated(Record {
id: r.id.clone(),
data: value,
}),
};
} }
fn with_value(mut self, value: T) -> RecordState<T> {
self.set_value(value);
self
} }
#[allow(unused)] #[allow(unused)]
@ -197,15 +211,16 @@ impl DayDetailViewModel {
*record = Some(new_record); *record = Some(new_record);
} }
pub fn new_time_distance(&self, type_: TimeDistanceWorkoutType) -> Record<TimeDistance> { pub fn new_time_distance(&self, activity: TimeDistanceActivity) -> Record<TimeDistance> {
let id = RecordId::default(); let id = RecordId::default();
let workout = TimeDistance { let workout = TimeDistance {
datetime: chrono::Local::now().into(), datetime: chrono::Local::now().into(),
activity,
distance: None, distance: None,
duration: None, duration: None,
comments: None, comments: None,
}; };
let tr = TraxRecord::from_time_distance(type_, workout.clone()); let tr = TraxRecord::from(workout.clone());
self.records self.records
.write() .write()
.unwrap() .unwrap()
@ -222,44 +237,19 @@ impl DayDetailViewModel {
let data = workout.data.clone(); let data = workout.data.clone();
let mut record_set = self.records.write().unwrap(); let mut record_set = self.records.write().unwrap();
if let Some(record_state) = record_set.get(&id) { record_set.entry(id).and_modify(|record_state| {
let updated_state = match **record_state { record_state.set_value(TraxRecord::TimeDistance(data));
TraxRecord::BikeRide(_) => { });
Some(record_state.clone().with_value(TraxRecord::BikeRide(data)))
}
TraxRecord::Row(_) => Some(record_state.clone().with_value(TraxRecord::Row(data))),
TraxRecord::Run(_) => Some(record_state.clone().with_value(TraxRecord::Run(data))),
TraxRecord::Swim(_) => {
Some(record_state.clone().with_value(TraxRecord::Swim(data)))
}
TraxRecord::Walk(_) => {
Some(record_state.clone().with_value(TraxRecord::Walk(data)))
}
_ => None,
};
if let Some(updated_state) = updated_state {
record_set.insert(id, updated_state);
}
}
} }
pub fn time_distance_records( pub fn time_distance_records(&self) -> Vec<Record<TimeDistance>> {
&self,
type_: TimeDistanceWorkoutType,
) -> Vec<Record<TimeDistance>> {
self.records self.records
.read() .read()
.unwrap() .unwrap()
.iter() .iter()
.filter(|(_, record)| record.exists()) .filter(|(_, record)| record.exists())
.filter(|(_, workout_state)| workout_state.is_time_distance_type(type_))
.filter_map(|(id, record_state)| match **record_state { .filter_map(|(id, record_state)| match **record_state {
TraxRecord::BikeRide(ref workout) TraxRecord::TimeDistance(ref workout) => Some(Record {
| TraxRecord::Row(ref workout)
| TraxRecord::Run(ref workout)
| TraxRecord::Swim(ref workout)
| TraxRecord::Walk(ref workout) => Some(Record {
id: id.clone(), id: id.clone(),
data: workout.clone(), data: workout.clone(),
}), }),
@ -270,12 +260,18 @@ impl DayDetailViewModel {
pub fn time_distance_summary( pub fn time_distance_summary(
&self, &self,
type_: TimeDistanceWorkoutType, activity: TimeDistanceActivity,
) -> (si::Meter<f64>, si::Second<f64>) { ) -> (si::Meter<f64>, si::Second<f64>) {
self.time_distance_records(type_).into_iter().fold( self.time_distance_records()
.into_iter()
.filter(|rec| rec.data.activity == activity)
.fold(
(0. * si::M, 0. * si::S), (0. * si::M, 0. * si::S),
|(distance, duration), workout| match (workout.data.distance, workout.data.duration) { |(distance, duration), workout| match (workout.data.distance, workout.data.duration)
(Some(distance_), Some(duration_)) => (distance + distance_, duration + duration_), {
(Some(distance_), Some(duration_)) => {
(distance + distance_, duration + duration_)
}
(Some(distance_), None) => (distance + distance_, duration), (Some(distance_), None) => (distance + distance_, duration),
(None, Some(duration_)) => (distance, duration + duration_), (None, Some(duration_)) => (distance, duration + duration_),
(None, None) => (distance, duration), (None, None) => (distance, duration),
@ -506,8 +502,9 @@ mod test {
}, },
Record { Record {
id: RecordId::default(), id: RecordId::default(),
data: TraxRecord::BikeRide(ft_core::TimeDistance { data: TraxRecord::TimeDistance(ft_core::TimeDistance {
datetime: oct_13_am, datetime: oct_13_am.clone(),
activity: TimeDistanceActivity::BikeRide,
distance: Some(15000. * si::M), distance: Some(15000. * si::M),
duration: Some(3600. * si::S), duration: Some(3600. * si::S),
comments: Some("somecomments present".to_owned()), comments: Some("somecomments present".to_owned()),
@ -557,11 +554,11 @@ mod test {
async fn it_can_construct_new_records() { async fn it_can_construct_new_records() {
let (view_model, provider) = create_empty_view_model().await; let (view_model, provider) = create_empty_view_model().await;
assert_eq!( assert_eq!(
view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide), view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
(0. * si::M, 0. * si::S) (0. * si::M, 0. * si::S)
); );
let mut record = view_model.new_time_distance(TimeDistanceWorkoutType::BikeRide); let mut record = view_model.new_time_distance(TimeDistanceActivity::BikeRide);
record.data.duration = Some(60. * si::S); record.data.duration = Some(60. * si::S);
view_model.async_save().await; view_model.async_save().await;
@ -574,24 +571,24 @@ mod test {
async fn it_can_update_a_new_record_before_saving() { async fn it_can_update_a_new_record_before_saving() {
let (view_model, provider) = create_empty_view_model().await; let (view_model, provider) = create_empty_view_model().await;
assert_eq!( assert_eq!(
view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide), view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
(0. * si::M, 0. * si::S) (0. * si::M, 0. * si::S)
); );
let mut record = view_model.new_time_distance(TimeDistanceWorkoutType::BikeRide); let mut record = view_model.new_time_distance(TimeDistanceActivity::BikeRide);
record.data.duration = Some(60. * si::S); record.data.duration = Some(60. * si::S);
view_model.update_time_distance(record.clone()); view_model.update_time_distance(record.clone());
let record = Record { let record = Record {
id: record.id, id: record.id,
data: TraxRecord::BikeRide(record.data), data: TraxRecord::TimeDistance(record.data),
}; };
assert_eq!(view_model.get_record(&record.id), Some(record)); assert_eq!(view_model.get_record(&record.id), Some(record));
assert_eq!( assert_eq!(
view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide), view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
(0. * si::M, 60. * si::S) (0. * si::M, 60. * si::S)
); );
assert_eq!( assert_eq!(
view_model.time_distance_summary(TimeDistanceWorkoutType::Run), view_model.time_distance_summary(TimeDistanceActivity::Running),
(0. * si::M, 0. * si::S) (0. * si::M, 0. * si::S)
); );
view_model.async_save().await; view_model.async_save().await;
@ -604,11 +601,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn it_can_update_an_existing_record() { async fn it_can_update_an_existing_record() {
let (view_model, provider) = create_view_model().await; let (view_model, provider) = create_view_model().await;
let mut workout = view_model let mut workout = view_model.time_distance_records().first().cloned().unwrap();
.time_distance_records(TimeDistanceWorkoutType::BikeRide)
.first()
.cloned()
.unwrap();
println!("found record: {:?}", workout); println!("found record: {:?}", workout);
@ -616,7 +609,7 @@ mod test {
view_model.update_time_distance(workout.clone()); view_model.update_time_distance(workout.clone());
assert_eq!( assert_eq!(
view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide), view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
(15000. * si::M, 1800. * si::S) (15000. * si::M, 1800. * si::S)
); );
@ -631,11 +624,11 @@ mod test {
async fn it_can_remove_a_new_record() { async fn it_can_remove_a_new_record() {
let (view_model, provider) = create_empty_view_model().await; let (view_model, provider) = create_empty_view_model().await;
assert_eq!( assert_eq!(
view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide), view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
(0. * si::M, 0. * si::S) (0. * si::M, 0. * si::S)
); );
let record = view_model.new_time_distance(TimeDistanceWorkoutType::BikeRide); let record = view_model.new_time_distance(TimeDistanceActivity::BikeRide);
view_model.remove_record(record.id); view_model.remove_record(record.id);
view_model.save(); view_model.save();
@ -647,15 +640,11 @@ mod test {
#[tokio::test] #[tokio::test]
async fn it_can_delete_an_existing_record() { async fn it_can_delete_an_existing_record() {
let (view_model, provider) = create_view_model().await; let (view_model, provider) = create_view_model().await;
let workout = view_model let mut workout = view_model.time_distance_records().first().cloned().unwrap();
.time_distance_records(TimeDistanceWorkoutType::BikeRide)
.first()
.cloned()
.unwrap();
view_model.remove_record(workout.id); view_model.remove_record(workout.id);
assert_eq!( assert_eq!(
view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide), view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
(0. * si::M, 0. * si::S) (0. * si::M, 0. * si::S)
); );
view_model.async_save().await; view_model.async_save().await;

View File

@ -1,4 +1,4 @@
mod legacy; mod legacy;
mod types; mod types;
pub use types::{RecordType, Steps, TimeDistance, TimeDistanceWorkoutType, TraxRecord, Weight}; pub use types::{Steps, TimeDistance, TimeDistanceActivity, TraxRecord, Weight};

View File

@ -33,6 +33,15 @@ impl Recordable for Steps {
} }
} }
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum TimeDistanceActivity {
BikeRide,
Running,
Rowing,
Swimming,
Walking,
}
/// TimeDistance represents workouts characterized by a duration and a distance travelled. These /// TimeDistance represents workouts characterized by a duration and a distance travelled. These
/// sorts of workouts can occur many times a day, depending on how one records things. I might /// sorts of workouts can occur many times a day, depending on how one records things. I might
/// record a single 30-km workout if I go on a long-distanec ride. Or I might record multiple 5km /// record a single 30-km workout if I go on a long-distanec ride. Or I might record multiple 5km
@ -48,6 +57,8 @@ pub struct TimeDistance {
/// in the database, but we can still get a Naive Date from the DateTime, which will still read /// in the database, but we can still get a Naive Date from the DateTime, which will still read
/// as the original day. /// as the original day.
pub datetime: DateTime<FixedOffset>, pub datetime: DateTime<FixedOffset>,
/// The activity
pub activity: TimeDistanceActivity,
/// The distance travelled. This is optional because such a workout makes sense even without /// The distance travelled. This is optional because such a workout makes sense even without
/// the distance. /// the distance.
pub distance: Option<si::Meter<f64>>, pub distance: Option<si::Meter<f64>>,
@ -85,61 +96,15 @@ impl Recordable for Weight {
} }
} }
#[derive(Clone, Debug, PartialEq)]
pub enum RecordType {
BikeRide,
Row,
Run,
Steps,
Swim,
Walk,
Weight,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TimeDistanceWorkoutType {
BikeRide,
Row,
Run,
Swim,
Walk,
}
/// The unified data structure for all records that are part of the app. /// The unified data structure for all records that are part of the app.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum TraxRecord { pub enum TraxRecord {
BikeRide(TimeDistance), TimeDistance(TimeDistance),
Row(TimeDistance),
Run(TimeDistance),
Steps(Steps), Steps(Steps),
Swim(TimeDistance),
Walk(TimeDistance),
Weight(Weight), Weight(Weight),
} }
impl TraxRecord { impl TraxRecord {
pub fn from_time_distance(type_: TimeDistanceWorkoutType, workout: TimeDistance) -> Self {
match type_ {
TimeDistanceWorkoutType::BikeRide => Self::BikeRide(workout),
TimeDistanceWorkoutType::Run => Self::Run(workout),
TimeDistanceWorkoutType::Row => Self::Row(workout),
TimeDistanceWorkoutType::Swim => Self::Swim(workout),
TimeDistanceWorkoutType::Walk => Self::Walk(workout),
}
}
pub fn workout_type(&self) -> RecordType {
match self {
TraxRecord::BikeRide(_) => RecordType::BikeRide,
TraxRecord::Row(_) => RecordType::Row,
TraxRecord::Run(_) => RecordType::Run,
TraxRecord::Steps(_) => RecordType::Steps,
TraxRecord::Swim(_) => RecordType::Swim,
TraxRecord::Walk(_) => RecordType::Walk,
TraxRecord::Weight(_) => RecordType::Weight,
}
}
pub fn is_weight(&self) -> bool { pub fn is_weight(&self) -> bool {
matches!(self, TraxRecord::Weight(_)) matches!(self, TraxRecord::Weight(_))
} }
@ -151,15 +116,27 @@ impl TraxRecord {
pub fn is_time_distance(&self) -> bool { pub fn is_time_distance(&self) -> bool {
matches!( matches!(
self, self,
TraxRecord::BikeRide(_) TraxRecord::TimeDistance(TimeDistance {
| TraxRecord::Row(_) activity: TimeDistanceActivity::BikeRide,
| TraxRecord::Run(_) ..
| TraxRecord::Swim(_) }) | TraxRecord::TimeDistance(TimeDistance {
| TraxRecord::Walk(_) activity: TimeDistanceActivity::Running,
..
}) | TraxRecord::TimeDistance(TimeDistance {
activity: TimeDistanceActivity::Rowing,
..
}) | TraxRecord::TimeDistance(TimeDistance {
activity: TimeDistanceActivity::Swimming,
..
}) | TraxRecord::TimeDistance(TimeDistance {
activity: TimeDistanceActivity::Walking,
..
})
) )
} }
pub fn is_time_distance_type(&self, type_: TimeDistanceWorkoutType) -> bool { /*
pub fn is_time_distance_type(&self, type_: TimeDistanceActivity) -> bool {
match type_ { match type_ {
TimeDistanceWorkoutType::BikeRide => matches!(self, TraxRecord::BikeRide(_)), TimeDistanceWorkoutType::BikeRide => matches!(self, TraxRecord::BikeRide(_)),
TimeDistanceWorkoutType::Row => matches!(self, TraxRecord::Row(_)), TimeDistanceWorkoutType::Row => matches!(self, TraxRecord::Row(_)),
@ -168,17 +145,14 @@ impl TraxRecord {
TimeDistanceWorkoutType::Walk => matches!(self, TraxRecord::Walk(_)), TimeDistanceWorkoutType::Walk => matches!(self, TraxRecord::Walk(_)),
} }
} }
*/
} }
impl Recordable for TraxRecord { impl Recordable for TraxRecord {
fn timestamp(&self) -> Timestamp { fn timestamp(&self) -> Timestamp {
match self { match self {
TraxRecord::BikeRide(rec) => Timestamp::DateTime(rec.datetime), TraxRecord::TimeDistance(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Row(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Run(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Steps(rec) => rec.timestamp(), TraxRecord::Steps(rec) => rec.timestamp(),
TraxRecord::Swim(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Walk(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Weight(rec) => rec.timestamp(), TraxRecord::Weight(rec) => rec.timestamp(),
} }
} }
@ -188,6 +162,12 @@ impl Recordable for TraxRecord {
} }
} }
impl From<TimeDistance> for TraxRecord {
fn from(td: TimeDistance) -> Self {
Self::TimeDistance(td)
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;