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