refactor: clean up MeetingScheduleInfo methods and enhance Term season handling

This commit is contained in:
2025-08-27 00:11:33 -05:00
parent a01a30d047
commit 5ace08327d
3 changed files with 47 additions and 101 deletions

View File

@@ -487,52 +487,6 @@ impl MeetingScheduleInfo {
(start.and_utc(), end.and_utc()) (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<i64> {
if let Some(time_range) = &self.time_range {
Some(time_range.duration_minutes())
} else {
None
}
}
} }
impl PartialEq for MeetingScheduleInfo { impl PartialEq for MeetingScheduleInfo {

View File

@@ -3,6 +3,14 @@ use std::{ops::RangeInclusive, str::FromStr};
use anyhow::Context; use anyhow::Context;
use serde::{Deserialize, Serialize}; 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<u32> = 2007..=(CURRENT_YEAR + 10);
/// Represents a term in the Banner system /// Represents a term in the Banner system
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Term { pub struct Term {
@@ -18,34 +26,37 @@ pub enum Season {
Summer, Summer,
} }
impl Term { impl ToString for Term {
pub fn to_string(&self) -> String { /// Returns the term in the format YYYYXX, where YYYY is the year and XX is the season code
format!("{}{}", self.year, self.season.to_string()) fn to_string(&self) -> String {
format!("{}{}", self.year, self.season.to_str())
} }
} }
impl Season { impl Season {
pub fn to_string(&self) -> String { /// Returns the season code as a string
(match self { fn to_str(&self) -> &'static str {
match self {
Season::Fall => "10", Season::Fall => "10",
Season::Spring => "20", Season::Spring => "20",
Season::Summer => "30", Season::Summer => "30",
})
.to_string()
}
pub fn from_string(s: &str) -> Option<Season> {
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; impl FromStr for Season {
const VALID_YEARS: RangeInclusive<u32> = 2007..=(CURRENT_YEAR + 10); type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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 { impl FromStr for Term {
type Err = anyhow::Error; type Err = anyhow::Error;
@@ -61,16 +72,8 @@ impl FromStr for Term {
} }
let season = 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 }) Ok(Term { year, season })
} }
} }
impl FromStr for Season {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_string(s).ok_or(())
}
}

View File

@@ -1,14 +1,12 @@
//! Google Calendar command implementation. //! Google Calendar command implementation.
use crate::banner::{Course, MeetingScheduleInfo, TimeRange}; use crate::banner::{Course, DayOfWeek, MeetingScheduleInfo};
use crate::bot::{Context, Error}; use crate::bot::{Context, Error};
use chrono::NaiveDate; use chrono::NaiveDate;
use std::collections::HashMap; use std::collections::HashMap;
use tracing::{error, info}; use tracing::{error, info};
use url::Url; 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 /// Generate a link to create a Google Calendar event for a course
#[poise::command(slash_command, prefix_command)] #[poise::command(slash_command, prefix_command)]
pub async fn gcal( pub async fn gcal(
@@ -66,7 +64,7 @@ pub async fn gcal(
}; };
// Get meeting times // 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, Ok(meeting_time) => meeting_time,
Err(e) => { Err(e) => {
error!("Failed to get meeting times: {}", e); error!("Failed to get meeting times: {}", e);
@@ -87,7 +85,7 @@ pub async fn gcal(
.map(|m| { .map(|m| {
let link = generate_gcal_url(&course, m)?; let link = generate_gcal_url(&course, m)?;
let detail = match &m.time_range { let detail = match &m.time_range {
Some(range) => range.format_12hr(), Some(range) => format!("{} {}", m.days_string(), range.format_12hr()),
None => m.days_string(), None => m.days_string(),
}; };
Ok(LinkDetail { link, detail }) Ok(LinkDetail { link, detail })
@@ -175,32 +173,23 @@ fn generate_gcal_url(
/// Generate RRULE for recurrence /// Generate RRULE for recurrence
fn generate_rrule(meeting_time: &MeetingScheduleInfo, end_date: NaiveDate) -> String { fn generate_rrule(meeting_time: &MeetingScheduleInfo, end_date: NaiveDate) -> String {
let by_day = meeting_time.days_string(); let days_of_week = meeting_time.days_of_week();
let by_day = days_of_week
// Handle edge cases where days_string might return "None" or empty .iter()
let by_day = if by_day.is_empty() || by_day == "None" { .map(|day| match day {
"MO".to_string() // Default to Monday DayOfWeek::Monday => "MO",
} else { DayOfWeek::Tuesday => "TU",
// Convert our day format to Google Calendar format DayOfWeek::Wednesday => "WE",
by_day DayOfWeek::Thursday => "TH",
.replace("M", "MO") DayOfWeek::Friday => "FR",
.replace("Tu", "TU") DayOfWeek::Saturday => "SA",
.replace("W", "WE") DayOfWeek::Sunday => "SU",
.replace("Th", "TH") })
.replace("F", "FR") .collect::<Vec<&str>>()
.replace("Sa", "SA") .join(",");
.replace("Su", "SU")
};
// Format end date for RRULE (YYYYMMDD format) // Format end date for RRULE (YYYYMMDD format)
let until = end_date.format("%Y%m%d").to_string(); let until = end_date.format("%Y%m%d").to_string();
// Build the RRULE string manually to avoid formatting issues format!("RRULE:FREQ=WEEKLY;BYDAY={by_day};UNTIL={until}")
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
} }