mirror of
https://github.com/Xevion/banner.git
synced 2025-12-07 09:14:26 -06:00
114 lines
3.7 KiB
Rust
114 lines
3.7 KiB
Rust
//! 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<String>,
|
|
#[description = "Course code (e.g. 3743, 3000-3999, 3xxx, 3000-)"] code: Option<String>,
|
|
#[description = "Maximum number of results"] max: Option<i32>,
|
|
#[description = "Keywords in title or description (space separated)"] keywords: Option<String>,
|
|
// #[description = "Instructor name"] instructor: Option<String>,
|
|
// #[description = "Subject (e.g Computer Science/CS, Mathematics/MAT)"] subject: Option<String>,
|
|
) -> 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<String> =
|
|
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())
|
|
}
|