//! Course search command implementation. use crate::banner::SearchQuery; use crate::bot::{Context, Error}; use regex::Regex; /// Search for courses with various filters #[poise::command(slash_command, prefix_command)] pub async fn search( ctx: Context<'_>, #[description = "Course title (exact, use autocomplete)"] title: Option, #[description = "Course code (e.g. 3743, 3000-3999, 3xxx, 3000-)"] code: Option, #[description = "Maximum number of results"] max: Option, #[description = "Keywords in title or description (space separated)"] keywords: Option, // #[description = "Instructor name"] instructor: Option, // #[description = "Subject (e.g Computer Science/CS, Mathematics/MAT)"] subject: Option, ) -> Result<(), Error> { // Defer the response since this might take a while ctx.defer().await?; // Build the search query let mut query = SearchQuery::new().credits(3, 6); if let Some(title) = title { query = query.title(title); } if let Some(code) = code { let (low, high) = parse_course_code(&code)?; query = query.course_numbers(low, high); } if let Some(keywords) = keywords { let keyword_list: Vec = keywords.split_whitespace().map(|s| s.to_string()).collect(); query = query.keywords(keyword_list); } if let Some(max_results) = max { query = query.max_results(max_results.min(25)); // Cap at 25 } // TODO: Get current term dynamically // TODO: Get BannerApi from context or global state // For now, we'll return an error ctx.say("Search functionality not yet implemented - BannerApi integration needed") .await?; Ok(()) } /// Parse course code input (e.g, "3743", "3000-3999", "3xxx", "3000-") fn parse_course_code(input: &str) -> Result<(i32, i32), Error> { let input = input.trim(); // Handle range format (e.g, "3000-3999") if input.contains('-') { let re = Regex::new(r"(\d{1,4})-(\d{1,4})?").unwrap(); if let Some(captures) = re.captures(input) { let low: i32 = captures[1].parse()?; let high = if captures.get(2).is_some() { captures[2].parse()? } else { 9999 // Open-ended range }; if low > high { return Err("Invalid range: low value greater than high value".into()); } if low < 1000 || high > 9999 { return Err("Course codes must be between 1000 and 9999".into()); } return Ok((low, high)); } return Err("Invalid range format".into()); } // Handle wildcard format (e.g, "34xx") if input.contains('x') { if input.len() != 4 { return Err("Wildcard format must be exactly 4 characters".into()); } let re = Regex::new(r"(\d+)(x+)").unwrap(); if let Some(captures) = re.captures(input) { let prefix: i32 = captures[1].parse()?; let x_count = captures[2].len(); let low = prefix * 10_i32.pow(x_count as u32); let high = low + 10_i32.pow(x_count as u32) - 1; if low < 1000 || high > 9999 { return Err("Course codes must be between 1000 and 9999".into()); } return Ok((low, high)); } return Err("Invalid wildcard format".into()); } // Handle single course code if input.len() == 4 { let code: i32 = input.parse()?; if code < 1000 || code > 9999 { return Err("Course codes must be between 1000 and 9999".into()); } return Ok((code, code)); } Err("Invalid course code format".into()) }