use serde::{Deserialize, Serialize};
use super::parameters::RepairabilityInputs;
use super::thresholds::RepairabilityRuleset;
use crate::clock::AssessmentClock;
use crate::error::CalcError;
use crate::receipt::{CalculationReceipt, jcs_hash};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RepairabilityClass {
A,
B,
C,
D,
E,
}
impl RepairabilityClass {
pub fn as_ordinal(self) -> u8 {
match self {
Self::A => 5,
Self::B => 4,
Self::C => 3,
Self::D => 2,
Self::E => 1,
}
}
}
impl std::fmt::Display for RepairabilityClass {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::A => "A",
Self::B => "B",
Self::C => "C",
Self::D => "D",
Self::E => "E",
}
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParameterContributions {
pub disassembly: f64,
pub spare_parts: f64,
pub repair_info: f64,
pub diagnostic_tools: f64,
pub software_updatability: f64,
pub customer_support: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepairabilityResult {
pub class: RepairabilityClass,
pub numeric_score: f64,
pub contributions: ParameterContributions,
pub receipt: CalculationReceipt,
}
pub fn calculate(
inputs: &RepairabilityInputs,
ruleset: &dyn RepairabilityRuleset,
clock: AssessmentClock,
) -> Result<RepairabilityResult, CalcError> {
validate_inputs(inputs)?;
ruleset.validate_cross_fields(inputs)?;
ruleset
.effectivity()
.ensure_active_on(ruleset.id(), clock.law_in_force_on)?;
let w = ruleset.weights();
let scale = 5.0;
let contributions = ParameterContributions {
disassembly: f64::from(inputs.disassembly) * w.disassembly * scale,
spare_parts: f64::from(inputs.spare_parts) * w.spare_parts * scale,
repair_info: f64::from(inputs.repair_info) * w.repair_info * scale,
diagnostic_tools: f64::from(inputs.diagnostic_tools) * w.diagnostic_tools * scale,
software_updatability: f64::from(inputs.software_updatability)
* w.software_updatability
* scale,
customer_support: f64::from(inputs.customer_support) * w.customer_support * scale,
};
let numeric_score = contributions.disassembly
+ contributions.spare_parts
+ contributions.repair_info
+ contributions.diagnostic_tools
+ contributions.software_updatability
+ contributions.customer_support;
let t = ruleset.thresholds();
let class = if numeric_score >= t.a {
RepairabilityClass::A
} else if numeric_score >= t.b {
RepairabilityClass::B
} else if numeric_score >= t.c {
RepairabilityClass::C
} else if numeric_score >= t.d {
RepairabilityClass::D
} else {
RepairabilityClass::E
};
let output_hash = jcs_hash(&(numeric_score, class.as_ordinal()))?;
let receipt = CalculationReceipt::for_ruleset(inputs, ruleset, clock, output_hash)?;
Ok(RepairabilityResult {
class,
numeric_score,
contributions,
receipt,
})
}
fn validate_inputs(inputs: &RepairabilityInputs) -> Result<(), CalcError> {
for (name, val) in [
("disassembly", inputs.disassembly),
("spare_parts", inputs.spare_parts),
("repair_info", inputs.repair_info),
("diagnostic_tools", inputs.diagnostic_tools),
("software_updatability", inputs.software_updatability),
("customer_support", inputs.customer_support),
] {
if val > 2 {
return Err(CalcError::InvalidInput(format!(
"{name} must be 0, 1, or 2; got {val}"
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::repairability::thresholds::SimplifiedRepairabilityHeuristic;
use chrono::NaiveDate;
fn in_force() -> AssessmentClock {
AssessmentClock::placed_on(NaiveDate::from_ymd_opt(2026, 1, 1).expect("valid date"))
}
#[test]
fn class_display_and_ordinal_for_all_grades() {
for (class, letter, ordinal) in [
(RepairabilityClass::A, "A", 5),
(RepairabilityClass::B, "B", 4),
(RepairabilityClass::C, "C", 3),
(RepairabilityClass::D, "D", 2),
(RepairabilityClass::E, "E", 1),
] {
assert_eq!(class.to_string(), letter);
assert_eq!(class.as_ordinal(), ordinal);
}
}
#[test]
fn out_of_range_parameter_is_rejected() {
let inputs = RepairabilityInputs {
disassembly: 3, spare_parts: 1,
repair_info: 1,
diagnostic_tools: 1,
software_updatability: 1,
customer_support: 1,
};
assert!(matches!(
calculate(&inputs, &SimplifiedRepairabilityHeuristic, in_force()),
Err(CalcError::InvalidInput(_))
));
}
}