use super::funejson::Value; use super::dates_add_days::{add_days, days_in_month, format_iso_date, parse_iso_date, CivilDate}; fn parse_month_day(text: &str) -> Option<(i64, i64)> { let b = text.as_bytes(); if b.len() != 5 || b[2] != b'-' || !b[0..2].iter().chain(b[3..5].iter()).all(|c| c.is_ascii_digit()) { return None; } let month = i64::from(b[0] - b'0') * 10 + i64::from(b[1] - b'0'); let day = i64::from(b[3] - b'0') * 10 + i64::from(b[4] - b'0'); if !(1..=12).contains(&month) || day < 1 || day > days_in_month(2001, month) { return None; } Some((month, day)) } /// The academic year and term for a date. /// /// The end is the day before the next year's start, worked out by date /// arithmetic, so a 1 March start ends on 29 February in a leap year. /// /// # Panics /// Panics on a malformed date or yearStart, a term that ends before it /// starts, or overlapping terms. pub fn academic_year(on_date: &str, year_start: Option<&str>, terms: &[Term]) -> AcademicYear { let on = parse_iso_date(on_date); let start_text = year_start.unwrap_or("09-01"); let (month, day) = match parse_month_day(start_text) { Some(md) => md, None => panic!("yearStart must be MM-DD on a day every year has, received \"{}\"", start_text), }; let mut sorted: Vec<&Term> = terms.iter().collect(); sorted.sort_by(|a, b| a.start.cmp(&b.start)); for t in &sorted { parse_iso_date(&t.start); parse_iso_date(&t.end); if t.end < t.start { panic!("term {} ends ({}) before it starts ({})", t.name, t.end, t.start); } } for i in 1..sorted.len() { if sorted[i].start <= sorted[i - 1].end { panic!("terms {} and {} overlap", sorted[i - 1].name, sorted[i].name); } } let before_start = (on.month, on.day) < (month, day); let start_year = if before_start { on.year - 1 } else { on.year }; let start = format_iso_date(&CivilDate { year: start_year, month, day }); let end = add_days(&format_iso_date(&CivilDate { year: start_year + 1, month, day }), -1); let end_year: i64 = end[0..4].parse().unwrap(); let label = if end_year == start_year { start_year.to_string() } else { format!("{}/{:02}", start_year, end_year % 100) }; let term = sorted.iter().find(|t| t.start.as_str() <= on_date && on_date <= t.end.as_str()); AcademicYear { label, start, end, term: term.map(|t| t.name.clone()), term_start: term.map(|t| t.start.clone()), term_end: term.map(|t| t.end.clone()), } } fn opt_str(value: &Option) -> Value { match value { Some(s) => Value::str(s), None => Value::Null, } } pub fn academic_year_to_value(y: &AcademicYear) -> Value { Value::obj(vec![ ("label", Value::str(&y.label)), ("start", Value::str(&y.start)), ("end", Value::str(&y.end)), ("term", opt_str(&y.term)), ("termStart", opt_str(&y.term_start)), ("termEnd", opt_str(&y.term_end)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let terms: Vec = args[2] .as_arr() .iter() .map(|v| Term { name: v.get("name").as_str().to_string(), start: v.get("start").as_str().to_string(), end: v.get("end").as_str().to_string(), }) .collect(); let year_start = if args[1].is_null() { None } else { Some(args[1].as_str()) }; academic_year_to_value(&academic_year(args[0].as_str(), year_start, &terms)) }