use crate::models::{Calendar, Resource, TimeWindow};
#[derive(Debug, Clone)]
pub struct ResourceTimeline {
pub resource_id: String,
capacity: i32,
calendar: Option<Calendar>,
bookings: Vec<TimeWindow>,
}
impl ResourceTimeline {
pub fn new(resource: &Resource) -> Self {
Self {
resource_id: resource.id.clone(),
capacity: resource.capacity.max(1),
calendar: resource.calendar.clone(),
bookings: Vec::new(),
}
}
pub fn fits(&self, start_ms: i64, duration_ms: i64) -> bool {
let end_ms = start_ms + duration_ms;
let cal_ok = self
.calendar
.as_ref()
.is_none_or(|c| c.interval_fits(start_ms, end_ms));
cal_ok && self.capacity_ok(start_ms, end_ms)
}
pub fn earliest_fit(&self, from_ms: i64, duration_ms: i64) -> Option<i64> {
let mut candidates: Vec<i64> = vec![from_ms];
if let Some(cal) = &self.calendar {
candidates.extend(cal.time_windows.iter().map(|w| w.start_ms));
candidates.extend(cal.blocked_periods.iter().map(|b| b.end_ms));
}
candidates.extend(self.bookings.iter().map(|b| b.end_ms));
candidates.retain(|&t| t >= from_ms);
candidates.sort_unstable();
candidates.dedup();
candidates.into_iter().find(|&t| self.fits(t, duration_ms))
}
pub fn book(&mut self, start_ms: i64, end_ms: i64) {
self.bookings.push(TimeWindow::new(start_ms, end_ms));
}
fn capacity_ok(&self, start_ms: i64, end_ms: i64) -> bool {
let overlapping: Vec<&TimeWindow> = self
.bookings
.iter()
.filter(|b| b.start_ms < end_ms && start_ms < b.end_ms)
.collect();
if (overlapping.len() as i32) < self.capacity {
return true;
}
overlapping.iter().all(|probe| {
let p = probe.start_ms.max(start_ms);
let concurrent = overlapping.iter().filter(|b| b.contains(p)).count() as i32;
concurrent < self.capacity
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{Calendar, Resource};
#[test]
fn test_earliest_fit_empty() {
let tl = ResourceTimeline::new(&Resource::primary("M1"));
assert_eq!(tl.earliest_fit(0, 1000), Some(0));
assert_eq!(tl.earliest_fit(500, 1000), Some(500));
}
#[test]
fn test_earliest_fit_after_booking() {
let mut tl = ResourceTimeline::new(&Resource::primary("M1"));
tl.book(0, 1000);
assert_eq!(tl.earliest_fit(0, 500), Some(1000));
tl.book(1500, 2000);
assert_eq!(tl.earliest_fit(0, 500), Some(1000));
assert_eq!(tl.earliest_fit(0, 600), Some(2000));
}
#[test]
fn test_capacity_two_parallel() {
let mut tl = ResourceTimeline::new(&Resource::primary("M1").with_capacity(2));
tl.book(0, 1000);
assert_eq!(tl.earliest_fit(0, 1000), Some(0)); tl.book(0, 1000);
assert_eq!(tl.earliest_fit(0, 1000), Some(1000)); }
#[test]
fn test_calendar_respected() {
let cal = Calendar::new("shift")
.with_window(0, 5000)
.with_window(10_000, 20_000);
let mut tl = ResourceTimeline::new(&Resource::primary("M1").with_calendar(cal));
tl.book(0, 3000);
assert_eq!(tl.earliest_fit(0, 3000), Some(10_000));
assert_eq!(tl.earliest_fit(0, 2000), Some(3000));
}
#[test]
fn test_calendar_never_fits() {
let cal = Calendar::new("short").with_window(0, 1000);
let tl = ResourceTimeline::new(&Resource::primary("M1").with_calendar(cal));
assert_eq!(tl.earliest_fit(0, 2000), None);
}
}