use crate::model::{CategoryInfo, TransactionType};
use chrono::{Datelike, NaiveDate};
use rust_decimal::Decimal;
pub fn validate_and_insert_date_char(field: &str, c: char) -> Option<String> {
if !c.is_ascii_digit() {
return None;
}
let len = field.len();
if len >= 10 {
return None;
}
let mut result = field.to_string();
if len == 5 {
let month_digit = c.to_digit(10).unwrap_or(0);
if month_digit > 1 {
result.push('0');
result.push(c);
result.push('-');
return Some(result);
} else {
result.push(c);
return Some(result);
}
} else if len == 6 {
let first_digit = field
.chars()
.nth(5)
.and_then(|ch| ch.to_digit(10))
.unwrap_or(0);
let month = first_digit * 10 + c.to_digit(10).unwrap_or(0);
if month == 0 || month > 12 {
return None;
}
}
if len == 8 {
let day_digit = c.to_digit(10).unwrap_or(0);
if day_digit > 3 {
return None;
}
} else if len == 9 {
if let (Ok(year), Ok(month)) = (field[0..4].parse::<i32>(), field[5..7].parse::<u32>()) {
let first_digit = field
.chars()
.nth(8)
.and_then(|ch| ch.to_digit(10))
.unwrap_or(0);
let day = first_digit * 10 + c.to_digit(10).unwrap_or(0);
let last_day = days_in_month(year, month);
if day == 0 || day > last_day {
return None;
}
}
}
result.push(c);
if result.len() == 4 {
if let Ok(year) = result.parse::<i32>() {
if !(1900..=2100).contains(&year) {
return None; }
}
result.push('-');
} else if result.len() == 7 {
result.push('-');
}
Some(result)
}
pub fn handle_date_backspace(field: &mut String) {
let len = field.len();
if field.ends_with('-') && (len == 5 || len == 8) {
if field
.chars()
.nth(len - 2)
.is_some_and(|ch| ch.is_ascii_digit())
{
field.pop(); field.pop(); } else {
field.pop();
}
} else if !field.is_empty() {
field.pop();
}
}
pub fn validate_amount_char(field: &str, c: char) -> bool {
c.is_ascii_digit() || (c == '.' && !field.contains('.'))
}
pub fn validate_amount_string(amount_str: &str) -> Result<Decimal, String> {
match amount_str.parse::<Decimal>() {
Ok(amount) if amount > Decimal::ZERO => Ok(amount),
Ok(_) => Err("Amount must be positive".to_string()),
Err(_) => Err("Invalid amount format".to_string()),
}
}
pub fn validate_category(
categories: &[CategoryInfo],
transaction_type: TransactionType,
category: &str,
subcategory: &str,
) -> Result<(), String> {
if category.is_empty() || category.eq_ignore_ascii_case("Uncategorized") {
return Ok(());
}
let category_lower = category.to_lowercase();
let subcategory_lower = subcategory.to_lowercase();
if subcategory.is_empty() {
let category_exists = categories.iter().any(|cat_info| {
cat_info.transaction_type == transaction_type
&& cat_info.category.to_lowercase() == category_lower
});
if category_exists {
return Ok(());
}
} else {
let pair_exists = categories.iter().any(|cat_info| {
cat_info.transaction_type == transaction_type
&& cat_info.category.to_lowercase() == category_lower
&& cat_info.subcategory.to_lowercase() == subcategory_lower
});
if pair_exists {
return Ok(());
}
}
Err(format!(
"Invalid Category/Subcategory: '{}' / '{}' for {:?}",
category, subcategory, transaction_type
))
}
pub fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
pub fn days_in_month(year: i32, month: u32) -> u32 {
match month {
1 => 31,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
3 => 31,
4 => 30,
5 => 31,
6 => 30,
7 => 31,
8 => 31,
9 => 30,
10 => 31,
11 => 30,
12 => 31,
_ => 31,
}
}
pub fn add_months(date: NaiveDate, months_to_add: i32) -> NaiveDate {
let mut year = date.year();
let mut month = date.month() as i32 + months_to_add;
if months_to_add.abs() > 120000 {
return date; }
while month > 12 {
year = year.saturating_add(1);
month -= 12;
if year > 3000 {
return date;
}
}
while month < 1 {
year = year.saturating_sub(1);
month += 12;
if year < 1000 {
return date;
}
}
let day = date.day();
let days_in_target_month = days_in_month(year, month as u32);
let actual_day = day.min(days_in_target_month);
NaiveDate::from_ymd_opt(year, month as u32, actual_day)
.or_else(|| NaiveDate::from_ymd_opt(year, month as u32, 28))
.or_else(|| NaiveDate::from_ymd_opt(year, month as u32, 1))
.unwrap_or(date) }
pub fn strip_path_quotes(path: &str) -> String {
let mut result = path.to_string();
if result.starts_with('"') || result.starts_with('\'') {
result = result[1..].to_string();
}
if result.ends_with('"') || result.ends_with('\'') {
result = result[..result.len() - 1].to_string();
}
result
}