diff --git a/Cargo.lock b/Cargo.lock index 723ce30..733973c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1069,6 +1069,7 @@ dependencies = [ "dimensioned 0.8.0", "emseries", "serde 1.0.193", + "serde_json", "tempfile", ] diff --git a/emseries/src/date_time_tz.rs b/emseries/src/date_time_tz.rs deleted file mode 100644 index 1022767..0000000 --- a/emseries/src/date_time_tz.rs +++ /dev/null @@ -1,198 +0,0 @@ -/* -Copyright 2020-2023, Savanni D'Gerinel - -This file is part of the Luminescent Dreams Tools. - -Luminescent Dreams Tools 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. - -Luminescent Dreams Tools 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 Lumeto. If not, see . -*/ - -extern crate chrono; -extern crate chrono_tz; - -use chrono::SecondsFormat; -use chrono_tz::Etc::UTC; -use serde::de::{self, Deserialize, Deserializer, Visitor}; -use serde::ser::{Serialize, Serializer}; -use std::{fmt, str::FromStr}; - -/// This is a wrapper around date time objects, using timezones from the chroon-tz database and -/// providing string representation and parsing of the form " ", i.e., -/// "2019-05-15T14:30:00Z US/Central". The to_string method, and serde serialization will -/// produce a string of this format. The parser will accept an RFC3339-only string of the forms -/// "2019-05-15T14:30:00Z", "2019-05-15T14:30:00+00:00", and also an "RFC3339 Timezone Name" -/// string. -/// -/// The function here is to generate as close to unambiguous time/date strings, (for earth's -/// gravitational frame of reference), as possible. Clumping together the time, offset from UTC, -/// and the named time zone allows future parsers to know the exact interpretation of the time in -/// the frame of reference of the original recording. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct DateTimeTz(pub chrono::DateTime); - -impl fmt::Display for DateTimeTz { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - if self.0.timezone() == UTC { - write!(f, "{}", self.0.to_rfc3339_opts(SecondsFormat::Secs, true)) - } else { - write!( - f, - "{} {}", - self.0 - .with_timezone(&chrono_tz::Etc::UTC) - .to_rfc3339_opts(SecondsFormat::Secs, true,), - self.0.timezone().name() - ) - } - } -} - -impl DateTimeTz { - pub fn map(&self, f: F) -> DateTimeTz - where - F: FnOnce(chrono::DateTime) -> chrono::DateTime, - { - DateTimeTz(f(self.0)) - } -} - -impl std::str::FromStr for DateTimeTz { - type Err = chrono::ParseError; - - fn from_str(s: &str) -> Result { - let v: Vec<&str> = s.split_terminator(' ').collect(); - if v.len() == 2 { - let tz = v[1].parse::().unwrap(); - chrono::DateTime::parse_from_rfc3339(v[0]).map(|ts| DateTimeTz(ts.with_timezone(&tz))) - } else { - chrono::DateTime::parse_from_rfc3339(v[0]).map(|ts| DateTimeTz(ts.with_timezone(&UTC))) - } - } -} - -impl From> for DateTimeTz { - fn from(dt: chrono::DateTime) -> DateTimeTz { - DateTimeTz(dt) - } -} - -struct DateTimeTzVisitor; - -impl<'de> Visitor<'de> for DateTimeTzVisitor { - type Value = DateTimeTz; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a string date time representation that can be parsed") - } - - fn visit_str(self, s: &str) -> Result { - DateTimeTz::from_str(s).or(Err(E::custom( - "string is not a parsable datetime representation".to_owned(), - ))) - } -} - -impl Serialize for DateTimeTz { - fn serialize(&self, serializer: S) -> Result { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for DateTimeTz { - fn deserialize>(deserializer: D) -> Result { - deserializer.deserialize_str(DateTimeTzVisitor) - } -} - -#[cfg(test)] -mod test { - extern crate serde_json; - - use super::*; - use chrono::TimeZone; - use chrono_tz::America::Phoenix; - use chrono_tz::Etc::UTC; - use chrono_tz::US::{Arizona, Central}; - use std::str::FromStr; - - #[test] - fn it_creates_timestamp_with_z() { - let t = DateTimeTz(UTC.with_ymd_and_hms(2019, 5, 15, 12, 0, 0).unwrap()); - assert_eq!(t.to_string(), "2019-05-15T12:00:00Z"); - } - - #[test] - fn it_parses_utc_rfc3339_z() { - let t = DateTimeTz::from_str("2019-05-15T12:00:00Z").unwrap(); - assert_eq!( - t, - DateTimeTz(UTC.with_ymd_and_hms(2019, 5, 15, 12, 0, 0).unwrap()) - ); - } - - #[test] - fn it_parses_rfc3339_with_offset() { - let t = DateTimeTz::from_str("2019-05-15T12:00:00-06:00").unwrap(); - assert_eq!( - t, - DateTimeTz(UTC.with_ymd_and_hms(2019, 5, 15, 18, 0, 0).unwrap()) - ); - } - - #[test] - fn it_parses_rfc3339_with_tz() { - let t = DateTimeTz::from_str("2019-06-15T19:00:00Z US/Arizona").unwrap(); - assert_eq!( - t, - DateTimeTz(UTC.with_ymd_and_hms(2019, 6, 15, 19, 0, 0).unwrap()) - ); - assert_eq!( - t, - DateTimeTz(Arizona.with_ymd_and_hms(2019, 6, 15, 12, 0, 0).unwrap()) - ); - assert_eq!( - t, - DateTimeTz(Central.with_ymd_and_hms(2019, 6, 15, 14, 0, 0).unwrap()) - ); - assert_eq!(t.to_string(), "2019-06-15T19:00:00Z US/Arizona"); - } - - #[derive(Serialize)] - struct DemoStruct { - id: String, - dt: DateTimeTz, - } - - // I used Arizona here specifically because large parts of Arizona do not honor DST, and so - // that adds in more ambiguity of the -0700 offset with Pacific time. - #[test] - fn it_json_serializes() { - let t = DateTimeTz::from_str("2019-06-15T19:00:00Z America/Phoenix").unwrap(); - assert_eq!( - serde_json::to_string(&t).unwrap(), - "\"2019-06-15T19:00:00Z America/Phoenix\"" - ); - - let demo = DemoStruct { - id: String::from("abcdefg"), - dt: t, - }; - assert_eq!( - serde_json::to_string(&demo).unwrap(), - "{\"id\":\"abcdefg\",\"dt\":\"2019-06-15T19:00:00Z America/Phoenix\"}" - ); - } - - #[test] - fn it_json_parses() { - let t = - serde_json::from_str::("\"2019-06-15T19:00:00Z America/Phoenix\"").unwrap(); - assert_eq!( - t, - DateTimeTz(Phoenix.with_ymd_and_hms(2019, 6, 15, 12, 0, 0).unwrap()) - ); - } -} diff --git a/fitnesstrax/app/src/app.rs b/fitnesstrax/app/src/app.rs index 94c83b8..546cecb 100644 --- a/fitnesstrax/app/src/app.rs +++ b/fitnesstrax/app/src/app.rs @@ -96,18 +96,8 @@ impl App { .unwrap() } - pub async fn get_record(&self, id: RecordId) -> Result>, AppError> { - let db = self.database.clone(); - self.runtime - .spawn_blocking(move || { - if let Some(ref db) = *db.read().unwrap() { - Ok(db.get(&id)) - } else { - Err(AppError::NoDatabase) - } - }) - .await - .unwrap() + pub fn database_is_open(&self) -> bool { + self.database.read().unwrap().is_some() } } diff --git a/fitnesstrax/app/src/app_window.rs b/fitnesstrax/app/src/app_window.rs index 31a05c3..3bdd4a4 100644 --- a/fitnesstrax/app/src/app_window.rs +++ b/fitnesstrax/app/src/app_window.rs @@ -136,7 +136,6 @@ impl AppWindow { s } - #[allow(unused)] fn show_welcome_view(&self) { let view = View::Welcome(WelcomeView::new({ let s = self.clone(); @@ -177,9 +176,13 @@ impl AppWindow { glib::spawn_future_local({ let s = self.clone(); async move { - let end = Local::now().date_naive(); - let start = end - Duration::days(7); - s.show_historical_view(DayInterval { start, end }); + if s.app.database_is_open() { + let end = Local::now().date_naive(); + let start = end - Duration::days(7); + s.show_historical_view(DayInterval { start, end }); + } else { + s.show_welcome_view(); + } } }); } diff --git a/fitnesstrax/app/src/main.rs b/fitnesstrax/app/src/main.rs index 921f708..b6ca965 100644 --- a/fitnesstrax/app/src/main.rs +++ b/fitnesstrax/app/src/main.rs @@ -25,8 +25,7 @@ mod views; use adw::prelude::*; use app_window::AppWindow; -use components::ActionGroup; -use gio::{Action, ActionEntry}; +use gio::ActionEntry; use std::{env, path::PathBuf}; const APP_ID_DEV: &str = "com.luminescent-dreams.fitnesstrax.dev"; @@ -81,7 +80,7 @@ fn main() { let icon_theme = gtk::IconTheme::for_display(&gdk::Display::default().unwrap()); icon_theme.add_resource_path(&(RESOURCE_BASE_PATH.to_owned() + "/icons/scalable/actions")); - setup_app_close_action(&adw_app); + setup_app_close_action(adw_app); AppWindow::new(app_id, RESOURCE_BASE_PATH, adw_app, ft_app.clone()); }); diff --git a/fitnesstrax/app/src/views/mod.rs b/fitnesstrax/app/src/views/mod.rs index a8c7360..9957823 100644 --- a/fitnesstrax/app/src/views/mod.rs +++ b/fitnesstrax/app/src/views/mod.rs @@ -30,7 +30,6 @@ pub use welcome_view::WelcomeView; pub enum View { Placeholder(PlaceholderView), - #[allow(unused)] Welcome(WelcomeView), Historical(HistoricalView), } diff --git a/fitnesstrax/core/Cargo.toml b/fitnesstrax/core/Cargo.toml index 520b69c..3e7a8ed 100644 --- a/fitnesstrax/core/Cargo.toml +++ b/fitnesstrax/core/Cargo.toml @@ -11,6 +11,7 @@ chrono-tz = { version = "0.8" } dimensioned = { version = "0.8", features = [ "serde" ] } emseries = { path = "../../emseries" } serde = { version = "1", features = [ "derive" ] } +serde_json = { version = "1" } [dev-dependencies] tempfile = "*" diff --git a/fitnesstrax/core/src/bin/legacy-importer.rs b/fitnesstrax/core/src/bin/legacy-importer.rs new file mode 100644 index 0000000..77b6423 --- /dev/null +++ b/fitnesstrax/core/src/bin/legacy-importer.rs @@ -0,0 +1,328 @@ +/* +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 chrono::SecondsFormat; +use chrono_tz::Etc::UTC; +use dimensioned::si; +use emseries::{Record, RecordId, Series, Timestamp}; +use ft_core::{self, DurationWorkout, DurationWorkoutActivity, SetRepActivity, TraxRecord}; +use serde::{ + de::{self, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use std::{ + fmt, + fs::File, + io::{BufRead, BufReader, Read}, + str::FromStr, +}; + +/// This is a wrapper around date time objects, using timezones from the chroon-tz database and +/// providing string representation and parsing of the form " ", i.e., +/// "2019-05-15T14:30:00Z US/Central". The to_string method, and serde serialization will +/// produce a string of this format. The parser will accept an RFC3339-only string of the forms +/// "2019-05-15T14:30:00Z", "2019-05-15T14:30:00+00:00", and also an "RFC3339 Timezone Name" +/// string. +/// +/// The function here is to generate as close to unambiguous time/date strings, (for earth's +/// gravitational frame of reference), as possible. Clumping together the time, offset from UTC, +/// and the named time zone allows future parsers to know the exact interpretation of the time in +/// the frame of reference of the original recording. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct DateTimeTz(pub chrono::DateTime); + +impl fmt::Display for DateTimeTz { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + if self.0.timezone() == UTC { + write!(f, "{}", self.0.to_rfc3339_opts(SecondsFormat::Secs, true)) + } else { + write!( + f, + "{} {}", + self.0 + .with_timezone(&chrono_tz::Etc::UTC) + .to_rfc3339_opts(SecondsFormat::Secs, true,), + self.0.timezone().name() + ) + } + } +} + +impl DateTimeTz { + pub fn map(&self, f: F) -> DateTimeTz + where + F: FnOnce(chrono::DateTime) -> chrono::DateTime, + { + DateTimeTz(f(self.0)) + } +} + +impl std::str::FromStr for DateTimeTz { + type Err = chrono::ParseError; + + fn from_str(s: &str) -> Result { + let v: Vec<&str> = s.split_terminator(' ').collect(); + if v.len() == 2 { + let tz = v[1].parse::().unwrap(); + chrono::DateTime::parse_from_rfc3339(v[0]).map(|ts| DateTimeTz(ts.with_timezone(&tz))) + } else { + chrono::DateTime::parse_from_rfc3339(v[0]).map(|ts| DateTimeTz(ts.with_timezone(&UTC))) + } + } +} + +impl From> for DateTimeTz { + fn from(dt: chrono::DateTime) -> DateTimeTz { + DateTimeTz(dt) + } +} + +struct DateTimeTzVisitor; + +impl<'de> Visitor<'de> for DateTimeTzVisitor { + type Value = DateTimeTz; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string date time representation that can be parsed") + } + + fn visit_str(self, s: &str) -> Result { + DateTimeTz::from_str(s).or(Err(E::custom( + "string is not a parsable datetime representation".to_owned(), + ))) + } +} + +impl Serialize for DateTimeTz { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for DateTimeTz { + fn deserialize>(deserializer: D) -> Result { + deserializer.deserialize_str(DateTimeTzVisitor) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct Steps { + date: DateTimeTz, + steps: u32, +} + +impl From for ft_core::Steps { + fn from(s: Steps) -> Self { + Self { + date: s.date.0.naive_utc().date(), + count: s.steps, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct Weight { + date: DateTimeTz, + weight: f64, +} + +impl From for ft_core::Weight { + fn from(w: Weight) -> Self { + Self { + date: w.date.0.naive_utc().date(), + weight: w.weight * si::KG, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum TDActivity { + Cycling, + Rowing, + Running, + Swimming, + Walking, +} + +impl From for ft_core::TimeDistanceActivity { + fn from(activity: TDActivity) -> Self { + match activity { + TDActivity::Cycling => ft_core::TimeDistanceActivity::Biking, + TDActivity::Rowing => ft_core::TimeDistanceActivity::Rowing, + TDActivity::Running => ft_core::TimeDistanceActivity::Running, + TDActivity::Swimming => ft_core::TimeDistanceActivity::Swimming, + TDActivity::Walking => ft_core::TimeDistanceActivity::Walking, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct TimeDistance { + date: DateTimeTz, + activity: TDActivity, + comments: Option, + distance: Option, + duration: Option, +} + +impl From for ft_core::TimeDistance { + fn from(td: TimeDistance) -> Self { + Self { + datetime: td.date.0.fixed_offset(), + activity: td.activity.into(), + comments: td.comments, + distance: td.distance.map(|d| d * si::M), + duration: td.duration.map(|d| d * si::S), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum SRActivity { + Pushups, + Situps, +} + +impl From for SetRepActivity { + fn from(activity: SRActivity) -> Self { + match activity { + SRActivity::Pushups => Self::Pushups, + SRActivity::Situps => Self::Situps, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct SetRep { + date: DateTimeTz, + activity: SRActivity, + sets: Vec, + comments: Option, +} + +impl From for ft_core::SetRep { + fn from(sr: SetRep) -> Self { + Self { + date: sr.date.0.naive_utc().date(), + activity: sr.activity.into(), + sets: sr.sets, + comments: sr.comments, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum RDActivity { + MartialArts, +} + +impl From for DurationWorkoutActivity { + fn from(_: RDActivity) -> Self { + Self::MartialArts + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct RepDuration { + date: DateTimeTz, + activity: RDActivity, + sets: Vec, +} + +impl From for DurationWorkout { + fn from(rd: RepDuration) -> Self { + Self { + datetime: rd.date.0.fixed_offset(), + activity: rd.activity.into(), + duration: rd.sets.into_iter().map(|d| d * si::S).next(), + comments: None, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum LegacyData { + RepDuration(RepDuration), + SetRep(SetRep), + Steps(Steps), + TimeDistance(TimeDistance), + Weight(Weight), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct LegacyRecord { + id: RecordId, + data: LegacyData, +} + +impl From for Record { + fn from(record: LegacyRecord) -> Self { + match record.data { + LegacyData::RepDuration(rd) => Record { + id: record.id, + data: TraxRecord::DurationWorkout(rd.into()), + }, + LegacyData::SetRep(sr) => Record { + id: record.id, + data: TraxRecord::SetRep(sr.into()), + }, + LegacyData::Steps(s) => Record { + id: record.id, + data: TraxRecord::Steps(s.into()), + }, + LegacyData::TimeDistance(td) => Record { + id: record.id, + data: TraxRecord::TimeDistance(td.into()), + }, + LegacyData::Weight(weight) => Record { + id: record.id, + data: TraxRecord::Weight(weight.into()), + }, + } + } +} + +fn main() { + let mut args = std::env::args(); + let _ = args.next().unwrap(); + let input_filename = args.next().unwrap(); + println!("input filename: {}", input_filename); + // let output: Series = Series::open_file("import.fitnesstrax").unwrap(); + + let input_file = File::open(input_filename).unwrap(); + let mut buf_reader = BufReader::new(input_file); + // let mut contents = String::new(); + // buf_reader.read_(&mut contents).unwrap(); + + let mut count = 0; + + loop { + let mut line = String::new(); + let res = buf_reader.read_line(&mut line); + match res { + Err(err) => { + panic!("failed after {} lines: {:?}", count, err); + } + Ok(0) => std::process::exit(0), + Ok(_) => { + let record = serde_json::from_str::(&line).unwrap(); + let record: Record = record.into(); + println!("{}", serde_json::to_string(&record).unwrap()); + count += 1; + } + } + } +} diff --git a/fitnesstrax/core/src/lib.rs b/fitnesstrax/core/src/lib.rs index f579628..007dc3f 100644 --- a/fitnesstrax/core/src/lib.rs +++ b/fitnesstrax/core/src/lib.rs @@ -2,5 +2,7 @@ mod legacy; mod types; pub use types::{ - Steps, TimeDistance, TimeDistanceActivity, TraxRecord, Weight, TIME_DISTANCE_ACTIVITIES, + DurationWorkout, DurationWorkoutActivity, SetRep, SetRepActivity, Steps, TimeDistance, + TimeDistanceActivity, TraxRecord, Weight, DURATION_WORKOUT_ACTIVITIES, SET_REP_ACTIVITIES, + TIME_DISTANCE_ACTIVITIES, }; diff --git a/fitnesstrax/core/src/types.rs b/fitnesstrax/core/src/types.rs index d2ef95c..97a9d69 100644 --- a/fitnesstrax/core/src/types.rs +++ b/fitnesstrax/core/src/types.rs @@ -19,17 +19,38 @@ use dimensioned::si; use emseries::{Recordable, Timestamp}; use serde::{Deserialize, Serialize}; +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum SetRepActivity { + Pushups, + Situps, +} + +pub const SET_REP_ACTIVITIES: [SetRepActivity; 2] = + [SetRepActivity::Pushups, SetRepActivity::Situps]; + /// SetRep represents workouts like pushups or situps, which involve doing a "set" of a number of /// actions, resting, and then doing another set. -#[allow(dead_code)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct SetRep { /// I assume that a set/rep workout is only done once in a day. - date: NaiveDate, + pub date: NaiveDate, + /// The activity involved + pub activity: SetRepActivity, /// Each set entry represents the number of times that the action was performed in a set. So, a /// pushup workout that involved five sets would have five entries. Each entry would be x /// number of pushups. A viable workout would be something like [6, 6, 4, 4, 5]. - sets: Vec, - comments: Option, + pub sets: Vec, + pub comments: Option, +} + +impl Recordable for SetRep { + fn timestamp(&self) -> Timestamp { + Timestamp::Date(self.date) + } + + fn tags(&self) -> Vec { + vec![] + } } /// The number of steps one takes in a single day. @@ -120,11 +141,45 @@ impl Recordable for Weight { } } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum DurationWorkoutActivity { + MartialArts, + Yoga, +} + +pub const DURATION_WORKOUT_ACTIVITIES: [DurationWorkoutActivity; 2] = [ + DurationWorkoutActivity::MartialArts, + DurationWorkoutActivity::Yoga, +]; + +/// Generic workouts for which only duration really matters. This is for things +/// such as Martial Arts or Yoga, which are activities done for an amount of +/// time, but with no other details. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct DurationWorkout { + pub datetime: DateTime, + pub activity: DurationWorkoutActivity, + pub duration: Option>, + pub comments: Option, +} + +impl Recordable for DurationWorkout { + fn timestamp(&self) -> Timestamp { + Timestamp::DateTime(self.datetime) + } + + fn tags(&self) -> Vec { + vec![] + } +} + /// The unified data structure for all records that are part of the app. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum TraxRecord { - TimeDistance(TimeDistance), + DurationWorkout(DurationWorkout), + SetRep(SetRep), Steps(Steps), + TimeDistance(TimeDistance), Weight(Weight), } @@ -164,8 +219,10 @@ impl Recordable for TraxRecord { fn timestamp(&self) -> Timestamp { match self { TraxRecord::TimeDistance(rec) => Timestamp::DateTime(rec.datetime), + TraxRecord::SetRep(rec) => rec.timestamp(), TraxRecord::Steps(rec) => rec.timestamp(), TraxRecord::Weight(rec) => rec.timestamp(), + TraxRecord::DurationWorkout(rec) => rec.timestamp(), } }