Add find_overlapping_timecards function to detect overlapping time ranges on the same day. This is used to generate warnings in the timesheet report.
111 lines
3.6 KiB
Rust
111 lines
3.6 KiB
Rust
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<Timecard> = 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);
|
|
}
|
|
}
|