#![allow(dead_code)]
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct MarkerRange {
pub id: u64,
pub name: String,
pub start: i64,
pub end: i64,
pub color: Option<String>,
pub category: Option<String>,
}
impl MarkerRange {
#[must_use]
pub fn new(id: u64, name: impl Into<String>, start: i64, end: i64) -> Self {
debug_assert!(start <= end, "start must not exceed end");
Self {
id,
name: name.into(),
start,
end,
color: None,
category: None,
}
}
#[must_use]
pub fn duration(&self) -> i64 {
(self.end - self.start).max(0) + 1
}
#[must_use]
pub fn contains(&self, frame: i64) -> bool {
frame >= self.start && frame <= self.end
}
#[must_use]
pub fn overlaps(&self, other: &Self) -> bool {
self.start <= other.end && other.start <= self.end
}
pub fn set_color(&mut self, color: impl Into<String>) {
self.color = Some(color.into());
}
pub fn set_category(&mut self, category: impl Into<String>) {
self.category = Some(category.into());
}
}
pub struct RangeCollection {
ranges: HashMap<u64, MarkerRange>,
next_id: u64,
}
impl RangeCollection {
#[must_use]
pub fn new() -> Self {
Self {
ranges: HashMap::new(),
next_id: 1,
}
}
pub fn add(&mut self, name: impl Into<String>, start: i64, end: i64) -> u64 {
let id = self.next_id;
self.next_id += 1;
self.ranges
.insert(id, MarkerRange::new(id, name, start, end));
id
}
pub fn remove(&mut self, id: u64) -> bool {
self.ranges.remove(&id).is_some()
}
#[must_use]
pub fn get(&self, id: u64) -> Option<&MarkerRange> {
self.ranges.get(&id)
}
pub fn get_mut(&mut self, id: u64) -> Option<&mut MarkerRange> {
self.ranges.get_mut(&id)
}
#[must_use]
pub fn sorted(&self) -> Vec<&MarkerRange> {
let mut v: Vec<&MarkerRange> = self.ranges.values().collect();
v.sort_by_key(|r| r.start);
v
}
#[must_use]
pub fn at_frame(&self, frame: i64) -> Vec<&MarkerRange> {
self.ranges.values().filter(|r| r.contains(frame)).collect()
}
#[must_use]
pub fn find_overlaps(&self) -> Vec<(u64, u64)> {
let sorted = self.sorted();
let mut pairs = Vec::new();
for (i, a) in sorted.iter().enumerate() {
for b in sorted.iter().skip(i + 1) {
if a.overlaps(b) {
pairs.push((a.id, b.id));
}
}
}
pairs
}
#[must_use]
pub fn len(&self) -> usize {
self.ranges.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.ranges.is_empty()
}
#[must_use]
pub fn to_csv(&self) -> String {
let mut lines = vec!["id,name,start,end,color".to_string()];
for r in self.sorted() {
lines.push(format!(
"{},{},{},{},{}",
r.id,
r.name,
r.start,
r.end,
r.color.as_deref().unwrap_or("")
));
}
lines.join("\n")
}
}
impl Default for RangeCollection {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_marker_range_new() {
let r = MarkerRange::new(1, "Act 1", 0, 99);
assert_eq!(r.start, 0);
assert_eq!(r.end, 99);
assert_eq!(r.name, "Act 1");
}
#[test]
fn test_marker_range_duration() {
let r = MarkerRange::new(1, "Scene", 10, 19);
assert_eq!(r.duration(), 10); }
#[test]
fn test_marker_range_duration_single_frame() {
let r = MarkerRange::new(1, "Snap", 5, 5);
assert_eq!(r.duration(), 1);
}
#[test]
fn test_marker_range_contains() {
let r = MarkerRange::new(1, "Range", 10, 20);
assert!(r.contains(10));
assert!(r.contains(15));
assert!(r.contains(20));
assert!(!r.contains(9));
assert!(!r.contains(21));
}
#[test]
fn test_marker_range_overlaps_yes() {
let a = MarkerRange::new(1, "A", 0, 10);
let b = MarkerRange::new(2, "B", 5, 15);
assert!(a.overlaps(&b));
assert!(b.overlaps(&a));
}
#[test]
fn test_marker_range_overlaps_adjacent() {
let a = MarkerRange::new(1, "A", 0, 10);
let b = MarkerRange::new(2, "B", 10, 20);
assert!(a.overlaps(&b));
}
#[test]
fn test_marker_range_no_overlap() {
let a = MarkerRange::new(1, "A", 0, 9);
let b = MarkerRange::new(2, "B", 10, 20);
assert!(!a.overlaps(&b));
}
#[test]
fn test_collection_add_and_get() {
let mut col = RangeCollection::new();
let id = col.add("Scene A", 0, 100);
assert_eq!(col.len(), 1);
let r = col.get(id).expect("get should succeed");
assert_eq!(r.name, "Scene A");
}
#[test]
fn test_collection_remove() {
let mut col = RangeCollection::new();
let id = col.add("X", 0, 10);
assert!(col.remove(id));
assert!(col.get(id).is_none());
assert!(!col.remove(id)); }
#[test]
fn test_collection_at_frame() {
let mut col = RangeCollection::new();
col.add("A", 0, 50);
col.add("B", 30, 100);
col.add("C", 60, 90);
let hits = col.at_frame(40);
assert_eq!(hits.len(), 2); }
#[test]
fn test_collection_find_overlaps() {
let mut col = RangeCollection::new();
let id1 = col.add("A", 0, 50);
let id2 = col.add("B", 40, 100);
let _id3 = col.add("C", 200, 300);
let pairs = col.find_overlaps();
assert_eq!(pairs.len(), 1);
let (a, b) = pairs[0];
assert!((a == id1 && b == id2) || (a == id2 && b == id1));
}
#[test]
fn test_collection_sorted() {
let mut col = RangeCollection::new();
col.add("Later", 100, 200);
col.add("Earlier", 0, 50);
let sorted = col.sorted();
assert_eq!(sorted[0].name, "Earlier");
assert_eq!(sorted[1].name, "Later");
}
#[test]
fn test_collection_to_csv() {
let mut col = RangeCollection::new();
let id = col.add("MyRange", 10, 20);
if let Some(r) = col.get_mut(id) {
r.set_color("#FF0000");
}
let csv = col.to_csv();
assert!(csv.contains("id,name,start,end,color"));
assert!(csv.contains("MyRange"));
assert!(csv.contains("#FF0000"));
}
#[test]
fn test_collection_default() {
let col: RangeCollection = Default::default();
assert!(col.is_empty());
}
}