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
3 changed files with 196 additions and 28 deletions
Showing only changes of commit 1527942f9c - Show all commits

View File

@ -14,19 +14,21 @@ 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/>. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::{
fs::File,
io::{BufRead, BufReader, Read},
fmt,
str::FromStr,
};
use chrono::SecondsFormat; use chrono::SecondsFormat;
use chrono_tz::Etc::UTC; use chrono_tz::Etc::UTC;
use emseries::{Record, Series, Timestamp, RecordId}; use dimensioned::si;
use ft_core::TraxRecord; use emseries::{Record, RecordId, Series, Timestamp};
use serde::{Deserialize, Serialize, Deserializer, Serializer, de::{self, Visitor}}; 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 /// 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., /// providing string representation and parsing of the form "<RFC3339> <Timezone Name>", i.e.,
@ -116,11 +118,19 @@ impl<'de> Deserialize<'de> for DateTimeTz {
} }
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
struct Steps { struct Steps {
date: DateTimeTz, date: DateTimeTz,
steps: u64, 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)] #[derive(Clone, Debug, Serialize, Deserialize)]
@ -129,13 +139,34 @@ struct Weight {
weight: f64, 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)] #[derive(Clone, Debug, Serialize, Deserialize)]
enum TDActivity { enum TDActivity {
Cycling, Cycling,
Running,
Walking,
Swimming,
Rowing, 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)] #[derive(Clone, Debug, Serialize, Deserialize)]
@ -147,25 +178,63 @@ struct TimeDistance {
duration: 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)] #[derive(Clone, Debug, Serialize, Deserialize)]
enum SRActivity { enum SRActivity {
Pushups, Pushups,
Situps, 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)] #[derive(Clone, Debug, Serialize, Deserialize)]
struct SetRep { struct SetRep {
date: DateTimeTz, date: DateTimeTz,
activity: SRActivity, activity: SRActivity,
sets: Vec<u64>, sets: Vec<u32>,
comments: Option<String>, 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)] #[derive(Clone, Debug, Serialize, Deserialize)]
enum RDActivity { enum RDActivity {
MartialArts, MartialArts,
} }
impl From<RDActivity> for DurationWorkoutActivity {
fn from(_: RDActivity) -> Self {
Self::MartialArts
}
}
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
struct RepDuration { struct RepDuration {
date: DateTimeTz, date: DateTimeTz,
@ -173,13 +242,24 @@ struct RepDuration {
sets: Vec<f64>, 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)] #[derive(Clone, Debug, Serialize, Deserialize)]
enum LegacyData { enum LegacyData {
Steps(Steps),
Weight(Weight),
TimeDistance(TimeDistance),
SetRep(SetRep),
RepDuration(RepDuration), RepDuration(RepDuration),
SetRep(SetRep),
Steps(Steps),
TimeDistance(TimeDistance),
Weight(Weight),
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
@ -188,11 +268,39 @@ struct LegacyRecord {
data: LegacyData, 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() { fn main() {
let mut args = std::env::args(); let mut args = std::env::args();
let _ = args.next().unwrap(); let _ = args.next().unwrap();
let input_filename = args.next().unwrap(); let input_filename = args.next().unwrap();
println!("input filename: {}", input_filename); 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 input_file = File::open(input_filename).unwrap();
let mut buf_reader = BufReader::new(input_file); let mut buf_reader = BufReader::new(input_file);
@ -211,7 +319,8 @@ fn main() {
Ok(0) => std::process::exit(0), Ok(0) => std::process::exit(0),
Ok(_) => { Ok(_) => {
let record = serde_json::from_str::<LegacyRecord>(&line).unwrap(); let record = serde_json::from_str::<LegacyRecord>(&line).unwrap();
println!("[{}] {:?}", count, record); let record: Record<TraxRecord> = record.into();
println!("{}", serde_json::to_string(&record).unwrap());
count += 1; count += 1;
} }
} }

View File

@ -2,5 +2,7 @@ mod legacy;
mod types; mod types;
pub use types::{ pub use types::{
Steps, TimeDistance, TimeDistanceActivity, TraxRecord, Weight, TIME_DISTANCE_ACTIVITIES, SetRep, SetRepActivity, Steps, TimeDistance, TimeDistanceActivity, TraxRecord, Weight,
DurationWorkoutActivity, DurationWorkout,
SET_REP_ACTIVITIES, TIME_DISTANCE_ACTIVITIES,
}; };

View File

@ -19,17 +19,38 @@ use dimensioned::si;
use emseries::{Recordable, Timestamp}; use emseries::{Recordable, Timestamp};
use serde::{Deserialize, Serialize}; 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 /// SetRep represents workouts like pushups or situps, which involve doing a "set" of a number of
/// actions, resting, and then doing another set. /// actions, resting, and then doing another set.
#[allow(dead_code)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SetRep { pub struct SetRep {
/// I assume that a set/rep workout is only done once in a day. /// 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 /// 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 /// 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]. /// number of pushups. A viable workout would be something like [6, 6, 4, 4, 5].
sets: Vec<u32>, pub sets: Vec<u32>,
comments: Option<String>, 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. /// 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. /// 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 {
TimeDistance(TimeDistance), DurationWorkout(DurationWorkout),
SetRep(SetRep),
Steps(Steps), Steps(Steps),
TimeDistance(TimeDistance),
Weight(Weight), Weight(Weight),
} }
@ -164,8 +219,10 @@ impl Recordable for TraxRecord {
fn timestamp(&self) -> Timestamp { fn timestamp(&self) -> Timestamp {
match self { match self {
TraxRecord::TimeDistance(rec) => Timestamp::DateTime(rec.datetime), TraxRecord::TimeDistance(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::SetRep(rec) => rec.timestamp(),
TraxRecord::Steps(rec) => rec.timestamp(), TraxRecord::Steps(rec) => rec.timestamp(),
TraxRecord::Weight(rec) => rec.timestamp(), TraxRecord::Weight(rec) => rec.timestamp(),
TraxRecord::DurationWorkout(rec) => rec.timestamp(),
} }
} }