use crate::models::calendar::Calendar;
use crate::models::calendar_interval::CalIntStatus;
use crate::models::goal::{BudgetPeriod, Slot};
use crate::models::interval::Interval;
use crate::models::time_grid::SLOTS_PER_DAY;
use chrono::{Datelike, Duration, NaiveDateTime};
use std::ops::Sub;
pub fn reduce(compatible_hours: &[bool]) -> Vec<Interval> {
let mut result: Vec<Interval> = vec![];
let mut start: Option<usize> = None;
for (index, hour) in compatible_hours.iter().enumerate() {
if *hour {
if start.is_none() {
start = Some(index);
}
} else if start.is_some() {
result.push(Interval {
start: start.unwrap(),
end: index,
});
start = None;
}
}
if let Some(start) = start {
result.push(Interval {
start,
end: compatible_hours.len(),
});
}
result
}
fn subtract_intervals(a: Vec<Interval>, b: &Vec<Interval>) -> Vec<Interval> {
let mut result = Vec::new();
for interval in a {
let mut current = interval;
for other_interval in b {
if other_interval.start < current.end && other_interval.end > current.start {
if other_interval.start > current.start {
result.push(Interval {
start: current.start,
end: other_interval.start,
});
}
if other_interval.end < current.end {
current.start = other_interval.end;
} else {
current.start = current.end;
break;
}
}
}
if current.start < current.end {
result.push(current);
}
}
result
}
pub(crate) fn get_compatible_intervals(
calendar: &Calendar,
periods: Option<&[BudgetPeriod]>,
start: NaiveDateTime,
end: NaiveDateTime,
not_on: &Option<Vec<Slot>>,
) -> Vec<Interval> {
let mut result = vec![Interval {
start: calendar.get_index_of(start),
end: calendar.get_index_of(end),
}];
if let Some(not_on) = not_on {
for slot in not_on {
result = subtract_intervals(
result,
&vec![Interval {
start: calendar.get_index_of(slot.start),
end: calendar.get_index_of(slot.end),
}],
);
}
}
let mut intervals_to_remove: Vec<Interval> = vec![];
if let Some(periods) = periods {
let end_dt = calendar.end_date_time;
let mut current = calendar.start_date_time.sub(Duration::days(1));
let mut current_index_offset: usize = 0;
while current <= end_dt {
let weekday = current.weekday();
let matching: Vec<&BudgetPeriod> = periods
.iter()
.filter(|p| p.on_days.contains(&weekday))
.collect();
if matching.is_empty() {
intervals_to_remove.push(Interval {
start: current_index_offset,
end: current_index_offset + SLOTS_PER_DAY,
});
} else {
let mut allowed = vec![false; SLOTS_PER_DAY];
for period in matching {
let after = period.window.after_time;
let before = period.window.before_time;
if after < before {
for slot in allowed
.iter_mut()
.take(before.min(SLOTS_PER_DAY))
.skip(after)
{
*slot = true;
}
} else {
for slot in allowed.iter_mut().skip(after) {
*slot = true;
}
for slot in allowed.iter_mut().take(before.min(SLOTS_PER_DAY)) {
*slot = true;
}
}
}
for (slot, is_allowed) in allowed.iter().enumerate() {
if !is_allowed {
intervals_to_remove.push(Interval {
start: current_index_offset + slot,
end: current_index_offset + slot + 1,
});
}
}
}
current += Duration::days(1);
current_index_offset += SLOTS_PER_DAY;
}
}
result = subtract_intervals(result, &intervals_to_remove);
let mut intervals_to_remove2: Vec<Interval> = vec![];
for cal_interval in &calendar.intervals {
match cal_interval.status {
CalIntStatus::Claimable(_) => {}
CalIntStatus::Occupied(_, _) => intervals_to_remove2.push(Interval {
start: cal_interval.interval.start,
end: cal_interval.interval.end,
}),
}
}
result = subtract_intervals(result, &intervals_to_remove2);
result
}
#[cfg(test)]
mod tests {
use super::{reduce, subtract_intervals};
use crate::models::interval::Interval;
fn iv(start: usize, end: usize) -> Interval {
Interval { start, end }
}
#[test]
fn reduce_groups_contiguous_true_runs() {
let mask = [true, true, false, true, true, true, false];
assert_eq!(reduce(&mask), vec![iv(0, 2), iv(3, 6)]);
}
#[test]
fn reduce_handles_trailing_true_and_all_false() {
assert_eq!(reduce(&[false, true, true]), vec![iv(1, 3)]);
assert_eq!(reduce(&[false, false]), vec![]);
assert_eq!(reduce(&[true, true]), vec![iv(0, 2)]);
}
#[test]
fn subtract_removes_middle_and_keeps_disjoint() {
assert_eq!(
subtract_intervals(vec![iv(0, 6)], &vec![iv(2, 4)]),
vec![iv(0, 2), iv(4, 6)]
);
assert_eq!(
subtract_intervals(vec![iv(0, 2)], &vec![iv(5, 9)]),
vec![iv(0, 2)]
);
}
#[test]
fn subtract_full_cover_yields_empty() {
assert_eq!(subtract_intervals(vec![iv(2, 4)], &vec![iv(0, 10)]), vec![]);
}
}