premise 0.1.5

the core pattern library of Premise: sequences, tables, records, money, days and text, every function carrying the reason it exists
Documentation
use patterns_macros::{because, source};

pub fn day_of(time: u64, day_seconds: u64) -> u64 {
    match day_seconds == 0 {
        true => time,
        false => (time / day_seconds) * day_seconds,
    }
}
because!(day_of, "a moment floored to the start of its day, the bucket every series is kept in, so two events on one day land on one row");

pub fn window_end(from: u64, span: u64, cap: u64) -> u64 {
    from.saturating_add(span).min(cap)
}
because!(window_end, "the far edge of a fetch window, a span past its start but never past the cap, which is how a request for a run of days stays inside what a source will answer and inside today");

pub fn days_between(from: u64, to: u64, day_seconds: u64) -> u64 {
    match day_seconds == 0 {
        true => 0,
        false => to.saturating_sub(from) / day_seconds,
    }
}
because!(days_between, "the whole days from one moment to a later one, the count a fetch window is sized by and an age is shown by");

pub struct CivilCalendar;
source!(CivilCalendar, "the proleptic Gregorian calendar as the epoch counts it, and the arithmetic that turns a day count into a year and month and back, taken from the well known days-from-civil derivation");

const EPOCH_SHIFT_DAYS: i64 = 719_468;
because!(EPOCH_SHIFT_DAYS, CivilCalendar, "the days from the start of the four hundred year cycle the derivation counts from to the epoch");

const ERA_DAYS: i64 = 146_097;
because!(ERA_DAYS, CivilCalendar, "the days in one four hundred year cycle of the calendar, after which leap years repeat exactly");

const ERA_YEARS: i64 = 400;
because!(ERA_YEARS, CivilCalendar, "the years in one cycle of the calendar");

const LEAP_EVERY: i64 = 4;
because!(LEAP_EVERY, CivilCalendar, "the years between leap years inside a century");

const CENTURY: i64 = 100;
because!(CENTURY, CivilCalendar, "the years in a century, at which a leap year is skipped");

const MARCH_SHIFT_MONTHS: i64 = 3;
because!(MARCH_SHIFT_MONTHS, CivilCalendar, "the months the derivation shifts the year by so that the leap day falls at the year's end and the month lengths follow one rule");

const MONTH_DAY_SCALE: i64 = 153;
because!(MONTH_DAY_SCALE, CivilCalendar, "the numerator of the five month rule the derivation uses, since five months from March hold this many days");

const MONTH_DAY_OFFSET: i64 = 2;
because!(MONTH_DAY_OFFSET, CivilCalendar, "the offset in the same five month rule");

const MONTH_DAY_DIVISOR: i64 = 5;
because!(MONTH_DAY_DIVISOR, CivilCalendar, "what the scaled day count is divided by in the month rule, the months that rule spans");

const MONTHS_IN_YEAR: i64 = 12;
because!(MONTHS_IN_YEAR, CivilCalendar, "the months in a year, where the shifted month wraps");

const MONTHS_BEFORE_WRAP: i64 = 10;
because!(MONTHS_BEFORE_WRAP, CivilCalendar, "the shifted month at which the calendar year turns over, since the derivation counts from March");

const YEAR_DAYS_WHOLE: i64 = 365;
because!(YEAR_DAYS_WHOLE, CivilCalendar, "how many days a year holds when it has no leap day, the base the leap corrections add to");

pub fn civil_day(day_seconds_since_epoch: u64, day_seconds: u64) -> (i64, u32, u32) {
    let days = match day_seconds == 0 {
        true => 0,
        false => (day_seconds_since_epoch / day_seconds) as i64,
    };
    let z = days + EPOCH_SHIFT_DAYS;
    let era = z.div_euclid(ERA_DAYS);
    let doe = z.rem_euclid(ERA_DAYS);
    let yoe = (doe - doe / (ERA_DAYS / ERA_YEARS * LEAP_EVERY - 1) + doe / (YEAR_DAYS_WHOLE * CENTURY + CENTURY / LEAP_EVERY - 1) - doe / (ERA_DAYS - 1)) / YEAR_DAYS_WHOLE;
    let y = yoe + era * ERA_YEARS;
    let doy = doe - (YEAR_DAYS_WHOLE * yoe + yoe / LEAP_EVERY - yoe / CENTURY);
    let mp = (MONTH_DAY_DIVISOR * doy + MONTH_DAY_OFFSET) / MONTH_DAY_SCALE;
    let d = doy - (MONTH_DAY_SCALE * mp + MONTH_DAY_OFFSET) / MONTH_DAY_DIVISOR + 1;
    let m = match mp < MONTHS_BEFORE_WRAP {
        true => mp + MARCH_SHIFT_MONTHS,
        false => mp - (MONTHS_IN_YEAR - MARCH_SHIFT_MONTHS),
    };
    let year = match m <= MONTH_DAY_OFFSET {
        true => y + 1,
        false => y,
    };
    (year, m as u32, d as u32)
}
because!(civil_day, "the calendar year, month and day a moment falls in, by the closed form derivation rather than a table, so a date is written without a calendar library");

pub fn civil_of(day_seconds_since_epoch: u64, day_seconds: u64) -> (i64, u32) {
    let (year, month, _) = civil_day(day_seconds_since_epoch, day_seconds);
    (year, month)
}
because!(civil_of, "the calendar year and month alone, the pair a month end is found from");

pub fn day_start(year: i64, month: u32, day: u32, day_seconds: u64) -> u64 {
    let (y, m) = match month <= MONTH_DAY_OFFSET as u32 {
        true => (year - 1, month as i64),
        false => (year, month as i64),
    };
    let era = y.div_euclid(ERA_YEARS);
    let yoe = y.rem_euclid(ERA_YEARS);
    let shifted = match m > MONTH_DAY_OFFSET {
        true => m - MARCH_SHIFT_MONTHS,
        false => m + (MONTHS_IN_YEAR - MARCH_SHIFT_MONTHS),
    };
    let doy = (MONTH_DAY_SCALE * shifted + MONTH_DAY_OFFSET) / MONTH_DAY_DIVISOR + i64::from(day) - 1;
    let doe = yoe * YEAR_DAYS_WHOLE + yoe / LEAP_EVERY - yoe / CENTURY + doy;
    let days = era * ERA_DAYS + doe - EPOCH_SHIFT_DAYS;
    (days.max(0) as u64).saturating_mul(day_seconds)
}
because!(day_start, "the first moment of a calendar day, the reverse of civil_day, which a date input names and a month end is one day back from");

pub fn month_start(year: i64, month: u32, day_seconds: u64) -> u64 {
    day_start(year, month, 1, day_seconds)
}
because!(month_start, "the first moment of a calendar month, the first day of it");

pub fn month_ends(report_day: u64, count: u64, day_seconds: u64) -> Vec<u64> {
    let (year, month) = civil_of(report_day, day_seconds);
    let mut out = vec![report_day];
    for back in 0..count as i64 {
        let total = year * MONTHS_IN_YEAR + (month as i64 - 1) - back;
        let y = total.div_euclid(MONTHS_IN_YEAR);
        let m = total.rem_euclid(MONTHS_IN_YEAR) + 1;
        let end = month_start(y, m as u32, day_seconds).saturating_sub(day_seconds);
        if end < report_day {
            out.push(end);
        }
    }
    out
}
because!(month_ends, "the report day followed by the last day of each of the months before it, newest first, so a report reads its figures at the same moments a reader would tick off on a calendar");