1use std::fmt;
7use std::str::FromStr;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
14pub enum Letter {
15 F,
16 E,
17 D,
18 C,
19 B,
20 A,
21 #[serde(rename = "A+")]
22 APlus,
23}
24
25impl Letter {
26 pub fn from_score(score: u32) -> Self {
28 match score {
29 95..=u32::MAX => Letter::APlus,
30 85..=94 => Letter::A,
31 75..=84 => Letter::B,
32 65..=74 => Letter::C,
33 50..=64 => Letter::D,
34 30..=49 => Letter::E,
35 _ => Letter::F,
36 }
37 }
38
39 pub fn as_str(self) -> &'static str {
41 match self {
42 Letter::APlus => "A+",
43 Letter::A => "A",
44 Letter::B => "B",
45 Letter::C => "C",
46 Letter::D => "D",
47 Letter::E => "E",
48 Letter::F => "F",
49 }
50 }
51}
52
53impl fmt::Display for Letter {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 f.write_str(self.as_str())
56 }
57}
58
59impl FromStr for Letter {
60 type Err = String;
61
62 fn from_str(s: &str) -> Result<Self, Self::Err> {
63 match s.trim().to_ascii_uppercase().as_str() {
64 "A+" => Ok(Letter::APlus),
65 "A" => Ok(Letter::A),
66 "B" => Ok(Letter::B),
67 "C" => Ok(Letter::C),
68 "D" => Ok(Letter::D),
69 "E" => Ok(Letter::E),
70 "F" => Ok(Letter::F),
71 other => Err(format!(
72 "invalid grade '{other}' (expected A+, A, B, C, D, E or F)"
73 )),
74 }
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn score_bands_match_the_header_audit_scale() {
84 for (score, letter) in [
85 (100, Letter::APlus),
86 (95, Letter::APlus),
87 (94, Letter::A),
88 (85, Letter::A),
89 (84, Letter::B),
90 (75, Letter::B),
91 (74, Letter::C),
92 (65, Letter::C),
93 (64, Letter::D),
94 (50, Letter::D),
95 (49, Letter::E),
96 (30, Letter::E),
97 (29, Letter::F),
98 (0, Letter::F),
99 ] {
100 assert_eq!(Letter::from_score(score), letter, "score {score}");
101 }
102 }
103
104 #[test]
105 fn letters_order_worst_to_best() {
106 assert!(Letter::F < Letter::E);
107 assert!(Letter::C < Letter::B);
108 assert!(Letter::A < Letter::APlus);
109 assert_eq!(Letter::APlus.min(Letter::C), Letter::C);
110 }
111
112 #[test]
113 fn parses_and_prints_round_trip() {
114 for letter in [
115 Letter::F,
116 Letter::E,
117 Letter::D,
118 Letter::C,
119 Letter::B,
120 Letter::A,
121 Letter::APlus,
122 ] {
123 assert_eq!(letter.as_str().parse::<Letter>(), Ok(letter));
124 assert_eq!(letter.to_string(), letter.as_str());
125 }
126 assert_eq!(" a+ ".parse::<Letter>(), Ok(Letter::APlus));
127 assert!("Z".parse::<Letter>().is_err());
128 }
129
130 #[test]
131 fn serializes_as_the_printed_letter() {
132 assert_eq!(serde_json::to_string(&Letter::APlus).unwrap(), "\"A+\"");
133 assert_eq!(serde_json::to_string(&Letter::C).unwrap(), "\"C\"");
134 }
135}