diff --git a/src/git/filter.rs b/src/git/filter.rs new file mode 100644 index 0000000..182845e --- /dev/null +++ b/src/git/filter.rs @@ -0,0 +1,200 @@ +use crate::git::Commit; +use anyhow::{anyhow, bail, Result}; +use chrono::{DateTime, Duration, Local, NaiveDate, ParseError, Utc}; + +#[derive(Debug, Clone, Copy)] +pub enum TimePeriod { + Days(u32), + Weeks(u32), + Months(u32), + Years(u32), + Custom(DateTime, DateTime), +} + +impl TimePeriod { + pub fn days(n: u32) -> Self { + Self::Days(n) + } + + pub fn weeks(n: u32) -> Self { + Self::Weeks(n) + } + + pub fn months(n: u32) -> Self { + Self::Months(n) + } + + pub fn years(n: u32) -> Self { + Self::Years(n) + } + + pub fn custom(start: DateTime, end: DateTime) -> Result { + if start > end { + bail!("Start date must be before end date"); + } + Ok(Self::Custom(start, end)) + } + + pub fn to_range(&self) -> (DateTime, DateTime) { + let now = Utc::now(); + match self { + TimePeriod::Days(n) => (now - Duration::days(*n as i64), now), + TimePeriod::Weeks(n) => (now - Duration::weeks(*n as i64), now), + TimePeriod::Months(n) => (now - Duration::days((*n as i64) * 30), now), + TimePeriod::Years(n) => (now - Duration::days((*n as i64) * 365), now), + TimePeriod::Custom(start, end) => (*start, *end), + } + } +} + +impl std::str::FromStr for TimePeriod { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + if s.ends_with('d') { + let n: u32 = s[..s.len() - 1].parse()?; + Ok(TimePeriod::Days(n)) + } else if s.ends_with('w') { + let n: u32 = s[..s.len() - 1].parse()?; + Ok(TimePeriod::Weeks(n)) + } else if s.ends_with('m') { + let n: u32 = s[..s.len() - 1].parse()?; + Ok(TimePeriod::Months(n)) + } else if s.ends_with('y') { + let n: u32 = s[..s.len() - 1].parse()?; + Ok(TimePeriod::Years(n)) + } else if s.contains(':') { + let parts: Vec<&str> = s.split(':').collect(); + if parts.len() == 2 { + let start = NaiveDate::parse_from_str(parts[0], "%Y-%m-%d")? + .and_hms_opt(0, 0, 0) + .unwrap(); + let end = NaiveDate::parse_from_str(parts[1], "%Y-%m-%d")? + .and_hms_opt(23, 59, 59) + .unwrap(); + return Ok(TimePeriod::Custom( + DateTime::from_utc(start, Utc), + DateTime::from_utc(end, Utc), + )); + } + Err(ParseError) + } else { + let date = NaiveDate::parse_from_str(s, "%Y-%m-%d")? + .and_hms_opt(0, 0, 0) + .unwrap(); + let start = DateTime::from_utc(date, Utc); + let end = start + Duration::days(1); + Ok(TimePeriod::Custom(start, end)) + } + } +} + +pub struct TimeFilter { + since: Option>, + until: Option>, + period: Option, +} + +impl TimeFilter { + pub fn new() -> Self { + Self { + since: None, + until: None, + period: None, + } + } + + pub fn since(mut self, date: DateTime) -> Self { + self.since = Some(date); + self + } + + pub fn until(mut self, date: DateTime) -> Self { + self.until = Some(date); + self + } + + pub fn period(mut self, period: TimePeriod) -> Self { + self.period = Some(period); + self + } + + pub fn days_back(mut self, days: u32) -> Self { + let now = Utc::now(); + self.since = Some(now - Duration::days(days as i64)); + self.until = Some(now); + self + } + + pub fn build(self) -> TimeFilter { + let (since, until) = if let Some(period) = self.period { + let (start, end) = period.to_range(); + (Some(start), Some(end)) + } else { + (self.since, self.until) + }; + TimeFilter { + since, + until, + period: None, + } + } + + pub fn contains(&self, commit: &Commit) -> bool { + let time = commit.time(); + if let Some(since) = self.since { + if time < since { + return false; + } + } + if let Some(until) = self.until { + if time > until { + return false; + } + } + true + } +} + +impl Default for TimeFilter { + fn default() -> Self { + Self::new().days_back(30) + } +} + +pub fn parse_date(s: &str) -> Result> { + let formats = [ + "%Y-%m-%d", + "%Y/%m/%d", + "%d-%m-%Y", + "%d/%m/%Y", + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%d %H:%M:%S", + ]; + + for fmt in &formats { + if let Ok(dt) = chrono::DateTime::parse_from_str(s, fmt) { + return Ok(dt.with_timezone(&Utc)); + } + } + + if s.ends_with('d') { + let n: u32 = s[..s.len() - 1].parse()?; + let now = Utc::now(); + return Ok(now - Duration::days(n as i64)); + } else if s.ends_with('w') { + let n: u32 = s[..s.len() - 1].parse()?; + let now = Utc::now(); + return Ok(now - Duration::weeks(n as i64)); + } else if s.ends_with('m') { + let n: u32 = s[..s.len() - 1].parse()?; + let now = Utc::now(); + return Ok(now - Duration::days((n as i64) * 30)); + } else if s.ends_with('y') { + let n: u32 = s[..s.len() - 1].parse()?; + let now = Utc::now(); + return Ok(now - Duration::days((n as i64) * 365)); + } + + Err(anyhow!("Cannot parse date: {}", s)) +}