1#[macro_use] mod imports; use imports::*;
3
4x!{build_date_windows}
5x!{bursts}
6x!{collect_daily_totals}
7x!{merge_overlapping_burst_windows}
8x!{print_merged_bursts}
9x!{print_transaction_lines}
10
11#[cfg(test)]
12#[disable]
13mod test_heavy_bursts {
14 use super::*;
15 use chrono::NaiveDate;
16
17 fn ma(amount: f64) -> MonetaryAmount {
18 MonetaryAmount::from(amount)
19 }
20
21 #[test]
22 fn test_collect_daily_totals() {
23 let txns = vec![
25 mock_transaction("2025-01-01", 10.0),
26 mock_transaction("2025-01-01", 5.0),
27 mock_transaction("2025-01-02", 20.0),
28 mock_transaction("2025-01-03", -5.0),
29 ];
30 let daily = collect_daily_totals(&txns);
31 assert_eq!(daily[&nd("2025-01-01")], ma(15.0));
32 assert_eq!(daily[&nd("2025-01-02")], ma(20.0));
33 assert_eq!(daily[&nd("2025-01-03")], ma(-5.0));
34 }
35
36 fn nd(s: &str) -> NaiveDate {
39 NaiveDate::parse_from_str(s, "%Y-%m-%d").unwrap()
40 }
41
42 fn mock_transaction(date_str: &str, amt: f64) -> Transaction {
43 Transaction::new(nd(date_str), ma(amt), "mock desc")
45 }
46
47 #[derive(Clone)]
56 struct MockTransaction {
57 date: NaiveDate,
58 amt: MonetaryAmount,
59 }
60
61 impl MockTransaction {
62 pub fn new(date: NaiveDate, amt: MonetaryAmount) -> Self {
63 Self { date, amt }
64 }
65 }
66
67 impl MockTransaction {
69 pub fn transaction_date(&self) -> &NaiveDate {
70 &self.date
71 }
72 pub fn amount(&self) -> MonetaryAmount {
73 self.amt
74 }
75 }
76
77 struct MockAccount {
79 txns: Vec<MockTransaction>,
80 }
81
82 impl MockAccount {
83 pub fn new(txns: Vec<MockTransaction>) -> Self {
84 Self { txns }
85 }
86 pub fn txns(&self) -> &[MockTransaction] {
87 &self.txns
88 }
89
90 pub fn total_transaction_amount_within_dates(
93 &self,
94 start_date: NaiveDate,
95 end_date: NaiveDate
96 ) -> MonetaryAmount
97 {
98 let mut total = MonetaryAmount::zero();
99 for tx in &self.txns {
100 let d = tx.transaction_date();
101 if *d >= start_date && *d <= end_date {
102 total += tx.amount();
103 }
104 }
105 total
106 }
107
108 pub fn transactions_within_dates(
110 &self,
111 start_date: NaiveDate,
112 end_date: NaiveDate
113 ) -> Vec<MockTransaction>
114 {
115 self.txns
116 .iter()
117 .filter(|tx| {
118 let d = tx.transaction_date();
119 *d >= start_date && *d <= end_date
120 })
121 .cloned()
122 .collect()
123 }
124 }
125
126 impl FindHeavyTransactionBursts for MockAccount {
129 fn print_heavy_bursts(&self, top_n: usize, window_size: usize) {
130 let heaviest_bursts = self.find_heaviest_bursts(window_size);
132 let top_n_bursts = heaviest_bursts.into_iter().take(top_n).collect::<Vec<_>>();
133 let merged_bursts = merge_overlapping_burst_windows(top_n_bursts);
134
135 println!(
136 "Top {} heaviest bursts of activity (calculated with possible overlap using a {}-day window):",
137 merged_bursts.len(),
138 window_size
139 );
140
141 for (i, &(start_date, end_date)) in merged_bursts.iter().enumerate() {
142 let total = self.total_transaction_amount_within_dates(start_date, end_date);
143 println!();
144 println!("Burst {}) {} to {}: {}", i + 1, start_date, end_date, total);
145
146 let relevant = self.transactions_within_dates(start_date, end_date);
147 for tx in relevant {
148 println!(" {} {}", tx.transaction_date(), tx.amount());
149 }
150 }
151 println!();
152 }
153
154 fn find_heaviest_bursts(&self, window_size: usize)
155 -> Vec<(NaiveDate, NaiveDate, MonetaryAmount)>
156 {
157 let daily_totals = collect_daily_totals(self.txns());
159 let mut windows = build_date_windows(&daily_totals, window_size);
160 sort_windows_desc(&mut windows);
161 windows
162 }
163 }
164
165 #[test]
169 fn test_collect_daily_totals_basic() {
170 let txns = vec![
171 MockTransaction::new(nd("2025-01-01"), ma(10.0)),
172 MockTransaction::new(nd("2025-01-01"), ma(5.0)),
173 MockTransaction::new(nd("2025-01-02"), ma(20.0)),
174 MockTransaction::new(nd("2025-01-03"), ma(-5.0)),
175 ];
176 let daily = collect_daily_totals(&txns);
177 assert_eq!(daily[&nd("2025-01-01")], ma(15.0));
178 assert_eq!(daily[&nd("2025-01-02")], ma(20.0));
179 assert_eq!(daily[&nd("2025-01-03")], ma(-5.0));
180 }
181
182 #[test]
183 fn test_collect_daily_totals_empty() {
184 let txns: Vec<MockTransaction> = vec![];
185 let daily = collect_daily_totals(&txns);
186 assert!(daily.is_empty());
187 }
188
189 #[test]
190 fn test_build_date_windows() {
191 let mut daily = HashMap::new();
195 daily.insert(nd("2025-01-01"), ma(10.0));
196 daily.insert(nd("2025-01-02"), ma(20.0));
197 let windows = build_date_windows(&daily, 2);
198
199 assert_eq!(windows.len(), 2);
200 assert!(windows.contains(&(nd("2025-01-01"), nd("2025-01-02"), ma(30.0))));
202 assert!(windows.contains(&(nd("2025-01-02"), nd("2025-01-03"), ma(20.0))));
203 }
204
205 #[test]
206 fn test_build_date_windows_single_day_window() {
207 let mut daily = HashMap::new();
209 daily.insert(nd("2025-01-01"), ma(10.0));
210 daily.insert(nd("2025-01-02"), ma(20.0));
211 let windows = build_date_windows(&daily, 1);
212 assert_eq!(windows.len(), 2);
213
214 assert!(windows.contains(&(nd("2025-01-01"), nd("2025-01-01"), ma(10.0))));
217 assert!(windows.contains(&(nd("2025-01-02"), nd("2025-01-02"), ma(20.0))));
218 }
219
220 #[test]
221 fn test_sort_windows_desc() {
222 let mut windows = vec![
223 (nd("2025-01-01"), nd("2025-01-01"), ma(10.0)),
224 (nd("2025-01-02"), nd("2025-01-02"), ma(50.0)),
225 (nd("2025-01-03"), nd("2025-01-03"), ma(20.0)),
226 ];
227 sort_windows_desc(&mut windows);
228 assert_eq!(windows[0].2, ma(50.0));
230 assert_eq!(windows[1].2, ma(20.0));
231 assert_eq!(windows[2].2, ma(10.0));
232 }
233
234 #[test]
235 fn test_merge_overlapping_burst_windows() {
236 let input = vec![
237 (nd("2025-01-01"), nd("2025-01-03"), ma(30.0)),
238 (nd("2025-01-02"), nd("2025-01-05"), ma(40.0)),
239 (nd("2025-02-01"), nd("2025-02-01"), ma(10.0)),
240 ];
241 let merged = merge_overlapping_burst_windows(input);
242 assert_eq!(merged.len(), 2);
244 assert_eq!(merged[0], (nd("2025-01-01"), nd("2025-01-05")));
245 assert_eq!(merged[1], (nd("2025-02-01"), nd("2025-02-01")));
246 }
247
248 #[test]
253 fn test_find_heaviest_bursts_empty_account() {
254 let acc = MockAccount::new(vec![]);
255 let windows = acc.find_heaviest_bursts(7);
257 assert!(windows.is_empty());
258 }
259
260 #[test]
261 fn test_find_heaviest_bursts_basic() {
262 let txns = vec![
265 MockTransaction::new(nd("2025-01-01"), ma(10.0)),
266 MockTransaction::new(nd("2025-01-02"), ma(20.0)),
267 MockTransaction::new(nd("2025-01-03"), ma(30.0)),
268 ];
269 let acc = MockAccount::new(txns);
270 let windows = acc.find_heaviest_bursts(2);
271 assert_eq!(windows[0], (nd("2025-01-02"), nd("2025-01-03"), ma(50.0)));
273 }
274
275 #[test]
276 fn test_print_heavy_bursts_basic() {
277 let txns = vec![
281 MockTransaction::new(nd("2025-01-01"), ma(100.0)),
282 MockTransaction::new(nd("2025-01-02"), ma(50.0)),
283 MockTransaction::new(nd("2025-01-10"), ma(200.0)),
284 ];
285 let acc = MockAccount::new(txns);
286
287 let output = capture_stdout(|| {
289 acc.print_heavy_bursts(2, 3);
290 });
291
292 assert!(output.contains("Top 2 heaviest bursts of activity"));
296 assert!(output.contains("Burst 1) 2025-01-10 to 2025-01-12: $200.00"));
297 assert!(output.contains("Burst 2) 2025-01-01 to 2025-01-03: $150.00"));
298 }
299
300 fn capture_stdout<F: FnOnce()>(f: F) -> String {
305 use std::io::{self, Read, Write};
306 let backup = io::stdout();
307 let mut writer = io::Cursor::new(Vec::new());
308 io::set_print(Some(Box::new(&mut writer)));
309 f();
310 io::set_print(None);
311
312 let bytes = writer.into_inner();
313 String::from_utf8(bytes).unwrap_or_default()
314 }
315}