diff --git a/src/timesheet/mod.rs b/src/timesheet/mod.rs index 4f0a9ad..0f76d12 100644 --- a/src/timesheet/mod.rs +++ b/src/timesheet/mod.rs @@ -3,9 +3,11 @@ mod configuration; mod extract; mod point_types; mod report; +mod validation; pub use config::{Period, RepositoryConfig, TimesheetConfig}; pub use configuration::{BasicTimesheetConfiguration, TIMESHEET_DIMENSION_NAME, TIMESHEET_TAG}; pub use extract::extract_timesheets; pub use point_types::TimesheetPointType; pub use report::{DayReport, DayType, DayWarning, MonthReport, ReportWarning, TimesheetReport}; +pub use validation::find_overlapping_timecards; diff --git a/src/timesheet/validation.rs b/src/timesheet/validation.rs new file mode 100644 index 0000000..0eacf06 --- /dev/null +++ b/src/timesheet/validation.rs @@ -0,0 +1,111 @@ +use chrono::NaiveTime; + +use crate::models::Timecard; + +/// Check if two time ranges overlap. +fn timecards_overlap(a: &Timecard, b: &Timecard) -> bool { + a.from_time < b.to_time && b.from_time < a.to_time +} + +/// Find all overlapping timecard pairs for a day. +/// Returns a list of tuples containing the two overlapping timecards. +pub fn find_overlapping_timecards( + timecards: &[Timecard], +) -> Vec<((NaiveTime, NaiveTime), (NaiveTime, NaiveTime))> { + let mut overlaps = Vec::new(); + for i in 0..timecards.len() { + for j in (i + 1)..timecards.len() { + if timecards_overlap(&timecards[i], &timecards[j]) { + overlaps.push(( + (timecards[i].from_time, timecards[i].to_time), + (timecards[j].from_time, timecards[j].to_time), + )); + } + } + } + overlaps +} + +#[cfg(test)] +mod tests { + use super::*; + + fn time(hour: u32, min: u32) -> NaiveTime { + NaiveTime::from_hms_opt(hour, min, 0).unwrap() + } + + fn card(from_h: u32, from_m: u32, to_h: u32, to_m: u32) -> Timecard { + Timecard::new(time(from_h, from_m), time(to_h, to_m)) + } + + #[test] + fn test_no_overlap_adjacent_timecards() { + let timecards = vec![card(9, 0, 12, 0), card(13, 0, 17, 0)]; + let overlaps = find_overlapping_timecards(&timecards); + assert!(overlaps.is_empty()); + } + + #[test] + fn test_no_overlap_exact_touch() { + // Touching at 12:00 is NOT an overlap (end time = start time) + let timecards = vec![card(9, 0, 12, 0), card(12, 0, 17, 0)]; + let overlaps = find_overlapping_timecards(&timecards); + assert!(overlaps.is_empty()); + } + + #[test] + fn test_partial_overlap() { + let timecards = vec![card(9, 0, 12, 30), card(12, 0, 13, 0)]; + let overlaps = find_overlapping_timecards(&timecards); + assert_eq!(overlaps.len(), 1); + assert_eq!(overlaps[0].0, (time(9, 0), time(12, 30))); + assert_eq!(overlaps[0].1, (time(12, 0), time(13, 0))); + } + + #[test] + fn test_full_containment() { + // One timecard fully contains another + let timecards = vec![card(9, 0, 17, 0), card(10, 0, 11, 0)]; + let overlaps = find_overlapping_timecards(&timecards); + assert_eq!(overlaps.len(), 1); + } + + #[test] + fn test_exact_match_overlap() { + let timecards = vec![card(9, 0, 12, 0), card(9, 0, 12, 0)]; + let overlaps = find_overlapping_timecards(&timecards); + assert_eq!(overlaps.len(), 1); + } + + #[test] + fn test_multiple_overlaps_same_day() { + // First overlaps with second, and second overlaps with third + let timecards = vec![card(9, 0, 11, 0), card(10, 0, 13, 0), card(12, 0, 15, 0)]; + let overlaps = find_overlapping_timecards(&timecards); + // 9-11 overlaps with 10-13, and 10-13 overlaps with 12-15 + assert_eq!(overlaps.len(), 2); + } + + #[test] + fn test_single_timecard_no_overlap() { + let timecards = vec![card(9, 0, 17, 0)]; + let overlaps = find_overlapping_timecards(&timecards); + assert!(overlaps.is_empty()); + } + + #[test] + fn test_empty_timecards_no_overlap() { + let timecards: Vec = vec![]; + let overlaps = find_overlapping_timecards(&timecards); + assert!(overlaps.is_empty()); + } + + #[test] + fn test_three_timecards_all_overlap() { + // All three overlap with each other + let timecards = vec![card(9, 0, 15, 0), card(10, 0, 16, 0), card(11, 0, 17, 0)]; + let overlaps = find_overlapping_timecards(&timecards); + // 9-15 with 10-16, 9-15 with 11-17, and 10-16 with 11-17 + assert_eq!(overlaps.len(), 3); + } +}