#![allow(dead_code)]
use std::collections::HashSet;
#[derive(Debug, Clone)]
pub struct FilterableEvent {
pub number: u32,
pub reel: String,
pub edit_type: String,
pub track_type: String,
pub record_in: u64,
pub record_out: u64,
pub source_in: u64,
pub source_out: u64,
pub clip_name: Option<String>,
}
impl FilterableEvent {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new(
number: u32,
reel: &str,
edit_type: &str,
track_type: &str,
record_in: u64,
record_out: u64,
source_in: u64,
source_out: u64,
) -> Self {
Self {
number,
reel: reel.to_string(),
edit_type: edit_type.to_string(),
track_type: track_type.to_string(),
record_in,
record_out,
source_in,
source_out,
clip_name: None,
}
}
#[must_use]
pub fn with_clip_name(mut self, name: impl Into<String>) -> Self {
self.clip_name = Some(name.into());
self
}
#[must_use]
pub fn record_duration(&self) -> u64 {
self.record_out.saturating_sub(self.record_in)
}
#[must_use]
pub fn source_duration(&self) -> u64 {
self.source_out.saturating_sub(self.source_in)
}
}
#[derive(Debug, Clone)]
pub enum FilterCriterion {
Reel(String),
ReelSet(HashSet<String>),
EditType(String),
TrackType(String),
DurationRange {
min_frames: u64,
max_frames: u64,
},
RecordRange {
start: u64,
end: u64,
},
ClipNameContains(String),
EventNumberRange {
min: u32,
max: u32,
},
Not(Box<FilterCriterion>),
And(Box<FilterCriterion>, Box<FilterCriterion>),
Or(Box<FilterCriterion>, Box<FilterCriterion>),
}
impl FilterCriterion {
#[must_use]
pub fn matches(&self, event: &FilterableEvent) -> bool {
match self {
Self::Reel(name) => event.reel == *name,
Self::ReelSet(names) => names.contains(&event.reel),
Self::EditType(et) => event.edit_type == *et,
Self::TrackType(tt) => event.track_type == *tt,
Self::DurationRange {
min_frames,
max_frames,
} => {
let dur = event.record_duration();
dur >= *min_frames && dur < *max_frames
}
Self::RecordRange { start, end } => event.record_in < *end && event.record_out > *start,
Self::ClipNameContains(substring) => event
.clip_name
.as_ref()
.is_some_and(|name| name.contains(substring.as_str())),
Self::EventNumberRange { min, max } => event.number >= *min && event.number <= *max,
Self::Not(inner) => !inner.matches(event),
Self::And(a, b) => a.matches(event) && b.matches(event),
Self::Or(a, b) => a.matches(event) || b.matches(event),
}
}
#[must_use]
pub fn not(criterion: Self) -> Self {
Self::Not(Box::new(criterion))
}
#[must_use]
pub fn and(a: Self, b: Self) -> Self {
Self::And(Box::new(a), Box::new(b))
}
#[must_use]
pub fn or(a: Self, b: Self) -> Self {
Self::Or(Box::new(a), Box::new(b))
}
}
pub struct EventFilter {
criterion: FilterCriterion,
}
impl EventFilter {
#[must_use]
pub fn new(criterion: FilterCriterion) -> Self {
Self { criterion }
}
#[must_use]
pub fn apply<'a>(&self, events: &'a [FilterableEvent]) -> Vec<&'a FilterableEvent> {
events
.iter()
.filter(|e| self.criterion.matches(e))
.collect()
}
#[must_use]
pub fn apply_owned(&self, events: &[FilterableEvent]) -> Vec<FilterableEvent> {
events
.iter()
.filter(|e| self.criterion.matches(e))
.cloned()
.collect()
}
#[must_use]
pub fn count(&self, events: &[FilterableEvent]) -> usize {
events.iter().filter(|e| self.criterion.matches(e)).count()
}
#[must_use]
pub fn any_match(&self, events: &[FilterableEvent]) -> bool {
events.iter().any(|e| self.criterion.matches(e))
}
}
pub struct FilterBuilder {
criteria: Vec<FilterCriterion>,
}
impl FilterBuilder {
#[must_use]
pub fn new() -> Self {
Self {
criteria: Vec::new(),
}
}
#[must_use]
pub fn reel(mut self, name: impl Into<String>) -> Self {
self.criteria.push(FilterCriterion::Reel(name.into()));
self
}
#[must_use]
pub fn edit_type(mut self, edit_type: impl Into<String>) -> Self {
self.criteria
.push(FilterCriterion::EditType(edit_type.into()));
self
}
#[must_use]
pub fn track_type(mut self, track_type: impl Into<String>) -> Self {
self.criteria
.push(FilterCriterion::TrackType(track_type.into()));
self
}
#[must_use]
pub fn duration_range(mut self, min_frames: u64, max_frames: u64) -> Self {
self.criteria.push(FilterCriterion::DurationRange {
min_frames,
max_frames,
});
self
}
#[must_use]
pub fn record_range(mut self, start: u64, end: u64) -> Self {
self.criteria
.push(FilterCriterion::RecordRange { start, end });
self
}
#[must_use]
pub fn build(self) -> EventFilter {
if self.criteria.is_empty() {
return EventFilter::new(FilterCriterion::DurationRange {
min_frames: 0,
max_frames: u64::MAX,
});
}
let mut iter = self.criteria.into_iter();
let first = match iter.next() {
Some(c) => c,
None => {
return EventFilter::new(FilterCriterion::DurationRange {
min_frames: 0,
max_frames: u64::MAX,
})
}
};
let combined = iter.fold(first, FilterCriterion::and);
EventFilter::new(combined)
}
}
impl Default for FilterBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_event(
num: u32,
reel: &str,
edit: &str,
track: &str,
rec_in: u64,
rec_out: u64,
) -> FilterableEvent {
FilterableEvent::new(num, reel, edit, track, rec_in, rec_out, rec_in, rec_out)
}
fn sample_events() -> Vec<FilterableEvent> {
vec![
make_event(1, "A001", "Cut", "Video", 0, 100),
make_event(2, "A002", "Dissolve", "Video", 100, 250),
make_event(3, "A001", "Cut", "Audio", 250, 400),
make_event(4, "A003", "Wipe", "Video", 400, 600),
make_event(5, "A002", "Cut", "Both", 600, 700),
]
}
#[test]
fn test_filter_by_reel() {
let events = sample_events();
let f = EventFilter::new(FilterCriterion::Reel("A001".to_string()));
let matches = f.apply(&events);
assert_eq!(matches.len(), 2);
assert!(matches.iter().all(|e| e.reel == "A001"));
}
#[test]
fn test_filter_by_reel_set() {
let events = sample_events();
let mut reels = HashSet::new();
reels.insert("A001".to_string());
reels.insert("A003".to_string());
let f = EventFilter::new(FilterCriterion::ReelSet(reels));
let matches = f.apply(&events);
assert_eq!(matches.len(), 3);
}
#[test]
fn test_filter_by_edit_type() {
let events = sample_events();
let f = EventFilter::new(FilterCriterion::EditType("Cut".to_string()));
let matches = f.apply(&events);
assert_eq!(matches.len(), 3);
}
#[test]
fn test_filter_by_track_type() {
let events = sample_events();
let f = EventFilter::new(FilterCriterion::TrackType("Video".to_string()));
let matches = f.apply(&events);
assert_eq!(matches.len(), 3);
}
#[test]
fn test_filter_by_duration_range() {
let events = sample_events();
let f = EventFilter::new(FilterCriterion::DurationRange {
min_frames: 100,
max_frames: 200,
});
let matches = f.apply(&events);
assert_eq!(matches.len(), 4);
}
#[test]
fn test_filter_by_record_range() {
let events = sample_events();
let f = EventFilter::new(FilterCriterion::RecordRange {
start: 200,
end: 500,
});
let matches = f.apply(&events);
assert_eq!(matches.len(), 3);
}
#[test]
fn test_filter_by_clip_name_contains() {
let mut events = sample_events();
events[0] = events[0].clone().with_clip_name("interview_take1.mov");
events[1] = events[1].clone().with_clip_name("broll_park.mov");
let f = EventFilter::new(FilterCriterion::ClipNameContains("interview".to_string()));
let matches = f.apply(&events);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].number, 1);
}
#[test]
fn test_filter_by_event_number_range() {
let events = sample_events();
let f = EventFilter::new(FilterCriterion::EventNumberRange { min: 2, max: 4 });
let matches = f.apply(&events);
assert_eq!(matches.len(), 3);
}
#[test]
fn test_filter_not() {
let events = sample_events();
let f = EventFilter::new(FilterCriterion::not(FilterCriterion::EditType(
"Cut".to_string(),
)));
let matches = f.apply(&events);
assert_eq!(matches.len(), 2); }
#[test]
fn test_filter_and() {
let events = sample_events();
let criterion = FilterCriterion::and(
FilterCriterion::Reel("A001".to_string()),
FilterCriterion::TrackType("Video".to_string()),
);
let f = EventFilter::new(criterion);
let matches = f.apply(&events);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].number, 1);
}
#[test]
fn test_filter_or() {
let events = sample_events();
let criterion = FilterCriterion::or(
FilterCriterion::EditType("Dissolve".to_string()),
FilterCriterion::EditType("Wipe".to_string()),
);
let f = EventFilter::new(criterion);
let matches = f.apply(&events);
assert_eq!(matches.len(), 2);
}
#[test]
fn test_filter_count() {
let events = sample_events();
let f = EventFilter::new(FilterCriterion::EditType("Cut".to_string()));
assert_eq!(f.count(&events), 3);
}
#[test]
fn test_filter_any_match() {
let events = sample_events();
let f = EventFilter::new(FilterCriterion::EditType("Wipe".to_string()));
assert!(f.any_match(&events));
let f2 = EventFilter::new(FilterCriterion::EditType("Key".to_string()));
assert!(!f2.any_match(&events));
}
#[test]
fn test_filter_apply_owned() {
let events = sample_events();
let f = EventFilter::new(FilterCriterion::Reel("A003".to_string()));
let owned = f.apply_owned(&events);
assert_eq!(owned.len(), 1);
assert_eq!(owned[0].reel, "A003");
}
#[test]
fn test_filter_builder() {
let events = sample_events();
let filter = FilterBuilder::new()
.reel("A001")
.track_type("Video")
.build();
let matches = filter.apply(&events);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].number, 1);
}
#[test]
fn test_filter_builder_empty() {
let events = sample_events();
let filter = FilterBuilder::new().build();
let matches = filter.apply(&events);
assert_eq!(matches.len(), 5); }
#[test]
fn test_filterable_event_durations() {
let ev = FilterableEvent::new(1, "R1", "Cut", "Video", 100, 200, 50, 175);
assert_eq!(ev.record_duration(), 100);
assert_eq!(ev.source_duration(), 125);
}
#[test]
fn test_filter_builder_default() {
let builder = FilterBuilder::default();
let events = sample_events();
let filter = builder.build();
assert_eq!(filter.count(&events), 5);
}
}