use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::Calendar;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Resource {
pub id: String,
pub name: String,
pub resource_type: ResourceType,
pub capacity: i32,
pub efficiency: f64,
pub calendar: Option<Calendar>,
pub skills: Vec<Skill>,
pub cost_per_hour: Option<f64>,
pub attributes: HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResourceType {
Primary,
Secondary,
Human,
Consumable,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Skill {
pub name: String,
pub level: f64,
}
impl Resource {
pub fn new(id: impl Into<String>, resource_type: ResourceType) -> Self {
Self {
id: id.into(),
name: String::new(),
resource_type,
capacity: 1,
efficiency: 1.0,
calendar: None,
skills: Vec::new(),
cost_per_hour: None,
attributes: HashMap::new(),
}
}
pub fn primary(id: impl Into<String>) -> Self {
Self::new(id, ResourceType::Primary)
}
pub fn human(id: impl Into<String>) -> Self {
Self::new(id, ResourceType::Human)
}
pub fn secondary(id: impl Into<String>) -> Self {
Self::new(id, ResourceType::Secondary)
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
pub fn with_capacity(mut self, capacity: i32) -> Self {
self.capacity = capacity;
self
}
pub fn with_efficiency(mut self, efficiency: f64) -> Self {
self.efficiency = efficiency;
self
}
pub fn with_calendar(mut self, calendar: Calendar) -> Self {
self.calendar = Some(calendar);
self
}
pub fn with_skill(mut self, name: impl Into<String>, level: f64) -> Self {
self.skills.push(Skill {
name: name.into(),
level: level.clamp(0.0, 1.0),
});
self
}
pub fn with_cost(mut self, cost_per_hour: f64) -> Self {
self.cost_per_hour = Some(cost_per_hour);
self
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.attributes.insert(key.into(), value.into());
self
}
pub fn has_skill(&self, name: &str) -> bool {
self.skills.iter().any(|s| s.name == name)
}
pub fn skill_level(&self, name: &str) -> f64 {
self.skills
.iter()
.find(|s| s.name == name)
.map(|s| s.level)
.unwrap_or(0.0)
}
pub fn is_available_at(&self, time_ms: i64) -> bool {
match &self.calendar {
None => true,
Some(cal) => cal.is_working_time(time_ms),
}
}
}
impl Skill {
pub fn new(name: impl Into<String>, level: f64) -> Self {
Self {
name: name.into(),
level: level.clamp(0.0, 1.0),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resource_builder() {
let r = Resource::primary("M1")
.with_name("CNC Machine 1")
.with_capacity(1)
.with_efficiency(1.2)
.with_skill("milling", 0.9)
.with_skill("drilling", 0.7)
.with_cost(50.0)
.with_attribute("location", "Shop Floor A");
assert_eq!(r.id, "M1");
assert_eq!(r.name, "CNC Machine 1");
assert_eq!(r.resource_type, ResourceType::Primary);
assert_eq!(r.capacity, 1);
assert!((r.efficiency - 1.2).abs() < 1e-10);
assert!(r.has_skill("milling"));
assert!(!r.has_skill("welding"));
assert!((r.skill_level("milling") - 0.9).abs() < 1e-10);
assert!((r.skill_level("unknown") - 0.0).abs() < 1e-10);
assert_eq!(r.cost_per_hour, Some(50.0));
}
#[test]
fn test_resource_types() {
let m = Resource::primary("M1");
assert_eq!(m.resource_type, ResourceType::Primary);
let w = Resource::human("W1");
assert_eq!(w.resource_type, ResourceType::Human);
let t = Resource::secondary("T1");
assert_eq!(t.resource_type, ResourceType::Secondary);
}
#[test]
fn test_resource_availability_no_calendar() {
let r = Resource::primary("M1");
assert!(r.is_available_at(0));
assert!(r.is_available_at(1_000_000));
}
#[test]
fn test_skill_clamping() {
let r = Resource::primary("M1")
.with_skill("over", 1.5)
.with_skill("under", -0.5);
assert!((r.skill_level("over") - 1.0).abs() < 1e-10);
assert!((r.skill_level("under") - 0.0).abs() < 1e-10);
}
}