use crate::error::ConformResult;
use crate::types::ClipReference;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone)]
pub enum PartialConformSelector {
All,
ByEventNumbers(HashSet<u32>),
ByReelNames(HashSet<String>),
ByClipIds(HashSet<String>),
All2(Box<PartialConformSelector>, Box<PartialConformSelector>),
Any2(Box<PartialConformSelector>, Box<PartialConformSelector>),
}
impl PartialConformSelector {
#[must_use]
pub fn matches(&self, clip: &ClipReference) -> bool {
match self {
Self::All => true,
Self::ByEventNumbers(nums) => clip
.metadata
.get("event_number")
.and_then(|s| s.parse::<u32>().ok())
.map_or(false, |n| nums.contains(&n)),
Self::ByReelNames(reels) => clip
.metadata
.get("reel")
.map_or(false, |r| reels.contains(r.as_str())),
Self::ByClipIds(ids) => ids.contains(clip.id.as_str()),
Self::All2(a, b) => a.matches(clip) && b.matches(clip),
Self::Any2(a, b) => a.matches(clip) || b.matches(clip),
}
}
#[must_use]
pub fn by_reels(reels: &[&str]) -> Self {
Self::ByReelNames(reels.iter().map(|s| s.to_string()).collect())
}
#[must_use]
pub fn by_events(numbers: &[u32]) -> Self {
Self::ByEventNumbers(numbers.iter().copied().collect())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PartialConformStats {
pub total_clips: usize,
pub selected_clips: usize,
pub skipped_clips: usize,
}
impl PartialConformStats {
#[must_use]
pub fn compute(total: usize, selected: usize) -> Self {
Self {
total_clips: total,
selected_clips: selected,
skipped_clips: total.saturating_sub(selected),
}
}
#[must_use]
pub fn selection_fraction(&self) -> f64 {
if self.total_clips == 0 {
0.0
} else {
self.selected_clips as f64 / self.total_clips as f64
}
}
}
pub struct PartialBatchProcessor {
pub selector: PartialConformSelector,
}
impl PartialBatchProcessor {
#[must_use]
pub fn new(selector: PartialConformSelector) -> Self {
Self { selector }
}
#[must_use]
pub fn filter_clips<'a>(&self, clips: &'a [ClipReference]) -> Vec<&'a ClipReference> {
clips.iter().filter(|c| self.selector.matches(c)).collect()
}
#[must_use]
pub fn select_clips(&self, clips: &[ClipReference]) -> Vec<ClipReference> {
clips
.iter()
.filter(|c| self.selector.matches(c))
.cloned()
.collect()
}
#[must_use]
pub fn stats(&self, clips: &[ClipReference]) -> PartialConformStats {
let selected = clips.iter().filter(|c| self.selector.matches(c)).count();
PartialConformStats::compute(clips.len(), selected)
}
#[must_use]
pub fn partition_clips(
&self,
clips: Vec<ClipReference>,
) -> (Vec<ClipReference>, Vec<ClipReference>) {
clips.into_iter().partition(|c| self.selector.matches(c))
}
pub fn build_plan(&self, clips: &[ClipReference]) -> ConformResult<PartialConformPlan> {
let mut selected = Vec::new();
let mut skipped = Vec::new();
for clip in clips {
if self.selector.matches(clip) {
selected.push(clip.id.clone());
} else {
skipped.push(clip.id.clone());
}
}
let stats = PartialConformStats::compute(clips.len(), selected.len());
Ok(PartialConformPlan {
selected_ids: selected,
skipped_ids: skipped,
stats,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PartialConformPlan {
pub selected_ids: Vec<String>,
pub skipped_ids: Vec<String>,
pub stats: PartialConformStats,
}
impl PartialConformPlan {
#[must_use]
pub fn has_work(&self) -> bool {
!self.selected_ids.is_empty()
}
#[must_use]
pub fn has_skipped(&self) -> bool {
!self.skipped_ids.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{FrameRate, Timecode, TrackType};
use std::collections::HashMap;
fn make_clip(id: &str, reel: &str, event: u32) -> ClipReference {
let mut metadata = HashMap::new();
metadata.insert("reel".to_string(), reel.to_string());
metadata.insert("event_number".to_string(), event.to_string());
let tc = Timecode::new(0, 0, 0, 0);
ClipReference {
id: id.to_string(),
source_file: None,
source_in: tc,
source_out: tc,
record_in: tc,
record_out: tc,
track: TrackType::Video,
fps: FrameRate::Fps25,
metadata,
}
}
#[test]
fn test_selector_all_matches_everything() {
let clips = vec![make_clip("c1", "R1", 1), make_clip("c2", "R2", 2)];
let proc = PartialBatchProcessor::new(PartialConformSelector::All);
assert_eq!(proc.filter_clips(&clips).len(), 2);
}
#[test]
fn test_selector_by_event_numbers() {
let clips = vec![
make_clip("c1", "R1", 1),
make_clip("c2", "R2", 2),
make_clip("c3", "R3", 3),
];
let sel = PartialConformSelector::by_events(&[1, 3]);
let proc = PartialBatchProcessor::new(sel);
let selected = proc.filter_clips(&clips);
assert_eq!(selected.len(), 2);
assert!(selected.iter().any(|c| c.id == "c1"));
assert!(selected.iter().any(|c| c.id == "c3"));
}
#[test]
fn test_selector_by_reel_names() {
let clips = vec![
make_clip("c1", "REEL_A", 1),
make_clip("c2", "REEL_B", 2),
make_clip("c3", "REEL_A", 3),
];
let sel = PartialConformSelector::by_reels(&["REEL_A"]);
let proc = PartialBatchProcessor::new(sel);
let selected = proc.filter_clips(&clips);
assert_eq!(selected.len(), 2);
}
#[test]
fn test_selector_by_clip_ids() {
let clips = vec![
make_clip("event_1", "R1", 1),
make_clip("event_2", "R2", 2),
make_clip("event_3", "R3", 3),
];
let ids: HashSet<String> = ["event_1", "event_3"]
.iter()
.map(|s| s.to_string())
.collect();
let sel = PartialConformSelector::ByClipIds(ids);
let proc = PartialBatchProcessor::new(sel);
let selected = proc.filter_clips(&clips);
assert_eq!(selected.len(), 2);
}
#[test]
fn test_selector_any2_disjunction() {
let clips = vec![
make_clip("c1", "REEL_A", 1),
make_clip("c2", "REEL_B", 2),
make_clip("c3", "REEL_C", 3),
];
let sel = PartialConformSelector::Any2(
Box::new(PartialConformSelector::by_reels(&["REEL_A"])),
Box::new(PartialConformSelector::by_events(&[2])),
);
let proc = PartialBatchProcessor::new(sel);
let selected = proc.filter_clips(&clips);
assert_eq!(selected.len(), 2);
}
#[test]
fn test_selector_all2_conjunction() {
let clips = vec![
make_clip("c1", "REEL_A", 1),
make_clip("c2", "REEL_A", 2),
make_clip("c3", "REEL_B", 1),
];
let sel = PartialConformSelector::All2(
Box::new(PartialConformSelector::by_reels(&["REEL_A"])),
Box::new(PartialConformSelector::by_events(&[1])),
);
let proc = PartialBatchProcessor::new(sel);
let selected = proc.filter_clips(&clips);
assert_eq!(selected.len(), 1);
assert_eq!(selected[0].id, "c1");
}
#[test]
fn test_stats_computation() {
let clips = vec![
make_clip("c1", "R1", 1),
make_clip("c2", "R2", 2),
make_clip("c3", "R3", 3),
make_clip("c4", "R4", 4),
];
let sel = PartialConformSelector::by_events(&[1, 2]);
let proc = PartialBatchProcessor::new(sel);
let stats = proc.stats(&clips);
assert_eq!(stats.total_clips, 4);
assert_eq!(stats.selected_clips, 2);
assert_eq!(stats.skipped_clips, 2);
assert!((stats.selection_fraction() - 0.5).abs() < 1e-9);
}
#[test]
fn test_partition_clips() {
let clips = vec![
make_clip("c1", "R1", 1),
make_clip("c2", "R2", 2),
make_clip("c3", "R3", 3),
];
let sel = PartialConformSelector::by_events(&[2]);
let proc = PartialBatchProcessor::new(sel);
let (selected, skipped) = proc.partition_clips(clips);
assert_eq!(selected.len(), 1);
assert_eq!(selected[0].id, "c2");
assert_eq!(skipped.len(), 2);
}
#[test]
fn test_build_plan() {
let clips = vec![make_clip("c1", "R1", 1), make_clip("c2", "R2", 2)];
let sel = PartialConformSelector::by_events(&[1]);
let proc = PartialBatchProcessor::new(sel);
let plan = proc.build_plan(&clips).expect("plan should build");
assert_eq!(plan.selected_ids, vec!["c1"]);
assert_eq!(plan.skipped_ids, vec!["c2"]);
assert!(plan.has_work());
assert!(plan.has_skipped());
}
#[test]
fn test_empty_clip_list() {
let clips: Vec<ClipReference> = vec![];
let sel = PartialConformSelector::All;
let proc = PartialBatchProcessor::new(sel);
assert!(proc.filter_clips(&clips).is_empty());
let stats = proc.stats(&clips);
assert_eq!(stats.total_clips, 0);
assert_eq!(stats.selection_fraction(), 0.0);
}
}