diff --git a/Cargo.lock b/Cargo.lock index 85b95c7..3e35dad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,7 +101,7 @@ dependencies = [ [[package]] name = "auto-frs-schedule" -version = "2.1.4" +version = "2.2.4" dependencies = [ "anyhow", "calamine", diff --git a/Cargo.toml b/Cargo.toml index 623ef05..12bb096 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "auto-frs-schedule" -version = "2.1.4" +version = "2.2.4" edition = "2021" description = "Automatically generate TC FRS schedule from Excel file" authors = ["Mohamad Kholid Bughowi "] diff --git a/src/excel/excel.rs b/src/excel/excel.rs index 6333251..8e60a5d 100644 --- a/src/excel/excel.rs +++ b/src/excel/excel.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; use anyhow::{Context, Result}; use calamine::{open_workbook, Reader, Xlsx}; @@ -6,58 +6,69 @@ use calamine::{open_workbook, Reader, Xlsx}; use super::Excel; impl Excel { - pub fn new(file_path: &PathBuf, sheet_name: &str) -> Result { + pub fn new( + file_path: &PathBuf, + sheet_name: &str, + list_subject: HashMap, + list_lecture: HashMap, + list_session: HashMap, + ) -> Result { let mut excel: Xlsx<_> = open_workbook(file_path).with_context(|| "Cannot open excel file")?; let range = excel .worksheet_range(sheet_name) .context("Error opening sheet, make sure sheet name is exists")? .with_context(|| format!("Could not read excel range from sheet {}", sheet_name))?; - Ok(Self { range }) + Ok(Self { + range, + list_lecture, + list_session, + list_subject, + }) } } -#[cfg(test)] -mod test { - use crate::excel::{Excel, IntoMap}; - use std::collections::HashMap; - - #[test] - fn test_parse_subject() { - let mut subject_map: HashMap = HashMap::new(); - subject_map.insert(String::from("jaringan komputer"), String::from("c636gggdd")); - assert_eq!( - Excel::subject_class_to_map("Jaringan Komputer A", &subject_map), - Some(("c636gggdd".to_string(), "A".to_string())) - ); - - subject_map.insert(String::from("realitas x"), String::from("377hh7cch")); - assert_eq!( - Excel::subject_class_to_map("Realitas X", &subject_map), - Some(("377hh7cch".to_string(), "-".to_string())) - ); - subject_map.insert( - String::from("interaksi manusia komputer"), - String::from("wjjfhhfw888"), - ); - assert_eq!( - Excel::subject_class_to_map("Interaksi Manusia Komputer D - EN", &subject_map), - Some(("wjjfhhfw888".to_string(), "D - EN".to_string())) - ); - assert_eq!( - Excel::subject_class_to_map("Interaksi Manusia Komputer - RKA", &subject_map), - Some(("wjjfhhfw888".to_string(), "RKA".to_string())) - ); - - assert_eq!( - Excel::subject_class_to_map("Jaringan Komputer - IUP", &subject_map), - Some(("c636gggdd".to_string(), "IUP".to_string())) - ); - - subject_map.insert(String::from("dasar pemrograman"), String::from("cc773hhe")); - assert_eq!( - Excel::subject_class_to_map("Dasar Pemrograman F", &subject_map), - Some(("cc773hhe".to_string(), "F".to_string())) - ); - } -} +// #[cfg(test)] +// mod test { +// use crate::excel::{Excel, IntoMap}; +// use std::collections::HashMap; + +// #[test] +// fn test_parse_subject() { +// let mut subject_map: HashMap = HashMap::new(); +// subject_map.insert(String::from("jaringan komputer"), String::from("c636gggdd")); +// assert_eq!( +// Excel::subject_class_to_map("Jaringan Komputer A", &subject_map), +// Some(("c636gggdd".to_string(), "A".to_string())) +// ); + +// subject_map.insert(String::from("realitas x"), String::from("377hh7cch")); +// assert_eq!( +// Excel::subject_class_to_map("Realitas X", &subject_map), +// Some(("377hh7cch".to_string(), "-".to_string())) +// ); +// subject_map.insert( +// String::from("interaksi manusia komputer"), +// String::from("wjjfhhfw888"), +// ); +// assert_eq!( +// Excel::subject_class_to_map("Interaksi Manusia Komputer D - EN", &subject_map), +// Some(("wjjfhhfw888".to_string(), "D - EN".to_string())) +// ); +// assert_eq!( +// Excel::subject_class_to_map("Interaksi Manusia Komputer - RKA", &subject_map), +// Some(("wjjfhhfw888".to_string(), "RKA".to_string())) +// ); + +// assert_eq!( +// Excel::subject_class_to_map("Jaringan Komputer - IUP", &subject_map), +// Some(("c636gggdd".to_string(), "IUP".to_string())) +// ); + +// subject_map.insert(String::from("dasar pemrograman"), String::from("cc773hhe")); +// assert_eq!( +// Excel::subject_class_to_map("Dasar Pemrograman F", &subject_map), +// Some(("cc773hhe".to_string(), "F".to_string())) +// ); +// } +// } diff --git a/src/excel/into_map.rs b/src/excel/into_map.rs index b88501f..71b83e4 100644 --- a/src/excel/into_map.rs +++ b/src/excel/into_map.rs @@ -1,93 +1,44 @@ -use std::collections::HashMap; - -use anyhow::Result; - use crate::repo::Class; -use super::{Excel, IntoMap, DAYS}; +use super::{Excel, IntoMap, Parser, DAYS}; impl IntoMap for Excel { - fn subject_class_to_map( - val: &str, - subject_map: &HashMap, - ) -> Option<(String, String)> { - let splitted = val.split("-").collect::>(); - let subject_name: String; - let code: String; - if splitted.len() < 2 { - let split_space = val.split_ascii_whitespace().collect::>(); - let last_str = split_space.last()?.trim(); - if last_str.len() == 1 && last_str <= "L" { - subject_name = split_space[0..(split_space.len() - 1)].join(" "); - code = last_str.to_string() - } else { - subject_name = split_space.join(" "); - code = "-".to_owned(); - } - } else { - let last_split = splitted.last()?.trim(); - if last_split.contains("EN") { - let split_space = splitted[0].split_ascii_whitespace().collect::>(); - subject_name = split_space[0..(split_space.len() - 1)].join(" "); - code = format!("{} - {}", split_space.last()?, "EN"); - } else { - subject_name = splitted[0].trim().to_owned(); - code = splitted[1].trim().to_owned(); - } - } - match subject_map.get(&subject_name.to_lowercase()) { + fn subject_with_code_to_map(&self, val: &str) -> Option<(String, String)> { + let (subject_name, code) = self.parse_subject_with_code(val)?; + match self.list_subject.get(&subject_name.to_lowercase()) { Some(val) => Some((val.to_string(), code)), None => None, } } - fn session_to_map(&self, row_idx: u32, session_map: &HashMap) -> Option { - let session_name = self - .range - .get_value((row_idx, 1))? - .get_string()? - .split(" - ") - .collect::>()[0]; - match session_map.get(session_name) { + fn session_to_map(&self, row_idx: u32) -> Option { + let session_name = self.parse_session(row_idx)?; + match self.list_session.get(&session_name) { Some(val) => Some(*val), None => None, } } - fn lecturer_to_map( - &self, - row: u32, - col: u32, - lecturer_map: &HashMap, - ) -> Option> { - let lecturer = self - .range - .get_value((row + 1, col))? - .get_string()? - .split("/") - .collect::>()[2]; - let lecturers_id: Vec<_> = lecturer - .split("-") + fn lecturer_to_map(&self, row: u32, col: u32) -> Option> { + let lecturers = self.parse_lecturer(row, col)?; + let lecturers_id: Vec = lecturers + .into_iter() .flat_map(|lecture_code| { - let id = match lecturer_map.get(lecture_code.trim()) { + let id = match self.list_lecture.get(lecture_code.trim()) { Some(code) => code, - None => lecturer_map.get("UNK").unwrap(), + None => self.list_lecture.get("UNK").unwrap(), }; vec![id.to_string()] }) .collect(); + match lecturers_id.is_empty() { true => None, false => Some(lecturers_id), } } - fn parse_excel( - &self, - list_subject: &HashMap, - list_lecture: &HashMap, - list_session: &HashMap, - ) -> Result> { + fn parse_excel(&self) -> Vec { let mut list_class: Vec = Vec::with_capacity(self.range.get_size().1 as usize); for (row_idx, row) in self.range.rows().enumerate() { @@ -96,26 +47,19 @@ impl IntoMap for Excel { Some(val) => val, None => continue, }; - - let (subject_id, class_code) = - match Excel::subject_class_to_map(&val, &list_subject) { - Some(val) => val, - None => continue, - }; - - let lecturers_id = - match self.lecturer_to_map(row_idx as u32, col_idx as u32, &list_lecture) { - Some(val) => val, - None => continue, - }; - + let (subject_id, class_code) = match self.subject_with_code_to_map(&val) { + Some(val) => val, + None => continue, + }; + let lecturers_id = match self.lecturer_to_map(row_idx as u32, col_idx as u32) { + Some(val) => val, + None => continue, + }; let day = DAYS[row_idx / 14]; - - let session_id = match self.session_to_map(row_idx as u32, &list_session) { + let session_id = match self.session_to_map(row_idx as u32) { Some(val) => val, None => continue, }; - list_class.push(Class { matkul_id: subject_id, lecturers_id, @@ -125,7 +69,6 @@ impl IntoMap for Excel { }); } } - - Ok(list_class) + list_class } } diff --git a/src/excel/into_str.rs b/src/excel/into_str.rs index 9d81f4e..0353975 100644 --- a/src/excel/into_str.rs +++ b/src/excel/into_str.rs @@ -1,121 +1,62 @@ -use std::collections::HashMap; - use crate::repo::ClassFromSchedule; -use super::{Excel, IntoStr, DAYS}; +use super::{Excel, IntoStr, Parser, DAYS}; impl IntoStr for Excel { - fn subject_class_to_str( - val: &str, - subject_map: &HashMap, - ) -> Option<(String, String)> { - let splitted = val.split("-").collect::>(); - let subject_name: String; - let code: String; - if splitted.len() < 2 { - let split_space = val.split_ascii_whitespace().collect::>(); - let last_str = split_space.last()?.trim(); - if last_str.len() == 1 && last_str <= "L" { - subject_name = split_space[0..(split_space.len() - 1)].join(" "); - code = last_str.to_string() - } else { - subject_name = split_space.join(" "); - code = "-".to_owned(); - } - } else { - let last_split = splitted.last()?.trim(); - if last_split.contains("EN") { - let split_space = splitted[0].split_ascii_whitespace().collect::>(); - subject_name = split_space[0..(split_space.len() - 1)].join(" "); - code = format!("{} - {}", split_space.last()?, "EN"); - } else { - subject_name = splitted[0].trim().to_owned(); - code = splitted[1].trim().to_owned(); - } - } - match subject_map.contains_key(&subject_name.to_lowercase()) { + fn subject_with_code_to_str(&self, val: &str) -> Option<(String, String)> { + let (subject_name, code) = self.parse_subject_with_code(val)?; + match self.list_subject.contains_key(&subject_name.to_lowercase()) { true => Some((subject_name, code)), false => None, } } - fn lecturer_to_str( - &self, - row: u32, - col: u32, - lecturer_map: &HashMap, - ) -> Option> { - let lecturer = self - .range - .get_value((row + 1, col))? - .get_string()? - .split("/") - .collect::>()[2]; - let lecturers_id: Vec<_> = lecturer - .split("-") + fn lecturer_to_str(&self, row: u32, col: u32) -> Option> { + let lecturers = self.parse_lecturer(row, col)?; + let lecturers_code: Vec = lecturers + .into_iter() .flat_map(|lecture_code| { - let code = match lecturer_map.contains_key(lecture_code.trim()) { + let code = match self.list_lecture.contains_key(lecture_code.trim()) { true => lecture_code.trim().to_string(), false => "UNK".to_string(), }; vec![code.to_string()] }) .collect(); - match lecturers_id.is_empty() { + + match lecturers_code.is_empty() { true => None, - false => Some(lecturers_id), + false => Some(lecturers_code), } } - fn session_to_str(&self, row_idx: u32, session_map: &HashMap) -> Option { - let session_name = self - .range - .get_value((row_idx, 1))? - .get_string()? - .split(" - ") - .collect::>()[0] - .to_string(); - match session_map.contains_key(&session_name) { + fn session_to_str(&self, row_idx: u32) -> Option { + let session_name = self.parse_session(row_idx)?; + match self.list_session.contains_key(&session_name) { true => Some(session_name), false => None, } } - fn updated_schedule_to_str( - &self, - list_subject: &HashMap, - list_lecture: &HashMap, - list_session: &HashMap, - ) -> Vec { - // TODO: parse updated schedule - // expected data to be returned: ClassFromSchedule - - /* - parsing steps: - 1. parse subject name and code - 2. parse lecturer code - 3. parse day - 4. parse session start - */ - let mut list_class: Vec = Vec::new(); + fn updated_schedule_to_str(&self) -> Vec { + let mut list_class: Vec = + Vec::with_capacity(self.range.get_size().1 as usize); for (row_idx, row) in self.range.rows().enumerate() { for (col_idx, c) in row.iter().enumerate() { let val = match c.get_string() { Some(val) => val, None => continue, }; - let (subject_name, class_code) = - match Excel::subject_class_to_str(&val, &list_subject) { - Some(val) => val, - None => continue, - }; - let lecturers = - match self.lecturer_to_str(row_idx as u32, col_idx as u32, &list_lecture) { - Some(val) => val, - None => continue, - }; + let (subject_name, class_code) = match self.subject_with_code_to_str(&val) { + Some(val) => val, + None => continue, + }; + let lecturers = match self.lecturer_to_str(row_idx as u32, col_idx as u32) { + Some(val) => val, + None => continue, + }; let day = DAYS[row_idx / 14]; - let session_start = match self.session_to_str(row_idx as u32, &list_session) { + let session_start = match self.session_to_str(row_idx as u32) { Some(val) => val, None => continue, }; diff --git a/src/excel/mod.rs b/src/excel/mod.rs index 161082b..e0b91a9 100644 --- a/src/excel/mod.rs +++ b/src/excel/mod.rs @@ -1,6 +1,5 @@ use std::collections::HashMap; -use anyhow::Result; use calamine::{DataType, Range}; use crate::repo::{Class, ClassFromSchedule}; @@ -8,49 +7,33 @@ use crate::repo::{Class, ClassFromSchedule}; pub mod excel; pub mod into_map; pub mod into_str; +pub mod parser; pub const DAYS: [&str; 5] = ["Senin", "Selasa", "Rabu", "Kamis", "Jum'at"]; pub struct Excel { range: Range, + list_subject: HashMap, + list_lecture: HashMap, + list_session: HashMap, +} + +pub trait Parser { + fn parse_subject_with_code(&self, val: &str) -> Option<(String, String)>; + fn parse_lecturer(&self, row: u32, col: u32) -> Option>; + fn parse_session(&self, row_idx: u32) -> Option; } pub trait IntoStr { - fn subject_class_to_str( - val: &str, - subject_map: &HashMap, - ) -> Option<(String, String)>; - fn lecturer_to_str( - &self, - row: u32, - col: u32, - lecturer_map: &HashMap, - ) -> Option>; - fn session_to_str(&self, row_idx: u32, session_map: &HashMap) -> Option; - fn updated_schedule_to_str( - &self, - list_subject: &HashMap, - list_lecture: &HashMap, - list_session: &HashMap, - ) -> Vec; + fn subject_with_code_to_str(&self, val: &str) -> Option<(String, String)>; + fn lecturer_to_str(&self, row: u32, col: u32) -> Option>; + fn session_to_str(&self, row_idx: u32) -> Option; + fn updated_schedule_to_str(&self) -> Vec; } pub trait IntoMap { - fn subject_class_to_map( - val: &str, - subject_map: &HashMap, - ) -> Option<(String, String)>; - fn lecturer_to_map( - &self, - row: u32, - col: u32, - lecturer_map: &HashMap, - ) -> Option>; - fn session_to_map(&self, row_idx: u32, session_map: &HashMap) -> Option; - fn parse_excel( - &self, - list_subject: &HashMap, - list_lecture: &HashMap, - list_session: &HashMap, - ) -> Result>; + fn subject_with_code_to_map(&self, val: &str) -> Option<(String, String)>; + fn lecturer_to_map(&self, row: u32, col: u32) -> Option>; + fn session_to_map(&self, row_idx: u32) -> Option; + fn parse_excel(&self) -> Vec; } diff --git a/src/excel/parser.rs b/src/excel/parser.rs new file mode 100644 index 0000000..f17afbd --- /dev/null +++ b/src/excel/parser.rs @@ -0,0 +1,54 @@ +use super::{Excel, Parser}; + +impl Parser for Excel { + fn parse_lecturer(&self, row: u32, col: u32) -> Option> { + let lecturer = self + .range + .get_value((row + 1, col))? + .get_string()? + .split("/") + .collect::>()[2] + .split("-") + .collect(); + Some(lecturer) + } + fn parse_session(&self, row_idx: u32) -> Option { + let session_name = self + .range + .get_value((row_idx, 1))? + .get_string()? + .split(" - ") + .collect::>()[0] + .to_string(); + Some(session_name) + } + + fn parse_subject_with_code(&self, val: &str) -> Option<(String, String)> { + let splitted = val.split("-").collect::>(); + let subject_name: String; + let code: String; + if splitted.len() < 2 { + let split_space = val.split_ascii_whitespace().collect::>(); + let last_str = split_space.last()?.trim(); + if last_str.len() == 1 && last_str <= "L" { + subject_name = split_space[0..(split_space.len() - 1)].join(" "); + code = last_str.to_string() + } else { + subject_name = split_space.join(" "); + code = "-".to_owned(); + } + } else { + let last_split = splitted.last()?.trim(); + if last_split.contains("EN") { + let split_space = splitted[0].split_ascii_whitespace().collect::>(); + subject_name = split_space[0..(split_space.len() - 1)].join(" "); + code = format!("{} - {}", split_space.last()?, "EN"); + } else { + subject_name = splitted[0].trim().to_owned(); + code = splitted[1].trim().to_owned(); + } + } + + Some((subject_name, code)) + } +} diff --git a/src/main.rs b/src/main.rs index e5cc6eb..ce7c072 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,17 +94,16 @@ async fn main() -> Result<()> { outdir, } => { println!("Parse class schedule from Excel"); - let excel = Excel::new(&file, &sheet).with_context(|| { - format!( - "Error opening {} with sheet name '{:?}'", - &file.display(), - &sheet, - ) - })?; - - let list_class: Vec = excel - .parse_excel(&subjects, &lecturers, &sessions) - .with_context(|| "Error parsing excel")?; + let excel = + Excel::new(&file, &sheet, subjects, lecturers, sessions).with_context(|| { + format!( + "Error opening {} with sheet name '{:?}'", + &file.display(), + &sheet, + ) + })?; + + let list_class: Vec = excel.parse_excel(); if *push == true { println!("Insert {} classes to DB", list_class.len()); @@ -122,7 +121,6 @@ async fn main() -> Result<()> { .await .with_context(|| "Error writing output to sql")?; } - println!("Done"); } Commands::Compare { file, @@ -134,9 +132,21 @@ async fn main() -> Result<()> { let mut changed: Vec<(ClassFromSchedule, ClassFromSchedule)> = Vec::new(); let class_repo = ClassRepository::new(&pool); - let mut db_classes = class_repo.get_schedule().await?; - let excel = Excel::new(&file, &sheet)?; - let excel_classes = excel.updated_schedule_to_str(&subjects, &lecturers, &sessions); + println!("Get existing schedule from DB"); + let mut db_classes = class_repo + .get_schedule() + .await + .with_context(|| "Error get schedules from DB")?; + + println!("Get latest schedule from Excel"); + let excel = Excel::new(&file, &sheet, subjects, lecturers, sessions) + .with_context(|| "Error opening excel file")?; + let excel_classes = excel.updated_schedule_to_str(); + + println!( + "Comparing {} classes from Excel with existing schedule", + excel_classes.len() + ); for class in excel_classes { let key = (class.subject_name.clone(), class.class_code.clone()); match db_classes.get(&key) { @@ -156,12 +166,22 @@ async fn main() -> Result<()> { deleted.push(val); } } - let mut out_writer = Writer::new(&outdir).await?; + println!( + "Detected {} changed, {} added, {} deleted class", + changed.len(), + added.len(), + deleted.len() + ); + println!("Write the result to {:?}", &outdir); + let mut out_writer = Writer::new(&outdir) + .await + .with_context(|| format!("Error creating {:?}", &outdir))?; out_writer .write_compare_result(&added, &changed, &deleted) .await - .with_context(|| "Error writing output to sql")?; + .with_context(|| "Error writing result")?; } } + println!("Done"); Ok(()) } diff --git a/src/repo.rs b/src/repo.rs index e166518..9004d5a 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -189,21 +189,17 @@ impl ClassRepository<'_> { } pub async fn get_schedule(&self) -> Result> { - // run a query, returning data that contains subject name, class code, lecturer code, day, and session start let rows = sqlx::query( "SELECT c.id, m.name as subject_name, c.code as class_code, c.day, l.code as lecture_code, cls.total_lecturer, s.session_time FROM Class c INNER JOIN (SELECT c.id, COUNT(c.id) as total_lecturer FROM Class c INNER JOIN `_ClassToLecturer` ctl ON c.id = ctl.A INNER JOIN Lecturer l ON ctl.B = l.id GROUP BY (c.id)) cls ON cls.id = c.id INNER JOIN Matkul m ON c.matkulId = m.id INNER JOIN Session s on s.id = c.sessionId INNER JOIN `_ClassToLecturer` ctl ON c.id = ctl.A INNER JOIN Lecturer l ON ctl.B = l.id;", ) .fetch_all(self.db_pool) .await?; - // store it inside a hashmap with (subject name, class code) as key, and {lecturer code, day, and session start as value} - let mut class_map = HashMap::new(); for row in rows.into_iter() { let total_lecturer = row.get::("total_lecturer"); let lecturer_code: Vec = if total_lecturer > 1 { let class_id: String = row.get("id"); - // get all lecturer code let lec_rows = sqlx::query("SELECT l.code FROM Lecturer l INNER JOIN `_ClassToLecturer` ctl ON l.id = ctl.B INNER JOIN Class c ON ctl.A = c.id WHERE c.id = ?").bind(class_id).fetch_all(self.db_pool).await?; lec_rows .into_iter() diff --git a/src/util/file.rs b/src/util/file.rs index ec15b8f..95a69c7 100644 --- a/src/util/file.rs +++ b/src/util/file.rs @@ -66,7 +66,7 @@ impl Writer { self.write(query).await?; } - let changed_header = format!("--- CHANGED ---\n"); + let changed_header = format!("\n\n--- CHANGED ---\n"); self.write(changed_header).await?; for class in changed { let query = format!( @@ -89,7 +89,7 @@ impl Writer { self.write(query).await?; } - let deleted_header = format!("--- DELETED ---\n"); + let deleted_header = format!("\n\n--- DELETED ---\n"); self.write(deleted_header).await?; for class in deleted { let query = format!(