1use std::collections::{HashMap, HashSet};
2
3pub const OPTIMISM_FLOOR: f32 = 0.05;
25pub const OPTIMISM_CEIL: f32 = 0.35;
26pub const DEFAULT_SKILL: f32 = 0.0;
27pub const DEFAULT_OPTIMISM: f32 = 0.1;
28
29#[derive(Debug, Clone)]
30pub struct ProfileState {
31 pub skill: f32,
32 pub optimism_bias: f32,
33 pub last_seen: HashMap<String, u64>,
34 pub category_count: HashMap<String, u32>,
35 pub resolved_set: HashSet<String>,
36 pub interaction_count: u64,
37 pub extended: HashMap<String, f32>,
39 pub extended_str: HashMap<String, String>,
41}
42
43impl ProfileState {
44 pub fn new() -> Self {
45 Self {
46 skill: DEFAULT_SKILL,
47 optimism_bias: DEFAULT_OPTIMISM,
48 last_seen: HashMap::new(),
49 category_count: HashMap::new(),
50 resolved_set: HashSet::new(),
51 interaction_count: 0,
52 extended: HashMap::new(),
53 extended_str: HashMap::new(),
54 }
55 }
56
57 pub fn target(&self) -> f32 {
59 (self.skill + self.optimism_bias).clamp(0.0, 1.0)
60 }
61
62 pub fn mean_category_count(&self) -> f32 {
64 if self.category_count.is_empty() {
65 return 1.0;
66 }
67 let total: u32 = self.category_count.values().sum();
68 total as f32 / self.category_count.len() as f32
69 }
70
71 pub fn elapsed_since(&self, item_id: &str, now: u64) -> Option<u64> {
73 self.last_seen.get(item_id).map(|&t| now.saturating_sub(t))
74 }
75}
76
77impl Default for ProfileState {
78 fn default() -> Self {
79 Self::new()
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn target_clamps_to_one() {
89 let mut s = ProfileState::new();
90 s.skill = 0.9;
91 s.optimism_bias = 0.35;
92 assert!(s.target() <= 1.0);
93 }
94
95 #[test]
96 fn mean_category_count_empty() {
97 let s = ProfileState::new();
98 assert_eq!(s.mean_category_count(), 1.0);
99 }
100
101 #[test]
102 fn mean_category_count_correct() {
103 let mut s = ProfileState::new();
104 s.category_count.insert("a".into(), 4);
105 s.category_count.insert("b".into(), 2);
106 assert!((s.mean_category_count() - 3.0).abs() < 1e-6);
107 }
108
109 #[test]
110 fn elapsed_since_never_seen() {
111 let s = ProfileState::new();
112 assert_eq!(s.elapsed_since("x", 100), None);
113 }
114}