use gpui::SharedString;
use crate::datetime::adapter::{
Clock, DateAdapter, Day, MonthCell, MonthGrid, MonthKey, Selectability, TimeOfDay,
};
const MONTH_NAMES: [&str; 12] = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const WEEKDAY_NAMES: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const EPOCH_OFFSET: i64 = 719_162;
#[derive(Debug, Clone)]
pub struct FixtureDateAdapter {
today: Option<Day>,
blocked: Vec<(Day, SharedString)>,
twelve_hour: bool,
month_bounds: Option<(MonthKey, MonthKey)>,
}
impl Default for FixtureDateAdapter {
fn default() -> Self {
Self::without_today()
}
}
impl FixtureDateAdapter {
pub fn pinned(year: i32, month: u32, day: u32) -> Self {
Self {
today: Some(day_of(year, month, day)),
blocked: Vec::new(),
twelve_hour: false,
month_bounds: None,
}
}
pub fn without_today() -> Self {
Self {
today: None,
blocked: Vec::new(),
twelve_hour: false,
month_bounds: None,
}
}
pub fn blocking(mut self, year: i32, month: u32, day: u32, reason: &'static str) -> Self {
self.blocked
.push((day_of(year, month, day), SharedString::new_static(reason)));
self
}
pub fn twelve_hour(mut self, twelve_hour: bool) -> Self {
self.twelve_hour = twelve_hour;
self
}
pub fn month_bounds(mut self, first: MonthKey, last: MonthKey) -> Self {
self.month_bounds = Some((first, last));
self
}
pub fn day(&self, year: i32, month: u32, day: u32) -> Day {
day_of(year, month, day)
}
pub fn month(&self, year: i32, month: u32) -> MonthKey {
month_key(year, month)
}
}
impl DateAdapter for FixtureDateAdapter {
fn today(&self) -> Option<Day> {
self.today
}
fn month_of(&self, day: Day) -> MonthKey {
let (year, month, _) = civil(day);
month_key(year, month)
}
fn month_grid(&self, month: MonthKey) -> MonthGrid {
let (year, index) = year_month(month);
let first = day_of(year, index, 1);
let length = month_length(year, index);
let lead = (first.0 + 3).rem_euclid(7) as usize;
let mut cells = Vec::new();
for offset in 0..lead {
cells.push(MonthCell::Adjacent(Day(first.0 - (lead - offset) as i64)));
}
for offset in 0..length {
cells.push(MonthCell::Day(Day(first.0 + offset as i64)));
}
while cells.len() % 7 != 0 {
let next = first.0 + cells.len() as i64 - lead as i64;
cells.push(MonthCell::Adjacent(Day(next)));
}
MonthGrid::new(cells.chunks(7).map(<[MonthCell]>::to_vec))
}
fn month_label(&self, month: MonthKey) -> SharedString {
let (year, index) = year_month(month);
SharedString::from(format!("{} {year}", MONTH_NAMES[index as usize - 1]))
}
fn weekday_labels(&self) -> Vec<SharedString> {
WEEKDAY_NAMES
.iter()
.map(|name| SharedString::new_static(name))
.collect()
}
fn format_day(&self, day: Day) -> SharedString {
let (year, month, date) = civil(day);
SharedString::from(format!("{year:04}-{month:02}-{date:02}"))
}
fn day_label(&self, day: Day) -> SharedString {
let (_, _, date) = civil(day);
SharedString::from(format!("{date}"))
}
fn parse_day(&self, text: &str) -> Result<Day, SharedString> {
let refusal = || {
SharedString::new_static(
"The fixture calendar reads dates as 2024-03-05 and nothing else.",
)
};
let mut parts = text.trim().split('-');
let (Some(year), Some(month), Some(date), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return Err(refusal());
};
let year: i32 = year.parse().map_err(|_| refusal())?;
let month: u32 = month.parse().map_err(|_| refusal())?;
let date: u32 = date.parse().map_err(|_| refusal())?;
if !(1..=12).contains(&month) || date < 1 || date > month_length(year, month) {
return Err(refusal());
}
Ok(day_of(year, month, date))
}
fn shift_month(&self, month: MonthKey, delta: i32) -> Option<MonthKey> {
let next = MonthKey(month.0 + delta as i64);
match self.month_bounds {
Some((first, last)) if next < first || next > last => None,
_ => Some(next),
}
}
fn is_selectable(&self, day: Day) -> Selectability {
self.blocked
.iter()
.find(|(blocked, _)| *blocked == day)
.map_or(Selectability::Selectable, |(_, reason)| {
Selectability::Blocked {
reason: reason.clone(),
}
})
}
fn days_in(&self, start: Day, end: Day) -> Option<Vec<Day>> {
if end < start {
return None;
}
Some((start.0..=end.0).map(Day).collect())
}
fn clock(&self) -> Clock {
Clock {
hour_min: if self.twelve_hour { 1 } else { 0 },
hour_max: if self.twelve_hour { 12 } else { 23 },
minute_max: 59,
second_max: 59,
meridiem: self.twelve_hour.then(|| {
(
SharedString::new_static("AM"),
SharedString::new_static("PM"),
)
}),
}
}
fn format_time(&self, time: TimeOfDay) -> SharedString {
let mut text = format!("{:02}:{:02}", time.hour, time.minute);
if let Some(second) = time.second {
text.push_str(&format!(":{second:02}"));
}
if let Some(meridiem) = time.meridiem
&& let Some(label) = self.clock().meridiem_label(meridiem)
{
text.push(' ');
text.push_str(label.as_ref());
}
SharedString::from(text)
}
}
fn month_key(year: i32, month: u32) -> MonthKey {
MonthKey(year as i64 * 12 + month as i64 - 1)
}
fn year_month(month: MonthKey) -> (i32, u32) {
(
month.0.div_euclid(12) as i32,
month.0.rem_euclid(12) as u32 + 1,
)
}
fn is_leap(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
fn month_length(year: i32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if is_leap(year) => 29,
2 => 28,
_ => 0,
}
}
fn days_before_year(year: i32) -> i64 {
let previous = (year - 1) as i64;
365 * previous + previous.div_euclid(4) - previous.div_euclid(100) + previous.div_euclid(400)
}
fn day_of_year(year: i32, month: u32, date: u32) -> i64 {
(1..month)
.map(|m| month_length(year, m) as i64)
.sum::<i64>()
+ date as i64
}
fn day_of(year: i32, month: u32, date: u32) -> Day {
Day(days_before_year(year) + day_of_year(year, month, date) - 1 - EPOCH_OFFSET)
}
fn civil(day: Day) -> (i32, u32, u32) {
let absolute = day.0 + EPOCH_OFFSET;
let mut year = ((absolute as f64) / 365.2425).floor() as i32 + 1;
while days_before_year(year) > absolute {
year -= 1;
}
while days_before_year(year + 1) <= absolute {
year += 1;
}
let mut remaining = absolute - days_before_year(year);
let mut month = 1;
while remaining >= month_length(year, month) as i64 {
remaining -= month_length(year, month) as i64;
month += 1;
}
(year, month, remaining as u32 + 1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_epoch_is_day_zero_and_a_thursday() {
assert_eq!(day_of(1970, 1, 1), Day(0));
assert_eq!(civil(Day(0)), (1970, 1, 1));
assert_eq!((Day(0).0 + 3).rem_euclid(7), 3);
}
#[test]
fn a_day_survives_a_round_trip_through_the_calendar() {
for (year, month, date) in [
(1970, 1, 1),
(1999, 12, 31),
(2000, 2, 29),
(2024, 3, 5),
(2100, 3, 1),
] {
assert_eq!(civil(day_of(year, month, date)), (year, month, date));
}
}
#[test]
fn a_month_grid_is_whole_weeks_that_start_on_the_first() {
let adapter = FixtureDateAdapter::pinned(2024, 3, 5);
let grid = adapter.month_grid(month_key(2024, 3));
assert!(grid.weeks.iter().all(|week| week.len() == 7));
let first = adapter.day(2024, 3, 1);
assert_eq!(grid.position_of(first), Some((0, 4)));
}
#[test]
fn an_unreadable_date_carries_the_hosts_own_message() {
let adapter = FixtureDateAdapter::pinned(2024, 3, 5);
assert_eq!(adapter.parse_day("2024-03-05"), Ok(adapter.day(2024, 3, 5)));
assert!(adapter.parse_day("the fifth").is_err());
assert!(adapter.parse_day("2024-02-30").is_err());
}
#[test]
fn a_host_may_refuse_a_month() {
let adapter = FixtureDateAdapter::pinned(2024, 3, 5)
.month_bounds(month_key(2024, 3), month_key(2024, 4));
assert_eq!(
adapter.shift_month(month_key(2024, 3), 1),
Some(month_key(2024, 4))
);
assert_eq!(adapter.shift_month(month_key(2024, 3), -1), None);
}
}