Skip to main content

agentic_time/
duration.rs

1//! Duration estimation with uncertainty (PERT model).
2
3use chrono::{DateTime, Duration as ChronoDuration, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::TemporalId;
7
8/// A duration estimate with uncertainty using the PERT model.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct DurationEstimate {
11    /// Unique identifier.
12    pub id: TemporalId,
13
14    /// What this duration is for.
15    pub label: String,
16
17    /// Optimistic estimate (best case) in seconds.
18    pub optimistic_secs: i64,
19
20    /// Most likely estimate in seconds.
21    pub expected_secs: i64,
22
23    /// Pessimistic estimate (worst case) in seconds.
24    pub pessimistic_secs: i64,
25
26    /// Confidence level (0.0 - 1.0).
27    pub confidence: f64,
28
29    /// Source of this estimate.
30    pub source: DurationSource,
31
32    /// When this estimate was created.
33    pub created_at: DateTime<Utc>,
34
35    /// Tags for categorization.
36    pub tags: Vec<String>,
37}
38
39/// Source of a duration estimate.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub enum DurationSource {
42    /// User provided the estimate.
43    UserEstimate,
44    /// Derived from historical data.
45    Historical {
46        /// Number of historical samples.
47        sample_count: u32,
48    },
49    /// AI predicted.
50    Predicted {
51        /// Model used.
52        model: String,
53        /// Prediction confidence.
54        confidence: f64,
55    },
56    /// Default/fallback.
57    Default,
58}
59
60impl DurationEstimate {
61    /// Create a new duration estimate.
62    pub fn new(label: impl Into<String>, optimistic: i64, expected: i64, pessimistic: i64) -> Self {
63        Self {
64            id: TemporalId::new(),
65            label: label.into(),
66            optimistic_secs: optimistic,
67            expected_secs: expected,
68            pessimistic_secs: pessimistic,
69            confidence: 0.8,
70            source: DurationSource::UserEstimate,
71            created_at: Utc::now(),
72            tags: Vec::new(),
73        }
74    }
75
76    /// Calculate PERT estimate: (O + 4M + P) / 6
77    pub fn pert_estimate(&self) -> ChronoDuration {
78        let pert = (self.optimistic_secs + 4 * self.expected_secs + self.pessimistic_secs) / 6;
79        ChronoDuration::seconds(pert)
80    }
81
82    /// Calculate standard deviation: (P - O) / 6
83    pub fn std_deviation(&self) -> ChronoDuration {
84        let sd = (self.pessimistic_secs - self.optimistic_secs) / 6;
85        ChronoDuration::seconds(sd)
86    }
87
88    /// Get optimistic duration.
89    pub fn optimistic(&self) -> ChronoDuration {
90        ChronoDuration::seconds(self.optimistic_secs)
91    }
92
93    /// Get expected duration.
94    pub fn expected(&self) -> ChronoDuration {
95        ChronoDuration::seconds(self.expected_secs)
96    }
97
98    /// Get pessimistic duration.
99    pub fn pessimistic(&self) -> ChronoDuration {
100        ChronoDuration::seconds(self.pessimistic_secs)
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn test_pert_estimate() {
110        let d = DurationEstimate::new("test", 60, 120, 300);
111        // (60 + 4*120 + 300) / 6 = (60 + 480 + 300) / 6 = 840 / 6 = 140
112        assert_eq!(d.pert_estimate().num_seconds(), 140);
113    }
114
115    #[test]
116    fn test_std_deviation() {
117        let d = DurationEstimate::new("test", 60, 120, 300);
118        // (300 - 60) / 6 = 240 / 6 = 40
119        assert_eq!(d.std_deviation().num_seconds(), 40);
120    }
121}