use crate::shared::shared;
use crate::time::calendar::{Calendar, CalendarImpl};
use crate::time::date::{Date, Month, Year};
use crate::time::weekday::Weekday;
pub const HOLIDAY_HORIZON: Year = 2029;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Market {
Tadawul,
}
pub struct SaudiArabia;
impl SaudiArabia {
pub fn new(market: Market) -> Calendar {
let imp: crate::shared::Shared<dyn CalendarImpl> = match market {
Market::Tadawul => shared(TadawulImpl),
};
Calendar::from_impl(imp)
}
}
fn is_true_weekend(d: Date) -> bool {
let w = d.weekday();
if d < Date::new(29, Month::June, 2013) {
w == Weekday::Thursday || w == Weekday::Friday
} else {
w == Weekday::Friday || w == Weekday::Saturday
}
}
type Window = ((i32, Month, i32), (i32, Month, i32));
fn in_any_window(d: Date, windows: &[Window]) -> bool {
windows.iter().any(|&((d1, m1, y1), (d2, m2, y2))| {
d >= Date::new(d1, m1, y1) && d <= Date::new(d2, m2, y2)
})
}
fn is_eid_al_adha(d: Date) -> bool {
const EID_AL_ADHA: &[Window] = &[
((6, Month::April, 1998), (11, Month::April, 1998)),
((26, Month::March, 1999), (31, Month::March, 1999)),
((15, Month::March, 2000), (20, Month::March, 2000)),
((4, Month::March, 2001), (9, Month::March, 2001)),
((22, Month::February, 2002), (27, Month::February, 2002)),
((11, Month::February, 2003), (16, Month::February, 2003)),
((31, Month::January, 2004), (5, Month::February, 2004)),
((20, Month::January, 2005), (25, Month::January, 2005)),
((9, Month::January, 2006), (14, Month::January, 2006)),
((30, Month::December, 2006), (4, Month::January, 2007)),
((19, Month::December, 2007), (24, Month::December, 2007)),
((7, Month::December, 2008), (12, Month::December, 2008)),
((26, Month::November, 2009), (1, Month::December, 2009)),
((15, Month::November, 2010), (20, Month::November, 2010)),
((5, Month::November, 2011), (10, Month::November, 2011)),
((25, Month::October, 2012), (3, Month::November, 2012)),
((11, Month::October, 2013), (19, Month::October, 2013)),
((3, Month::October, 2014), (11, Month::October, 2014)),
((22, Month::September, 2015), (27, Month::September, 2015)),
((10, Month::September, 2016), (15, Month::September, 2016)),
((31, Month::August, 2017), (5, Month::September, 2017)),
((22, Month::August, 2018), (27, Month::August, 2018)),
((11, Month::August, 2019), (16, Month::August, 2019)),
((30, Month::July, 2020), (4, Month::August, 2020)),
((19, Month::July, 2021), (24, Month::July, 2021)),
((9, Month::July, 2022), (14, Month::July, 2022)),
((23, Month::June, 2023), (1, Month::July, 2023)),
((14, Month::June, 2024), (22, Month::June, 2024)),
((5, Month::June, 2025), (10, Month::June, 2025)),
((22, Month::May, 2026), (30, Month::May, 2026)),
((16, Month::May, 2027), (20, Month::May, 2027)),
((3, Month::May, 2028), (9, Month::May, 2028)),
((22, Month::April, 2029), (26, Month::April, 2029)),
];
in_any_window(d, EID_AL_ADHA)
}
fn is_eid_al_fitr(d: Date) -> bool {
const EID_AL_FITR: &[Window] = &[
((15, Month::December, 2001), (20, Month::December, 2001)),
((4, Month::December, 2002), (9, Month::December, 2002)),
((24, Month::November, 2003), (29, Month::November, 2003)),
((12, Month::November, 2004), (17, Month::November, 2004)),
((2, Month::November, 2005), (7, Month::November, 2005)),
((22, Month::October, 2006), (27, Month::October, 2006)),
((11, Month::October, 2007), (16, Month::October, 2007)),
((29, Month::September, 2008), (4, Month::October, 2008)),
((19, Month::September, 2009), (24, Month::September, 2009)),
((9, Month::September, 2010), (14, Month::September, 2010)),
((29, Month::August, 2011), (3, Month::September, 2011)),
((16, Month::August, 2012), (24, Month::August, 2012)),
((6, Month::August, 2013), (12, Month::August, 2013)),
((25, Month::July, 2014), (3, Month::August, 2014)),
((18, Month::July, 2015), (21, Month::July, 2015)),
((5, Month::July, 2016), (10, Month::July, 2016)),
((24, Month::June, 2017), (29, Month::June, 2017)),
((14, Month::June, 2018), (19, Month::June, 2018)),
((3, Month::June, 2019), (8, Month::June, 2019)),
((23, Month::May, 2020), (28, Month::May, 2020)),
((12, Month::May, 2021), (17, Month::May, 2021)),
((1, Month::May, 2022), (6, Month::May, 2022)),
((17, Month::April, 2023), (25, Month::April, 2023)),
((4, Month::April, 2024), (14, Month::April, 2024)),
((27, Month::March, 2025), (2, Month::April, 2025)),
((17, Month::March, 2026), (23, Month::March, 2026)),
((7, Month::March, 2027), (11, Month::March, 2027)),
((27, Month::February, 2028), (2, Month::March, 2028)),
((12, Month::February, 2029), (18, Month::February, 2029)),
];
in_any_window(d, EID_AL_FITR)
}
struct TadawulImpl;
impl CalendarImpl for TadawulImpl {
fn holiday_horizon(&self) -> Option<Year> {
Some(HOLIDAY_HORIZON)
}
fn name(&self) -> String {
"Tadawul".to_string()
}
fn is_weekend(&self, w: Weekday) -> bool {
w == Weekday::Friday || w == Weekday::Saturday
}
fn is_weekend_on(&self, date: Date) -> bool {
is_true_weekend(date)
}
fn is_business_day(&self, date: Date) -> bool {
let d = date.day_of_month();
let m = date.month();
let y = date.year();
let w = date.weekday();
!(is_true_weekend(date)
|| is_eid_al_adha(date)
|| is_eid_al_fitr(date)
|| (d == 23 && m == Month::September)
|| ((d == 22 || ((d == 23 || d == 24) && w == Weekday::Sunday))
&& m == Month::February
&& y >= 2022)
|| (d == 26 && m == Month::February && y == 2011)
|| (d == 19 && m == Month::March && y == 2011))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn name_matches_quantlib() {
assert_eq!(SaudiArabia::new(Market::Tadawul).name(), "Tadawul");
}
#[test]
fn national_day_is_holiday() {
let c = SaudiArabia::new(Market::Tadawul);
assert!(c.is_holiday(Date::new(23, Month::September, 2019)));
}
#[test]
fn weekend_rule() {
let c = SaudiArabia::new(Market::Tadawul);
assert!(c.is_weekend(Weekday::Friday));
assert!(c.is_weekend(Weekday::Saturday));
assert!(!c.is_weekend(Weekday::Sunday));
}
#[test]
fn in_horizon_query_works() {
let c = SaudiArabia::new(Market::Tadawul);
assert!(c.is_holiday(Date::new(23, Month::September, HOLIDAY_HORIZON)));
}
#[test]
fn beyond_horizon_falls_through() {
let c = SaudiArabia::new(Market::Tadawul);
assert!(c.is_holiday(Date::new(23, Month::September, 2030)));
assert!(c.is_holiday(Date::new(24, Month::February, 2030)));
assert!(c.is_business_day(Date::new(5, Month::February, 2030)));
}
#[test]
fn eid_windows_follow_quantlib_v1_44_tables() {
let c = SaudiArabia::new(Market::Tadawul);
assert!(c.is_holiday(Date::new(13, Month::October, 2013)));
assert!(c.is_holiday(Date::new(28, Month::June, 2023)));
assert!(c.is_business_day(Date::new(2, Month::July, 2023)));
}
#[test]
fn founding_day_starts_in_2022() {
let c = SaudiArabia::new(Market::Tadawul);
assert!(c.is_business_day(Date::new(22, Month::February, 2021)));
assert!(c.is_holiday(Date::new(22, Month::February, 2022)));
assert!(c.is_holiday(Date::new(22, Month::February, 2027)));
}
#[test]
fn saudi_arabia_founding_day_and_eid_holidays() {
let saudi = SaudiArabia::new(Market::Tadawul);
let hol = |d, m, y| assert!(saudi.is_holiday(Date::new(d, m, y)), "{d} {m:?} {y}");
let bus = |d, m, y| assert!(saudi.is_business_day(Date::new(d, m, y)), "{d} {m:?} {y}");
hol(23, Month::February, 2025);
hol(22, Month::February, 2026);
bus(19, Month::February, 2026);
hol(21, Month::July, 2015);
bus(16, Month::July, 2015);
bus(22, Month::July, 2015);
hol(22, Month::September, 2015);
bus(21, Month::September, 2015);
bus(28, Month::September, 2015);
hol(5, Month::June, 2025);
hol(10, Month::June, 2025);
bus(4, Month::June, 2025);
bus(11, Month::June, 2025);
hol(20, Month::March, 2026);
bus(16, Month::March, 2026);
bus(24, Month::March, 2026);
hol(25, Month::May, 2026);
bus(21, Month::May, 2026);
bus(31, Month::May, 2026);
}
#[rustfmt::skip]
const SOURCE_HOLIDAYS: &[(Year, &[(i32, i32)])] = &[
(2022, &[(22, 2), (1, 5), (2, 5), (3, 5), (4, 5), (5, 5), (10, 7), (11, 7), (12, 7),
(13, 7), (14, 7)]),
(2023, &[(22, 2), (17, 4), (18, 4), (19, 4), (20, 4), (23, 4), (24, 4), (25, 4),
(25, 6), (26, 6), (27, 6), (28, 6), (29, 6)]),
(2024, &[(22, 2), (4, 4), (7, 4), (8, 4), (9, 4), (10, 4), (11, 4), (14, 4), (16, 6),
(17, 6), (18, 6), (19, 6), (20, 6), (23, 9)]),
(2025, &[(23, 2), (27, 3), (30, 3), (31, 3), (1, 4), (2, 4), (5, 6), (8, 6), (9, 6),
(10, 6), (23, 9)]),
(2026, &[(22, 2), (17, 3), (18, 3), (19, 3), (22, 3), (23, 3), (24, 5), (25, 5),
(26, 5), (27, 5), (28, 5), (23, 9)]),
(2027, &[(22, 2), (7, 3), (8, 3), (9, 3), (10, 3), (11, 3), (16, 5), (17, 5), (18, 5),
(19, 5), (20, 5), (23, 9)]),
(2028, &[(22, 2), (27, 2), (28, 2), (29, 2), (1, 3), (2, 3), (3, 5), (4, 5), (7, 5),
(8, 5), (9, 5)]),
(2029, &[(12, 2), (13, 2), (14, 2), (15, 2), (18, 2), (22, 2), (22, 4), (23, 4),
(24, 4), (25, 4), (26, 4), (23, 9)]),
];
#[test]
fn holiday_lists_match_quantlib_v1_44_source() {
let c = SaudiArabia::new(Market::Tadawul);
for &(y, expected) in SOURCE_HOLIDAYS {
let expected: Vec<Date> = expected
.iter()
.map(|&(d, m)| Date::new(d, Month::from_ordinal(m), y))
.collect();
let got = c.holiday_list(
Date::new(1, Month::January, y),
Date::new(31, Month::December, y),
false,
);
assert_eq!(got, expected, "{y}");
}
}
}