1use std::collections::HashMap;
13
14use ocel::Ocel;
15use serde::Serialize;
16
17use crate::trace;
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
21pub struct Variant {
22 pub activities: Vec<String>,
24 pub count: usize,
26 pub example: String,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct VariantsReport {
34 pub object_type: String,
35 pub objects: usize,
37 pub with_events: usize,
39 pub variants: Vec<Variant>,
41}
42
43#[must_use]
49pub fn variants(log: &Ocel, object_type: &str) -> VariantsReport {
50 let traces = trace::build(log, object_type);
51 let sequences: Vec<Vec<u16>> = traces
52 .steps
53 .iter()
54 .map(|steps| steps.iter().map(|&(activity, _)| activity).collect())
55 .collect();
56
57 let mut groups: HashMap<&[u16], (usize, usize)> = HashMap::new();
59 let mut with_events = 0usize;
60 for (slot, sequence) in sequences.iter().enumerate() {
61 if sequence.is_empty() {
62 continue;
63 }
64 with_events += 1;
65 let entry = groups.entry(sequence.as_slice()).or_insert((0, slot));
66 entry.0 += 1;
67 }
68
69 let mut variants: Vec<Variant> = groups
70 .into_iter()
71 .map(|(sequence, (count, example_slot))| Variant {
72 activities: sequence
73 .iter()
74 .map(|&id| traces.activity_names[id as usize].to_owned())
75 .collect(),
76 count,
77 example: traces.object_ids[example_slot].to_owned(),
78 })
79 .collect();
80 variants.sort_unstable_by(|a, b| {
81 b.count
82 .cmp(&a.count)
83 .then_with(|| a.activities.cmp(&b.activities))
84 });
85
86 VariantsReport {
87 object_type: object_type.to_owned(),
88 objects: traces.object_ids.len(),
89 with_events,
90 variants,
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use chrono::{TimeZone, Utc};
98 use ocel::{Event, EventType, Object, ObjectType, Relationship};
99
100 fn rel(object_id: &str) -> Relationship {
101 Relationship {
102 object_id: object_id.into(),
103 qualifier: "q".into(),
104 }
105 }
106
107 fn event(id: &str, event_type: &str, minute: u32, objects: &[&str]) -> Event {
108 Event {
109 id: id.into(),
110 event_type: event_type.into(),
111 time: Utc.with_ymd_and_hms(2026, 1, 1, 9, minute, 0).unwrap(),
112 attributes: vec![],
113 relationships: objects.iter().map(|o| rel(o)).collect(),
114 }
115 }
116
117 fn log(events: Vec<Event>, objects: &[&str]) -> Ocel {
118 let mut builder = Ocel::builder();
119 for name in ["created", "closed", "changed"] {
120 builder.add_event_type(EventType {
121 name: name.into(),
122 attributes: vec![],
123 });
124 }
125 builder.add_object_type(ObjectType {
126 name: "task".into(),
127 attributes: vec![],
128 });
129 for id in objects {
130 builder.add_object(Object {
131 id: (*id).into(),
132 object_type: "task".into(),
133 attributes: vec![],
134 relationships: vec![],
135 });
136 }
137 for e in events {
138 builder.add_event(e);
139 }
140 builder.build().expect("valid log")
141 }
142
143 #[test]
144 fn groups_identical_sequences() {
145 let log = log(
146 vec![
147 event("e1", "created", 0, &["a"]),
148 event("e2", "closed", 1, &["a"]),
149 event("e3", "created", 2, &["b"]),
150 event("e4", "closed", 3, &["b"]),
151 event("e5", "created", 4, &["c"]),
152 ],
153 &["a", "b", "c"],
154 );
155 let report = variants(&log, "task");
156 assert_eq!(report.objects, 3);
157 assert_eq!(report.with_events, 3);
158 assert_eq!(report.variants.len(), 2);
159 assert_eq!(report.variants[0].activities, ["created", "closed"]);
160 assert_eq!(report.variants[0].count, 2);
161 assert_eq!(report.variants[1].activities, ["created"]);
162 assert_eq!(report.variants[1].count, 1);
163 assert_eq!(report.variants[1].example, "c");
164 }
165
166 #[test]
167 fn duplicate_links_from_one_event_count_once() {
168 let mut e = event("e1", "created", 0, &["a", "a"]);
169 e.relationships[1].qualifier = "other".into();
170 let log = log(vec![e], &["a"]);
171 let report = variants(&log, "task");
172 assert_eq!(report.variants[0].activities, ["created"]);
173 }
174
175 #[test]
176 fn same_timestamp_orders_by_event_index() {
177 let log = log(
178 vec![
179 event("e2", "closed", 5, &["a"]),
180 event("e1", "created", 5, &["a"]),
181 ],
182 &["a"],
183 );
184 let report = variants(&log, "task");
185 assert_eq!(report.variants[0].activities, ["closed", "created"]);
187 }
188
189 #[test]
190 fn objects_without_events_form_no_variant() {
191 let log = log(vec![event("e1", "created", 0, &["a"])], &["a", "b"]);
192 let report = variants(&log, "task");
193 assert_eq!(report.objects, 2);
194 assert_eq!(report.with_events, 1);
195 assert_eq!(report.variants.len(), 1);
196 }
197
198 #[test]
199 fn unknown_type_is_empty() {
200 let log = log(vec![], &[]);
201 let report = variants(&log, "nope");
202 assert_eq!(report.objects, 0);
203 assert!(report.variants.is_empty());
204 }
205}