use crate::domain::model::temporal::duration::Duration;
use crate::domain::model::temporal::iso_date::IsoDate;
use crate::domain::model::temporal::timestamp::Timestamp;
use serde::Serialize;
use super::{Event, EventAction, State};
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
#[serde(transparent)]
pub struct EventLog(Vec<Event>);
impl EventLog {
pub fn new() -> Self {
Self(Vec::new())
}
pub(crate) fn push(&mut self, event: Event) {
self.0.push(event);
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn first(&self) -> Option<&Event> {
self.0.first()
}
pub fn last(&self) -> Option<&Event> {
self.0.last()
}
pub fn iter(&self) -> std::slice::Iter<'_, Event> {
self.0.iter()
}
pub fn with_appended(&self, event: Event) -> EventLog {
let mut out = self.clone();
out.0.push(event);
out
}
pub fn creation_date(&self, fallback: &IsoDate) -> IsoDate {
self.0
.iter()
.find(|e| e.action.is_created())
.map(|e| e.timestamp.to_iso_date())
.unwrap_or(*fallback)
}
pub fn last_activity_date(&self, fallback: &IsoDate) -> IsoDate {
self.0
.iter()
.map(|e| e.timestamp.to_iso_date())
.max()
.unwrap_or(*fallback)
}
pub fn latest_state(&self) -> Option<&State> {
self.0
.iter()
.rev()
.map(|e| match &e.action {
EventAction::StatusChanged { to, .. } => to,
EventAction::Created { state } => state,
})
.next()
}
pub fn close_date(&self, is_terminal: impl Fn(&str) -> bool) -> Option<IsoDate> {
self.0.iter().rev().find_map(|e| {
if let EventAction::StatusChanged { to, .. } = &e.action {
if is_terminal(to.as_str()) {
return Some(e.timestamp.to_iso_date());
}
}
None
})
}
pub fn first_ongoing_date(
&self,
fallback: &IsoDate,
is_ongoing: impl Fn(&str) -> bool,
) -> IsoDate {
self.0
.iter()
.find(|e| {
matches!(
&e.action,
EventAction::StatusChanged { to, .. } if is_ongoing(to.as_str())
)
})
.map(|e| e.timestamp.to_iso_date())
.unwrap_or(*fallback)
}
pub fn first_ongoing_timestamp(&self, is_ongoing: impl Fn(&str) -> bool) -> Option<String> {
self.0
.iter()
.find(|e| {
matches!(
&e.action,
EventAction::StatusChanged { to, .. } if is_ongoing(to.as_str())
)
})
.map(|e| e.timestamp.as_str().to_string())
}
pub fn active_duration(&self, is_ongoing: impl Fn(&str) -> bool) -> Option<Duration> {
if self.0.len() < 2 {
return None;
}
let mut total = Duration::default();
let mut ongoing_since: Option<&Timestamp> = None;
for event in &self.0 {
let ongoing = match &event.action {
EventAction::StatusChanged { to, .. } => is_ongoing(to.as_str()),
_ => continue,
};
if ongoing {
if ongoing_since.is_none() {
ongoing_since = Some(&event.timestamp);
}
} else if let Some(since) = ongoing_since.take() {
let d = event.timestamp.duration_since(since);
if d.is_positive() {
total += d;
}
}
}
if total.is_zero() {
None
} else {
Some(total)
}
}
pub fn lead_time(
&self,
creation_fallback: &IsoDate,
is_terminal: impl Fn(&str) -> bool,
) -> Option<Duration> {
let created = self.creation_date(creation_fallback);
let closed = self.close_date(is_terminal)?;
let d = created.duration_until(&closed);
if d.is_positive() || d.is_zero() {
Some(d)
} else {
None
}
}
pub fn flow_efficiency_pct(
&self,
is_ongoing: impl Fn(&str) -> bool,
is_stalled: impl Fn(&str) -> bool,
) -> Option<f64> {
let active = self.active_duration(is_ongoing)?.as_days();
let stalled = self
.active_duration(is_stalled)
.map(|d| d.as_days())
.unwrap_or(0.0);
let denom = active + stalled;
if denom <= 0.0 {
return None;
}
Some((active / denom) * 100.0)
}
pub fn queue_time(
&self,
creation_fallback: &IsoDate,
is_ongoing: impl Fn(&str) -> bool,
) -> Option<Duration> {
let created = self.creation_date(creation_fallback);
let started = self.first_ongoing_date(&created, is_ongoing);
if started == created {
return None;
}
let d = created.duration_until(&started);
if d.is_positive() {
Some(d)
} else {
None
}
}
pub fn cycle_time(
&self,
creation_fallback: &IsoDate,
is_terminal: impl Fn(&str) -> bool,
is_ongoing: impl Fn(&str) -> bool,
) -> Option<Duration> {
let started = self.first_ongoing_date(&self.creation_date(creation_fallback), is_ongoing);
let closed = self.close_date(is_terminal)?;
let d = started.duration_until(&closed);
if d.is_positive() || d.is_zero() {
Some(d)
} else {
None
}
}
}
impl std::ops::Index<usize> for EventLog {
type Output = Event;
fn index(&self, i: usize) -> &Self::Output {
&self.0[i]
}
}
impl<'a> IntoIterator for &'a EventLog {
type Item = &'a Event;
type IntoIter = std::slice::Iter<'a, Event>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl IntoIterator for EventLog {
type Item = Event;
type IntoIter = std::vec::IntoIter<Event>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl FromIterator<Event> for EventLog {
fn from_iter<I: IntoIterator<Item = Event>>(iter: I) -> Self {
Self(iter.into_iter().collect())
}
}
#[cfg(test)]
pub mod strategy {
use super::EventLog;
use crate::domain::model::event::event_value::strategy::event;
use proptest::prelude::*;
pub fn event_log() -> impl Strategy<Value = EventLog> {
proptest::collection::vec(event(), 0..6).prop_map(|events| events.into_iter().collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn len_matches_iter_count(log in strategy::event_log()) {
prop_assert_eq!(log.len(), log.iter().count());
}
#[test]
fn first_is_some_iff_log_non_empty(log in strategy::event_log()) {
prop_assert_eq!(log.first().is_some(), !log.is_empty());
}
}
}