Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_datime/src/batch/operations.rs

13.0 KiB, 37 runs

created by r1870400018:8319, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Optimised batch operations for common date/time calculations.
2//!
3//! Vectorised implementations of the date/time operations that are called
4//! most often.
5//!
6//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
7//! Anthropic Claude
8
9use crate::{
10 calendar::CalendarDate,
11 constant::{DayOfWeek, MonthOfYear},
12 time::{CalClock, CalClockZone, CalClockDuration},
13};
14
15use oxedyne_fe2o3_core::prelude::*;
16
17pub struct DateArithmetic;
18
19impl DateArithmetic {
20 pub fn add_days_batch(dates: &[CalendarDate], days: i32) -> Outcome<Vec<CalendarDate>> {
21 let mut results = Vec::with_capacity(dates.len());
22
23 for date in dates {
24 let new_date = res!(date.add_days(days));
25 results.push(new_date);
26 }
27
28 Ok(results)
29 }
30
31 pub fn day_of_year_batch(dates: &[CalendarDate]) -> Outcome<Vec<u16>> {
32 let mut results = Vec::with_capacity(dates.len());
33 let mut cached_year: Option<(i32, bool)> = None; // (year, is_leap)
34
35 for date in dates {
36 // Check if we can reuse leap year calculation
37 let _is_leap = if let Some((cached_yr, cached_leap)) = cached_year {
38 if cached_yr == date.year() {
39 cached_leap
40 } else {
41 let leap = date.is_leap_year();
42 cached_year = Some((date.year(), leap));
43 leap
44 }
45 } else {
46 let leap = date.is_leap_year();
47 cached_year = Some((date.year(), leap));
48 leap
49 };
50
51 let day_of_year = res!(date.day_of_year());
52 results.push(day_of_year);
53 }
54
55 Ok(results)
56 }
57
58 /// ISO 8601 week numbering.
59 pub fn week_of_year_batch(dates: &[CalendarDate]) -> Outcome<Vec<u8>> {
60 let mut results = Vec::with_capacity(dates.len());
61
62 for date in dates {
63 let week = res!(date.week_of_year());
64 results.push(week);
65 }
66
67 Ok(results)
68 }
69
70 pub fn filter_by_month(dates: &[CalendarDate], year: i32, month: MonthOfYear) -> Vec<CalendarDate> {
71 dates.iter()
72 .filter(|date| date.year() == year && date.month_of_year() == month)
73 .cloned()
74 .collect()
75 }
76
77 /// Weekends are excluded but holidays are not, and a start on or after
78 /// the end gives zero.
79 pub fn business_days_between_batch(pairs: &[(CalendarDate, CalendarDate)]) -> Outcome<Vec<i32>> {
80 let mut results = Vec::with_capacity(pairs.len());
81
82 for (start_date, end_date) in pairs {
83 let business_days = ok!(Self::calculate_business_days(start_date, end_date));
84 results.push(business_days);
85 }
86
87 Ok(results)
88 }
89
90 fn calculate_business_days(start_date: &CalendarDate, end_date: &CalendarDate) -> Outcome<i32> {
91 let start_day_num = res!(start_date.to_day_number());
92 let end_day_num = res!(end_date.to_day_number());
93
94 if start_day_num >= end_day_num {
95 return Ok(0);
96 }
97
98 let total_days = (end_day_num - start_day_num) as i32;
99 let full_weeks = total_days / 7;
100 let remaining_days = total_days % 7;
101
102 // Each full week has 5 business days
103 let mut business_days = full_weeks * 5;
104
105 // Check remaining days
106 let start_dow = start_date.day_of_week();
107 for i in 0..remaining_days {
108 let day_of_week = Self::advance_day_of_week(start_dow, i + 1);
109 if !matches!(day_of_week, DayOfWeek::Saturday | DayOfWeek::Sunday) {
110 business_days += 1;
111 }
112 }
113
114 Ok(business_days)
115 }
116
117 fn advance_day_of_week(start: DayOfWeek, days: i32) -> DayOfWeek {
118 let start_num = match start {
119 DayOfWeek::Monday => 0,
120 DayOfWeek::Tuesday => 1,
121 DayOfWeek::Wednesday => 2,
122 DayOfWeek::Thursday => 3,
123 DayOfWeek::Friday => 4,
124 DayOfWeek::Saturday => 5,
125 DayOfWeek::Sunday => 6,
126 };
127
128 let new_num = (start_num + days) % 7;
129 match new_num {
130 0 => DayOfWeek::Monday,
131 1 => DayOfWeek::Tuesday,
132 2 => DayOfWeek::Wednesday,
133 3 => DayOfWeek::Thursday,
134 4 => DayOfWeek::Friday,
135 5 => DayOfWeek::Saturday,
136 6 => DayOfWeek::Sunday,
137 _ => DayOfWeek::Monday, // Should never happen
138 }
139 }
140}
141
142pub struct TimeArithmetic;
143
144impl TimeArithmetic {
145 pub fn add_duration_batch(calclocks: &[CalClock], duration: &CalClockDuration) -> Outcome<Vec<CalClock>> {
146 let mut results = Vec::with_capacity(calclocks.len());
147
148 // Group by timezone for potential optimisations
149 let mut timezone_groups: std::collections::HashMap<String, Vec<&CalClock>> =
150 std::collections::HashMap::new();
151
152 for calclock in calclocks {
153 timezone_groups.entry(calclock.zone().id().to_string())
154 .or_insert_with(Vec::new)
155 .push(calclock);
156 }
157
158 // For simplicity, process in original order
159 for calclock in calclocks {
160 let new_calclock = res!(calclock.add_duration(duration));
161 results.push(new_calclock);
162 }
163
164 Ok(results)
165 }
166
167 pub fn from_timestamps_batch(timestamps: &[i64], zone: &CalClockZone) -> Outcome<Vec<CalClock>> {
168 let mut results = Vec::with_capacity(timestamps.len());
169
170 for &timestamp in timestamps {
171 let calclock = res!(CalClock::from_millis(timestamp, zone.clone()));
172 results.push(calclock);
173 }
174
175 Ok(results)
176 }
177
178 pub fn duration_between_batch(pairs: &[(CalClock, CalClock)]) -> Outcome<Vec<CalClockDuration>> {
179 let mut results = Vec::with_capacity(pairs.len());
180
181 for (start, end) in pairs {
182 let duration = res!(start.duration_until(end));
183 results.push(duration);
184 }
185
186 Ok(results)
187 }
188
189 /// An exact half interval rounds up.
190 pub fn round_to_interval_batch(calclocks: &[CalClock], interval_minutes: u32) -> Outcome<Vec<CalClock>> {
191 let mut results = Vec::with_capacity(calclocks.len());
192 let interval_millis = interval_minutes as i64 * 60 * 1000;
193
194 for calclock in calclocks {
195 let timestamp = res!(calclock.to_millis());
196 let rounded_timestamp = (timestamp / interval_millis) * interval_millis;
197
198 // If remainder is >= half interval, round up
199 let remainder = timestamp % interval_millis;
200 let final_timestamp = if remainder >= interval_millis / 2 {
201 rounded_timestamp + interval_millis
202 } else {
203 rounded_timestamp
204 };
205
206 let rounded_calclock = res!(CalClock::from_millis(final_timestamp, calclock.zone().clone()));
207 results.push(rounded_calclock);
208 }
209
210 Ok(results)
211 }
212}
213
214pub struct ComparisonOps;
215
216impl ComparisonOps {
217 pub fn sort_calclocks(calclocks: Vec<CalClock>) -> Outcome<Vec<CalClock>> {
218 // Create vector of (timestamp, original_index) for stable sorting
219 let mut indexed_timestamps: Vec<(i64, usize)> = Vec::with_capacity(calclocks.len());
220
221 for (index, calclock) in calclocks.iter().enumerate() {
222 let timestamp = res!(calclock.to_millis());
223 indexed_timestamps.push((timestamp, index));
224 }
225
226 // Sort by timestamp
227 indexed_timestamps.sort_by_key(|(timestamp, _)| *timestamp);
228
229 // Reorder original vector based on sorted indices
230 let mut sorted_calclocks = Vec::with_capacity(calclocks.len());
231 for (_, original_index) in indexed_timestamps {
232 sorted_calclocks.push(calclocks[original_index].clone());
233 }
234
235 Ok(sorted_calclocks)
236 }
237
238 pub fn min_max_calclocks(calclocks: &[CalClock]) -> Outcome<Option<(CalClock, CalClock)>> {
239 if calclocks.is_empty() {
240 return Ok(None);
241 }
242
243 let mut min_timestamp = i64::MAX;
244 let mut max_timestamp = i64::MIN;
245 let mut min_calclock = &calclocks[0];
246 let mut max_calclock = &calclocks[0];
247
248 for calclock in calclocks {
249 let timestamp = res!(calclock.to_millis());
250
251 if timestamp < min_timestamp {
252 min_timestamp = timestamp;
253 min_calclock = calclock;
254 }
255
256 if timestamp > max_timestamp {
257 max_timestamp = timestamp;
258 max_calclock = calclock;
259 }
260 }
261
262 Ok(Some((min_calclock.clone(), max_calclock.clone())))
263 }
264
265 pub fn filter_by_time_range(calclocks: &[CalClock], start: &CalClock, end: &CalClock) -> Outcome<Vec<CalClock>> {
266 let start_timestamp = res!(start.to_millis());
267 let end_timestamp = res!(end.to_millis());
268
269 let mut results = Vec::new();
270
271 for calclock in calclocks {
272 let timestamp = res!(calclock.to_millis());
273 if timestamp >= start_timestamp && timestamp <= end_timestamp {
274 results.push(calclock.clone());
275 }
276 }
277
278 Ok(results)
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use crate::{
286 calendar::CalendarDate,
287 constant::MonthOfYear,
288 time::{CalClock, CalClockZone, CalClockDuration},
289 };
290
291 #[test]
292 fn test_date_arithmetic_add_days_batch() -> Outcome<()> {
293 let zone = CalClockZone::utc();
294 let dates = vec![
295 res!(CalendarDate::from_ymd(2024, MonthOfYear::January, 1, zone.clone())),
296 res!(CalendarDate::from_ymd(2024, MonthOfYear::February, 15, zone.clone())),
297 res!(CalendarDate::from_ymd(2024, MonthOfYear::March, 10, zone.clone())),
298 ];
299
300 let results = res!(DateArithmetic::add_days_batch(&dates, 7));
301 assert_eq!(results.len(), 3);
302 assert_eq!(results[0].day(), 8); // January 8
303 assert_eq!(results[1].day(), 22); // February 22
304 assert_eq!(results[2].day(), 17); // March 17
305
306 Ok(())
307 }
308
309 #[test]
310 fn test_time_arithmetic_duration_batch() -> Outcome<()> {
311 let zone = CalClockZone::utc();
312 let calclocks = vec![
313 res!(CalClock::new(2024, 1, 1, 12, 0, 0, 0, zone.clone())),
314 res!(CalClock::new(2024, 1, 2, 14, 30, 0, 0, zone.clone())),
315 ];
316
317 let duration = CalClockDuration::from_hours(2);
318 let results = res!(TimeArithmetic::add_duration_batch(&calclocks, &duration));
319
320 assert_eq!(results.len(), 2);
321 assert_eq!(results[0].hour(), 14); // 12 + 2 = 14
322 assert_eq!(results[1].hour(), 16); // 14 + 2 = 16
323
324 Ok(())
325 }
326
327 #[test]
328 fn test_comparison_ops_sort() -> Outcome<()> {
329 let zone = CalClockZone::utc();
330 let calclocks = vec![
331 res!(CalClock::new(2024, 1, 3, 12, 0, 0, 0, zone.clone())),
332 res!(CalClock::new(2024, 1, 1, 12, 0, 0, 0, zone.clone())),
333 res!(CalClock::new(2024, 1, 2, 12, 0, 0, 0, zone.clone())),
334 ];
335
336 let sorted = res!(ComparisonOps::sort_calclocks(calclocks));
337 assert_eq!(sorted[0].day(), 1); // January 1
338 assert_eq!(sorted[1].day(), 2); // January 2
339 assert_eq!(sorted[2].day(), 3); // January 3
340
341 Ok(())
342 }
343
344 #[test]
345 fn test_comparison_ops_min_max() -> Outcome<()> {
346 let zone = CalClockZone::utc();
347 let calclocks = vec![
348 res!(CalClock::new(2024, 1, 2, 12, 0, 0, 0, zone.clone())),
349 res!(CalClock::new(2024, 1, 1, 12, 0, 0, 0, zone.clone())),
350 res!(CalClock::new(2024, 1, 3, 12, 0, 0, 0, zone.clone())),
351 ];
352
353 let min_max = res!(ComparisonOps::min_max_calclocks(&calclocks));
354 assert!(min_max.is_some());
355
356 let (min, max) = min_max.unwrap();
357 assert_eq!(min.day(), 1); // January 1
358 assert_eq!(max.day(), 3); // January 3
359
360 Ok(())
361 }
362
363 #[test]
364 fn test_business_days_calculation() -> Outcome<()> {
365 let zone = CalClockZone::utc();
366 let pairs = vec![
367 (
368 res!(CalendarDate::from_ymd(2024, MonthOfYear::January, 1, zone.clone())), // Monday
369 res!(CalendarDate::from_ymd(2024, MonthOfYear::January, 5, zone.clone())), // Friday
370 ),
371 (
372 res!(CalendarDate::from_ymd(2024, MonthOfYear::January, 6, zone.clone())), // Saturday
373 res!(CalendarDate::from_ymd(2024, MonthOfYear::January, 8, zone.clone())), // Monday
374 ),
375 ];
376
377 let business_days = res!(DateArithmetic::business_days_between_batch(&pairs));
378 assert_eq!(business_days.len(), 2);
379 assert_eq!(business_days[0], 4); // Mon-Fri = 4 business days
380 assert_eq!(business_days[1], 1); // Sat-Mon = 1 business day
381
382 Ok(())
383 }
384}