1use ocel::Ocel;
8use serde::Serialize;
9
10use crate::trace;
11
12#[derive(Debug, Clone, PartialEq, Serialize)]
14#[serde(rename_all = "camelCase")]
15pub struct TypeStats {
16 pub object_type: String,
17 pub objects: usize,
19 pub with_events: usize,
21 pub median_trace_len: f64,
23 pub median_active_span_secs: f64,
26 pub activity_types: usize,
32}
33
34#[allow(clippy::cast_precision_loss)]
37fn median(sorted: &[i64]) -> f64 {
38 let n = sorted.len();
39 if n == 0 {
40 return 0.0;
41 }
42 if n % 2 == 1 {
43 sorted[n / 2] as f64
44 } else {
45 (sorted[n / 2 - 1] + sorted[n / 2]) as f64 / 2.0
46 }
47}
48
49#[must_use]
52pub fn type_stats(log: &Ocel) -> Vec<TypeStats> {
53 let mut names: Vec<&str> = log.object_types.iter().map(|t| t.name.as_str()).collect();
54 for object in &log.objects {
55 if !names.contains(&object.object_type.as_str()) {
56 names.push(object.object_type.as_str());
57 }
58 }
59
60 let mut out: Vec<TypeStats> = names
61 .iter()
62 .map(|&name| {
63 let traces = trace::build(log, name);
64 let mut lengths: Vec<i64> = traces
65 .steps
66 .iter()
67 .map(Vec::len)
68 .filter(|&len| len > 0)
69 .map(|len| i64::try_from(len).expect("trace length fits i64"))
70 .collect();
71 lengths.sort_unstable();
72 let mut spans: Vec<i64> = traces
73 .steps
74 .iter()
75 .filter(|steps| steps.len() >= 2)
76 .map(|steps| {
77 let first = steps[0].1;
78 let last = steps[steps.len() - 1].1;
79 (last - first).num_seconds()
80 })
81 .collect();
82 spans.sort_unstable();
83 let mut seen: Vec<bool> = vec![false; traces.activity_names.len()];
84 for steps in &traces.steps {
85 for &(activity, _) in steps {
86 seen[usize::from(activity)] = true;
87 }
88 }
89 TypeStats {
90 object_type: name.to_owned(),
91 objects: traces.object_ids.len(),
92 with_events: lengths.len(),
93 median_trace_len: median(&lengths),
94 median_active_span_secs: median(&spans),
95 activity_types: seen.iter().filter(|&&s| s).count(),
96 }
97 })
98 .collect();
99 out.sort_unstable_by(|a, b| {
100 b.objects
101 .cmp(&a.objects)
102 .then_with(|| a.object_type.cmp(&b.object_type))
103 });
104 out
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::test_util::log_from_sequences;
111
112 #[test]
113 fn medians_and_coverage() {
114 let log = log_from_sequences(&[&["a", "b"], &["a", "b", "c"], &["a", "b", "c", "d"]]);
117 let stats = type_stats(&log);
118 let case = stats.iter().find(|s| s.object_type == "case").unwrap();
119 assert_eq!(case.objects, 3);
120 assert_eq!(case.with_events, 3);
121 assert!((case.median_trace_len - 3.0).abs() < f64::EPSILON);
122 assert!((case.median_active_span_secs - 120.0).abs() < f64::EPSILON);
123 }
124
125 #[test]
126 fn single_event_objects_do_not_drag_the_span_down() {
127 let log = log_from_sequences(&[&["a"], &["a"], &["a"], &["a", "b", "c", "d", "e"]]);
129 let stats = type_stats(&log);
130 let case = stats.iter().find(|s| s.object_type == "case").unwrap();
131 assert!((case.median_active_span_secs - 240.0).abs() < f64::EPSILON);
132 }
133
134 #[test]
135 fn activity_alphabet_counts_distinct_activities_across_traces() {
136 let log = log_from_sequences(&[&["a", "b"], &["c"], &["a"]]);
137 let stats = type_stats(&log);
138 let case = stats.iter().find(|s| s.object_type == "case").unwrap();
139 assert_eq!(case.activity_types, 3);
140 }
141
142 #[test]
143 fn empty_log_yields_declared_types_with_zeros() {
144 let log = log_from_sequences(&[]);
145 let stats = type_stats(&log);
146 assert_eq!(stats.len(), 1); assert_eq!(stats[0].with_events, 0);
148 assert!(stats[0].median_trace_len.abs() < f64::EPSILON);
149 }
150}