use crate::currency::number_to_words;
use chrono::{Datelike, NaiveDate, Weekday};
fn get_month_name(month: u32) -> &'static str {
match month {
1 => "janeiro",
2 => "fevereiro",
3 => "março",
4 => "abril",
5 => "maio",
6 => "junho",
7 => "julho",
8 => "agosto",
9 => "setembro",
10 => "outubro",
11 => "novembro",
12 => "dezembro",
_ => "",
}
}
pub fn convert_date_to_text(date: &str) -> Option<String> {
let parts: Vec<&str> = date.split('/').collect();
if parts.len() != 3 {
return None;
}
let day: u32 = parts[0].parse().ok()?;
let month: u32 = parts[1].parse().ok()?;
let year: i32 = parts[2].parse().ok()?;
NaiveDate::from_ymd_opt(year, month, day)?;
let day_str = if day == 1 {
"Primeiro".to_string()
} else {
let mut text = number_to_words(day as i64);
if let Some(first_char) = text.chars().next() {
text = first_char.to_uppercase().to_string() + &text[first_char.len_utf8()..];
}
text
};
let month_name = get_month_name(month);
let year_str = number_to_words(year as i64);
Some(format!("{} de {} de {}", day_str, month_name, year_str))
}
fn calculate_easter(year: i32) -> NaiveDate {
let a = year % 19;
let b = year / 100;
let c = year % 100;
let d = b / 4;
let e = b % 4;
let f = (b + 8) / 25;
let g = (b - f + 1) / 3;
let h = (19 * a + b - d - g + 15) % 30;
let i = c / 4;
let k = c % 4;
let l = (32 + 2 * e + 2 * i - h - k) % 7;
let m = (a + 11 * h + 22 * l) / 451;
let month = (h + l - 7 * m + 114) / 31;
let day = ((h + l - 7 * m + 114) % 31) + 1;
NaiveDate::from_ymd_opt(year, month as u32, day as u32).unwrap()
}
pub fn is_holiday(target_date: NaiveDate, uf: Option<&str>) -> Option<bool> {
const VALID_UFS: &[&str] = &[
"AC", "AL", "AM", "AP", "BA", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB",
"PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO",
];
if let Some(state) = uf {
if !VALID_UFS.contains(&state) {
return None;
}
}
let year = target_date.year();
let month = target_date.month();
let day = target_date.day();
if is_national_holiday(year, month, day, target_date) {
return Some(true);
}
if let Some(state) = uf {
if is_state_holiday(year, month, day, target_date, state) {
return Some(true);
}
}
Some(false)
}
fn is_national_holiday(year: i32, month: u32, day: u32, date: NaiveDate) -> bool {
let is_fixed_holiday = match (month, day) {
(1, 1) => true, (4, 21) if year != 1931 && year != 1932 => true, (5, 1) if year >= 1925 => true, (9, 7) if year >= 1890 => true, (10, 12) if year <= 1930 || year >= 1980 => true, (11, 2) => true, (11, 15) => true, (12, 25) if year >= 1922 => true, _ => false,
};
if is_fixed_holiday {
return true;
}
let easter = calculate_easter(year);
let good_friday = easter - chrono::Duration::days(2);
date == good_friday
}
fn is_state_holiday(year: i32, month: u32, day: u32, date: NaiveDate, uf: &str) -> bool {
if year < 1996 {
return false;
}
match uf {
"AC" => {
match (month, day) {
(1, 23) if year >= 2005 => true, (3, 8) if year >= 2002 => true, (6, 15) => true, (9, 5) if year >= 2004 => true, (11, 17) => true, _ => false,
}
}
"AL" => {
matches!(
(month, day),
(6, 24) | (6, 29) | (9, 16) | (11, 20) ) || (month == 11 && day == 30 && year >= 2013) }
"AM" => {
(month == 9 && day == 5) || (month == 11 && day == 20 && year >= 2010) }
"AP" => {
match (month, day) {
(3, 19) if year >= 2003 => true, (7, 25) if year >= 2012 => true, (9, 13) => true, (11, 20) if year >= 2008 => true, _ => false,
}
}
"BA" => {
matches!((month, day), (7, 2)) }
"CE" => {
matches!(
(month, day),
(3, 19) | (3, 25) ) || (month == 8 && day == 15 && year >= 2004) }
"DF" => {
matches!(
(month, day),
(4, 21) | (11, 30) )
}
"ES" => {
if year >= 2020 {
let easter = calculate_easter(year);
let penha = easter + chrono::Duration::days(8);
date == penha
} else {
false
}
}
"GO" => {
matches!(
(month, day),
(7, 26) | (10, 24) )
}
"MA" => {
matches!((month, day), (7, 28)) }
"MG" => {
matches!((month, day), (4, 21)) }
"MS" => {
matches!((month, day), (10, 11)) }
"MT" => {
month == 11 && day == 20 && year >= 2003 }
"PA" => {
matches!((month, day), (8, 15)) }
"PB" => {
matches!((month, day), (8, 5)) }
"PE" => {
if year >= 2008 && month == 3 {
let first_day = NaiveDate::from_ymd_opt(year, 3, 1).unwrap();
let first_sunday = match first_day.weekday() {
Weekday::Sun => first_day,
_ => {
let days_until_sunday = 7 - first_day.weekday().num_days_from_sunday();
first_day + chrono::Duration::days(days_until_sunday as i64)
}
};
date == first_sunday
} else {
false
}
}
"PI" => {
matches!((month, day), (10, 19)) }
"PR" => {
matches!((month, day), (12, 19)) }
"RJ" => {
match (month, day) {
(4, 23) if year >= 2008 => true, (11, 20) if year >= 2002 => true, _ => false,
}
}
"RN" => {
match (month, day) {
(8, 7) if year >= 2000 => true, (10, 3) if year >= 2007 => true, _ => false,
}
}
"RO" => {
(month == 1 && day == 4) || (month == 6 && day == 18 && year >= 2002) }
"RR" => {
matches!((month, day), (10, 5)) }
"RS" => {
matches!((month, day), (9, 20)) }
"SC" => {
let is_sc_state_day = if year >= 2005 {
if month == 8 && day >= 11 {
let target = NaiveDate::from_ymd_opt(year, 8, 11).unwrap();
let first_sunday = match target.weekday() {
Weekday::Sun => target,
_ => {
let days_until_sunday = 7 - target.weekday().num_days_from_sunday();
target + chrono::Duration::days(days_until_sunday as i64)
}
};
date == first_sunday
} else {
false
}
} else if year == 2004 {
month == 8 && day == 11
} else {
false
};
let is_saint_catherine = if (1999..=2030).contains(&year) && year != 2004 {
if month == 11 && day >= 25 {
let target = NaiveDate::from_ymd_opt(year, 11, 25).unwrap();
let first_sunday = match target.weekday() {
Weekday::Sun => target,
_ => {
let days_until_sunday = 7 - target.weekday().num_days_from_sunday();
target + chrono::Duration::days(days_until_sunday as i64)
}
};
date == first_sunday
} else {
false
}
} else {
month == 11 && day == 25
};
is_sc_state_day || is_saint_catherine
}
"SE" => {
matches!((month, day), (7, 8)) }
"SP" => {
month == 7 && day == 9 && year >= 1997 }
"TO" => {
match (month, day) {
(3, 18) if year >= 1998 => true, (9, 8) => true, (10, 5) => true, _ => false,
}
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_convert_date_to_text_basic() {
assert_eq!(
convert_date_to_text("01/01/2024"),
Some("Primeiro de janeiro de dois mil e vinte e quatro".to_string())
);
assert_eq!(
convert_date_to_text("15/08/1990"),
Some("Quinze de agosto de mil, novecentos e noventa".to_string())
);
assert_eq!(
convert_date_to_text("25/12/2000"),
Some("Vinte e cinco de dezembro de dois mil".to_string())
);
}
#[test]
fn test_convert_date_to_text_invalid() {
assert_eq!(convert_date_to_text("invalid"), None);
assert_eq!(convert_date_to_text("32/01/2024"), None);
assert_eq!(convert_date_to_text("01/13/2024"), None);
assert_eq!(convert_date_to_text("29/02/2023"), None); }
#[test]
fn test_is_holiday_national() {
assert_eq!(
is_holiday(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(), None),
Some(true)
);
assert_eq!(
is_holiday(NaiveDate::from_ymd_opt(2024, 1, 2).unwrap(), None),
Some(false)
);
assert_eq!(
is_holiday(NaiveDate::from_ymd_opt(2024, 9, 7).unwrap(), None),
Some(true)
);
assert_eq!(
is_holiday(NaiveDate::from_ymd_opt(2024, 12, 25).unwrap(), None),
Some(true)
);
}
#[test]
fn test_is_holiday_state() {
assert_eq!(
is_holiday(NaiveDate::from_ymd_opt(2024, 7, 2).unwrap(), Some("BA")),
Some(true)
);
assert_eq!(
is_holiday(NaiveDate::from_ymd_opt(2024, 7, 2).unwrap(), Some("SP")),
Some(false)
);
assert_eq!(
is_holiday(NaiveDate::from_ymd_opt(2024, 7, 9).unwrap(), Some("SP")),
Some(true)
);
}
#[test]
fn test_is_holiday_invalid_uf() {
assert_eq!(
is_holiday(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(), Some("XX")),
None
);
}
#[test]
fn test_is_holiday_good_friday() {
assert_eq!(
is_holiday(NaiveDate::from_ymd_opt(2024, 3, 29).unwrap(), None),
Some(true)
);
assert_eq!(
is_holiday(NaiveDate::from_ymd_opt(2023, 4, 7).unwrap(), None),
Some(true)
);
}
#[test]
fn test_calculate_easter() {
assert_eq!(
calculate_easter(2024),
NaiveDate::from_ymd_opt(2024, 3, 31).unwrap()
);
assert_eq!(
calculate_easter(2023),
NaiveDate::from_ymd_opt(2023, 4, 9).unwrap()
);
assert_eq!(
calculate_easter(2025),
NaiveDate::from_ymd_opt(2025, 4, 20).unwrap()
);
}
#[test]
fn test_get_month_name() {
assert_eq!(get_month_name(1), "janeiro");
assert_eq!(get_month_name(2), "fevereiro");
assert_eq!(get_month_name(3), "março");
assert_eq!(get_month_name(12), "dezembro");
}
}