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}