use chrono::{Datelike, Duration, NaiveDate};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Day {
pub date: NaiveDate,
pub count: u32,
pub level: u8,
pub future: bool,
}
#[derive(Debug, Clone, Default)]
pub struct Week {
pub days: [Option<Day>; 7],
}
#[derive(Debug, Clone)]
pub struct Calendar {
pub login: String,
pub year: i32,
pub total: u32,
pub years: Vec<i32>,
pub weeks: Vec<Week>,
grid_start: Option<NaiveDate>,
}
impl Calendar {
pub fn build(
login: String,
year: i32,
total: u32,
mut years: Vec<i32>,
days: Vec<Day>,
) -> Self {
years.sort_unstable();
let grid_start = days.first().and_then(|d| {
d.date.checked_sub_signed(Duration::days(i64::from(
d.date.weekday().num_days_from_sunday(),
)))
});
const MAX_WEEKS: usize = 60;
let mut weeks: Vec<Week> = Vec::new();
if let Some(start) = grid_start {
for day in &days {
let offset = (day.date - start).num_days();
if offset < 0 || offset >= (MAX_WEEKS * 7) as i64 {
continue;
}
let (w, wd) = ((offset / 7) as usize, (offset % 7) as usize);
if weeks.len() <= w {
weeks.resize(w + 1, Week::default());
}
weeks[w].days[wd] = Some(*day);
}
}
Self {
login,
year,
total,
years,
weeks,
grid_start,
}
}
pub fn position(&self, date: NaiveDate) -> Option<(usize, usize)> {
let offset = (date - self.grid_start?).num_days();
if offset < 0 {
return None;
}
let (w, wd) = ((offset / 7) as usize, (offset % 7) as usize);
(w < self.weeks.len()).then_some((w, wd))
}
pub fn day(&self, date: NaiveDate) -> Option<Day> {
let (w, wd) = self.position(date)?;
self.weeks[w].days[wd]
}
pub fn days(&self) -> impl Iterator<Item = Day> + '_ {
self.weeks
.iter()
.flat_map(|w| w.days.iter().flatten().copied())
}
pub fn elapsed(&self) -> impl Iterator<Item = Day> + '_ {
self.days().filter(|day| !day.future)
}
pub fn has_elapsed_days(&self) -> bool {
self.elapsed().next().is_some()
}
pub fn first_date(&self) -> Option<NaiveDate> {
self.days().next().map(|d| d.date)
}
pub fn last_date(&self) -> Option<NaiveDate> {
self.days().last().map(|d| d.date)
}
pub fn starts_after(&self, today: NaiveDate) -> bool {
self.year > today.year()
}
pub fn month_labels(&self) -> Vec<(usize, &'static str)> {
const NAMES: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
let mut labels = Vec::new();
let mut prev = 0;
for (w, week) in self.weeks.iter().enumerate() {
let Some(day) = week.days.iter().flatten().next() else {
continue;
};
let month = day.date.month();
if month != prev {
labels.push((w, NAMES[(month - 1) as usize]));
prev = month;
}
}
labels
}
pub fn stats(&self, today: NaiveDate) -> Stats {
let mut stats = Stats::default();
let mut run = 0u32;
let mut previous: Option<NaiveDate> = None;
for day in self.elapsed() {
if day.count == 0 {
run = 0;
previous = None;
continue;
}
stats.active_days += 1;
run = match previous {
Some(prev) if prev.succ_opt() == Some(day.date) => run + 1,
_ => 1,
};
previous = Some(day.date);
if run > stats.longest_streak {
stats.longest_streak = run;
}
if stats.best.is_none_or(|(_, best)| day.count > best) {
stats.best = Some((day.date, day.count));
}
}
let mut walk = today;
if self
.day(today)
.is_some_and(|day| !day.future && day.count == 0)
{
walk = today.pred_opt().unwrap_or(today);
}
while let Some(day) = self.day(walk) {
if day.future || day.count == 0 {
break;
}
stats.current_streak += 1;
match walk.pred_opt() {
Some(previous) => walk = previous,
None => break,
}
}
stats
}
}
pub fn demo(year: i32) -> Calendar {
let (Some(first), Some(last)) = (
NaiveDate::from_ymd_opt(year, 1, 1),
NaiveDate::from_ymd_opt(year, 12, 31),
) else {
return Calendar::build("demo".to_string(), year, 0, vec![year], Vec::new());
};
let mut seed: u32 = 0x9e37_79b9 ^ (year as u32);
let mut next = move || {
seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
seed >> 16
};
let mut counts = Vec::new();
let mut date = first;
while date <= last {
let weekend = matches!(date.weekday(), chrono::Weekday::Sat | chrono::Weekday::Sun);
let roll = next() % 100;
let count = match (weekend, roll) {
(true, 0..=64) | (false, 0..=24) => 0,
(_, 25..=79) => 1 + next() % 6,
(_, 80..=95) => 6 + next() % 12,
_ => 18 + next() % 30,
};
counts.push((date, count));
date += Duration::days(1);
}
let peak = counts
.iter()
.map(|(_, count)| *count)
.max()
.unwrap_or(1)
.max(1);
let total = counts
.iter()
.fold(0u32, |sum, (_, count)| sum.saturating_add(*count));
let days = counts
.into_iter()
.map(|(date, count)| Day {
date,
count,
level: crate::art::level(count, peak),
future: false,
})
.collect();
Calendar::build("demo".to_string(), year, total, vec![year], days)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Stats {
pub active_days: u32,
pub best: Option<(NaiveDate, u32)>,
pub current_streak: u32,
pub longest_streak: u32,
}