/* Copyright 2024, Savanni D'Gerinel This file is part of FitnessTrax. FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 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::{ReadError, RecordProvider, WriteError}; use chrono::NaiveDate; use dimensioned::si; use emseries::{Record, RecordId, Recordable}; use ft_core::{RecordType, TimeDistance, TimeDistanceWorkoutType, TraxRecord}; use std::{ collections::HashMap, ops::Deref, sync::{Arc, RwLock}, }; #[derive(Clone, Debug)] enum RecordState { Original(Record), New(T), Updated(Record), Deleted(Record), } impl RecordState { #[allow(unused)] fn id(&self) -> Option<&RecordId> { match self { RecordState::Original(ref r) => Some(&r.id), RecordState::New(ref r) => None, RecordState::Updated(ref r) => Some(&r.id), RecordState::Deleted(ref r) => Some(&r.id), } } fn exists(&self) -> bool { match self { RecordState::Original(ref r) => true, RecordState::New(ref r) => true, RecordState::Updated(ref r) => true, RecordState::Deleted(ref r) => false, } } fn with_value(self, value: T) -> RecordState { match self { RecordState::Original(r) => RecordState::Updated(Record { data: value, ..r }), RecordState::New(_) => RecordState::New(value), RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..r }), RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..r }), } } #[allow(unused)] fn with_delete(self) -> Option> { match self { RecordState::Original(r) => Some(RecordState::Deleted(r)), RecordState::New(r) => None, RecordState::Updated(r) => Some(RecordState::Deleted(r)), RecordState::Deleted(r) => Some(RecordState::Deleted(r)), } } } impl Deref for RecordState { type Target = T; fn deref(&self) -> &Self::Target { match self { RecordState::Original(ref r) => &r.data, RecordState::New(ref r) => r, RecordState::Updated(ref r) => &r.data, RecordState::Deleted(ref r) => &r.data, } } } impl std::ops::DerefMut for RecordState { fn deref_mut(&mut self) -> &mut Self::Target { match self { RecordState::Original(ref mut r) => &mut r.data, RecordState::New(ref mut r) => r, RecordState::Updated(ref mut r) => &mut r.data, RecordState::Deleted(ref mut r) => &mut r.data, } } } #[derive(Clone)] pub struct DayDetailViewModel { provider: Arc, pub date: chrono::NaiveDate, weight: Arc>>>, steps: Arc>>>, records: Arc>>>, } impl DayDetailViewModel { pub async fn new( date: chrono::NaiveDate, provider: impl RecordProvider + 'static, ) -> Result { let (weight_records, records): (Vec>, Vec>) = provider .records(date, date) .await? .into_iter() .partition(|r| r.data.is_weight()); let (step_records, records): (Vec>, Vec>) = records.into_iter().partition(|r| r.data.is_steps()); if weight_records.len() > 1 { eprintln!("warning: multiple weight records found for {}. This is unsupported and the one presented is unpredictable.", date.format("%Y-%m-%d")); } if step_records.len() > 1 { eprintln!("warning: multiple step records found for {}. This is unsupported and the one presented is unpredictable.", date.format("%Y-%m-%d")); } Ok(Self { provider: Arc::new(provider), date, weight: Arc::new(RwLock::new( weight_records .first() .and_then(|r| match r.data { TraxRecord::Weight(ref w) => Some((r.id.clone(), w.clone())), _ => None, }) .map(|(id, w)| RecordState::Original(Record { id, data: w })), )), steps: Arc::new(RwLock::new( step_records .first() .and_then(|r| match r.data { TraxRecord::Steps(ref w) => Some((r.id.clone(), w.clone())), _ => None, }) .map(|(id, w)| RecordState::Original(Record { id, data: w })), )), records: Arc::new(RwLock::new( records .into_iter() .map(|r| (r.id.clone(), RecordState::Original(r))) .collect::>>(), )), }) } pub fn weight(&self) -> Option> { (*self.weight.read().unwrap()).as_ref().map(|w| (*w).weight) } pub fn set_weight(&self, new_weight: si::Kilogram) { 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, }), None => RecordState::New(ft_core::Weight { date: self.date, weight: new_weight, }), }; *record = Some(new_record); } pub fn steps(&self) -> Option { (*self.steps.read().unwrap()).as_ref().map(|w| w.count) } pub fn set_steps(&self, new_count: u32) { let mut record = self.steps.write().unwrap(); let new_record = match *record { Some(ref rstate) => rstate.clone().with_value(ft_core::Steps { date: self.date, count: new_count, }), None => RecordState::New(ft_core::Steps { date: self.date, count: new_count, }), }; *record = Some(new_record); } pub fn new_time_distance(&self, type_: TimeDistanceWorkoutType) -> Record { let id = RecordId::default(); let workout = TimeDistance { datetime: chrono::Local::now().into(), distance: None, duration: None, comments: None, }; let tr = TraxRecord::from_time_distance(type_, workout.clone()); self.records .write() .unwrap() .insert(id.clone(), RecordState::New(tr)); println!( "records after new_time_distance: {:?}", self.records.read().unwrap() ); Record { id, data: workout } } pub fn update_time_distance(&self, workout: Record) { let id = workout.id.clone(); let data = workout.data.clone(); let mut record_set = self.records.write().unwrap(); if let Some(record_state) = record_set.get(&id).clone() { let updated_state = match **record_state { 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( &self, type_: TimeDistanceWorkoutType, ) -> Vec> { self.records .read() .unwrap() .iter() .filter(|(_, record)| record.exists()) .filter(|(_, workout_state)| workout_state.is_time_distance_type(type_)) .filter_map(|(id, record_state)| match **record_state { TraxRecord::BikeRide(ref workout) | TraxRecord::Row(ref workout) | TraxRecord::Run(ref workout) | TraxRecord::Swim(ref workout) | TraxRecord::Walk(ref workout) => Some(Record { id: id.clone(), data: workout.clone(), }), _ => None, }) .collect() } pub fn time_distance_summary( &self, type_: TimeDistanceWorkoutType, ) -> (si::Meter, si::Second) { self.time_distance_records(type_).into_iter().fold( (0. * si::M, 0. * si::S), |(distance, duration), workout| match (workout.data.distance, workout.data.duration) { (Some(distance_), Some(duration_)) => (distance + distance_, duration + duration_), (Some(distance_), None) => (distance + distance_, duration), (None, Some(duration_)) => (distance, duration + duration_), (None, None) => (distance, duration), }, ) } fn get_record(&self, id: &RecordId) -> Option> { let record_set = self.records.read().unwrap(); match record_set.get(&id) { Some(record) => Some(Record { id: id.clone(), data: (**record).clone(), }), None => None, } } pub fn remove_record(&self, id: RecordId) { let mut record_set = self.records.write().unwrap(); let updated_record = match record_set.remove(&id) { Some(RecordState::Original(r)) => Some(RecordState::Deleted(r)), Some(RecordState::New(_)) => None, Some(RecordState::Updated(r)) => Some(RecordState::Deleted(r)), Some(RecordState::Deleted(r)) => Some(RecordState::Deleted(r)), None => None, }; if let Some(updated_record) = updated_record { record_set.insert(id, updated_record); } } pub fn save(&self) { glib::spawn_future({ let s = self.clone(); async move { s.async_save().await } }); } pub async fn async_save(&self) { println!("async_save"); let weight_record = self.weight.read().unwrap().clone(); match weight_record { Some(RecordState::New(data)) => { let _ = self.provider.put_record(TraxRecord::Weight(data)).await; } Some(RecordState::Original(_)) => {} Some(RecordState::Updated(weight)) => { let _ = self .provider .update_record(Record { id: weight.id, data: TraxRecord::Weight(weight.data), }) .await; } Some(RecordState::Deleted(_)) => {} None => {} } let steps_record = self.steps.read().unwrap().clone(); match steps_record { Some(RecordState::New(data)) => { let _ = self.provider.put_record(TraxRecord::Steps(data)).await; } Some(RecordState::Original(_)) => {} Some(RecordState::Updated(steps)) => { let _ = self .provider .update_record(Record { id: steps.id, data: TraxRecord::Steps(steps.data), }) .await; } Some(RecordState::Deleted(_)) => {} None => {} } let records = self .records .write() .unwrap() .drain() .map(|(_, record)| record) .collect::>>(); for record in records { println!("saving record: {:?}", record); match record { RecordState::New(data) => { let _ = self.provider.put_record(data).await; } RecordState::Original(_) => {} RecordState::Updated(r) => { let _ = self.provider.update_record(r.clone()).await; } RecordState::Deleted(r) => { let _ = self.provider.delete_record(r.id).await; } } } } pub fn revert(&self) { unimplemented!(); } } #[cfg(test)] mod test { use super::*; use async_trait::async_trait; use chrono::{DateTime, FixedOffset, TimeZone}; use dimensioned::si; use emseries::Record; #[derive(Clone, Debug)] struct MockProvider { records: Arc>>>, put_records: Arc>>>, updated_records: Arc>>>, deleted_records: Arc>>, } impl MockProvider { fn new(records: Vec>) -> Self { let record_map = records .into_iter() .map(|r| (r.id.clone(), r)) .collect::>>(); Self { records: Arc::new(RwLock::new(record_map)), put_records: Arc::new(RwLock::new(vec![])), updated_records: Arc::new(RwLock::new(vec![])), deleted_records: Arc::new(RwLock::new(vec![])), } } } #[async_trait] impl RecordProvider for MockProvider { async fn records( &self, start: NaiveDate, end: NaiveDate, ) -> Result>, ReadError> { let start = emseries::Timestamp::Date(start); let end = emseries::Timestamp::Date(end); Ok(self .records .read() .unwrap() .iter() .map(|(_, r)| r) .filter(|r| r.timestamp() >= start && r.timestamp() <= end) .cloned() .collect::>>()) } async fn put_record(&self, record: TraxRecord) -> Result { let id = RecordId::default(); let record = Record { id: id.clone(), data: record, }; self.put_records.write().unwrap().push(record.clone()); self.records.write().unwrap().insert(id.clone(), record); Ok(id) } async fn update_record(&self, record: Record) -> Result<(), WriteError> { println!("updated record: {:?}", record); self.updated_records.write().unwrap().push(record.clone()); self.records .write() .unwrap() .insert(record.id.clone(), record); Ok(()) } async fn delete_record(&self, id: RecordId) -> Result<(), WriteError> { self.deleted_records.write().unwrap().push(id.clone()); let _ = self.records.write().unwrap().remove(&id); Ok(()) } } async fn create_empty_view_model() -> (DayDetailViewModel, MockProvider) { let provider = MockProvider::new(vec![]); let oct_13 = chrono::NaiveDate::from_ymd_opt(2023, 10, 13).unwrap(); let model = DayDetailViewModel::new(oct_13, provider.clone()) .await .unwrap(); (model, provider) } async fn create_view_model() -> (DayDetailViewModel, MockProvider) { let oct_12 = chrono::NaiveDate::from_ymd_opt(2023, 10, 12).unwrap(); let oct_13 = chrono::NaiveDate::from_ymd_opt(2023, 10, 13).unwrap(); let oct_13_am: DateTime = oct_13 .clone() .and_hms_opt(3, 28, 0) .unwrap() .and_utc() .with_timezone(&FixedOffset::east_opt(5 * 3600).unwrap()); let provider = MockProvider::new(vec![ Record { id: RecordId::default(), data: TraxRecord::Weight(ft_core::Weight { date: oct_12.clone(), weight: 93. * si::KG, }), }, Record { id: RecordId::default(), data: TraxRecord::Weight(ft_core::Weight { date: oct_13.clone(), weight: 95. * si::KG, }), }, Record { id: RecordId::default(), data: TraxRecord::Steps(ft_core::Steps { date: oct_13.clone(), count: 2500, }), }, Record { id: RecordId::default(), data: TraxRecord::BikeRide(ft_core::TimeDistance { datetime: oct_13_am.clone(), distance: Some(15000. * si::M), duration: Some(3600. * si::S), comments: Some("somecomments present".to_owned()), }), }, ]); let model = DayDetailViewModel::new(oct_13, provider.clone()) .await .unwrap(); (model, provider) } #[tokio::test] async fn it_honors_only_the_first_weight_and_step_record() { let (view_model, provider) = create_view_model().await; assert_eq!(view_model.weight(), Some(95. * si::KG)); assert_eq!(view_model.steps(), Some(2500)); } #[tokio::test] async fn it_can_create_a_weight_and_stepcount() { let (view_model, provider) = create_empty_view_model().await; assert_eq!(view_model.weight(), None); assert_eq!(view_model.steps(), None); view_model.set_weight(95. * si::KG); view_model.set_steps(250); assert_eq!(view_model.weight(), Some(95. * si::KG)); assert_eq!(view_model.steps(), Some(250)); view_model.set_weight(93. * si::KG); view_model.set_steps(255); assert_eq!(view_model.weight(), Some(93. * si::KG)); assert_eq!(view_model.steps(), Some(255)); view_model.async_save().await; println!("provider: {:?}", provider); assert_eq!(provider.put_records.read().unwrap().len(), 2); assert_eq!(provider.updated_records.read().unwrap().len(), 0); assert_eq!(provider.deleted_records.read().unwrap().len(), 0); } #[tokio::test] async fn it_can_construct_new_records() { let (view_model, provider) = create_empty_view_model().await; assert_eq!( view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide), (0. * si::M, 0. * si::S) ); let mut record = view_model.new_time_distance(TimeDistanceWorkoutType::BikeRide); record.data.duration = Some(60. * si::S); view_model.async_save().await; assert_eq!(provider.put_records.read().unwrap().len(), 1); assert_eq!(provider.updated_records.read().unwrap().len(), 0); assert_eq!(provider.deleted_records.read().unwrap().len(), 0); } #[tokio::test] async fn it_can_update_a_new_record_before_saving() { let (view_model, provider) = create_empty_view_model().await; assert_eq!( view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide), (0. * si::M, 0. * si::S) ); let mut record = view_model.new_time_distance(TimeDistanceWorkoutType::BikeRide); record.data.duration = Some(60. * si::S); view_model.update_time_distance(record.clone()); let record = Record { id: record.id, data: TraxRecord::BikeRide(record.data), }; assert_eq!(view_model.get_record(&record.id), Some(record)); assert_eq!( view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide), (0. * si::M, 60. * si::S) ); assert_eq!( view_model.time_distance_summary(TimeDistanceWorkoutType::Run), (0. * si::M, 0. * si::S) ); view_model.async_save().await; assert_eq!(provider.put_records.read().unwrap().len(), 1); assert_eq!(provider.updated_records.read().unwrap().len(), 0); assert_eq!(provider.deleted_records.read().unwrap().len(), 0); } #[tokio::test] async fn it_can_update_an_existing_record() { let (view_model, provider) = create_view_model().await; let mut workout = view_model .time_distance_records(TimeDistanceWorkoutType::BikeRide) .first() .cloned() .unwrap(); println!("found record: {:?}", workout); workout.data.duration = Some(1800. * si::S); view_model.update_time_distance(workout.clone()); assert_eq!( view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide), (15000. * si::M, 1800. * si::S) ); view_model.async_save().await; assert_eq!(provider.put_records.read().unwrap().len(), 0); assert_eq!(provider.updated_records.read().unwrap().len(), 1); assert_eq!(provider.deleted_records.read().unwrap().len(), 0); } #[tokio::test] async fn it_can_remove_a_new_record() { let (view_model, provider) = create_empty_view_model().await; assert_eq!( view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide), (0. * si::M, 0. * si::S) ); let record = view_model.new_time_distance(TimeDistanceWorkoutType::BikeRide); view_model.remove_record(record.id); view_model.save(); assert_eq!(provider.put_records.read().unwrap().len(), 0); assert_eq!(provider.updated_records.read().unwrap().len(), 0); assert_eq!(provider.deleted_records.read().unwrap().len(), 0); } #[tokio::test] async fn it_can_delete_an_existing_record() { let (view_model, provider) = create_view_model().await; let mut workout = view_model .time_distance_records(TimeDistanceWorkoutType::BikeRide) .first() .cloned() .unwrap(); view_model.remove_record(workout.id); assert_eq!( view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide), (0. * si::M, 0. * si::S) ); view_model.async_save().await; assert_eq!(provider.put_records.read().unwrap().len(), 0); assert_eq!(provider.updated_records.read().unwrap().len(), 0); assert_eq!(provider.deleted_records.read().unwrap().len(), 1); } }