use crate::{
commands::{Error, Result},
entities::{DateTime, Duration, NaiveDate, NaiveDateTime, NaiveTime},
};
pub(crate) fn from_start(user_input: &impl ToString) -> Result<DateTime> {
from_start_ex(user_input, DateTime::now())
}
pub(crate) fn from_start_ex(user_input: &impl ToString, now: DateTime) -> Result<DateTime> {
match interpret(&user_input.to_string())? {
Interpretation::Now => Ok(now),
Interpretation::DateTime(naive_date_time) => Ok(DateTime::from_naive(naive_date_time)),
Interpretation::Time(naive_time) => Ok(now.with_time(naive_time)),
Interpretation::Relative(time_delta) => Ok(now.with_delta(time_delta)),
_ => todo!(),
}
}
pub(crate) fn from_end(user_input: &impl ToString, start: DateTime) -> Result<DateTime> {
from_end_ex(user_input, start, DateTime::now())
}
pub(crate) fn from_end_ex(
user_input: &impl ToString,
start: DateTime,
now: DateTime,
) -> Result<DateTime> {
match interpret(&user_input.to_string())? {
Interpretation::Now => Ok(now),
Interpretation::DateTime(naive_date_time) => Ok(DateTime::from_naive(naive_date_time)),
Interpretation::Time(naive_time) => {
let dt = now.with_time(naive_time);
let dt = if start > dt {
dt + Duration::days(1)
} else {
dt
};
Ok(dt)
}
Interpretation::Relative(time_delta) => Ok(start - time_delta),
_ => todo!(),
}
}
#[allow(unused)]
pub(crate) fn from_range_days(
user_input_start: &impl ToString,
user_input_end: &Option<impl ToString>,
now: DateTime,
) -> Result<(DateTime, DateTime)> {
from_range_ex(user_input_start, user_input_end, false, now)
}
pub(crate) fn from_range_months(
user_input_start: &impl ToString,
user_input_end: &Option<impl ToString>,
now: DateTime,
) -> Result<(DateTime, DateTime)> {
from_range_ex(user_input_start, user_input_end, true, now)
}
fn from_range_ex(
user_input_start: &impl ToString,
user_input_end: &Option<impl ToString>,
single_number_is_month: bool,
now: DateTime,
) -> Result<(DateTime, DateTime)> {
let start = match interpret(&user_input_start.to_string())? {
Interpretation::Now => now,
Interpretation::DateTime(naive_date_time) => DateTime::from_naive(naive_date_time),
Interpretation::Date(naive_date) => DateTime::from_naive_date(naive_date),
Interpretation::DayMonth(day, month) => {
if user_input_end.is_none() {
return Ok((
DateTime::from_ymd(now.year(), month, day)?,
DateTime::from_ymd(now.year(), month, day + 1)?,
));
} else {
DateTime::from_ymd(now.year(), month, day)?
}
}
Interpretation::MonthYear(month, year) => {
if user_input_end.is_none() {
return Ok((
DateTime::from_ymd(year, month, 1)?,
DateTime::from_ymd(year, month + 1, 1)?,
));
} else {
DateTime::from_ymd(year, month, 1)?
}
}
Interpretation::Year(year) => {
if user_input_end.is_none() {
return Ok((
DateTime::from_ymd(year, 1, 1)?,
DateTime::from_ymd(year + 1, 1, 1)?,
));
} else {
DateTime::from_ymd(year, 1, 1)?
}
}
Interpretation::Day(day) => {
if single_number_is_month {
return Err(Error::InvalidRange(
user_input_start.to_string(),
user_input_end
.as_ref()
.map(|s| s.to_string())
.unwrap_or_default(),
));
} else {
DateTime::from_ymd(now.year(), 1, day)?
}
}
Interpretation::MonthOrDay(month_or_day) => {
if single_number_is_month {
DateTime::from_ymd(now.year(), month_or_day, 1)?
} else {
DateTime::from_ymd(now.year(), 1, month_or_day)?
}
}
Interpretation::Time(_) | Interpretation::Relative(_) => {
return Err(Error::InvalidRange(
user_input_start.to_string(),
user_input_end
.as_ref()
.map(|s| s.to_string())
.unwrap_or_default(),
));
}
};
let end = if let Some(end) = user_input_end {
match interpret(&end.to_string())? {
Interpretation::Now => now,
Interpretation::DateTime(naive_date_time) => DateTime::from_naive(naive_date_time),
Interpretation::Date(naive_date) => DateTime::from_naive_date(naive_date),
Interpretation::Year(year) => DateTime::from_ymd(year + 1, 1, 1)?,
Interpretation::DayMonth(day, month) => DateTime::from_ymd(start.year(), month, day)?,
Interpretation::MonthYear(month, year) => DateTime::from_ymd(year, month, 1)?,
Interpretation::Day(day) => {
if single_number_is_month {
return Err(Error::InvalidRange(
user_input_start.to_string(),
user_input_end
.as_ref()
.map(|s| s.to_string())
.unwrap_or_default(),
));
} else {
DateTime::from_ymd(now.year(), 1, day + 1)?
}
}
Interpretation::MonthOrDay(month_or_day) => {
if single_number_is_month {
DateTime::from_ymd(now.year(), month_or_day + 1, 1)?
} else {
DateTime::from_ymd(now.year(), 1, month_or_day + 1)?
}
}
Interpretation::Time(_) | Interpretation::Relative(_) => {
return Err(Error::InvalidRange(
user_input_start.to_string(),
user_input_end
.as_ref()
.map(|s| s.to_string())
.unwrap_or_default(),
));
}
}
} else {
start.end_of_day()
};
Ok((start, end))
}
#[derive(Eq, PartialEq, Debug)]
enum Interpretation {
Now,
DateTime(NaiveDateTime),
Time(NaiveTime),
Date(NaiveDate),
Year(i32),
Day(u32),
MonthOrDay(u32),
DayMonth(u32, u32),
MonthYear(u32, i32),
Relative(Duration),
}
fn interpret(user_input: &str) -> Result<Interpretation> {
if user_input.to_lowercase() == "now" {
return Ok(Interpretation::Now);
}
if let Ok(time) = chrono::NaiveTime::parse_from_str(user_input, "%H:%M") {
return Ok(Interpretation::Time(time));
}
if let Ok(date_time) = chrono::NaiveDateTime::parse_from_str(user_input, "%Y-%m-%d %H:%M") {
return Ok(Interpretation::DateTime(date_time));
}
if let Ok(date) = chrono::NaiveDate::parse_from_str(user_input, "%Y-%m-%d") {
return Ok(Interpretation::Date(date));
}
let factor: i64 = if user_input.starts_with('+') { 1 } else { -1 };
if user_input.ends_with('m')
&& let Ok(m) = user_input[0..user_input.len() - 1].parse::<u32>()
{
return Ok(Interpretation::Relative(chrono::TimeDelta::minutes(
factor * m as i64,
)));
} else if user_input.ends_with('h')
&& let Ok(m) = user_input[0..user_input.len() - 1].parse::<u32>()
{
return Ok(Interpretation::Relative(chrono::TimeDelta::hours(
factor * m as i64,
)));
}
let split: Vec<_> = user_input.split('/').collect();
match split.len() {
1 => {
let input: u32 = split[0].parse()?;
match input {
1..13 => {
return Ok(Interpretation::MonthOrDay(input));
}
13..32 => {
return Ok(Interpretation::Day(input));
}
1000..10000 => {
return Ok(Interpretation::Year(input as i32));
}
_ => (),
}
}
2 => {
let left: u32 = split[0].parse()?;
let right: u32 = split[1].parse()?;
match right {
1..32 => {
return Ok(Interpretation::DayMonth(right, left));
}
1000..10000 => {
return Ok(Interpretation::MonthYear(left, right as i32));
}
_ => (),
}
}
_ => (),
}
Err(Error::CannotConvertDateTime(user_input.to_string()))
}
#[cfg(test)]
mod test {
use super::*;
fn time(time: &str) -> Interpretation {
Interpretation::Time(chrono::NaiveTime::parse_from_str(time, "%H:%M").unwrap())
}
#[test]
fn interpret_test() {
assert_eq!(interpret("now").unwrap(), Interpretation::Now);
assert_eq!(interpret("1:00").unwrap(), time("1:00"));
assert_eq!(interpret("12:00").unwrap(), time("12:00"));
assert!(interpret("25:00").is_err(),);
interpret("5m").unwrap();
interpret("2h").unwrap();
interpret("+5m").unwrap();
interpret("+2h").unwrap();
}
#[test]
fn from_range_test() {
let now = DateTime::from_ymd(2026, 6, 1).unwrap();
assert_eq!(
(
chrono::NaiveDateTime::parse_from_str("2026-01-01 00:00", "%Y-%m-%d %H:%M")
.unwrap()
.and_local_timezone(chrono::Local)
.unwrap()
.into(),
chrono::NaiveDateTime::parse_from_str("2027-01-01 00:00", "%Y-%m-%d %H:%M")
.unwrap()
.and_local_timezone(chrono::Local)
.unwrap()
.into(),
),
from_range_months(&"2026", &Option::<&str>::None, now)
.map(|(l, r)| (l.as_chrono_utc(), r.as_chrono_utc()))
.unwrap()
);
}
}