ecb_rates/caching/
cache_line.rs1use std::rc::Rc;
2
3use chrono::serde::ts_seconds;
4use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveDate, TimeDelta, Utc, Weekday};
5use serde::{Deserialize, Serialize};
6
7use crate::Hollidays;
8use crate::models::ExchangeRateResult;
9
10const CET: FixedOffset = unsafe { FixedOffset::east_opt(3600).unwrap_unchecked() };
11
12#[derive(Serialize, Deserialize, Debug, PartialEq)]
13pub struct CacheLine {
14 #[serde(with = "ts_seconds")]
15 date: DateTime<Utc>,
16
17 #[serde(rename = "camelCase")]
18 pub exchange_rate_results: Vec<ExchangeRateResult>,
19}
20
21impl CacheLine {
22 pub fn is_valid(&self) -> bool {
23 self.is_valid_at(Local::now().with_timezone(&CET))
24 }
25
26 pub fn is_valid_at(&self, now_cet: DateTime<FixedOffset>) -> bool {
27 let saved_cet = self.date.with_timezone(&CET);
28
29 if saved_cet > now_cet {
31 return false;
32 }
33
34 let hollidays_opt = if now_cet.year() == saved_cet.year() {
36 Some(Rc::new(Hollidays::new(now_cet.year())))
37 } else {
38 None
39 };
40
41 let mut day_iter = saved_cet.date_naive();
42 let end_day = now_cet.date_naive();
43
44 let is_open_day = |date: NaiveDate| {
47 let wd = date.weekday();
48 let is_weekend = wd == Weekday::Sat || wd == Weekday::Sun;
49
50 let hollidays = hollidays_opt
51 .clone()
52 .unwrap_or_else(|| Rc::new(Hollidays::new(date.year())));
53
54 let is_holiday = hollidays.is_holliday(&date);
55
56 !(is_weekend || is_holiday)
57 };
58
59 while day_iter <= end_day {
60 if is_open_day(day_iter) {
61 let publish_time_cet = unsafe {
63 day_iter
64 .and_hms_opt(16, 0, 0)
65 .unwrap_unchecked()
66 .and_local_timezone(CET)
67 .unwrap()
68 };
69
70 if publish_time_cet > saved_cet && publish_time_cet <= now_cet {
71 return false;
72 }
73 }
74 day_iter += TimeDelta::days(1);
75 }
76
77 true
79 }
80
81 pub fn new(exchange_rate_results: Vec<ExchangeRateResult>) -> Self {
82 let date = Local::now().to_utc();
83 Self {
84 exchange_rate_results,
85 date,
86 }
87 }
88}
89
90impl PartialEq<Vec<ExchangeRateResult>> for CacheLine {
91 fn eq(&self, other: &Vec<ExchangeRateResult>) -> bool {
92 &self.exchange_rate_results == other
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use chrono::TimeZone;
100
101 fn cl(date_utc: DateTime<Utc>) -> CacheLine {
102 CacheLine {
103 date: date_utc,
104 exchange_rate_results: vec![],
105 }
106 }
107
108 #[test]
109 fn test_cache_in_future() {
110 let now_cet = Utc
111 .with_ymd_and_hms(2025, 1, 1, 9, 0, 0)
112 .unwrap()
113 .with_timezone(&CET);
114 let future_utc = Utc.with_ymd_and_hms(2025, 1, 2, 9, 0, 0).unwrap();
115 assert!(!cl(future_utc).is_valid_at(now_cet));
116 }
117
118 #[test]
119 fn test_same_open_day_before_16() {
120 let now_cet = Utc
121 .with_ymd_and_hms(2025, 1, 8, 12, 0, 0)
122 .unwrap()
123 .with_timezone(&CET);
124 let cache_utc = Utc.with_ymd_and_hms(2025, 1, 8, 10, 0, 0).unwrap();
125 assert!(cl(cache_utc).is_valid_at(now_cet));
126 }
127
128 #[test]
129 fn test_same_day_after_16() {
130 let now_cet = Utc
131 .with_ymd_and_hms(2025, 1, 8, 17, 0, 0)
132 .unwrap()
133 .with_timezone(&CET);
134 let cache_utc = Utc.with_ymd_and_hms(2025, 1, 8, 14, 0, 0).unwrap();
135 assert!(!cl(cache_utc).is_valid_at(now_cet));
136 }
137
138 #[test]
139 fn test_saved_after_16_same_day() {
140 let now_cet = Utc
141 .with_ymd_and_hms(2025, 1, 8, 18, 0, 0)
142 .unwrap()
143 .with_timezone(&CET);
144 let cache_utc = Utc.with_ymd_and_hms(2025, 1, 8, 17, 0, 0).unwrap();
145 assert!(cl(cache_utc).is_valid_at(now_cet));
146 }
147
148 #[test]
149 fn test_multi_day_old_cache_should_invalidate_if_open_day_passed() {
150 let now_cet = Utc
151 .with_ymd_and_hms(2025, 1, 10, 18, 0, 0)
152 .unwrap()
153 .with_timezone(&CET);
154 let cache_utc = Utc.with_ymd_and_hms(2025, 1, 5, 10, 0, 0).unwrap();
155 assert!(!cl(cache_utc).is_valid_at(now_cet));
156 }
157
158 #[test]
159 fn test_multi_day_holiday_scenario() {
160 let now_cet = Utc
161 .with_ymd_and_hms(2025, 12, 26, 19, 0, 0)
162 .unwrap()
163 .with_timezone(&CET);
164 let cache_utc = Utc.with_ymd_and_hms(2025, 12, 24, 10, 0, 0).unwrap();
165 assert!(cl(cache_utc).is_valid_at(now_cet));
166 }
167}