feat(timesheet): add overlap detection for timecards
Add find_overlapping_timecards function to detect overlapping time ranges on the same day. This is used to generate warnings in the timesheet report.
This commit is contained in:
parent
92ca364e55
commit
e0ba2cddf3
2 changed files with 113 additions and 0 deletions
|
|
@ -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;
|
||||
|
|
|
|||
111
src/timesheet/validation.rs
Normal file
111
src/timesheet/validation.rs
Normal file
|
|
@ -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<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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue