Functional Weave
Code in Rust

education.academic-year@1.0.0

impl/rust.rs

3,608 bytes · the Rust implementation · view raw

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))
}