use super::Dimension;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Scaling {
Linear,
Logarithmic,
Fixed,
}
#[derive(Debug, Clone, Copy)]
pub struct ScoreImpact {
pub base_penalty: f32,
pub max_penalty: f32,
pub occurrence_scaling: Scaling,
}
pub const SATURATION_THRESHOLD: usize = 10;
const PHASE2_RATE: f32 = 0.05;
impl ScoreImpact {
pub fn calculate_penalty(&self, occurrences: usize) -> f32 {
if occurrences == 0 {
return 0.0;
}
match self.occurrence_scaling {
Scaling::Fixed => self.base_penalty.min(self.max_penalty),
Scaling::Linear => (occurrences as f32 * self.base_penalty).min(self.max_penalty),
Scaling::Logarithmic => {
let phase1_count = occurrences.min(SATURATION_THRESHOLD);
let phase2_count = occurrences.saturating_sub(SATURATION_THRESHOLD);
let phase1 =
(self.base_penalty * (1.0 + (phase1_count as f32).ln())).min(self.max_penalty);
let phase2 = phase2_count as f32 * self.base_penalty * PHASE2_RATE;
(phase1 + phase2).min(self.max_penalty * 2.0)
}
}
}
}
pub const MODULE_WEIGHTS: &[(Dimension, f32)] = &[
(Dimension::Accessibility, 35.0),
(Dimension::Performance, 20.0),
(Dimension::Seo, 20.0),
(Dimension::Security, 15.0),
(Dimension::Mobile, 10.0),
];
pub fn weight_for(dimension: Dimension) -> f32 {
MODULE_WEIGHTS
.iter()
.find(|(d, _)| *d == dimension)
.map(|(_, w)| *w)
.unwrap_or(0.0)
}
pub fn score_label(score: u32) -> &'static str {
match score {
90..=100 => "Sehr gut",
75..=89 => "Gut",
60..=74 => "Verbesserungswürdig",
40..=59 => "Ausbaufähig",
_ => "Kritisch",
}
}
pub fn score_grade(score: u32) -> &'static str {
match score {
90..=100 => "A",
80..=89 => "B",
70..=79 => "C",
60..=69 => "D",
_ => "F",
}
}
pub fn score_status(score: u32) -> &'static str {
match score {
80..=100 => "good",
60..=79 => "warning",
_ => "bad",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fixed_penalty() {
let impact = ScoreImpact {
base_penalty: 10.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Fixed,
};
assert_eq!(impact.calculate_penalty(1), 10.0);
assert_eq!(impact.calculate_penalty(5), 10.0);
}
#[test]
fn test_linear_penalty_capped() {
let impact = ScoreImpact {
base_penalty: 3.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Linear,
};
assert_eq!(impact.calculate_penalty(1), 3.0);
assert_eq!(impact.calculate_penalty(3), 9.0);
assert_eq!(impact.calculate_penalty(5), 10.0); }
#[test]
fn test_logarithmic_penalty() {
let impact = ScoreImpact {
base_penalty: 3.0,
max_penalty: 15.0,
occurrence_scaling: Scaling::Logarithmic,
};
let p1 = impact.calculate_penalty(1);
let p3 = impact.calculate_penalty(3);
let p10 = impact.calculate_penalty(10);
assert!(p1 < p3);
assert!(p3 < p10);
assert!(p10 <= 15.0);
}
#[test]
fn test_depth_saturation_phase2() {
let impact = ScoreImpact {
base_penalty: 3.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Logarithmic,
};
let p10 = impact.calculate_penalty(10);
let p50 = impact.calculate_penalty(50);
let p200 = impact.calculate_penalty(200);
assert!(p50 > p10, "p50={} should exceed p10={}", p50, p10);
assert!(p200 > p50, "p200={} should exceed p50={}", p200, p50);
assert!(p200 <= 20.0, "p200={} should not exceed 2×max", p200);
assert!((impact.calculate_penalty(2) - 3.0 * (1.0 + 2.0_f32.ln())).abs() < 0.01);
}
#[test]
fn test_score_labels() {
assert_eq!(score_label(95), "Sehr gut");
assert_eq!(score_label(80), "Gut");
assert_eq!(score_label(65), "Verbesserungswürdig");
assert_eq!(score_label(45), "Ausbaufähig");
assert_eq!(score_label(20), "Kritisch");
}
#[test]
fn test_module_weights_sum() {
let total: f32 = MODULE_WEIGHTS.iter().map(|(_, w)| w).sum();
assert_eq!(total, 100.0);
}
}