use crate::model::{RecurrenceFrequency, Transaction};
use chrono::{Datelike, Duration, NaiveDate};
pub fn generate_recurring_transactions(
recurring_transactions: &[Transaction],
up_to_date: NaiveDate,
) -> Vec<Transaction> {
let mut generated = Vec::new();
for recurring_tx in recurring_transactions {
if !recurring_tx.is_recurring {
continue;
}
let Some(frequency) = recurring_tx.recurrence_frequency else {
continue; };
match frequency {
RecurrenceFrequency::SemiMonthly | RecurrenceFrequency::SemiMonthlyWorkday => {
let workdays_only = frequency == RecurrenceFrequency::SemiMonthlyWorkday;
generated.extend(generate_semi_monthly_instances(
recurring_tx,
up_to_date,
workdays_only,
));
}
_ => {
let mut current_date = recurring_tx.date;
while current_date <= up_to_date {
if current_date != recurring_tx.date {
if let Some(new_tx) = create_generated_transaction(
recurring_tx,
current_date,
recurring_tx.date,
up_to_date,
) {
generated.push(new_tx);
} else if current_date
> recurring_tx.recurrence_end_date.unwrap_or(NaiveDate::MAX)
{
break;
}
}
current_date = match frequency {
RecurrenceFrequency::Daily => current_date + Duration::days(1),
RecurrenceFrequency::Weekly => current_date + Duration::weeks(1),
RecurrenceFrequency::BiWeekly => current_date + Duration::weeks(2),
RecurrenceFrequency::Monthly => {
crate::validation::add_months(current_date, 1)
}
RecurrenceFrequency::Quarterly => {
crate::validation::add_months(current_date, 3)
}
RecurrenceFrequency::Yearly => {
let next_year = current_date.year() + 1;
if current_date.month() == 2 && current_date.day() == 29 {
if !crate::validation::is_leap_year(next_year) {
NaiveDate::from_ymd_opt(next_year, 2, 28)
.unwrap_or(current_date + Duration::days(365))
} else {
current_date
.with_year(next_year)
.unwrap_or(current_date + Duration::days(365))
}
} else {
current_date
.with_year(next_year)
.unwrap_or(current_date + Duration::days(365))
}
}
RecurrenceFrequency::SemiMonthly
| RecurrenceFrequency::SemiMonthlyWorkday => unreachable!(), };
}
}
}
}
generated
}
fn generate_semi_monthly_instances(
recurring_tx: &Transaction,
up_to_date: NaiveDate,
workdays_only: bool,
) -> Vec<Transaction> {
let mut generated = Vec::new();
let start_date = recurring_tx.date;
let mut current_date =
NaiveDate::from_ymd_opt(start_date.year(), start_date.month(), 1).unwrap_or(start_date);
while current_date <= up_to_date {
let year = current_date.year();
let month = current_date.month();
for day in [15, crate::validation::days_in_month(year, month)] {
if let Some(mut target_date) = NaiveDate::from_ymd_opt(year, month, day) {
if workdays_only {
target_date = previous_weekday_on_or_before(target_date);
}
if let Some(new_tx) =
create_generated_transaction(recurring_tx, target_date, start_date, up_to_date)
{
generated.push(new_tx);
} else if target_date > recurring_tx.recurrence_end_date.unwrap_or(NaiveDate::MAX) {
return generated;
}
}
}
current_date = crate::validation::add_months(current_date, 1);
if current_date > up_to_date {
break;
}
}
generated
}
fn previous_weekday_on_or_before(mut date: NaiveDate) -> NaiveDate {
while matches!(date.weekday(), chrono::Weekday::Sat | chrono::Weekday::Sun) {
date -= Duration::days(1);
}
date
}
fn create_generated_transaction(
recurring_tx: &Transaction,
target_date: NaiveDate,
start_date: NaiveDate,
up_to_date: NaiveDate,
) -> Option<Transaction> {
if target_date <= start_date || target_date > up_to_date {
return None;
}
if let Some(end_date) = recurring_tx.recurrence_end_date
&& target_date > end_date
{
return None;
}
let mut new_tx = recurring_tx.clone();
new_tx.date = target_date;
new_tx.is_generated_from_recurring = true;
new_tx.parent_id = recurring_tx.id;
new_tx.id = None;
Some(new_tx)
}
pub fn remove_generated_recurring_transactions(transactions: &mut Vec<Transaction>) {
transactions.retain(|tx| !tx.is_generated_from_recurring);
}