use super::prelude::*;
use regex::Regex;
#[derive(Debug, PartialEq, Clone)]
pub enum Range {
None,
All,
Count(usize),
At(Vec<usize>),
PositionRange(usize, usize),
FromPosition(usize),
Day(Date),
TimeRange(DateTime, DateTime),
Since(DateTime),
}
impl Range {
pub fn parse(list: Option<String>, context: &Context) -> Result<Self, Error> {
if let Some(list) = list {
match Self::parse_count(&list).or(Self::parse_at(&list).or(Self::parse_position_range(
&list,
)
.or(
Self::parse_time_range(&list, context).or(Self::parse_day(&list, context)
.or(Self::parse_from_position(&list).or(Self::parse_since(&list, context)))),
))) {
Range::None => Err(Error::RangeFormat(list)),
range => Ok(range),
}
} else {
Ok(Range::All)
}
}
fn or(self, other: Self) -> Self {
match self {
Self::None => other,
_ => self,
}
}
fn parse_count(list: &str) -> Range {
let re = Regex::new(r"^~(\d+)$").unwrap();
if let Some(cap) = re.captures_iter(list).next() {
return Self::Count(cap[1].parse::<usize>().unwrap());
}
Self::None
}
fn parse_at(list: &str) -> Range {
let re = Regex::new(r"^([\d,]+)$").unwrap();
if let Some(cap) = re.captures_iter(list).next() {
return Self::At(
cap[1]
.split(',')
.map(|c| c.parse::<usize>().unwrap() - 1)
.collect(),
);
}
Self::None
}
fn parse_position_range(list: &str) -> Range {
let re = Regex::new(r"^(\d+)-(\d+)$").unwrap();
if let Some(cap) = re.captures_iter(list).next() {
return Self::PositionRange(
cap[1].parse::<usize>().unwrap() - 1,
cap[2].parse::<usize>().unwrap() - 1,
);
}
Self::None
}
fn parse_from_position(list: &str) -> Range {
let re = Regex::new(r"^(\d+)-$").unwrap();
if let Some(cap) = re.captures_iter(list).next() {
return Self::FromPosition(cap[1].parse::<usize>().unwrap() - 1);
}
Self::None
}
fn parse_day(list: &str, context: &Context) -> Range {
let pt = PartialDateTime::parse_opt(Some(list.to_string()));
match pt {
PartialDateTime::None => Self::None,
_ => Range::Day(Date::from(pt.into(context.time()))),
}
}
fn parse_time_range(list: &str, context: &Context) -> Range {
let list: Vec<String> = if list.contains("...") {
let list: Vec<&str> = list.split("...").collect();
vec![list[0].to_string() + ".", list[1].to_string()]
} else {
let list: Vec<&str> = list.split("..").collect();
match list.len() {
1 => vec![list[0].to_string()],
2 => vec![list[0].to_string(), list[1].to_string()],
_ => return Range::None,
}
};
if list.len() == 2 {
let from = PartialDateTime::parse_opt(Some(list[0].to_string()));
let to = PartialDateTime::parse_opt(Some(list[1].to_string()));
match (from, to) {
(PartialDateTime::None, PartialDateTime::None) => Self::None,
(from, PartialDateTime::None) => {
Self::TimeRange(from.into(context.time()), context.time())
}
(PartialDateTime::None, to) => {
use chrono::{TimeZone, Utc};
Self::TimeRange(
Utc.with_ymd_and_hms(1900, 1, 1, 0, 0, 0).unwrap().into(),
to.into(context.time()) + Duration::days(1),
)
}
(from, to) => {
let from = from.into(context.time());
Self::TimeRange(from, to.into(from) + Duration::days(1))
}
}
} else {
Self::None
}
}
fn parse_since(list: &str, context: &Context) -> Range {
let re = Regex::new(r"^(.+)\.\.$").unwrap();
if let Some(cap) = re.captures_iter(list).next() {
let pt = PartialDateTime::parse_opt(Some(cap[1].to_string()));
return match pt {
PartialDateTime::None => Self::None,
_ => Range::Since(pt.into(context.time())),
};
}
Self::None
}
}
impl std::fmt::Display for Range {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => write!(f, "none"),
Self::All => write!(f, "all job(s)"),
Self::Count(count) => write!(f, "last {count} job(s)"),
Self::At(pos) => write!(
f,
"job at position(s) {pos}",
pos = {
let v: Vec<String> = pos.iter().map(|p| p.to_string()).collect();
v.join(",")
}
),
Self::PositionRange(from, to) => write!(
f,
"job(s) from position {from} to {to}",
from = from + 1,
to = to + 1
),
Self::FromPosition(from) => write!(f, "job(s) from position {from}", from = from + 1),
Self::Day(day) => write!(f, "job(s) at {day}"),
Self::TimeRange(since, until) => write!(f, "job(s) since {since} until {until}"),
Self::Since(since) => write!(f, "job(s) since {since}"),
}
}
}
#[cfg(test)]
fn new_job(start: &str, end: Option<&str>, message: Option<&str>, tags: Option<&str>) -> Job {
Job::new(
start.into(),
end.map(|end| end.into()),
message.map(|message| message.to_string()),
tags.map(|tags| tags.into()),
)
.expect("can't parse new job")
}
#[test]
fn test_parse_time_range() {
let context = Context::new_test("2023-2-1 12:00");
let mut jobs = Jobs::new();
jobs._push(new_job(
"2022-12-31 18:00",
Some("2022-12-31 23:59"),
None,
None,
));
jobs._push(new_job(
"2023-01-01 00:00",
Some("2023-01-01 00:10"),
None,
None,
));
let january = Range::parse(Some("1.1...31.1.".into()), &context).unwrap();
assert!(jobs._filter(&january, &TagSet::new()).unwrap().len() == 1);
assert_eq!(
january,
Range::TimeRange("2023-1-1 00:00".into(), "2023-2-1 00:00".into())
);
}
#[test]
fn test_parse_fails() {
let context = Context::new_test("2023-2-1 12:00");
assert!(Range::parse(Some("1.1.-".into()), &context).is_err());
}