From b6be28b2a28ac76ec41c487021bf81de23cbd7b5 Mon Sep 17 00:00:00 2001 From: marcantoinem <marc-antoine.m@outlook.com> Date: Sun, 25 Aug 2024 03:22:20 -0400 Subject: [PATCH] Reintroduce ICS generation --- aep-schedule-generator/Cargo.lock | 63 +++++---- aep-schedule-generator/Cargo.toml | 4 +- aep-schedule-generator/alternance.csv | 130 +++++++++--------- .../src/algorithm/schedule.rs | 49 +------ .../src/algorithm/taken_course.rs | 12 +- aep-schedule-generator/src/bin/ics.rs | 12 +- .../src/data/time/calendar.rs | 56 -------- aep-schedule-generator/src/data/time/day.rs | 21 +++ aep-schedule-generator/src/data/time/mod.rs | 1 - .../src/icalendar/calendar.rs | 83 +++++++++++ aep-schedule-generator/src/icalendar/dates.rs | 108 +++++++++++++++ .../src/icalendar/dates_builder.rs | 60 ++++++++ aep-schedule-generator/src/icalendar/mod.rs | 3 + aep-schedule-generator/src/lib.rs | 1 + aep-schedule-website/Cargo.lock | 25 +++- aep-schedule-website/alternance.csv | 4 +- aep-schedule-website/src/backend/routes.rs | 9 +- aep-schedule-website/src/backend/state.rs | 2 +- .../src/frontend/components/schedule.rs | 21 +-- aep-schedule-website/style/main.scss | 2 +- 20 files changed, 432 insertions(+), 234 deletions(-) delete mode 100644 aep-schedule-generator/src/data/time/calendar.rs create mode 100644 aep-schedule-generator/src/icalendar/calendar.rs create mode 100644 aep-schedule-generator/src/icalendar/dates.rs create mode 100644 aep-schedule-generator/src/icalendar/dates_builder.rs create mode 100644 aep-schedule-generator/src/icalendar/mod.rs diff --git a/aep-schedule-generator/Cargo.lock b/aep-schedule-generator/Cargo.lock index 1a4b44b..963b51e 100644 --- a/aep-schedule-generator/Cargo.lock +++ b/aep-schedule-generator/Cargo.lock @@ -9,7 +9,7 @@ dependencies = [ "chrono", "compact_str", "criterion", - "ical", + "icalendar", "log", "rand", "serde", @@ -101,6 +101,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets", ] @@ -258,8 +259,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -302,12 +305,14 @@ dependencies = [ ] [[package]] -name = "ical" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7cab7543a8b7729a19e2c04309f902861293dcdae6558dfbeb634454d279f6" +name = "icalendar" +version = "0.16.3" +source = "git+https://github.com/marcantoinem/icalendar-rs?branch=fix/wrapping-behaviour#2827a053f6fa62453643e35691b6e024bafed16e" dependencies = [ - "thiserror", + "chrono", + "iso8601", + "nom", + "uuid", ] [[package]] @@ -321,6 +326,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "iso8601" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" +dependencies = [ + "nom", +] + [[package]] name = "itertools" version = "0.10.5" @@ -363,6 +377,22 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -584,26 +614,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "thiserror" -version = "1.0.62" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.62" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tinytemplate" version = "1.2.1" @@ -627,6 +637,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", + "wasm-bindgen", ] [[package]] diff --git a/aep-schedule-generator/Cargo.toml b/aep-schedule-generator/Cargo.toml index 60d7e46..a4e93b8 100644 --- a/aep-schedule-generator/Cargo.toml +++ b/aep-schedule-generator/Cargo.toml @@ -26,8 +26,8 @@ harness = false [dependencies] compact_str = { version = "0.8", features = ["serde"] } serde = { version = "1.0.187", features = ["derive", "rc"] } -ical = { version = "0.11.0", features = ["ical", "vcard", "generator"] } -chrono = "0.4.31" +icalendar = { git = "https://github.com/marcantoinem/icalendar-rs", branch = "fix/wrapping-behaviour" } +chrono = { version = "0.4.38", features = ["serde"] } uuid = { version = "1.6.1", features = ["v4"] } rand = { version = "0.8.5", features = ["std_rng"] } log = "0.4.21" diff --git a/aep-schedule-generator/alternance.csv b/aep-schedule-generator/alternance.csv index e9d808d..d558439 100644 --- a/aep-schedule-generator/alternance.csv +++ b/aep-schedule-generator/alternance.csv @@ -1,66 +1,66 @@ date,day,semaine -2024-01-08,LUNDI,B1 -2024-01-09,MARDI,B1 -2024-01-10,MERCREDI,B1 -2024-01-11,JEUDI,B1 -2024-01-12,VENDREDI,B1 -2024-01-15,LUNDI,B2 -2024-01-16,MARDI,B2 -2024-01-17,MERCREDI,B2 -2024-01-18,JEUDI,B2 -2024-01-19,VENDREDI,B2 -2024-01-22,LUNDI,B1 -2024-01-23,MARDI,B1 -2024-01-24,MERCREDI,B1 -2024-01-25,JEUDI,B1 -2024-01-26,VENDREDI,B1 -2024-01-29,LUNDI,B2 -2024-01-30,MARDI,B2 -2024-01-31,MERCREDI,B2 -2024-02-01,JEUDI,B2 -2024-02-02,VENDREDI,B2 -2024-02-05,LUNDI,B1 -2024-02-06,MARDI,B1 -2024-02-07,MERCREDI,B1 -2024-02-08,JEUDI,B1 -2024-02-09,VENDREDI,B1 -2024-02-12,LUNDI,B2 -2024-02-13,MARDI,B2 -2024-02-14,MERCREDI,B2 -2024-02-15,JEUDI,B2 -2024-02-16,VENDREDI,B2 -2024-02-19,LUNDI,B1 -2024-02-20,MARDI,B1 -2024-02-21,MERCREDI,B1 -2024-02-22,JEUDI,B1 -2024-02-23,VENDREDI,B1 -2024-02-26,LUNDI,B2 -2024-02-27,MARDI,B2 -2024-02-28,MERCREDI,B2 -2024-02-29,JEUDI,B2 -2024-03-01,VENDREDI,B2 -2024-03-11,LUNDI,B1 -2024-03-12,MARDI,B1 -2024-03-13,MERCREDI,B1 -2024-03-14,JEUDI,B1 -2024-03-15,VENDREDI,B1 -2024-03-18,LUNDI,B2 -2024-03-19,MARDI,B2 -2024-03-20,MERCREDI,B2 -2024-03-21,JEUDI,B2 -2024-03-22,VENDREDI,B2 -2024-03-25,LUNDI,B1 -2024-03-26,MARDI,B1 -2024-03-27,MERCREDI,B1 -2024-03-28,JEUDI,B1 -2024-04-02,MARDI,B2 -2024-04-03,MERCREDI,B2 -2024-04-04,JEUDI,B2 -2024-04-05,VENDREDI,B1 -2024-04-08,LUNDI,B2 -2024-04-09,MARDI,B1 -2024-04-10,MERCREDI,B1 -2024-04-11,JEUDI,B1 -2024-04-12,VENDREDI,B2 -2024-04-15,LUNDI,B1 -2024-04-16,MARDI,B1 +2024-08-26,LUNDI,B1 +2024-08-27,MARDI,B1 +2024-08-28,MERCREDI,B1 +2024-08-29,JEUDI,B1 +2024-08-30,VENDREDI,B1 +2024-09-03,MARDI,B2 +2024-09-04,MERCREDI,B2 +2024-09-05,JEUDI,B2 +2024-09-06,VENDREDI,B2 +2024-09-09,LUNDI,B2 +2024-09-10,MARDI,B1 +2024-09-11,MERCREDI,B1 +2024-09-12,JEUDI,B1 +2024-09-13,VENDREDI,B1 +2024-09-16,LUNDI,B1 +2024-09-17,MARDI,B2 +2024-09-18,MERCREDI,B2 +2024-09-19,JEUDI,B2 +2024-09-20,VENDREDI,B2 +2024-09-23,LUNDI,B2 +2024-09-24,MARDI,B1 +2024-09-25,MERCREDI,B1 +2024-09-26,JEUDI,B1 +2024-09-27,VENDREDI,B1 +2024-10-01,LUNDI,B1 +2024-10-02,MERCREDI,B2 +2024-10-03,JEUDI,B2 +2024-10-04,VENDREDI,B2 +2024-10-07,LUNDI,B2 +2024-10-08,MARDI,B2 +2024-10-09,MERCREDI,B1 +2024-10-10,JEUDI,B1 +2024-10-11,VENDREDI,B1 +2024-10-21,LUNDI,B1 +2024-10-22,MARDI,B1 +2024-10-23,MERCREDI,B2 +2024-10-24,JEUDI,B2 +2024-10-25,VENDREDI,B2 +2024-10-28,LUNDI,B2 +2024-10-29,MARDI,B2 +2024-10-30,MERCREDI,B1 +2024-10-31,JEUDI,B1 +2024-11-01,VENDREDI,B1 +2024-11-04,LUNDI,B1 +2024-11-05,MARDI,B1 +2024-11-06,MERCREDI,B2 +2024-11-07,JEUDI,B2 +2024-11-08,VENDREDI,B2 +2024-11-11,LUNDI,B2 +2024-11-12,MARDI,B2 +2024-11-13,MERCREDI,B1 +2024-11-14,JEUDI,B1 +2024-11-15,VENDREDI,B1 +2024-11-18,LUNDI,B1 +2024-11-19,MARDI,B1 +2024-11-20,MERCREDI,B2 +2024-11-21,JEUDI,B2 +2024-11-22,VENDREDI,B2 +2024-11-25,LUNDI,B2 +2024-11-26,MARDI,B2 +2024-11-27,MERCREDI,B1 +2024-11-28,JEUDI,B1 +2024-11-29,VENDREDI,B1 +2024-12-02,LUNDI,B1 +2024-12-03,MARDI,B1 diff --git a/aep-schedule-generator/src/algorithm/schedule.rs b/aep-schedule-generator/src/algorithm/schedule.rs index 1584717..c971593 100644 --- a/aep-schedule-generator/src/algorithm/schedule.rs +++ b/aep-schedule-generator/src/algorithm/schedule.rs @@ -5,24 +5,12 @@ use super::{ }; use crate::data::{ course::Course, - time::{ - calendar::{date_to_timestamp, Calendar}, - day::Day, - hours::NO_HOUR, - period::Period, - weeks::Weeks, - }, -}; -use ical::{ - generator::{Emitter, IcalCalendarBuilder, IcalEventBuilder}, - ical_property, - property::Property, + time::{day::Day, hours::NO_HOUR, period::Period, weeks::Weeks}, }; use std::{ cmp::Ordering, hash::{DefaultHasher, Hash, Hasher}, }; -use uuid::Uuid; #[derive(PartialEq, Debug, Clone)] pub struct ScheduleBuilder<'a> { @@ -148,38 +136,3 @@ pub struct Schedule { pub last_day: u8, pub id: u64, } - -impl Schedule { - pub fn generate_ics(&self, calendar: &Calendar) -> String { - let mut cal = IcalCalendarBuilder::version("2.0") - .gregorian() - .prodid("-//ical-rs//github.com//") - .build(); - - for course in self.taken_courses.iter() { - course.for_each_group(|g| { - for p in g.periods.iter() { - calendar.iter_apply(p.week_nb, p.day, |d| { - let start = - date_to_timestamp(&d, p.hours.starting_hour(), p.hours.start_minutes()); - let end = - date_to_timestamp(&d, p.hours.last_hour(), p.hours.last_minutes()); - let event = IcalEventBuilder::tzid("America/New_York") - .uid(Uuid::new_v4()) - .changed(chrono::Local::now().format("%Y%m%dT%H%M%S").to_string()) - .start(start) - .end(end) - .set(ical_property!( - "SUMMARY", - format!("Laboratoire {}", course.sigle) - )) - .set(ical_property!("DESCRIPTION", p.room.to_string())) - .build(); - cal.events.push(event); - }); - } - }); - } - cal.generate() - } -} diff --git a/aep-schedule-generator/src/algorithm/taken_course.rs b/aep-schedule-generator/src/algorithm/taken_course.rs index a7968e3..1c3bae7 100644 --- a/aep-schedule-generator/src/algorithm/taken_course.rs +++ b/aep-schedule-generator/src/algorithm/taken_course.rs @@ -1,7 +1,7 @@ -use crate::data::course::Course; use crate::data::course_type::CourseType; use crate::data::group::Group; use crate::data::group_index::GroupIndex; +use crate::data::{course::Course, group_sigle::GroupType}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -114,10 +114,10 @@ impl TakenCourse { _ => None, } } - pub fn for_each_group(&self, mut function: impl FnMut(&Group)) { + pub fn for_each_group(&self, mut function: impl FnMut(&Group, GroupType)) { match &self.taken_course_type { - TakenCourseType::LabOnly { lab_group } => function(lab_group), - TakenCourseType::TheoOnly { theo_group } => function(theo_group), + TakenCourseType::LabOnly { lab_group } => function(lab_group, GroupType::LabGroup), + TakenCourseType::TheoOnly { theo_group } => function(theo_group, GroupType::TheoGroup), TakenCourseType::Linked { theo_group, lab_group, @@ -126,8 +126,8 @@ impl TakenCourse { theo_group, lab_group, } => { - function(theo_group); - function(lab_group) + function(theo_group, GroupType::TheoGroup); + function(lab_group, GroupType::LabGroup) } } } diff --git a/aep-schedule-generator/src/bin/ics.rs b/aep-schedule-generator/src/bin/ics.rs index d04106c..07580c1 100644 --- a/aep-schedule-generator/src/bin/ics.rs +++ b/aep-schedule-generator/src/bin/ics.rs @@ -1,10 +1,7 @@ use aep_schedule_generator::{ algorithm::{generation::SchedulesOptions, scores::EvaluationOption}, - data::{ - course::Course, - courses::Courses, - time::{calendar::Calendar, week::Week}, - }, + data::{course::Course, courses::Courses, time::week::Week}, + icalendar::calendar::Calendar, }; use std::{fs::File, io::BufReader}; @@ -36,10 +33,11 @@ fn main() { let result = options.get_schedules().into_sorted_vec(); let result = result.first().unwrap(); - println!("{:?}", result); let days = BufReader::new(File::open("alternance.csv").unwrap()); let calendar = Calendar::from_csv(days); - let _ = std::fs::write("test.ics", result.generate_ics(&calendar)); + println!("{:#?}", calendar); + + let _ = std::fs::write("test.ics", calendar.generate_ics(&result)); } diff --git a/aep-schedule-generator/src/data/time/calendar.rs b/aep-schedule-generator/src/data/time/calendar.rs deleted file mode 100644 index 23a3549..0000000 --- a/aep-schedule-generator/src/data/time/calendar.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::{array, io::BufRead}; - -use serde::{Deserialize, Serialize}; - -use super::{day::Day, week_number::WeekNumber}; - -type Date = String; - -#[derive(Clone, Default, Debug, Serialize, Deserialize)] -pub struct Calendar { - weeks: [[Vec<Date>; 7]; 2], -} - -impl Calendar { - pub fn from_csv(days: impl BufRead) -> Self { - let mut days = days.lines(); - days.next(); - let mut calendar = Calendar::default(); - - for day in days { - let day = &day.unwrap(); - let mut day = day.split(","); - let [Some(date), Some(day), Some(b_type)] = array::from_fn(|_| day.next()) else { - continue; - }; - let week: WeekNumber = b_type.into(); - let day: Day = day[0..3].into(); - calendar.push(date.to_string(), week, day); - } - calendar - } - /// There should be no day that are both B1 and B2 in the calendar - pub fn push(&mut self, date: Date, week: WeekNumber, day: Day) { - self.weeks[week as usize][day as usize].push(date); - } - pub fn iter_apply(&self, week: WeekNumber, day: Day, mut function: impl FnMut(&Date)) { - if week == WeekNumber::Both { - for date in self.weeks[0][day as usize] - .iter() - .chain(self.weeks[1][day as usize].iter()) - { - function(date); - } - return; - } - for date in self.weeks[week as usize][day as usize].iter() { - function(date); - } - } -} - -pub fn date_to_timestamp(date: &str, hours: u8, minutes: u8) -> String { - let mut timestamp: String = date.chars().filter(|c| c.is_ascii_alphanumeric()).collect(); - timestamp.push_str(&format!("T{:0>2}{:0>2}00", hours, minutes)); - timestamp -} diff --git a/aep-schedule-generator/src/data/time/day.rs b/aep-schedule-generator/src/data/time/day.rs index 1771247..48a63a8 100644 --- a/aep-schedule-generator/src/data/time/day.rs +++ b/aep-schedule-generator/src/data/time/day.rs @@ -1,5 +1,6 @@ use std::fmt::Display; +use chrono::Weekday; use serde::{Deserialize, Serialize}; // There is no course the saturday at Poly, but knowing them, it wouldn't be far @@ -16,6 +17,20 @@ pub enum Day { Saturday = 6, } +impl From<u8> for Day { + fn from(value: u8) -> Self { + match value { + 0 => Self::Monday, + 1 => Self::Tuesday, + 2 => Self::Wednesday, + 3 => Self::Thursday, + 4 => Self::Friday, + 5 => Self::Sunday, + _ => Self::Saturday, + } + } +} + impl From<&str> for Day { fn from(value: &str) -> Self { match value { @@ -44,3 +59,9 @@ impl Display for Day { } } } + +impl PartialEq<Weekday> for Day { + fn eq(&self, other: &Weekday) -> bool { + *self as u8 == *other as u8 + } +} diff --git a/aep-schedule-generator/src/data/time/mod.rs b/aep-schedule-generator/src/data/time/mod.rs index 3e6ae30..ddff066 100644 --- a/aep-schedule-generator/src/data/time/mod.rs +++ b/aep-schedule-generator/src/data/time/mod.rs @@ -4,4 +4,3 @@ pub mod period; pub mod week; pub mod week_number; pub mod weeks; -pub mod calendar; \ No newline at end of file diff --git a/aep-schedule-generator/src/icalendar/calendar.rs b/aep-schedule-generator/src/icalendar/calendar.rs new file mode 100644 index 0000000..3500653 --- /dev/null +++ b/aep-schedule-generator/src/icalendar/calendar.rs @@ -0,0 +1,83 @@ +use std::{array, io::BufRead}; + +use serde::{Deserialize, Serialize}; + +use crate::{ + algorithm::schedule::Schedule, + data::time::{day::Day, week_number::WeekNumber}, +}; + +use super::{dates::Dates, dates_builder::DatesBuilder}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Calendar { + weeks: [[Dates; 7]; 2], + both: [Dates; 7], +} + +impl Calendar { + pub fn from_csv(days: impl BufRead) -> Self { + let mut days = days.lines(); + let mut weeks: [[DatesBuilder; 7]; 2] = Default::default(); + let mut both: [DatesBuilder; 7] = Default::default(); + + let mut session_start = String::new(); + let mut session_end = String::new(); + days.next(); + for day in days { + let day = &day.unwrap(); + let mut day = day.split(","); + let [Some(date), Some(day), Some(b_type)] = array::from_fn(|_| day.next()) else { + continue; + }; + if session_start.is_empty() { + session_start = date.to_string(); + } + session_end = date.to_string(); + let week: WeekNumber = b_type.into(); + let day: Day = day[0..3].into(); + + weeks[week as usize][day as usize].add_date(date); + both[day as usize].add_date(date); + } + + let weeks = weeks.map(|week| { + let mut i = 0; + week.map(|d| { + let d = d.build(&session_start, &session_end, (i as u8).into()); + i += 1; + d + }) + }); + + let mut i = 0; + + let both = both.map(|d| { + let d = d.build(&session_start, &session_end, (i as u8).into()); + i += 1; + d + }); + Self { weeks, both } + } + + pub fn generate_ics(&self, schedule: &Schedule) -> String { + let mut cal = icalendar::Calendar::new(); + cal.name("horaire"); + + for course in schedule.taken_courses.iter() { + course.for_each_group(|g, group_type| { + for p in g.periods.iter() { + match p.week_nb { + WeekNumber::B1 | WeekNumber::B2 => self.weeks[p.week_nb as usize] + [p.day as usize] + .push_events(&mut cal, course, p, group_type), + WeekNumber::Both => { + self.both[p.day as usize].push_events(&mut cal, course, p, group_type) + } + } + } + }); + } + cal.done().to_string() + } +} diff --git a/aep-schedule-generator/src/icalendar/dates.rs b/aep-schedule-generator/src/icalendar/dates.rs new file mode 100644 index 0000000..f117495 --- /dev/null +++ b/aep-schedule-generator/src/icalendar/dates.rs @@ -0,0 +1,108 @@ +use chrono::{NaiveDate, NaiveDateTime}; +use icalendar::{Calendar, Component, Event, EventLike}; +use serde::{Deserialize, Serialize}; + +use crate::algorithm::taken_course::TakenCourse; +use crate::data::group_sigle::GroupType; +use crate::data::time::period::Period; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum Dates { + Week(Vec<NaiveDate>), + Weekend { + session_start: NaiveDate, + session_end: NaiveDate, + }, +} + +const NAIVE_DATE_TIME_FORMAT: &str = "%Y%m%dT%H%M%S"; + +impl Dates { + pub fn push_events( + &self, + cal: &mut Calendar, + course: &TakenCourse, + p: &Period, + group_type: GroupType, + ) { + let labo = match group_type { + GroupType::LabGroup => "Laboratoire", + GroupType::TheoGroup => "Théorie", + }; + + let mut main = Event::new(); + + main.summary(&format!("{} {}", labo, course.sigle)) + .description(p.room.as_str()) + .location(p.room.as_str()); + + match self { + Dates::Week(all_dates) => { + let session_start = all_dates[0]; + let start = session_start + .and_hms_opt( + p.hours.starting_hour() as u32, + p.hours.start_minutes() as u32, + 0, + ) + .unwrap(); + let end = session_start + .and_hms_opt(p.hours.last_hour() as u32, p.hours.last_minutes() as u32, 0) + .unwrap(); + + main.starts(start).ends(end); + + if all_dates.len() > 1 { + let start_dates: Vec<NaiveDateTime> = all_dates + .into_iter() + .map(|d| { + d.and_hms_opt( + p.hours.starting_hour() as u32, + p.hours.start_minutes() as u32, + 0, + ) + .unwrap() + }) + .collect(); + + let mut rdate = start_dates[0].format(NAIVE_DATE_TIME_FORMAT).to_string(); + for date in start_dates[1..].iter() { + let date = date.format(NAIVE_DATE_TIME_FORMAT); + rdate.push(','); + rdate.push_str(&date.to_string()); + } + + main.add_property("RDATE", &rdate); + } + } + Dates::Weekend { + session_start, + session_end, + } => { + let start = session_start + .and_hms_opt( + p.hours.starting_hour() as u32, + p.hours.start_minutes() as u32, + 0, + ) + .unwrap(); + let end = session_start + .and_hms_opt(p.hours.last_hour() as u32, p.hours.last_minutes() as u32, 0) + .unwrap(); + let last = session_end + .and_hms_opt( + p.hours.starting_hour() as u32, + p.hours.start_minutes() as u32, + 0, + ) + .unwrap() + .format(NAIVE_DATE_TIME_FORMAT); + let rrule = format!("FREQ=WEEKLY;UNTIL={}", last); + + main.starts(start).ends(end).add_property("RRULE", &rrule); + } + } + + cal.push(main.done()); + } +} diff --git a/aep-schedule-generator/src/icalendar/dates_builder.rs b/aep-schedule-generator/src/icalendar/dates_builder.rs new file mode 100644 index 0000000..f600279 --- /dev/null +++ b/aep-schedule-generator/src/icalendar/dates_builder.rs @@ -0,0 +1,60 @@ +use chrono::{Datelike, NaiveDate}; + +use crate::data::time::day::Day; + +use super::dates::Dates; + +#[derive(Default)] +pub struct DatesBuilder { + pub dates: Vec<NaiveDate>, +} + +impl DatesBuilder { + // Always add date in chronological order + pub fn add_date(&mut self, date: &str) { + let date = NaiveDate::parse_from_str(date, "%Y-%m-%d").unwrap(); + + self.dates.push(date); + } + + fn new_weekend(session_start: &str, session_end: &str, day: Day) -> Dates { + let session_start = find_next_weekday(&session_start, day); + let session_end = find_last_weekday(&session_end, day); + Dates::Weekend { + session_start, + session_end, + } + } + + pub fn build(self, session_start: &str, session_end: &str, day: Day) -> Dates { + let is_empty = self.dates.is_empty(); + match is_empty { + false => Dates::Week(self.dates), + _ => Self::new_weekend(session_start, session_end, day), + } + } +} + +fn find_next_weekday(first_date: &str, next_day: Day) -> NaiveDate { + let first_date = NaiveDate::parse_from_str(first_date, "%Y-%m-%d").unwrap(); + let mut date = next_day as i8 - first_date.weekday() as i8; + if date < 0 { + date += 7; + } + let fixed = first_date + .checked_add_days(chrono::Days::new(date as u64)) + .unwrap(); + fixed +} + +fn find_last_weekday(first_date: &str, next_day: Day) -> NaiveDate { + let first_date = NaiveDate::parse_from_str(first_date, "%Y-%m-%d").unwrap(); + let mut date = next_day as i8 - first_date.weekday() as i8; + if date > 0 { + date -= 7; + } + let fixed = first_date + .checked_sub_days(chrono::Days::new(-date as u64)) + .unwrap(); + fixed +} diff --git a/aep-schedule-generator/src/icalendar/mod.rs b/aep-schedule-generator/src/icalendar/mod.rs new file mode 100644 index 0000000..916728c --- /dev/null +++ b/aep-schedule-generator/src/icalendar/mod.rs @@ -0,0 +1,3 @@ +pub mod calendar; +pub mod dates; +pub mod dates_builder; diff --git a/aep-schedule-generator/src/lib.rs b/aep-schedule-generator/src/lib.rs index fb4722a..296165e 100644 --- a/aep-schedule-generator/src/lib.rs +++ b/aep-schedule-generator/src/lib.rs @@ -1,3 +1,4 @@ pub mod algorithm; pub mod data; +pub mod icalendar; pub mod tests; diff --git a/aep-schedule-website/Cargo.lock b/aep-schedule-website/Cargo.lock index ce4ba56..25d6c70 100644 --- a/aep-schedule-website/Cargo.lock +++ b/aep-schedule-website/Cargo.lock @@ -65,7 +65,7 @@ version = "0.1.0" dependencies = [ "chrono", "compact_str", - "ical", + "icalendar", "log", "rand", "serde", @@ -461,6 +461,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.52.6", ] @@ -1417,12 +1418,14 @@ dependencies = [ ] [[package]] -name = "ical" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7cab7543a8b7729a19e2c04309f902861293dcdae6558dfbeb634454d279f6" +name = "icalendar" +version = "0.16.3" +source = "git+https://github.com/marcantoinem/icalendar-rs?branch=fix/wrapping-behaviour#2827a053f6fa62453643e35691b6e024bafed16e" dependencies = [ - "thiserror", + "chrono", + "iso8601", + "nom", + "uuid", ] [[package]] @@ -1487,6 +1490,15 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "iso8601" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" +dependencies = [ + "nom", +] + [[package]] name = "itertools" version = "0.12.1" @@ -3502,6 +3514,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", + "wasm-bindgen", ] [[package]] diff --git a/aep-schedule-website/alternance.csv b/aep-schedule-website/alternance.csv index d1dbbda..d558439 100644 --- a/aep-schedule-website/alternance.csv +++ b/aep-schedule-website/alternance.csv @@ -23,7 +23,7 @@ date,day,semaine 2024-09-25,MERCREDI,B1 2024-09-26,JEUDI,B1 2024-09-27,VENDREDI,B1 -2024-10-01,MARDI,B1 +2024-10-01,LUNDI,B1 2024-10-02,MERCREDI,B2 2024-10-03,JEUDI,B2 2024-10-04,VENDREDI,B2 @@ -63,4 +63,4 @@ date,day,semaine 2024-11-28,JEUDI,B1 2024-11-29,VENDREDI,B1 2024-12-02,LUNDI,B1 -2024-12-03,MARDI,B1 \ No newline at end of file +2024-12-03,MARDI,B1 diff --git a/aep-schedule-website/src/backend/routes.rs b/aep-schedule-website/src/backend/routes.rs index c2d2479..27f6ef6 100644 --- a/aep-schedule-website/src/backend/routes.rs +++ b/aep-schedule-website/src/backend/routes.rs @@ -1,6 +1,9 @@ -use aep_schedule_generator::data::{ - course::{Course, CourseName}, - time::{calendar::Calendar, period::PeriodCourse}, +use aep_schedule_generator::{ + data::{ + course::{Course, CourseName}, + time::period::PeriodCourse, + }, + icalendar::calendar::Calendar, }; use compact_str::CompactString; use leptos::*; diff --git a/aep-schedule-website/src/backend/state.rs b/aep-schedule-website/src/backend/state.rs index 391be8e..453eb8c 100644 --- a/aep-schedule-website/src/backend/state.rs +++ b/aep-schedule-website/src/backend/state.rs @@ -1,6 +1,6 @@ use crate::frontend::app::App; use aep_schedule_generator::data::courses::Courses; -use aep_schedule_generator::data::time::calendar::Calendar; +use aep_schedule_generator::icalendar::calendar::Calendar; use axum::{ body::Body as AxumBody, extract::FromRef, diff --git a/aep-schedule-website/src/frontend/components/schedule.rs b/aep-schedule-website/src/frontend/components/schedule.rs index 75d5094..1aefa47 100644 --- a/aep-schedule-website/src/frontend/components/schedule.rs +++ b/aep-schedule-website/src/frontend/components/schedule.rs @@ -3,12 +3,13 @@ use std::rc::Rc; use crate::frontend::components::common::schedule::{Schedule, ScheduleEvent}; use crate::frontend::components::icons::download::Download; use crate::frontend::components::icons::IconWeight; +use aep_schedule_generator::icalendar::calendar::Calendar; use aep_schedule_generator::{ algorithm::{ schedule::Schedule, taken_course::{TakenCourse, TakenCourseType}, }, - data::time::{calendar::Calendar, period::Period, week_number::WeekNumber}, + data::time::{period::Period, week_number::WeekNumber}, }; use leptos::{html::A, *}; @@ -134,15 +135,15 @@ pub fn ScheduleComponent(schedule: Schedule, calendar: Rc<Calendar>) -> impl Int <Schedule last_day=schedule.last_day> {schedule.taken_courses.iter().enumerate().map(|(i, c)| view!{<CoursePeriods i course=c />}).collect_view()} </Schedule> - //<button class="button-download flex" on:pointerdown=move |_| { - // let ics = schedule2.generate_ics(&calendar); - // let url = url_escape::encode_fragment(&ics); - // set_download("data:text/plain;charset=utf-8,".to_string() + &url); - // link().unwrap().click(); - //}> - // <Download weight=IconWeight::Regular size="3vh"/> - // <span>"Télécharger le calendrier de cet horaire"</span> - //</button> + <button class="button-download flex" on:pointerdown=move |_| { + let ics = calendar.generate_ics(&schedule2); + let url = url_escape::encode_fragment(&ics); + set_download("data:text/plain;charset=utf-8,".to_string() + &url); + link().unwrap().click(); + }> + <Download weight=IconWeight::Regular size="3vh"/> + <span>"Télécharger le calendrier de cet horaire"</span> + </button> </div> } } diff --git a/aep-schedule-website/style/main.scss b/aep-schedule-website/style/main.scss index 3e989a6..ee495f0 100644 --- a/aep-schedule-website/style/main.scss +++ b/aep-schedule-website/style/main.scss @@ -128,7 +128,7 @@ main { flex-direction: column; align-items: center; overflow-y: auto; - gap: 3rem; + gap: 1rem; padding: 1rem; background-color: $light-background; } -- GitLab