use colored::{ColoredString, Colorize};
pub fn score_to_gpa(score: &str) -> Option<f64> {
let s = score.trim();
if let Ok(num) = s.parse::<f64>() {
return Some(numeric_to_gpa(num));
}
match s {
"A+" | "A" => Some(4.0),
"A-" => Some(3.7),
"B+" => Some(3.3),
"B" => Some(3.0),
"B-" => Some(2.7),
"C+" => Some(2.3),
"C" => Some(2.0),
"C-" => Some(1.5),
"D+" | "D" | "D-" => Some(1.0),
"F" => Some(0.0),
_ => None, }
}
fn numeric_to_gpa(score: f64) -> f64 {
if score >= 90.0 {
4.0
} else if score >= 85.0 {
3.7
} else if score >= 82.0 {
3.3
} else if score >= 78.0 {
3.0
} else if score >= 75.0 {
2.7
} else if score >= 72.0 {
2.3
} else if score >= 68.0 {
2.0
} else if score >= 64.0 {
1.5
} else if score >= 60.0 {
1.0
} else {
0.0
}
}
pub fn is_fail(score: &str) -> bool {
let s = score.trim();
if let Ok(num) = s.parse::<f64>() {
return num < 60.0;
}
matches!(s, "F" | "不合格" | "NP")
}
fn hsl_to_rgb(h: f64, s: f64, l: f64) -> (u8, u8, u8) {
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let h_prime = h / 60.0;
let x = c * (1.0 - (h_prime % 2.0 - 1.0).abs());
let (r1, g1, b1) = if h_prime < 1.0 {
(c, x, 0.0)
} else if h_prime < 2.0 {
(x, c, 0.0)
} else if h_prime < 3.0 {
(0.0, c, x)
} else if h_prime < 4.0 {
(0.0, x, c)
} else if h_prime < 5.0 {
(x, 0.0, c)
} else {
(c, 0.0, x)
};
let m = l - c / 2.0;
(
((r1 + m) * 255.0).round() as u8,
((g1 + m) * 255.0).round() as u8,
((b1 + m) * 255.0).round() as u8,
)
}
pub fn colorize_score(score: &str) -> ColoredString {
let s = score.trim();
if is_fail(s) {
return s.red().bold();
}
match score_to_gpa(s) {
Some(gpa) => {
let prec = ((gpa - 1.0) / 3.0).clamp(0.0, 1.0);
let hue = 120.0 * prec; let (r, g, b) = hsl_to_rgb(hue, 0.8, 0.45);
s.truecolor(r, g, b).bold()
}
None => {
if s == "合格" || s == "P" {
s.green()
} else if s == "W" {
s.dimmed()
} else {
s.normal()
}
}
}
}
pub fn colorize_gpa(gpa_str: &str) -> ColoredString {
if let Ok(gpa) = gpa_str.parse::<f64>() {
let prec = ((gpa - 1.0) / 3.0).clamp(0.0, 1.0);
let hue = 120.0 * prec;
let (r, g, b) = hsl_to_rgb(hue, 0.8, 0.45);
gpa_str.truecolor(r, g, b).bold()
} else {
gpa_str.normal()
}
}
pub fn score_bar(score: &str, width: usize) -> String {
let s = score.trim();
if is_fail(s) {
return "░".repeat(width).red().to_string();
}
match score_to_gpa(s) {
Some(gpa) => {
let prec = ((gpa - 1.0) / 3.0).clamp(0.0, 1.0);
let filled = (prec * width as f64).round() as usize;
let hue = 120.0 * prec;
let (r, g, b) = hsl_to_rgb(hue, 0.8, 0.45);
let bar_filled = "█".repeat(filled).truecolor(r, g, b);
let bar_empty = "░".repeat(width.saturating_sub(filled)).dimmed();
format!("{bar_filled}{bar_empty}")
}
None => "░".repeat(width).dimmed().to_string(),
}
}