use std::collections::BTreeSet;
use std::fmt;
use crate::shared::{Shared, SharedMut, shared_mut};
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::date::{Date, Day, SerialNumber, Year};
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
use crate::time::weekday::Weekday;
use crate::types::Integer;
pub trait CalendarImpl {
fn name(&self) -> String;
fn is_business_day(&self, date: Date) -> bool;
fn is_weekend(&self, weekday: Weekday) -> bool;
fn is_weekend_on(&self, date: Date) -> bool {
self.is_weekend(date.weekday())
}
}
pub fn is_weekend_sat_sun(w: Weekday) -> bool {
w == Weekday::Saturday || w == Weekday::Sunday
}
#[derive(Clone)]
pub struct Calendar {
imp: Shared<dyn CalendarImpl>,
added_holidays: SharedMut<BTreeSet<Date>>,
removed_holidays: SharedMut<BTreeSet<Date>>,
}
impl Calendar {
pub fn from_impl(imp: Shared<dyn CalendarImpl>) -> Calendar {
Calendar {
imp,
added_holidays: shared_mut(BTreeSet::new()),
removed_holidays: shared_mut(BTreeSet::new()),
}
}
pub fn name(&self) -> String {
self.imp.name()
}
pub fn is_business_day(&self, d: Date) -> bool {
if self.added_holidays.borrow().contains(&d) {
return false;
}
if self.removed_holidays.borrow().contains(&d) {
return true;
}
self.imp.is_business_day(d)
}
pub fn is_holiday(&self, d: Date) -> bool {
!self.is_business_day(d)
}
pub fn is_weekend(&self, w: Weekday) -> bool {
self.imp.is_weekend(w)
}
pub fn is_weekend_on(&self, d: Date) -> bool {
self.imp.is_weekend_on(d)
}
pub fn added_holidays(&self) -> BTreeSet<Date> {
self.added_holidays.borrow().clone()
}
pub fn removed_holidays(&self) -> BTreeSet<Date> {
self.removed_holidays.borrow().clone()
}
pub fn reset_added_and_removed_holidays(&self) {
self.added_holidays.borrow_mut().clear();
self.removed_holidays.borrow_mut().clear();
}
pub fn add_holiday(&self, d: Date) {
self.removed_holidays.borrow_mut().remove(&d);
if self.imp.is_business_day(d) {
self.added_holidays.borrow_mut().insert(d);
}
}
pub fn remove_holiday(&self, d: Date) {
self.added_holidays.borrow_mut().remove(&d);
if !self.imp.is_business_day(d) {
self.removed_holidays.borrow_mut().insert(d);
}
}
pub fn is_start_of_month(&self, d: Date) -> bool {
d <= self.start_of_month(d)
}
pub fn start_of_month(&self, d: Date) -> Date {
self.adjust(Date::start_of_month(d), BusinessDayConvention::Following)
}
pub fn is_end_of_month(&self, d: Date) -> bool {
d >= self.end_of_month(d)
}
pub fn end_of_month(&self, d: Date) -> Date {
self.adjust(Date::end_of_month(d), BusinessDayConvention::Preceding)
}
pub fn holiday_list(&self, from: Date, to: Date, include_weekends: bool) -> Vec<Date> {
let mut result = Vec::new();
let mut d = from;
while d <= to {
if self.is_holiday(d) && (include_weekends || !self.is_weekend_on(d)) {
result.push(d);
}
if d == to {
break;
}
d += 1;
}
result
}
pub fn business_day_list(&self, from: Date, to: Date) -> Vec<Date> {
let mut result = Vec::new();
let mut d = from;
while d <= to {
if self.is_business_day(d) {
result.push(d);
}
if d == to {
break;
}
d += 1;
}
result
}
pub fn adjust(&self, d: Date, c: BusinessDayConvention) -> Date {
use BusinessDayConvention::*;
assert!(d != Date::null(), "null date");
if c == Unadjusted {
return d;
}
let mut d1 = d;
if c == Following || c == ModifiedFollowing || c == HalfMonthModifiedFollowing {
while self.is_holiday(d1) {
d1 += 1;
}
if c == ModifiedFollowing || c == HalfMonthModifiedFollowing {
if d1.month() != d.month() {
return self.adjust(d, Preceding);
}
if c == HalfMonthModifiedFollowing
&& d.day_of_month() <= 15
&& d1.day_of_month() > 15
{
return self.adjust(d, Preceding);
}
}
d1
} else if c == Preceding || c == ModifiedPreceding {
while self.is_holiday(d1) {
d1 -= 1;
}
if c == ModifiedPreceding && d1.month() != d.month() {
return self.adjust(d, Following);
}
d1
} else if c == Nearest {
let mut d2 = d;
while self.is_holiday(d1) && self.is_holiday(d2) {
d1 += 1;
d2 -= 1;
}
if self.is_holiday(d1) { d2 } else { d1 }
} else {
panic!("unknown business-day convention");
}
}
pub fn advance(
&self,
d: Date,
mut n: Integer,
unit: TimeUnit,
c: BusinessDayConvention,
end_of_month: bool,
) -> Date {
assert!(d != Date::null(), "null date");
if n == 0 {
return self.adjust(d, c);
}
match unit {
TimeUnit::Days => {
let mut d1 = d;
if n > 0 {
while n > 0 {
d1 += 1;
while self.is_holiday(d1) {
d1 += 1;
}
n -= 1;
}
} else {
while n < 0 {
d1 -= 1;
while self.is_holiday(d1) {
d1 -= 1;
}
n += 1;
}
}
d1
}
TimeUnit::Weeks => {
let d1 = d + Period::new(n, unit);
self.adjust(d1, c)
}
_ => {
let d1 = d + Period::new(n, unit);
if end_of_month {
if c == BusinessDayConvention::Unadjusted {
if Date::is_end_of_month(d) {
return Date::end_of_month(d1);
}
} else if self.is_end_of_month(d) {
return self.end_of_month(d1);
}
}
self.adjust(d1, c)
}
}
}
pub fn advance_by_period(
&self,
d: Date,
p: Period,
c: BusinessDayConvention,
end_of_month: bool,
) -> Date {
self.advance(d, p.length(), p.units(), c, end_of_month)
}
pub fn business_days_between(
&self,
from: Date,
to: Date,
include_first: bool,
include_last: bool,
) -> SerialNumber {
if from < to {
days_between_impl(self, from, to, include_first, include_last)
} else if from > to {
-days_between_impl(self, to, from, include_last, include_first)
} else {
SerialNumber::from(include_first && include_last && self.is_business_day(from))
}
}
}
impl fmt::Debug for Calendar {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Calendar")
.field("name", &self.name())
.finish()
}
}
impl fmt::Display for Calendar {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.name())
}
}
impl PartialEq for Calendar {
fn eq(&self, other: &Calendar) -> bool {
self.name() == other.name()
}
}
impl Eq for Calendar {}
fn days_between_impl(
cal: &Calendar,
from: Date,
to: Date,
include_first: bool,
include_last: bool,
) -> SerialNumber {
let mut res = SerialNumber::from(include_last && cal.is_business_day(to));
let mut d = if include_first { from } else { from + 1 };
while d < to {
res += SerialNumber::from(cal.is_business_day(d));
d += 1;
}
res
}
pub fn western_easter_monday(y: Year) -> Day {
const EASTER_MONDAY: [u8; 299] = [
98, 90, 103, 95, 114, 106, 91, 111, 102, 87, 107, 99, 83, 103, 95, 115, 99, 91, 111, 96, 87, 107, 92, 112, 103, 95, 108, 100, 91, 111, 96, 88, 107, 92, 112, 104, 88, 108, 100, 85, 104, 96, 116, 101, 92, 112, 97, 89, 108, 100, 85, 105, 96, 109, 101, 93, 112, 97, 89, 109, 93, 113, 105, 90, 109, 101, 86, 106, 97, 89, 102, 94, 113, 105, 90, 110, 101, 86, 106, 98, 110, 102, 94, 114, 98, 90, 110, 95, 86, 106, 91, 111, 102, 94, 107, 99, 90, 103, 95, 115, 106, 91, 111, 103, 87, 107, 99, 84, 103, 95, 115, 100, 91, 111, 96, 88, 107, 92, 112, 104, 95, 108, 100, 92, 111, 96, 88, 108, 92, 112, 104, 89, 108, 100, 85, 105, 96, 116, 101, 93, 112, 97, 89, 109, 100, 85, 105, 97, 109, 101, 93, 113, 97, 89, 109, 94, 113, 105, 90, 110, 101, 86, 106, 98, 89, 102, 94, 114, 105, 90, 110, 102, 86, 106, 98, 111, 102, 94, 114, 99, 90, 110, 95, 87, 106, 91, 111, 103, 94, 107, 99, 91, 103, 95, 115, 107, 91, 111, 103, 88, 108, 100, 85, 105, 96, 109, 101, 93, 112, 97, 89, 109, 93, 113, 105, 90, 109, 101, 86, 106, 97, 89, 102, 94, 113, 105, 90, 110, 101, 86, 106, 98, 110, 102, 94, 114, 98, 90, 110, 95, 86, 106, 91, 111, 102, 94, 107, 99, 90, 103, 95, 115, 106, 91, 111, 103, 87, 107, 99, 84, 103, 95, 115, 100, 91, 111, 96, 88, 107, 92, 112, 104, 95, 108, 100, 92, 111, 96, 88, 108, 92, 112, 104, 89, 108, 100, 85, 105, 96, 116, 101, 93, 112, 97, 89, 109, 100, 85, 105, ];
Day::from(EASTER_MONDAY[(y - 1901) as usize])
}
pub fn orthodox_easter_monday(y: Year) -> Day {
const EASTER_MONDAY: [u8; 299] = [
105, 118, 110, 102, 121, 106, 126, 118, 102, 122, 114, 99, 118, 110, 95, 115, 106, 126, 111, 103, 122, 107, 99, 119, 110, 123, 115, 107, 126, 111, 103, 123, 107, 99, 119, 104, 123, 115, 100, 120, 111, 96, 116, 108, 127, 112, 104, 124, 115, 100, 120, 112, 96, 116, 108, 128, 112, 104, 124, 109, 100, 120, 105, 125, 116, 101, 121, 113, 104, 117, 109, 101, 120, 105, 125, 117, 101, 121, 113, 98, 117, 109, 129, 114, 105, 125, 110, 102, 121, 106, 98, 118, 109, 122, 114, 106, 118, 110, 102, 122, 106, 126, 118, 103, 122, 114, 99, 119, 110, 95, 115, 107, 126, 111, 103, 123, 107, 99, 119, 111, 123, 115, 107, 127, 111, 103, 123, 108, 99, 119, 104, 124, 115, 100, 120, 112, 96, 116, 108, 128, 112, 104, 124, 116, 100, 120, 112, 97, 116, 108, 128, 113, 104, 124, 109, 101, 120, 105, 125, 117, 101, 121, 113, 105, 117, 109, 101, 121, 105, 125, 110, 102, 121, 113, 98, 118, 109, 129, 114, 106, 125, 110, 102, 122, 106, 98, 118, 110, 122, 114, 99, 119, 110, 102, 115, 107, 126, 118, 103, 123, 115, 100, 120, 112, 96, 116, 108, 128, 112, 104, 124, 109, 100, 120, 105, 125, 116, 108, 121, 113, 104, 124, 109, 101, 120, 105, 125, 117, 101, 121, 113, 98, 117, 109, 129, 114, 105, 125, 110, 102, 121, 113, 98, 118, 109, 129, 114, 106, 125, 110, 102, 122, 106, 126, 118, 103, 122, 114, 99, 119, 110, 102, 115, 107, 126, 111, 103, 123, 114, 99, 119, 111, 130, 115, 107, 127, 111, 103, 123, 108, 99, 119, 104, 124, 115, 100, 120, 112, 103, 116, 108, 128, 119, 104, 124, 116, 100, 120, 112, ];
Day::from(EASTER_MONDAY[(y - 1901) as usize])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shared::shared;
use crate::time::date::Month;
struct WeekendsCal;
impl CalendarImpl for WeekendsCal {
fn name(&self) -> String {
"Weekends test".to_string()
}
fn is_business_day(&self, date: Date) -> bool {
!is_weekend_sat_sun(date.weekday())
}
fn is_weekend(&self, weekday: Weekday) -> bool {
is_weekend_sat_sun(weekday)
}
}
fn cal() -> Calendar {
Calendar::from_impl(shared(WeekendsCal))
}
struct SwitchingWeekendCal;
impl CalendarImpl for SwitchingWeekendCal {
fn name(&self) -> String {
"switching test".to_string()
}
fn is_weekend(&self, w: Weekday) -> bool {
w == Weekday::Friday || w == Weekday::Saturday
}
fn is_weekend_on(&self, date: Date) -> bool {
let w = date.weekday();
if date < Date::new(29, Month::June, 2013) {
w == Weekday::Thursday || w == Weekday::Friday
} else {
w == Weekday::Friday || w == Weekday::Saturday
}
}
fn is_business_day(&self, date: Date) -> bool {
!self.is_weekend_on(date)
}
}
#[test]
fn holiday_list_uses_date_aware_weekend() {
let c = Calendar::from_impl(shared(SwitchingWeekendCal));
let thu = Date::new(27, Month::June, 2013);
assert_eq!(thu.weekday(), Weekday::Thursday);
assert!(c.is_weekend_on(thu));
assert!(!c.is_weekend(thu.weekday()));
assert!(c.holiday_list(thu, thu, false).is_empty());
assert_eq!(c.holiday_list(thu, thu, true), vec![thu]);
}
#[test]
fn weekend_days_are_holidays() {
let c = cal();
let sat = Date::new(1, Month::January, 2000);
assert_eq!(sat.weekday(), Weekday::Saturday);
assert!(c.is_holiday(sat));
assert!(c.is_business_day(Date::new(3, Month::January, 2000))); }
#[test]
fn adjust_following_and_preceding() {
let c = cal();
let sat = Date::new(1, Month::January, 2000); assert_eq!(
c.adjust(sat, BusinessDayConvention::Following),
Date::new(3, Month::January, 2000) );
assert_eq!(
c.adjust(sat, BusinessDayConvention::Preceding),
Date::new(31, Month::December, 1999) );
}
#[test]
fn adjust_modified_following_rolls_back_across_month() {
let c = cal();
let sat = Date::new(31, Month::July, 2021);
assert_eq!(sat.weekday(), Weekday::Saturday);
assert_eq!(
c.adjust(sat, BusinessDayConvention::ModifiedFollowing),
Date::new(30, Month::July, 2021)
);
}
#[test]
fn advance_days_skips_weekends() {
let c = cal();
let fri = Date::new(7, Month::January, 2000);
assert_eq!(fri.weekday(), Weekday::Friday);
let next = c.advance(
fri,
1,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
);
assert_eq!(next, Date::new(10, Month::January, 2000));
}
#[test]
fn business_days_between_counts_weekdays() {
let c = cal();
let mon = Date::new(3, Month::January, 2000);
let next_mon = Date::new(10, Month::January, 2000);
assert_eq!(c.business_days_between(mon, next_mon, false, true), 5);
assert_eq!(c.business_days_between(next_mon, mon, true, false), -5);
assert_eq!(c.business_days_between(mon, mon, true, true), 1);
assert_eq!(c.business_days_between(mon, mon, true, false), 0);
}
#[test]
fn advance_unadjusted_end_of_month_uses_calendar_month_end() {
let c = cal();
let jan31 = Date::new(31, Month::January, 2020);
assert_eq!(
c.advance(
jan31,
1,
TimeUnit::Months,
BusinessDayConvention::Unadjusted,
true,
),
Date::new(29, Month::February, 2020)
);
}
#[test]
fn add_and_remove_holiday_are_local_and_reversible() {
let c = cal();
let wed = Date::new(5, Month::January, 2000); assert!(c.is_business_day(wed));
c.add_holiday(wed);
assert!(c.is_holiday(wed));
assert!(c.added_holidays().contains(&wed));
let clone = c.clone();
assert!(clone.is_holiday(wed));
assert!(cal().is_business_day(wed));
c.remove_holiday(wed);
assert!(c.is_business_day(wed));
}
#[test]
fn holiday_and_business_day_lists_handle_max_date_endpoint() {
let c = cal();
let from = Date::max_date() - 3;
let holidays = c.holiday_list(from, Date::max_date(), true);
let business = c.business_day_list(from, Date::max_date());
assert_eq!(holidays.len() + business.len(), 4);
assert!(c.holiday_list(from, from - 1, true).is_empty());
assert!(c.business_day_list(from, from - 1).is_empty());
assert_eq!(
c.business_day_list(from, from).len() + c.holiday_list(from, from, true).len(),
1
);
}
#[test]
fn easter_monday_known_values() {
assert_eq!(western_easter_monday(2000), 115);
assert_eq!(orthodox_easter_monday(2000), 122);
}
}