Import legacy data. Show welcome screen when no database is configured. #198
|
@ -1069,6 +1069,7 @@ dependencies = [
|
|||
"dimensioned 0.8.0",
|
||||
"emseries",
|
||||
"serde 1.0.193",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
|
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 = "*"
|
||||
|
|
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
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 std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Read},
|
||||
fmt,
|
||||
str::FromStr,
|
||||
};
|
||||
use chrono::SecondsFormat;
|
||||
use chrono_tz::Etc::UTC;
|
||||
use emseries::{Record, Series, Timestamp, RecordId};
|
||||
use ft_core::TraxRecord;
|
||||
use serde::{Deserialize, Serialize, Deserializer, Serializer, de::{self, Visitor}};
|
||||
|
||||
|
||||
|
||||
/// 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: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct Weight {
|
||||
date: DateTimeTz,
|
||||
weight: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
enum TDActivity {
|
||||
Cycling,
|
||||
Running,
|
||||
Walking,
|
||||
Swimming,
|
||||
Rowing,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct TimeDistance {
|
||||
date: DateTimeTz,
|
||||
activity: TDActivity,
|
||||
comments: Option<String>,
|
||||
distance: Option<f64>,
|
||||
duration: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
enum SRActivity {
|
||||
Pushups,
|
||||
Situps,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct SetRep {
|
||||
date: DateTimeTz,
|
||||
activity: SRActivity,
|
||||
sets: Vec<u64>,
|
||||
comments: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
enum RDActivity {
|
||||
MartialArts,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct RepDuration {
|
||||
date: DateTimeTz,
|
||||
activity: RDActivity,
|
||||
sets: Vec<f64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
enum LegacyData {
|
||||
Steps(Steps),
|
||||
Weight(Weight),
|
||||
TimeDistance(TimeDistance),
|
||||
SetRep(SetRep),
|
||||
RepDuration(RepDuration),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct LegacyRecord {
|
||||
id: RecordId,
|
||||
data: LegacyData,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut args = std::env::args();
|
||||
let _ = args.next().unwrap();
|
||||
let input_filename = args.next().unwrap();
|
||||
println!("input filename: {}", input_filename);
|
||||
|
||||
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();
|
||||
println!("[{}] {:?}", count, record);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue