Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_datime/src/calendar/incrementor.rs

29.2 KiB, 62 runs

created by r1870400018:6297, 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//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
2//! Anthropic Claude
3
4use crate::{
5 calendar::CalendarDate,
6 constant::{DayOfWeek, OrdinalEnglish},
7 time::CalClockZone,
8};
9
10use oxedyne_fe2o3_core::prelude::*;
11
12/// A day named by description rather than number -- "second business day",
13/// "3rd Monday", "4th weekday" -- and composable, as in "second business day
14/// after the 1st Monday".
15#[derive(Clone, Debug, PartialEq)]
16pub struct DayIncrementor {
17 value: i32,
18 sign: i32, // Relative to child (next) incrementor: -1 = before, +1 = after
19 day_of_week: Option<DayOfWeek>,
20 day_type: Option<DayType>,
21 next_inc: Option<Box<DayIncrementor>>,
22}
23
24#[derive(Clone, Debug, PartialEq)]
25pub enum DayType {
26 Weekday, // Monday-Friday
27 Workday, // Monday-Friday, excluding holidays
28 DayOfWeek,
29 OrdinaryDay,
30 DayOfMonthFromStart,
31 DayOfMonthFromEnd, // counts back from the last day
32}
33
34pub const END_OF_MONTH_TOKEN: &str = "END_OF_MONTH_TOKEN";
35
36impl DayIncrementor {
37 pub fn new() -> Self {
38 Self {
39 value: 0,
40 sign: 0,
41 day_of_week: None,
42 day_type: None,
43 next_inc: None,
44 }
45 }
46
47 pub fn with_next(next_inc: DayIncrementor) -> Self {
48 Self {
49 value: 0,
50 sign: 0,
51 day_of_week: None,
52 day_type: None,
53 next_inc: Some(Box::new(next_inc)),
54 }
55 }
56
57 pub fn with_day_of_week(value: i32, day_of_week: DayOfWeek) -> Self {
58 let mut inc = Self::new();
59 inc.value = value.abs();
60 inc.set_day_of_week(day_of_week);
61 inc.next_inc = Some(Box::new(DayIncrementor::new()));
62 inc
63 }
64
65 pub fn with_day_of_month(value: i32) -> Self {
66 let mut inc = Self::new();
67 inc.value = value.abs();
68 inc.day_type = Some(DayType::DayOfMonthFromStart);
69 inc.next_inc = Some(Box::new(DayIncrementor::new()));
70 inc
71 }
72
73 /// Accepts, among others:
74 /// - "3rd Monday"
75 /// - "second business day"
76 /// - "last Sunday"
77 /// - "2nd weekday before the 25th"
78 /// - "final day of the month"
79 pub fn from_string(input: &str) -> Outcome<Self> {
80 let mut input = input.trim().to_lowercase();
81
82 // Remove leading "the"
83 if input.starts_with("the ") {
84 input = input[4..].to_string();
85 }
86
87 // Convert last/final colloquialisms into our formalism
88 // "last Sunday" -> "1st Sunday before the end of the month"
89 let word_count = input.split_whitespace().count();
90 if word_count == 2 {
91 if input.starts_with("last ") || input.starts_with("final ") {
92 let parts: Vec<&str> = input.split_whitespace().collect();
93 if parts.len() == 2 {
94 input = format!("1st {} before the end of the month", parts[1]);
95 }
96 }
97 }
98
99 // Replace multiple word expressions with single tokens
100 input = input
101 .replace("end of month", END_OF_MONTH_TOKEN)
102 .replace("end of the month", END_OF_MONTH_TOKEN)
103 .replace("last day of month", END_OF_MONTH_TOKEN)
104 .replace("last day of the month", END_OF_MONTH_TOKEN)
105 .replace("final day of month", END_OF_MONTH_TOKEN)
106 .replace("final day of the month", END_OF_MONTH_TOKEN);
107
108 let words: Vec<&str> = input.split_whitespace().collect();
109
110 // Create two-step incrementor structure
111 let mut main_inc = Self::new();
112 main_inc.next_inc = Some(Box::new(DayIncrementor::new()));
113
114 // Parse each word
115 let current_inc = &mut main_inc;
116 let mut i = 0;
117
118 while i < words.len() {
119 let word = words[i];
120
121 if current_inc.is_complete() {
122 if current_inc.is_sign_defined() {
123 // Move to next incrementor
124 if let Some(ref mut next) = current_inc.next_inc {
125 res!(next.lex_word(word));
126 }
127 } else {
128 // Parse qualifier (before/after)
129 res!(current_inc.lex_qualifier(word));
130 }
131 } else {
132 res!(current_inc.lex_word(word));
133 }
134
135 i += 1;
136 }
137
138 // Post-processing: handle incomplete incrementors
139 if !main_inc.is_complete() && !main_inc.next_inc.as_ref().unwrap().is_complete()
140 && !main_inc.next_inc.as_ref().unwrap().is_value_defined() {
141 main_inc.day_type = Some(DayType::DayOfMonthFromStart);
142 }
143
144 if main_inc.is_complete() && !main_inc.next_inc.as_ref().unwrap().is_complete()
145 && main_inc.next_inc.as_ref().unwrap().is_value_defined() {
146 main_inc.next_inc.as_mut().unwrap().day_type = Some(DayType::DayOfMonthFromStart);
147 }
148
149 if !main_inc.is_complete() {
150 return Err(err!("Incomplete DayIncrementor from input: {}", input; Invalid, Input));
151 }
152
153 Ok(main_inc)
154 }
155
156 fn lex_word(&mut self, word: &str) -> Outcome<()> {
157 // Try to parse as ordinal
158 if let Some(ordinal) = OrdinalEnglish::from_name(word) {
159 self.value = ordinal.value() as i32;
160 return Ok(());
161 }
162
163 // Try to parse as day of week
164 if let Some(dow) = DayOfWeek::from_name(word) {
165 self.set_day_of_week(dow);
166 return Ok(());
167 }
168
169 // Parse day type keywords
170 match word {
171 "business" | "working" | "work" => {
172 self.day_type = Some(DayType::Workday);
173 },
174 "weekday" => {
175 self.day_type = Some(DayType::Weekday);
176 },
177 "day" => {
178 if self.day_type.is_none() {
179 self.day_type = Some(DayType::OrdinaryDay);
180 }
181 },
182 END_OF_MONTH_TOKEN => {
183 self.day_type = Some(DayType::DayOfMonthFromEnd);
184 },
185 _ => {
186 // Unknown word - ignore for now
187 }
188 }
189
190 Ok(())
191 }
192
193 fn lex_qualifier(&mut self, word: &str) -> Outcome<()> {
194 match word {
195 "before" | "prior" => {
196 self.sign = -1;
197 },
198 "after" | "following" => {
199 self.sign = 1;
200 },
201 _ => {
202 // Unknown qualifier - ignore
203 }
204 }
205 Ok(())
206 }
207
208 pub fn set_day_of_week(&mut self, day_of_week: DayOfWeek) {
209 self.day_of_week = Some(day_of_week);
210 self.day_type = Some(DayType::DayOfWeek);
211 }
212
213 pub fn is_complete(&self) -> bool {
214 (self.is_value_defined() && self.is_day_type_defined()) ||
215 (self.day_type == Some(DayType::DayOfMonthFromEnd))
216 }
217
218 pub fn last(&self) -> &DayIncrementor {
219 let mut current = self;
220 while let Some(ref next) = current.next_inc {
221 current = next;
222 }
223 current
224 }
225
226 // Getters
227 pub fn value(&self) -> i32 { self.value }
228 pub fn sign(&self) -> i32 { self.sign }
229 pub fn day_of_week(&self) -> Option<DayOfWeek> { self.day_of_week }
230 pub fn day_type(&self) -> Option<DayType> { self.day_type.clone() }
231 pub fn next_inc(&self) -> Option<&DayIncrementor> {
232 self.next_inc.as_ref().map(|b| b.as_ref())
233 }
234
235 // State checkers
236 pub fn is_value_defined(&self) -> bool { self.value != 0 }
237 pub fn is_sign_defined(&self) -> bool { self.sign != 0 }
238 pub fn is_day_of_week_defined(&self) -> bool { self.day_of_week.is_some() }
239 pub fn is_day_type_defined(&self) -> bool { self.day_type.is_some() }
240 pub fn has_next_inc(&self) -> bool { self.next_inc.is_some() }
241
242 // Setters
243 pub fn set_value(&mut self, value: i32) {
244 self.value = value.abs();
245 }
246
247 pub fn set_sign(&mut self, sign: i32) {
248 self.sign = if sign < 0 { -1 } else if sign > 0 { 1 } else { 0 };
249 }
250
251 pub fn set_day_type(&mut self, day_type: DayType) {
252 self.day_type = Some(day_type);
253 }
254
255 pub fn set_next_inc(&mut self, next_inc: DayIncrementor) {
256 self.next_inc = Some(Box::new(next_inc));
257 }
258
259 pub fn calculate_date(&self, year: i32, month: u8, zone: CalClockZone) -> Outcome<CalendarDate> {
260 use crate::calendar::CalendarDate;
261 use crate::constant::MonthOfYear;
262
263 let month_enum = res!(MonthOfYear::from_number(month));
264
265 // If this is a simple day-of-month reference
266 if self.day_type == Some(DayType::DayOfMonthFromStart) && self.next_inc.is_none() {
267 return CalendarDate::from_ymd(year, month_enum, self.value as u8, zone);
268 }
269
270 // If this is an end-of-month reference
271 if self.day_type == Some(DayType::DayOfMonthFromEnd) {
272 let days_in_month = month_enum.days_in_month(year);
273 if self.value == 0 {
274 // "end of month" -> last day
275 return CalendarDate::from_ymd(year, month_enum, days_in_month, zone);
276 } else {
277 // "2nd last day" -> days_in_month - value + 1
278 let day = days_in_month.saturating_sub(self.value as u8 - 1);
279 return CalendarDate::from_ymd(year, month_enum, day.max(1), zone);
280 }
281 }
282
283 // Handle simple cases without next_inc first
284 if self.next_inc.is_none() || !self.next_inc.as_ref().unwrap().is_complete() {
285 // Simple case: "3rd Monday", "2nd business day", etc.
286 match self.day_type {
287 Some(DayType::DayOfWeek) => {
288 if let Some(target_dow) = self.day_of_week {
289 return self.find_nth_day_of_week(year, month, target_dow, self.value, zone);
290 }
291 },
292 Some(DayType::Weekday) => {
293 return self.find_nth_weekday(year, month, self.value, zone);
294 },
295 Some(DayType::Workday) => {
296 return self.find_nth_business_day(year, month, self.value, zone);
297 },
298 Some(DayType::DayOfMonthFromStart) => {
299 return self.calculate_day_of_month(year, month, zone);
300 },
301 _ => {}
302 }
303 }
304
305 // For complex expressions with next_inc, we need a base date to work from
306 let base_date = if let Some(ref next_inc) = self.next_inc {
307 if next_inc.is_complete() {
308 res!(next_inc.calculate_date(year, month, zone.clone()))
309 } else {
310 // Default to first day of month for incomplete next_inc
311 res!(CalendarDate::from_ymd(year, month_enum, 1, zone.clone()))
312 }
313 } else {
314 // Default to first day of month
315 res!(CalendarDate::from_ymd(year, month_enum, 1, zone.clone()))
316 };
317
318 // Apply this incrementor to the base date
319 self.apply_incrementor_to_date(&base_date)
320 }
321
322 fn apply_incrementor_to_date(&self, base_date: &CalendarDate) -> Outcome<CalendarDate> {
323 use crate::calendar::CalendarDate;
324
325 match self.day_type {
326 Some(DayType::DayOfWeek) => {
327 if let Some(target_dow) = self.day_of_week {
328 if self.sign == 0 {
329 // Find nth occurrence in same month
330 self.find_nth_day_of_week(base_date.year(), base_date.month(), target_dow, self.value, base_date.zone().clone())
331 } else if self.sign > 0 {
332 // Find nth occurrence after base_date
333 let mut current = base_date.clone();
334 for _ in 0..self.value {
335 current = res!(self.find_next_day_of_week(&current, target_dow));
336 }
337 Ok(current)
338 } else {
339 // Find nth occurrence before base_date
340 // Special case: if base_date is end of month and we want "last" day of week,
341 // we want the last occurrence in the month, not before the end
342 if self.value == 1 && base_date.day() == res!(base_date.days_in_month()) {
343 // This is "last X" in month - find the last occurrence of target_dow in this month
344 self.find_last_day_of_week_in_month(base_date.year(), base_date.month(), target_dow, base_date.zone().clone())
345 } else {
346 let mut current = base_date.clone();
347 for _ in 0..self.value {
348 current = res!(self.find_previous_day_of_week(&current, target_dow));
349 }
350 Ok(current)
351 }
352 }
353 } else {
354 Err(err!("DayOfWeek type requires day_of_week to be set"; Invalid, Input))
355 }
356 },
357 Some(DayType::Weekday) => {
358 if self.sign == 0 {
359 self.find_nth_weekday(base_date.year(), base_date.month(), self.value, base_date.zone().clone())
360 } else if self.sign > 0 {
361 let mut current = base_date.clone();
362 for _ in 0..self.value {
363 current = res!(self.find_next_weekday(&current));
364 }
365 Ok(current)
366 } else {
367 let mut current = base_date.clone();
368 for _ in 0..self.value {
369 current = res!(self.find_previous_weekday(&current));
370 }
371 Ok(current)
372 }
373 },
374 Some(DayType::Workday) => {
375 if self.sign == 0 {
376 self.find_nth_business_day(base_date.year(), base_date.month(), self.value, base_date.zone().clone())
377 } else if self.sign > 0 {
378 let mut current = base_date.clone();
379 for _ in 0..self.value {
380 current = res!(self.find_next_business_day(&current));
381 }
382 Ok(current)
383 } else {
384 let mut current = base_date.clone();
385 for _ in 0..self.value {
386 current = res!(self.find_previous_business_day(&current));
387 }
388 Ok(current)
389 }
390 },
391 Some(DayType::DayOfMonthFromStart) => {
392 self.calculate_day_of_month(base_date.year(), base_date.month(), base_date.zone().clone())
393 },
394 Some(DayType::DayOfMonthFromEnd) => {
395 let month_enum = res!(crate::constant::MonthOfYear::from_number(base_date.month()));
396 let days_in_month = month_enum.days_in_month(base_date.year());
397 let day = days_in_month.saturating_sub(self.value as u8 - 1);
398 CalendarDate::from_ymd(base_date.year(), month_enum, day.max(1), base_date.zone().clone())
399 },
400 _ => {
401 Err(err!("Unsupported DayType for date calculation: {:?}", self.day_type; Invalid, Input))
402 }
403 }
404 }
405
406 fn find_last_day_of_week_in_month(&self, year: i32, month: u8, target_dow: DayOfWeek, zone: CalClockZone) -> Outcome<CalendarDate> {
407 use crate::calendar::CalendarDate;
408 use crate::constant::MonthOfYear;
409
410 let month_enum = res!(MonthOfYear::from_number(month));
411 let days_in_month = month_enum.days_in_month(year);
412
413 // Start from the last day and work backwards
414 for day in (1..=days_in_month).rev() {
415 let current = res!(CalendarDate::from_ymd(year, month_enum, day, zone.clone()));
416 if current.day_of_week() == target_dow {
417 return Ok(current);
418 }
419 }
420
421 Err(err!("No {} found in {}/{}", target_dow, month, year; Invalid, Input, Range))
422 }
423
424 fn find_nth_day_of_week(&self, year: i32, month: u8, target_dow: DayOfWeek, n: i32, zone: CalClockZone) -> Outcome<CalendarDate> {
425 use crate::calendar::CalendarDate;
426 use crate::constant::MonthOfYear;
427
428 let month_enum = res!(MonthOfYear::from_number(month));
429 let first_day = res!(CalendarDate::from_ymd(year, month_enum, 1, zone.clone()));
430 let first_dow = first_day.day_of_week();
431
432 // Calculate offset to first occurrence of target day
433 let target_dow_num = target_dow.of() as i32;
434 let first_dow_num = first_dow.of() as i32;
435 let mut offset = (target_dow_num - first_dow_num + 7) % 7;
436
437 // Calculate the nth occurrence
438 offset += (n - 1) * 7;
439 let target_day = 1 + offset;
440
441 // Validate the day is within the month
442 let days_in_month = month_enum.days_in_month(year);
443 if target_day < 1 || target_day > days_in_month as i32 {
444 return Err(err!("Day {} of {} {} does not exist in month", n, target_dow, month; Invalid, Input, Range));
445 }
446
447 CalendarDate::from_ymd(year, month_enum, target_day as u8, zone)
448 }
449
450 fn find_nth_weekday(&self, year: i32, month: u8, n: i32, zone: CalClockZone) -> Outcome<CalendarDate> {
451 use crate::calendar::CalendarDate;
452 use crate::constant::MonthOfYear;
453
454 let month_enum = res!(MonthOfYear::from_number(month));
455 let mut current: CalendarDate;
456 let days_in_month = month_enum.days_in_month(year);
457 let mut weekday_count = 0;
458
459 for day in 1..=days_in_month {
460 current = res!(CalendarDate::from_ymd(year, month_enum, day, zone.clone()));
461 if self.is_weekday(&current) {
462 weekday_count += 1;
463 if weekday_count == n {
464 return Ok(current);
465 }
466 }
467 }
468
469 Err(err!("Only {} weekdays in {}/{}, cannot find {}", weekday_count, month, year, n; Invalid, Input, Range))
470 }
471
472 fn find_nth_business_day(&self, year: i32, month: u8, n: i32, zone: CalClockZone) -> Outcome<CalendarDate> {
473 use crate::calendar::CalendarDate;
474 use crate::constant::MonthOfYear;
475
476 let month_enum = res!(MonthOfYear::from_number(month));
477 let mut current: CalendarDate;
478 let days_in_month = month_enum.days_in_month(year);
479 let mut business_day_count = 0;
480
481 for day in 1..=days_in_month {
482 current = res!(CalendarDate::from_ymd(year, month_enum, day, zone.clone()));
483 if self.is_business_day(&current) {
484 business_day_count += 1;
485 if business_day_count == n {
486 return Ok(current);
487 }
488 }
489 }
490
491 Err(err!("Only {} business days in {}/{}, cannot find {}", business_day_count, month, year, n; Invalid, Input, Range))
492 }
493
494 fn calculate_day_of_month(&self, year: i32, month: u8, zone: CalClockZone) -> Outcome<CalendarDate> {
495 use crate::calendar::CalendarDate;
496 use crate::constant::MonthOfYear;
497
498 let month_enum = res!(MonthOfYear::from_number(month));
499 let days_in_month = month_enum.days_in_month(year);
500 let day = if self.value <= days_in_month as i32 {
501 self.value as u8
502 } else {
503 return Err(err!("Day {} does not exist in month {} of year {}", self.value, month, year; Invalid, Input, Range));
504 };
505
506 CalendarDate::from_ymd(year, month_enum, day, zone)
507 }
508
509 fn find_next_day_of_week(&self, date: &CalendarDate, target_dow: DayOfWeek) -> Outcome<CalendarDate> {
510 let mut current = date.clone();
511 let target_dow_num = target_dow.of();
512
513 loop {
514 current = res!(current.add_days(1));
515 if current.day_of_week().of() == target_dow_num {
516 return Ok(current);
517 }
518 }
519 }
520
521 fn find_previous_day_of_week(&self, date: &CalendarDate, target_dow: DayOfWeek) -> Outcome<CalendarDate> {
522 let mut current = date.clone();
523 let target_dow_num = target_dow.of();
524
525 loop {
526 current = res!(current.add_days(-1));
527 if current.day_of_week().of() == target_dow_num {
528 return Ok(current);
529 }
530 }
531 }
532
533 fn find_next_weekday(&self, date: &CalendarDate) -> Outcome<CalendarDate> {
534 let mut current = date.clone();
535
536 loop {
537 current = res!(current.add_days(1));
538 if self.is_weekday(&current) {
539 return Ok(current);
540 }
541 }
542 }
543
544 fn find_previous_weekday(&self, date: &CalendarDate) -> Outcome<CalendarDate> {
545 let mut current = date.clone();
546
547 loop {
548 current = res!(current.add_days(-1));
549 if self.is_weekday(&current) {
550 return Ok(current);
551 }
552 }
553 }
554
555 fn find_next_business_day(&self, date: &CalendarDate) -> Outcome<CalendarDate> {
556 let mut current = date.clone();
557
558 loop {
559 current = res!(current.add_days(1));
560 if self.is_business_day(&current) {
561 return Ok(current);
562 }
563 }
564 }
565
566 fn find_previous_business_day(&self, date: &CalendarDate) -> Outcome<CalendarDate> {
567 let mut current = date.clone();
568
569 loop {
570 current = res!(current.add_days(-1));
571 if self.is_business_day(&current) {
572 return Ok(current);
573 }
574 }
575 }
576
577 fn is_weekday(&self, date: &CalendarDate) -> bool {
578 let dow_num = date.day_of_week().of();
579 dow_num >= 1 && dow_num <= 5 // Monday=1 to Friday=5
580 }
581
582 /// Holidays come from the BusinessDayEngine, so this is narrower than "not a weekend".
583 pub fn is_business_day(&self, date: &CalendarDate) -> bool {
584 use crate::calendar::business_day_engine::BusinessDayEngine;
585
586 // Create a standard business day engine
587 let engine = BusinessDayEngine::new();
588
589 // Use the engine's comprehensive business day logic
590 match engine.is_business_day(date) {
591 Ok(is_business) => is_business,
592 Err(_) => {
593 // Fallback to weekday check if engine fails
594 self.is_weekday(date)
595 }
596 }
597 }
598}
599
600impl Default for DayIncrementor {
601 fn default() -> Self {
602 Self::new()
603 }
604}
605
606impl std::fmt::Display for DayIncrementor {
607 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
608 write!(f, "Sign: {} Value: {} DayType: {:?} DayOfWeek: {:?} NextInc: {:?}",
609 self.sign, self.value, self.day_type, self.day_of_week,
610 self.next_inc.as_ref().map(|n| n.to_string()))
611 }
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617
618 #[test]
619 fn test_simple_day_of_week() {
620 let inc = DayIncrementor::from_string("3rd Monday").unwrap();
621 assert_eq!(inc.value(), 3);
622 assert_eq!(inc.day_of_week(), Some(DayOfWeek::Monday));
623 assert_eq!(inc.day_type(), Some(DayType::DayOfWeek));
624 }
625
626 #[test]
627 fn test_business_day() {
628 let inc = DayIncrementor::from_string("second business day").unwrap();
629 assert_eq!(inc.value(), 2);
630 assert_eq!(inc.day_type(), Some(DayType::Workday));
631 }
632
633 #[test]
634 fn test_last_sunday() {
635 let inc = DayIncrementor::from_string("last Sunday").unwrap();
636 assert_eq!(inc.value(), 1);
637 assert_eq!(inc.day_of_week(), Some(DayOfWeek::Sunday));
638 assert_eq!(inc.sign(), -1);
639
640 // Should have next incrementor for "end of month"
641 let next = inc.next_inc().unwrap();
642 assert_eq!(next.day_type(), Some(DayType::DayOfMonthFromEnd));
643 }
644
645 #[test]
646 fn test_complex_expression() {
647 let inc = DayIncrementor::from_string("2nd weekday before the 25th").unwrap();
648 assert_eq!(inc.value(), 2);
649 assert_eq!(inc.day_type(), Some(DayType::Weekday));
650 assert_eq!(inc.sign(), -1);
651
652 let next = inc.next_inc().unwrap();
653 assert_eq!(next.value(), 25);
654 assert_eq!(next.day_type(), Some(DayType::DayOfMonthFromStart));
655 }
656
657 #[test]
658 fn test_end_of_month() {
659 let inc = DayIncrementor::from_string("end of the month").unwrap();
660 assert_eq!(inc.day_type(), Some(DayType::DayOfMonthFromEnd));
661 }
662
663 #[test]
664 fn test_calculate_date_simple_day_of_week() {
665 // Test "3rd Monday" in June 2024
666 // June 2024: starts on Saturday, so Mondays are 3rd, 10th, 17th, 24th
667 let inc = DayIncrementor::from_string("3rd Monday").unwrap();
668 let zone = crate::time::CalClockZone::utc();
669 let result = inc.calculate_date(2024, 6, zone).unwrap();
670 assert_eq!(result.year(), 2024);
671 assert_eq!(result.month(), 6);
672 assert_eq!(result.day(), 17); // 3rd Monday
673 assert_eq!(result.day_of_week(), crate::constant::DayOfWeek::Monday);
674 }
675
676 #[test]
677 fn test_calculate_date_business_day() {
678 // Test "2nd business day" in June 2024
679 // June 2024: starts on Saturday, so business days start Monday 3rd
680 let inc = DayIncrementor::from_string("2nd business day").unwrap();
681 let zone = crate::time::CalClockZone::utc();
682 let result = inc.calculate_date(2024, 6, zone).unwrap();
683 assert_eq!(result.year(), 2024);
684 assert_eq!(result.month(), 6);
685 assert_eq!(result.day(), 4); // 2nd business day (Monday 3rd, Tuesday 4th)
686 }
687
688 #[test]
689 fn test_calculate_date_end_of_month() {
690 // Test "end of the month" in February 2024 (leap year)
691 let inc = DayIncrementor::from_string("end of the month").unwrap();
692 let zone = crate::time::CalClockZone::utc();
693 let result = inc.calculate_date(2024, 2, zone.clone()).unwrap();
694 assert_eq!(result.year(), 2024);
695 assert_eq!(result.month(), 2);
696 assert_eq!(result.day(), 29); // February in leap year
697
698 // Test in non-leap year
699 let result = inc.calculate_date(2023, 2, zone).unwrap();
700 assert_eq!(result.day(), 28); // February in non-leap year
701 }
702
703 #[test]
704 fn test_calculate_last_sunday() {
705 // Test "last Sunday" -> "1st Sunday before the end of the month"
706 let inc = DayIncrementor::from_string("last Sunday").unwrap();
707 let zone = crate::time::CalClockZone::utc();
708 let result = inc.calculate_date(2024, 6, zone).unwrap();
709 assert_eq!(result.year(), 2024);
710 assert_eq!(result.month(), 6);
711 assert_eq!(result.day_of_week(), crate::constant::DayOfWeek::Sunday);
712 // Should be the last Sunday of June 2024 (June 30th is Sunday)
713 assert_eq!(result.day(), 30);
714 }
715
716 #[test]
717 fn test_calculate_complex_expression() {
718 // Test "2nd weekday before the 25th" in June 2024
719 // June 25th 2024 is Tuesday
720 // Weekdays before: Monday 24th, Friday 21st
721 // So 2nd weekday before should be Friday 21st
722 let inc = DayIncrementor::from_string("2nd weekday before the 25th").unwrap();
723 let zone = crate::time::CalClockZone::utc();
724 let result = inc.calculate_date(2024, 6, zone).unwrap();
725 assert_eq!(result.year(), 2024);
726 assert_eq!(result.month(), 6);
727 assert_eq!(result.day(), 21); // Friday 21st
728 }
729
730 #[test]
731 fn test_is_weekday() {
732 let inc = DayIncrementor::new();
733 let zone = crate::time::CalClockZone::utc();
734 let june = crate::constant::MonthOfYear::from_number(6).unwrap();
735
736 // Test Monday (weekday)
737 let monday = crate::calendar::CalendarDate::from_ymd(2024, june, 3, zone.clone()).unwrap(); // Monday
738 assert!(inc.is_weekday(&monday));
739
740 // Test Saturday (not weekday)
741 let saturday = crate::calendar::CalendarDate::from_ymd(2024, june, 1, zone.clone()).unwrap(); // Saturday
742 assert!(!inc.is_weekday(&saturday));
743
744 // Test Sunday (not weekday)
745 let sunday = crate::calendar::CalendarDate::from_ymd(2024, june, 2, zone).unwrap(); // Sunday
746 assert!(!inc.is_weekday(&sunday));
747 }
748
749 #[test]
750 fn test_find_nth_day_of_week() {
751 let inc = DayIncrementor::new();
752 let zone = crate::time::CalClockZone::utc();
753
754 // Find 1st Monday in June 2024 (should be 3rd)
755 let result = inc.find_nth_day_of_week(2024, 6, crate::constant::DayOfWeek::Monday, 1, zone.clone()).unwrap();
756 assert_eq!(result.day(), 3);
757
758 // Find 3rd Monday in June 2024 (should be 17th)
759 let result = inc.find_nth_day_of_week(2024, 6, crate::constant::DayOfWeek::Monday, 3, zone.clone()).unwrap();
760 assert_eq!(result.day(), 17);
761
762 // Try to find 5th Monday in June 2024 (should fail - only 4 Mondays)
763 let result = inc.find_nth_day_of_week(2024, 6, crate::constant::DayOfWeek::Monday, 5, zone);
764 assert!(result.is_err());
765 }
766
767 #[test]
768 fn test_day_of_month_calculation() {
769 let zone = crate::time::CalClockZone::utc();
770
771 // Simple day of month
772 let inc = DayIncrementor::with_day_of_month(15);
773 let result = inc.calculate_date(2024, 6, zone.clone()).unwrap();
774 assert_eq!(result.day(), 15);
775
776 // Invalid day (32nd of June)
777 let inc = DayIncrementor::with_day_of_month(32);
778 let result = inc.calculate_date(2024, 6, zone);
779 assert!(result.is_err());
780 }
781}