use lazy_static::lazy_static;
use regex::Regex;
lazy_static! {
static ref ISO_DATE_PATTERN: Regex = Regex::new(r"^(\d{4})-(\d{2})-(\d{2})$")
.expect("ISO date pattern is hardcoded and valid");
static ref SLASH_DATE_PATTERN: Regex = Regex::new(r"^(\d{1,2})[/-](\d{1,2})[/-](\d{2,4})$")
.expect("Slash date pattern is hardcoded and valid");
}
pub fn validate_date(value: &str) -> f64 {
if let Some(caps) = ISO_DATE_PATTERN.captures(value) {
let year: i32 = caps[1].parse().unwrap_or(0);
let month: i32 = caps[2].parse().unwrap_or(0);
let day: i32 = caps[3].parse().unwrap_or(0);
return validate_date_components(year, month, day);
}
if let Some(caps) = SLASH_DATE_PATTERN.captures(value) {
let part1: i32 = caps[1].parse().unwrap_or(0);
let part2: i32 = caps[2].parse().unwrap_or(0);
let year: i32 = caps[3].parse().unwrap_or(0);
let year = if year < 100 { 2000 + year } else { year };
if part1 >= 1 && part1 <= 31 && part2 >= 1 && part2 <= 12 {
return validate_date_components(year, part2, part1);
} else if part2 >= 1 && part2 <= 31 && part1 >= 1 && part1 <= 12 {
return validate_date_components(year, part1, part2);
} else {
return -0.50;
}
}
-0.50
}
fn validate_date_components(year: i32, month: i32, day: i32) -> f64 {
if year < 1900 || year > 2100 {
return -0.50;
}
if month < 1 || month > 12 {
return -0.50;
}
if day < 1 || day > 31 {
return -0.50;
}
let max_days = match month {
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
4 | 6 | 9 | 11 => 30, _ => 31, };
if day > max_days {
return 0.10;
}
0.20
}
fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
pub fn validate_amount(value: &str) -> f64 {
let cleaned = value.replace(",", "").replace(".", "");
if !cleaned
.chars()
.all(|c| c.is_ascii_digit() || c == '-' || c == '.')
{
return -0.50;
}
let amount_str = value.replace(",", "");
let amount: f64 = match amount_str.parse() {
Ok(a) => a,
Err(_) => return -0.50,
};
if amount < 0.0 {
return -0.30;
}
if amount == 0.0 {
return -0.20;
}
if let Some(dot_pos) = amount_str.find('.') {
let decimals = amount_str.len() - dot_pos - 1;
if decimals == 2 {
return 0.20;
} else {
return 0.10;
}
}
0.10
}
pub fn validate_invoice_number(value: &str) -> f64 {
if value.len() < 2 {
return -0.30;
}
let has_letters = value.chars().any(|c| c.is_alphabetic());
let has_separators = value.contains('-') || value.contains('/') || value.contains('_');
if has_letters && has_separators {
return 0.10;
}
if has_letters {
return 0.08;
}
let all_numeric = value.chars().all(|c| c.is_ascii_digit());
if all_numeric {
return 0.05;
}
0.0
}
pub fn validate_vat_number(value: &str) -> f64 {
if value.is_empty() {
return -0.20;
}
if value.starts_with("GB") && value.len() >= 11 {
let numeric_part = &value[2..];
if numeric_part.chars().all(|c| c.is_ascii_digit()) {
return 0.15;
}
}
if value.len() >= 9 {
if let Some(first_char) = value.chars().next() {
if first_char.is_alphabetic() {
let middle = &value[1..9];
if middle.chars().all(|c| c.is_ascii_digit()) {
return 0.15;
}
}
}
}
if value.starts_with("IT") && value.len() == 13 {
let numeric_part = &value[2..];
if numeric_part.chars().all(|c| c.is_ascii_digit()) {
return 0.15;
}
}
if value.starts_with("DE") && value.len() == 11 {
let numeric_part = &value[2..];
if numeric_part.chars().all(|c| c.is_ascii_digit()) {
return 0.15;
}
}
if value.chars().all(|c| c.is_ascii_digit()) && value.len() >= 8 {
return 0.05;
}
0.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_date_iso8601() {
assert_eq!(validate_date("2025-01-20"), 0.20);
assert_eq!(validate_date("2025-12-31"), 0.20);
assert_eq!(validate_date("2025-02-29"), 0.10); assert_eq!(validate_date("2024-02-29"), 0.20); }
#[test]
fn test_validate_date_invalid() {
assert_eq!(validate_date("2025-13-01"), -0.50); assert_eq!(validate_date("2025-00-15"), -0.50); assert_eq!(validate_date("2025-01-32"), -0.50); assert_eq!(validate_date("99/99/9999"), -0.50); assert_eq!(validate_date("not-a-date"), -0.50); }
#[test]
fn test_validate_date_slash_format() {
assert_eq!(validate_date("20/01/2025"), 0.20); assert_eq!(validate_date("01/20/2025"), 0.20); assert_eq!(validate_date("20/01/25"), 0.20); }
#[test]
fn test_validate_amount_valid() {
assert_eq!(validate_amount("1234.56"), 0.20);
assert_eq!(validate_amount("1,234.56"), 0.20);
assert_eq!(validate_amount("0.01"), 0.20);
}
#[test]
fn test_validate_amount_invalid() {
assert_eq!(validate_amount("-123.45"), -0.30); assert_eq!(validate_amount("0.00"), -0.20); }
#[test]
fn test_validate_amount_non_standard() {
assert_eq!(validate_amount("1234"), 0.10); assert_eq!(validate_amount("1234.5"), 0.10); assert_eq!(validate_amount("1234.567"), 0.10); }
#[test]
fn test_validate_invoice_number() {
assert_eq!(validate_invoice_number("INV-2025-001"), 0.10);
assert_eq!(validate_invoice_number("FAC/2025/123"), 0.10);
assert_eq!(validate_invoice_number("12345"), 0.05);
assert_eq!(validate_invoice_number("1"), -0.30);
assert_eq!(validate_invoice_number(""), -0.30);
}
#[test]
fn test_validate_vat_number() {
assert_eq!(validate_vat_number("GB272052232"), 0.15); assert_eq!(validate_vat_number("A12345678Z"), 0.15); assert_eq!(validate_vat_number("IT12345678901"), 0.15); assert_eq!(validate_vat_number("DE123456789"), 0.15); assert_eq!(validate_vat_number("123456789"), 0.05); assert_eq!(validate_vat_number(""), -0.20); }
#[test]
fn test_leap_year() {
assert!(is_leap_year(2024));
assert!(!is_leap_year(2025));
assert!(is_leap_year(2000));
assert!(!is_leap_year(1900));
}
#[test]
fn test_validate_invoice_number_letters_only() {
assert_eq!(validate_invoice_number("INV2025001"), 0.08);
assert_eq!(validate_invoice_number("FAC123"), 0.08);
}
#[test]
fn test_validate_invoice_number_special_chars() {
assert_eq!(validate_invoice_number("12#34"), 0.0);
assert_eq!(validate_invoice_number("12.34.56"), 0.0);
assert_eq!(validate_invoice_number("AB.CD"), 0.08); }
#[test]
fn test_validate_vat_unknown_format() {
assert_eq!(validate_vat_number("XY12345"), 0.0);
assert_eq!(validate_vat_number("ABC"), 0.0);
}
#[test]
fn test_validate_amount_non_numeric() {
assert_eq!(validate_amount("abc"), -0.50);
assert_eq!(validate_amount("12.34.56"), -0.50);
}
#[test]
fn test_validate_date_year_out_of_range() {
assert_eq!(validate_date("1899-01-01"), -0.50);
assert_eq!(validate_date("2101-01-01"), -0.50);
}
}