Import legacy data. Show welcome screen when no database is configured. #198

Merged
savanni merged 4 commits from fitnesstrax/legacy-importer into main 2024-02-19 21:45:39 +00:00
10 changed files with 406 additions and 224 deletions

1
Cargo.lock generated
View File

@ -1069,6 +1069,7 @@ dependencies = [
"dimensioned 0.8.0",
"emseries",
"serde 1.0.193",
"serde_json",
"tempfile",
]

View File

@ -1,198 +0,0 @@
/*
Copyright 2020-2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>
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 <https://www.gnu.org/licenses/>.
*/
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 "<RFC3339> <Timezone Name>", 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<chrono_tz::Tz>);
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<F>(&self, f: F) -> DateTimeTz
where
F: FnOnce(chrono::DateTime<chrono_tz::Tz>) -> chrono::DateTime<chrono_tz::Tz>,
{
DateTimeTz(f(self.0))
}
}
impl std::str::FromStr for DateTimeTz {
type Err = chrono::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let v: Vec<&str> = s.split_terminator(' ').collect();
if v.len() == 2 {
let tz = v[1].parse::<chrono_tz::Tz>().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<chrono::DateTime<chrono_tz::Tz>> for DateTimeTz {
fn from(dt: chrono::DateTime<chrono_tz::Tz>) -> 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<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
DateTimeTz::from_str(s).or(Err(E::custom(
"string is not a parsable datetime representation".to_owned(),
)))
}
}
impl Serialize for DateTimeTz {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for DateTimeTz {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
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::<DateTimeTz>("\"2019-06-15T19:00:00Z America/Phoenix\"").unwrap();
assert_eq!(
t,
DateTimeTz(Phoenix.with_ymd_and_hms(2019, 6, 15, 12, 0, 0).unwrap())
);
}
}

View File

@ -96,18 +96,8 @@ impl App {
.unwrap()
}
pub async fn get_record(&self, id: RecordId) -> Result<Option<Record<TraxRecord>>, 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()
}
}

View File

@ -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();
}
}
});
}

View File

@ -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());
});

View File

@ -30,7 +30,6 @@ pub use welcome_view::WelcomeView;
pub enum View {
Placeholder(PlaceholderView),
#[allow(unused)]
Welcome(WelcomeView),
Historical(HistoricalView),
}

View File

@ -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 = "*"

View File

@ -0,0 +1,328 @@
/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
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 <https://www.gnu.org/licenses/>.
*/
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 "<RFC3339> <Timezone Name>", 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<chrono_tz::Tz>);
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<F>(&self, f: F) -> DateTimeTz
where
F: FnOnce(chrono::DateTime<chrono_tz::Tz>) -> chrono::DateTime<chrono_tz::Tz>,
{
DateTimeTz(f(self.0))
}
}
impl std::str::FromStr for DateTimeTz {
type Err = chrono::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let v: Vec<&str> = s.split_terminator(' ').collect();
if v.len() == 2 {
let tz = v[1].parse::<chrono_tz::Tz>().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<chrono::DateTime<chrono_tz::Tz>> for DateTimeTz {
fn from(dt: chrono::DateTime<chrono_tz::Tz>) -> 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<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
DateTimeTz::from_str(s).or(Err(E::custom(
"string is not a parsable datetime representation".to_owned(),
)))
}
}
impl Serialize for DateTimeTz {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for DateTimeTz {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(DateTimeTzVisitor)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct Steps {
date: DateTimeTz,
steps: u32,
}
impl From<Steps> 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<Weight> 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<TDActivity> 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<String>,
distance: Option<f64>,
duration: Option<f64>,
}
impl From<TimeDistance> 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<SRActivity> 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<u32>,
comments: Option<String>,
}
impl From<SetRep> 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<RDActivity> for DurationWorkoutActivity {
fn from(_: RDActivity) -> Self {
Self::MartialArts
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct RepDuration {
date: DateTimeTz,
activity: RDActivity,
sets: Vec<f64>,
}
impl From<RepDuration> 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<LegacyRecord> for Record<TraxRecord> {
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<ft_core::TraxRecord> = 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::<LegacyRecord>(&line).unwrap();
let record: Record<TraxRecord> = record.into();
println!("{}", serde_json::to_string(&record).unwrap());
count += 1;
}
}
}
}

View File

@ -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,
};

View File

@ -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<u32>,
comments: Option<String>,
pub sets: Vec<u32>,
pub comments: Option<String>,
}
impl Recordable for SetRep {
fn timestamp(&self) -> Timestamp {
Timestamp::Date(self.date)
}
fn tags(&self) -> Vec<String> {
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<FixedOffset>,
pub activity: DurationWorkoutActivity,
pub duration: Option<si::Second<f64>>,
pub comments: Option<String>,
}
impl Recordable for DurationWorkout {
fn timestamp(&self) -> Timestamp {
Timestamp::DateTime(self.datetime)
}
fn tags(&self) -> Vec<String> {
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(),
}
}