nu_analytics/core/models/
school.rs1use super::{Course, Degree, Plan};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct School {
10 pub name: String,
12
13 courses: HashMap<String, Course>,
15
16 pub degrees: Vec<Degree>,
18
19 pub plans: Vec<Plan>,
21}
22
23impl School {
24 #[must_use]
29 pub fn new(name: String) -> Self {
30 Self {
31 name,
32 courses: HashMap::new(),
33 degrees: Vec::new(),
34 plans: Vec::new(),
35 }
36 }
37
38 pub fn add_course(&mut self, course: Course) -> bool {
53 let key = course.key();
54 self.courses.insert(key, course).is_none()
55 }
56
57 pub fn add_course_with_key(&mut self, key: String, course: Course) {
63 self.courses.insert(key, course);
64 }
65
66 #[must_use]
74 pub fn get_course(&self, storage_key: &str) -> Option<&Course> {
75 self.courses.get(storage_key)
76 }
77
78 #[must_use]
86 pub fn get_course_by_natural_key(&self, natural_key: &str) -> Option<&Course> {
87 self.courses
88 .iter()
89 .find(|(_, course)| course.key() == natural_key)
90 .map(|(_, course)| course)
91 }
92
93 #[must_use]
101 pub fn get_storage_key(&self, natural_key: &str) -> Option<String> {
102 self.courses
103 .iter()
104 .find(|(_, course)| course.key() == natural_key)
105 .map(|(storage_key, _)| storage_key.clone())
106 }
107
108 pub fn get_course_mut(&mut self, key: &str) -> Option<&mut Course> {
116 self.courses.get_mut(key)
117 }
118
119 #[must_use]
121 pub fn courses(&self) -> Vec<&Course> {
122 self.courses.values().collect()
123 }
124
125 pub fn courses_with_keys(&self) -> impl Iterator<Item = (&String, &Course)> {
130 self.courses.iter()
131 }
132
133 pub fn add_degree(&mut self, degree: Degree) {
135 self.degrees.push(degree);
136 }
137
138 #[must_use]
146 pub fn get_degree(&self, degree_id: &str) -> Option<&Degree> {
147 self.degrees.iter().find(|d| d.id() == degree_id)
148 }
149
150 pub fn add_plan(&mut self, plan: Plan) {
152 self.plans.push(plan);
153 }
154
155 #[must_use]
163 pub fn get_plans_for_degree(&self, degree_id: &str) -> Vec<&Plan> {
164 self.plans
165 .iter()
166 .filter(|p| p.degree_id == degree_id)
167 .collect()
168 }
169
170 pub fn validate_plans(&self) -> Result<(), Vec<String>> {
178 let mut missing = Vec::new();
179
180 for plan in &self.plans {
181 for course_key in &plan.courses {
182 if self.get_course(course_key).is_none() {
183 missing.push(format!(
184 "Plan '{}': missing course '{}'",
185 plan.name, course_key
186 ));
187 }
188 }
189 }
190
191 if missing.is_empty() {
192 Ok(())
193 } else {
194 Err(missing)
195 }
196 }
197
198 pub fn validate_course_dependencies(&self) -> Result<(), Vec<String>> {
206 let mut invalid = Vec::new();
207
208 for course in self.courses.values() {
209 for prereq in &course.prerequisites {
210 if self.get_course(prereq).is_none() {
211 invalid.push(format!(
212 "Course '{}': prerequisite '{}' not found",
213 course.key(),
214 prereq
215 ));
216 }
217 }
218
219 for coreq in &course.corequisites {
220 if self.get_course(coreq).is_none() {
221 invalid.push(format!(
222 "Course '{}': corequisite '{}' not found",
223 course.key(),
224 coreq
225 ));
226 }
227 }
228 }
229
230 if invalid.is_empty() {
231 Ok(())
232 } else {
233 Err(invalid)
234 }
235 }
236
237 #[must_use]
242 pub fn build_dag(&self) -> super::DAG {
243 let mut dag = super::DAG::new();
244
245 for stored_key in self.courses.keys() {
247 dag.add_course(stored_key.clone());
248 }
249
250 for (stored_key, course) in &self.courses {
254 for prereq_key in &course.prerequisites {
255 if self.courses.contains_key(prereq_key) {
257 dag.add_prerequisite(stored_key.clone(), prereq_key.as_str());
258 }
259 }
260
261 for coreq_key in &course.corequisites {
262 if self.courses.contains_key(coreq_key) {
264 dag.add_corequisite(stored_key.clone(), coreq_key.as_str());
265 }
266 }
267
268 for coreq_key in &course.strict_corequisites {
269 if self.courses.contains_key(coreq_key) {
270 dag.add_corequisite(stored_key.clone(), coreq_key.as_str());
271 }
272 }
273 }
274
275 dag
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn test_school_creation() {
285 let school = School::new("Northeastern University".to_string());
286
287 assert_eq!(school.name, "Northeastern University");
288 assert!(school.courses().is_empty());
289 assert!(school.degrees.is_empty());
290 assert!(school.plans.is_empty());
291 }
292
293 #[test]
294 fn test_add_and_get_course() {
295 let mut school = School::new("Test University".to_string());
296
297 let course = Course::new(
298 "Discrete Structures".to_string(),
299 "CS".to_string(),
300 "1800".to_string(),
301 4.0,
302 );
303
304 assert!(school.add_course(course));
305
306 let retrieved = school.get_course("CS1800");
307 assert!(retrieved.is_some());
308 assert_eq!(retrieved.unwrap().name, "Discrete Structures");
309 }
310
311 #[test]
312 fn test_add_duplicate_course() {
313 let mut school = School::new("Test University".to_string());
314
315 let course1 = Course::new(
316 "Discrete Structures".to_string(),
317 "CS".to_string(),
318 "1800".to_string(),
319 4.0,
320 );
321
322 let course2 = Course::new(
323 "Different Name".to_string(),
324 "CS".to_string(),
325 "1800".to_string(),
326 4.0,
327 );
328
329 assert!(school.add_course(course1));
330 assert!(!school.add_course(course2)); assert_eq!(school.courses().len(), 1);
333 }
334
335 #[test]
336 fn test_course_lookup() {
337 let mut school = School::new("Test University".to_string());
338
339 for i in 1..=5 {
340 let course = Course::new(
341 format!("Course {i}"),
342 "CS".to_string(),
343 format!("{i}000"),
344 4.0,
345 );
346 school.add_course(course);
347 }
348
349 assert!(school.get_course("CS3000").is_some());
350 assert!(school.get_course("CS9999").is_none());
351 }
352
353 #[test]
354 fn test_add_degree() {
355 let mut school = School::new("Test University".to_string());
356
357 let degree = Degree::new(
358 "Computer Science".to_string(),
359 "BS".to_string(),
360 "11.0701".to_string(),
361 "semester".to_string(),
362 );
363
364 school.add_degree(degree);
365
366 assert_eq!(school.degrees.len(), 1);
367
368 let retrieved = school.get_degree("BS Computer Science");
369 assert!(retrieved.is_some());
370 assert_eq!(retrieved.unwrap().cip_code, "11.0701");
371 }
372
373 #[test]
374 fn test_add_plan() {
375 let mut school = School::new("Test University".to_string());
376
377 let plan = Plan::new(
378 "Standard Track".to_string(),
379 "BS Computer Science".to_string(),
380 );
381
382 school.add_plan(plan);
383
384 assert_eq!(school.plans.len(), 1);
385 }
386
387 #[test]
388 fn test_get_plans_for_degree() {
389 let mut school = School::new("Test University".to_string());
390
391 let plan1 = Plan::new("Track 1".to_string(), "BS Computer Science".to_string());
392 let plan2 = Plan::new("Track 2".to_string(), "BS Computer Science".to_string());
393 let plan3 = Plan::new("Track 3".to_string(), "BA Data Science".to_string());
394
395 school.add_plan(plan1);
396 school.add_plan(plan2);
397 school.add_plan(plan3);
398
399 let cs_plans = school.get_plans_for_degree("BS Computer Science");
400 assert_eq!(cs_plans.len(), 2);
401
402 let ds_plans = school.get_plans_for_degree("BA Data Science");
403 assert_eq!(ds_plans.len(), 1);
404 }
405
406 #[test]
407 fn test_validate_plans_success() {
408 let mut school = School::new("Test University".to_string());
409
410 school.add_course(Course::new(
412 "Course 1".to_string(),
413 "CS".to_string(),
414 "1800".to_string(),
415 4.0,
416 ));
417 school.add_course(Course::new(
418 "Course 2".to_string(),
419 "CS".to_string(),
420 "2510".to_string(),
421 4.0,
422 ));
423
424 let mut plan = Plan::new("Track".to_string(), "BS CS".to_string());
426 plan.add_course("CS1800".to_string());
427 plan.add_course("CS2510".to_string());
428 school.add_plan(plan);
429
430 assert!(school.validate_plans().is_ok());
431 }
432
433 #[test]
434 fn test_validate_plans_failure() {
435 let mut school = School::new("Test University".to_string());
436
437 school.add_course(Course::new(
439 "Course 1".to_string(),
440 "CS".to_string(),
441 "1800".to_string(),
442 4.0,
443 ));
444
445 let mut plan = Plan::new("Track".to_string(), "BS CS".to_string());
447 plan.add_course("CS1800".to_string());
448 plan.add_course("CS9999".to_string()); school.add_plan(plan);
450
451 let result = school.validate_plans();
452 assert!(result.is_err());
453 let errors = result.unwrap_err();
454 assert_eq!(errors.len(), 1);
455 assert!(errors[0].contains("CS9999"));
456 }
457
458 #[test]
459 fn test_validate_course_dependencies_success() {
460 let mut school = School::new("Test University".to_string());
461
462 school.add_course(Course::new(
464 "Discrete".to_string(),
465 "CS".to_string(),
466 "1800".to_string(),
467 4.0,
468 ));
469
470 let mut course2 = Course::new(
471 "Data Structures".to_string(),
472 "CS".to_string(),
473 "2510".to_string(),
474 4.0,
475 );
476 course2.add_prerequisite("CS1800".to_string());
477 school.add_course(course2);
478
479 assert!(school.validate_course_dependencies().is_ok());
480 }
481
482 #[test]
483 fn test_validate_course_dependencies_failure() {
484 let mut school = School::new("Test University".to_string());
485
486 let mut course = Course::new(
487 "Data Structures".to_string(),
488 "CS".to_string(),
489 "2510".to_string(),
490 4.0,
491 );
492 course.add_prerequisite("CS9999".to_string()); school.add_course(course);
494
495 let result = school.validate_course_dependencies();
496 assert!(result.is_err());
497 let errors = result.unwrap_err();
498 assert_eq!(errors.len(), 1);
499 assert!(errors[0].contains("prerequisite"));
500 }
501
502 #[test]
503 fn test_get_course_mut() {
504 let mut school = School::new("Test University".to_string());
505
506 let course = Course::new(
507 "Test".to_string(),
508 "CS".to_string(),
509 "1800".to_string(),
510 4.0,
511 );
512 school.add_course(course);
513
514 {
515 let course_mut = school.get_course_mut("CS1800").unwrap();
516 course_mut.set_canonical_name("Testing 101".to_string());
517 }
518
519 let course = school.get_course("CS1800").unwrap();
520 assert_eq!(course.canonical_name, Some("Testing 101".to_string()));
521 }
522
523 #[test]
524 fn test_courses_iteration() {
525 let mut school = School::new("Test University".to_string());
526
527 school.add_course(Course::new(
528 "C1".to_string(),
529 "CS".to_string(),
530 "1800".to_string(),
531 4.0,
532 ));
533 school.add_course(Course::new(
534 "C2".to_string(),
535 "CS".to_string(),
536 "2510".to_string(),
537 4.0,
538 ));
539
540 let courses = school.courses();
541 assert_eq!(courses.len(), 2);
542
543 let keys: Vec<String> = courses.iter().map(|c| c.key()).collect();
544 assert!(keys.contains(&"CS1800".to_string()));
545 assert!(keys.contains(&"CS2510".to_string()));
546 }
547}