ass_core/analysis/linting/rules/
negative_duration.rs

1//! Negative duration detection rule for ASS script linting.
2//!
3//! Detects events with negative or zero duration that would cause
4//! rendering issues or indicate timing errors in subtitle scripts.
5
6use crate::{
7    analysis::{
8        linting::{IssueCategory, IssueSeverity, LintIssue, LintRule},
9        ScriptAnalysis,
10    },
11    parser::Section,
12    utils::parse_ass_time,
13};
14use alloc::{format, string::ToString, vec::Vec};
15
16/// Rule for detecting events with negative or zero duration
17///
18/// Events with start time >= end time are invalid and will not display
19/// properly in most subtitle renderers. This rule catches timing errors
20/// that could result from manual editing mistakes or conversion issues.
21///
22/// # Performance
23///
24/// - Time complexity: O(n) for n events
25/// - Memory: O(1) additional space
26/// - Target: <0.5ms for 1000 events
27///
28/// # Example
29///
30/// ```rust
31/// use ass_core::analysis::linting::rules::negative_duration::NegativeDurationRule;
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:05.00,0:00:02.00,Default,,0,0,0,,Invalid event
39/// "#)?;
40///
41/// let analysis = ScriptAnalysis::analyze(&script)?;
42/// let rule = NegativeDurationRule;
43/// let issues = rule.check_script(&analysis);
44/// assert!(!issues.is_empty()); // Should detect the negative duration
45/// # Ok::<(), Box<dyn std::error::Error>>(())
46/// ```
47pub struct NegativeDurationRule;
48
49impl LintRule for NegativeDurationRule {
50    fn id(&self) -> &'static str {
51        "negative-duration"
52    }
53
54    fn name(&self) -> &'static str {
55        "Negative Duration"
56    }
57
58    fn description(&self) -> &'static str {
59        "Detects events with negative or zero duration"
60    }
61
62    fn default_severity(&self) -> IssueSeverity {
63        IssueSeverity::Error
64    }
65
66    fn category(&self) -> IssueCategory {
67        IssueCategory::Timing
68    }
69
70    fn check_script(&self, analysis: &ScriptAnalysis) -> Vec<LintIssue> {
71        let mut issues = Vec::new();
72
73        if let Some(Section::Events(events)) = analysis
74            .script()
75            .sections()
76            .iter()
77            .find(|s| matches!(s, Section::Events(_)))
78        {
79            for event in events {
80                if let (Ok(start), Ok(end)) =
81                    (parse_ass_time(event.start), parse_ass_time(event.end))
82                {
83                    if start >= end {
84                        let issue = LintIssue::new(
85                            self.default_severity(),
86                            IssueCategory::Timing,
87                            self.id(),
88                            format!(
89                                "Invalid duration: start {} >= end {}",
90                                event.start, event.end
91                            ),
92                        )
93                        .with_description(
94                            "Events must have positive duration for proper display".to_string(),
95                        );
96
97                        issues.push(issue);
98                    }
99                }
100            }
101        }
102
103        issues
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn rule_metadata_correct() {
113        let rule = NegativeDurationRule;
114        assert_eq!(rule.id(), "negative-duration");
115        assert_eq!(rule.name(), "Negative Duration");
116        assert_eq!(
117            rule.description(),
118            "Detects events with negative or zero duration"
119        );
120        assert_eq!(rule.default_severity(), IssueSeverity::Error);
121        assert_eq!(rule.category(), IssueCategory::Timing);
122    }
123
124    #[test]
125    fn empty_script_no_issues() {
126        let script_text = "[Script Info]\nTitle: Test";
127        let script = crate::parser::Script::parse(script_text).unwrap();
128        let analysis = ScriptAnalysis::analyze(&script).unwrap();
129
130        let rule = NegativeDurationRule;
131        let issues = rule.check_script(&analysis);
132
133        assert!(issues.is_empty());
134    }
135
136    #[test]
137    fn valid_duration_no_issues() {
138        let script_text = r"[Events]
139Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
140Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Valid event";
141
142        let script = crate::parser::Script::parse(script_text).unwrap();
143        let analysis = ScriptAnalysis::analyze(&script).unwrap();
144        let rule = NegativeDurationRule;
145        let issues = rule.check_script(&analysis);
146
147        assert!(issues.is_empty());
148    }
149
150    #[test]
151    fn negative_duration_detected() {
152        let script_text = r"[Events]
153Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
154Dialogue: 0,0:00:05.00,0:00:02.00,Default,,0,0,0,,Invalid event";
155
156        let script = crate::parser::Script::parse(script_text).unwrap();
157        let analysis = ScriptAnalysis::analyze(&script).unwrap();
158        let rule = NegativeDurationRule;
159        let issues = rule.check_script(&analysis);
160
161        assert_eq!(issues.len(), 1);
162        assert_eq!(issues[0].severity(), IssueSeverity::Error);
163        assert_eq!(issues[0].category(), IssueCategory::Timing);
164    }
165
166    #[test]
167    fn zero_duration_detected() {
168        let script_text = r"[Events]
169Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
170Dialogue: 0,0:00:05.00,0:00:05.00,Default,,0,0,0,,Zero duration";
171
172        let script = crate::parser::Script::parse(script_text).unwrap();
173        let analysis = ScriptAnalysis::analyze(&script).unwrap();
174        let rule = NegativeDurationRule;
175        let issues = rule.check_script(&analysis);
176
177        assert_eq!(issues.len(), 1);
178        assert_eq!(issues[0].severity(), IssueSeverity::Error);
179    }
180
181    #[test]
182    fn multiple_invalid_durations() {
183        let script_text = r"[Events]
184Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
185Dialogue: 0,0:00:05.00,0:00:02.00,Default,,0,0,0,,First invalid
186Dialogue: 0,0:00:01.00,0:00:06.00,Default,,0,0,0,,Valid event
187Dialogue: 0,0:00:10.00,0:00:10.00,Default,,0,0,0,,Second invalid";
188
189        let script = crate::parser::Script::parse(script_text).unwrap();
190        let analysis = ScriptAnalysis::analyze(&script).unwrap();
191        let rule = NegativeDurationRule;
192        let issues = rule.check_script(&analysis);
193
194        assert_eq!(issues.len(), 2);
195    }
196}