From ede064be87f696c2628d037f67fa26829704e9fd Mon Sep 17 00:00:00 2001 From: Xevion Date: Wed, 27 Aug 2025 02:36:22 -0500 Subject: [PATCH] feat: add current term identification, term point state machine --- src/banner/api.rs | 7 +- src/banner/models/terms.rs | 163 +++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 5 deletions(-) diff --git a/src/banner/api.rs b/src/banner/api.rs index 1990d6e..1fce5eb 100644 --- a/src/banner/api.rs +++ b/src/banner/api.rs @@ -198,14 +198,11 @@ impl BannerApi { /// Retrieves meeting time information for a course. pub async fn get_course_meeting_time( &self, - term: i32, + term: &str, crn: i32, ) -> Result> { let url = format!("{}/searchResults/getFacultyMeetingTimes", self.base_url); - let params = [ - ("term", &term.to_string()), - ("courseReferenceNumber", &crn.to_string()), - ]; + let params = [("term", term), ("courseReferenceNumber", &crn.to_string())]; let response = self .client diff --git a/src/banner/models/terms.rs b/src/banner/models/terms.rs index e1fd13e..8f98471 100644 --- a/src/banner/models/terms.rs +++ b/src/banner/models/terms.rs @@ -1,6 +1,7 @@ use std::{ops::RangeInclusive, str::FromStr}; use anyhow::Context; +use chrono::{Datelike, Local, NaiveDate}; use serde::{Deserialize, Serialize}; /// The current year at the time of compilation @@ -18,6 +19,15 @@ pub struct Term { pub season: Season, } +/// Represents the term status at a specific point in time +#[derive(Debug, Clone)] +pub enum TermPoint { + /// Currently in a term + InTerm { current: Term }, + /// Between terms, with the next term specified + BetweenTerms { next: Term }, +} + /// Represents a season within a term #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Season { @@ -26,6 +36,149 @@ pub enum Season { Summer, } +impl Term { + /// Returns the current term status - either currently in a term or between terms + pub fn get_current() -> TermPoint { + let now = Local::now().naive_local(); + Self::get_status_for_date(now.date()) + } + + /// Returns the current term status for a specific date + pub fn get_status_for_date(date: NaiveDate) -> TermPoint { + let literal_year = date.year() as u32; + let day_of_year = date.ordinal() as u32; + let ranges = Self::get_season_ranges(literal_year); + + // If we're past the end of the summer term, we're 'in' the next school year. + let term_year = if day_of_year > ranges.summer.end { + literal_year + 1 + } else { + literal_year + }; + + if (day_of_year < ranges.spring.start) || (day_of_year >= ranges.fall.end) { + // Fall over, Spring not yet begun + TermPoint::BetweenTerms { + next: Term { + year: term_year, + season: Season::Spring, + }, + } + } else if (day_of_year >= ranges.spring.start) && (day_of_year < ranges.spring.end) { + // Spring + TermPoint::InTerm { + current: Term { + year: term_year, + season: Season::Spring, + }, + } + } else if day_of_year < ranges.summer.start { + // Spring over, Summer not yet begun + TermPoint::BetweenTerms { + next: Term { + year: term_year, + season: Season::Summer, + }, + } + } else if (day_of_year >= ranges.summer.start) && (day_of_year < ranges.summer.end) { + // Summer + TermPoint::InTerm { + current: Term { + year: term_year, + season: Season::Summer, + }, + } + } else if day_of_year < ranges.fall.start { + // Summer over, Fall not yet begun + TermPoint::BetweenTerms { + next: Term { + year: term_year, + season: Season::Fall, + }, + } + } else if (day_of_year >= ranges.fall.start) && (day_of_year < ranges.fall.end) { + // Fall + TermPoint::InTerm { + current: Term { + year: term_year, + season: Season::Fall, + }, + } + } else { + // This should never happen, but Rust requires exhaustive matching + panic!("Impossible code reached (dayOfYear: {})", day_of_year); + } + } + + /// Returns the start and end day of each term for the given year. + /// The ranges are inclusive of the start day and exclusive of the end day. + fn get_season_ranges(year: u32) -> SeasonRanges { + let spring_start = NaiveDate::from_ymd_opt(year as i32, 1, 14) + .unwrap() + .ordinal() as u32; + let spring_end = NaiveDate::from_ymd_opt(year as i32, 5, 1) + .unwrap() + .ordinal() as u32; + let summer_start = NaiveDate::from_ymd_opt(year as i32, 5, 25) + .unwrap() + .ordinal() as u32; + let summer_end = NaiveDate::from_ymd_opt(year as i32, 8, 15) + .unwrap() + .ordinal() as u32; + let fall_start = NaiveDate::from_ymd_opt(year as i32, 8, 18) + .unwrap() + .ordinal() as u32; + let fall_end = NaiveDate::from_ymd_opt(year as i32, 12, 10) + .unwrap() + .ordinal() as u32; + + SeasonRanges { + spring: YearDayRange { + start: spring_start, + end: spring_end, + }, + summer: YearDayRange { + start: summer_start, + end: summer_end, + }, + fall: YearDayRange { + start: fall_start, + end: fall_end, + }, + } + } + + /// Returns a long string representation of the term (e.g., "Fall 2025") + pub fn to_long_string(&self) -> String { + format!("{} {}", self.season, self.year) + } +} + +impl TermPoint { + /// Returns the inner Term regardless of the status + pub fn inner(&self) -> &Term { + match self { + TermPoint::InTerm { current } => current, + TermPoint::BetweenTerms { next } => next, + } + } +} + +/// Represents the start and end day of each term within a year +#[derive(Debug, Clone)] +struct SeasonRanges { + spring: YearDayRange, + summer: YearDayRange, + fall: YearDayRange, +} + +/// Represents the start and end day of a term within a year +#[derive(Debug, Clone)] +struct YearDayRange { + start: u32, + end: u32, +} + 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 { @@ -44,6 +197,16 @@ impl Season { } } +impl std::fmt::Display for Season { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Season::Fall => write!(f, "Fall"), + Season::Spring => write!(f, "Spring"), + Season::Summer => write!(f, "Summer"), + } + } +} + impl FromStr for Season { type Err = anyhow::Error;