pub use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use std::fmt;
use std::path::PathBuf;
pub type FixpNum = fixed::types::U54F10;
pub struct Handle {
base: PathBuf,
name: String,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("CSV error: {0:?}")]
Csv(#[from] csv::Error),
#[error("CSV into_inner/flush error: {}", .0.error())]
CsvIntoInner(#[from] csv::IntoInnerError<csv::Writer<tempfile::NamedTempFile>>),
#[error("persist error: {}", .0.error)]
Persist(#[from] tempfile::PersistError),
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct LogEntry {
pub date: NaiveDate,
pub cmd: LogCommand,
pub value: Option<FixpNum>,
pub comment: String,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogCommand {
#[serde(rename = "brk")]
Broken,
#[serde(rename = "rst")]
Reset,
#[serde(rename = "inc")]
Increase,
#[serde(rename = "dec")]
Decrease,
}
impl Handle {
pub fn new(base: PathBuf, name: String) -> Self {
Self { base, name }
}
pub fn name(&self) -> &str {
&self.name
}
pub fn log_path(&self) -> PathBuf {
let mut path = self.base.join(&self.name);
path.set_extension("metercsv");
path
}
pub fn read_logdata(&self) -> Result<BTreeSet<LogEntry>, Error> {
csv::ReaderBuilder::new()
.delimiter(0x1f)
.from_path(self.log_path())?
.deserialize()
.map(|i| i.map_err(Into::into))
.collect()
}
pub fn write_logdata(&self, data: &BTreeSet<LogEntry>) -> Result<(), Error> {
use std::io::Write;
let f = tempfile::NamedTempFile::new_in(&self.base).map_err(csv::Error::from)?;
let mut f = csv::WriterBuilder::new().delimiter(0x1f).from_writer(f);
for i in data {
f.serialize(i)?;
}
let mut f = f.into_inner()?;
f.flush().map_err(csv::Error::from)?;
f.persist(self.log_path())?;
Ok(())
}
}
#[derive(Clone, Copy)]
pub enum ValidateErrorKind {
InvalidValueDir,
}
pub struct ValidateError<'e> {
a: &'e LogEntry,
b: &'e LogEntry,
kind: ValidateErrorKind,
}
impl fmt::Display for ValidateError<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use ValidateErrorKind as K;
writeln!(
f,
"log error: {}\n a = {:?}\n b = {:?}",
match self.kind {
K::InvalidValueDir => "invalid value direction",
},
self.a,
self.b
)
}
}
pub fn validate_logdata<'a>(
logdata: &'a BTreeSet<LogEntry>,
limit_date: Option<&NaiveDate>,
) -> Vec<ValidateError<'a>> {
let ldv: Vec<&_> = logdata.iter().collect();
ldv.windows(2)
.map(|i| match i {
[a, b] => (a, b),
_ => unreachable!(),
})
.filter(|(a, b)| {
if let Some(x) = limit_date {
if !(&a.date == x || &b.date == x) {
return false;
}
}
true
})
.filter_map(|(a, b)| {
use LogCommand as C;
use ValidateErrorKind as K;
Some(ValidateError {
a,
b,
kind: match (a.cmd, b.cmd) {
(C::Increase, C::Increase) if a.value >= b.value => K::InvalidValueDir,
(C::Decrease, C::Decrease) if a.value <= b.value => K::InvalidValueDir,
_ => return None,
},
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn paths() {
assert_eq!(
Handle::new(PathBuf::from("../meters"), "whubbs".to_string()).log_path(),
PathBuf::from("../meters/whubbs.metercsv")
);
}
#[test]
fn basic() {
let dir = tempfile::tempdir().expect("unable to create temporary directory");
let handle = Handle::new(
dir.path().to_path_buf(),
"basic-countermeter-test".to_string(),
);
let data = vec![
LogEntry {
date: NaiveDate::from_ymd(2021, 10, 10),
cmd: LogCommand::Reset,
value: None,
comment: "it starts happening\"!!".to_string(),
},
LogEntry {
date: NaiveDate::from_ymd(2021, 10, 11),
cmd: LogCommand::Increase,
value: Some(fixed_macro::fixed!(0.010: U54F10)),
comment: String::new(),
},
LogEntry {
date: NaiveDate::from_ymd(2021, 10, 12),
cmd: LogCommand::Increase,
value: Some(fixed_macro::fixed!(0.011: U54F10)),
comment: String::new(),
},
LogEntry {
date: NaiveDate::from_ymd(2021, 10, 13),
cmd: LogCommand::Broken,
value: Some(fixed_macro::fixed!(0.012: U54F10)),
comment: "water damage, ouch...".to_string(),
},
LogEntry {
date: NaiveDate::from_ymd(2021, 10, 14),
cmd: LogCommand::Decrease,
value: Some(fixed_macro::fixed!(0.010: U54F10)),
comment: String::new(),
},
]
.into_iter()
.collect::<BTreeSet<_>>();
handle.write_logdata(&data).expect("unable to write data");
{
let datv = std::fs::read(handle.log_path()).expect("unable to read data (1)");
assert_eq!(
std::str::from_utf8(&datv[..]).expect("utf8?"),
r#"datecmdvaluecomment
2021-10-10rst"it starts happening""!!"
2021-10-11inc0.01
2021-10-12inc0.011
2021-10-13brk0.012water damage, ouch...
2021-10-14dec0.01
"#
);
}
assert_eq!(
data,
handle.read_logdata().expect("unable to read data (2)")
);
}
}