use rusqlite::{
OptionalExtension,
Result as SqlResult,
};
use greg::{
Point,
Frame,
Span
};
use greg::calendar::zone::Shift;
use liter::{
HasKey,
Fetch,
Id
};
use super::{
Database,
Plan,
Zone,
zone::Table
};
use crate::cache::{
StreakIteration,
StreakDamage,
StreakSegment
};
use crate::data::streak::{
Streak,
state::Raw as StreakState
};
pub trait Measure: Database {
fn get_segment(&self, streak: &Streak, number: u64)
-> SqlResult<Option<StreakSegment>>
{
self.cache().query_row(
"SELECT id, definition_id, start, stop \
FROM streaksegment \
WHERE definition_id = ?1 \
AND ?2 BETWEEN start AND stop \
ORDER BY start ASC \
LIMIT 1",
(streak.id, number),
Fetch::from_row
).optional()
}
fn segment_streak(&self, streak: &Streak, build_until: u64)
-> SqlResult<Option<StreakSegment>>
{
let table = self.load_zone_table()?;
let streak_id = streak.id;
let build_up_to = get_frame(&table, streak, build_until).stop.timestamp;
let build_from = match self.cache().get(streak.id)? {
Some(StreakDamage {damage, ..}) if damage >= build_up_to => {
return self.get_segment(streak, build_until)
},
Some(StreakDamage {damage, ..}) => {
let start = table.apply(Point::from_epoch(damage));
get_number_at(streak, start).unwrap_or(0)
},
None => {
self.set_activity_affects(streak)?;
self.cache().upsert(&StreakDamage {streak_id, damage: 0})?;
0
}
};
self.cache().execute(
"DELETE FROM streaksegment \
WHERE definition_id = ?1 \
AND start >= ?2",
&(streak.id, build_from)
)?;
self.cache().execute(
"DELETE FROM streakiteration \
WHERE streak_id = ?1 \
AND number >= ?2",
&(streak.id, build_from)
)?;
let mut segment = self.get_segment(
streak,
build_from.saturating_sub(1)
)?;
let (query_sql, subject_id, min_duration) = streak.rule.get_query();
let mut query_stmt = self.db().prepare(query_sql)?;
let mut iter_upserter = self.cache().prepare(StreakIteration::UPSERT)?;
for number in build_from..=build_until {
let frame = get_frame(&table, streak, number);
let value = query_stmt.query_row(
(subject_id, frame.start, frame.stop),
|row| row.get::<_, Option<u64>>(0)
).optional().map(|opt_r| opt_r.flatten().unwrap_or(0))?;
if value != 0 {
iter_upserter.execute((streak_id, number, value))?;
}
let fulfilled = Span::from_seconds(value);
let completed = fulfilled >= min_duration;
if completed == segment.is_some() {continue}
match segment.take() {
Some(mut seg) => {
seg.stop = number.saturating_sub(1);
self.cache().upsert(&seg)?;
},
None => segment = Some(StreakSegment {
id: Id::NULL,
definition_id: streak.id,
start: number,
stop: 0
})
}
}
if let Some(seg) = segment.as_mut() {
seg.stop = build_until;
self.cache().upsert(seg)?;
}
self.cache().update(&StreakDamage {streak_id, damage: build_up_to})?;
Ok(segment)
}
fn build_streak(&self, streak: &Streak, until: Point)
-> SqlResult<Option<StreakSegment>>
{
match get_number_at(streak, self.load_zone_table()?.apply(until)) {
Some(number) => self.segment_streak(streak, number),
None => match (until, streak.end) {
(p, _) if p < streak.begin => Ok(None),
(p, Some(end)) if p >= end => {
let last_iter = get_number_at(streak, end - Span::SECOND)
.expect("invalid zero-length streak");
let _ = self.segment_streak(streak, last_iter)?;
Ok(None)
},
_ => panic!("integer overflow trying to build streak")
}
}
}
fn state_at(&self, streak: &Streak, until: Point)
-> SqlResult<Option<StreakState>>
{
let table = self.load_zone_table()?;
let (iter, length) = match get_number_at(streak, table.apply(until)) {
Some(n) => match self.segment_streak(streak, n.saturating_sub(1))? {
Some(seg) if seg.start == n => (n, 0),
Some(mut seg) if seg.stop >= n => {
seg.stop = n.saturating_sub(1);
(n, seg.length())
},
Some(seg) => (n, seg.length()),
None => (n, 0)
},
None => return Ok(None)
};
let last_frame = get_frame(&table, streak, iter);
let value = value(self, streak, Frame {stop: until, ..last_frame})?;
let min_duration = streak.rule.required();
let fulfilled = Span::from_seconds(value);
let completed = fulfilled >= min_duration;
let unfulfilled = min_duration
.checked_sub(fulfilled)
.unwrap_or_else(|| Span::from_seconds(0));
let next_iteration_at = last_frame.stop;
let state = StreakState {
streak_id: streak.id,
length: if completed {length + 1} else {length},
fulfilled,
unfulfilled,
next_iteration_at
};
Ok(Some(state))
}
fn calculate_state_at(&self, streak: &Streak, until: Point)
-> SqlResult<Option<StreakState>>
{
let table = self.load_zone_table()?;
let last_iter = match get_number_at(streak, table.apply(until)) {
Some(number) => number,
None => return Ok(None)
};
let (sql, act_or_cat_id, min_duration) = streak.rule.get_query();
let mut stmt = self.db().prepare(sql)?;
let mut length = None;
for number in (0..last_iter).rev() {
let frame = get_frame(&table, streak, number);
let value = stmt.query_row(
(act_or_cat_id, frame.start, frame.stop),
|row| row.get::<_, Option<u64>>(0)
).optional().map(|opt_r| opt_r.flatten().unwrap_or(0))?;
if value < min_duration.seconds {
length = Some((last_iter - number).saturating_sub(1));
break
}
}
let length = length.unwrap_or(last_iter);
let last_frame = get_frame(&table, streak, last_iter);
let value = stmt.query_row(
(act_or_cat_id, last_frame.start, until),
|row| row.get::<_, Option<u64>>(0)
).optional().map(|opt_r| opt_r.flatten().unwrap_or(0))?;
let fulfilled = Span::from_seconds(value);
let completed = fulfilled >= min_duration;
let unfulfilled = min_duration
.checked_sub(fulfilled)
.unwrap_or_else(|| Span::from_seconds(0));
let next_iteration_at = last_frame.stop;
let state = StreakState {
streak_id: streak.id,
length: if completed {length + 1} else {length},
fulfilled,
unfulfilled,
next_iteration_at
};
Ok(Some(state))
}
fn show(&self) -> SqlResult<Vec<StreakState>> {
let mut states = Vec::new();
for streak in self.list_active_streaks()? {
states.push(self.state_at(&streak, Point::now())?.unwrap());
}
Ok(states)
}
fn damage_at(&self, activity_id: u64, when: Point) -> SqlResult<()> {
self.cache().execute(
"UPDATE streakdamage \
SET damage = MIN(damage, ?2) \
WHERE streak_id IN ( \
SELECT streak_id \
FROM activityaffects \
WHERE activity_id = ?1 \
)",
&(activity_id, when.timestamp)
).map(|_| ())
}
}
impl<Db: Database + ?Sized> Measure for Db {}
fn get_number_at(streak: &Streak, at: Point) -> Option<u64> {
if streak.begin > at || streak.end.is_some_and(|e| at >= e) {return None}
at.timestamp
.checked_sub(streak.begin.timestamp)?
.checked_div(streak.interval.seconds as i64)
.map(|i| i as u64)
}
fn get_frame(table: &Table, streak: &Streak, number: u64) -> Frame {
let start = streak.begin + streak.interval * number;
let stop = start + streak.interval;
table.revert_frame(Frame{start, stop})
}
fn value<Db: Database + ?Sized>(
db: &Db,
streak: &Streak,
frame: Frame)
-> SqlResult<u64>
{
let (sql, act_or_cat_id, _min_duration) = streak.rule.get_query();
db.db().query_row(
sql,
(act_or_cat_id, frame.start, frame.stop),
|row| row.get::<_, Option<u64>>(0)
).optional().map(|opt_r| opt_r.flatten().unwrap_or(0))
}
#[test]
fn frame() -> SqlResult<()> {
use super::{Log, Tree};
let nunc = crate::Nunc::testing().unwrap();
crate::tests::add_example_tree(&nunc)?;
let act = nunc.get_activity(1)?;
let begin = Point::now();
let interval = "1d".parse().unwrap();
let duration = "10s".parse().unwrap();
let rule = crate::data::StreakRule::activity(&act, duration);
let streak = nunc.begin(rule, begin, interval)?;
let frame = get_frame(&nunc.load_zone_table()?, &streak, 0);
assert_eq!(value(&nunc, &streak, frame)?, 0);
let state = nunc.state_at(&streak, Point::now())?.unwrap();
let calc_state = nunc.calculate_state_at(&streak, Point::now())?.unwrap();
assert_eq!(state, calc_state);
assert_eq!(state, nunc.show()?.pop().unwrap());
assert_eq!(state.streak_id, streak.id);
assert_eq!(state.length, 0);
assert_eq!(state.fulfilled, Span::ZERO);
assert_eq!(state.unfulfilled, duration);
let _entry = nunc.create(
&act,
streak.begin + duration,
streak.begin + duration * 2
)?;
assert_eq!(value(&nunc, &streak, frame)?, duration.seconds);
let state = nunc.state_at(&streak, Point::now())?.unwrap();
let calc_state = nunc.calculate_state_at(&streak, Point::now())?.unwrap();
assert_eq!(state, calc_state);
assert_eq!(state.streak_id, streak.id);
assert_eq!(state.length, 1);
assert_eq!(state.unfulfilled, Span::ZERO);
assert_eq!(state.fulfilled, duration);
Ok(())
}
#[test]
fn category_streak() -> SqlResult<()> {
use super::{Log, Tree};
let nunc = crate::Nunc::testing().unwrap();
crate::tests::add_example_tree(&nunc)?;
let cat = nunc.find_category("Alpha")?.unwrap();
let interval = Span::parse("1d");
let duration = Span::parse("10m");
let begin = Point::now() - duration;
let rule = crate::data::StreakRule::category(&cat, duration);
let streak = nunc.begin(rule, begin, interval)?;
let state = nunc.state_at(&streak, Point::now())?.unwrap();
let calc_state = nunc.calculate_state_at(&streak, Point::now())?.unwrap();
assert_eq!(calc_state, state);
assert_eq!(state, nunc.show()?.pop().unwrap());
assert_eq!(state.streak_id, streak.id);
assert_eq!(state.length, 0);
assert_eq!(state.fulfilled, Span::ZERO);
assert_eq!(state.unfulfilled, duration);
assert!(nunc.cache.get_all::<StreakIteration>()?.is_empty());
assert!(nunc.cache.get_all::<StreakSegment>()?.is_empty());
let _entry = nunc.create(
&nunc.find_activity("Run")?.unwrap(),
streak.begin + duration,
streak.begin + duration * 2
)?;
let _entry = nunc.create(
&nunc.find_activity("Chess")?.unwrap(),
streak.begin + duration * 2,
streak.begin + duration * 3
)?;
let state = nunc.state_at(&streak, Point::now())?.unwrap();
let calc_state = nunc.calculate_state_at(&streak, Point::now())?.unwrap();
assert_eq!(state, calc_state);
assert_eq!(state.streak_id, streak.id);
assert_eq!(state.length, 1);
assert_eq!(state.unfulfilled, Span::ZERO);
assert_eq!(state.fulfilled, duration * 2);
Ok(())
}
#[test]
fn segment() -> SqlResult<()> {
use super::{Log, Tree};
use crate::data::Entry;
let nunc = crate::Nunc::testing().unwrap();
let cat = nunc.get_category(nunc.create_root_category("Root")?)?;
let activity_id = nunc.create_activity(&cat, "Activity")?;
let act = nunc.get_activity(activity_id)?;
let min_duration = Span::parse("10m");
let segment_count = || nunc.cache()
.get_all::<StreakSegment>()
.map(|v| v.len());
let iteration_count = || nunc.cache()
.get_all::<StreakIteration>()
.map(|v| v.len());
let monday_morning = greg::utc!(2023-09-25 10:15:00);
let rule = crate::data::StreakRule::Activity {activity_id, min_duration};
let streak = nunc.begin(rule, monday_morning, Span::DAY)?;
let cache_contains_segment = |start, stop| -> SqlResult<bool> {
nunc.cache().get_all::<StreakSegment>().map(|segments| segments
.iter()
.any(|seg| seg.definition_id == streak.id
&& seg.start == start
&& seg.stop == stop
))
};
let cache_contains_iteration = |number, value: Span| -> SqlResult<bool> {
nunc.cache().get_all::<StreakIteration>().map(|iterations| iterations
.iter()
.any(|iter| iter.streak_id == streak.id
&& iter.number == number
&& iter.value == value.seconds
))
};
let check_state = |when, length, fulfilled| -> SqlResult<bool> {
let expected = StreakState {
streak_id: streak.id,
length,
unfulfilled: min_duration.saturating_sub(fulfilled),
next_iteration_at: greg::Utc::resolve_midnight(
greg::Utc::lookup(when).0.add_day()
),
fulfilled
};
let first = nunc.state_at(&streak, when)?;
let cached = nunc.state_at(&streak, when)?;
if first != cached {
println!("cache mismatch on {}", greg::Utc::lookup(when));
println!("expected: {expected:#?}");
println!("first: {first:#?}");
println!("cached: {cached:#?}");
Ok(false)
}
else if !first.as_ref().is_some_and(|s| s == &expected) {
println!("state mismatch on {}", greg::Utc::lookup(when));
println!("expected: {expected:#?}");
println!("received: {first:#?}");
Ok(false)
}
else {Ok(true)}
};
assert!(check_state(monday_morning, 0, Span::ZERO)?);
assert_eq!(segment_count()?, 0);
assert_eq!(iteration_count()?, 0);
let monday_evening = greg::utc!(2023-09-25 18:30:00);
nunc.create(&act, monday_evening - min_duration, monday_evening)?;
assert!(check_state(monday_evening, 1, min_duration)?);
let tuesday_morning = greg::utc!(2023-09-26 06:30:00);
assert!(check_state(tuesday_morning, 1, Span::ZERO)?);
assert_eq!(segment_count()?, 1);
assert!(cache_contains_segment(0, 0)?);
assert_eq!(iteration_count()?, 1);
assert!(cache_contains_iteration(0, min_duration)?);
let ent_id = nunc.start(&act, tuesday_morning)?;
assert!(check_state(
tuesday_morning + Span::parse("5m"),
1,
Span::parse("5m")
)?);
assert!(check_state(
tuesday_morning + Span::parse("10m"),
2,
Span::parse("10m")
)?);
let entry = nunc.get_entry(ent_id)?;
assert!(nunc.stop(&entry, tuesday_morning + Span::parse("5m"))?);
assert!(check_state(
tuesday_morning + Span::parse("5m"),
1,
Span::parse("5m")
)?);
let wednesday_morning = greg::utc!(2023-09-27 00:30:00);
assert!(check_state(wednesday_morning, 0, Span::ZERO)?);
assert!(check_state(wednesday_morning, 0, Span::ZERO)?);
assert_eq!(segment_count()?, 1);
assert!(cache_contains_segment(0, 0)?);
assert_eq!(iteration_count()?, 2);
assert!(cache_contains_iteration(1, Span::parse("5m"))?);
let edited_entry = Entry {
stop: Some(tuesday_morning + Span::parse("10m")),
..entry
};
assert!(nunc.update(&edited_entry)?);
assert!(check_state(wednesday_morning, 2, Span::ZERO)?);
assert_eq!(segment_count()?, 1);
assert!(cache_contains_segment(0, 1)?);
assert_eq!(iteration_count()?, 2);
assert!(cache_contains_iteration(1, min_duration)?);
let wednesday_noon = greg::utc!(2023-09-27 12:45:00);
nunc.create(&act, wednesday_noon - min_duration, wednesday_noon)?;
assert!(check_state(wednesday_noon, 3, min_duration)?);
assert_eq!(segment_count()?, 1);
assert!(cache_contains_segment(0, 1)?);
assert_eq!(iteration_count()?, 2);
assert!(cache_contains_iteration(1, min_duration)?);
let wednesday_afternoon = greg::utc!(2023-09-27 16:45:00);
nunc.create(&act, wednesday_afternoon - min_duration, wednesday_afternoon)?;
assert!(check_state(wednesday_afternoon, 3, min_duration * 2)?);
assert_eq!(segment_count()?, 1);
assert!(cache_contains_segment(0, 1)?);
assert_eq!(iteration_count()?, 2);
assert!(cache_contains_iteration(1, min_duration)?);
let friday_morning = greg::utc!(2023-09-29 05:04:03);
assert!(check_state(friday_morning, 0, Span::ZERO)?);
assert_eq!(segment_count()?, 1);
assert!(cache_contains_segment(0, 2)?);
assert_eq!(iteration_count()?, 3);
assert!(cache_contains_iteration(2, min_duration * 2)?);
let friday_entry_id = nunc.create(
&act,
friday_morning - Span::parse("10m4s"),
friday_morning
)?;
assert!(check_state(friday_morning, 1, Span::parse("10m4s"))?);
assert_eq!(segment_count()?, 1);
assert!(cache_contains_segment(0, 2)?);
assert_eq!(iteration_count()?, 3);
assert!(cache_contains_iteration(2, min_duration * 2)?);
let saturday_evening = greg::utc!(2023-09-30 19:20:21);
assert!(check_state(saturday_evening, 1, Span::ZERO)?);
nunc.create(
&act,
saturday_evening - Span::parse("10m5s"),
saturday_evening
)?;
assert!(check_state(saturday_evening, 2, Span::parse("10m5s"))?);
assert_eq!(segment_count()?, 2);
assert!(cache_contains_segment(0, 2)?);
assert!(cache_contains_segment(4, 4)?);
assert_eq!(iteration_count()?, 4);
assert!(cache_contains_iteration(4, Span::parse("10m4s"))?);
let sunday_morning = greg::utc!(2023-10-01 23:49:49);
assert!(check_state(sunday_morning, 2, Span::ZERO)?);
assert_eq!(segment_count()?, 2);
assert!(cache_contains_segment(0, 2)?);
assert!(cache_contains_segment(4, 5)?);
assert_eq!(iteration_count()?, 5);
assert!(cache_contains_iteration(5, Span::parse("10m5s"))?);
let friday_entry_edited = Entry {
stop: Some(friday_morning - Span::parse("10m")),
..nunc.get_entry(friday_entry_id)?
};
assert!(nunc.update(&friday_entry_edited)?);
assert!(check_state(sunday_morning, 1, Span::ZERO)?);
assert_eq!(segment_count()?, 2);
assert!(cache_contains_segment(0, 2)?);
assert!(cache_contains_segment(5, 5)?);
assert_eq!(iteration_count()?, 5);
assert!(cache_contains_iteration(4, Span::parse("4s"))?);
assert!(nunc.delete(&friday_entry_edited)?);
assert!(check_state(sunday_morning, 1, Span::ZERO)?);
assert_eq!(segment_count()?, 2);
assert!(cache_contains_segment(0, 2)?);
assert!(cache_contains_segment(5, 5)?);
assert_eq!(iteration_count()?, 4);
assert!(!nunc.cache().get_all::<StreakIteration>()?
.iter()
.any(|iter| iter.streak_id == streak.id && iter.number == 4)
);
let sunday_night = greg::utc!(2023-10-01 23:49:59);
assert!(check_state(sunday_night, 1, Span::ZERO)?);
let ent_id = nunc.start(&act, sunday_night)?;
assert!(check_state(
sunday_night + Span::parse("5m"),
1,
Span::parse("5m")
)?);
assert!(check_state(
sunday_night + Span::parse("10m"),
2,
Span::parse("10m")
)?);
assert!(check_state(
sunday_night + Span::parse("10m2s"),
2,
Span::parse("1s")
)?);
assert_eq!(segment_count()?, 2);
assert!(cache_contains_segment(0, 2)?);
assert!(cache_contains_segment(5, 6)?);
assert_eq!(iteration_count()?, 5);
assert!(cache_contains_iteration(6, Span::parse("10m1s"))?);
let entry = nunc.get_entry(ent_id)?;
assert!(nunc.stop(&entry, sunday_night + Span::parse("5m"))?);
assert!(check_state(
sunday_night + Span::parse("10m2s"),
0,
Span::ZERO
)?);
assert_eq!(segment_count()?, 2);
assert!(cache_contains_segment(0, 2)?);
assert!(cache_contains_segment(5, 5)?);
assert_eq!(iteration_count()?, 5);
assert!(cache_contains_iteration(6, Span::parse("5m"))?);
let monday_morning = greg::utc!(2023-10-02 01:23:45);
assert!(check_state(monday_morning, 0, Span::ZERO)?);
assert_eq!(segment_count()?, 2);
assert!(cache_contains_segment(0, 2)?);
assert!(cache_contains_segment(5, 5)?);
assert_eq!(iteration_count()?, 5);
assert!(cache_contains_iteration(6, Span::parse("5m"))?);
let entries = nunc.get_last_entries(2)?;
assert!(nunc.delete(&entries[1])?);
assert!(nunc.delete(&entries[0])?);
assert!(check_state(monday_morning, 0, Span::ZERO)?);
assert_eq!(segment_count()?, 1);
assert!(cache_contains_segment(0, 2)?);
assert!(!cache_contains_segment(5, 5)?);
assert_eq!(iteration_count()?, 3);
Ok(())
}