cal7tor/src/timetable.rs

369 lines
13 KiB
Rust
Raw Normal View History

#![allow(clippy::cast_sign_loss)]
use chrono::{Datelike, Duration, TimeZone, Utc};
use regex::Regex;
use scraper::Selector;
2024-01-01 14:14:13 +01:00
use std::{collections::HashMap, sync::Arc};
2022-08-23 16:51:02 +02:00
use crate::utils::{
2024-01-01 14:14:13 +01:00
self, get_hours, get_semester, get_webpage, get_year,
models::{Info, InfoList, Position, TabChar},
2023-09-28 22:09:43 +02:00
Capitalize,
2022-08-23 16:51:02 +02:00
};
use self::models::Day;
2022-08-15 19:20:04 +02:00
pub mod models;
2022-08-15 12:18:08 +02:00
2022-08-15 17:25:14 +02:00
/// Fetch the timetable for a class
2022-08-15 20:09:41 +02:00
pub async fn timetable(
level: i8,
2022-08-17 14:09:08 +02:00
semester_opt: Option<i8>,
year_opt: Option<i32>,
2022-08-29 11:44:35 +02:00
user_agent: &str,
2024-01-01 14:14:13 +01:00
) -> models::Timetable {
let semester = get_semester(semester_opt);
2022-08-17 14:09:08 +02:00
let year = get_year(year_opt, semester);
let document = get_webpage(level, semester, &year, user_agent)
2022-08-15 14:52:57 +02:00
.await
.expect("Can't reach timetable website.");
2022-08-15 12:18:08 +02:00
// Selectors
2023-09-18 20:52:18 +02:00
let sel_table = Selector::parse("table").unwrap();
let sel_tbody = Selector::parse("tbody").unwrap();
let sel_td = Selector::parse("td").unwrap();
let sel_small = Selector::parse("small").unwrap();
let sel_b = Selector::parse("b").unwrap();
2024-01-30 14:09:17 +01:00
let sel_span = Selector::parse("span").unwrap();
2022-08-15 12:18:08 +02:00
// Find the timetable
2022-08-15 12:23:01 +02:00
let raw_timetable = document.select(&sel_table).next().unwrap();
2022-08-15 12:18:08 +02:00
2024-01-01 14:14:13 +01:00
let schedules = get_hours();
let mut timetable: Vec<models::Day> = Vec::new();
raw_timetable
.select(&sel_tbody)
2023-09-18 20:52:18 +02:00
.next()
.unwrap()
.select(&sel_td)
.filter(|element| element.value().attr("title").is_some())
.for_each(|i| {
2024-01-30 14:09:17 +01:00
let extra_data = i.select(&sel_span).next().map(|span|
span.inner_html().replace("<br>", "").trim().to_owned()
);
2024-08-25 13:47:42 +02:00
/* TODO: Instead of searching *_M2, just find any TD_* and TP_* */
let matches =
2024-09-13 19:19:37 +02:00
Regex::new(
r"(?P<type>COURS|COURS_TD|TD|TD_M2|TP|TP_M2)? (?P<name>.*) : (?P<day>(lundi|mardi|mercredi|jeudi|vendredi)) (?P<startime>.*) \(durée : (?P<duration>.*)\)")
.unwrap()
.captures(i.value().attr("title").unwrap())
.unwrap();
2023-09-26 23:55:54 +02:00
let day = matches
.name("day")
.unwrap()
2023-09-28 22:09:43 +02:00
.as_str()
.capitalize();
2023-09-26 23:55:54 +02:00
let startime = matches
2024-09-13 19:23:00 +02:00
.name("startime")
.unwrap()
.as_str();
let binding = i.select(&sel_b).last().unwrap().inner_html();
let course = models::Course{
category: match matches
.name("type")
2024-08-25 13:47:42 +02:00
.map_or("", |m| m.as_str()) {
2024-09-13 19:19:37 +02:00
/* TODO: Instead of searching *_M2, just find any TD_* and TP_* */
2024-01-01 13:47:23 +01:00
"COURS" => [models::Category::Cours].into(),
2024-08-25 13:47:42 +02:00
"TP" | "TP_M2" => [models::Category::TP].into(),
"TD" | "TD_M2" => [models::Category::TD].into(),
2024-01-01 13:47:23 +01:00
"COURS_TD" => [models::Category::Cours, models::Category::TD].into(),
2024-08-25 13:47:42 +02:00
_ => {
println!("Unknown type of course, falling back to 'COURS': {}", i.value().attr("title").unwrap());
[models::Category::Cours].into()
},
},
name: matches
2024-09-13 19:23:00 +02:00
.name("name")
.unwrap()
.as_str().to_owned(),
2023-09-28 00:23:51 +02:00
professor: if let Some(raw_prof) = i.select(&sel_small).last() {
2024-09-13 19:23:00 +02:00
match raw_prof.inner_html() {
i if i.starts_with("<span") => None,
i => Some(i),
}
} else { None },
2023-09-28 00:23:51 +02:00
room: Regex::new(r"(<table.*<\/table>|<br>.*?<br>.*?)?<br>(?P<location>.*?)<br>")
2024-09-13 19:23:00 +02:00
.unwrap()
.captures(&binding)
.unwrap().name("location")
.unwrap()
.as_str().to_owned(),
start: schedules.iter().position(|r| r.starts_with(startime)).unwrap(),
size: i.value().attr("rowspan").unwrap().parse::<usize>().unwrap(),
dtstart: None,
dtend: None,
2024-01-30 14:09:17 +01:00
data: extra_data,
};
// Search for the day in the timetable
if let Some(existing_day) = timetable.iter_mut().find(|x| x.name == day) {
existing_day.courses.push(Some(course));
} else {
// Day with the name doesn't exist, create a new Day
timetable.push(models::Day {
name: day.clone(),
courses: vec![Some(course)],
});
}
});
2024-01-01 12:58:57 +01:00
// Sort by days
let day_positions = ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi"]
.iter()
.enumerate()
.map(|(i, &day)| (day.to_owned(), i))
.collect::<HashMap<String, usize>>();
timetable.sort_by(|a, b| day_positions[&a.name].cmp(&day_positions[&b.name]));
(schedules, (semester as usize, timetable))
2022-08-15 12:18:08 +02:00
}
/// Build the timetable
pub fn build(timetable: &models::Timetable, dates: &Info) -> Vec<models::Course> {
let mut schedules = Vec::new();
// h1 => heure de début | m1 => minute de début
// h2 => heure de fin | m2 => minute de fin
let re = Regex::new(r"(?P<h1>\d{1,2})h(?P<m1>\d{2})-(?P<h2>\d{1,2})h(?P<m2>\d{2})").unwrap();
2024-01-01 14:14:13 +01:00
for hour in timetable.0.iter() {
let captures = re.captures(hour).unwrap();
let h1 = match captures.name("h1") {
Some(h) => h.as_str().parse().unwrap(),
None => 0,
};
let m1 = match captures.name("m1") {
Some(h) => h.as_str().parse().unwrap(),
None => 0,
};
let h2 = match captures.name("h2") {
Some(h) => h.as_str().parse().unwrap(),
None => 0,
};
let m2 = match captures.name("m2") {
Some(h) => h.as_str().parse().unwrap(),
None => 0,
};
schedules.push(((h1, m1), (h2, m2)));
}
2022-08-16 14:05:25 +02:00
// Store all the courses for the semester
let mut semester = Vec::new();
2022-08-16 14:05:25 +02:00
// Start date of the back-to-school week
2022-08-16 15:48:13 +02:00
let datetimes = dates.get(&timetable.1 .0).unwrap();
add_courses(
&mut semester,
&schedules,
&timetable.1 .1,
&datetimes.course,
&Some(vec![models::Category::Cours]),
&None,
);
add_courses(
&mut semester,
&schedules,
&timetable.1 .1,
&datetimes.td_tp,
&None,
&Some(vec![models::Category::Cours]),
);
semester
}
type Schedule = [((u32, u32), (u32, u32))];
/// Add a course to the semester list
fn add_courses(
// Accumulator of courses of semester
semester: &mut Vec<models::Course>,
// Hours used
schedules: &Schedule,
// List of days
days: &Vec<Day>,
// Current courses list
info: &InfoList,
// List of category allowed
keep: &Option<Vec<models::Category>>,
// List of category excluded
exclude: &Option<Vec<models::Category>>,
) {
let before_break = info.first().unwrap();
2022-08-16 14:50:22 +02:00
let mut date = before_break.0;
let mut rep = before_break.1;
2022-08-16 14:41:51 +02:00
// For each weeks
2022-08-16 14:50:22 +02:00
for _ in 0..2 {
for _ in 0..rep {
for day in days {
for mut course in day.courses.iter().flatten().cloned() {
2022-08-16 14:50:22 +02:00
// Get the hours
let start = schedules.get(course.start).unwrap().0;
// -1 because we only add when the size is > 1
let end = schedules.get(course.start + course.size - 1).unwrap().1;
// Check keep and exclude filters
if keep
.clone()
.is_some_and(|list| !course.category.iter().any(|item| list.contains(item)))
|| exclude.clone().is_some_and(|list| {
2024-01-22 22:16:11 +01:00
course.category.iter().any(|item| list.contains(item))
})
{
continue;
}
2022-08-16 14:50:22 +02:00
// Add the changed datetimes
course.dtstart = Some(
2023-01-10 12:01:45 +01:00
Utc.with_ymd_and_hms(
date.year(),
date.month(),
date.day(),
start.0,
start.1,
0,
)
.unwrap(),
2022-08-16 14:50:22 +02:00
);
course.dtend = Some(
2023-01-10 12:01:45 +01:00
Utc.with_ymd_and_hms(
date.year(),
date.month(),
date.day(),
end.0,
end.1,
0,
)
.unwrap(),
2022-08-16 14:50:22 +02:00
);
2022-08-16 14:50:22 +02:00
semester.push(course);
}
date += Duration::days(1);
2022-08-16 14:41:51 +02:00
}
2022-08-16 14:50:22 +02:00
// From friday to monday
date += Duration::days(2);
2022-08-16 14:05:25 +02:00
}
let after_break = info.last().unwrap();
2022-08-16 14:50:22 +02:00
date = after_break.0;
rep = after_break.1;
2022-08-16 14:05:25 +02:00
}
}
2022-08-17 14:09:08 +02:00
/// Display the timetable
pub fn display(timetable: &(Arc<[String]>, (usize, Vec<models::Day>)), cell_length: usize) {
2022-08-23 16:51:02 +02:00
// Cell length for hours
let clh = 11;
// Cell number
let cn = 6;
2022-08-23 18:30:01 +02:00
// 3/4 of cell length
2022-08-23 18:38:47 +02:00
let quarter = (3 * cell_length) / 4;
2022-08-23 16:51:02 +02:00
let sep = TabChar::Bv.val();
// Top of the tab
utils::line_table(clh, cell_length, cn, &Position::Top, &HashMap::new());
2022-08-23 16:51:02 +02:00
// First empty case
print!("{}{:^clh$}{}", sep, "", sep);
// Print day's of the week
let mut days = HashMap::new();
2023-01-10 11:53:19 +01:00
for (i, data) in timetable.1 .1.iter().enumerate() {
2022-08-23 16:51:02 +02:00
days.insert(i, &data.name);
2022-08-23 18:38:47 +02:00
print!("{:^cell_length$}{}", &data.name, sep);
2022-08-23 16:51:02 +02:00
}
// Store the data of the course for utils::line_table
let mut next_skip = HashMap::new();
2022-08-23 17:23:29 +02:00
// For each hours -- i the hour's number
2024-01-01 14:14:13 +01:00
for (i, hour) in timetable.0.iter().enumerate() {
2022-08-23 16:51:02 +02:00
// Draw separator line
utils::line_table(clh, cell_length, cn, &Position::Middle, &next_skip);
2022-08-23 16:51:02 +02:00
// Reset
next_skip = HashMap::new();
// Print hour
print!("{sep}{hour:^clh$}");
2022-08-23 16:51:02 +02:00
2022-08-23 17:23:29 +02:00
// For all the days - `j` the day's number
2023-01-10 11:53:19 +01:00
for (j, day) in timetable.1 .1.iter().enumerate() {
2022-08-23 16:58:22 +02:00
// True if we found something about the slot we are looking for
let mut info_slot = false;
2022-08-23 17:23:29 +02:00
// For all the courses of each days - `k` the possible course.start
2023-01-10 11:53:19 +01:00
for (k, course_opt) in day.courses.iter().enumerate() {
2022-08-23 16:51:02 +02:00
match course_opt {
// If there is a course
Some(course) => {
2022-08-23 16:58:22 +02:00
// Check the course's hour
2022-08-23 17:23:29 +02:00
if i == course.start {
2022-08-23 18:30:01 +02:00
// If the course uses more than one time slot
2022-08-23 16:58:22 +02:00
if course.size > 1 {
2022-08-23 18:30:01 +02:00
// If the data is too long
if course.name.len() > quarter {
let data = utils::split_half(&course.name);
2022-08-23 18:32:06 +02:00
next_skip.insert(j, data.1.trim());
2022-08-23 18:38:47 +02:00
print!("{}{:^cell_length$}", sep, data.0.trim());
2022-08-23 18:30:01 +02:00
} else {
next_skip.insert(j, &course.name);
2022-08-23 18:38:47 +02:00
print!("{}{:^cell_length$}", sep, "");
2022-08-23 18:30:01 +02:00
}
2022-08-23 16:58:22 +02:00
info_slot = true;
2022-08-23 16:51:02 +02:00
break;
}
// Else simply print the course
// If the data is too long
if course.name.len() > quarter {
print!("{}{:^cell_length$}", sep, utils::etc_str(&course.name));
2022-08-23 16:51:02 +02:00
} else {
print!("{}{:^cell_length$}", sep, &course.name);
2022-08-23 16:51:02 +02:00
}
info_slot = true;
break;
2022-08-23 16:51:02 +02:00
}
}
// If no course was found
None => {
// Verify the "no course" is in the correct day and hour
if *days.get(&j).unwrap() == &day.name.to_string() && k == i {
2022-08-23 16:58:22 +02:00
// If yes print empty row because there is no course
2022-08-23 18:38:47 +02:00
print!("{}{:^cell_length$}", sep, "");
2022-08-23 16:58:22 +02:00
info_slot = true;
2022-08-23 16:51:02 +02:00
break;
}
// Else it was a course of another day/time
}
};
}
2022-08-23 16:58:22 +02:00
if !info_slot {
// We found nothing about the slot because the precedent course
// takes more place than one slot
2022-08-23 18:38:47 +02:00
print!("{}{:^cell_length$}", sep, "");
2022-08-23 16:58:22 +02:00
}
2022-08-23 16:51:02 +02:00
}
print!("{sep}");
2022-08-23 16:51:02 +02:00
}
// Bottom of the table
utils::line_table(clh, cell_length, cn, &Position::Bottom, &HashMap::new());
2022-08-23 16:51:02 +02:00
}