use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CueEntry {
pub timestamp_ns: u64,
pub cluster_offset: u64,
pub track_number: Option<u64>,
}
impl CueEntry {
#[must_use]
pub fn new(timestamp_ns: u64, cluster_offset: u64) -> Self {
Self {
timestamp_ns,
cluster_offset,
track_number: None,
}
}
#[must_use]
pub fn with_track(mut self, track_number: u64) -> Self {
self.track_number = Some(track_number);
self
}
}
#[derive(Debug, Clone, Default)]
pub struct CuePositionCache {
entries: BTreeMap<u64, CueEntry>,
}
impl CuePositionCache {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, entry: CueEntry) {
self.entries.insert(entry.timestamp_ns, entry);
}
pub fn extend(&mut self, entries: impl IntoIterator<Item = CueEntry>) {
for entry in entries {
self.insert(entry);
}
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn clear(&mut self) {
self.entries.clear();
}
#[must_use]
pub fn find_cluster_before(&self, timestamp_ns: u64) -> Option<&CueEntry> {
self.entries
.range(..=timestamp_ns)
.next_back()
.map(|(_, v)| v)
}
#[must_use]
pub fn find_cluster_after(&self, timestamp_ns: u64) -> Option<&CueEntry> {
self.entries
.range((std::ops::Bound::Excluded(timestamp_ns), std::ops::Bound::Unbounded))
.next()
.map(|(_, v)| v)
}
#[must_use]
pub fn first(&self) -> Option<&CueEntry> {
self.entries.values().next()
}
#[must_use]
pub fn last(&self) -> Option<&CueEntry> {
self.entries.values().next_back()
}
pub fn iter(&self) -> impl Iterator<Item = &CueEntry> {
self.entries.values()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_cache() -> CuePositionCache {
let mut c = CuePositionCache::new();
for (ts, offset) in [(0, 1000), (1_000_000_000, 5000), (2_000_000_000, 12000), (5_000_000_000, 30000)] {
c.insert(CueEntry::new(ts, offset));
}
c
}
#[test]
fn test_cache_len() {
let c = make_cache();
assert_eq!(c.len(), 4);
}
#[test]
fn test_find_before_exact() {
let c = make_cache();
let entry = c.find_cluster_before(1_000_000_000).expect("should find");
assert_eq!(entry.cluster_offset, 5000);
}
#[test]
fn test_find_before_between() {
let c = make_cache();
let entry = c.find_cluster_before(1_500_000_000).expect("should find");
assert_eq!(entry.cluster_offset, 5000);
}
#[test]
fn test_find_before_at_start() {
let c = make_cache();
let entry = c.find_cluster_before(0);
assert!(entry.is_some());
assert_eq!(entry.unwrap().cluster_offset, 1000);
}
#[test]
fn test_find_before_before_first() {
let mut c = CuePositionCache::new();
c.insert(CueEntry::new(1_000_000_000, 5000));
let entry = c.find_cluster_before(500_000_000);
assert!(entry.is_none());
}
#[test]
fn test_find_after() {
let c = make_cache();
let entry = c.find_cluster_after(1_000_000_000).expect("should find after");
assert_eq!(entry.cluster_offset, 12000);
}
#[test]
fn test_find_after_past_last() {
let c = make_cache();
let entry = c.find_cluster_after(5_000_000_000);
assert!(entry.is_none());
}
#[test]
fn test_first_and_last() {
let c = make_cache();
assert_eq!(c.first().map(|e| e.cluster_offset), Some(1000));
assert_eq!(c.last().map(|e| e.cluster_offset), Some(30000));
}
#[test]
fn test_empty_cache() {
let c = CuePositionCache::new();
assert!(c.is_empty());
assert!(c.find_cluster_before(1_000_000_000).is_none());
}
#[test]
fn test_insert_overwrites() {
let mut c = CuePositionCache::new();
c.insert(CueEntry::new(1_000_000_000, 5000));
c.insert(CueEntry::new(1_000_000_000, 9999));
assert_eq!(c.len(), 1);
assert_eq!(c.find_cluster_before(1_000_000_000).unwrap().cluster_offset, 9999);
}
#[test]
fn test_extend() {
let mut c = CuePositionCache::new();
c.extend([CueEntry::new(0, 0), CueEntry::new(1_000_000_000, 100)]);
assert_eq!(c.len(), 2);
}
}