#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverlapType {
Full,
Partial,
}
impl OverlapType {
#[must_use]
pub fn severity(self) -> u8 {
match self {
Self::Full => 2,
Self::Partial => 1,
}
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Full => "full",
Self::Partial => "partial",
}
}
}
#[derive(Debug, Clone)]
pub struct SubtitleOverlap {
pub cue_a: usize,
pub cue_b: usize,
pub overlap_start_ms: i64,
pub overlap_end_ms: i64,
pub overlap_type: OverlapType,
}
impl SubtitleOverlap {
#[must_use]
pub fn duration_ms(&self) -> i64 {
(self.overlap_end_ms - self.overlap_start_ms).max(0)
}
#[must_use]
pub fn is_full(&self) -> bool {
self.overlap_type == OverlapType::Full
}
#[must_use]
pub fn severity(&self) -> u8 {
self.overlap_type.severity()
}
}
#[derive(Debug, Clone)]
pub struct DetectableCue {
pub index: usize,
pub start_ms: i64,
pub end_ms: i64,
pub text: String,
}
impl DetectableCue {
#[must_use]
pub fn new(index: usize, start_ms: i64, end_ms: i64, text: impl Into<String>) -> Self {
Self {
index,
start_ms,
end_ms,
text: text.into(),
}
}
#[must_use]
pub fn overlaps_with(&self, other: &Self) -> bool {
self.start_ms < other.end_ms && other.start_ms < self.end_ms
}
#[must_use]
pub fn overlap_window(&self, other: &Self) -> Option<(i64, i64)> {
let start = self.start_ms.max(other.start_ms);
let end = self.end_ms.min(other.end_ms);
if start < end {
Some((start, end))
} else {
None
}
}
#[must_use]
pub fn overlap_type_with(&self, other: &Self) -> OverlapType {
if (self.start_ms >= other.start_ms && self.end_ms <= other.end_ms)
|| (other.start_ms >= self.start_ms && other.end_ms <= self.end_ms)
{
OverlapType::Full
} else {
OverlapType::Partial
}
}
}
#[derive(Debug, Default)]
pub struct OverlapDetector {
pub allow_touching: bool,
}
impl OverlapDetector {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_allow_touching(mut self, allow: bool) -> Self {
self.allow_touching = allow;
self
}
#[must_use]
pub fn find_overlaps(&self, cues: &[DetectableCue]) -> Vec<SubtitleOverlap> {
let mut overlaps = Vec::new();
for i in 0..cues.len() {
for j in (i + 1)..cues.len() {
let a = &cues[i];
let b = &cues[j];
let does_overlap = if self.allow_touching {
a.start_ms <= b.end_ms && b.start_ms <= a.end_ms
} else {
a.start_ms < b.end_ms && b.start_ms < a.end_ms
};
if does_overlap {
if let Some((start, end)) = a.overlap_window(b) {
let overlap_type = a.overlap_type_with(b);
overlaps.push(SubtitleOverlap {
cue_a: a.index,
cue_b: b.index,
overlap_start_ms: start,
overlap_end_ms: end,
overlap_type,
});
}
}
}
}
overlaps
}
}
#[derive(Debug, Clone)]
pub struct OverlapReport {
pub total_overlaps: usize,
pub full_overlap_count: usize,
pub partial_overlap_count: usize,
pub max_overlap_ms: i64,
pub overlaps: Vec<SubtitleOverlap>,
}
impl OverlapReport {
#[must_use]
pub fn from_overlaps(overlaps: Vec<SubtitleOverlap>) -> Self {
let total_overlaps = overlaps.len();
let full_overlap_count = overlaps.iter().filter(|o| o.is_full()).count();
let partial_overlap_count = total_overlaps - full_overlap_count;
let max_overlap_ms = overlaps.iter().map(|o| o.duration_ms()).max().unwrap_or(0);
Self {
total_overlaps,
full_overlap_count,
partial_overlap_count,
max_overlap_ms,
overlaps,
}
}
#[must_use]
pub fn has_full_overlaps(&self) -> bool {
self.full_overlap_count > 0
}
#[must_use]
pub fn is_clean(&self) -> bool {
self.total_overlaps == 0
}
#[must_use]
pub fn overlaps_for_cue(&self, cue_index: usize) -> Vec<&SubtitleOverlap> {
self.overlaps
.iter()
.filter(|o| o.cue_a == cue_index || o.cue_b == cue_index)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_cue(index: usize, start_ms: i64, end_ms: i64) -> DetectableCue {
DetectableCue::new(index, start_ms, end_ms, format!("Cue {index}"))
}
#[test]
fn test_overlap_type_severity() {
assert_eq!(OverlapType::Full.severity(), 2);
assert_eq!(OverlapType::Partial.severity(), 1);
}
#[test]
fn test_overlap_type_label() {
assert_eq!(OverlapType::Full.label(), "full");
assert_eq!(OverlapType::Partial.label(), "partial");
}
#[test]
fn test_subtitle_overlap_duration_ms() {
let o = SubtitleOverlap {
cue_a: 0,
cue_b: 1,
overlap_start_ms: 1000,
overlap_end_ms: 1500,
overlap_type: OverlapType::Partial,
};
assert_eq!(o.duration_ms(), 500);
}
#[test]
fn test_subtitle_overlap_is_full() {
let o = SubtitleOverlap {
cue_a: 0,
cue_b: 1,
overlap_start_ms: 0,
overlap_end_ms: 1000,
overlap_type: OverlapType::Full,
};
assert!(o.is_full());
}
#[test]
fn test_detectable_cue_overlaps_with() {
let a = make_cue(0, 0, 2000);
let b = make_cue(1, 1000, 3000);
assert!(a.overlaps_with(&b));
}
#[test]
fn test_detectable_cue_no_overlap() {
let a = make_cue(0, 0, 1000);
let b = make_cue(1, 1000, 2000); assert!(!a.overlaps_with(&b));
}
#[test]
fn test_detectable_cue_overlap_window() {
let a = make_cue(0, 0, 2000);
let b = make_cue(1, 1000, 3000);
assert_eq!(a.overlap_window(&b), Some((1000, 2000)));
}
#[test]
fn test_detectable_cue_overlap_type_full() {
let outer = make_cue(0, 0, 5000);
let inner = make_cue(1, 1000, 3000);
assert_eq!(outer.overlap_type_with(&inner), OverlapType::Full);
}
#[test]
fn test_detectable_cue_overlap_type_partial() {
let a = make_cue(0, 0, 2000);
let b = make_cue(1, 1000, 3000);
assert_eq!(a.overlap_type_with(&b), OverlapType::Partial);
}
#[test]
fn test_detector_no_overlaps_sequential() {
let cues = vec![
make_cue(0, 0, 1000),
make_cue(1, 1000, 2000),
make_cue(2, 2000, 3000),
];
let overlaps = OverlapDetector::new().find_overlaps(&cues);
assert!(overlaps.is_empty());
}
#[test]
fn test_detector_finds_partial_overlap() {
let cues = vec![make_cue(0, 0, 2000), make_cue(1, 1500, 3000)];
let overlaps = OverlapDetector::new().find_overlaps(&cues);
assert_eq!(overlaps.len(), 1);
assert_eq!(overlaps[0].overlap_type, OverlapType::Partial);
}
#[test]
fn test_detector_finds_full_overlap() {
let cues = vec![make_cue(0, 0, 5000), make_cue(1, 1000, 3000)];
let overlaps = OverlapDetector::new().find_overlaps(&cues);
assert_eq!(overlaps.len(), 1);
assert!(overlaps[0].is_full());
}
#[test]
fn test_report_has_full_overlaps() {
let o = SubtitleOverlap {
cue_a: 0,
cue_b: 1,
overlap_start_ms: 0,
overlap_end_ms: 1000,
overlap_type: OverlapType::Full,
};
let report = OverlapReport::from_overlaps(vec![o]);
assert!(report.has_full_overlaps());
assert!(!report.is_clean());
}
#[test]
fn test_report_is_clean_when_no_overlaps() {
let report = OverlapReport::from_overlaps(vec![]);
assert!(report.is_clean());
assert!(!report.has_full_overlaps());
}
#[test]
fn test_report_overlaps_for_cue() {
let cues = vec![
make_cue(0, 0, 2000),
make_cue(1, 1500, 3000),
make_cue(2, 5000, 7000),
];
let overlaps = OverlapDetector::new().find_overlaps(&cues);
let report = OverlapReport::from_overlaps(overlaps);
let for_cue_0 = report.overlaps_for_cue(0);
assert_eq!(for_cue_0.len(), 1);
let for_cue_2 = report.overlaps_for_cue(2);
assert!(for_cue_2.is_empty());
}
}