Last active
August 28, 2020 01:20
-
-
Save joshuaclayton/37461af290e25dcde288128d425bf4b2 to your computer and use it in GitHub Desktop.
Grouping values by date
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(¤t_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