first_days_of_week 0.1.0

Package made for searching for defined first days sequence in the next months
Documentation
//! # First days of week
//! Library helping finding sequence of first occurences of defined days of week in the next months (useful for people following Devotion of the Nine First Fridays or First Saturdays Devotion)
use chrono::{Datelike, NaiveDate, Days, Weekday};
/// Finds the first occurrences of a specific weekday for the upcoming months.
///
/// The function starts searching from the month following `start_from`
/// (or moves to the next month if the target weekday in the current month has already passed or is today).
///
/// # Arguments
///
/// * `start_from` - The reference date to start the search from.
/// * `target_day` - The specific day of the week to look for (e.g., `Weekday::Fri`).
/// * `count` - The number of occurrences (months) to return.
///
/// # Examples
///
/// ```
/// use chrono::{NaiveDate, Weekday};
/// use first_days_of_week::get_n_first_days_of_week;
/// // Assuming start date is May 12th, 2026, the first Friday of June is June 5th.
/// let start = NaiveDate::from_ymd_opt(2026, 5, 12).unwrap();
/// let fridays = get_n_first_days_of_week(start, Weekday::Fri, 1);
/// assert_eq!(fridays[0], NaiveDate::from_ymd_opt(2026, 6, 5).unwrap());
///```
pub fn get_n_first_days_of_week(start_from: NaiveDate, target_day: Weekday, count: usize) -> Vec<NaiveDate> {
    let mut result = Vec::with_capacity(count);

    let first_of_current_month = NaiveDate::from_ymd_opt(start_from.year(), start_from.month(), 1).unwrap();
    let first_weekday = first_of_current_month.weekday().num_days_from_monday();
    let target_weekday = target_day.num_days_from_monday();

    let days_to_add = (target_weekday + 7 - first_weekday) % 7;
    let mut current_date = first_of_current_month + Days::new(days_to_add as u64);

    if current_date <= start_from {
        current_date = jump_to_next_occurrence(current_date);
    }

    for _ in 0..count {
        result.push(current_date);
        current_date = jump_to_next_occurrence(current_date);
    }

    result
}

fn jump_to_next_occurrence(current_date: NaiveDate) -> NaiveDate {
    let next_jump = current_date + Days::new(28);
    if next_jump.month() == current_date.month() {
        next_jump + Days::new(7)
    } else {
        next_jump
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_forward_looking_logic() {
        let start_date = NaiveDate::from_ymd_opt(2026, 5, 12).unwrap();
        let target = Weekday::Fri;

        let fridays = get_n_first_days_of_week(start_date, target, 3);

        assert_eq!(fridays[0], NaiveDate::from_ymd_opt(2026, 6, 5).unwrap());
        assert_eq!(fridays[1], NaiveDate::from_ymd_opt(2026, 7, 3).unwrap());
    }

    #[test]
    fn test_exact_day_start() {
        let start_date = NaiveDate::from_ymd_opt(2026, 5, 1).unwrap();
        let target = Weekday::Fri;

        let fridays = get_n_first_days_of_week(start_date, target, 1);

        assert_eq!(fridays[0], NaiveDate::from_ymd_opt(2026, 6, 5).unwrap());
    }
}