diff --git a/src/banner/models/meetings.rs b/src/banner/models/meetings.rs index 62b6e3b..75927be 100644 --- a/src/banner/models/meetings.rs +++ b/src/banner/models/meetings.rs @@ -487,52 +487,6 @@ impl MeetingScheduleInfo { (start.and_utc(), end.and_utc()) } - - /// Get formatted time string - pub fn time_string(&self) -> String { - match &self.time_range { - Some(time_range) => format!("{} {}", self.days_string(), time_range.format_12hr()), - None => "No Time".to_string(), - } - } - - /// Get formatted schedule string - pub fn schedule_string(&self) -> String { - match self.meeting_type { - MeetingType::HybridBlended => { - format!( - "{}\nHybrid {}", - self.time_string(), - self.location.to_string() - ) - } - MeetingType::OnlineSynchronous => { - format!("{}\nOnline Only", self.time_string()) - } - MeetingType::OnlineAsynchronous => "No Time\nOnline Asynchronous".to_string(), - MeetingType::OnlineHybrid => { - format!("{}\nOnline Partial", self.time_string()) - } - MeetingType::IndependentStudy => "To Be Arranged".to_string(), - MeetingType::FaceToFace => { - format!("{}\n{}", self.time_string(), self.location.to_string()) - } - MeetingType::Unknown(_) => "Unknown".to_string(), - } - } - - pub fn occurs_on_day(&self, day: DayOfWeek) -> bool { - self.days.contains(MeetingDays::from(day)) - } - - /// Get meeting duration in minutes - pub fn duration_minutes(&self) -> Option { - if let Some(time_range) = &self.time_range { - Some(time_range.duration_minutes()) - } else { - None - } - } } impl PartialEq for MeetingScheduleInfo { diff --git a/src/banner/models/terms.rs b/src/banner/models/terms.rs index c4fe60b..e1fd13e 100644 --- a/src/banner/models/terms.rs +++ b/src/banner/models/terms.rs @@ -3,6 +3,14 @@ use std::{ops::RangeInclusive, str::FromStr}; use anyhow::Context; use serde::{Deserialize, Serialize}; +/// The current year at the time of compilation +const CURRENT_YEAR: u32 = compile_time::date!().year() as u32; + +/// The valid years for terms +/// We set a semi-static upper limit to avoid having to update this value while also keeping a tight bound +/// TODO: Recheck the lower bound, it's just a guess right now. +const VALID_YEARS: RangeInclusive = 2007..=(CURRENT_YEAR + 10); + /// Represents a term in the Banner system #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Term { @@ -18,34 +26,37 @@ pub enum Season { Summer, } -impl Term { - pub fn to_string(&self) -> String { - format!("{}{}", self.year, self.season.to_string()) +impl ToString for Term { + /// Returns the term in the format YYYYXX, where YYYY is the year and XX is the season code + fn to_string(&self) -> String { + format!("{}{}", self.year, self.season.to_str()) } } impl Season { - pub fn to_string(&self) -> String { - (match self { + /// Returns the season code as a string + fn to_str(&self) -> &'static str { + match self { Season::Fall => "10", Season::Spring => "20", Season::Summer => "30", - }) - .to_string() - } - - pub fn from_string(s: &str) -> Option { - match s { - "10" => Some(Season::Fall), - "20" => Some(Season::Spring), - "30" => Some(Season::Summer), - _ => None, } } } -const CURRENT_YEAR: u32 = compile_time::date!().year() as u32; -const VALID_YEARS: RangeInclusive = 2007..=(CURRENT_YEAR + 10); +impl FromStr for Season { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let season = match s { + "10" => Season::Fall, + "20" => Season::Spring, + "30" => Season::Summer, + _ => return Err(anyhow::anyhow!("Invalid season: {}", s)), + }; + Ok(season) + } +} impl FromStr for Term { type Err = anyhow::Error; @@ -61,16 +72,8 @@ impl FromStr for Term { } let season = - Season::from_string(&s[4..6]).ok_or_else(|| anyhow::anyhow!("Invalid season code"))?; + Season::from_str(&s[4..6]).map_err(|e| anyhow::anyhow!("Invalid season: {}", e))?; Ok(Term { year, season }) } } - -impl FromStr for Season { - type Err = (); - - fn from_str(s: &str) -> Result { - Self::from_string(s).ok_or(()) - } -} diff --git a/src/bot/commands/gcal.rs b/src/bot/commands/gcal.rs index a51e974..aadb86f 100644 --- a/src/bot/commands/gcal.rs +++ b/src/bot/commands/gcal.rs @@ -1,14 +1,12 @@ //! Google Calendar command implementation. -use crate::banner::{Course, MeetingScheduleInfo, TimeRange}; +use crate::banner::{Course, DayOfWeek, MeetingScheduleInfo}; use crate::bot::{Context, Error}; use chrono::NaiveDate; use std::collections::HashMap; use tracing::{error, info}; use url::Url; -const TIMESTAMP_FORMAT: &str = "%Y%m%dT%H%M%SZ"; - /// Generate a link to create a Google Calendar event for a course #[poise::command(slash_command, prefix_command)] pub async fn gcal( @@ -66,7 +64,7 @@ pub async fn gcal( }; // Get meeting times - let mut meeting_times = match banner_api.get_course_meeting_time(term, crn).await { + let meeting_times = match banner_api.get_course_meeting_time(term, crn).await { Ok(meeting_time) => meeting_time, Err(e) => { error!("Failed to get meeting times: {}", e); @@ -87,7 +85,7 @@ pub async fn gcal( .map(|m| { let link = generate_gcal_url(&course, m)?; let detail = match &m.time_range { - Some(range) => range.format_12hr(), + Some(range) => format!("{} {}", m.days_string(), range.format_12hr()), None => m.days_string(), }; Ok(LinkDetail { link, detail }) @@ -175,32 +173,23 @@ fn generate_gcal_url( /// Generate RRULE for recurrence fn generate_rrule(meeting_time: &MeetingScheduleInfo, end_date: NaiveDate) -> String { - let by_day = meeting_time.days_string(); - - // Handle edge cases where days_string might return "None" or empty - let by_day = if by_day.is_empty() || by_day == "None" { - "MO".to_string() // Default to Monday - } else { - // Convert our day format to Google Calendar format - by_day - .replace("M", "MO") - .replace("Tu", "TU") - .replace("W", "WE") - .replace("Th", "TH") - .replace("F", "FR") - .replace("Sa", "SA") - .replace("Su", "SU") - }; + let days_of_week = meeting_time.days_of_week(); + let by_day = days_of_week + .iter() + .map(|day| match day { + DayOfWeek::Monday => "MO", + DayOfWeek::Tuesday => "TU", + DayOfWeek::Wednesday => "WE", + DayOfWeek::Thursday => "TH", + DayOfWeek::Friday => "FR", + DayOfWeek::Saturday => "SA", + DayOfWeek::Sunday => "SU", + }) + .collect::>() + .join(","); // Format end date for RRULE (YYYYMMDD format) let until = end_date.format("%Y%m%d").to_string(); - // Build the RRULE string manually to avoid formatting issues - let mut rrule = String::new(); - rrule.push_str("FREQ=WEEKLY;BYDAY="); - rrule.push_str(&by_day); - rrule.push_str(";UNTIL="); - rrule.push_str(&until); - - rrule + format!("RRULE:FREQ=WEEKLY;BYDAY={by_day};UNTIL={until}") }