exms/exam/
mod.rs

1mod parse;
2mod plot;
3mod statistics;
4mod student;
5
6use std::cmp::Ordering;
7use std::path::Path;
8
9use unidecode::unidecode;
10
11use crate::error::ParseError;
12use parse::parse_exam_file;
13use statistics::ExamStatistics;
14pub use student::Student;
15
16/// This type represents and exam.
17#[derive(Debug, Clone)]
18pub struct Exam {
19    title: Option<String>,
20    max_grade: f32,
21    students: Vec<Student>,
22    statistics: ExamStatistics,
23}
24
25impl Exam {
26    /// Creates a new `Exam` from a given set of students.
27    ///
28    /// # Examples
29    ///
30    /// ```
31    /// use exms::exam::Exam;
32    /// use exms::exam::Student;
33    ///
34    /// let students = &[
35    ///     Student::new("Joan Beltrán Peris", 4.6),
36    ///     Student::new("Jose Abad Martínez", 3.6),
37    ///     Student::new("David Jiménez Hidalgo", 7.94),
38    /// ];
39    ///
40    /// let exam = Exam::new(students);
41    /// ```
42    pub fn new(students: impl Into<Vec<Student>>) -> Self {
43        let mut students = students.into();
44        let statistics = ExamStatistics::new(&mut students, 10.0);
45
46        Self {
47            title: None,
48            max_grade: 10.0,
49            students,
50            statistics,
51        }
52    }
53
54    /// Creates a new `Exam` from a given file.
55    /// The file formats suppported for the moment are only JSON and TOML files.
56    /// The file should have a student object with all the students and their
57    /// grades as key/value pairs. For more information about wich format
58    /// a file should follow, please see [exms](crate).
59    ///
60    /// # Examples
61    ///
62    /// ```no_run
63    /// use std::path::Path;
64    ///
65    /// use exms::error::ParseError;
66    /// use exms::exam::Exam;
67    ///
68    /// fn main() -> Result<(), ParseError> {
69    ///     let file_path = Path::new("students.json");
70    ///     let exam = Exam::from_file(&file_path)?;
71    ///
72    ///     Ok(())
73    /// }
74    /// ```
75    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ParseError> {
76        parse_exam_file(path.as_ref())
77    }
78
79    /// Sets the maximum achievable grade in the exam.
80    ///
81    /// # Examples
82    ///
83    /// ```
84    /// use exms::exam::Exam;
85    /// use exms::exam::Student;
86    ///
87    /// let students = &[
88    ///     Student::new("Joan Beltrán Peris", 4.6),
89    ///     Student::new("Jose Abad Martínez", 3.6),
90    ///     Student::new("David Jiménez Hidalgo", 7.94),
91    /// ];
92    ///
93    /// let mut exam = Exam::new(students);
94    /// exam.set_max_grade(6.0);
95    /// ```
96    pub fn set_max_grade(&mut self, max_grade: f32) {
97        self.max_grade = max_grade;
98        self.statistics = ExamStatistics::new(&mut self.students, max_grade);
99    }
100
101    /// Sets the title of the exam.
102    ///
103    /// # Examples
104    ///
105    /// ```
106    /// use exms::exam::Exam;
107    /// use exms::exam::Student;
108    ///
109    /// let students = &[
110    ///     Student::new("Joan Beltrán Peris", 4.6),
111    ///     Student::new("Jose Abad Martínez", 3.6),
112    ///     Student::new("David Jiménez Hidalgo", 7.94),
113    /// ];
114    ///
115    /// let mut exam = Exam::new(students);
116    /// exam.set_title("Econometrics");
117    /// ```
118    pub fn set_title(&mut self, title: impl Into<String>) {
119        self.title = Some(title.into())
120    }
121
122    /// Sorts the exam students based on their grade in descending order.
123    ///
124    /// # Examples
125    ///
126    /// ```
127    /// use exms::exam::Exam;
128    /// use exms::exam::Student;
129    ///
130    /// let students = &[
131    ///     Student::new("Joan Beltrán Peris", 4.6),
132    ///     Student::new("Jose Abad Martínez", 3.6),
133    ///     Student::new("David Jiménez Hidalgo", 7.94),
134    /// ];
135    ///
136    /// let mut exam = Exam::new(students);
137    /// exam.sort_by_grade();
138    ///
139    /// assert_eq!(exam.students[0].grade, 7.94);
140    /// assert_eq!(exam.students[1].grade, 4.6);
141    /// assert_eq!(exam.students[2].grade, 3.6);
142    /// ```
143    pub fn sort_by_grade(&mut self) {
144        // Sort students by name so that students with the same grade are sorted
145        // alphabetically
146        Self::sort_by_alphabetic_order(self);
147
148        self.students
149            .sort_by(|a, b| b.grade.partial_cmp(&a.grade).unwrap_or(Ordering::Equal))
150    }
151
152    /// Sorts the exam students based on their name alphabetically.
153    ///
154    /// # Examples
155    ///
156    /// ```
157    /// use exms::exam::Exam;
158    /// use exms::exam::Student;
159    ///
160    /// let students = &[
161    ///     Student::new("Joan Beltrán Peris", 4.6),
162    ///     Student::new("Jose Abad Martínez", 3.6),
163    ///     Student::new("David Jiménez Hidalgo", 7.94),
164    /// ];
165    ///
166    /// let mut exam = Exam::new(students);
167    /// exam.sort_by_alphabetic_order();
168    ///
169    /// assert_eq!(exam.students[0].name, "David Jiménez Hidalgo");
170    /// assert_eq!(exam.students[1].name, "Joan Beltrán Peris");
171    /// assert_eq!(exam.students[2].name, "Jose Abad Martínez");
172    /// ```
173    pub fn sort_by_alphabetic_order(&mut self) {
174        self.students
175            .sort_by_key(|s| unidecode(&s.name.to_lowercase()))
176    }
177
178    /// Filters the exam students yielding only the students which name contains
179    /// the given query
180    ///
181    /// # Examples
182    ///
183    /// ```
184    /// use exms::exam::Exam;
185    /// use exms::exam::Student;
186    ///
187    /// let students = &[
188    ///     Student::new("Joan Beltrán Peris", 4.6),
189    ///     Student::new("Jose Abad Martínez", 3.6),
190    ///     Student::new("David Jiménez Hidalgo", 7.94),
191    /// ];
192    ///
193    /// let mut exam = Exam::new(students);
194    /// exam.filter_by_name(&["joan", "jorge", "jim"]);
195    ///
196    /// assert_eq!(exam.students.len(), 2);
197    /// assert_eq!(exam.students[0].name, "Joan Beltrán Peris");
198    /// assert_eq!(exam.students[1].name, "David Jiménez Hidalgo");
199    /// ```
200    pub fn filter_by_name<S: AsRef<str>>(&mut self, query: &[S]) {
201        self.students.retain(|student| {
202            query.iter().any(|name| {
203                student
204                    .name
205                    .to_lowercase()
206                    .contains(&name.as_ref().to_lowercase())
207            })
208        });
209    }
210
211    /// Filters the exam students yielding only the students that are in the
212    /// given file. The file format should be the same as the one used in
213    /// [from_file](Exam::from_file).
214    ///
215    /// # Examples
216    ///
217    /// ```no_run
218    /// use std::path::Path;
219    ///
220    /// use exms::error::ParseError;
221    /// use exms::exam::Exam;
222    ///
223    /// fn main() -> Result<(), ParseError> {
224    ///     let file_path = Path::new("students.json");
225    ///     let mut exam = Exam::from_file(&file_path)?;
226    ///
227    ///     let file_path = Path::new("students2.json");
228    ///     exam.filter_by_file(&[file_path])?;
229    ///
230    ///     Ok(())
231    /// }
232    /// ```
233    pub fn filter_by_file<P: AsRef<Path>>(&mut self, file_paths: &[P]) -> Result<(), ParseError> {
234        for path in file_paths {
235            let exam = parse_exam_file(path.as_ref())?;
236            let students = exam.students;
237
238            self.students.retain(|student| {
239                students
240                    .iter()
241                    .any(|s| s.name.to_lowercase() == student.name.to_lowercase())
242            });
243        }
244
245        Ok(())
246    }
247
248    /// Print the exam students in a well formatted table with some statistical
249    /// information about each student, like the percentile, the rank, etc...
250    ///
251    /// # Examples
252    ///
253    /// ```
254    /// use exms::exam::Exam;
255    /// use exms::exam::Student;
256    ///
257    /// let students = &[
258    ///     Student::new("Joan Beltrán Peris", 4.6),
259    ///     Student::new("Jose Abad Martínez", 3.6),
260    ///     Student::new("David Jiménez Hidalgo", 7.94),
261    /// ];
262    ///
263    /// let mut exam = Exam::new(students);
264    /// exam.students();
265    /// ```
266    pub fn students(&self) {
267        self.statistics.students(&self.students)
268    }
269
270    /// Print statistical information about the exam in a well formatted table,
271    /// like the mean, total students, the exam pass percentage etc...
272    ///
273    /// # Examples
274    ///
275    /// ```
276    /// use exms::exam::Exam;
277    /// use exms::exam::Student;
278    ///
279    /// let students = &[
280    ///     Student::new("Joan Beltrán Peris", 4.6),
281    ///     Student::new("Jose Abad Martínez", 3.6),
282    ///     Student::new("David Jiménez Hidalgo", 7.94),
283    /// ];
284    ///
285    /// let mut exam = Exam::new(students);
286    /// exam.summary();
287    /// ```
288    pub fn summary(&self) {
289        self.statistics.summary(&self.title)
290    }
291
292    /// Print a histogram of the exam grades.
293    ///
294    /// # Examples
295    ///
296    /// ```
297    /// use exms::exam::Exam;
298    /// use exms::exam::Student;
299    ///
300    /// let students = &[
301    ///     Student::new("Joan Beltrán Peris", 4.6),
302    ///     Student::new("Jose Abad Martínez", 3.6),
303    ///     Student::new("David Jiménez Hidalgo", 7.94),
304    /// ];
305    ///
306    /// let mut exam = Exam::new(students);
307    /// exam.histogram();
308    /// ```
309    pub fn histogram(&self, step: Option<f64>) {
310        plot::histogram(&self.students, self.max_grade, step)
311    }
312}