use chrono::{Datelike, Local, Timelike, Utc};
use once_cell::sync::OnceCell;
use regex::Regex;
use std::error::Error;
use std::fmt;
const YEAR_START: u16 = 1970;
const YEAR_END: u16 = 2100;
const UNIT_SECONDS: &str = "s";
const UNIT_MINUTES: &str = "m";
const UNIT_HOURS: &str = "h";
const UNIT_DAYS: &str = "D";
const UNIT_MONTHS: &str = "M";
const UNIT_YEARS: &str = "Y";
const UNIT_DELIMITER: &str = "to";
#[derive(Debug)]
pub struct DateTime {
year: u16,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
}
#[derive(Debug, Copy, Clone)]
pub enum TimeZoneOption {
Local,
Utc,
}
#[derive(Debug)]
pub enum DateError {
EmptyInput,
InvalidDateFormat,
InvalidRegex,
OutOfRange(String),
ParsingError(String),
IncorrectUnit(String),
}
impl fmt::Display for DateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let msg = match self {
DateError::EmptyInput => "The input provided is empty.",
DateError::InvalidDateFormat => "The date format is invalid.",
DateError::InvalidRegex => "The regex pattern format is incorrect.",
DateError::OutOfRange(detail) => return write!(f, "Value out of range: {}", detail),
DateError::ParsingError(detail) => return write!(f, "Error parsing value: {}", detail),
DateError::IncorrectUnit(unit) => {
return write!(f, "Unrecognized unit of measurement: {}", unit)
}
};
write!(f, "{}", msg)
}
}
impl Error for DateError {}
static TZ_OPTION: OnceCell<TimeZoneOption> = OnceCell::new();
pub fn set_timezone_option(option: TimeZoneOption) {
let _ = TZ_OPTION.set(option);
}
fn get_timezone_option() -> TimeZoneOption {
*TZ_OPTION.get().unwrap_or(&TimeZoneOption::Local)
}
pub fn get_current_date() -> DateTime {
let tz_option = get_timezone_option();
let now = match tz_option {
TimeZoneOption::Local => Local::now().naive_local(),
TimeZoneOption::Utc => Utc::now().naive_utc(),
};
let dt = DateTime {
year: now.year() as u16,
month: now.month() as u8,
day: now.day() as u8,
hour: now.hour() as u8,
minute: now.minute() as u8,
second: now.second() as u8,
};
dt
}
fn get_start_date() -> DateTime {
DateTime {
year: YEAR_START,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
}
}
fn get_end_date() -> DateTime {
DateTime {
year: YEAR_END,
month: 12,
day: 31,
hour: 23,
minute: 59,
second: 59,
}
}
fn leap_year(year: u16) -> bool {
((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)
}
fn days_of_the_month(month: u8, year: u16) -> u8 {
match month {
4 | 6 | 9 | 11 => 30,
2 => {
if leap_year(year) {
29
} else {
28
}
}
_ => 31,
}
}
pub fn seconds_to_date(mut seconds: u64) -> DateTime {
let mut dt = get_start_date();
loop {
let secs_in_year: u64 = if leap_year(dt.year) {
366 * 86400
} else {
365 * 86400
};
if seconds < secs_in_year {
break;
}
seconds -= secs_in_year;
dt.year += 1;
}
loop {
let secs_in_month: u64 = (days_of_the_month(dt.month, dt.year) as u64) * 86400;
if seconds < secs_in_month {
break;
}
seconds -= secs_in_month;
dt.month += 1;
}
dt.day = ((seconds / 86400) + 1) as u8;
seconds %= 86400;
dt.hour = (seconds / 3600) as u8;
seconds %= 3600;
dt.minute = (seconds / 60) as u8;
dt.second = (seconds % 60) as u8;
dt
}
pub fn date_to_seconds(dt: DateTime) -> u64 {
let mut seconds: u64 = 0;
for y in YEAR_START..dt.year {
seconds += if leap_year(y) {
366 * 86400
} else {
365 * 86400
};
}
for m in 1..dt.month {
seconds += (days_of_the_month(m, dt.year) as u64) * 86400;
}
seconds += ((dt.day - 1) as u64) * 86400;
seconds += (dt.hour as u64) * 3600;
seconds += (dt.minute as u64) * 60;
seconds += dt.second as u64;
seconds
}
fn get_start_of_day(mut dt: DateTime) -> DateTime {
dt.hour = 0;
dt.minute = 0;
dt.second = 0;
dt
}
fn get_start_of_month(mut dt: DateTime) -> DateTime {
dt.day = 1;
dt.hour = 0;
dt.minute = 0;
dt.second = 0;
dt
}
fn get_start_of_year(mut dt: DateTime) -> DateTime {
dt.month = 1;
dt.day = 1;
dt.hour = 0;
dt.minute = 0;
dt.second = 0;
dt
}
fn subtract_months(now: u64, months: u64) -> u64 {
let mut dt = seconds_to_date(now);
let mut total_months = dt.year as u64 * 12 + dt.month as u64 - 1;
total_months = total_months.saturating_sub(months);
dt.year = (total_months / 12) as u16;
dt.month = ((total_months % 12) + 1) as u8;
let dim = days_of_the_month(dt.month, dt.year);
if dt.day > dim {
dt.day = dim;
}
date_to_seconds(dt)
}
fn subtract_years(now: u64, years: u64) -> u64 {
let mut dt = seconds_to_date(now);
dt.year = dt.year.saturating_sub(years as u16);
let dim = days_of_the_month(dt.month, dt.year);
if dt.day > dim {
dt.day = dim;
}
date_to_seconds(dt)
}
fn subtract_days(now: u64, days: u64) -> u64 {
now.saturating_sub(days * 86400)
}
pub fn search_generic(target: u64, lower_bound: u64, upper_bound: u64) -> bool {
target >= lower_bound && target <= upper_bound
}
pub fn validate_datetime(input: &str, default: bool) -> Result<DateTime, DateError> {
let re = Regex::new(
r"^(\d{4})(?:[\-_:;.,/\\|\s]+(\d{1,2}))?(?:[\-_:;.,/\\|\s]+(\d{1,2}))?(?:[\-_:;.,/\\|\s]+(\d{1,2}))?(?:[\-_:;.,/\\|\s]+(\d{1,2}))?(?:[\-_:;.,/\\|\s]+(\d{1,2}))?$",
)
.map_err(|_| DateError::InvalidRegex)?;
let caps = re.captures(input).ok_or(DateError::InvalidDateFormat)?;
let mut dt = if default {
get_start_date()
} else {
get_end_date()
};
if let Some(y) = caps.get(1) {
dt.year = y
.as_str()
.parse()
.map_err(|e| DateError::ParsingError(format!("Year: {}", e)))?;
if dt.year < YEAR_START || dt.year > YEAR_END {
return Err(DateError::OutOfRange(format!(
"The year must be between {} and {}.",
YEAR_START, YEAR_END
)));
}
}
if let Some(m) = caps.get(2) {
dt.month = m
.as_str()
.parse()
.map_err(|e| DateError::ParsingError(format!("Month: {}", e)))?;
if dt.month < 1 || dt.month > 12 {
return Err(DateError::OutOfRange(
"The month must be between 1 and 12.".to_string(),
));
}
}
if let Some(d) = caps.get(3) {
dt.day = d
.as_str()
.parse()
.map_err(|e| DateError::ParsingError(format!("Day: {}", e)))?;
let max_day = days_of_the_month(dt.month, dt.year);
if dt.day < 1 || dt.day > max_day {
return Err(DateError::OutOfRange(format!(
"The day must be between 1 and {} for the month {}.",
max_day, dt.month
)));
}
}
if let Some(h) = caps.get(4) {
dt.hour = h
.as_str()
.parse()
.map_err(|e| DateError::ParsingError(format!("Hour: {}", e)))?;
if dt.hour > 23 {
return Err(DateError::OutOfRange(
"The hour must be between 0 and 23.".to_string(),
));
}
}
if let Some(mi) = caps.get(5) {
dt.minute = mi
.as_str()
.parse()
.map_err(|e| DateError::ParsingError(format!("Minute: {}", e)))?;
if dt.minute > 59 {
return Err(DateError::OutOfRange(
"The minutes must be between 0 and 59.".to_string(),
));
}
}
if let Some(s) = caps.get(6) {
dt.second = s
.as_str()
.parse()
.map_err(|e| DateError::ParsingError(format!("Seconds: {}", e)))?;
if dt.second > 59 {
return Err(DateError::OutOfRange(
"The seconds must be between 0 and 59.".to_string(),
));
}
}
Ok(dt)
}
fn parsing_search_absolute(input: &str) -> Result<(u64, u64), DateError> {
let parts: Vec<&str> = input.splitn(2, UNIT_DELIMITER).map(|s| s.trim()).collect();
let start_dt = validate_datetime(parts[0], true)?;
let start = date_to_seconds(start_dt);
let end = if parts.len() > 1 {
let end_dt = validate_datetime(parts[1], false)?;
date_to_seconds(end_dt)
} else {
let end_dt = validate_datetime(parts[0], false)?;
date_to_seconds(end_dt)
};
Ok((start, end))
}
fn parsing_search_relative(input: &str) -> Result<(u64, u64), DateError> {
let re = Regex::new(r"^(\d+)\s*([A-Za-z])$").map_err(|_| DateError::InvalidRegex)?;
let caps = re.captures(input).ok_or(DateError::InvalidDateFormat)?;
let value: u64 = caps
.get(1)
.unwrap()
.as_str()
.parse()
.map_err(|e| DateError::ParsingError(format!("Value: {}", e)))?;
let unit = caps.get(2).unwrap().as_str();
let now_dt = get_current_date();
let start: u64 = match unit {
UNIT_SECONDS => date_to_seconds(get_current_date()).saturating_sub(value),
UNIT_MINUTES => date_to_seconds(get_current_date()).saturating_sub(value * 60),
UNIT_HOURS => date_to_seconds(get_current_date()).saturating_sub(value * 3600),
UNIT_DAYS => {
let start_dt = get_start_of_day(now_dt);
if value > 1 {
let start_ts = subtract_days(date_to_seconds(start_dt), value - 1);
date_to_seconds(get_start_of_day(seconds_to_date(start_ts)))
} else {
date_to_seconds(start_dt)
}
}
UNIT_MONTHS => {
let start_dt = get_start_of_month(now_dt);
if value > 1 {
let start_ts = subtract_months(date_to_seconds(start_dt), value - 1);
date_to_seconds(get_start_of_month(seconds_to_date(start_ts)))
} else {
date_to_seconds(start_dt)
}
}
UNIT_YEARS => {
let start_dt = get_start_of_year(now_dt);
if value > 1 {
let start_ts = subtract_years(date_to_seconds(start_dt), value - 1);
date_to_seconds(get_start_of_year(seconds_to_date(start_ts)))
} else {
date_to_seconds(start_dt)
}
}
other => return Err(DateError::IncorrectUnit(other.to_string())),
};
let global_start = date_to_seconds(get_start_date());
if start < global_start {
return Err(DateError::OutOfRange(
"The requested period is earlier than the minimum allowed date.".to_string(),
));
}
let end = date_to_seconds(get_current_date());
Ok((start, end))
}
pub fn parsing_search(input: &str) -> Result<(u64, u64), DateError> {
if input.trim().is_empty() {
return Err(DateError::EmptyInput);
}
let pat = Regex::new(r"^(\d+)\s*([A-Za-z])$").unwrap();
let (start, end);
if let Some(_) = pat.captures(input) {
(start, end) = parsing_search_relative(input)?;
} else {
(start, end) = parsing_search_absolute(input)?;
}
Ok((start, end))
}
pub fn string_to_date_seconds(input: &str) -> Result<u64, DateError> {
if input.trim().is_empty() {
return Err(DateError::EmptyInput);
}
let dt = validate_datetime(input, true)?;
Ok(date_to_seconds(dt))
}