Compare commits

..

2 Commits

Author SHA1 Message Date
Savanni D'Gerinel 24276d172b Introduce the RecordProvider interface
DayDetailViewModel needs testing. I've worked out an improved API, and a set of tests to go along with it, and those can be made more easily with a mockable RecordProvider. So, in addition to stubbing out a bunch of tests, I've also created RecordProvider, mocked it, and implemented it for App.
2024-02-01 10:12:35 -05:00
Savanni D'Gerinel 96317f5692 Finish removing the previous record grouping
Now that the DayDetailViewModel knows to retrieve its own records, the grouping functions, and passing groups of records around, no longer make sens.
2024-02-01 10:08:18 -05:00
6 changed files with 266 additions and 213 deletions

50
Cargo.lock generated
View File

@ -133,6 +133,17 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "async-trait"
version = "0.1.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "atoi"
version = "2.0.0"
@ -420,7 +431,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.41",
"syn 2.0.48",
]
[[package]]
@ -736,7 +747,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.41",
"syn 2.0.48",
]
[[package]]
@ -928,6 +939,7 @@ name = "fitnesstrax"
version = "0.3.0"
dependencies = [
"async-channel",
"async-trait",
"chrono",
"chrono-tz",
"dimensioned 0.8.0",
@ -1132,7 +1144,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.41",
"syn 2.0.48",
]
[[package]]
@ -1367,7 +1379,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.41",
"syn 2.0.48",
]
[[package]]
@ -2443,7 +2455,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.41",
"syn 2.0.48",
]
[[package]]
@ -2648,7 +2660,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.41",
"syn 2.0.48",
]
[[package]]
@ -2792,9 +2804,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.70"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
@ -2836,9 +2848,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quote"
version = "1.0.33"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
@ -3321,7 +3333,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.41",
"syn 2.0.48",
]
[[package]]
@ -3720,9 +3732,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.41"
version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [
"proc-macro2",
"quote",
@ -3818,7 +3830,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.41",
"syn 2.0.48",
]
[[package]]
@ -3942,7 +3954,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.41",
"syn 2.0.48",
]
[[package]]
@ -4063,7 +4075,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.41",
"syn 2.0.48",
]
[[package]]
@ -4433,7 +4445,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.41",
"syn 2.0.48",
"wasm-bindgen-shared",
]
@ -4467,7 +4479,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.41",
"syn 2.0.48",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -4708,7 +4720,7 @@ checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.41",
"syn 2.0.48",
]
[[package]]

View File

@ -8,6 +8,7 @@ edition = "2021"
[dependencies]
adw = { version = "0.5", package = "libadwaita", features = [ "v1_4" ] }
async-channel = { version = "2.1" }
async-trait = { version = "0.1" }
chrono = { version = "0.4" }
chrono-tz = { version = "0.8" }
dimensioned = { version = "0.8", features = [ "serde" ] }

View File

