1use patterns_macros::{because, source};
2
3pub fn day_of(time: u64, day_seconds: u64) -> u64 {
4 match day_seconds == 0 {
5 true => time,
6 false => (time / day_seconds) * day_seconds,
7 }
8}
9because!(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");
10
11pub fn window_end(from: u64, span: u64, cap: u64) -> u64 {
12 from.saturating_add(span).min(cap)
13}
14because!(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");
15
16pub fn days_between(from: u64, to: u64, day_seconds: u64) -> u64 {
17 match day_seconds == 0 {
18 true => 0,
19 false => to.saturating_sub(from) / day_seconds,
20 }
21}
22because!(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");
23
24pub struct CivilCalendar;
25source!(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");
26
27const EPOCH_SHIFT_DAYS: i64 = 719_468;
28because!(EPOCH_SHIFT_DAYS, CivilCalendar, "the days from the start of the four hundred year cycle the derivation counts from to the epoch");
29
30const ERA_DAYS: i64 = 146_097;
31because!(ERA_DAYS, CivilCalendar, "the days in one four hundred year cycle of the calendar, after which leap years repeat exactly");
32
33const ERA_YEARS: i64 = 400;
34because!(ERA_YEARS, CivilCalendar, "the years in one cycle of the calendar");
35
36const LEAP_EVERY: i64 = 4;
37because!(LEAP_EVERY, CivilCalendar, "the years between leap years inside a century");
38
39const CENTURY: i64 = 100;
40because!(CENTURY, CivilCalendar, "the years in a century, at which a leap year is skipped");
41
42const MARCH_SHIFT_MONTHS: i64 = 3;
43because!(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");
44
45const MONTH_DAY_SCALE: i64 = 153;
46because!(MONTH_DAY_SCALE, CivilCalendar, "the numerator of the five month rule the derivation uses, since five months from March hold this many days");
47
48const MONTH_DAY_OFFSET: i64 = 2;
49because!(MONTH_DAY_OFFSET, CivilCalendar, "the offset in the same five month rule");
50
51const MONTH_DAY_DIVISOR: i64 = 5;
52because!(MONTH_DAY_DIVISOR, CivilCalendar, "what the scaled day count is divided by in the month rule, the months that rule spans");
53
54const MONTHS_IN_YEAR: i64 = 12;
55because!(MONTHS_IN_YEAR, CivilCalendar, "the months in a year, where the shifted month wraps");
56
57const MONTHS_BEFORE_WRAP: i64 = 10;
58because!(MONTHS_BEFORE_WRAP, CivilCalendar, "the shifted month at which the calendar year turns over, since the derivation counts from March");
59
60const YEAR_DAYS_WHOLE: i64 = 365;
61because!(YEAR_DAYS_WHOLE, CivilCalendar, "how many days a year holds when it has no leap day, the base the leap corrections add to");
62
63pub fn civil_day(day_seconds_since_epoch: u64, day_seconds: u64) -> (i64, u32, u32) {
64 let days = match day_seconds == 0 {
65 true => 0,
66 false => (day_seconds_since_epoch / day_seconds) as i64,
67 };
68 let z = days + EPOCH_SHIFT_DAYS;
69 let era = z.div_euclid(ERA_DAYS);
70 let doe = z.rem_euclid(ERA_DAYS);
71 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;
72 let y = yoe + era * ERA_YEARS;
73 let doy = doe - (YEAR_DAYS_WHOLE * yoe + yoe / LEAP_EVERY - yoe / CENTURY);
74 let mp = (MONTH_DAY_DIVISOR * doy + MONTH_DAY_OFFSET) / MONTH_DAY_SCALE;
75 let d = doy - (MONTH_DAY_SCALE * mp + MONTH_DAY_OFFSET) / MONTH_DAY_DIVISOR + 1;
76 let m = match mp < MONTHS_BEFORE_WRAP {
77 true => mp + MARCH_SHIFT_MONTHS,
78 false => mp - (MONTHS_IN_YEAR - MARCH_SHIFT_MONTHS),
79 };
80 let year = match m <= MONTH_DAY_OFFSET {
81 true => y + 1,
82 false => y,
83 };
84 (year, m as u32, d as u32)
85}
86because!(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");
87
88pub fn civil_of(day_seconds_since_epoch: u64, day_seconds: u64) -> (i64, u32) {
89 let (year, month, _) = civil_day(day_seconds_since_epoch, day_seconds);
90 (year, month)
91}
92because!(civil_of, "the calendar year and month alone, the pair a month end is found from");
93
94pub fn day_start(year: i64, month: u32, day: u32, day_seconds: u64) -> u64 {
95 let (y, m) = match month <= MONTH_DAY_OFFSET as u32 {
96 true => (year - 1, month as i64),
97 false => (year, month as i64),
98 };
99 let era = y.div_euclid(ERA_YEARS);
100 let yoe = y.rem_euclid(ERA_YEARS);
101 let shifted = match m > MONTH_DAY_OFFSET {
102 true => m - MARCH_SHIFT_MONTHS,
103 false => m + (MONTHS_IN_YEAR - MARCH_SHIFT_MONTHS),
104 };
105 let doy = (MONTH_DAY_SCALE * shifted + MONTH_DAY_OFFSET) / MONTH_DAY_DIVISOR + i64::from(day) - 1;
106 let doe = yoe * YEAR_DAYS_WHOLE + yoe / LEAP_EVERY - yoe / CENTURY + doy;
107 let days = era * ERA_DAYS + doe - EPOCH_SHIFT_DAYS;
108 (days.max(0) as u64).saturating_mul(day_seconds)
109}
110because!(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");
111
112pub fn month_start(year: i64, month: u32, day_seconds: u64) -> u64 {
113 day_start(year, month, 1, day_seconds)
114}
115because!(month_start, "the first moment of a calendar month, the first day of it");
116
117pub fn month_ends(report_day: u64, count: u64, day_seconds: u64) -> Vec<u64> {
118 let (year, month) = civil_of(report_day, day_seconds);
119 let mut out = vec![report_day];
120 for back in 0..count as i64 {
121 let total = year * MONTHS_IN_YEAR + (month as i64 - 1) - back;
122 let y = total.div_euclid(MONTHS_IN_YEAR);
123 let m = total.rem_euclid(MONTHS_IN_YEAR) + 1;
124 let end = month_start(y, m as u32, day_seconds).saturating_sub(day_seconds);
125 if end < report_day {
126 out.push(end);
127 }
128 }
129 out
130}
131because!(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");