zinzen 0.3.0

Algorithm for auto-scheduling time-constrained tasks on a timeline
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;
        }
    }
    // Handle the case where the last element is true
    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
}

/// Allowed intervals in `[start, end]`, constrained by the union of `periods`
/// (when present), minus `not_on` and already-occupied calendar slots.
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 {
                // Build allowed mask for the day from the union of period windows.
                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 {
                        // Midnight-crossing window: [after, day) U [0, before).
                        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![]);
    }
}