Compare commits
No commits in common. "1527942f9c73887899229e6f1186d9c602825ac2" and "aed473520912019a9926f60cb6a47e21415f0e54" have entirely different histories.
1527942f9c
...
aed4735209
|
@ -1069,7 +1069,6 @@ dependencies = [
|
||||||
"dimensioned 0.8.0",
|
"dimensioned 0.8.0",
|
||||||
"emseries",
|
"emseries",
|
||||||
"serde 1.0.193",
|
"serde 1.0.193",
|
||||||
"serde_json",
|
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,198 @@
|
||||||
|
/*
|
||||||
|
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())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,7 +11,6 @@ chrono-tz = { version = "0.8" }
|
||||||
dimensioned = { version = "0.8", features = [ "serde" ] }
|
dimensioned = { version = "0.8", features = [ "serde" ] }
|
||||||
emseries = { path = "../../emseries" }
|
emseries = { path = "../../emseries" }
|
||||||
serde = { version = "1", features = [ "derive" ] }
|
serde = { version = "1", features = [ "derive" ] }
|
||||||
serde_json = { version = "1" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "*"
|
tempfile = "*"
|
||||||
|
|
|
@ -1,328 +0,0 @@
|
||||||
/*
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,7 +2,5 @@ mod legacy;
|
||||||
|
|
||||||
mod types;
|
mod types;
|
||||||
pub use types::{
|
pub use types::{
|
||||||
SetRep, SetRepActivity, Steps, TimeDistance, TimeDistanceActivity, TraxRecord, Weight,
|
Steps, TimeDistance, TimeDistanceActivity, TraxRecord, Weight, TIME_DISTANCE_ACTIVITIES,
|
||||||
DurationWorkoutActivity, DurationWorkout,
|
|
||||||
SET_REP_ACTIVITIES, TIME_DISTANCE_ACTIVITIES,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,38 +19,17 @@ 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.
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[allow(dead_code)]
|
||||||
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.
|
||||||
pub date: NaiveDate,
|
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].
|
||||||
pub sets: Vec<u32>,
|
sets: Vec<u32>,
|
||||||
pub comments: Option<String>,
|
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.
|
||||||
|
@ -141,45 +120,11 @@ 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 {
|
||||||
DurationWorkout(DurationWorkout),
|
|
||||||
SetRep(SetRep),
|
|
||||||
Steps(Steps),
|
|
||||||
TimeDistance(TimeDistance),
|
TimeDistance(TimeDistance),
|
||||||
|
Steps(Steps),
|
||||||
Weight(Weight),
|
Weight(Weight),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,10 +164,8 @@ 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(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue