Skip to main content

finquant/time/
calendars.rs

1use chrono::{Datelike, Duration, NaiveDate, Weekday};
2use std::cmp::Ordering;
3use std::fmt::Debug;
4
5use crate::error::Result;
6use crate::time::businessdayconvention::BusinessDayConvention;
7
8static EASTER_MONDAY: [u32; 299] = [
9    98, 90, 103, 95, 114, 106, 91, 111, 102, // 1901-1909
10    87, 107, 99, 83, 103, 95, 115, 99, 91, 111, // 1910-1919
11    96, 87, 107, 92, 112, 103, 95, 108, 100, 91, // 1920-1929
12    111, 96, 88, 107, 92, 112, 104, 88, 108, 100, // 1930-1939
13    85, 104, 96, 116, 101, 92, 112, 97, 89, 108, // 1940-1949
14    100, 85, 105, 96, 109, 101, 93, 112, 97, 89, // 1950-1959
15    109, 93, 113, 105, 90, 109, 101, 86, 106, 97, // 1960-1969
16    89, 102, 94, 113, 105, 90, 110, 101, 86, 106, // 1970-1979
17    98, 110, 102, 94, 114, 98, 90, 110, 95, 86, // 1980-1989
18    106, 91, 111, 102, 94, 107, 99, 90, 103, 95, // 1990-1999
19    115, 106, 91, 111, 103, 87, 107, 99, 84, 103, // 2000-2009
20    95, 115, 100, 91, 111, 96, 88, 107, 92, 112, // 2010-2019
21    104, 95, 108, 100, 92, 111, 96, 88, 108, 92, // 2020-2029
22    112, 104, 89, 108, 100, 85, 105, 96, 116, 101, // 2030-2039
23    93, 112, 97, 89, 109, 100, 85, 105, 97, 109, // 2040-2049
24    101, 93, 113, 97, 89, 109, 94, 113, 105, 90, // 2050-2059
25    110, 101, 86, 106, 98, 89, 102, 94, 114, 105, // 2060-2069
26    90, 110, 102, 86, 106, 98, 111, 102, 94, 114, // 2070-2079
27    99, 90, 110, 95, 87, 106, 91, 111, 103, 94, // 2080-2089
28    107, 99, 91, 103, 95, 115, 107, 91, 111, 103, // 2090-2099
29    88, 108, 100, 85, 105, 96, 109, 101, 93, 112, // 2100-2109
30    97, 89, 109, 93, 113, 105, 90, 109, 101, 86, // 2110-2119
31    106, 97, 89, 102, 94, 113, 105, 90, 110, 101, // 2120-2129
32    86, 106, 98, 110, 102, 94, 114, 98, 90, 110, // 2130-2139
33    95, 86, 106, 91, 111, 102, 94, 107, 99, 90, // 2140-2149
34    103, 95, 115, 106, 91, 111, 103, 87, 107, 99, // 2150-2159
35    84, 103, 95, 115, 100, 91, 111, 96, 88, 107, // 2160-2169
36    92, 112, 104, 95, 108, 100, 92, 111, 96, 88, // 2170-2179
37    108, 92, 112, 104, 89, 108, 100, 85, 105, 96, // 2180-2189
38    116, 101, 93, 112, 97, 89, 109, 100, 85, 105, // 2190-2199
39];
40
41static ORTHODOX_EASTER_MONDAY: [u32; 299] = [
42    105, 118, 110, 102, 121, 106, 126, 118, 102, // 1901-1909
43    122, 114, 99, 118, 110, 95, 115, 106, 126, 111, // 1910-1919
44    103, 122, 107, 99, 119, 110, 123, 115, 107, 126, // 1920-1929
45    111, 103, 123, 107, 99, 119, 104, 123, 115, 100, // 1930-1939
46    120, 111, 96, 116, 108, 127, 112, 104, 124, 115, // 1940-1949
47    100, 120, 112, 96, 116, 108, 128, 112, 104, 124, // 1950-1959
48    109, 100, 120, 105, 125, 116, 101, 121, 113, 104, // 1960-1969
49    117, 109, 101, 120, 105, 125, 117, 101, 121, 113, // 1970-1979
50    98, 117, 109, 129, 114, 105, 125, 110, 102, 121, // 1980-1989
51    106, 98, 118, 109, 122, 114, 106, 118, 110, 102, // 1990-1999
52    122, 106, 126, 118, 103, 122, 114, 99, 119, 110, // 2000-2009
53    95, 115, 107, 126, 111, 103, 123, 107, 99, 119, // 2010-2019
54    111, 123, 115, 107, 127, 111, 103, 123, 108, 99, // 2020-2029
55    119, 104, 124, 115, 100, 120, 112, 96, 116, 108, // 2030-2039
56    128, 112, 104, 124, 116, 100, 120, 112, 97, 116, // 2040-2049
57    108, 128, 113, 104, 124, 109, 101, 120, 105, 125, // 2050-2059
58    117, 101, 121, 113, 105, 117, 109, 101, 121, 105, // 2060-2069
59    125, 110, 102, 121, 113, 98, 118, 109, 129, 114, // 2070-2079
60    106, 125, 110, 102, 122, 106, 98, 118, 110, 122, // 2080-2089
61    114, 99, 119, 110, 102, 115, 107, 126, 118, 103, // 2090-2099
62    123, 115, 100, 120, 112, 96, 116, 108, 128, 112, // 2100-2109
63    104, 124, 109, 100, 120, 105, 125, 116, 108, 121, // 2110-2119
64    113, 104, 124, 109, 101, 120, 105, 125, 117, 101, // 2120-2129
65    121, 113, 98, 117, 109, 129, 114, 105, 125, 110, // 2130-2139
66    102, 121, 113, 98, 118, 109, 129, 114, 106, 125, // 2140-2149
67    110, 102, 122, 106, 126, 118, 103, 122, 114, 99, // 2150-2159
68    119, 110, 102, 115, 107, 126, 111, 103, 123, 114, // 2160-2169
69    99, 119, 111, 130, 115, 107, 127, 111, 103, 123, // 2170-2179
70    108, 99, 119, 104, 124, 115, 100, 120, 112, 103, // 2180-2189
71    116, 108, 128, 119, 104, 124, 116, 100, 120, 112, // 2190-2199
72];
73
74pub mod argentina;
75pub use argentina::Argentina;
76pub mod austria;
77pub use austria::Austria;
78pub mod australia;
79pub use australia::Australia;
80pub mod botswana;
81pub use botswana::Botswana;
82pub mod brazil;
83pub use brazil::Brazil;
84pub mod canada;
85pub use canada::Canada;
86pub mod chile;
87pub use chile::Chile;
88pub mod china;
89pub use china::China;
90pub mod czechrepublic;
91pub use czechrepublic::CzechRepublic;
92pub mod denmark;
93pub use denmark::Denmark;
94pub mod finland;
95pub use finland::Finland;
96pub mod france;
97pub use france::France;
98pub mod germany;
99pub use germany::Germany;
100pub mod hongkong;
101pub use hongkong::HongKong;
102pub mod hungary;
103pub use hungary::Hungary;
104pub mod iceland;
105pub use iceland::Iceland;
106pub mod india;
107pub use india::India;
108pub mod indonesia;
109pub use indonesia::Indonesia;
110pub mod israel;
111pub use israel::Israel;
112pub mod italy;
113pub use italy::Italy;
114pub mod japan;
115pub use japan::Japan;
116pub mod jointcalendar;
117pub use jointcalendar::JointCalendar;
118pub mod mexico;
119pub use mexico::Mexico;
120pub mod newzealand;
121pub use newzealand::NewZealand;
122pub mod norway;
123pub use norway::Norway;
124pub mod poland;
125pub use poland::Poland;
126pub mod romania;
127pub use romania::Romania;
128pub mod russia;
129pub use russia::Russia;
130pub mod singapore;
131pub use singapore::Singapore;
132pub mod slovakia;
133pub use slovakia::Slovakia;
134pub mod southafrica;
135pub use southafrica::SouthAfrica;
136pub mod southkorea;
137pub use southkorea::SouthKorea;
138pub mod sweden;
139pub use sweden::Sweden;
140pub mod switzerland;
141pub use switzerland::Switzerland;
142pub mod taiwan;
143pub use taiwan::Taiwan;
144pub mod target;
145pub use target::Target;
146pub mod thailand;
147pub use thailand::Thailand;
148pub mod turkey;
149pub use turkey::Turkey;
150pub mod ukraine;
151pub use ukraine::Ukraine;
152pub mod unitedkingdom;
153pub use unitedkingdom::UnitedKingdom;
154pub mod unitedstates;
155pub use unitedstates::UnitedStates;
156pub mod weekendsonly;
157use crate::time::period::{ONE_DAY, Period};
158pub use weekendsonly::WeekendsOnly;
159
160#[typetag::serde(tag = "type")]
161pub trait Calendar: Debug {
162    fn naive_date_to_dkmy(&self, date: NaiveDate) -> (u32, Weekday, u32, i32, u32) {
163        (
164            date.day(),
165            date.weekday(),
166            date.month(),
167            date.year(),
168            date.ordinal(),
169        )
170    }
171
172    fn last_day_of_month(&self, date: NaiveDate) -> NaiveDate {
173        let year = date.year();
174        let month = date.month();
175        NaiveDate::from_ymd_opt(year, month + 1, 1)
176            .unwrap_or(NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap())
177            .pred_opt()
178            .unwrap()
179    }
180
181    fn end_of_month(&self, date: NaiveDate) -> NaiveDate {
182        let mut last_day_of_month = self.last_day_of_month(date);
183        while self.is_holiday(last_day_of_month) {
184            last_day_of_month -= ONE_DAY;
185        }
186        last_day_of_month
187    }
188
189    fn easter_monday(&self, year: i32) -> u32 {
190        EASTER_MONDAY[year as usize - 1901usize]
191    }
192
193    fn orthodox_easter_monday(&self, year: i32) -> u32 {
194        ORTHODOX_EASTER_MONDAY[year as usize - 1901usize]
195    }
196
197    fn is_weekend(&self, date: NaiveDate) -> bool {
198        let weekday = date.weekday();
199        matches!(weekday, Weekday::Sat | Weekday::Sun)
200    }
201
202    fn is_business_day(&self, date: NaiveDate) -> bool;
203
204    fn is_holiday(&self, date: NaiveDate) -> bool {
205        !self.is_business_day(date)
206    }
207
208    fn adjust(&self, date: NaiveDate, bdc: BusinessDayConvention) -> Option<NaiveDate> {
209        if bdc == BusinessDayConvention::Unadjusted {
210            return Some(date);
211        }
212
213        let mut d1 = date;
214
215        if bdc == BusinessDayConvention::Following
216            || bdc == BusinessDayConvention::ModifiedFollowing
217            || bdc == BusinessDayConvention::HalfMonthModifiedFollowing
218        {
219            while self.is_holiday(d1) {
220                d1 += ONE_DAY;
221            }
222            if (bdc == BusinessDayConvention::ModifiedFollowing
223                || bdc == BusinessDayConvention::HalfMonthModifiedFollowing)
224                && d1.month() != date.month()
225            {
226                return self.adjust(date, BusinessDayConvention::Preceding);
227            }
228            if bdc == BusinessDayConvention::HalfMonthModifiedFollowing
229                && date.day() <= 15
230                && d1.day() > 15
231            {
232                return self.adjust(date, BusinessDayConvention::Preceding);
233            }
234        } else if bdc == BusinessDayConvention::Preceding
235            || bdc == BusinessDayConvention::ModifiedPreceding
236        {
237            while self.is_holiday(d1) {
238                d1 -= ONE_DAY;
239            }
240            if bdc == BusinessDayConvention::ModifiedPreceding && d1.month() != date.month() {
241                return self.adjust(date, BusinessDayConvention::Following);
242            }
243        } else if bdc == BusinessDayConvention::Nearest {
244            let mut d2 = date;
245            while self.is_holiday(d1) && self.is_holiday(d2) {
246                d1 += ONE_DAY;
247                d2 -= ONE_DAY;
248            }
249            return if self.is_holiday(d1) {
250                Some(d1)
251            } else {
252                Some(d2)
253            };
254        } else {
255            return None;
256        }
257        Some(d1)
258    }
259
260    fn advance(
261        &self,
262        date: NaiveDate,
263        period: Period,
264        bdc: BusinessDayConvention,
265        end_of_month: Option<bool>,
266    ) -> Result<Option<NaiveDate>> {
267        let end_of_month = end_of_month.unwrap_or(false);
268
269        match period {
270            Period::Months(_) | Period::Years(_) => {
271                let advance_date = (date + period)?;
272                Ok(if end_of_month {
273                    Some(self.end_of_month(self.adjust(advance_date, bdc).unwrap()))
274                } else {
275                    self.adjust(advance_date, bdc)
276                })
277            }
278            Period::Days(mut num) => {
279                let mut advance_date = date;
280                let target: i64 = 0;
281                match num.cmp(&target) {
282                    Ordering::Equal => Ok(self.adjust(date, bdc)),
283                    Ordering::Greater => {
284                        while num > 0 {
285                            advance_date = (advance_date + Period::Days(1))?;
286                            while !self.is_business_day(advance_date) {
287                                advance_date = (advance_date + Period::Days(1))?;
288                            }
289                            num -= 1;
290                        }
291                        Ok(Some(advance_date))
292                    }
293                    Ordering::Less => {
294                        while num < 0 {
295                            advance_date = (advance_date - Period::Days(1))?;
296                            while !self.is_business_day(advance_date) {
297                                advance_date = (advance_date - Period::Days(1))?;
298                            }
299                            num += 1;
300                        }
301                        Ok(Some(advance_date))
302                    }
303                }
304            }
305            _ => {
306                let advance_date = (date + period)?;
307                Ok(self.adjust(advance_date, bdc))
308            }
309        }
310    }
311
312    fn business_days_between(
313        &self,
314        from: NaiveDate,
315        to: NaiveDate,
316        include_first: Option<bool>,
317        include_last: Option<bool>,
318    ) -> i64 {
319        if from > to {
320            return -self.business_days_between(to, from, include_first, include_last);
321        }
322        let include_first = include_first.unwrap_or(true);
323        let include_last = include_last.unwrap_or(false);
324        let mut day_count = 0;
325        let day_diff = ((to + Duration::try_days(if include_last { 1 } else { 0 }).unwrap())
326            - (from + Duration::try_days(if include_first { 1 } else { 0 }).unwrap()))
327        .num_days();
328        for date in (from + Duration::try_days(if include_first { 1 } else { 0 }).unwrap())
329            .iter_days()
330            .take(day_diff as usize)
331        {
332            if self.is_business_day(date) {
333                day_count += 1;
334            }
335        }
336        day_count
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::UnitedKingdom;
343    use crate::time::calendars::Calendar;
344    use chrono::NaiveDate;
345
346    #[test]
347    fn test_business_days_between() {
348        let calendar = UnitedKingdom::default();
349        let from = NaiveDate::from_ymd_opt(2023, 4, 1).unwrap();
350        let to = NaiveDate::from_ymd_opt(2023, 4, 30).unwrap();
351        assert_eq!(calendar.business_days_between(from, to, None, None,), 18);
352        assert_eq!(calendar.business_days_between(to, from, None, None,), -18);
353    }
354}