use std::str::FromStr; use chrono::{DateTime, Datelike, FixedOffset, Utc}; use crate::holiday_utils::common::{ HolidayDetails, HolidayInfo, SupportedCountries, DAY, MONTH, YEAR, }; use crate::holiday_utils::fixed_holidays::FixedHolidays; use crate::holiday_utils::movable_holidays::MovableHolidays; /// Represents a holiday engine that processes and retrieves holiday information pub struct HolidayEngine<'a> { movable_holidays: MovableHolidays, holiday_details: &'a HolidayDetails, } impl<'a> HolidayEngine<'a> { /// Creates a new instance of HolidayEngine /// /// # Arguments /// /// * `holiday_details` - A reference to HolidayDetails containing information about the holiday /// /// # Returns /// /// A new HolidayEngine instance pub fn new(holiday_details: &'a HolidayDetails) -> Self { Self { movable_holidays: MovableHolidays::new(), holiday_details, } } pub fn location_bound_holiday_check( &self, holiday_name: &str, from_time: &DateTime, ) -> Option { self.holiday_details.location.as_deref().map_or_else( || { Some(HolidayInfo::new( holiday_name, ( self.holiday_details.year.parse().unwrap_or_default(), from_time.month() as MONTH, from_time.day() as DAY, ), )) }, |country| { SupportedCountries::from_str(country).ok().and_then(|loc| { if loc != SupportedCountries::US { self.movable_holidays .get_movable_holiday( holiday_name, from_time.year() as YEAR, Some(country), ) .map_or_else( || self.get_fixed_holiday_date(holiday_name, Some(country)), |(day, month)| { Some(HolidayInfo::new( holiday_name, (from_time.year() as YEAR, month, day), )) }, ) } else { None } }) }, ) } /// Retrieves holiday information based on the provided holiday details /// /// # Returns /// /// An Option containing HolidayInfo if a holiday is found, None otherwise pub fn get_holiday(&self) -> Option { match ( &self.holiday_details.holiday, &self.holiday_details.from_time, ) { (Some(holiday_name), Some(from_time)) => { //there are holidays which are same but celebrated on different days based on location but identified by drake //only for US self.location_bound_holiday_check(holiday_name, from_time) } (Some(holiday_name), None) => self .get_fixed_holiday_date(holiday_name, self.holiday_details.location.as_deref()) .or_else(|| { self.holiday_details.year.parse().ok().and_then(|year| { self.get_movable_holiday_date( holiday_name, year, self.holiday_details.location.as_deref(), ) }) }), (None, Some(from_time)) => self.is_holiday_exist( from_time.to_owned(), self.holiday_details.location.as_deref(), ), _ => None, } } /// Retrieves information for a fixed holiday given its name /// /// # Arguments /// /// * `holiday_name` - The name of the holiday /// * `country` - An optional country name to check for country-specific holidays /// /// # Returns /// /// An Option containing HolidayInfo if a fixed holiday is found, None otherwise pub fn get_fixed_holiday_date( &self, holiday_name: &str, country: Option<&str>, ) -> Option { FixedHolidays::from_str(holiday_name) .ok() .and_then(|holiday| { let year = Utc::now().year() as YEAR; HolidayInfo::from_fixed_holidays(year, holiday, country) }) } /// Checks if the given date is a holiday /// /// # Arguments /// /// * `year` - The year to check /// * `month` - The month to check /// * `day` - The day to check /// /// # Returns /// /// An Option containing HolidayInfo if the date is a holiday, None otherwise pub fn is_holiday(&self, year: YEAR, month: MONTH, day: DAY) -> Option { FixedHolidays::is_holiday(month, day) .and_then(|holiday| HolidayInfo::from_fixed_holidays(year, holiday, None)) .or_else(|| { self.movable_holidays .is_holiday(year, month, day) .map(|holiday| { HolidayInfo::new(holiday.to_string().as_str(), (year, month, day)) }) }) } /// Checks if a holiday exists for the given date and country /// /// # Arguments /// /// * `from_time` - The date and time to check /// * `country` - An optional country name to check for country-specific holidays /// /// # Returns /// /// An Option containing HolidayInfo if a holiday is found, None otherwise pub fn is_holiday_exist( &self, from_time: DateTime, country: Option<&str>, ) -> Option { let year = from_time.year() as YEAR; let month = from_time.month() as MONTH; let day = from_time.day() as DAY; country.map_or_else( || self.is_holiday(year, month, day), |location| self.is_holiday_in_country(location, year, month, day), ) } /// Checks if the given date is a holiday in the specified country /// /// # Arguments /// /// * `country` - The country to check /// * `year` - The year to check /// * `month` - The month to check /// * `day` - The day to check /// /// # Returns /// /// An Option containing HolidayInfo if the date is a holiday in the specified country, None otherwise pub fn is_holiday_in_country( &self, country: &str, year: YEAR, month: MONTH, day: DAY, ) -> Option { FixedHolidays::is_holiday_in_country(month, day, country) .and_then(|holiday| HolidayInfo::from_fixed_holidays(year, holiday, Some(country))) .or_else(|| { self.movable_holidays .is_holiday_in_country(year, month, day, country) .map(|holiday| { HolidayInfo::new(holiday.to_string().as_str(), (year, month, day)) }) }) } /// Retrieves the date of a movable holiday for the given year /// /// # Arguments /// /// * `holiday_name` - The name of the holiday /// * `year` - The year to check /// * `country` - An optional country name to check for country-specific holidays /// /// # Returns /// /// An Option containing HolidayInfo if the movable holiday is found, None otherwise pub fn get_movable_holiday_date( &self, holiday_name: &str, year: YEAR, country: Option<&str>, ) -> Option { self.movable_holidays .get_movable_holiday(holiday_name, year, country) .map(|(day, month)| HolidayInfo::new(holiday_name, (year, month, day))) } } #[cfg(test)] mod tests { use super::*; use chrono::{NaiveDate, TimeZone}; /// Tests for get_holiday with name and from_time #[test] fn test_get_holiday_with_name_and_from_time() { let details = HolidayDetails { holiday: Some("christmas".to_string()), from_time: Some( NaiveDate::from_ymd_opt(2024, 12, 25) .unwrap() .and_hms_opt(0, 0, 0) .unwrap() .and_local_timezone(Utc) .unwrap() .fixed_offset(), ), year: "2024".to_string(), location: None, to_time: Some( NaiveDate::from_ymd_opt(2024, 12, 25) .unwrap() .and_hms_opt(0, 0, 0) .unwrap() .and_local_timezone(Utc) .unwrap() .fixed_offset(), ), time_of_day: "".to_string(), }; let engine = HolidayEngine::new(&details); let result = engine.get_holiday(); assert_eq!(result, Some(HolidayInfo::new("christmas", (2024, 12, 25)))); } /// Tests for get_holiday with name only #[test] fn test_get_holiday_with_name() { let mut details = HolidayDetails { holiday: Some("culture day".to_string()), from_time: None, year: "2024".to_string(), location: None, to_time: None, time_of_day: "".to_string(), }; let mut engine = HolidayEngine::new(&details); let mut result = engine.get_holiday(); assert_eq!(result, Some(HolidayInfo::new("Culture Day", (2024, 11, 3)))); details.holiday = Some("victoria day".to_string()); details.location = Some("france".to_string()); engine = HolidayEngine::new(&details); result = engine.get_holiday(); assert_eq!(result, None); details.location = Some("canada".to_string()); engine = HolidayEngine::new(&details); result = engine.get_holiday(); assert_eq!( result, Some(HolidayInfo::new("victoria day", (2024, 5, 20))) ); } /// Tests for get_independence_day #[test] fn test_get_independence_day() { let mut details = HolidayDetails { holiday: Some("independence day".to_string()), from_time: None, year: "2024".to_string(), location: None, to_time: None, time_of_day: "".to_string(), }; let mut engine = HolidayEngine::new(&details); let mut result = engine.get_holiday(); assert_eq!( result, Some(HolidayInfo::new("Independence Day", (2024, 7, 4))) ); details.location = Some("canada".to_string()); engine = HolidayEngine::new(&details); result = engine.get_holiday(); assert_eq!( result, Some(HolidayInfo::new("Independence Day", (2024, 7, 1))) ); } /// Tests for get_holiday with fixed holidays #[test] fn test_get_holiday_fixed_holiday() { let details = HolidayDetails { holiday: Some("new year".to_string()), from_time: None, to_time: None, year: "2024".to_string(), location: None, time_of_day: "".to_string(), }; let engine = HolidayEngine::new(&details); let result = engine.get_holiday(); assert_eq!( result, Some(HolidayInfo::new("New Year's Day", (2024, 1, 1))) ); } /// Tests for get_holiday with movable holidays #[test] fn test_get_holiday_movable_holiday() { let mut details = HolidayDetails { holiday: Some("Easter Sunday".to_string()), from_time: None, to_time: None, year: "2024".to_string(), location: None, time_of_day: "".to_string(), }; let mut engine = HolidayEngine::new(&details); let mut result = engine.get_holiday(); assert_eq!( result, Some(HolidayInfo::new("Easter Sunday", (2024, 3, 31))) ); details.holiday = Some("Aged Day".to_string()); engine = HolidayEngine::new(&details); result = engine.get_holiday(); assert_eq!(result, Some(HolidayInfo::new("Aged Day", (2024, 9, 16)))); details.holiday = Some("Whit Monday".to_string()); engine = HolidayEngine::new(&details); result = engine.get_holiday(); assert_eq!(result, Some(HolidayInfo::new("Whit Monday", (2024, 5, 20)))); details.holiday = Some("Victoria Day".to_string()); engine = HolidayEngine::new(&details); result = engine.get_holiday(); assert_eq!( result, Some(HolidayInfo::new("Victoria Day", (2024, 5, 20))) ); } /// Tests for get_holiday with from_time only #[test] fn test_get_holiday_from_time_only() { let mut details = HolidayDetails { holiday: None, from_time: Some( NaiveDate::from_ymd_opt(2024, 12, 25) .unwrap() .and_hms_opt(0, 0, 0) .unwrap() .and_local_timezone(Utc) .unwrap() .fixed_offset(), ), to_time: None, year: "2024".to_string(), location: Some("US".to_string()), time_of_day: "".to_string(), }; let mut engine = HolidayEngine::new(&details); let mut result = engine.get_holiday(); assert_eq!(result, Some(HolidayInfo::new("Christmas", (2024, 12, 25)))); details.from_time = Some( NaiveDate::from_ymd_opt(2024, 5, 20) .unwrap() .and_hms_opt(0, 0, 0) .unwrap() .and_local_timezone(Utc) .unwrap() .fixed_offset(), ); details.location = None; engine = HolidayEngine::new(&details); result = engine.get_holiday(); assert_eq!( result, Some(HolidayInfo::new("Victoria Day", (2024, 5, 20))) ); details.from_time = Some( NaiveDate::from_ymd_opt(2024, 5, 20) .unwrap() .and_hms_opt(0, 0, 0) .unwrap() .and_local_timezone(Utc) .unwrap() .fixed_offset(), ); details.location = Some("france".to_string()); engine = HolidayEngine::new(&details); result = engine.get_holiday(); assert_eq!(result, Some(HolidayInfo::new("Whit Monday", (2024, 5, 20)))); details.from_time = Some( NaiveDate::from_ymd_opt(2025, 6, 9) .unwrap() .and_hms_opt(0, 0, 0) .unwrap() .and_local_timezone(Utc) .unwrap() .fixed_offset(), ); details.year = "2025".to_string(); engine = HolidayEngine::new(&details); result = engine.get_holiday(); assert_eq!(result, Some(HolidayInfo::new("Whit Monday", (2025, 6, 9)))); } /// Tests for get_holiday with from_time and Canada Day #[test] fn test_get_holiday_from_time_canada_day() { let details = HolidayDetails { holiday: None, from_time: Some( NaiveDate::from_ymd_opt(2024, 7, 1) .unwrap() .and_hms_opt(0, 0, 0) .unwrap() .and_local_timezone(Utc) .unwrap() .fixed_offset(), ), to_time: None, year: "2024".to_string(), location: Some("canada".to_string()), time_of_day: "".to_string(), }; let engine = HolidayEngine::new(&details); let result = engine.get_holiday(); assert_eq!(result, Some(HolidayInfo::new("Canada Day", (2024, 7, 1)))); } /// Tests for get_holiday with no holiday #[test] fn test_get_holiday_no_holiday() { let details = HolidayDetails { holiday: None, from_time: Some( NaiveDate::from_ymd_opt(2024, 3, 15) .unwrap() .and_hms_opt(0, 0, 0) .unwrap() .and_local_timezone(Utc) .unwrap() .fixed_offset(), ), to_time: None, year: "2024".to_string(), location: None, time_of_day: "".to_string(), }; let engine = HolidayEngine::new(&details); let result = engine.get_holiday(); assert_eq!(result, None); } /// Tests for get_fixed_holiday_date #[test] fn test_get_fixed_holiday_date() { let details = HolidayDetails::default(); let engine = HolidayEngine::new(&details); assert_eq!( engine.get_fixed_holiday_date("christmas", None), Some(HolidayInfo::new( "Christmas", (Utc::now().year() as YEAR, 12, 25) )) ); assert_eq!( engine.get_fixed_holiday_date("independence day", Some("US")), Some(HolidayInfo::new( "Independence Day", (Utc::now().year() as YEAR, 7, 4) )) ); assert_eq!( engine.get_fixed_holiday_date("non-existent holiday", None), None ); } /// Tests for is_holiday #[test] fn test_is_holiday() { let details = HolidayDetails::default(); let engine = HolidayEngine::new(&details); assert_eq!( engine.is_holiday(2024, 1, 1), Some(HolidayInfo::new("New Year's Day", (2024, 1, 1))) ); assert_eq!( engine.is_holiday(2024, 12, 25), Some(HolidayInfo::new("Christmas", (2024, 12, 25))) ); assert_eq!( engine.is_holiday(2024, 3, 31), Some(HolidayInfo::new("Easter Sunday", (2024, 3, 31))) ); assert_eq!(engine.is_holiday(2024, 3, 15), None); } /// Tests for is_holiday_in_country #[test] fn test_is_holiday_in_country() { let details = HolidayDetails::default(); let engine = HolidayEngine::new(&details); assert_eq!( engine.is_holiday_in_country("US", 2024, 7, 4), Some(HolidayInfo::new("Independence Day", (2024, 7, 4))) ); assert_eq!( engine.is_holiday_in_country("Canada", 2024, 7, 1), Some(HolidayInfo::new("Canada Day", (2024, 7, 1))) ); assert_eq!(engine.is_holiday_in_country("France", 2024, 7, 4), None); } /// Tests for get_movable_holiday_date #[test] fn test_get_movable_holiday_date() { let details = HolidayDetails::default(); let engine = HolidayEngine::new(&details); assert_eq!( engine.get_movable_holiday_date("easter sunday", 2024, None), Some(HolidayInfo::new("easter sunday", (2024, 3, 31))) ); assert_eq!( engine.get_movable_holiday_date("victoria day", 2024, Some("Canada")), Some(HolidayInfo::new("victoria day", (2024, 5, 20))) ); assert_eq!( engine.get_movable_holiday_date("non-existent holiday", 2024, None), None ); } /// Tests for get_holiday with invalid inputs #[test] fn test_get_holiday_invalid_inputs() { let details = HolidayDetails { holiday: None, from_time: None, year: "invalid".to_string(), location: None, to_time: None, time_of_day: "".to_string(), }; let engine = HolidayEngine::new(&details); let result = engine.get_holiday(); assert_eq!(result, None); } #[test] fn test_location_bound_holiday_check() { let details = HolidayDetails { holiday: Some("test holiday".to_string()), from_time: None, year: "2024".to_string(), location: None, to_time: None, time_of_day: "".to_string(), }; let engine = HolidayEngine::new(&details); // Test with no location let from_time = Utc .with_ymd_and_hms(2024, 7, 4, 0, 0, 0) .unwrap() .fixed_offset(); let result = engine.location_bound_holiday_check("independence day", &from_time); assert_eq!( result, Some(HolidayInfo::new("independence day", (2024, 7, 4))) ); // Test with US location (should return None as per the function logic) let details_us = HolidayDetails { location: Some("US".to_string()), holiday: details.holiday.clone(), from_time: details.from_time, year: details.year.clone(), to_time: details.to_time, time_of_day: details.time_of_day.clone(), }; let engine_us = HolidayEngine::new(&details_us); let result_us = engine_us.location_bound_holiday_check("independence day", &from_time); assert_eq!(result_us, None); // Test with non-US location (e.g., Canada) let details_canada = HolidayDetails { location: Some("Canada".to_string()), holiday: details.holiday.clone(), from_time: details.from_time, year: details.year.clone(), to_time: details.to_time, time_of_day: details.time_of_day.clone(), }; let engine_canada = HolidayEngine::new(&details_canada); let from_time_canada = Utc .with_ymd_and_hms(2024, 7, 1, 0, 0, 0) .unwrap() .fixed_offset(); let result_canada = engine_canada.location_bound_holiday_check("canada day", &from_time_canada); assert_eq!( result_canada, Some(HolidayInfo::new("Canada Day", (2024, 7, 1))) ); // Test with non-existent holiday let result_non_existent = engine.location_bound_holiday_check("non-existent holiday", &from_time); assert_eq!( result_non_existent, Some(HolidayInfo::new("non-existent holiday", (2024, 7, 4))) ); } }