#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum DateTimeError {
InvalidMonth,
InvalidDay,
InvalidHour,
InvalidMinute,
InvalidSecond,
InvalidWeekday,
InvalidYear,
}
impl core::fmt::Display for DateTimeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
DateTimeError::InvalidMonth => write!(f, "invalid month"),
DateTimeError::InvalidDay => write!(f, "invalid day"),
DateTimeError::InvalidHour => write!(f, "invalid hour"),
DateTimeError::InvalidMinute => write!(f, "invalid minute"),
DateTimeError::InvalidSecond => write!(f, "invalid second"),
DateTimeError::InvalidWeekday => write!(f, "invalid weekday"),
DateTimeError::InvalidYear => write!(f, "invalid year"),
}
}
}
impl core::error::Error for DateTimeError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DateTime {
year: u16,
month: u8,
day_of_month: u8,
hour: u8,
minute: u8,
second: u8,
}
impl DateTime {
pub fn new(
year: u16,
month: u8,
day_of_month: u8,
hour: u8,
minute: u8,
second: u8,
) -> Result<Self, DateTimeError> {
let dt = DateTime {
year,
month,
day_of_month,
hour,
minute,
second,
};
dt.validate()?;
Ok(dt)
}
pub fn validate(&self) -> Result<(), DateTimeError> {
Self::validate_year(self.year)?;
Self::validate_month(self.month)?;
Self::validate_day(self.year, self.month, self.day_of_month)?;
Self::validate_hour(self.hour)?;
Self::validate_minute(self.minute)?;
Self::validate_second(self.second)?;
Ok(())
}
fn validate_year(year: u16) -> Result<(), DateTimeError> {
if year < 1970 {
return Err(DateTimeError::InvalidYear);
}
Ok(())
}
fn validate_month(month: u8) -> Result<(), DateTimeError> {
if month == 0 || month > 12 {
return Err(DateTimeError::InvalidMonth);
}
Ok(())
}
fn validate_day(year: u16, month: u8, day: u8) -> Result<(), DateTimeError> {
let max_day = days_in_month(year, month);
if day == 0 || day > max_day {
return Err(DateTimeError::InvalidDay);
}
Ok(())
}
fn validate_hour(hour: u8) -> Result<(), DateTimeError> {
if hour > 23 {
return Err(DateTimeError::InvalidHour);
}
Ok(())
}
fn validate_minute(minute: u8) -> Result<(), DateTimeError> {
if minute > 59 {
return Err(DateTimeError::InvalidMinute);
}
Ok(())
}
fn validate_second(second: u8) -> Result<(), DateTimeError> {
if second > 59 {
return Err(DateTimeError::InvalidSecond);
}
Ok(())
}
pub fn year(&self) -> u16 {
self.year
}
pub fn month(&self) -> u8 {
self.month
}
pub fn day_of_month(&self) -> u8 {
self.day_of_month
}
pub fn hour(&self) -> u8 {
self.hour
}
pub fn minute(&self) -> u8 {
self.minute
}
pub fn second(&self) -> u8 {
self.second
}
pub fn set_year(&mut self, year: u16) -> Result<(), DateTimeError> {
Self::validate_year(year)?;
Self::validate_day(year, self.month, self.day_of_month)?;
self.year = year;
Ok(())
}
pub fn set_month(&mut self, month: u8) -> Result<(), DateTimeError> {
Self::validate_month(month)?;
Self::validate_day(self.year, month, self.day_of_month)?;
self.month = month;
Ok(())
}
pub fn set_day_of_month(&mut self, day_of_month: u8) -> Result<(), DateTimeError> {
Self::validate_day(self.year, self.month, day_of_month)?;
self.day_of_month = day_of_month;
Ok(())
}
pub fn set_hour(&mut self, hour: u8) -> Result<(), DateTimeError> {
Self::validate_hour(hour)?;
self.hour = hour;
Ok(())
}
pub fn set_minute(&mut self, minute: u8) -> Result<(), DateTimeError> {
Self::validate_minute(minute)?;
self.minute = minute;
Ok(())
}
pub fn set_second(&mut self, second: u8) -> Result<(), DateTimeError> {
Self::validate_second(second)?;
self.second = second;
Ok(())
}
pub fn calculate_weekday(&self) -> Result<Weekday, DateTimeError> {
calculate_weekday(self.year, self.month, self.day_of_month)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Weekday {
Sunday = 1,
Monday = 2,
Tuesday = 3,
Wednesday = 4,
Thursday = 5,
Friday = 6,
Saturday = 7,
}
impl Weekday {
pub fn from_number(n: u8) -> Result<Self, DateTimeError> {
match n {
1 => Ok(Self::Sunday),
2 => Ok(Self::Monday),
3 => Ok(Self::Tuesday),
4 => Ok(Self::Wednesday),
5 => Ok(Self::Thursday),
6 => Ok(Self::Friday),
7 => Ok(Self::Saturday),
_ => Err(DateTimeError::InvalidWeekday),
}
}
pub fn to_number(self) -> u8 {
self as u8
}
pub fn as_str(&self) -> &'static str {
match self {
Weekday::Sunday => "Sunday",
Weekday::Monday => "Monday",
Weekday::Tuesday => "Tuesday",
Weekday::Wednesday => "Wednesday",
Weekday::Thursday => "Thursday",
Weekday::Friday => "Friday",
Weekday::Saturday => "Saturday",
}
}
}
pub fn is_leap_year(year: u16) -> bool {
(year % 4 == 0) && (year % 100 != 0 || year % 400 == 0)
}
pub fn days_in_month(year: u16, month: u8) -> u8 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
_ => 0,
}
}
pub fn calculate_weekday(year: u16, month: u8, day_of_month: u8) -> Result<Weekday, DateTimeError> {
let (year, month) = if month < 3 {
(year - 1, month + 12)
} else {
(year, month)
};
let k = year % 100;
let j = year / 100;
let h =
(day_of_month as u16 + ((13 * (month as u16 + 1)) / 5) + k + (k / 4) + (j / 4) - 2 * j) % 7;
let weekday_num = ((h + 6) % 7) + 1;
Weekday::from_number(weekday_num as u8)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_datetime_creation() {
let dt = DateTime::new(2024, 3, 15, 14, 30, 45).unwrap();
assert_eq!(dt.year(), 2024);
assert_eq!(dt.month(), 3);
assert_eq!(dt.day_of_month(), 15);
assert_eq!(dt.hour(), 14);
assert_eq!(dt.minute(), 30);
assert_eq!(dt.second(), 45);
}
#[test]
fn test_invalid_year() {
let result = DateTime::new(1969, 1, 1, 0, 0, 0);
assert_eq!(result.unwrap_err(), DateTimeError::InvalidYear);
}
#[test]
fn test_invalid_month() {
assert_eq!(
DateTime::new(2024, 0, 1, 0, 0, 0).unwrap_err(),
DateTimeError::InvalidMonth
);
assert_eq!(
DateTime::new(2024, 13, 1, 0, 0, 0).unwrap_err(),
DateTimeError::InvalidMonth
);
}
#[test]
fn test_invalid_day() {
assert_eq!(
DateTime::new(2024, 2, 30, 0, 0, 0).unwrap_err(),
DateTimeError::InvalidDay
);
assert_eq!(
DateTime::new(2024, 1, 0, 0, 0, 0).unwrap_err(),
DateTimeError::InvalidDay
);
assert_eq!(
DateTime::new(2024, 4, 31, 0, 0, 0).unwrap_err(),
DateTimeError::InvalidDay
);
}
#[test]
fn test_invalid_hour() {
assert_eq!(
DateTime::new(2024, 1, 1, 24, 0, 0).unwrap_err(),
DateTimeError::InvalidHour
);
}
#[test]
fn test_invalid_minute() {
assert_eq!(
DateTime::new(2024, 1, 1, 0, 60, 0).unwrap_err(),
DateTimeError::InvalidMinute
);
}
#[test]
fn test_invalid_second() {
assert_eq!(
DateTime::new(2024, 1, 1, 0, 0, 60).unwrap_err(),
DateTimeError::InvalidSecond
);
}
#[test]
fn test_leap_year_february_29() {
assert!(DateTime::new(2024, 2, 29, 0, 0, 0).is_ok());
assert_eq!(
DateTime::new(2023, 2, 29, 0, 0, 0).unwrap_err(),
DateTimeError::InvalidDay
);
}
#[test]
fn test_setters_with_validation() {
let mut dt = DateTime::new(2024, 1, 1, 0, 0, 0).unwrap();
assert!(dt.set_year(2025).is_ok());
assert_eq!(dt.year(), 2025);
assert!(dt.set_month(12).is_ok());
assert_eq!(dt.month(), 12);
assert!(dt.set_hour(23).is_ok());
assert_eq!(dt.hour(), 23);
assert_eq!(dt.set_year(1969), Err(DateTimeError::InvalidYear));
assert_eq!(dt.set_month(13), Err(DateTimeError::InvalidMonth));
assert_eq!(dt.set_hour(24), Err(DateTimeError::InvalidHour));
}
#[test]
fn test_leap_year_edge_cases_in_setters() {
let mut dt = DateTime::new(2024, 2, 29, 0, 0, 0).unwrap();
assert_eq!(dt.set_year(2023), Err(DateTimeError::InvalidDay));
assert_eq!(dt.year(), 2024);
assert_eq!(dt.day_of_month(), 29);
}
#[test]
fn test_month_day_validation_in_setters() {
let mut dt = DateTime::new(2024, 1, 31, 0, 0, 0).unwrap();
assert_eq!(dt.set_month(2), Err(DateTimeError::InvalidDay));
assert_eq!(dt.month(), 1);
assert_eq!(dt.day_of_month(), 31);
assert!(dt.set_month(3).is_ok());
assert_eq!(dt.month(), 3);
}
#[test]
fn test_weekday_calculation() {
let dt = DateTime::new(2024, 1, 1, 0, 0, 0).unwrap(); let weekday = dt.calculate_weekday().unwrap();
assert_eq!(weekday, Weekday::Monday);
let dt = DateTime::new(2024, 12, 25, 0, 0, 0).unwrap();
let weekday = dt.calculate_weekday().unwrap();
assert_eq!(weekday, Weekday::Wednesday); }
#[test]
fn test_weekday_from_number() {
assert_eq!(Weekday::from_number(1).unwrap(), Weekday::Sunday);
assert_eq!(Weekday::from_number(2).unwrap(), Weekday::Monday);
assert_eq!(Weekday::from_number(7).unwrap(), Weekday::Saturday);
assert_eq!(Weekday::from_number(3).unwrap(), Weekday::Tuesday);
assert_eq!(
Weekday::from_number(0).unwrap_err(),
DateTimeError::InvalidWeekday
);
assert_eq!(
Weekday::from_number(8).unwrap_err(),
DateTimeError::InvalidWeekday
);
}
#[test]
fn test_weekday_to_number() {
assert_eq!(Weekday::Sunday.to_number(), 1);
assert_eq!(Weekday::Monday.to_number(), 2);
assert_eq!(Weekday::Saturday.to_number(), 7);
}
#[test]
fn test_weekday_as_str() {
assert_eq!(Weekday::Sunday.as_str(), "Sunday");
assert_eq!(Weekday::Monday.as_str(), "Monday");
assert_eq!(Weekday::Tuesday.as_str(), "Tuesday");
assert_eq!(Weekday::Wednesday.as_str(), "Wednesday");
assert_eq!(Weekday::Thursday.as_str(), "Thursday");
assert_eq!(Weekday::Friday.as_str(), "Friday");
assert_eq!(Weekday::Saturday.as_str(), "Saturday");
}
#[test]
fn test_calculate_weekday_known_dates() {
assert_eq!(calculate_weekday(2000, 1, 1).unwrap(), Weekday::Saturday);
assert_eq!(calculate_weekday(2024, 1, 1).unwrap(), Weekday::Monday);
assert_eq!(calculate_weekday(2025, 8, 15).unwrap(), Weekday::Friday);
assert_eq!(calculate_weekday(2024, 2, 29).unwrap(), Weekday::Thursday); }
#[test]
fn test_is_leap_year() {
assert!(is_leap_year(2024));
assert!(is_leap_year(2020));
assert!(is_leap_year(1996));
assert!(!is_leap_year(2023));
assert!(!is_leap_year(2021));
assert!(!is_leap_year(1999));
assert!(!is_leap_year(1900));
assert!(!is_leap_year(2100));
assert!(is_leap_year(2000));
assert!(is_leap_year(1600));
}
#[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);
assert_eq!(days_in_month(2024, 12), 31);
assert_eq!(days_in_month(2024, 13), 0);
assert_eq!(days_in_month(2024, 0), 0);
}
#[test]
fn test_setter_interdependency_edge_cases() {
let mut dt = DateTime::new(2023, 1, 31, 0, 0, 0).unwrap();
assert_eq!(dt.set_month(2), Err(DateTimeError::InvalidDay));
let mut dt = DateTime::new(2023, 3, 31, 0, 0, 0).unwrap();
assert_eq!(dt.set_month(4), Err(DateTimeError::InvalidDay));
let mut dt = DateTime::new(2024, 2, 29, 0, 0, 0).unwrap();
assert_eq!(dt.set_year(2023), Err(DateTimeError::InvalidDay));
let mut dt = DateTime::new(2023, 2, 28, 0, 0, 0).unwrap();
assert!(dt.set_year(2024).is_ok());
}
#[test]
fn test_display_datetime_error() {
assert_eq!(format!("{}", DateTimeError::InvalidMonth), "invalid month");
assert_eq!(format!("{}", DateTimeError::InvalidDay), "invalid day");
assert_eq!(format!("{}", DateTimeError::InvalidHour), "invalid hour");
assert_eq!(
format!("{}", DateTimeError::InvalidMinute),
"invalid minute"
);
assert_eq!(
format!("{}", DateTimeError::InvalidSecond),
"invalid second"
);
assert_eq!(
format!("{}", DateTimeError::InvalidWeekday),
"invalid weekday"
);
assert_eq!(format!("{}", DateTimeError::InvalidYear), "invalid year");
}
#[test]
fn test_datetime_error_trait() {
let error = DateTimeError::InvalidMonth;
let _: &dyn core::error::Error = &error;
}
#[test]
fn test_boundary_values() {
assert!(DateTime::new(1970, 1, 1, 0, 0, 0).is_ok());
assert!(DateTime::new(2024, 12, 31, 23, 59, 59).is_ok());
assert!(DateTime::new(2024, 1, 1, 0, 0, 0).is_ok());
}
#[test]
fn test_february_edge_cases() {
assert!(DateTime::new(2024, 2, 28, 0, 0, 0).is_ok());
assert!(DateTime::new(2023, 2, 28, 0, 0, 0).is_ok());
assert_eq!(
DateTime::new(2023, 2, 29, 0, 0, 0).unwrap_err(),
DateTimeError::InvalidDay
);
}
#[test]
fn test_all_month_max_days() {
let year = 2023;
for month in [1, 3, 5, 7, 8, 10, 12] {
assert!(DateTime::new(year, month, 31, 0, 0, 0).is_ok());
assert_eq!(
DateTime::new(year, month, 32, 0, 0, 0).unwrap_err(),
DateTimeError::InvalidDay
);
}
for month in [4, 6, 9, 11] {
assert!(DateTime::new(year, month, 30, 0, 0, 0).is_ok());
assert_eq!(
DateTime::new(year, month, 31, 0, 0, 0).unwrap_err(),
DateTimeError::InvalidDay
);
}
}
#[test]
fn test_set_day_of_month() {
let mut dt = DateTime::new(2024, 5, 15, 12, 30, 45).unwrap();
assert!(dt.set_day_of_month(10).is_ok());
assert_eq!(dt.day_of_month, 10);
}
#[test]
fn test_all_setters_preserve_state_on_error() {
let mut dt = DateTime::new(2024, 5, 15, 12, 30, 45).unwrap();
let original = dt;
assert!(dt.set_day_of_month(40).is_err());
assert_eq!(dt, original);
assert!(dt.set_minute(70).is_err());
assert_eq!(dt, original);
assert!(dt.set_second(70).is_err());
assert_eq!(dt, original);
}
#[test]
fn test_set_minute() {
let mut dt = DateTime::new(2024, 5, 15, 12, 30, 45).unwrap();
assert!(dt.set_minute(10).is_ok());
assert_eq!(dt.minute, 10);
}
#[test]
fn test_set_second() {
let mut dt = DateTime::new(2024, 5, 15, 12, 30, 45).unwrap();
assert!(dt.set_second(10).is_ok());
assert_eq!(dt.second, 10);
}
}