1#![warn(clippy::pedantic, clippy::nursery, clippy::cargo, missing_docs)]
4#![allow(clippy::multiple_crate_versions)]
5
6use std::{
7 fs::File,
8 io::{self, Read},
9 path::{Path, PathBuf},
10};
11
12use chrono::{Datelike, NaiveDate, NaiveDateTime};
13use school::{bells::BellTime, Day, School};
14use serde::{Deserialize, Serialize};
15use serde_json::from_str;
16use subjects::Subject;
17pub mod color;
19pub mod school;
21pub mod subjects;
23
24use thiserror::Error;
25use uuid::Uuid;
26
27#[derive(Error, Debug)]
29pub enum LoadDataError {
30 #[error("Couldn't find Subjective data file, which was expected at {0:?}.")]
32 DataFileNotFound(PathBuf, io::Error),
33 #[error("Failed to read Subjective data file.")]
35 DataFileReadError(io::Error),
36 #[error("Failed to parse Subjective data file. This may be due to invalid or outdated data. Try re-exporting your data again.")]
38 DataFileParseError(serde_json::Error),
39}
40
41#[derive(Error, Debug)]
43pub enum FindBellError {
44 #[error("The ISO 8601 weekday number {0} is out of the range `1..=5`.")]
46 WeekdayOutOfRange(usize),
47 #[error("No bell was found.")]
49 NoBellFound,
50}
51
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
53pub struct Subjective {
55 pub school: School,
57 pub subjects: Vec<Subject>,
59}
60
61impl Subjective {
62 pub fn from_config(config_directory: &Path) -> Result<Self, LoadDataError> {
67 let timetable_path = config_directory.join(".subjective");
68 let mut timetable = File::open(timetable_path.clone())
69 .map_err(|error| LoadDataError::DataFileNotFound(timetable_path, error))?;
70 let mut raw = String::new();
71 timetable
72 .read_to_string(&mut raw)
73 .map_err(LoadDataError::DataFileReadError)?;
74 let data = from_str(&raw).map_err(LoadDataError::DataFileParseError)?;
75 Ok(data)
76 }
77
78 #[must_use]
79 pub fn new(school: School, subjects: Vec<Subject>) -> Self {
81 Self { school, subjects }
82 }
83
84 #[must_use]
85 pub fn from_school(school: School) -> Self {
87 Self {
88 school,
89 subjects: Vec::new(),
90 }
91 }
92
93 pub fn find_all_after(
102 &self,
103 date_time: NaiveDateTime,
104 variant_offset: usize,
105 ) -> Result<Vec<&BellTime>, FindBellError> {
106 let day = self.get_day(date_time.date(), variant_offset)?;
107 let time = date_time.time();
108 let bells: Vec<&BellTime> = day
109 .iter()
110 .filter(|bell| bell.time >= time && bell.enabled)
111 .collect();
112 if bells.is_empty() {
113 return Err(FindBellError::NoBellFound);
114 }
115 Ok(bells)
116 }
117
118 pub fn find_all_before(
130 &self,
131 date_time: NaiveDateTime,
132 variant_offset: usize,
133 ) -> Result<Vec<&BellTime>, FindBellError> {
134 let day = self.get_day(date_time.date(), variant_offset)?;
135 let time = date_time.time();
136 let bells: Vec<&BellTime> = day
137 .iter()
138 .rev()
139 .filter(|bell| bell.time <= time && bell.enabled)
140 .collect();
141 if bells.is_empty() {
142 return Err(FindBellError::NoBellFound);
143 }
144 Ok(bells)
145 }
146
147 pub fn find_first_after(
158 &self,
159 date_time: NaiveDateTime,
160 variant_offset: usize,
161 ) -> Result<&BellTime, FindBellError> {
162 let day = self.get_day(date_time.date(), variant_offset)?;
163 let time = date_time.time();
164 day.iter()
165 .find(|bell| bell.time >= time && bell.enabled)
166 .ok_or(FindBellError::NoBellFound)
167 }
168
169 pub fn find_first_before(
180 &self,
181 date_time: NaiveDateTime,
182 variant_offset: usize,
183 ) -> Result<&BellTime, FindBellError> {
184 let day = self.get_day(date_time.date(), variant_offset)?;
185 let time = date_time.time();
186 day.iter()
187 .rev()
188 .find(|bell| bell.time <= time && bell.enabled)
189 .ok_or(FindBellError::NoBellFound)
190 }
191
192 #[allow(clippy::cast_sign_loss)]
199 pub fn get_day(&self, date: NaiveDate, variant_offset: usize) -> Result<&Day, FindBellError> {
200 let weekday = date.weekday().number_from_monday() as usize;
201 let current_variant =
202 get_current_variant(date, variant_offset, self.school.bell_times.len());
203 let bell_times = &self.school.bell_times[current_variant].1;
204 let day = bell_times
205 .get(weekday)
206 .ok_or(FindBellError::WeekdayOutOfRange(weekday))?;
207 Ok(day)
208 }
209
210 #[must_use]
211 pub fn get_subject(&self, subject_id: Uuid) -> Option<&Subject> {
216 self.subjects
217 .iter()
218 .find(|subject| subject.id == subject_id)
219 }
220}
221
222#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
224#[must_use]
225pub fn get_current_variant(date: NaiveDate, variant_offset: usize, variants: usize) -> usize {
226 let weeks = variants;
227 let week_number = date.iso_week().week() as usize;
228 (week_number + variant_offset) % weeks
229}