use std::io::Write;
use conciliator::{
Buffer,
Inline,
print::Formatter,
Paint
};
use rusqlite::{
Row,
Result as SqlResult,
Error as SqlError
};
use greg::{
Point,
Span
};
use crate::Nunc;
use crate::core::{
Tree,
Plan
};
use crate::util::format::LookupFormatter;
use super::{
Activity,
activity::ActOrCat,
Category
};
#[derive(Debug, Eq, PartialEq)]
pub struct Streak {
pub id: u64,
pub interval: Span,
pub begin: Point,
pub end: Option<Point>,
pub added: Point,
pub removed: Option<Point>,
pub rule: StreakRule
}
#[derive(Debug, Eq, PartialEq)]
pub enum StreakRule {
Activity {
activity_id: u64,
min_duration: Span
},
Category {
category_id: u64,
min_duration: Span
}
}
pub struct StreakIteration {
pub streak_id: u64,
pub number: u64,
pub value: u64
}
pub mod state {
use super::*;
#[derive(Debug, Eq, PartialEq)]
pub struct Raw {
pub streak_id: u64,
pub length: u64,
pub next_iteration_at: Point,
pub fulfilled: Span,
pub unfulfilled: Span
}
pub struct Format {
pub(crate) subject: ActOrCat,
pub(crate) length: u64,
pub(crate) next_iteration_at: Point,
pub(crate) required: Span,
pub(crate) fulfilled: Span
}
impl Raw {
pub fn make_inline(
&self,
nunc: &Nunc)
-> SqlResult<Format>
{
let streak = nunc.get_streak(self.streak_id)?;
let fmt = Format {
subject: streak.rule.get_subject(nunc)?,
length: self.length,
next_iteration_at: self.next_iteration_at,
required: streak.rule.required(),
fulfilled: self.fulfilled
};
Ok(fmt)
}
}
impl Inline for Format {
fn inline(&self, buffer: &mut Buffer) {
let padding = 10usize.saturating_sub(self.subject.len());
buffer
.push_plain("[")
.push_alpha_bold(&format_args!("{:4} ▲ ", self.length))
.push(&self.subject)
.push_plain(&format_args!("{:<padding$}\t", "]"));
let until_next = self.next_iteration_at - Point::now();
if self.required > self.fulfilled {
buffer
.push_bold("[ ")
.push_iota_bold("✖")
.push_bold(" ] ");
if self.fulfilled.seconds > 0 {
let percent = self.fulfilled.seconds as f32
/ self.required.seconds as f32
* 100.0;
write!(buffer, "{percent:.0}% done, ").unwrap();
}
write!(
buffer,
"{until_next:#.2} left to do {:#}",
self.required.checked_sub(self.fulfilled).unwrap()
).unwrap();
}
else {
buffer
.push_bold("[ ")
.push_alpha_bold("✔")
.push_bold(" ] ");
write!(
buffer,
"did {:#}, next in {until_next:#.2}",
self.fulfilled
).unwrap();
}
}
}
}
impl<'r> TryFrom<&Row<'r>> for Streak {
type Error = SqlError;
fn try_from(value: &Row<'r>) -> Result<Self, Self::Error> {
let (
id,
interval,
begin,
end,
added,
removed,
rule_type,
activity_id,
min_duration,
category_id
) = value.try_into()?;
let rule_type: String = rule_type;
let rule = match (rule_type.as_str(), activity_id, category_id) {
("activity", Some(activity_id), None) => StreakRule::Activity {
activity_id,
min_duration
},
("category", None, Some(category_id)) => StreakRule::Category {
category_id,
min_duration
},
_ => unreachable!("database constraint violated")
};
Ok( Streak {id, interval, begin, end, added, removed, rule} )
}
}
impl<'db> Formatter<&Streak> for LookupFormatter<'db> {
fn format(&mut self, buf: &mut Buffer, streak: &Streak) {
let subject = streak.rule.get_subject(self.nunc).unwrap();
buf.push(subject)
.push(": do ")
.push(streak.rule.required())
.push(" every ")
.push(streak.interval)
.push(", ");
let (scale, _count) = streak.interval.scale_div();
let begin = self.format_by(scale, streak.begin);
match streak.end.map(|p| self.cal.lookup(p)) {
Some(end) => buf
.push("from ")
.push(begin)
.push(" until ")
.push(end),
None => buf
.push("since ")
.push(begin)
};
}
}
impl StreakRule {
pub fn activity(act: &Activity, min_duration: Span) -> Self {
Self::Activity {
activity_id: act.id,
min_duration
}
}
pub fn category(cat: &Category, min_duration: Span) -> Self {
Self::Category {
category_id: cat.id,
min_duration
}
}
pub fn required(&self) -> Span {
match self {
Self::Activity {min_duration, ..} => *min_duration,
Self::Category {min_duration, ..} => *min_duration
}
}
pub(crate) fn get_subject(
&self,
tree: &Nunc)
-> SqlResult<ActOrCat>
{
match *self {
Self::Activity {activity_id, ..} => tree
.get_activity(activity_id)
.map(ActOrCat::Activity),
Self::Category {category_id, ..} => tree
.get_category(category_id)
.map(ActOrCat::Category)
}
}
pub(crate) fn get_query(&self) -> (&'static str, u64, Span) {
match *self {
Self::Activity {activity_id, min_duration} => {
let sql =
"SELECT \
SUM( \
MIN(?3, IFNULL(stop, ?3)) \
- MAX(?2, start) \
) \
FROM entry \
WHERE activity_id = ?1 \
AND ( \
(?2 <= start AND start < ?3) \
OR (?2 < stop AND stop <= ?3) \
OR (start < ?2 AND ?3 < stop) \
OR (start <= ?2 AND stop IS NULL) \
) \
GROUP BY activity_id";
(sql, activity_id, min_duration)
},
Self::Category {category_id, min_duration} => {
let sql =
"SELECT \
SUM( \
MIN(?3, IFNULL(stop, ?3)) \
- MAX(?2, start) \
) \
FROM entry \
JOIN activity ON entry.activity_id == activity.id \
JOIN activity_belongs_to \
ON activity_belongs_to.activity_id == activity.id \
JOIN category_belongs_to \
ON activity_belongs_to.category_id \
== category_belongs_to.category_id \
WHERE ( \
activity_belongs_to.category_id == ?1 \
OR category_belongs_to.path LIKE '%/' || ?1 || '/%' \
) \
AND ( \
(?2 <= start AND start < ?3) \
OR (?2 < stop AND stop <= ?3) \
OR (start < ?2 AND ?3 < stop) \
OR (start <= ?2 AND stop IS NULL) \
)";
(sql, category_id, min_duration)
}
}
}
}