Add cli & lib crates

This commit is contained in:
2021-04-22 15:13:28 +02:00
parent e39cffa3f6
commit 3a9ecc398a
53 changed files with 5065 additions and 99 deletions

View File

@ -0,0 +1,299 @@
use crate::auth::Authorized;
use crate::database::latest::trees::{category, session};
use crate::database::util::category::get_category;
use crate::status_json::StatusJson;
use bincode::deserialize;
use chrono::{DateTime, Local, Timelike};
use rocket::http::Status;
use rocket::{get, State};
use rocket_contrib::templates::Template;
use rocket_contrib::uuid::Uuid;
use serde_derive::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::{BTreeMap, HashMap};
#[derive(Debug, Serialize, Deserialize)]
struct CategoryStatsContext {
category_id: category::K,
category: category::V,
last_session_start: Option<DateTime<Local>>,
secs_last_session: u64,
secs_last_week: u64,
secs_last_month: u64,
bars_max: f64,
bars: Vec<(u32, f64, f64)>,
}
fn sum_sessions<'a>(iter: impl IntoIterator<Item = &'a session::V>) -> u64 {
iter.into_iter()
.map(|session| session.ended - session.started)
.map(|duration| duration.num_seconds() as u64)
.sum()
}
#[get("/stats/<category_uuid>")]
pub fn single_stats(
_auth: Authorized,
category_uuid: Uuid,
db: State<'_, sled::Db>,
) -> Result<Template, StatusJson> {
let categories_tree = db.open_tree(category::NAME)?;
let sessions_tree = db.open_tree(session::NAME)?;
let category: category::V =
get_category(&categories_tree, &category_uuid)?.ok_or(Status::NotFound)?;
let sessions: HashMap<session::K, session::V> = sessions_tree
.iter()
.map(|result| {
result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v))))
})
.collect::<Result<Result<_, _>, _>>()??;
let my_sessions = sessions
.values()
.filter(|session| session.category == *category_uuid);
let last_session = my_sessions.clone().max_by_key(|session| &session.started);
let secs_last_session = sum_sessions(last_session);
let now = Local::now();
let secs_last_week = sum_sessions(
my_sessions
.clone()
.filter(|session| (now - session.started) <= chrono::Duration::days(7)),
);
let secs_last_month = sum_sessions(
my_sessions
.clone()
.filter(|session| (now - session.started) <= chrono::Duration::days(30)),
);
let mut stats_per_hour = compute_percentage_per_hour(my_sessions);
let biggest_hour = *stats_per_hour
.values()
.max_by(|f1, f2| match () {
_ if f1 == f2 => Ordering::Equal,
_ if f1 > f2 => Ordering::Greater,
_ => Ordering::Less,
})
.unwrap_or(&1.0);
let context = CategoryStatsContext {
category_id: *category_uuid,
category,
last_session_start: last_session.map(|session| session.started),
secs_last_session,
secs_last_week,
secs_last_month,
bars_max: biggest_hour,
bars: (0..24)
.map(|hour| {
let percentage = *stats_per_hour.entry(hour).or_default();
(hour, percentage, biggest_hour - percentage)
})
.collect(),
};
Ok(Template::render("stats_single", &context))
}
#[get("/stats")]
pub fn all_stats(_auth: Authorized, db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
#[derive(Debug, Serialize, Deserialize)]
struct StatsContext {
categories_stats: Vec<CategoryStatsContext>,
}
let now = Local::now();
let categories_tree = db.open_tree(category::NAME)?;
let sessions_tree = db.open_tree(session::NAME)?;
let categories: HashMap<category::K, category::V> = categories_tree
.iter()
.map(|result| {
result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v))))
})
.collect::<Result<Result<_, _>, _>>()??;
let sessions: HashMap<session::K, session::V> = sessions_tree
.iter()
.map(|result| {
result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v))))
})
.collect::<Result<Result<_, _>, _>>()??;
let mut categories_stats: Vec<_> = categories
.into_iter()
.map(|(category_id, category)| {
let my_sessions = sessions
.values()
.filter(|session| session.category == category_id);
let last_session = my_sessions.clone().max_by_key(|session| &session.started);
let secs_last_session = sum_sessions(last_session);
let secs_last_week = sum_sessions(
my_sessions
.clone()
.filter(|session| (now - session.started) <= chrono::Duration::days(7)),
);
let secs_last_month = sum_sessions(
my_sessions
.clone()
.filter(|session| (now - session.started) <= chrono::Duration::days(30)),
);
let mut stats_per_hour = compute_percentage_per_hour(my_sessions);
let biggest_hour = *stats_per_hour
.values()
.max_by(|f1, f2| match () {
_ if f1 == f2 => Ordering::Equal,
_ if f1 > f2 => Ordering::Greater,
_ => Ordering::Less,
})
.unwrap_or(&1.0);
CategoryStatsContext {
category_id,
category,
last_session_start: last_session.map(|session| session.started),
secs_last_session,
secs_last_week,
secs_last_month,
bars_max: biggest_hour,
bars: (0..24)
.map(|hour| {
let percentage = *stats_per_hour.entry(hour).or_default();
(hour, percentage, biggest_hour - percentage)
})
.collect(),
}
})
.collect();
categories_stats.sort_by(|a, b| a.category.name.cmp(&b.category.name));
let context = StatsContext { categories_stats };
Ok(Template::render("stats_all", &context))
}
fn compute_percentage_per_hour<'a, I>(sessions: I) -> BTreeMap<u32, f64>
where
I: Iterator<Item = &'a session::V>,
{
let mut stats_per_hour = BTreeMap::new();
for session in sessions {
let an_hour = chrono::Duration::minutes(60);
let hour_of = |time: DateTime<Local>| {
time.with_minute(0)
.and_then(|time| time.with_second(0))
.and_then(|time| time.with_nanosecond(0))
.unwrap()
};
let next_hour_of =
|time: DateTime<Local>| hour_of(time).checked_add_signed(an_hour).unwrap();
let mut add_hour_stats =
|time: DateTime<Local>, hours| *stats_per_hour.entry(time.hour()).or_default() += hours;
let mut hour = hour_of(session.started);
loop {
if hour_of(session.started) == hour {
let minutes_started = (session.started - hour).num_minutes() as u32;
if hour_of(session.ended) == hour {
let minutes_ended = (session.ended - hour).num_minutes() as u32;
let minutes_last_hour = minutes_ended - minutes_started;
add_hour_stats(hour, minutes_last_hour as f64);
break;
} else {
let minutes_first_hour = 60 - minutes_started;
add_hour_stats(hour, minutes_first_hour as f64);
}
} else if hour_of(session.ended) == hour {
let minutes_last_hour = (session.ended - hour).num_minutes() as u32;
add_hour_stats(hour, minutes_last_hour as f64);
break;
} else {
add_hour_stats(hour, 60.0);
}
hour = next_hour_of(hour);
}
}
let sum_weight: f64 = stats_per_hour.values().sum();
for weight in stats_per_hour.values_mut() {
*weight = *weight * 100.0 / sum_weight;
*weight = (*weight * 10.0).trunc() / 10.0;
}
stats_per_hour
}
#[cfg(test)]
mod test {
use super::*;
use crate::database::latest::trees::sessions;
use chrono::{DateTime, Local};
#[test]
fn test_compute_percentage_per_hour() {
let today = Local::now();
let test_data = vec![
(
vec![((11, 20), (13, 20))],
vec![(11, 33.3), (12, 50.0), (13, 16.6)],
),
(vec![((09, 00), (09, 01))], vec![(09, 100.0)]),
(vec![((09, 00), (09, 59))], vec![(09, 100.0)]),
(
vec![((13, 00), (16, 00))],
vec![(13, 33.3), (14, 33.3), (15, 33.3), (16, 0.0)],
),
];
for (sessions, expected) in test_data {
let sessions: Vec<_> = sessions
.into_iter()
.map(|((h1, m1), (h2, m2))| {
let set_hm = |t: DateTime<Local>, h, m| {
t.with_hour(h)
.and_then(|t| t.with_minute(m))
.and_then(|t| t.with_second(0))
.and_then(|t| t.with_nanosecond(0))
.unwrap()
};
session::V {
category: Default::default(),
deleted: false,
started: set_hm(today, h1, m1),
ended: set_hm(today, h2, m2),
}
})
.collect();
let percentages = compute_percentage_per_hour(sessions.iter());
println!("{:#?}", percentages);
assert!(percentages.into_iter().eq(expected.into_iter()));
}
}
}