416 lines
15 KiB
Rust
416 lines
15 KiB
Rust
/*
|
|
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/>.
|
|
*/
|
|
|
|
#[macro_use]
|
|
extern crate serde_derive;
|
|
|
|
extern crate chrono;
|
|
extern crate chrono_tz;
|
|
extern crate dimensioned;
|
|
extern crate emseries;
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use chrono::prelude::*;
|
|
use chrono_tz::Etc::UTC;
|
|
use dimensioned::si::{Kilogram, Meter, Second, KG, M, S};
|
|
|
|
use emseries::*;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
|
struct Distance(Meter<f64>);
|
|
|
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
|
struct Duration(Second<f64>);
|
|
|
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
|
struct BikeTrip {
|
|
datetime: DateTimeTz,
|
|
distance: Distance,
|
|
duration: Duration,
|
|
comments: String,
|
|
}
|
|
|
|
impl Recordable for BikeTrip {
|
|
fn timestamp(&self) -> Timestamp {
|
|
self.datetime.clone().into()
|
|
}
|
|
fn tags(&self) -> Vec<String> {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
fn mk_trips() -> [BikeTrip; 5] {
|
|
[
|
|
BikeTrip {
|
|
datetime: DateTimeTz(UTC.ymd(2011, 10, 29).and_hms(0, 0, 0)),
|
|
distance: Distance(58741.055 * M),
|
|
duration: Duration(11040.0 * S),
|
|
comments: String::from("long time ago"),
|
|
},
|
|
BikeTrip {
|
|
datetime: DateTimeTz(UTC.ymd(2011, 10, 31).and_hms(0, 0, 0)),
|
|
distance: Distance(17702.0 * M),
|
|
duration: Duration(2880.0 * S),
|
|
comments: String::from("day 2"),
|
|
},
|
|
BikeTrip {
|
|
datetime: DateTimeTz(UTC.ymd(2011, 11, 02).and_hms(0, 0, 0)),
|
|
distance: Distance(41842.945 * M),
|
|
duration: Duration(7020.0 * S),
|
|
comments: String::from("Do Some Distance!"),
|
|
},
|
|
BikeTrip {
|
|
datetime: DateTimeTz(UTC.ymd(2011, 11, 04).and_hms(0, 0, 0)),
|
|
distance: Distance(34600.895 * M),
|
|
duration: Duration(5580.0 * S),
|
|
comments: String::from("I did a lot of distance back then"),
|
|
},
|
|
BikeTrip {
|
|
datetime: DateTimeTz(UTC.ymd(2011, 11, 05).and_hms(0, 0, 0)),
|
|
distance: Distance(6437.376 * M),
|
|
duration: Duration(960.0 * S),
|
|
comments: String::from("day 5"),
|
|
},
|
|
]
|
|
}
|
|
|
|
fn run_test<T>(test: T) -> ()
|
|
where
|
|
T: FnOnce(tempfile::TempPath),
|
|
{
|
|
let tmp_file = tempfile::NamedTempFile::new().expect("temporary path created");
|
|
let tmp_path = tmp_file.into_temp_path();
|
|
test(tmp_path);
|
|
}
|
|
|
|
fn run<T>(test: T) -> ()
|
|
where
|
|
T: FnOnce(Series<BikeTrip>),
|
|
{
|
|
let tmp_file = tempfile::NamedTempFile::new().expect("temporary path created");
|
|
let tmp_path = tmp_file.into_temp_path();
|
|
let ts: Series<BikeTrip> = Series::open(&tmp_path.to_string_lossy())
|
|
.expect("the time series should open correctly");
|
|
test(ts);
|
|
}
|
|
|
|
#[test]
|
|
pub fn can_add_and_retrieve_entries() {
|
|
run(|mut series| {
|
|
let trips = mk_trips();
|
|
let uuid = series
|
|
.put(trips[0].clone())
|
|
.expect("expect a successful put");
|
|
let record_res = series.get(&uuid);
|
|
|
|
for trip in &trips[1..=4] {
|
|
series.put(trip.clone()).expect("expect a successful put");
|
|
}
|
|
|
|
match record_res {
|
|
None => assert!(false, "There should have been a value here"),
|
|
Some(tr) => {
|
|
assert_eq!(
|
|
tr.timestamp(),
|
|
DateTimeTz(UTC.ymd(2011, 10, 29).and_hms(0, 0, 0)).into()
|
|
);
|
|
assert_eq!(tr.duration, Duration(11040.0 * S));
|
|
assert_eq!(tr.comments, String::from("long time ago"));
|
|
assert_eq!(tr, trips[0]);
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
pub fn can_search_for_an_entry_with_exact_time() {
|
|
run_test(|path| {
|
|
let trips = mk_trips();
|
|
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
|
.expect("expect the time series to open correctly");
|
|
|
|
for trip in &trips[0..=4] {
|
|
ts.put(trip.clone()).expect("expect a successful put");
|
|
}
|
|
|
|
let v: Vec<(&UniqueId, &BikeTrip)> = ts
|
|
.search(exact_time(
|
|
DateTimeTz(UTC.ymd(2011, 10, 31).and_hms(0, 0, 0)).into(),
|
|
))
|
|
.collect();
|
|
assert_eq!(v.len(), 1);
|
|
assert_eq!(*v[0].1, trips[1]);
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
pub fn can_get_entries_in_time_range() {
|
|
run_test(|path| {
|
|
let trips = mk_trips();
|
|
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
|
.expect("expect the time series to open correctly");
|
|
|
|
for trip in &trips[0..=4] {
|
|
ts.put(trip.clone()).expect("expect a successful put");
|
|
}
|
|
|
|
let v: Vec<(&UniqueId, &BikeTrip)> = ts.search_sorted(
|
|
time_range(
|
|
DateTimeTz(UTC.ymd(2011, 10, 31).and_hms(0, 0, 0)).into(),
|
|
true,
|
|
DateTimeTz(UTC.ymd(2011, 11, 04).and_hms(0, 0, 0)).into(),
|
|
true,
|
|
),
|
|
|l, r| l.1.timestamp().cmp(&r.1.timestamp()),
|
|
);
|
|
assert_eq!(v.len(), 3);
|
|
assert_eq!(*v[0].1, trips[1]);
|
|
assert_eq!(*v[1].1, trips[2]);
|
|
assert_eq!(*v[2].1, trips[3]);
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
pub fn persists_and_reads_an_entry() {
|
|
run_test(|path| {
|
|
let trips = mk_trips();
|
|
|
|
{
|
|
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
|
.expect("expect the time series to open correctly");
|
|
|
|
for trip in &trips[0..=4] {
|
|
ts.put(trip.clone()).expect("expect a successful put");
|
|
}
|
|
}
|
|
|
|
{
|
|
let ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
|
.expect("expect the time series to open correctly");
|
|
let v: Vec<(&UniqueId, &BikeTrip)> = ts.search_sorted(
|
|
time_range(
|
|
DateTimeTz(UTC.ymd(2011, 10, 31).and_hms(0, 0, 0)).into(),
|
|
true,
|
|
DateTimeTz(UTC.ymd(2011, 11, 04).and_hms(0, 0, 0)).into(),
|
|
true,
|
|
),
|
|
|l, r| l.1.timestamp().cmp(&r.1.timestamp()),
|
|
);
|
|
assert_eq!(v.len(), 3);
|
|
assert_eq!(*v[0].1, trips[1]);
|
|
assert_eq!(*v[1].1, trips[2]);
|
|
assert_eq!(*v[2].1, trips[3]);
|
|
}
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
pub fn can_write_to_existing_file() {
|
|
run_test(|path| {
|
|
let trips = mk_trips();
|
|
|
|
{
|
|
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
|
.expect("expect the time series to open correctly");
|
|
|
|
for trip in &trips[0..=2] {
|
|
ts.put(trip.clone()).expect("expect a successful put");
|
|
}
|
|
}
|
|
|
|
{
|
|
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
|
.expect("expect the time series to open correctly");
|
|
let v: Vec<(&UniqueId, &BikeTrip)> = ts.search_sorted(
|
|
time_range(
|
|
DateTimeTz(UTC.ymd(2011, 10, 31).and_hms(0, 0, 0)).into(),
|
|
true,
|
|
DateTimeTz(UTC.ymd(2011, 11, 04).and_hms(0, 0, 0)).into(),
|
|
true,
|
|
),
|
|
|l, r| l.1.timestamp().cmp(&r.1.timestamp()),
|
|
);
|
|
assert_eq!(v.len(), 2);
|
|
assert_eq!(*v[0].1, trips[1]);
|
|
assert_eq!(*v[1].1, trips[2]);
|
|
ts.put(trips[3].clone()).expect("expect a successful put");
|
|
ts.put(trips[4].clone()).expect("expect a successful put");
|
|
}
|
|
|
|
{
|
|
let ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
|
.expect("expect the time series to open correctly");
|
|
let v: Vec<(&UniqueId, &BikeTrip)> = ts.search_sorted(
|
|
time_range(
|
|
DateTimeTz(UTC.ymd(2011, 10, 31).and_hms(0, 0, 0)).into(),
|
|
true,
|
|
DateTimeTz(UTC.ymd(2011, 11, 05).and_hms(0, 0, 0)).into(),
|
|
true,
|
|
),
|
|
|l, r| l.1.timestamp().cmp(&r.1.timestamp()),
|
|
);
|
|
assert_eq!(v.len(), 4);
|
|
assert_eq!(*v[0].1, trips[1]);
|
|
assert_eq!(*v[1].1, trips[2]);
|
|
assert_eq!(*v[2].1, trips[3]);
|
|
assert_eq!(*v[3].1, trips[4]);
|
|
}
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
pub fn can_overwrite_existing_entry() {
|
|
run_test(|path| {
|
|
let trips = mk_trips();
|
|
|
|
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
|
.expect("expect the time series to open correctly");
|
|
|
|
ts.put(trips[0].clone()).expect("expect a successful put");
|
|
ts.put(trips[1].clone()).expect("expect a successful put");
|
|
let trip_id = ts.put(trips[2].clone()).expect("expect a successful put");
|
|
|
|
match ts.get(&trip_id) {
|
|
None => assert!(false, "record not found"),
|
|
Some(mut trip) => {
|
|
trip.distance = Distance(50000.0 * M);
|
|
ts.update(trip_id.clone(), trip)
|
|
.expect("expect record to update");
|
|
}
|
|
};
|
|
|
|
match ts.get(&trip_id) {
|
|
None => assert!(false, "record not found"),
|
|
Some(trip) => {
|
|
assert_eq!(
|
|
trip.datetime,
|
|
DateTimeTz(UTC.ymd(2011, 11, 02).and_hms(0, 0, 0))
|
|
);
|
|
assert_eq!(trip.distance, Distance(50000.0 * M));
|
|
assert_eq!(trip.duration, Duration(7020.0 * S));
|
|
assert_eq!(trip.comments, String::from("Do Some Distance!"));
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
pub fn record_overwrites_get_persisted() {
|
|
run_test(|path| {
|
|
let trips = mk_trips();
|
|
|
|
{
|
|
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
|
.expect("expect the time series to open correctly");
|
|
|
|
ts.put(trips[0].clone()).expect("expect a successful put");
|
|
ts.put(trips[1].clone()).expect("expect a successful put");
|
|
let trip_id = ts.put(trips[2].clone()).expect("expect a successful put");
|
|
|
|
match ts.get(&trip_id) {
|
|
None => assert!(false, "record not found"),
|
|
Some(mut trip) => {
|
|
trip.distance = Distance(50000.0 * M);
|
|
ts.update(trip_id, trip).expect("expect record to update");
|
|
}
|
|
};
|
|
}
|
|
|
|
{
|
|
let ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
|
.expect("expect the time series to open correctly");
|
|
|
|
let trips: Vec<(&UniqueId, &BikeTrip)> = ts.records().collect();
|
|
assert_eq!(trips.len(), 3);
|
|
|
|
let trips: Vec<(&UniqueId, &BikeTrip)> = ts
|
|
.search(exact_time(
|
|
DateTimeTz(UTC.ymd(2011, 11, 02).and_hms(0, 0, 0)).into(),
|
|
))
|
|
.collect();
|
|
assert_eq!(trips.len(), 1);
|
|
assert_eq!(
|
|
trips[0].1.datetime,
|
|
DateTimeTz(UTC.ymd(2011, 11, 02).and_hms(0, 0, 0))
|
|
);
|
|
assert_eq!(trips[0].1.distance, Distance(50000.0 * M));
|
|
assert_eq!(trips[0].1.duration, Duration(7020.0 * S));
|
|
assert_eq!(trips[0].1.comments, String::from("Do Some Distance!"));
|
|
}
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
pub fn can_delete_an_entry() {
|
|
run_test(|path| {
|
|
let trips = mk_trips();
|
|
|
|
{
|
|
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
|
.expect("expect the time series to open correctly");
|
|
let trip_id = ts.put(trips[0].clone()).expect("expect a successful put");
|
|
ts.put(trips[1].clone()).expect("expect a successful put");
|
|
ts.put(trips[2].clone()).expect("expect a successful put");
|
|
|
|
ts.delete(&trip_id).expect("successful delete");
|
|
|
|
let recs: Vec<(&UniqueId, &BikeTrip)> = ts.records().collect();
|
|
assert_eq!(recs.len(), 2);
|
|
}
|
|
|
|
{
|
|
let ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
|
.expect("expect the time series to open correctly");
|
|
let recs: Vec<(&UniqueId, &BikeTrip)> = ts.records().collect();
|
|
assert_eq!(recs.len(), 2);
|
|
}
|
|
})
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
|
pub struct Weight(Kilogram<f64>);
|
|
|
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
|
pub struct WeightRecord {
|
|
pub date: chrono::NaiveDate,
|
|
pub weight: Weight,
|
|
}
|
|
|
|
impl Recordable for WeightRecord {
|
|
fn timestamp(&self) -> Timestamp {
|
|
self.date.into()
|
|
}
|
|
|
|
fn tags(&self) -> Vec<String> {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
/*
|
|
#[test]
|
|
pub fn legacy_file_load() {
|
|
let ts: Series<WeightRecord> =
|
|
Series::open("fixtures/weight.json").expect("legacy series should open correctly");
|
|
|
|
let uid = "3330c5b0-783f-4919-b2c4-8169c38f65ff"
|
|
.parse()
|
|
.expect("something is wrong with this ID");
|
|
let rec = ts.get(&uid);
|
|
match rec {
|
|
None => assert!(false, "no record found"),
|
|
Some(rec) => assert_eq!(rec.weight, Weight(77.79109 * KG)),
|
|
}
|
|
}
|
|
*/
|
|
}
|