ass_core/analysis/events/utils.rs
1//! Utility functions for dialogue event analysis
2//!
3//! Provides common operations for collections of dialogue events including
4//! sorting, duration calculations, and overlap detection. Optimized for
5//! performance with large event collections.
6//!
7//! # Features
8//!
9//! - Efficient sorting by timing with stable ordering
10//! - Total duration calculation across event collections
11//! - Overlap detection delegation to efficient algorithms
12//! - Zero-allocation operations where possible
13//!
14//! # Performance
15//!
16//! - Sorting: O(n log n) with optimized comparison
17//! - Duration: O(n) single pass with min/max tracking
18//! - Overlap detection: O(n log n) via sweep-line algorithm
19
20use crate::analysis::events::{dialogue_info::DialogueInfo, overlap::find_overlapping_event_refs};
21use crate::parser::Event;
22use alloc::vec::Vec;
23use core::cmp::Ordering;
24
25/// Find overlapping dialogue events using efficient timing analysis
26///
27/// Returns pairs of event indices that have overlapping timing.
28/// Delegates to the efficient O(n log n) sweep-line algorithm for optimal performance.
29///
30/// # Arguments
31///
32/// * `events` - Slice of `DialogueInfo` to analyze for overlaps
33///
34/// # Returns
35///
36/// Vector of (index1, index2) pairs representing overlapping events.
37///
38/// # Example
39///
40/// ```rust
41/// # use ass_core::analysis::events::utils::find_overlapping_dialogue_events;
42/// # use ass_core::analysis::events::dialogue_info::DialogueInfo;
43/// # use ass_core::parser::Event;
44/// let event1 = Event { start: "0:00:00.00", end: "0:00:05.00", ..Default::default() };
45/// let event2 = Event { start: "0:00:03.00", end: "0:00:08.00", ..Default::default() };
46/// let events = vec![
47/// DialogueInfo::analyze(&event1)?,
48/// DialogueInfo::analyze(&event2)?,
49/// ];
50///
51/// let overlaps = find_overlapping_dialogue_events(&events);
52/// assert_eq!(overlaps.len(), 1);
53/// # Ok::<(), Box<dyn std::error::Error>>(())
54/// ```
55#[must_use]
56pub fn find_overlapping_dialogue_events(events: &[DialogueInfo<'_>]) -> Vec<(usize, usize)> {
57 let event_refs: Vec<&Event> = events
58 .iter()
59 .map(super::dialogue_info::DialogueInfo::event)
60 .collect();
61 find_overlapping_event_refs(&event_refs).unwrap_or_else(|_| Vec::new())
62}
63
64/// Count overlapping dialogue events efficiently
65///
66/// Convenience wrapper that returns only the count of overlapping pairs.
67/// More memory efficient when the specific overlap pairs aren't needed.
68///
69/// # Arguments
70///
71/// * `events` - Slice of `DialogueInfo` to check for overlaps
72///
73/// # Returns
74///
75/// Number of overlapping event pairs found.
76#[must_use]
77pub fn count_overlapping_dialogue_events(events: &[DialogueInfo<'_>]) -> usize {
78 find_overlapping_dialogue_events(events).len()
79}
80
81/// Sort events by timing with stable ordering
82///
83/// Sorts events by start time first, then by end time for events that
84/// start simultaneously. Uses stable sort to preserve relative order
85/// of equal elements.
86///
87/// # Arguments
88///
89/// * `events` - Mutable slice of `DialogueInfo` to sort in-place
90///
91/// # Performance
92///
93/// O(n log n) time complexity with minimal allocations.
94/// Comparison operations are optimized for centisecond timing.
95///
96/// # Example
97///
98/// ```rust
99/// # use ass_core::analysis::events::utils::sort_events_by_time;
100/// # use ass_core::analysis::events::dialogue_info::DialogueInfo;
101/// # use ass_core::parser::Event;
102/// let event1 = Event {
103/// start: "0:00:05.00",
104/// end: "0:00:10.00",
105/// ..Default::default()
106/// };
107/// let event2 = Event {
108/// start: "0:00:01.00",
109/// end: "0:00:06.00",
110/// ..Default::default()
111/// };
112/// let dialogue_info1 = DialogueInfo::analyze(&event1)?;
113/// let dialogue_info2 = DialogueInfo::analyze(&event2)?;
114/// let mut events = vec![dialogue_info1, dialogue_info2];
115/// sort_events_by_time(&mut events);
116/// // Events are now sorted by start time, then end time
117/// # Ok::<(), Box<dyn std::error::Error>>(())
118/// ```
119pub fn sort_events_by_time(events: &mut [DialogueInfo<'_>]) {
120 events.sort_by(|a, b| match a.start_time_cs().cmp(&b.start_time_cs()) {
121 Ordering::Equal => a.end_time_cs().cmp(&b.end_time_cs()),
122 other => other,
123 });
124}
125
126/// Calculate total duration spanning all events
127///
128/// Computes the duration from the earliest start time to the latest
129/// end time across all events. Returns None for empty collections.
130///
131/// # Arguments
132///
133/// * `events` - Slice of `DialogueInfo` to analyze
134///
135/// # Returns
136///
137/// Total duration in centiseconds, or None if no events provided.
138///
139/// # Performance
140///
141/// Single O(n) pass with iterator optimizations for min/max operations.
142///
143/// # Example
144///
145/// ```rust
146/// # use ass_core::analysis::events::utils::calculate_total_duration;
147/// # use ass_core::analysis::events::dialogue_info::DialogueInfo;
148/// # use ass_core::parser::Event;
149/// let event1 = Event {
150/// start: "0:00:01.00",
151/// end: "0:00:05.00",
152/// ..Default::default()
153/// };
154/// let event2 = Event {
155/// start: "0:00:03.00",
156/// end: "0:00:08.00",
157/// ..Default::default()
158/// };
159/// let dialogue_info1 = DialogueInfo::analyze(&event1)?;
160/// let dialogue_info2 = DialogueInfo::analyze(&event2)?;
161/// let events = vec![dialogue_info1, dialogue_info2];
162/// if let Some(duration) = calculate_total_duration(&events) {
163/// println!("Total span: {}ms", duration);
164/// }
165/// # Ok::<(), Box<dyn std::error::Error>>(())
166/// ```
167#[must_use]
168pub fn calculate_total_duration(events: &[DialogueInfo<'_>]) -> Option<u32> {
169 if events.is_empty() {
170 return None;
171 }
172
173 let start = events
174 .iter()
175 .map(super::dialogue_info::DialogueInfo::start_time_cs)
176 .min()?;
177 let end = events
178 .iter()
179 .map(super::dialogue_info::DialogueInfo::end_time_cs)
180 .max()?;
181
182 Some(end - start)
183}
184
185/// Calculate average event duration
186///
187/// Computes the mean duration of all events in the collection.
188/// Returns None for empty collections.
189///
190/// # Arguments
191///
192/// * `events` - Slice of `DialogueInfo` to analyze
193///
194/// # Returns
195///
196/// Average duration in centiseconds, or None if no events.
197#[must_use]
198pub fn calculate_average_duration(events: &[DialogueInfo<'_>]) -> Option<u32> {
199 if events.is_empty() {
200 return None;
201 }
202
203 let total_duration: u32 = events
204 .iter()
205 .map(super::dialogue_info::DialogueInfo::duration_cs)
206 .sum();
207 Some(total_duration / u32::try_from(events.len()).unwrap_or(u32::MAX))
208}
209
210/// Find events within a specific time range
211///
212/// Returns indices of events that overlap with the given time range.
213/// Useful for temporal filtering and range-based analysis.
214///
215/// # Arguments
216///
217/// * `events` - Slice of `DialogueInfo` to search
218/// * `start_cs` - Range start time in centiseconds
219/// * `end_cs` - Range end time in centiseconds
220///
221/// # Returns
222///
223/// Vector of indices for events overlapping the time range.
224#[must_use]
225pub fn find_events_in_range(events: &[DialogueInfo<'_>], start_cs: u32, end_cs: u32) -> Vec<usize> {
226 events
227 .iter()
228 .enumerate()
229 .filter_map(|(idx, event)| {
230 if event.overlaps_time_range(start_cs, end_cs) {
231 Some(idx)
232 } else {
233 None
234 }
235 })
236 .collect()
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::parser::{ast::Span, Event};
243 #[cfg(not(feature = "std"))]
244 use alloc::boxed::Box;
245 #[cfg(not(feature = "std"))]
246 use alloc::vec;
247 #[cfg(not(feature = "std"))]
248 fn create_test_dialogue_info(start: &'static str, end: &'static str) -> DialogueInfo<'static> {
249 // Create a static event for the lifetime requirement
250 // Using Box::leak is acceptable in tests for simplicity
251 let event = Box::leak(Box::new(Event {
252 event_type: crate::parser::ast::EventType::Dialogue,
253 start,
254 end,
255 text: "Test",
256 layer: "0",
257 style: "Default",
258 name: "",
259 margin_l: "0",
260 margin_r: "0",
261 margin_v: "0",
262 margin_t: None,
263 margin_b: None,
264 effect: "",
265 span: Span::new(0, 0, 0, 0),
266 }));
267 DialogueInfo::analyze(event).unwrap()
268 }
269
270 #[cfg(feature = "std")]
271 fn create_test_dialogue_info(start: &'static str, end: &'static str) -> DialogueInfo<'static> {
272 // Create a static event for the lifetime requirement
273 // Using Box::leak is acceptable in tests for simplicity
274 let event = Box::leak(Box::new(Event {
275 event_type: crate::parser::ast::EventType::Dialogue,
276 start,
277 end,
278 text: "Test",
279 layer: "0",
280 style: "Default",
281 name: "",
282 margin_l: "0",
283 margin_r: "0",
284 margin_v: "0",
285 margin_t: None,
286 margin_b: None,
287 effect: "",
288 span: Span::new(0, 0, 0, 0),
289 }));
290 DialogueInfo::analyze(event).unwrap()
291 }
292
293 #[test]
294 fn empty_events_no_overlaps() {
295 let events = vec![];
296 assert_eq!(find_overlapping_dialogue_events(&events).len(), 0);
297 assert_eq!(count_overlapping_dialogue_events(&events), 0);
298 }
299
300 #[test]
301 fn calculate_duration_empty() {
302 let events = vec![];
303 assert_eq!(calculate_total_duration(&events), None);
304 assert_eq!(calculate_average_duration(&events), None);
305 }
306
307 #[test]
308 fn calculate_duration_single_event() {
309 let events = vec![create_test_dialogue_info("0:00:00.00", "0:00:05.00")];
310 assert_eq!(calculate_total_duration(&events), Some(500)); // 5 seconds = 500cs
311 assert_eq!(calculate_average_duration(&events), Some(500));
312 }
313
314 #[test]
315 fn calculate_duration_multiple_events() {
316 let events = vec![
317 create_test_dialogue_info("0:00:00.00", "0:00:05.00"),
318 create_test_dialogue_info("0:00:03.00", "0:00:08.00"),
319 create_test_dialogue_info("0:00:10.00", "0:00:15.00"),
320 ];
321
322 // Total: 0:00:00.00 to 0:00:15.00 = 1500cs
323 assert_eq!(calculate_total_duration(&events), Some(1500));
324
325 // Average: (500 + 500 + 500) / 3 = 500cs
326 assert_eq!(calculate_average_duration(&events), Some(500));
327 }
328
329 #[test]
330 fn sort_events_maintains_order() {
331 let mut events = vec![
332 create_test_dialogue_info("0:00:05.00", "0:00:10.00"),
333 create_test_dialogue_info("0:00:00.00", "0:00:05.00"),
334 create_test_dialogue_info("0:00:02.00", "0:00:07.00"),
335 ];
336
337 sort_events_by_time(&mut events);
338
339 assert_eq!(events[0].start_time_cs(), 0); // 0:00:00.00
340 assert_eq!(events[1].start_time_cs(), 200); // 0:00:02.00
341 assert_eq!(events[2].start_time_cs(), 500); // 0:00:05.00
342 }
343
344 #[test]
345 fn find_events_in_range_filters_correctly() {
346 let events = vec![
347 create_test_dialogue_info("0:00:00.00", "0:00:05.00"), // 0-500cs
348 create_test_dialogue_info("0:00:03.00", "0:00:08.00"), // 300-800cs
349 create_test_dialogue_info("0:00:10.00", "0:00:15.00"), // 1000-1500cs
350 ];
351
352 let indices = find_events_in_range(&events, 250, 600); // 2.5s to 6s
353 assert_eq!(indices, vec![0, 1]); // First two events overlap this range
354 }
355
356 #[test]
357 fn sort_events_same_start_time() {
358 // Test sorting when events have same start time (covers line 121)
359 let mut events = vec![
360 create_test_dialogue_info("0:00:05.00", "0:00:10.00"), // Same start, longer duration
361 create_test_dialogue_info("0:00:05.00", "0:00:08.00"), // Same start, shorter duration
362 create_test_dialogue_info("0:00:05.00", "0:00:12.00"), // Same start, longest duration
363 ];
364
365 sort_events_by_time(&mut events);
366
367 // All should have same start time but be sorted by end time
368 assert_eq!(events[0].start_time_cs(), 500);
369 assert_eq!(events[0].end_time_cs(), 800); // Shortest duration first
370 assert_eq!(events[1].start_time_cs(), 500);
371 assert_eq!(events[1].end_time_cs(), 1000); // Medium duration second
372 assert_eq!(events[2].start_time_cs(), 500);
373 assert_eq!(events[2].end_time_cs(), 1200); // Longest duration last
374 }
375}