Skip to content

Instantly share code, notes, and snippets.

@joshuaclayton
Last active August 28, 2020 01:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joshuaclayton/37461af290e25dcde288128d425bf4b2 to your computer and use it in GitHub Desktop.
Save joshuaclayton/37461af290e25dcde288128d425bf4b2 to your computer and use it in GitHub Desktop.
Grouping values by date
use chrono::prelude::*;
// weeks
pub fn beginning_of_week(date: &NaiveDate) -> Option<NaiveDate> {
if date.weekday() == Weekday::Sun {
Some(date.clone())
} else {
NaiveDate::from_isoywd_opt(date.iso_week().year(), date.iso_week().week(), Weekday::Sun)
.map(|d| d - chrono::Duration::weeks(1))
}
}
pub fn end_of_week(date: &NaiveDate) -> Option<NaiveDate> {
beginning_of_week(date).map(|d| d + chrono::Duration::days(6))
}
pub fn next_week(date: &NaiveDate) -> Option<NaiveDate> {
beginning_of_week(date).map(|d| d + chrono::Duration::weeks(1))
}
pub fn previous_week(date: &NaiveDate) -> Option<NaiveDate> {
beginning_of_week(date).map(|d| d - chrono::Duration::weeks(1))
}
pub fn beginning_of_month(date: &NaiveDate) -> Option<NaiveDate> {
date.with_day(1)
}
pub fn end_of_month(date: &NaiveDate) -> Option<NaiveDate> {
next_month(date).map(|d| d - chrono::Duration::days(1))
}
pub fn next_month(date: &NaiveDate) -> Option<NaiveDate> {
if date.month() == 12 {
next_year(date)
} else {
beginning_of_month(date)?.with_month(date.month() + 1)
}
}
pub fn previous_month(date: &NaiveDate) -> Option<NaiveDate> {
if date.month() == 1 {
beginning_of_month(date)?
.with_month(12)?
.with_year(date.year() - 1)
} else {
beginning_of_month(date)?.with_month(date.month() - 1)
}
}
pub fn beginning_of_quarter(date: &NaiveDate) -> Option<NaiveDate> {
beginning_of_month(date)?.with_month(quarter_month(date))
}
pub fn end_of_quarter(date: &NaiveDate) -> Option<NaiveDate> {
next_quarter(date).map(|d| d - chrono::Duration::days(1))
}
pub fn next_quarter(date: &NaiveDate) -> Option<NaiveDate> {
if date.month() >= 10 {
beginning_of_year(date)?.with_year(date.year() + 1)
} else {
beginning_of_month(date)?.with_month(quarter_month(date) + 3)
}
}
pub fn previous_quarter(date: &NaiveDate) -> Option<NaiveDate> {
if date.month() < 4 {
beginning_of_month(date)?
.with_year(date.year() - 1)?
.with_month(10)
} else {
beginning_of_month(date)?.with_month(quarter_month(date) - 3)
}
}
fn quarter_month(date: &NaiveDate) -> u32 {
1 + 3 * ((date.month() - 1) / 3)
}
pub fn beginning_of_year(date: &NaiveDate) -> Option<NaiveDate> {
beginning_of_month(date)?.with_month(1)
}
pub fn end_of_year(date: &NaiveDate) -> Option<NaiveDate> {
date.with_month(12)?.with_day(31)
}
pub fn next_year(date: &NaiveDate) -> Option<NaiveDate> {
beginning_of_year(date)?.with_year(date.year() + 1)
}
pub fn previous_year(date: &NaiveDate) -> Option<NaiveDate> {
beginning_of_year(date)?.with_year(date.year() - 1)
}
#[cfg(test)]
mod tests {
use super::*;
use num::clamp;
use quickcheck::{Arbitrary, Gen};
use quickcheck_macros::quickcheck;
#[derive(Clone, Debug)]
struct NaiveDateWrapper(NaiveDate);
#[quickcheck]
fn beginning_of_week_works(d: NaiveDateWrapper) -> bool {
let since = d.0.signed_duration_since(beginning_of_week(&d.0).unwrap());
beginning_of_week(&d.0).unwrap().weekday() == Weekday::Sun
&& since.num_days() >= 0
&& since.num_days() < 7
}
#[quickcheck]
fn end_of_week_works(d: NaiveDateWrapper) -> bool {
end_of_week(&d.0).unwrap().weekday() == Weekday::Sat
}
#[quickcheck]
fn next_week_works(d: NaiveDateWrapper) -> bool {
let since = next_week(&d.0).unwrap().signed_duration_since(d.0);
next_week(&d.0).unwrap().weekday() == Weekday::Sun
&& since.num_days() > 0
&& since.num_days() <= 7
}
#[quickcheck]
fn previous_week_works(d: NaiveDateWrapper) -> bool {
let since = previous_week(&d.0).unwrap().signed_duration_since(d.0);
previous_week(&d.0).unwrap().weekday() == Weekday::Sun
&& since.num_days() <= -7
&& since.num_days() > -14
}
#[quickcheck]
fn beginning_of_month_works(d: NaiveDateWrapper) -> bool {
beginning_of_month(&d.0).unwrap().day() == 1
&& beginning_of_month(&d.0).unwrap().month() == d.0.month()
&& beginning_of_month(&d.0).unwrap().year() == d.0.year()
}
#[quickcheck]
fn end_of_month_works(d: NaiveDateWrapper) -> bool {
end_of_month(&d.0).unwrap().month() == d.0.month()
&& end_of_month(&d.0).unwrap().year() == d.0.year()
&& (end_of_month(&d.0).unwrap() + chrono::Duration::days(1))
== next_month(&d.0).unwrap()
}
#[quickcheck]
fn beginning_of_year_works(d: NaiveDateWrapper) -> bool {
beginning_of_year(&d.0).unwrap().month() == 1
&& beginning_of_year(&d.0).unwrap().day() == 1
&& beginning_of_year(&d.0).unwrap().year() == d.0.year()
}
#[quickcheck]
fn end_of_year_works(d: NaiveDateWrapper) -> bool {
end_of_year(&d.0).unwrap().month() == 12
&& end_of_year(&d.0).unwrap().day() == 31
&& end_of_year(&d.0).unwrap().year() == d.0.year()
}
#[quickcheck]
fn next_year_works(d: NaiveDateWrapper) -> bool {
next_year(&d.0).unwrap().month() == 1
&& next_year(&d.0).unwrap().day() == 1
&& next_year(&d.0).unwrap().year() == d.0.year() + 1
}
#[quickcheck]
fn previous_year_works(d: NaiveDateWrapper) -> bool {
previous_year(&d.0).unwrap().month() == 1
&& previous_year(&d.0).unwrap().day() == 1
&& previous_year(&d.0).unwrap().year() == d.0.year() - 1
}
#[quickcheck]
fn beginning_of_quarter_works(d: NaiveDateWrapper) -> bool {
[1, 4, 7, 10].contains(&beginning_of_quarter(&d.0).unwrap().month())
&& beginning_of_quarter(&d.0).unwrap().day() == 1
&& beginning_of_quarter(&d.0).unwrap().year() == d.0.year()
}
#[quickcheck]
fn end_of_quarter_works(d: NaiveDateWrapper) -> bool {
[3, 6, 9, 12].contains(&end_of_quarter(&d.0).unwrap().month())
&& end_of_quarter(&d.0)
.map(|x| x + chrono::Duration::days(1))
.unwrap()
== next_quarter(&d.0).unwrap()
&& end_of_quarter(&d.0).unwrap().year() == d.0.year()
}
#[quickcheck]
fn next_quarter_works(d: NaiveDateWrapper) -> bool {
let current_month = d.0.month();
let year = if current_month >= 10 {
d.0.year() + 1
} else {
d.0.year()
};
[1, 4, 7, 10].contains(&next_quarter(&d.0).unwrap().month())
&& next_quarter(&d.0).unwrap().day() == 1
&& next_quarter(&d.0).unwrap().year() == year
}
#[quickcheck]
fn previous_quarter_works(d: NaiveDateWrapper) -> bool {
let current_month = d.0.month();
let year = if current_month <= 3 {
d.0.year() - 1
} else {
d.0.year()
};
[1, 4, 7, 10].contains(&previous_quarter(&d.0).unwrap().month())
&& previous_quarter(&d.0).unwrap().day() == 1
&& previous_quarter(&d.0).unwrap().year() == year
}
impl Arbitrary for NaiveDateWrapper {
fn arbitrary<G: Gen>(g: &mut G) -> NaiveDateWrapper {
let year = clamp(i32::arbitrary(g), 1584, 2800);
let month = 1 + u32::arbitrary(g) % 12;
let day = 1 + u32::arbitrary(g) % 31;
let first_date = NaiveDate::from_ymd_opt(year, month, day);
if day > 27 {
let result = vec![
first_date,
NaiveDate::from_ymd_opt(year, month, day - 1),
NaiveDate::from_ymd_opt(year, month, day - 2),
]
.into_iter()
.filter_map(|v| v)
.nth(0)
.unwrap();
NaiveDateWrapper(result)
} else {
NaiveDateWrapper(first_date.unwrap())
}
}
}
}
use crate::date_calculations;
use chrono::prelude::*;
use std::collections::HashMap;
use std::marker::PhantomData;
pub type GroupedByWeek<T> = GroupedByDate<T, Week>;
pub type GroupedByMonth<T> = GroupedByDate<T, Month>;
pub type GroupedByQuarter<T> = GroupedByDate<T, Quarter>;
pub type GroupedByYear<T> = GroupedByDate<T, Year>;
pub trait Dated {
fn occurred_on(&self) -> NaiveDate;
}
pub trait Period {
fn beginning(date: &NaiveDate) -> Option<NaiveDate>;
fn advance(date: &NaiveDate) -> Option<NaiveDate>;
}
pub struct Week;
pub struct Month;
pub struct Quarter;
pub struct Year;
impl Period for Week {
fn beginning(date: &NaiveDate) -> Option<NaiveDate> {
date_calculations::beginning_of_week(date)
}
fn advance(date: &NaiveDate) -> Option<NaiveDate> {
date_calculations::next_week(date)
}
}
impl Period for Month {
fn beginning(date: &NaiveDate) -> Option<NaiveDate> {
date_calculations::beginning_of_month(date)
}
fn advance(date: &NaiveDate) -> Option<NaiveDate> {
date_calculations::next_month(date)
}
}
impl Period for Quarter {
fn beginning(date: &NaiveDate) -> Option<NaiveDate> {
date_calculations::beginning_of_quarter(date)
}
fn advance(date: &NaiveDate) -> Option<NaiveDate> {
date_calculations::next_quarter(date)
}
}
impl Period for Year {
fn beginning(date: &NaiveDate) -> Option<NaiveDate> {
date_calculations::beginning_of_year(date)
}
fn advance(date: &NaiveDate) -> Option<NaiveDate> {
date_calculations::next_year(date)
}
}
pub struct GroupedByDate<T: Dated, P: Period> {
records: HashMap<NaiveDate, Vec<T>>,
lock: PhantomData<P>,
}
impl<T: Dated, P: Period> IntoIterator for GroupedByDate<T, P> {
type Item = (NaiveDate, Vec<T>);
type IntoIter = std::vec::IntoIter<(NaiveDate, Vec<T>)>;
fn into_iter(self) -> Self::IntoIter {
let mut records: Vec<_> = self.records.into_iter().collect();
records.sort_by(|a, b| b.0.cmp(&a.0));
records.into_iter()
}
}
impl<T: Dated + Clone, P: Period> GroupedByDate<T, P> {
pub fn new(mut records: Vec<T>) -> Self {
let mut result = HashMap::default();
let today = Local::now().naive_local().date();
if let Some(earliest) = records.iter().map(|v| v.occurred_on()).min() {
let mut current_date = P::beginning(&earliest).unwrap();
while current_date <= today {
let next_date = P::advance(&current_date).unwrap();
result.insert(
current_date,
records
.drain_filter(|r| {
r.occurred_on() >= current_date && r.occurred_on() < next_date
})
.collect(),
);
current_date = next_date;
}
}
GroupedByDate {
records: result,
lock: PhantomData,
}
}
pub fn records(&self) -> Vec<T> {
let mut records = self
.records
.iter()
.fold(vec![], |mut acc, grouped_records| {
acc.extend(grouped_records.1.to_vec());
acc
});
records.sort_by(|a, b| b.occurred_on().cmp(&a.occurred_on()));
records
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment