ass_core/analysis/linting/rules/
timing_overlap.rs

1//! Timing overlap detection rule for ASS script linting.
2//!
3//! Detects overlapping dialogue events that may cause rendering conflicts
4//! using efficient O(n log n) sweep-line algorithm.
5
6use crate::{
7    analysis::{
8        events::find_overlapping_events,
9        linting::{IssueCategory, IssueSeverity, LintIssue, LintRule},
10        ScriptAnalysis,
11    },
12    parser::Section,
13};
14use alloc::{format, string::ToString, vec::Vec};
15
16/// Rule for detecting timing overlaps between dialogue events
17///
18/// Uses sweep-line algorithm for efficient O(n log n) overlap detection.
19/// Overlapping events can cause rendering conflicts where multiple subtitles
20/// appear simultaneously, potentially causing readability issues.
21///
22/// # Performance
23///
24/// - Time complexity: O(n log n) via sweep-line algorithm
25/// - Memory: O(n) for temporary data structures
26/// - Target: <1ms for 1000 events on typical hardware
27///
28/// # Example
29///
30/// ```rust
31/// use ass_core::analysis::linting::rules::timing_overlap::TimingOverlapRule;
32/// use ass_core::analysis::linting::LintRule;
33/// use ass_core::{Script, ScriptAnalysis};
34///
35/// let script = Script::parse(r#"
36/// [Events]
37/// Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
38/// Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,First event
39/// Dialogue: 0,0:00:03.00,0:00:08.00,Default,,0,0,0,,Overlapping event
40/// "#)?;
41///
42/// let analysis = ScriptAnalysis::analyze(&script)?;
43/// let rule = TimingOverlapRule;
44/// let issues = rule.check_script(&analysis);
45/// assert!(!issues.is_empty()); // Should detect the overlap
46/// # Ok::<(), Box<dyn std::error::Error>>(())
47/// ```
48pub struct TimingOverlapRule;
49
50impl LintRule for TimingOverlapRule {
51    fn id(&self) -> &'static str {
52        "timing-overlap"
53    }
54
55    fn name(&self) -> &'static str {
56        "Timing Overlap"
57    }
58
59    fn description(&self) -> &'static str {
60        "Detects overlapping dialogue events that may cause rendering conflicts"
61    }
62
63    fn default_severity(&self) -> IssueSeverity {
64        IssueSeverity::Warning
65    }
66
67    fn category(&self) -> IssueCategory {
68        IssueCategory::Timing
69    }
70
71    fn check_script(&self, analysis: &ScriptAnalysis) -> Vec<LintIssue> {
72        let mut issues = Vec::new();
73
74        if let Some(Section::Events(events)) = analysis
75            .script()
76            .sections()
77            .iter()
78            .find(|s| matches!(s, Section::Events(_)))
79        {
80            if let Ok(overlaps) = find_overlapping_events(events) {
81                for (i, j) in overlaps {
82                    let event1 = &events[i];
83                    let event2 = &events[j];
84
85                    let issue = LintIssue::new(
86                        self.default_severity(),
87                        IssueCategory::Timing,
88                        self.id(),
89                        format!(
90                            "Event overlaps: {} to {} overlaps with {} to {}",
91                            event1.start, event1.end, event2.start, event2.end
92                        ),
93                    )
94                    .with_description(
95                        "Overlapping events may cause rendering conflicts".to_string(),
96                    );
97
98                    issues.push(issue);
99                }
100            } else {
101                let issue = LintIssue::new(
102                    IssueSeverity::Warning,
103                    IssueCategory::Timing,
104                    self.id(),
105                    "Could not analyze event overlaps due to timing parse errors".to_string(),
106                );
107                issues.push(issue);
108            }
109        }
110
111        issues
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn rule_metadata_correct() {
121        let rule = TimingOverlapRule;
122        assert_eq!(rule.id(), "timing-overlap");
123        assert_eq!(rule.name(), "Timing Overlap");
124        assert_eq!(
125            rule.description(),
126            "Detects overlapping dialogue events that may cause rendering conflicts"
127        );
128        assert_eq!(rule.default_severity(), IssueSeverity::Warning);
129        assert_eq!(rule.category(), IssueCategory::Timing);
130    }
131
132    #[test]
133    fn empty_script_no_issues() {
134        let script_text = "[Script Info]\nTitle: Test";
135        let script = crate::parser::Script::parse(script_text).unwrap();
136        let analysis = ScriptAnalysis::analyze(&script).unwrap();
137
138        let rule = TimingOverlapRule;
139        let issues = rule.check_script(&analysis);
140
141        assert!(issues.is_empty());
142    }
143
144    #[test]
145    fn non_overlapping_events_no_issues() {
146        let script_text = r"[Events]
147Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
148Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,First event
149Dialogue: 0,0:00:05.00,0:00:10.00,Default,,0,0,0,,Second event";
150
151        let script = crate::parser::Script::parse(script_text).unwrap();
152        let analysis = ScriptAnalysis::analyze(&script).unwrap();
153        let rule = TimingOverlapRule;
154        let issues = rule.check_script(&analysis);
155
156        assert!(issues.is_empty());
157    }
158}