use crate::components::{Box as RnkBox, Text};
use crate::core::{AlignItems, Color, Element, FlexDirection, JustifyContent};
#[derive(Debug, Clone)]
pub struct Calendar {
year: i32,
month: u32,
selected_day: Option<u32>,
highlighted: Vec<u32>,
first_day_of_week: u8,
show_week_numbers: bool,
header_color: Color,
selected_color: Color,
today_color: Color,
highlighted_color: Color,
today: Option<(i32, u32, u32)>,
key: Option<String>,
}
impl Calendar {
pub fn new(year: i32, month: u32) -> Self {
Self {
year,
month: month.clamp(1, 12),
selected_day: None,
highlighted: Vec::new(),
first_day_of_week: 0, show_week_numbers: false,
header_color: Color::Cyan,
selected_color: Color::Green,
today_color: Color::Yellow,
highlighted_color: Color::Magenta,
today: None,
key: None,
}
}
pub fn selected(mut self, day: u32) -> Self {
self.selected_day = Some(day);
self
}
pub fn highlighted(mut self, days: Vec<u32>) -> Self {
self.highlighted = days;
self
}
pub fn first_day_of_week(mut self, day: u8) -> Self {
self.first_day_of_week = day % 7;
self
}
pub fn monday_first(mut self) -> Self {
self.first_day_of_week = 1;
self
}
pub fn show_week_numbers(mut self, show: bool) -> Self {
self.show_week_numbers = show;
self
}
pub fn header_color(mut self, color: Color) -> Self {
self.header_color = color;
self
}
pub fn selected_color(mut self, color: Color) -> Self {
self.selected_color = color;
self
}
pub fn today_color(mut self, color: Color) -> Self {
self.today_color = color;
self
}
pub fn highlighted_color(mut self, color: Color) -> Self {
self.highlighted_color = color;
self
}
pub fn today(mut self, year: i32, month: u32, day: u32) -> Self {
self.today = Some((year, month, day));
self
}
pub fn key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn into_element(self) -> Element {
let mut rows = Vec::new();
let month_name = month_name(self.month);
let header = Text::new(format!("{} {}", month_name, self.year))
.color(self.header_color)
.bold();
rows.push(
RnkBox::new()
.justify_content(JustifyContent::Center)
.child(header.into_element())
.into_element(),
);
let day_headers = self.build_day_headers();
rows.push(day_headers);
let weeks = self.build_weeks();
for week in weeks {
rows.push(week);
}
let mut container = RnkBox::new()
.flex_direction(FlexDirection::Column)
.gap(0.0)
.children(rows);
if let Some(key) = self.key {
container = container.key(key);
}
container.into_element()
}
fn build_day_headers(&self) -> Element {
let days = if self.first_day_of_week == 1 {
["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
} else {
["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
};
let mut children = Vec::new();
if self.show_week_numbers {
children.push(
RnkBox::new()
.width(3)
.child(Text::new("Wk").dim().into_element())
.into_element(),
);
}
for day in days {
children.push(
RnkBox::new()
.width(3)
.justify_content(JustifyContent::Center)
.child(Text::new(day).dim().into_element())
.into_element(),
);
}
RnkBox::new()
.flex_direction(FlexDirection::Row)
.children(children)
.into_element()
}
fn build_weeks(&self) -> Vec<Element> {
let days_in_month = days_in_month(self.year, self.month);
let first_day = day_of_week(self.year, self.month, 1);
let offset = (first_day + 7 - self.first_day_of_week as u32) % 7;
let mut weeks = Vec::new();
let mut current_day = 1u32;
let mut week_num = week_number(self.year, self.month, 1);
let mut first_week = Vec::new();
if self.show_week_numbers {
first_week.push(
RnkBox::new()
.width(3)
.child(Text::new(format!("{:2}", week_num)).dim().into_element())
.into_element(),
);
}
for i in 0..7 {
if i < offset {
first_week.push(
RnkBox::new()
.width(3)
.child(Text::new(" ").into_element())
.into_element(),
);
} else {
first_week.push(self.build_day_cell(current_day));
current_day += 1;
}
}
weeks.push(
RnkBox::new()
.flex_direction(FlexDirection::Row)
.children(first_week)
.into_element(),
);
while current_day <= days_in_month {
week_num += 1;
let mut week = Vec::new();
if self.show_week_numbers {
week.push(
RnkBox::new()
.width(3)
.child(
Text::new(format!("{:2}", week_num % 53))
.dim()
.into_element(),
)
.into_element(),
);
}
for _ in 0..7 {
if current_day <= days_in_month {
week.push(self.build_day_cell(current_day));
current_day += 1;
} else {
week.push(
RnkBox::new()
.width(3)
.child(Text::new(" ").into_element())
.into_element(),
);
}
}
weeks.push(
RnkBox::new()
.flex_direction(FlexDirection::Row)
.children(week)
.into_element(),
);
}
weeks
}
fn build_day_cell(&self, day: u32) -> Element {
let is_selected = self.selected_day == Some(day);
let is_today = self
.today
.map(|(y, m, d)| y == self.year && m == self.month && d == day)
.unwrap_or(false);
let is_highlighted = self.highlighted.contains(&day);
let text = format!("{:2}", day);
let mut text_elem = Text::new(text);
if is_selected {
text_elem = text_elem.color(self.selected_color).bold();
} else if is_today {
text_elem = text_elem.color(self.today_color);
} else if is_highlighted {
text_elem = text_elem.color(self.highlighted_color);
}
RnkBox::new()
.width(3)
.justify_content(JustifyContent::Center)
.align_items(AlignItems::Center)
.child(text_elem.into_element())
.into_element()
}
}
fn month_name(month: u32) -> &'static str {
match month {
1 => "January",
2 => "February",
3 => "March",
4 => "April",
5 => "May",
6 => "June",
7 => "July",
8 => "August",
9 => "September",
10 => "October",
11 => "November",
12 => "December",
_ => "Unknown",
}
}
fn days_in_month(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(year) {
29
} else {
28
}
}
_ => 30,
}
}
fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
fn day_of_week(year: i32, month: u32, day: u32) -> u32 {
let (y, m) = if month < 3 {
(year - 1, month + 12)
} else {
(year, month)
};
let q = day as i32;
let m = m as i32;
let k = y % 100;
let j = y / 100;
let h = (q + (13 * (m + 1)) / 5 + k + k / 4 + j / 4 - 2 * j) % 7;
((h + 6) % 7) as u32 }
fn week_number(year: i32, month: u32, day: u32) -> u32 {
let day_of_year = day_of_year(year, month, day);
let first_day = day_of_week(year, 1, 1);
let offset = (7 - first_day) % 7;
if day_of_year <= offset {
week_number(year - 1, 12, 31)
} else {
((day_of_year - offset - 1) / 7 + 1).min(52)
}
}
fn day_of_year(year: i32, month: u32, day: u32) -> u32 {
let mut total = day;
for m in 1..month {
total += days_in_month(year, m);
}
total
}
impl Default for Calendar {
fn default() -> Self {
Self::new(2024, 1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calendar_creation() {
let cal = Calendar::new(2024, 6);
assert_eq!(cal.year, 2024);
assert_eq!(cal.month, 6);
}
#[test]
fn test_days_in_month() {
assert_eq!(days_in_month(2024, 1), 31);
assert_eq!(days_in_month(2024, 2), 29); assert_eq!(days_in_month(2023, 2), 28);
assert_eq!(days_in_month(2024, 4), 30);
}
#[test]
fn test_leap_year() {
assert!(is_leap_year(2024));
assert!(!is_leap_year(2023));
assert!(is_leap_year(2000));
assert!(!is_leap_year(1900));
}
#[test]
fn test_day_of_week() {
assert_eq!(day_of_week(2024, 1, 1), 1);
assert_eq!(day_of_week(2024, 7, 4), 4);
}
#[test]
fn test_calendar_with_selection() {
let cal = Calendar::new(2024, 6)
.selected(15)
.highlighted(vec![1, 10, 20])
.today(2024, 6, 3);
assert_eq!(cal.selected_day, Some(15));
assert_eq!(cal.highlighted, vec![1, 10, 20]);
}
#[test]
fn test_calendar_monday_first() {
let cal = Calendar::new(2024, 6).monday_first();
assert_eq!(cal.first_day_of_week, 1);
}
#[test]
fn test_calendar_into_element() {
let cal = Calendar::new(2024, 6).selected(15);
let _ = cal.into_element();
}
}