use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Schedule {
pub assignments: Vec<Assignment>,
pub violations: Vec<Violation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Assignment {
pub activity_id: String,
pub task_id: String,
pub resource_id: String,
pub start_ms: i64,
pub end_ms: i64,
pub setup_ms: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Violation {
pub violation_type: ViolationType,
pub entity_id: String,
pub message: String,
pub severity: i32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ViolationType {
DeadlineMiss,
CapacityExceeded,
PrecedenceViolation,
ResourceUnavailable,
SkillMismatch,
Custom(String),
}
impl Assignment {
pub fn new(
activity_id: impl Into<String>,
task_id: impl Into<String>,
resource_id: impl Into<String>,
start_ms: i64,
end_ms: i64,
) -> Self {
Self {
activity_id: activity_id.into(),
task_id: task_id.into(),
resource_id: resource_id.into(),
start_ms,
end_ms,
setup_ms: 0,
}
}
pub fn with_setup(mut self, setup_ms: i64) -> Self {
self.setup_ms = setup_ms;
self
}
#[inline]
pub fn duration_ms(&self) -> i64 {
self.end_ms - self.start_ms
}
#[inline]
pub fn process_ms(&self) -> i64 {
self.duration_ms() - self.setup_ms
}
}
impl Violation {
pub fn deadline_miss(task_id: impl Into<String>, message: impl Into<String>) -> Self {
Self {
violation_type: ViolationType::DeadlineMiss,
entity_id: task_id.into(),
message: message.into(),
severity: 80,
}
}
pub fn capacity_exceeded(resource_id: impl Into<String>, message: impl Into<String>) -> Self {
Self {
violation_type: ViolationType::CapacityExceeded,
entity_id: resource_id.into(),
message: message.into(),
severity: 90,
}
}
pub fn precedence_violation(
activity_id: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
violation_type: ViolationType::PrecedenceViolation,
entity_id: activity_id.into(),
message: message.into(),
severity: 95,
}
}
}
impl Schedule {
pub fn new() -> Self {
Self::default()
}
pub fn add_assignment(&mut self, assignment: Assignment) {
self.assignments.push(assignment);
}
pub fn add_violation(&mut self, violation: Violation) {
self.violations.push(violation);
}
pub fn is_valid(&self) -> bool {
self.violations.is_empty()
}
pub fn makespan_ms(&self) -> i64 {
self.assignments.iter().map(|a| a.end_ms).max().unwrap_or(0)
}
pub fn assignment_for_activity(&self, activity_id: &str) -> Option<&Assignment> {
self.assignments
.iter()
.find(|a| a.activity_id == activity_id)
}
pub fn assignments_for_task(&self, task_id: &str) -> Vec<&Assignment> {
self.assignments
.iter()
.filter(|a| a.task_id == task_id)
.collect()
}
pub fn assignments_for_resource(&self, resource_id: &str) -> Vec<&Assignment> {
self.assignments
.iter()
.filter(|a| a.resource_id == resource_id)
.collect()
}
pub fn resource_utilization(&self, resource_id: &str, horizon_ms: i64) -> Option<f64> {
if horizon_ms <= 0 {
return None;
}
let busy: i64 = self
.assignments_for_resource(resource_id)
.iter()
.map(|a| a.duration_ms())
.sum();
Some(busy as f64 / horizon_ms as f64)
}
pub fn all_utilizations(&self) -> HashMap<String, f64> {
let horizon = self.makespan_ms();
if horizon <= 0 {
return HashMap::new();
}
let mut resource_busy: HashMap<String, i64> = HashMap::new();
for a in &self.assignments {
*resource_busy.entry(a.resource_id.clone()).or_insert(0) += a.duration_ms();
}
resource_busy
.into_iter()
.map(|(id, busy)| (id, busy as f64 / horizon as f64))
.collect()
}
pub fn task_completion_time(&self, task_id: &str) -> Option<i64> {
self.assignments_for_task(task_id)
.iter()
.map(|a| a.end_ms)
.max()
}
pub fn assignment_count(&self) -> usize {
self.assignments.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_schedule() -> Schedule {
let mut s = Schedule::new();
s.add_assignment(Assignment::new("O1", "J1", "M1", 0, 5000).with_setup(500));
s.add_assignment(Assignment::new("O2", "J1", "M2", 1000, 4000));
s.add_assignment(Assignment::new("O3", "J2", "M1", 5000, 8000));
s
}
#[test]
fn test_schedule_makespan() {
let s = sample_schedule();
assert_eq!(s.makespan_ms(), 8000);
}
#[test]
fn test_schedule_is_valid() {
let s = sample_schedule();
assert!(s.is_valid());
let mut s2 = sample_schedule();
s2.add_violation(Violation::deadline_miss("J1", "Late by 1000ms"));
assert!(!s2.is_valid());
}
#[test]
fn test_assignment_duration() {
let a = Assignment::new("O1", "J1", "M1", 0, 5000).with_setup(500);
assert_eq!(a.duration_ms(), 5000);
assert_eq!(a.process_ms(), 4500);
assert_eq!(a.setup_ms, 500);
}
#[test]
fn test_assignment_for_activity() {
let s = sample_schedule();
let a = s.assignment_for_activity("O1").unwrap();
assert_eq!(a.resource_id, "M1");
assert!(s.assignment_for_activity("O99").is_none());
}
#[test]
fn test_assignments_for_task() {
let s = sample_schedule();
let j1 = s.assignments_for_task("J1");
assert_eq!(j1.len(), 2);
let j2 = s.assignments_for_task("J2");
assert_eq!(j2.len(), 1);
}
#[test]
fn test_assignments_for_resource() {
let s = sample_schedule();
let m1 = s.assignments_for_resource("M1");
assert_eq!(m1.len(), 2); }
#[test]
fn test_resource_utilization() {
let s = sample_schedule();
let util = s.resource_utilization("M1", 8000).unwrap();
assert!((util - 1.0).abs() < 1e-10);
let util2 = s.resource_utilization("M2", 8000).unwrap();
assert!((util2 - 0.375).abs() < 1e-10);
}
#[test]
fn test_task_completion_time() {
let s = sample_schedule();
assert_eq!(s.task_completion_time("J1"), Some(5000)); assert_eq!(s.task_completion_time("J2"), Some(8000));
assert_eq!(s.task_completion_time("J99"), None);
}
#[test]
fn test_all_utilizations() {
let s = sample_schedule();
let utils = s.all_utilizations();
assert!((utils["M1"] - 1.0).abs() < 1e-10);
assert!((utils["M2"] - 0.375).abs() < 1e-10);
}
#[test]
fn test_empty_schedule() {
let s = Schedule::new();
assert_eq!(s.makespan_ms(), 0);
assert!(s.is_valid());
assert_eq!(s.assignment_count(), 0);
}
#[test]
fn test_violation_factories() {
let v1 = Violation::deadline_miss("J1", "Late");
assert_eq!(v1.violation_type, ViolationType::DeadlineMiss);
assert_eq!(v1.entity_id, "J1");
let v2 = Violation::capacity_exceeded("M1", "Over capacity");
assert_eq!(v2.violation_type, ViolationType::CapacityExceeded);
let v3 = Violation::precedence_violation("O2", "Started before O1");
assert_eq!(v3.violation_type, ViolationType::PrecedenceViolation);
}
}