malevich 1.18.2

Terminal plotting: a small grammar of marks, honest axes, millions of points
Documentation
use super::super::Ticks;
use super::{MAX_TIME_TICKS, civil_from_days, days_from_civil, supported_seconds};

const DAY: i64 = 86_400;

fn labels(ticks: &Ticks) -> Vec<&str> {
    ticks.iter().map(|tick| tick.label.as_str()).collect()
}

#[test]
fn civil_dates_roundtrip_across_eras_and_leap_years() {
    for &(year, month, day) in &[
        (1970, 1, 1),
        (2000, 2, 29),
        (2024, 2, 29),
        (2026, 8, 2),
        (1969, 12, 31),
        (1900, 3, 1),
        (2100, 1, 1),
    ] {
        let days = days_from_civil(year, month, day);
        assert_eq!(civil_from_days(days), (year, month, day));
    }
    assert_eq!(days_from_civil(1970, 1, 1), 0);
}

#[test]
fn hour_ticks_show_the_date_at_midnight() {
    let start = days_from_civil(2026, 8, 1) * DAY + 12 * 3_600;
    let end = days_from_civil(2026, 8, 2) * DAY + 4 * 3_600;
    let ticks = Ticks::time(start as f64, end as f64, 6);
    assert_eq!(
        labels(&ticks),
        ["12:00", "15:00", "18:00", "21:00", "Aug 2", "03:00"]
    );
}

#[test]
fn day_ticks_show_the_year_at_january_first() {
    let start = days_from_civil(2026, 12, 29) * DAY;
    let end = days_from_civil(2027, 1, 3) * DAY;
    let ticks = Ticks::time(start as f64, end as f64, 7);
    assert_eq!(
        labels(&ticks),
        ["Dec 29", "Dec 30", "Dec 31", "2027", "Jan 2", "Jan 3"]
    );
}

#[test]
fn month_ticks_quarter_and_mark_the_new_year() {
    let start = days_from_civil(2026, 1, 1) * DAY;
    let end = days_from_civil(2027, 6, 1) * DAY;
    let ticks = Ticks::time(start as f64, end as f64, 8);
    assert_eq!(labels(&ticks), ["2026", "Apr", "Jul", "Oct", "2027", "Apr"]);
}

#[test]
fn decade_spans_use_round_years() {
    let start = days_from_civil(1988, 6, 1) * DAY;
    let end = days_from_civil(2026, 1, 1) * DAY;
    let ticks = Ticks::time(start as f64, end as f64, 5);
    assert_eq!(labels(&ticks), ["1990", "2000", "2010", "2020"]);
}

#[test]
fn week_ticks_land_on_mondays() {
    let start = days_from_civil(2026, 7, 1) * DAY;
    let end = days_from_civil(2026, 8, 20) * DAY;
    let ticks = Ticks::time(start as f64, end as f64, 8);
    for tick in &ticks {
        let days = (tick.value as i64).div_euclid(DAY);
        assert_eq!((days - 4).rem_euclid(7), 0, "not a Monday: {}", tick.label);
    }
    assert!(ticks.len() >= 5);
}

#[test]
fn second_ticks_show_full_clock_time() {
    let start = days_from_civil(2026, 8, 2) * DAY + 3_600;
    let ticks = Ticks::time(start as f64, start as f64 + 90.0, 6);
    assert_eq!(ticks.as_slice()[0].label, "01:00:00");
}

#[test]
fn finite_values_outside_the_calendar_range_fall_back_without_panicking() {
    let one = Ticks::time(f64::MAX, f64::MAX, 5);
    assert_eq!(one.len(), 1);
    assert_eq!(one.as_slice()[0].value, f64::MAX);

    let both = Ticks::time(-f64::MAX, f64::MAX, usize::MAX);
    assert_eq!(both.len(), 2);
    assert_eq!(both.as_slice()[0].value, -f64::MAX);
    assert_eq!(both.as_slice()[1].value, f64::MAX);
}

#[test]
fn supported_calendar_generation_is_bounded_for_hostile_targets() {
    let (low, high) = supported_seconds();
    let ticks = Ticks::time(low as f64, high as f64, usize::MAX);
    assert!(!ticks.is_empty());
    assert!(ticks.len() <= MAX_TIME_TICKS);
    assert!(ticks.iter().all(|tick| tick.value.is_finite()));
}

#[test]
fn subsecond_domains_still_produce_ticks() {
    let ticks = Ticks::time(0.1, 0.2, 5);
    assert!(!ticks.is_empty());
    assert!(ticks.len() <= MAX_TIME_TICKS);
}