use core::cmp::max; #[derive(Clone, PartialEq, Eq)] pub struct Calendar { base_year: u16, ticks: u32, frozen: bool, hours: u8, minutes: u8, seconds: u8, day: u8, month: u8, year: u16, } impl Calendar { pub fn new(hours: u8, minutes: u8, seconds: u8, day: u8, month: u8, year: u16) -> Self { Self::with_base_year(year, hours, minutes, seconds, day, month, year) } /// Calendar may have a base year, that will be used when calling [to_ticks](`Calendar::to_ticks`). pub fn with_base_year( base_year: u16, hours: u8, minutes: u8, seconds: u8, day: u8, month: u8, year: u16, ) -> Self { let mut result = Self { base_year, hours, minutes, seconds, day, month, year, frozen: false, ticks: 0, }; result.ticks = result.to_ticks(); result } /// Calculate current date based off of the seconds elapsed since /// a base date pub fn from_ticks(base_year: u16, seconds: u32) -> Self { let total_seconds = seconds; let seconds = total_seconds % 60; let total_minutes = total_seconds / 60; let minutes = total_minutes % 60; let total_hours = total_minutes / 60; let hours = total_hours % 24; let elapsed_days = total_hours / 24; // elapsed_days / 365 / 4 subtracts leap days each 4 years, except for the last leap day // that could have been in the last 3 years, solution for that day is presented // below. let estimated_years_elapsed = (elapsed_days - elapsed_days / 365 / 4) / 365; let estimated_year = base_year as u32 + estimated_years_elapsed; // estimated_years_elapsed may not be correct, imagine a situation, // the current year is a leap year, ie. 2024, base is 2021 (1. 1. 00:00:00), // it's 31. 12. 2024, the problem is that the last leap year, 2024, is not captured // by elapsed_days / 365 / 4 in this case. // If we did not subtract a day from the elapsed days, the program would // incorrectly conclude it's 1. 1. 2025. let mut elapsed_days_without_leap_day_in_past_3_years = total_hours / 24; // uhh... naming of this property is 1. long, 2. still not correct, // the variable should have leap days that were in last 3 years, // but not in the current one. for current_year in max(base_year as u32, estimated_year - 3)..=estimated_year - 1 { elapsed_days_without_leap_day_in_past_3_years -= if Self::is_leap_year(current_year as u16) { 1 } else { 0 }; } // this should be the correct year let years_elapsed = (elapsed_days_without_leap_day_in_past_3_years - elapsed_days_without_leap_day_in_past_3_years / 365 / 4) / 365; let year = base_year as u32 + years_elapsed; if year != estimated_year && Self::is_leap_year(year as u16) { // we went back one year, the current year is a leap year, // because leap day is included in Self::days_in_month, // it should not be present in the elapsed days... elapsed_days_without_leap_day_in_past_3_years += 1; } let leap_year = Self::is_leap_year(year as u16); let days_from_year_start = elapsed_days_without_leap_day_in_past_3_years - years_elapsed * 365 - years_elapsed / 4; let mut month = 1; let mut day = days_from_year_start; let mut total_days_in_month = Self::days_in_month(month, leap_year) as u32; while day >= total_days_in_month { day -= total_days_in_month; month += 1; total_days_in_month = Self::days_in_month(month, leap_year) as u32; } day += 1; Self { base_year, seconds: seconds as u8, minutes: minutes as u8, hours: hours as u8, day: day as u8, month: month as u8, year: year as u16, frozen: false, ticks: seconds, } } /// Converts the date into ticks, elapsed seconds /// from base year, that was specified upon creation of the calendar. pub fn to_ticks(&self) -> u32 { let mut ticks = 0u32; ticks += self.seconds as u32; ticks += self.minutes as u32 * 60; ticks += self.hours as u32 * 60 * 60; ticks += (self.day - 1) as u32 * 24 * 60 * 60; ticks += Self::days_in_year(self.month, Self::is_leap_year(self.year)) as u32 * 24 * 60 * 60; let elapsed_years = self.year as u32 - self.base_year as u32; ticks += (elapsed_years * 365 + elapsed_years / 4) * 24 * 60 * 60; for year in max(self.year - 3, self.base_year)..=self.year - 1 { ticks += if Self::is_leap_year(year) { 24 * 60 * 60 } else { 0 }; } ticks } /// Like Calendar::to_ticks, but the ticks may get /// desynchronized at times. Use Calendar::to_ticks /// for correct ticks. pub fn estimated_ticks(&self) -> u32 { self.ticks } /// Adds a second, correctly updating /// minutes, hours, days, month, year... pub fn second_elapsed(&mut self) { if self.frozen { return; } self.seconds = (self.seconds + 1) % 60; let minute_elapsed = self.seconds == 0; self.minutes = (self.minutes + if minute_elapsed { 1 } else { 0 }) % 60; let hour_elapsed = minute_elapsed && self.minutes == 0; self.hours = (self.hours + if hour_elapsed { 1 } else { 0 }) % 24; let day_elapsed = hour_elapsed && self.hours == 0; let day = self.day - 1 + if day_elapsed { 1 } else { 0 }; let days_in_month = Self::days_in_month(self.month, Self::is_leap_year(self.year)); self.day = day % days_in_month + 1; let month_elapsed = day_elapsed && self.day == 1; self.month = (self.month - 1 + if month_elapsed { 1 } else { 0 }) % 12 + 1; let year_elapsed = month_elapsed && self.month == 1; self.year += if year_elapsed { 1 } else { 0 }; self.ticks += 1; } pub fn hours(&self) -> u8 { self.hours } pub fn minutes(&self) -> u8 { self.minutes } pub fn seconds(&self) -> u8 { self.seconds } pub fn day(&self) -> u8 { self.day } pub fn month(&self) -> u8 { self.month } pub fn year(&self) -> u16 { self.year } /// Sets the current hour of the day, /// gets clamped to 0 - 24. pub fn set_hours(&mut self, hours: u8) { self.hours = hours.clamp(0, 23); } /// Sets the current minute of the hour, /// gets clamped to 0 - 59. pub fn set_minutes(&mut self, minutes: u8) { self.minutes = minutes.clamp(0, 59); } /// Sets the current seconds of the minute, /// gets clamped to 0 - 59. pub fn set_seconds(&mut self, seconds: u8) { self.seconds = seconds.clamp(0, 59); } /// Sets the current day of the month, /// gets clamped to 1 - days in the month. pub fn set_day(&mut self, day: u8) { self.day = day.clamp( 1, Self::days_in_month(self.month, Self::is_leap_year(self.year)), ); } /// Sets the current day of the month, /// gets clamped to 1 - days in the month. pub fn set_month(&mut self, month: u8) { self.month = month.clamp(1, 12); } /// Sets the current year, /// The minimum is the base year specified /// upon Calendar creation. Lower year /// will be adjusted to base year. pub fn set_year(&mut self, year: u16) { self.year = max(year, self.base_year); } fn is_leap_year(year: u16) -> bool { matches!(year % 4, 0 if year % 100 != 0 || year % 400 == 0) } pub fn freeze(&mut self) { self.frozen = true; } pub fn unfreeze(&mut self) { self.frozen = false; } fn days_in_month(month: u8, leap_year: bool) -> u8 { match month { 2 if leap_year => 29, 2 => 28, 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, 4 | 6 | 9 | 11 => 30, _ => panic!("Month {month} does not exist, cannot get days in that month."), } } fn days_in_year(before_month: u8, leap_year: bool) -> u16 { let mut days_in_year = 0u16; for i in 1..before_month { days_in_year += Self::days_in_month(i, leap_year) as u16; } days_in_year } }