use crate::backlog::{Backlog, Depth};
use chrono::{DateTime, Utc};
const COUNT_HALF_AT: usize = 3;
const AGE_HALF_AT_HOURS: f64 = 24.0 * 7.0;
pub fn anticipated_guilt(
backlog: &Backlog,
peak_context_pressure: Option<f32>,
now: DateTime<Utc>,
) -> Option<f32> {
let depths: [&Depth; 3] = [
backlog.outbox.as_ref()?,
backlog.questions.as_ref()?,
backlog.frontdoor.as_ref()?,
];
let mut waiting = 0usize;
let mut oldest_hours: Option<f64> = None;
let mut age_unknown = false;
for depth in depths {
waiting += depth.waiting;
if depth.waiting == 0 {
continue;
}
match depth.oldest.as_deref().and_then(|s| hours_since(s, now)) {
Some(hours) => oldest_hours = Some(oldest_hours.map_or(hours, |h: f64| h.max(hours))),
None => age_unknown = true,
}
}
if waiting == 0 {
return Some(0.0);
}
if age_unknown {
return None;
}
let oldest_hours = oldest_hours?;
let age = (oldest_hours / (oldest_hours + AGE_HALF_AT_HOURS)) as f32;
let above_one = waiting.saturating_sub(1) as f32;
let count = above_one / (above_one + (COUNT_HALF_AT - 1) as f32);
let pressure = peak_context_pressure?.clamp(0.0, 1.0);
let combined = 1.0 - (1.0 - age) * (1.0 - count) * (1.0 - pressure);
Some(combined.clamp(0.0, 1.0))
}
fn hours_since(stamp: &str, now: DateTime<Utc>) -> Option<f64> {
let then = DateTime::parse_from_rfc3339(stamp)
.ok()?
.with_timezone(&Utc);
Some((now - then).num_seconds().max(0) as f64 / 3600.0)
}
#[cfg(test)]
mod tests {
use super::*;
fn depth(waiting: usize, oldest: Option<&str>) -> Depth {
Depth {
waiting,
oldest: oldest.map(str::to_string),
}
}
fn readable_and_empty() -> Backlog {
Backlog {
outbox: Some(Depth::default()),
questions: Some(Depth::default()),
frontdoor: Some(Depth::default()),
..Backlog::default()
}
}
#[test]
fn every_store_unreadable_is_unknown_rather_than_zero() {
let backlog = Backlog::default();
assert_eq!(anticipated_guilt(&backlog, Some(0.5), Utc::now()), None);
}
#[test]
fn one_unreadable_store_beside_two_empty_ones_is_still_unknown() {
let backlog = Backlog {
outbox: Some(Depth::default()),
questions: Some(Depth::default()),
frontdoor: None,
..Backlog::default()
};
assert_eq!(anticipated_guilt(&backlog, Some(0.9), Utc::now()), None);
}
#[test]
fn nothing_waiting_is_a_real_zero() {
let backlog = readable_and_empty();
assert_eq!(
anticipated_guilt(&backlog, Some(0.9), Utc::now()),
Some(0.0)
);
}
#[test]
fn a_fresh_lone_commitment_under_no_pressure_reads_near_zero() {
let now = Utc::now();
let backlog = Backlog {
outbox: Some(depth(1, Some(&now.to_rfc3339()))),
..readable_and_empty()
};
let g = anticipated_guilt(&backlog, Some(0.0), now).unwrap();
assert!(g < 0.1, "{g}");
}
#[test]
fn a_week_old_commitment_reads_half_of_maximal_on_the_age_term() {
let now = Utc::now();
let old = now - chrono::Duration::hours(AGE_HALF_AT_HOURS.round() as i64);
let backlog = Backlog {
questions: Some(depth(1, Some(&old.to_rfc3339()))),
..readable_and_empty()
};
let g = anticipated_guilt(&backlog, Some(0.0), now).unwrap();
assert!((g - 0.5).abs() < 1e-3, "{g}");
}
#[test]
fn a_standing_week_old_backlog_does_not_pin_the_reading_at_a_constant() {
let now = Utc::now();
let eight_days = now - chrono::Duration::hours(24 * 8);
let two_days = now - chrono::Duration::hours(48);
let live_shape = Backlog {
outbox: Some(depth(4, Some(&eight_days.to_rfc3339()))),
questions: Some(depth(3, Some(&two_days.to_rfc3339()))),
..readable_and_empty()
};
let g = anticipated_guilt(&live_shape, Some(0.06), now).unwrap();
assert!(g > 0.5, "eight-day-old debt should still read high: {g}");
assert!(g < 1.0 - 1e-3, "…but must not pin the reading: {g}");
let under_pressure = anticipated_guilt(&live_shape, Some(0.6), now).unwrap();
assert!(under_pressure > g, "{g} vs {under_pressure}");
let older = Backlog {
outbox: Some(depth(
4,
Some(&(now - chrono::Duration::hours(24 * 16)).to_rfc3339()),
)),
questions: Some(depth(3, Some(&two_days.to_rfc3339()))),
..readable_and_empty()
};
let g_older = anticipated_guilt(&older, Some(0.06), now).unwrap();
assert!(
g_older > g,
"older debt must still read worse: {g} vs {g_older}"
);
}
#[test]
fn a_two_day_old_commitment_does_not_saturate_the_age_term() {
let now = Utc::now();
let two_days = now - chrono::Duration::hours(48);
let backlog = Backlog {
questions: Some(depth(1, Some(&two_days.to_rfc3339()))),
..readable_and_empty()
};
let g = anticipated_guilt(&backlog, Some(0.0), now).unwrap();
assert!(g > 0.0 && g < 0.5, "{g}");
}
#[test]
fn unknown_pressure_is_unknown_not_a_measured_zero() {
let now = Utc::now();
let old = now - chrono::Duration::hours(48);
let backlog = Backlog {
questions: Some(depth(1, Some(&old.to_rfc3339()))),
..readable_and_empty()
};
assert_eq!(anticipated_guilt(&backlog, None, now), None);
}
#[test]
fn pressure_alone_can_saturate_the_reading_by_design() {
let now = Utc::now();
let recent = now - chrono::Duration::hours(1);
let backlog = Backlog {
frontdoor: Some(depth(1, Some(&recent.to_rfc3339()))),
..readable_and_empty()
};
let low_pressure = anticipated_guilt(&backlog, Some(0.0), now).unwrap();
let high_pressure = anticipated_guilt(&backlog, Some(1.0), now).unwrap();
assert!(
high_pressure > low_pressure,
"{low_pressure} vs {high_pressure}"
);
assert!((high_pressure - 1.0).abs() < 1e-6, "{high_pressure}");
}
#[test]
fn several_waiting_items_raise_the_count_term_even_when_fresh() {
let now = Utc::now();
let one = Backlog {
outbox: Some(depth(1, Some(&now.to_rfc3339()))),
..readable_and_empty()
};
let several = Backlog {
outbox: Some(depth(COUNT_HALF_AT, Some(&now.to_rfc3339()))),
..readable_and_empty()
};
let g_one = anticipated_guilt(&one, Some(0.0), now).unwrap();
let g_several = anticipated_guilt(&several, Some(0.0), now).unwrap();
assert!(g_several > g_one, "{g_one} vs {g_several}");
}
#[test]
fn the_oldest_across_stores_wins_not_the_first() {
let now = Utc::now();
let fresh_first = Backlog {
outbox: Some(depth(
1,
Some(&(now - chrono::Duration::hours(1)).to_rfc3339()),
)),
questions: Some(depth(
1,
Some(
&(now - chrono::Duration::hours(AGE_HALF_AT_HOURS.round() as i64 * 2))
.to_rfc3339(),
),
)),
..readable_and_empty()
};
let both_fresh = Backlog {
outbox: Some(depth(
1,
Some(&(now - chrono::Duration::hours(1)).to_rfc3339()),
)),
questions: Some(depth(
1,
Some(&(now - chrono::Duration::hours(1)).to_rfc3339()),
)),
..readable_and_empty()
};
let g_old_behind = anticipated_guilt(&fresh_first, Some(0.0), now).unwrap();
let g_fresh = anticipated_guilt(&both_fresh, Some(0.0), now).unwrap();
assert!(g_old_behind > g_fresh, "{g_fresh} vs {g_old_behind}");
assert!((g_old_behind - 7.0 / 9.0).abs() < 1e-2, "{g_old_behind}");
}
#[test]
fn a_count_with_no_parseable_age_is_unknown_rather_than_fresh() {
let backlog = Backlog {
outbox: Some(depth(1, Some("not-a-timestamp"))),
..readable_and_empty()
};
assert_eq!(anticipated_guilt(&backlog, None, Utc::now()), None);
}
#[test]
fn one_undated_store_is_unknown_even_when_a_sibling_store_is_dated() {
let now = Utc::now();
let backlog = Backlog {
outbox: Some(depth(1, Some("not-a-timestamp"))),
questions: Some(depth(1, Some(&now.to_rfc3339()))),
..readable_and_empty()
};
assert_eq!(anticipated_guilt(&backlog, Some(0.0), now), None);
}
}