cal7tor/src/timetable.rs

356 lines
11 KiB
Rust
Raw Normal View History

2022-08-16 14:05:25 +02:00
use chrono::{Datelike, Duration, TimeZone, Utc};
use regex::Regex;
2022-08-16 11:57:36 +02:00
use scraper::{Html, Selector};
2022-08-23 16:51:02 +02:00
use std::collections::HashMap;
use crate::utils::{
2022-08-30 22:59:04 +02:00
self, capitalize,
2022-08-23 16:51:02 +02:00
models::{Position, TabChar},
};
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>,
pathway: &str,
2022-08-29 11:44:35 +02:00
user_agent: &str,
2022-08-16 09:33:35 +02:00
) -> (Vec<String>, (usize, Vec<models::Day>)) {
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_thead = Selector::parse("thead").unwrap();
2022-08-15 12:18:08 +02:00
let sel_tr = Selector::parse("tr").unwrap();
let sel_th = Selector::parse("th").unwrap();
// 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
2023-09-18 20:52:18 +02:00
// Find days
let days_size: Vec<_> = raw_timetable
.select(&sel_thead)
.next()
.unwrap()
.select(&sel_tr)
.next()
.unwrap()
.select(&sel_th)
.next()
.unwrap()
.next_siblings()
.flat_map(|i| {
let element = i.value().as_element().unwrap();
element
.attrs()
.filter_map(|f| {
if f.0.contains("colspan") {
Some(f.1)
} else {
None
}
})
.collect::<Vec<_>>()
2022-08-15 12:18:08 +02:00
})
2023-09-18 20:52:18 +02:00
.collect();
2022-08-15 12:18:08 +02:00
2023-09-18 20:52:18 +02:00
println!("{:#?}", days);
2022-08-15 12:18:08 +02:00
2023-09-18 20:52:18 +02:00
(vec![], (0, vec![]))
2022-08-15 12:18:08 +02:00
}
2022-08-15 17:25:14 +02:00
/// Get timetable webpage
2022-08-15 12:18:08 +02:00
async fn get_webpage(
level: i8,
2022-08-15 12:18:08 +02:00
semester: i8,
year: &str,
2022-08-29 11:44:35 +02:00
user_agent: &str,
2022-08-15 12:18:08 +02:00
) -> Result<Html, Box<dyn std::error::Error>> {
let url = format!("https://silice.informatique.univ-paris-diderot.fr/ufr/U{}/EDT/visualiserEmploiDuTemps.php?quoi=M{},{}", year, level, semester);
2022-08-15 12:18:08 +02:00
2022-08-29 11:44:35 +02:00
// Use custom User-Agent
let client = reqwest::Client::builder().user_agent(user_agent).build()?;
let html = client.get(&url).send().await?.text().await?;
2022-08-15 12:18:08 +02:00
2022-08-16 15:48:13 +02:00
// Panic on error
crate::utils::check_errors(&html, &url);
2022-08-15 12:18:08 +02:00
2022-08-16 15:48:13 +02:00
// Parse document
let document = Html::parse_document(&html);
2022-08-15 12:18:08 +02:00
Ok(document)
}
/// Check if the timetable is well built
2022-08-23 17:53:41 +02:00
fn check_consistency(schedules: &[String], timetable: &Vec<models::Day>) -> bool {
2022-08-15 12:18:08 +02:00
let mut checker = true;
for day in timetable {
let mut i = 0;
for course in &day.courses {
match course {
Some(course_it) => {
// Checks the consistency of course start times
if i != course_it.start {
checker = false;
break;
}
// Keep the track of how many courses are in the day
i += course_it.size
}
None => i += 1,
}
}
// The counter should be the same as the amount of possible hours of the day
if i != schedules.len() {
checker = false;
break;
}
}
checker
}
2022-08-16 11:57:36 +02:00
// Data builded in the timetable webpage
type T = (
// Schedules
Vec<String>,
// Timetable per days with the semester as the key
2022-08-16 11:57:36 +02:00
(usize, Vec<models::Day>),
);
2022-08-16 11:57:36 +02:00
// Data builded in the info webpage
2022-08-23 16:51:02 +02:00
type D = HashMap<
2022-08-16 11:57:36 +02:00
// Semester
usize,
2022-08-16 14:41:51 +02:00
// List of start and repetition of course weeks
Vec<(chrono::DateTime<Utc>, i64)>,
>;
/// Build the timetable
2022-08-16 11:57:36 +02:00
pub fn build(timetable: T, dates: D) -> 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{1,2})?.(?P<h2>\d{1,2})(h|:)(?P<m2>\d{1,2})?")
.unwrap();
for hour in timetable.0 {
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();
let before_break = datetimes.get(0).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 &timetable.1 .1 {
for mut course in day.courses.clone().into_iter().flatten() {
// 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;
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
}
2022-08-16 15:48:13 +02:00
let after_break = datetimes.get(1).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
}
semester
}
2022-08-17 14:09:08 +02:00
/// Get the current semester depending on the current date
fn get_semester(semester: Option<i8>) -> i8 {
2022-08-17 16:51:09 +02:00
match semester {
// Force the asked semester
Some(n) => n,
// Find the potential semester
None => {
if Utc::now().month() > 6 {
// From july to december
1
} else {
// from january to june
2
2022-08-17 16:51:09 +02:00
}
}
}
}
/// Get the current year depending on the current date
fn get_year(year: Option<i32>, semester: i8) -> String {
let wanted_year = match year {
// Force the asked semester
Some(n) => n,
// Find the potential semester
None => Utc::now().year(),
};
if semester == 1 {
format!("{}-{}", wanted_year, wanted_year + 1)
} else {
format!("{}-{}", wanted_year - 1, wanted_year)
2022-08-17 16:51:09 +02:00
}
2022-08-17 14:09:08 +02:00
}
/// Display the timetable
2022-08-23 18:38:47 +02:00
pub fn display(timetable: (Vec<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
2022-08-23 18:38:47 +02:00
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
2022-08-23 16:51:02 +02:00
for (i, hour) in timetable.0.into_iter().enumerate() {
// Draw separator line
2022-08-23 18:38:47 +02:00
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!("{}{:^clh$}", sep, hour);
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 {
// Else simply print the course
2022-08-23 18:30:01 +02:00
// If the data is too long
if course.name.len() > quarter {
2022-08-23 18:38:47 +02:00
print!("{}{:^cell_length$}", sep, utils::etc_str(&course.name));
2022-08-23 18:30:01 +02:00
} else {
2022-08-23 18:38:47 +02:00
print!("{}{:^cell_length$}", sep, &course.name);
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;
}
}
}
// 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);
}
// Bottom of the table
2022-08-23 18:38:47 +02:00
utils::line_table(clh, cell_length, cn, Position::Bottom, HashMap::new());
2022-08-23 16:51:02 +02:00
}