Compare commits

...
This repository has been archived on 2024-05-23. You can view files and clone it, but cannot push or open issues or pull requests.

76 commits

Author SHA1 Message Date
778a4c52d8 print it lines by line, without table 2024-12-12 17:19:10 +01:00
9f4d90fab0 dead code 2024-12-12 17:19:10 +01:00
018d201a09 todo 2024-12-12 17:19:10 +01:00
25f91f70e7 remove warnings 2024-12-12 17:19:10 +01:00
400a927ce1
clippy 2024-12-12 09:42:59 +01:00
c0023d3a83
update dependencies 2024-12-12 08:40:05 +01:00
8e18f7d2e5
keywords 2024-09-14 14:18:38 +02:00
61a85ca1f1
bump 2024-09-14 14:18:05 +02:00
586fb4ba28
update dependencie 2024-09-13 19:30:57 +02:00
1cab8ef246
trim class from courses name (#9) 2024-09-13 19:27:29 +02:00
b621436483
format 2024-09-13 19:23:00 +02:00
090a16433d
todo 2024-09-13 19:19:37 +02:00
8e7fdaf3c8
optional shift for td/tp (#10) 2024-09-13 19:07:18 +02:00
fabcd6dac9
start date chooser (#8) 2024-09-13 19:00:18 +02:00
b6a2fec3cb
update dependencies 2024-08-25 13:52:59 +02:00
c81b07c277
fix for M2 2024-08-25 13:47:42 +02:00
181ebf53f4
some are already defaults 2024-05-29 12:02:51 +02:00
07068a0150
update dependencies and follow more clippy advices 2024-05-28 21:19:19 +02:00
0c26ed24de
valid uri 2024-01-31 00:20:37 +01:00
9269ff12db
0.9.2 release 2024-01-30 20:21:27 +01:00
52648ac61a
mention the name in the correct field, mark the professor as the chairperson and make they accept the rdv 2024-01-30 20:21:21 +01:00
ae14925d9e
0.9.1 release 2024-01-30 15:47:53 +01:00
71a47096b1
fix contact 2024-01-30 15:46:05 +01:00
dfcf1226d0
stick to RFC5545 2024-01-30 15:27:03 +01:00
e444d29a15
represent professors as "attendee" and extra data as comments 2024-01-30 15:21:42 +01:00
4d0f14d045
better usage of the lib API 2024-01-30 15:19:00 +01:00
6c9ccff574
0.9.0 release 2024-01-30 14:19:42 +01:00
28f2c2b192
extract extra data from calendar 2024-01-30 14:09:17 +01:00
4791463150
documentation 2024-01-24 09:36:21 +01:00
aafd734728
0.8.0 release 2024-01-24 09:29:10 +01:00
6bc3a5643c
option to not differentiate TD from TP (#5) 2024-01-24 09:28:43 +01:00
e0afc9414f
doc 2024-01-24 09:28:09 +01:00
d237ff3098
doc 2024-01-24 09:11:44 +01:00
8fdebbe4a1
rename calendar (#4) 2024-01-23 10:07:37 +01:00
51284fb5ae
0.7.1 release 2024-01-22 22:16:39 +01:00
3db016519f
fix Cours/TD showing when it shouldn't 2024-01-22 22:16:11 +01:00
8abec03d0b
0.7.0 release 2024-01-22 21:47:13 +01:00
23892ad245
TD/TP always start one week after lectures, and ends one week later 2024-01-22 21:46:25 +01:00
1eaa257dbf
fix calendar based on the 23/05/2023 edition 2024-01-22 18:27:18 +01:00
3a43782bd0
0.6.0 release 2024-01-01 14:14:31 +01:00
5c922a530e
replace Vec by Arc when possible 2024-01-01 14:14:13 +01:00
64ce6e478b
support cours/td 2024-01-01 13:47:23 +01:00
862c02b3e7
Sort days 2024-01-01 12:58:57 +01:00
9ce116f620
reduce code repetition 2024-01-01 12:09:44 +01:00
9ad0de6222
add course selection 2024-01-01 11:39:31 +01:00
77c2444de1
don't crash when the selected courses don't have multiples TP/TD options 2024-01-01 11:24:27 +01:00
220f46c5de
clippy 2024-01-01 11:21:59 +01:00
83563529be
update dependencies 2024-01-01 11:21:04 +01:00
f97d6c41e8
typo 2023-12-11 09:11:33 +01:00
82ae498a93
0.5.1 release 2023-10-02 14:54:29 +02:00
1d9389e41b
Append the course category to the summary 2023-10-02 14:53:40 +02:00
be6670b02f
Changes
- Rename type to category for courses
- Add category to the exported calendar
- Default to non-selected data when filtering TD/TP
2023-10-02 14:47:31 +02:00
e65534d8f9
0.5.0 release 2023-09-29 00:14:50 +02:00
e679602b76
Merge branch 'main' of git.mylloon.fr:Anri/cal7tor 2023-09-29 00:13:28 +02:00
9e5dd7009d
Merge branch 'rewrite' 2023-09-29 00:12:41 +02:00
54317e2356
filter TP/TD 2023-09-29 00:03:45 +02:00
6e13936980
fill hours is useful for the filter 2023-09-29 00:03:41 +02:00
041b8625f1
WIP: filter 2023-09-28 23:16:39 +02:00
384dd9eaa9
Capitalize + Type in models 2023-09-28 22:09:43 +02:00
75832958d8
add notice 2023-09-28 00:30:33 +02:00
2ec3455d16
refactor 2023-09-28 00:27:16 +02:00
5fc7d9209c
Fix some bugs 2023-09-28 00:23:51 +02:00
dc3a5c6d8e
fmt 2023-09-28 00:01:51 +02:00
be636f552a
Changes
- We are now searching backtoschool day in the same page that the timetable
- It should work now
2023-09-28 00:01:48 +02:00
c671587b7a
Sadly break-weeks are voted every year 2023-09-28 00:00:46 +02:00
bbee1d58be
Should be better now ? 2023-09-26 23:55:54 +02:00
189b77cc4f
Correctly build the timetable from scratch 2023-09-26 23:08:41 +02:00
b3fec12292
I made a discovery that changed my life! 2023-09-26 19:16:15 +02:00
9ba185b247
oui 2023-09-18 20:54:48 +02:00
1dbfe12566
find size of days 2023-09-18 20:52:18 +02:00
e50475f0a7
Actualiser README.md 2023-09-18 20:02:17 +02:00
ff95bf09b5
currently alpha 2023-09-18 20:00:35 +02:00
16a9bba6dd
find the right timetable for the current semester/year combo 2023-09-18 19:50:30 +02:00
ade74b4e4c
fix url 2023-09-18 18:49:08 +02:00
58399ca1e9
typo 2023-09-18 18:48:44 +02:00
3d51a4b743
Update branding 2023-09-18 18:46:50 +02:00
11 changed files with 1615 additions and 1050 deletions

1309
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,21 +1,25 @@
[package]
name = "cal8tor"
version = "0.4.5"
name = "cal7tor"
version = "0.10.0"
authors = ["Mylloon"]
edition = "2021"
description = "Extractor of the calendar of the IT degree of university Paris 8"
description = "Timetable extractor for the Paris Cité master's degree in IT"
readme = "README.md"
repository = "https://git.kennel.ml/Anri/cal8tor"
keywords = ["scrape", "calendar"]
repository = "https://git.mylloon.fr/Anri/cal7tor"
keywords = ["scrape", "calendar", "paris diderot", "paris cité"]
publish = false
license = "AGPL-3.0-or-later"
[dependencies]
reqwest = { version = "0.11" }
tokio = { version = "1.32", features = ["full"] }
scraper = "0.17"
regex = "1.9"
chrono = "0.4.28"
ics = "0.5"
uuid = { version = "1.4", features = ["v4", "fast-rng"] }
clap = { version = "4.4", features = ["derive"] }
reqwest = { version = "0.12" }
tokio = { version = "1.42", features = ["full"] }
scraper = "0.22"
regex = "1.11"
chrono = "0.4.39"
ics = { version = "0.5", default-features = false }
uuid = { version = "1.11", features = ["v4", "fast-rng"] }
clap = { version = "4.5", features = ["derive"] }
dialoguer = "0.11"
[lints.clippy]
pedantic = "warn"

View file

@ -1,16 +1,13 @@
# cal8tor • *cal*endar P*8* extrac*tor*
# cal7tor • *cal*endar P*7* extrac*tor*
Extracteur d'emploi du temps pour la licence d'informatique de Paris 8
> Fork de [cal8tor](https://git.mylloon.fr/Anri/cal8tor)
[![dependency status](https://deps.rs/repo/gitea/git.mylloon.fr/Anri/cal8tor/status.svg)](https://deps.rs/repo/gitea/git.mylloon.fr/Anri/cal8tor)
Extracteur d'emploi du temps pour les masters d'informatique de Paris Cité (Diderot)
[![dependency status](https://deps.rs/repo/gitea/git.mylloon.fr/Anri/cal7tor/status.svg)](https://deps.rs/repo/gitea/git.mylloon.fr/Anri/cal7tor)
## Installation
### Arch
cal8tor est disponible sur le AUR : [`cal8tor`](https://aur.archlinux.org/packages/cal8tor)
et [`cal8tor-git`](https://aur.archlinux.org/packages/cal8tor-git).
### Manuellement
Cf. [Compilation et installation](#compilation-et-installation).
@ -20,26 +17,28 @@ Cf. [Compilation et installation](#compilation-et-installation).
Pour afficher la page d'aide
```
$ cal8tor --help
$ cal7tor --help
```
## Voir le calendrier dans le terminal
<!-- ## Voir le calendrier dans le terminal
Pour les L2-X par exemple, lance :
> Cette partie est héritée de cal8tor et n'est actuellement pas compatible avec cal7tor.
Pour les M1 par exemple, lance :
```bash
$ cal8tor l2-X
$ cal7tor M1
```
> Le rendu peut parfois être difficile à lire, n'hésites pas à utiliser l'option
> `-c` (ou `--cl`) pour ajuster la longueur des cellules du planning.
> `-c` (ou `--cl`) pour ajuster la longueur des cellules du planning. -->
## Exporter le calendrier au format `.ics`
Pour les L1-A par exemple, lance :
Pour les M1 par exemple, lance :
```bash
$ cal8tor L1A --export calendar.ics
$ cal7tor M1 --export calendar.ics
```
> Le fichier comprend le fuseau horaire pour `Europe/Paris` et est
@ -58,7 +57,7 @@ Vous aurez besoin de Rust pour compiler le programme.
1. Clone le dépôt et s'y rendre
```bash
$ git clone https://git.mylloon.fr/Anri/cal8tor.git && cd cal8tor
$ git clone https://git.mylloon.fr/Anri/cal7tor.git && cd cal7tor
```
2. Compiler et installer l'application
@ -67,7 +66,7 @@ $ git clone https://git.mylloon.fr/Anri/cal8tor.git && cd cal8tor
$ cargo install --path .
```
3. Tu peux maintenant supprimer le dossier `cal8tor` !
3. Tu peux maintenant supprimer le dossier `cal7tor` !
---

172
src/filter.rs Normal file
View file

@ -0,0 +1,172 @@
use dialoguer::MultiSelect;
use crate::timetable::models::Category;
use crate::timetable::models::Timetable;
use crate::utils::get_count;
use crate::utils::get_entry;
use crate::utils::get_entry_nocat;
use crate::utils::get_selection;
const DISCLAIMER: &str = "(selection avec ESPACE, ENTRER pour valider)";
/// Filter the timetable
pub fn timetable(timetable: Timetable, merge_td_tp: bool) -> Timetable {
let mut my_timetable = timetable;
/* Note on Cours/TD:
* We use the "as long as x interests us, we accept" approach.
*
* Because when a course and its TD are on the same slot,
* it's probably because there's an alternation between course
* and TD and no other choice is possible. */
choice(&mut my_timetable);
courses(&mut my_timetable);
tdtp(&mut my_timetable, merge_td_tp);
my_timetable
}
/// Exclude some courses
fn choice(timetable: &mut Timetable) {
let mut multiselected = vec![];
timetable.1 .1.iter().for_each(|day| {
day.courses.iter().for_each(|course_opt| {
if let Some(course) = course_opt {
if !multiselected.contains(&course.name) {
multiselected.push(course.name.clone());
}
}
});
});
let defaults = vec![true; multiselected.len()];
let selections = MultiSelect::new()
.with_prompt(format!("Choisis tes matières {DISCLAIMER}"))
.items(&multiselected[..])
.defaults(&defaults[..])
.interact()
.unwrap();
for day in &mut timetable.1 .1 {
day.courses.retain(|course_opt| {
if let Some(course) = course_opt {
// Remove courses not followed
for i in &selections {
if course.name == multiselected[*i] {
return true;
}
}
}
false
});
}
}
/// Filter the multiple courses
fn courses(timetable: &mut Timetable) {
let entry_getter = get_entry;
// List of courses and Counter of how much they appears
// to know if multiples slots are available
let (mut courses, counts) = get_count(timetable, &[Category::Cours], entry_getter);
// Keep only elements who have multiples slots
courses.retain(|course| *counts.get(&entry_getter(course.0)).unwrap() > 1);
let mut multiselected: Vec<String> = courses.iter().map(get_selection).collect();
multiselected.sort();
let mut selections = vec![];
if !multiselected.is_empty() {
let defaults = vec![false; multiselected.len()];
selections = MultiSelect::new()
.with_prompt(format!("Choisis tes horaires de Cours {DISCLAIMER}"))
.items(&multiselected[..])
.defaults(&defaults[..])
.interact()
.unwrap();
}
// Keep only wanted courses
for day in &mut timetable.1 .1 {
day.courses.retain(|course_opt| {
if let Some(course) = course_opt {
// Keep if it's a TD/TP
if course.category.contains(&Category::TD)
|| course.category.contains(&Category::TP)
{
return true;
}
// Keep if only one slot is available
if *counts.get(&entry_getter(course)).unwrap() == 1 {
return true;
}
// Keep only chosen courses if multiple was available
for i in &selections {
if get_selection(&(course, day.name.clone())) == multiselected[*i] {
return true;
}
}
}
false
});
}
}
/// Filter the multiples TD/TP
fn tdtp(timetable: &mut Timetable, merge: bool) {
// If we differentiate TD from TP
let entry_getter = if merge { get_entry_nocat } else { get_entry };
// List of TP/TD and Counter of how much they appears
// to know if multiples slots are available
let (mut td_or_tp, counts) = get_count(timetable, &[Category::TD, Category::TP], entry_getter);
// Keep only elements who have multiples TD/TP
td_or_tp.retain(|course| *counts.get(&entry_getter(course.0)).unwrap() > 1);
let mut multiselected: Vec<String> = td_or_tp.iter().map(get_selection).collect();
multiselected.sort();
let mut selections = vec![];
if !multiselected.is_empty() {
let defaults = vec![false; multiselected.len()];
selections = MultiSelect::new()
.with_prompt(format!("Choisis tes horaires de TD/TP {DISCLAIMER}"))
.items(&multiselected[..])
.defaults(&defaults[..])
.interact()
.unwrap();
}
// Keep only wanted courses
for day in &mut timetable.1 .1 {
day.courses.retain(|course_opt| {
if let Some(course) = course_opt {
// Keep if it's a course
if course.category.contains(&Category::Cours) {
return true;
}
// Keep if only one slot is available of the TD/TP
if *counts.get(&entry_getter(course)).unwrap() == 1 {
return true;
}
// Keep only chosen TD/TP if multiple was available
for i in &selections {
if get_selection(&(course, day.name.clone())) == multiselected[*i] {
return true;
}
}
}
false
});
}
}

View file

@ -1,12 +1,16 @@
use std::sync::Arc;
use chrono::TimeZone;
use ics::{
parameters,
properties::{Class, Description, DtEnd, DtStart, Location, Summary, Transp},
parameters::{Language, PartStat, Role, TzIDParam, CN},
properties::{
Attendee, Categories, Class, Description, DtEnd, DtStart, Location, Summary, Transp,
},
Event, ICalendar, Standard,
};
pub fn export(courses: Vec<crate::timetable::models::Course>, filename: &mut String) {
let mut calendar = ICalendar::new("2.0", "cal8tor");
let mut calendar = ICalendar::new("2.0", "cal7tor");
// Add Europe/Paris timezone
let timezone_name = "Europe/Paris";
@ -36,34 +40,57 @@ pub fn export(courses: Vec<crate::timetable::models::Course>, filename: &mut Str
// Professor's name
if course.professor.is_some() {
event.push(Description::new(course.professor.unwrap()));
let name = course.professor.unwrap();
let mut contact = Attendee::new("mailto:place@holder.com");
contact.add(CN::new(name));
contact.add(PartStat::ACCEPTED);
contact.add(Role::CHAIR);
event.push(contact);
}
// Start time of the course
let mut date_start = DtStart::new(dt_ical(course.dtstart.unwrap()));
date_start.append(parameters!("TZID" => timezone_name));
date_start.add(TzIDParam::new(timezone_name));
event.push(date_start);
// End time of the course
let mut date_end = DtEnd::new(dt_ical(course.dtend.unwrap()));
date_end.append(parameters!("TZID" => timezone_name));
date_end.add(TzIDParam::new(timezone_name));
event.push(date_end);
// Room location
event.push(Location::new(course.room));
let categories = course
.category
.iter()
.map(std::string::ToString::to_string)
.collect::<Arc<[String]>>()
.join("/");
// Course's name
let mut course_name = Summary::new(course.name);
course_name.append(parameters!("LANGUAGE" => "fr"));
let mut course_name = Summary::new(format!("{} - {}", categories, course.name));
course_name.add(Language::new("fr"));
event.push(course_name);
// Course's category
event.push(Categories::new(categories));
// Course extra data
if course.data.is_some() {
event.push(Description::new(course.data.unwrap()));
}
// Add the course to the calendar
calendar.add_event(event);
}
// Add the extension if needed
if !filename.ends_with(".ics") {
*filename = format!("{}.ics", filename)
if !std::path::Path::new(filename)
.extension()
.map_or(false, |ext| ext.eq_ignore_ascii_case("ics"))
{
*filename = format!("{filename}.ics");
};
calendar.save_file(filename).unwrap();

View file

@ -1,76 +1,100 @@
use chrono::{DateTime, Utc};
use chrono::{DateTime, Duration, Utc};
use regex::{Captures, Regex};
use scraper::{Html, Selector};
use std::collections::HashMap;
use scraper::Selector;
use std::{collections::HashMap, sync::Arc};
pub async fn info(user_agent: &str) -> HashMap<usize, Vec<(DateTime<Utc>, i64)>> {
let document = get_webpage(user_agent)
use crate::utils::{
get_semester, get_webpage, get_year,
models::{Info, InfoList, InfoType},
};
pub async fn get_start_date(
level: i8,
semester_opt: Option<i8>,
year_opt: Option<i32>,
user_agent: &str,
) -> String {
let semester = get_semester(semester_opt);
let year = get_year(year_opt, semester);
// Fetch the timetable of the FIRST semester
let document = get_webpage(level, 1, &year, user_agent)
.await
.expect("Can't reach info website.");
// Selectors
let sel_ul = Selector::parse("ul").unwrap();
let sel_li = Selector::parse("li").unwrap();
let sel_b = Selector::parse("b").unwrap();
let sel_font = Selector::parse("font").unwrap();
// Find the raw infos in html page
let mut raw_data = Vec::new();
for (i, data) in document.select(&sel_ul).enumerate() {
if [1, 2].contains(&i) {
raw_data.push(data);
}
}
// Find when is the back-to-school date
let raw_data = document
.select(&sel_b)
.find(|element| element.select(&sel_font).next().is_some())
.unwrap()
.inner_html();
let mut data = HashMap::new();
// d => date
// r => repetition
let re = Regex::new(r"(?P<d>\d{1,2} \w+ \d{4}).+(?P<r>\d)").unwrap();
for (i, ul) in raw_data.into_iter().enumerate() {
for element in ul.select(&sel_li) {
match element.inner_html() {
e if e.starts_with("Début") => {
let captures = re.captures(&e).unwrap();
let re = Regex::new(r"\d{1,2} (septembre|octobre)").unwrap();
let start_date = get_date(captures.name("d").unwrap().as_str());
let rep: i64 = captures.name("r").unwrap().as_str().parse().unwrap();
data.insert(i + 1, vec![(start_date, rep)]);
}
e if e.starts_with("Reprise") => {
let captures = re.captures(&e).unwrap();
captures.name("g");
let start_date = get_date(captures.name("d").unwrap().as_str());
let rep: i64 = captures.name("r").unwrap().as_str().parse().unwrap();
let it = i + 1;
let mut vec = data.get(&it).unwrap().to_owned();
vec.push((start_date, rep));
data.insert(it, vec);
}
_ => (),
}
}
}
data
re.captures(&raw_data)
.and_then(|caps| caps.get(0))
.map_or("1 septembre".to_owned(), |m| m.as_str().to_owned())
}
/// Get info webpage
async fn get_webpage(user_agent: &str) -> Result<Html, Box<dyn std::error::Error>> {
let url = "https://informatique.up8.edu/licence-iv/edt";
pub fn info(semester_opt: Option<i8>, year_opt: Option<i32>, date: &str, skip_week: bool) -> Info {
let semester = get_semester(semester_opt);
let year = get_year(year_opt, semester);
// Use custom User-Agent
let client = reqwest::Client::builder().user_agent(user_agent).build()?;
let html = client.get(url).send().await?.text().await?;
// 1st semester
let weeks_s1_1 = 6; // Weeks before break
let weeks_s1_2 = 7; // Weeks after break
let date_s1_1 = get_date(&format!("{} {}", date, year.split_once('-').unwrap().0)); // Get first week of school
let date_s1_2 = date_s1_1 + Duration::weeks(weeks_s1_1 + 1); // Back-to-school week - add week of holidays
// Panic on error
crate::utils::check_errors(&html, url);
// 2nd semester
let weeks_s2_1 = 11; // Weeks before break
let weeks_s2_2 = 1; // Weeks after break
let date_s2_1 = date_s1_2 + Duration::weeks(weeks_s1_2 + 4); // Get first week - add week of 'christmas/new year holidays'
let date_s2_2 = date_s2_1 + Duration::weeks(weeks_s2_1 + 2); // Back-to-school week - add week of holidays
Ok(Html::parse_document(&html))
// Group courses values and derive it for TD/TP
let cours_s1 = vec![(date_s1_1, weeks_s1_1), (date_s1_2, weeks_s1_2)];
let cours_s2 = vec![(date_s2_1, weeks_s2_1), (date_s2_2, weeks_s2_2)];
let delta = i64::from(skip_week);
let tdtp_s1 = derive_from_cours(&cours_s1, delta);
let tdtp_s2 = derive_from_cours(&cours_s2, delta);
HashMap::from([
(
1_usize,
InfoType {
course: cours_s1,
td_tp: tdtp_s1,
},
),
(
2_usize,
InfoType {
course: cours_s2,
td_tp: tdtp_s2,
},
),
])
}
/// Find TD/TP dates, based on the ones from courses
fn derive_from_cours(courses: &InfoList, delta: i64) -> Vec<(DateTime<Utc>, i64)> {
// TD/TP start one week after courses
let before_break = courses.first().unwrap();
let after_break = courses.last().unwrap();
vec![
(
before_break.0 + Duration::weeks(delta),
before_break.1 - delta,
),
(after_break.0, after_break.1 + delta),
]
}
/// Turn a french date to an english one
@ -93,7 +117,7 @@ fn anglophonization(date: &str) -> String {
// New regex of all the french month
let re = Regex::new(&format!(
"({})",
dico.keys().cloned().collect::<Vec<_>>().join("|")
dico.keys().copied().collect::<Arc<[_]>>().join("|")
))
.unwrap();
@ -101,19 +125,19 @@ fn anglophonization(date: &str) -> String {
// Use 12:00 and UTC TZ for chrono parser
"{} 12:00 +0000",
// Replace french by english month
re.replace_all(date, |cap: &Captures| match &cap[0] {
re.replace_all(&date.to_lowercase(), |cap: &Captures| match &cap[0] {
month if dico.contains_key(month) => dico.get(month).unwrap(),
month => {
panic!("Unknown month: {}", month)
panic!("Unknown month: {month}")
}
})
)
}
/// Turn a string to a DateTime
/// Turn a string to a `DateTime`
fn get_date(date: &str) -> DateTime<Utc> {
// Use and keep UTC time, we have the hour set to 12h and
// Paris 8 is in France so there is no problems
// Paris 7 is in France so there is no problems
DateTime::parse_from_str(&anglophonization(date), "%e %B %Y %H:%M %z")
.unwrap()
.into()

View file

@ -1,6 +1,8 @@
use clap::Parser;
use dialoguer::Input;
use regex::Regex;
mod filter;
mod ics;
mod info;
mod timetable;
@ -9,73 +11,81 @@ mod utils;
#[derive(Parser)]
#[clap(version, about, long_about = None)]
struct Args {
/// The class you want to get the timetable, i.e.: L2-A
/// The class you want to get the timetable, i.e.: M1
#[clap(value_parser)]
class: String,
/// The semester you want (useful only in 3rd year, 1-2 use letter in class)
/// The semester you want (1 or 2), default to current semester
#[clap(short, long, value_parser, value_name = "SEMESTER NUMBER")]
semester: Option<i8>,
/// The year, default to current year
#[clap(short, long, value_parser, value_name = "YEAR")]
year: Option<i32>,
/// Export to iCalendar format (.ics)
#[clap(short, long, value_name = "FILE NAME")]
export: Option<String>,
/// Size of cell of the timetable (irrelevant when exporting the timetable)
#[clap(short, long, value_name = "CELL LENGTH", default_value_t = 35)]
cl: usize,
/// Doesn't distinguish TD from TP
#[clap(short, long)]
td_are_tp: bool,
/// First day of your year
#[clap(short, long)]
first_day: Option<String>,
/// If TD/TP start a week after courses
#[clap(short, long)]
week_skip: bool,
}
#[tokio::main]
async fn main() {
let args = Args::parse();
let matches = Regex::new(r"[Ll](?P<year>\d)[-–•·]?(?P<letter>.)?")
let matches = Regex::new(r"(?i)M(?P<level>[1,2])")
.unwrap()
.captures(&args.class)
.unwrap();
let year = matches
.name("year")
let level = matches
.name("level")
.unwrap()
.as_str()
.parse::<i8>()
.unwrap();
let letter = matches
.name("letter")
.map(|c| c.as_str().chars().next().expect("Error in letter"));
// Show a separator only if we need one
let seperator = match letter {
Some(_) => "-",
None => "",
let user_agent = format!("cal7tor/{}", env!("CARGO_PKG_VERSION"));
println!("Récupération de l'emploi du temps des M{level}...");
let mut timetable = timetable::timetable(level, args.semester, args.year, &user_agent).await;
timetable = filter::timetable(timetable, args.td_are_tp);
let date = match args.first_day {
None => Input::new()
.with_prompt("Début des cours de l'année")
.default(info::get_start_date(level, args.semester, args.year, &user_agent).await)
.interact_text()
.unwrap(),
Some(day) => day,
};
let user_agent = format!("cal8tor/{}", env!("CARGO_PKG_VERSION"));
println!(
"Récupération de l'emploi du temps des L{}{}{}...",
year,
seperator,
letter.unwrap_or_default().to_uppercase()
);
let timetable = timetable::timetable(year, args.semester, letter, &user_agent).await;
println!("Récupération des informations par rapport à l'année...");
let info = info::info(&user_agent).await;
let info = info::info(args.semester, args.year, &date, args.week_skip);
if args.export.is_some() {
// Export the calendar
let mut filename = args.export.unwrap();
let builded_timetable = timetable::build(timetable, info);
let builded_timetable = timetable::build(&timetable, &info);
ics::export(builded_timetable, &mut filename);
println!("Fichier .ICS construit et exporté => {}", filename);
println!("Fichier .ICS construit et exporté => {filename}");
} else {
// Show the calendar
println!("Affichage...");
timetable::display(timetable, args.cl);
println!("Vous devrez peut-être mettre votre terminal en plein écran si ce n'est pas déjà le cas.");
timetable::display(&timetable);
}
}

View file

@ -1,242 +1,152 @@
#![allow(clippy::cast_sign_loss)]
use chrono::{Datelike, Duration, TimeZone, Utc};
use regex::Regex;
use scraper::{Html, Selector};
use std::collections::HashMap;
use scraper::Selector;
use std::{collections::HashMap, sync::Arc};
use crate::utils::{
self, capitalize,
models::{Position, TabChar},
format_time_slot, get_hours, get_semester, get_webpage, get_year,
models::{Info, InfoList},
Capitalize,
};
use self::models::Day;
pub mod models;
/// Fetch the timetable for a class
pub async fn timetable(
year: i8,
level: i8,
semester_opt: Option<i8>,
letter: Option<char>,
year_opt: Option<i32>,
user_agent: &str,
) -> (Vec<String>, (usize, Vec<models::Day>)) {
let semester = get_semester(semester_opt, letter);
) -> models::Timetable {
let semester = get_semester(semester_opt);
let document = get_webpage(year, semester, letter, user_agent)
let year = get_year(year_opt, semester);
let document = get_webpage(level, semester, &year, user_agent)
.await
.expect("Can't reach timetable website.");
// Selectors
let sel_table = Selector::parse("table").unwrap();
let sel_tr = Selector::parse("tr").unwrap();
let sel_tbody = Selector::parse("tbody").unwrap();
let sel_th = Selector::parse("th").unwrap();
let sel_td = Selector::parse("td").unwrap();
let sel_em = Selector::parse("em").unwrap();
let sel_small = Selector::parse("small").unwrap();
let sel_strong = Selector::parse("strong").unwrap();
let sel_b = Selector::parse("b").unwrap();
let sel_span = Selector::parse("span").unwrap();
// Find the timetable
let raw_timetable = document.select(&sel_table).next().unwrap();
// Find the slots available for the timetable
let raw_schedules = raw_timetable.select(&sel_tr).next().unwrap();
let schedules = get_hours();
// Find availables schedules
let mut schedules = Vec::new();
for time in raw_schedules.select(&sel_th) {
schedules.push(time.inner_html());
}
let mut timetable: Vec<models::Day> = Vec::new();
// Find the timetable values
let raw_timetable_values = raw_timetable.select(&sel_tbody).next().unwrap();
// For each days
let mut timetable = Vec::new();
let span_regex = Regex::new(r"<span.*</span>").unwrap();
for day in raw_timetable_values.select(&sel_tr) {
let mut courses_vec = Vec::new();
let mut location_tracker = 0;
for course in day.select(&sel_td) {
if course.inner_html() == "" {
courses_vec.push(None);
location_tracker += 1;
} else {
courses_vec.push(Some(models::Course {
name: match course.select(&sel_em).next() {
Some(value) => span_regex.replace(&value.inner_html(), " ").to_string(),
None => span_regex
.replace(course.inner_html().split("<br>").next().unwrap(), " ")
.to_string(),
},
professor: match course
.select(&sel_small)
raw_timetable
.select(&sel_tbody)
.next()
.unwrap()
.inner_html()
.split("<br>")
.next()
{
Some(data) => {
if data.contains("</strong>") {
// This is the room, so there is no professor assigned
// to this courses yet
None
} else {
Some(data.to_string())
}
}
None => None,
.select(&sel_td)
.filter(|element| element.value().attr("title").is_some())
.for_each(|i| {
let extra_data = i.select(&sel_span).next().map(|span|
span.inner_html().replace("<br>", "").trim().to_owned()
);
/* TODO: Instead of searching *_M2, just find any TD_* and TP_* */
let matches =
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();
let day = matches
.name("day")
.unwrap()
.as_str()
.capitalize();
let startime = matches
.name("startime")
.unwrap()
.as_str();
let binding = i.select(&sel_b).last().unwrap().inner_html();
let course = models::Course{
category: match matches
.name("type")
.map_or("", |m| m.as_str()) {
/* TODO: Instead of searching *_M2, just find any TD_* and TP_* */
"COURS" => [models::Category::Cours].into(),
"TP" | "TP_M2" => [models::Category::TP].into(),
"TD" | "TD_M2" => [models::Category::TD].into(),
"COURS_TD" => [models::Category::Cours, models::Category::TD].into(),
_ => {
println!("Unknown type of course, falling back to 'COURS': {}", i.value().attr("title").unwrap());
[models::Category::Cours].into()
},
room: capitalize(&mut match course.select(&sel_strong).next() {
Some(el) => el.inner_html().replace("<br>", ""),
// Error in the site, silently passing... (the room is probably at the professor member)
None => String::new(),
}),
start: location_tracker,
size: match course.value().attr("colspan") {
Some(i) => i.parse().unwrap(),
None => 1,
},
name: Regex::new(r"[ -][ML][1-3]$").unwrap().replace(
matches
.name("name")
.unwrap()
.as_str(),
""
).to_string(),
professor: if let Some(raw_prof) = i.select(&sel_small).last() {
match raw_prof.inner_html() {
i if i.starts_with("<span") => None,
i => Some(i),
}
} else { None },
room: Regex::new(r"(<table.*<\/table>|<br>.*?<br>.*?)?<br>(?P<location>.*?)<br>")
.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,
}));
match &courses_vec[courses_vec.len() - 1] {
Some(course) => location_tracker += course.size,
None => location_tracker += 1,
}
}
}
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.select(&sel_th).next().unwrap().inner_html(),
courses: courses_vec,
})
name: day.clone(),
courses: vec![Some(course)],
});
}
});
if !check_consistency(&schedules, &timetable) {
panic!("Error when building the timetable.");
}
// 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))
}
/// Get timetable webpage
async fn get_webpage(
year: i8,
semester: i8,
letter: Option<char>,
user_agent: &str,
) -> Result<Html, Box<dyn std::error::Error>> {
let url = {
let panic_semester_message = "Unknown semester.";
let panic_letter_message = "Unknown letter.";
let base_url = "https://informatique.up8.edu/licence-iv/edt";
let allow_letters_1 = match semester {
1 => ['a', 'b', 'c'],
2 => ['x', 'y', 'z'],
_ => panic!("{}", panic_semester_message),
};
let allow_letters_2_3 = match semester {
1 => ['a', 'b'],
2 => ['x', 'y'],
_ => panic!("{}", panic_semester_message),
};
match year {
1 => {
let c = letter.expect(panic_letter_message).to_ascii_lowercase();
if allow_letters_1.contains(&c) {
format!("{}/l1-{}.html", base_url, c)
} else {
panic!("{}", panic_letter_message)
}
}
2 => {
let c = letter.expect(panic_letter_message).to_ascii_lowercase();
if allow_letters_2_3.contains(&c) {
format!("{}/l2-{}.html", base_url, c)
} else {
panic!("{}", panic_letter_message)
}
}
3 => {
let c = letter.expect(panic_letter_message).to_ascii_lowercase();
if allow_letters_2_3.contains(&c) {
format!("{}/l3-{}.html", base_url, c)
} else {
panic!("{}", panic_letter_message)
}
}
_ => panic!("Unknown year."),
}
};
// Use custom User-Agent
let client = reqwest::Client::builder().user_agent(user_agent).build()?;
let html = client.get(&url).send().await?.text().await?;
// Panic on error
crate::utils::check_errors(&html, &url);
// Parse document
let document = Html::parse_document(&html);
Ok(document)
}
/// Check if the timetable is well built
fn check_consistency(schedules: &[String], timetable: &Vec<models::Day>) -> bool {
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
}
// Data builded in the timetable webpage
type T = (
// Schedules
Vec<String>,
// Timetable per days with the semester as the key
(usize, Vec<models::Day>),
);
// Data builded in the info webpage
type D = HashMap<
// Semester
usize,
// List of start and repetition of course weeks
Vec<(chrono::DateTime<Utc>, i64)>,
>;
/// Build the timetable
pub fn build(timetable: T, dates: D) -> Vec<models::Course> {
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{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 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();
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(),
@ -262,19 +172,66 @@ pub fn build(timetable: T, dates: D) -> Vec<models::Course> {
// Start date of the back-to-school week
let datetimes = dates.get(&timetable.1 .0).unwrap();
let before_break = datetimes.get(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();
let mut date = before_break.0;
let mut rep = before_break.1;
// For each weeks
for _ in 0..2 {
for _ in 0..rep {
for day in &timetable.1 .1 {
for mut course in day.courses.clone().into_iter().flatten() {
for day in days {
for mut course in day.courses.iter().flatten().cloned() {
// 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
.is_some_and(|list| !course.category.iter().any(|item| list.contains(item)))
|| exclude.is_some_and(|list| {
course.category.iter().any(|item| list.contains(item))
})
{
continue;
}
// Add the changed datetimes
course.dtstart = Some(
Utc.with_ymd_and_hms(
@ -306,141 +263,35 @@ pub fn build(timetable: T, dates: D) -> Vec<models::Course> {
// From friday to monday
date += Duration::days(2);
}
let after_break = datetimes.get(1).unwrap();
let after_break = info.last().unwrap();
date = after_break.0;
rep = after_break.1;
}
semester
}
/// Get the current semester depending on the letter or the current date
fn get_semester(semester: Option<i8>, letter: Option<char>) -> i8 {
match semester {
// Force the asked semester
Some(n) => n,
// Find the potential semester
None => match letter {
// Based on letter (kinda accurate)
Some(c) => {
if c.to_ascii_uppercase() as i8 > 77 {
// If letter is N or after
2
} else {
// If letter is before N
1
}
}
// Based on the time (kinda less accurate)
None => {
if Utc::now().month() > 6 {
// From july to december
1
} else {
// from january to june
2
}
}
},
}
}
/// Display the timetable
pub fn display(timetable: (Vec<String>, (usize, Vec<models::Day>)), cell_length: usize) {
// Cell length for hours
let clh = 11;
// Cell number
let cn = 6;
// 3/4 of cell length
let quarter = (3 * cell_length) / 4;
let sep = TabChar::Bv.val();
// Top of the tab
utils::line_table(clh, cell_length, cn, Position::Top, HashMap::new());
// First empty case
print!("{}{:^clh$}{}", sep, "", sep);
// Print day's of the week
let mut days = HashMap::new();
for (i, data) in timetable.1 .1.iter().enumerate() {
days.insert(i, &data.name);
print!("{:^cell_length$}{}", &data.name, sep);
pub fn display(timetable: &(Arc<[String]>, (usize, Vec<Day>))) {
for day in &timetable.1 .1 {
for (index, course_option) in day.courses.iter().enumerate() {
if let Some(course) = course_option {
if index == 0 {
println!("\n{}:", day.name);
}
// Store the data of the course for utils::line_table
let mut next_skip = HashMap::new();
// For each hours -- i the hour's number
for (i, hour) in timetable.0.into_iter().enumerate() {
// Draw separator line
utils::line_table(clh, cell_length, cn, Position::Middle, next_skip);
// Reset
next_skip = HashMap::new();
// Print hour
print!("{}{:^clh$}", sep, hour);
// For all the days - `j` the day's number
for (j, day) in timetable.1 .1.iter().enumerate() {
// True if we found something about the slot we are looking for
let mut info_slot = false;
// For all the courses of each days - `k` the possible course.start
for (k, course_opt) in day.courses.iter().enumerate() {
match course_opt {
// If there is a course
Some(course) => {
// Check the course's hour
if i == course.start {
// If the course uses more than one time slot
if course.size > 1 {
// If the data is too long
if course.name.len() > quarter {
let data = utils::split_half(&course.name);
next_skip.insert(j, data.1.trim());
print!("{}{:^cell_length$}", sep, data.0.trim());
} else {
next_skip.insert(j, &course.name);
print!("{}{:^cell_length$}", sep, "");
}
info_slot = true;
break;
} else {
// 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));
} else {
print!("{}{:^cell_length$}", sep, &course.name);
}
info_slot = true;
break;
println!(
" {} - {} : {} ({}) // {}",
format_time_slot(course.start, course.size),
course
.category
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<String>>()
.join(", "),
course.name,
course.room,
course.professor.as_deref().unwrap_or("N/A"),
);
}
}
}
// 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 {
// If yes print empty row because there is no course
print!("{}{:^cell_length$}", sep, "");
info_slot = true;
break;
}
// Else it was a course of another day/time
}
};
}
if !info_slot {
// We found nothing about the slot because the precedent course
// takes more place than one slot
print!("{}{:^cell_length$}", sep, "");
}
}
print!("{}", sep);
}
// Bottom of the table
utils::line_table(clh, cell_length, cn, Position::Bottom, HashMap::new());
}

View file

@ -1,5 +1,23 @@
#[derive(Clone)]
use std::sync::Arc;
#[derive(Clone, Debug, PartialEq)]
pub enum Category {
Cours,
TP,
TD,
}
impl std::fmt::Display for Category {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Course {
/// Type du cours
pub category: Arc<[Category]>,
/// Course's name
pub name: String,
@ -25,11 +43,23 @@ pub struct Course {
/// Datetime when the course end
/// Filled only when building for the ICS
pub dtend: Option<chrono::DateTime<chrono::Utc>>,
/// Extra data
pub data: Option<String>,
}
#[derive(Debug)]
pub struct Day {
/// Day's name
pub name: String,
/// Ordered list of all the courses of the day
pub courses: Vec<Option<Course>>,
}
// Data builded in the timetable webpage
pub type Timetable = (
// Schedules
Arc<[String]>,
// Timetable per days with the semester as the key
(usize, Vec<Day>),
);

View file

@ -1,122 +1,172 @@
use std::{collections::HashMap, sync::Arc};
use chrono::{Datelike, Utc};
use scraper::Html;
use crate::timetable::models::{Category, Course, Timetable};
pub mod models;
/// Panic if an error happened
pub fn check_errors(html: &String, loc: &str) {
let no_timetable = "Aucun créneau horaire affecté";
match html {
t if t.contains(&err_code(429)) => panic!(
"URL: {} • HTTP 429: Slow down - Rate limited (too many access attempts detected)",
loc
),
t if t.contains(no_timetable) => panic!("URL: {loc}{no_timetable}"),
_ => (),
}
}
/// Create String error code
fn err_code(code: i32) -> String {
format!("HTTP Code : {}", code)
/// Get timetable webpage
pub async fn get_webpage(
level: i8,
semester: i8,
year: &str,
user_agent: &str,
) -> Result<Html, Box<dyn std::error::Error>> {
let url = format!("https://silice.informatique.univ-paris-diderot.fr/ufr/U{year}/EDT/visualiserEmploiDuTemps.php?quoi=M{level},{semester}");
// Use custom User-Agent
let client = reqwest::Client::builder().user_agent(user_agent).build()?;
let html = client.get(&url).send().await?.text().await?;
// Panic on error
crate::utils::check_errors(&html, &url);
// Parse document
let document = Html::parse_document(&html);
Ok(document)
}
/// Print a line for the table
pub fn line_table(
cell_length_hours: usize,
cell_length: usize,
number_cell: usize,
pos: models::Position,
skip_with: std::collections::HashMap<usize, &str>,
) {
// Left side
let ls = match pos {
models::Position::Top => models::TabChar::Jtl.val(),
models::Position::Middle => models::TabChar::Jl.val(),
models::Position::Bottom => models::TabChar::Jbl.val(),
};
// Middle
let ms = match pos {
models::Position::Top => models::TabChar::Jtb.val(),
models::Position::Middle => models::TabChar::Jm.val(),
models::Position::Bottom => models::TabChar::Jtt.val(),
};
// Right side
let rs = match pos {
models::Position::Top => models::TabChar::Jtr.val(),
models::Position::Middle => models::TabChar::Jr.val(),
models::Position::Bottom => models::TabChar::Jbr.val(),
};
// Right side before big cell
let rs_bbc = models::TabChar::Jr.val();
// Right side big cell before big cell
let rsbc_bbc = models::TabChar::Bv.val();
// Right side big cell
let rsbc = models::TabChar::Jl.val();
let line = models::TabChar::Bh.val().to_string().repeat(cell_length);
let line_h = models::TabChar::Bh
.val()
.to_string()
.repeat(cell_length_hours);
// Hours column
match skip_with.get(&0) {
Some(_) => print!("\n{}{}{}", ls, line_h, rs_bbc),
None => print!("\n{}{}{}", ls, line_h, ms),
};
// Courses columns
let range = number_cell - 1;
let mut last_day = false;
for i in 0..range {
// Check if it's a big cell
if i == range - 1 {
// Friday only
if let Some(text) = skip_with.get(&i) {
println!("{:^cell_length$}{}", text, rsbc_bbc);
last_day = true;
}
/// Get the current semester depending on the current date
pub fn get_semester(semester: Option<i8>) -> i8 {
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 {
match skip_with.get(&i) {
Some(text) => match skip_with.get(&(i + 1)) {
// Match check if the next cell will be big
Some(_) => print!("{:^cell_length$}{}", text, rsbc_bbc),
None => print!("{:^cell_length$}{}", text, rsbc),
},
None => match skip_with.get(&(i + 1)) {
// Match check if the next cell will be big
Some(_) => print!("{}{}", line, rs_bbc),
None => print!("{}{}", line, ms),
},
// from january to june
2
}
}
}
if !last_day {
println!("{}{}", line, rs);
}
}
// Split a string in half with respect of words
pub fn split_half(text: &str) -> (&str, &str) {
let mid = text.len() / 2;
for (i, j) in (mid..text.len()).enumerate() {
if text.as_bytes()[j] == b' ' {
return text.split_at(mid + i);
}
}
/// Get the current year depending on the current date
pub 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(),
};
text.split_at(mid)
if semester == 1 {
format!("{}-{}", wanted_year, wanted_year + 1)
} else {
format!("{}-{}", wanted_year - 1, wanted_year)
}
}
// Reduce size of string by adding etc. to it, and cutting some info
pub fn etc_str(text: &str) -> String {
format!("{}...", split_half(text).0.trim())
pub trait Capitalize {
/// Capitalize string
fn capitalize(&self) -> String;
}
// Capitalize string
pub fn capitalize(text: &mut str) -> String {
if let Some(r) = text.get_mut(0..1) {
impl Capitalize for str {
fn capitalize(&self) -> String {
let mut string = self.to_owned();
if let Some(r) = string.get_mut(0..1) {
r.make_ascii_uppercase();
}
text.to_string()
string
}
}
/// Get all hours used the source, from 08:00 to at least 20:00
pub fn get_hours() -> Arc<[String]> {
let mut hours = vec![];
for hour in 8..=20 {
for minute in &[0, 15, 30, 45] {
let hour_str = format!("{hour}h{minute:02}");
if let Some(last_hour) = hours.pop() {
hours.push(format!("{last_hour}-{hour_str}"));
}
hours.push(hour_str);
}
}
for _ in 0..4 {
hours.pop();
}
hours.into()
}
/// Names showed to the users
pub fn get_selection(data: &(&Course, String)) -> String {
let hours = get_hours();
format!(
"{} - {} {}-{}",
data.0.name,
data.1,
hours[data.0.start].split_once('-').unwrap().0,
hours[data.0.start + data.0.size - 1]
.split_once('-')
.unwrap()
.1
)
}
/// Entry's name used for finding duplicates
pub fn get_entry(course: &Course) -> String {
format!("{} - {:?}", course.name, course.category)
}
/// Entry's name used for finding duplicates, ignoring categories
pub fn get_entry_nocat(course: &Course) -> String {
course.name.clone()
}
/// Returns a couple of (list of courses) and (a hashmap of how much they appears in the vector)
pub fn get_count<'a>(
timetable: &'a mut Timetable,
allowed_list: &'a [Category],
getter: fn(&Course) -> String,
) -> (Vec<(&'a Course, String)>, HashMap<String, i32>) {
// List of courses who will be courses
let mut courses = vec![];
let mut counts = HashMap::new();
timetable.1 .1.iter().for_each(|day| {
day.courses.iter().for_each(|course_opt| {
if let Some(course) = course_opt {
if course
.category
.iter()
.any(|category| allowed_list.contains(category))
{
courses.push((course, day.name.clone()));
let count = counts.entry(getter(course)).or_insert(0);
*count += 1;
}
}
});
});
(courses, counts)
}
pub fn format_time_slot(start: usize, size: usize) -> String {
let start_hour = 8 + (start * 15) / 60;
let start_minute = (start * 15) % 60;
let end_hour = start_hour + (size * 15) / 60;
let end_minute = (start_minute + (size * 15)) % 60;
format!("{start_hour:02}h{start_minute:02}-{end_hour:02}h{end_minute:02}")
}

View file

@ -1,51 +1,18 @@
/// Collection of char for the table
pub enum TabChar {
/// Vertical bar
Bv,
/// Horizontal bar
Bh,
/// Joint left
Jl,
/// Joint right
Jr,
/// Joint bottom left
Jbl,
/// Joint bottom right
Jbr,
/// Joint top left
Jtl,
/// Joint top right
Jtr,
/// Joint to top
Jtt,
/// Joint to bottom
Jtb,
/// Joint of the middle
Jm,
use std::collections::HashMap;
use chrono::Utc;
pub type InfoList = Vec<(chrono::DateTime<Utc>, i64)>;
pub struct InfoType {
pub course: InfoList,
pub td_tp: InfoList,
}
impl TabChar {
/// Value of the element
pub fn val(&self) -> char {
match *self {
Self::Bv => '│',
Self::Bh => '─',
Self::Jl => '├',
Self::Jr => '┤',
Self::Jbl => '└',
Self::Jbr => '┘',
Self::Jtl => '┌',
Self::Jtr => '┐',
Self::Jtt => '┴',
Self::Jtb => '┬',
Self::Jm => '┼',
}
}
}
/// Position for lines inside the table
pub enum Position {
Top,
Middle,
Bottom
}
// Info who old the start and end of courses
pub type Info = HashMap<
// Semester
usize,
// List of start and repetition of course and TD/TP weeks
InfoType,
>;