Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::{add_days, days_in_month, format_iso_date, parse_iso_date, CivilDate}; ← from dates.add-days ^1.0.0 · built alongside by fune
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<String>) -> 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<Term> = 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))
}