@ -14,6 +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 <https://www.gnu.org/licenses/>.
*/
use async_trait::async_trait;
use chrono::NaiveDate;
use emseries::{time_range, Record, RecordId, Series, Timestamp};
use ft_core::TraxRecord;
@ -34,6 +35,32 @@ pub enum AppError {
Unhandled,
}
#[derive(Debug, Error)]
pub enum ReadError {
#[error("no database loaded")]
NoDatabase,
}
#[derive(Debug, Error)]
pub enum WriteError {
#[error("no database loaded")]
NoDatabase,
#[error("unhandled error")]
Unhandled,
}
#[async_trait]
pub trait RecordProvider: Send + Sync {
async fn records(
&self,
start: NaiveDate,
end: NaiveDate,
) -> Result<Vec<Record<TraxRecord>>, ReadError>;
async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError>;
async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), WriteError>;
async fn delete_record(&self, id: RecordId) -> Result<(), WriteError>;
}
/// The real, headless application. This is where all of the logic will reside.
#[derive(Clone)]
pub struct App {
@ -57,11 +84,26 @@ impl App {
}
}
pub async fn records(
pub async fn open_db(&self, path: PathBuf) -> Result<(), AppError> {
let db_ref = self.database.clone();
self.runtime
.spawn_blocking(move || {
let db = Series::open(path).map_err(|_| AppError::FailedToOpenDatabase)?;
*db_ref.write().unwrap() = Some(db);
Ok(())
})
.await
.unwrap()
}
}
#[async_trait]
impl RecordProvider for App {
async fn records(
&self,
start: NaiveDate,
end: NaiveDate,
) -> Result<Vec<Record<TraxRecord>>, AppError> {
) -> Result<Vec<Record<TraxRecord>>, ReadError> {
let db = self.database.clone();
self.runtime
.spawn_blocking(move || {
@ -77,14 +119,14 @@ impl App {
.collect::<Vec<Record<TraxRecord>>>();
Ok(records)
} else {
Err(AppError::NoDatabase)
Err(ReadError::NoDatabase)
}
})
.await
.unwrap()
}
pub async fn put_record(&self, record: TraxRecord) -> Result<RecordId, AppError> {
async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError> {
let db = self.database.clone();
self.runtime
.spawn_blocking(move || {
@ -97,10 +139,10 @@ impl App {
})
.await
.unwrap()
.map_err(|_| AppError::Unhandled)
.map_err(|_| WriteError::Unhandled)
}
pub async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), AppError> {
async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), WriteError> {
let db = self.database.clone();
self.runtime
.spawn_blocking(move || {
@ -112,18 +154,10 @@ impl App {
})
.await
.unwrap()
.map_err(|_| AppError::Unhandled)
.map_err(|_| WriteError::Unhandled)
}
pub async fn open_db(&self, path: PathBuf) -> Result<(), AppError> {
let db_ref = self.database.clone();
self.runtime
.spawn_blocking(move || {
let db = Series::open(path).map_err(|_| AppError::FailedToOpenDatabase)?;
*db_ref.write().unwrap() = Some(db);
Ok(())
})
.await
.unwrap()
async fn delete_record(&self, id: RecordId) -> Result<(), WriteError> {
unimplemented!()
}
}

View File

@ -15,7 +15,8 @@ You should have received a copy of the GNU General Public License along with Fit
*/
use crate::{
app::App,
app::{App, RecordProvider},
types::DayInterval,
view_models::DayDetailViewModel,
views::{DayDetailView, HistoricalView, PlaceholderView, View, WelcomeView},
};
@ -133,15 +134,15 @@ impl AppWindow {
self.swap_main(view);
}
fn show_historical_view(&self, records: Vec<Record<TraxRecord>>) {
let view = View::Historical(HistoricalView::new(self.app.clone(), records, {
fn show_historical_view(&self, interval: DayInterval) {
let view = View::Historical(HistoricalView::new(self.app.clone(), interval, {
let s = self.clone();
Rc::new(move |date, _records| {
Rc::new(move |date| {
let s = s.clone();
glib::spawn_future_local(async move {
let layout = gtk::Box::new(gtk::Orientation::Vertical, 0);
layout.append(&adw::HeaderBar::new());
let view_model = DayDetailViewModel::new(date, s.app.clone()).await;
let view_model = DayDetailViewModel::new(date, s.app.clone()).await.unwrap();
layout.append(&DayDetailView::new(view_model));
let page = &adw::NavigationPage::builder()
.title(date.format("%Y-%m-%d").to_string())
@ -160,10 +161,7 @@ impl AppWindow {
async move {
let end = Local::now().date_naive();
let start = end - Duration::days(7);
match s.app.records(start, end).await {
Ok(records) => s.show_historical_view(records),
Err(_) => s.show_welcome_view(),
}
s.show_historical_view(DayInterval { start, end });
}
});
}

View File

@ -14,7 +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 <https://www.gnu.org/licenses/>.
*/
use crate::{app::App, types::WeightFormatter};
use crate::{
app::{ReadError, RecordProvider, WriteError},
types::WeightFormatter,
};
use chrono::NaiveDate;
use emseries::{Record, RecordId, Recordable};
use ft_core::TraxRecord;
use std::{
@ -77,7 +81,7 @@ impl<T: Clone + emseries::Recordable> Deref for RecordState<T> {
#[derive(Clone)]
pub struct DayDetailViewModel {
app: App,
provider: Arc<dyn RecordProvider>,
pub date: chrono::NaiveDate,
weight: Arc<RwLock<Option<RecordState<ft_core::Weight>>>>,
steps: Arc<RwLock<Option<RecordState<ft_core::Steps>>>>,
@ -85,15 +89,20 @@ pub struct DayDetailViewModel {
}
impl DayDetailViewModel {
pub async fn new(date: chrono::NaiveDate, app: App) -> Self {
let records = app.records(date, date).await.unwrap();
pub async fn new(
date: chrono::NaiveDate,
provider: impl RecordProvider + 'static,
) -> Result<Self, ReadError> {
let (weight_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
records.into_iter().partition(|r| r.data.is_weight());
provider
.records(date, date)
.await?
.into_iter()
.partition(|r| r.data.is_weight());
let (step_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
records.into_iter().partition(|r| r.data.is_steps());
Self {
app,
Ok(Self {
provider: Arc::new(provider),
date,
weight: Arc::new(RwLock::new(
weight_records
@ -120,7 +129,7 @@ impl DayDetailViewModel {
.map(|r| (r.id.clone(), RecordState::Original(r)))
.collect::<HashMap<RecordId, RecordState<TraxRecord>>>(),
)),
}
})
}
pub fn weight(&self) -> Option<WeightFormatter> {
@ -170,12 +179,12 @@ impl DayDetailViewModel {
let weight_record = s.weight.read().unwrap().clone();
match weight_record {
Some(RecordState::New(data)) => {
let _ = s.app.put_record(TraxRecord::Weight(data)).await;
let _ = s.provider.put_record(TraxRecord::Weight(data)).await;
}
Some(RecordState::Original(_)) => {}
Some(RecordState::Updated(weight)) => {
let _ = s
.app
.provider
.update_record(Record {
id: weight.id,
data: TraxRecord::Weight(weight.data),
@ -189,12 +198,12 @@ impl DayDetailViewModel {
let steps_record = s.steps.read().unwrap().clone();
match steps_record {
Some(RecordState::New(data)) => {
let _ = s.app.put_record(TraxRecord::Steps(data)).await;
let _ = s.provider.put_record(TraxRecord::Steps(data)).await;
}
Some(RecordState::Original(_)) => {}
Some(RecordState::Updated(steps)) => {
let _ = s
.app
.provider
.update_record(Record {
id: steps.id,
data: TraxRecord::Steps(steps.data),
@ -216,11 +225,11 @@ impl DayDetailViewModel {
for record in records {
match record {
RecordState::New(data) => {
let _ = s.app.put_record(data).await;
let _ = s.provider.put_record(data).await;
}
RecordState::Original(_) => {}
RecordState::Updated(r) => {
let _ = s.app.update_record(r.clone()).await;
let _ = s.provider.update_record(r.clone()).await;
}
RecordState::Deleted(_) => unimplemented!(),
}
@ -233,3 +242,131 @@ impl DayDetailViewModel {
unimplemented!();
}
}
#[cfg(test)]
mod test {
use super::*;
use async_trait::async_trait;
use dimensioned::si;
use emseries::Record;
struct MockProvider {
records: Vec<Record<TraxRecord>>,
}
#[async_trait]
impl RecordProvider for MockProvider {
async fn records(
&self,
start: NaiveDate,
end: NaiveDate,
) -> Result<Vec<Record<TraxRecord>>, ReadError> {
let start = emseries::Timestamp::Date(start);
let end = emseries::Timestamp::Date(end);
Ok(self
.records
.iter()
.filter(|r| r.timestamp() >= start && r.timestamp() <= end)
.cloned()
.collect::<Vec<Record<TraxRecord>>>())
}
async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError> {
Err(WriteError::NoDatabase)
}
async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), WriteError> {
Err(WriteError::NoDatabase)
}
async fn delete_record(&self, id: RecordId) -> Result<(), WriteError> {
Err(WriteError::NoDatabase)
}
}
async fn with_view_model<TestFn>(test: TestFn)
where
TestFn: Fn(DayDetailViewModel),
{
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 provider = MockProvider {
records: 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::Weight(ft_core::Weight {
date: oct_13.clone(),
weight: 91. * si::KG,
}),
},
Record {
id: RecordId::default(),
data: TraxRecord::Steps(ft_core::Steps {
date: oct_13.clone(),
count: 2750,
}),
},
],
};
let model = DayDetailViewModel::new(oct_13, provider).await.unwrap();
test(model)
}
#[tokio::test]
async fn it_honors_only_the_first_weight_and_step_record() {
with_view_model(|view_model| {
assert_eq!(view_model.weight().map(|val| *val), Some(95. * si::KG));
assert_eq!(view_model.steps(), Some(2500));
})
.await;
}
#[test]
#[ignore]
fn it_enforces_one_weight_and_stepcount_per_day() {}
#[test]
#[ignore]
fn it_can_construct_new_records() {}
#[test]
#[ignore]
fn it_can_update_an_existing_record() {}
#[test]
#[ignore]
fn it_can_remove_a_new_record() {}
#[test]
#[ignore]
fn it_can_delete_an_existing_record() {}
#[test]
#[ignore]
fn it_retrieve_records_by_workout() {}
#[test]
#[ignore]
fn it_summarizes_records_by_workout_type() {}
}

View File

@ -62,11 +62,11 @@ impl ObjectSubclass for HistoricalViewPrivate {
let app = app.clone();
let list_item = list_item.clone();
glib::spawn_future_local(async move {
let records = list_item
let date = list_item
.downcast_ref::<gtk::ListItem>()
.expect("should be a ListItem")
.item()
.and_downcast::<DayRecords>()
.and_downcast::<Date>()
.expect("should be a DaySummary");
let summary = list_item
@ -77,8 +77,11 @@ impl ObjectSubclass for HistoricalViewPrivate {
.expect("should be a DaySummary");
if let Some(app) = app.borrow().clone() {
let view_model = DayDetailViewModel::new(records.date(), app.clone()).await;
glib::spawn_future_local(async move {
let view_model =
DayDetailViewModel::new(date.date(), app).await.unwrap();
summary.set_data(view_model);
});
}
});
}
@ -97,13 +100,9 @@ glib::wrapper! {
}
impl HistoricalView {
pub fn new<SelectFn>(
app: App,
records: Vec<Record<TraxRecord>>,
on_select_day: Rc<SelectFn>,
) -> Self
pub fn new<SelectFn>(app: App, interval: DayInterval, on_select_day: Rc<SelectFn>) -> Self
where
SelectFn: Fn(chrono::NaiveDate, Vec<Record<TraxRecord>>) + 'static,
SelectFn: Fn(chrono::NaiveDate) + 'static,
{
let s: Self = Object::builder().build();
s.set_orientation(gtk::Orientation::Vertical);
@ -111,11 +110,8 @@ impl HistoricalView {
*s.imp().app.borrow_mut() = Some(app);
let grouped_records =
GroupedRecords::new((*s.imp().time_window.borrow()).clone()).with_data(records);
let mut model = gio::ListStore::new::<DayRecords>();
model.extend(grouped_records.items());
let mut model = gio::ListStore::new::<Date>();
model.extend(interval.days().map(Date::new));
s.imp()
.list_view
.set_model(Some(&gtk::NoSelection::new(Some(model))));
@ -127,8 +123,8 @@ impl HistoricalView {
// actually want to do here is to open a modal dialog that shows all of the details of
// the day and which allows the user to edit items within that dialog.
let item = s.model().unwrap().item(idx).unwrap();
let records = item.downcast_ref::<DayRecords>().unwrap();
on_select_day(records.date(), records.records());
let date = item.downcast_ref::<Date>().unwrap();
on_select_day(date.date());
}
});
@ -137,12 +133,9 @@ impl HistoricalView {
s
}
pub fn set_records(&self, records: Vec<Record<TraxRecord>>) {
println!("set_records: {:?}", records);
let grouped_records =
GroupedRecords::new((self.imp().time_window.borrow()).clone()).with_data(records);
let mut model = gio::ListStore::new::<DayRecords>();
model.extend(grouped_records.items());
pub fn set_interval(&self, interval: DayInterval) {
let mut model = gio::ListStore::new::<Date>();
model.extend(interval.days().map(Date::new));
self.imp()
.list_view
.set_model(Some(&gtk::NoSelection::new(Some(model))));
@ -154,152 +147,30 @@ impl HistoricalView {
}
#[derive(Default)]
pub struct DayRecordsPrivate {
pub struct DatePrivate {
date: RefCell<chrono::NaiveDate>,
records: RefCell<Vec<Record<TraxRecord>>>,
}
#[glib::object_subclass]
impl ObjectSubclass for DayRecordsPrivate {
const NAME: &'static str = "DayRecords";
type Type = DayRecords;
impl ObjectSubclass for DatePrivate {
const NAME: &'static str = "Date";
type Type = Date;
}
impl ObjectImpl for DayRecordsPrivate {}
impl ObjectImpl for DatePrivate {}
glib::wrapper! {
pub struct DayRecords(ObjectSubclass<DayRecordsPrivate>);
pub struct Date(ObjectSubclass<DatePrivate>);
}
impl DayRecords {
pub fn new(date: chrono::NaiveDate, records: Vec<Record<TraxRecord>>) -> Self {
impl Date {
pub fn new(date: chrono::NaiveDate) -> Self {
let s: Self = Object::builder().build();
*s.imp().date.borrow_mut() = date;
*s.imp().records.borrow_mut() = records;
s
}
pub fn date(&self) -> chrono::NaiveDate {
*self.imp().date.borrow()
}
pub fn records(&self) -> Vec<Record<TraxRecord>> {
self.imp().records.borrow().clone()
}
pub fn add_record(&self, record: Record<TraxRecord>) {
self.imp().records.borrow_mut().push(record);
}
}
// This isn't feeling quite right. DayRecords is a glib object, but I'm not sure that I want to
// really be passing that around. It seems not generic enough. I feel like this whole grouped
// records thing can be made more generic.
struct GroupedRecords {
interval: DayInterval,
data: HashMap<NaiveDate, DayRecords>,
}
impl GroupedRecords {
fn new(interval: DayInterval) -> Self {
let mut s = Self {
interval: interval.clone(),
data: HashMap::new(),
};
interval.days().for_each(|date| {
let _ = s.data.insert(date, DayRecords::new(date, vec![]));
});
s
}
fn with_data(mut self, records: Vec<Record<TraxRecord>>) -> Self {
records.into_iter().for_each(|record| {
self.data
.entry(record.date())
.and_modify(|entry: &mut DayRecords| (*entry).add_record(record.clone()))
.or_insert(DayRecords::new(record.date(), vec![record]));
});
self
}
fn items(&self) -> impl Iterator<Item = DayRecords> + '_ {
self.interval.days().map(|date| {
self.data
.get(&date)
.cloned()
.unwrap_or(DayRecords::new(date, vec![]))
})
}
}
#[cfg(test)]
mod test {
use super::GroupedRecords;
use crate::types::DayInterval;
use chrono::{FixedOffset, NaiveDate, TimeZone};
use dimensioned::si::{KG, M, S};
use emseries::{Record, RecordId};
use ft_core::{Steps, TimeDistance, TraxRecord, Weight};
#[test]
fn groups_records() {
let records = vec![
Record {
id: RecordId::default(),
data: TraxRecord::Steps(Steps {
date: NaiveDate::from_ymd_opt(2023, 10, 13).unwrap(),
count: 1500,
}),
},
Record {
id: RecordId::default(),
data: TraxRecord::Weight(Weight {
date: NaiveDate::from_ymd_opt(2023, 10, 13).unwrap(),
weight: 85. * KG,
}),
},
Record {
id: RecordId::default(),
data: TraxRecord::Weight(Weight {
date: NaiveDate::from_ymd_opt(2023, 10, 14).unwrap(),
weight: 86. * KG,
}),
},
Record {
id: RecordId::default(),
data: TraxRecord::BikeRide(TimeDistance {
datetime: FixedOffset::west_opt(10 * 60 * 60)
.unwrap()
.with_ymd_and_hms(2019, 6, 15, 12, 0, 0)
.unwrap(),
distance: Some(1000. * M),
duration: Some(150. * S),
comments: Some("Test Comments".to_owned()),
}),
},
Record {
id: RecordId::default(),
data: TraxRecord::BikeRide(TimeDistance {
datetime: FixedOffset::west_opt(10 * 60 * 60)
.unwrap()
.with_ymd_and_hms(2019, 6, 15, 23, 0, 0)
.unwrap(),
distance: Some(1000. * M),
duration: Some(150. * S),
comments: Some("Test Comments".to_owned()),
}),
},
];
let groups = GroupedRecords::new(DayInterval {
start: NaiveDate::from_ymd_opt(2023, 10, 14).unwrap(),
end: NaiveDate::from_ymd_opt(2023, 10, 14).unwrap(),
})
.with_data(records)
.data;
assert_eq!(groups.len(), 3);
}
}