blob: f6b57179016af5fb3df2e53fed19ddd89e609fcb [file] [log] [blame]
/// The datetime coordinates
use chrono::{Date, DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, TimeZone, Timelike};
use std::ops::{Add, Range, Sub};
use crate::coord::ranged1d::{
AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged,
ValueFormatter,
};
/// The trait that describe some time value. This is the uniformed abstraction that works
/// for both Date, DateTime and Duration, etc.
pub trait TimeValue: Eq {
type DateType: Datelike + PartialOrd;
/// Returns the date that is no later than the time
fn date_floor(&self) -> Self::DateType;
/// Returns the date that is no earlier than the time
fn date_ceil(&self) -> Self::DateType;
/// Returns the maximum value that is earlier than the given date
fn earliest_after_date(date: Self::DateType) -> Self;
/// Returns the duration between two time value
fn subtract(&self, other: &Self) -> Duration;
/// Instantiate a date type for current time value;
fn ymd(&self, year: i32, month: u32, date: u32) -> Self::DateType;
/// Cast current date type into this type
fn from_date(date: Self::DateType) -> Self;
/// Map the coord spec
fn map_coord(value: &Self, begin: &Self, end: &Self, limit: (i32, i32)) -> i32 {
let total_span = end.subtract(begin);
let value_span = value.subtract(begin);
// First, lets try the nanoseconds precision
if let Some(total_ns) = total_span.num_nanoseconds() {
if let Some(value_ns) = value_span.num_nanoseconds() {
return (f64::from(limit.1 - limit.0) * value_ns as f64 / total_ns as f64) as i32
+ limit.0;
}
}
// Yes, converting them to floating point may lose precision, but this is Ok.
// If it overflows, it means we have a time span nearly 300 years, we are safe to ignore the
// portion less than 1 day.
let total_days = total_span.num_days() as f64;
let value_days = value_span.num_days() as f64;
(f64::from(limit.1 - limit.0) * value_days / total_days) as i32 + limit.0
}
}
impl TimeValue for NaiveDate {
type DateType = NaiveDate;
fn date_floor(&self) -> NaiveDate {
*self
}
fn date_ceil(&self) -> NaiveDate {
*self
}
fn earliest_after_date(date: NaiveDate) -> Self {
date
}
fn subtract(&self, other: &NaiveDate) -> Duration {
*self - *other
}
fn ymd(&self, year: i32, month: u32, date: u32) -> Self::DateType {
NaiveDate::from_ymd(year, month, date)
}
fn from_date(date: Self::DateType) -> Self {
date
}
}
impl<Z: TimeZone> TimeValue for Date<Z> {
type DateType = Date<Z>;
fn date_floor(&self) -> Date<Z> {
self.clone()
}
fn date_ceil(&self) -> Date<Z> {
self.clone()
}
fn earliest_after_date(date: Date<Z>) -> Self {
date
}
fn subtract(&self, other: &Date<Z>) -> Duration {
self.clone() - other.clone()
}
fn ymd(&self, year: i32, month: u32, date: u32) -> Self::DateType {
self.timezone().ymd(year, month, date)
}
fn from_date(date: Self::DateType) -> Self {
date
}
}
impl<Z: TimeZone> TimeValue for DateTime<Z> {
type DateType = Date<Z>;
fn date_floor(&self) -> Date<Z> {
self.date()
}
fn date_ceil(&self) -> Date<Z> {
if self.time().num_seconds_from_midnight() > 0 {
self.date() + Duration::days(1)
} else {
self.date()
}
}
fn earliest_after_date(date: Date<Z>) -> DateTime<Z> {
date.and_hms(0, 0, 0)
}
fn subtract(&self, other: &DateTime<Z>) -> Duration {
self.clone() - other.clone()
}
fn ymd(&self, year: i32, month: u32, date: u32) -> Self::DateType {
self.timezone().ymd(year, month, date)
}
fn from_date(date: Self::DateType) -> Self {
date.and_hms(0, 0, 0)
}
}
impl TimeValue for NaiveDateTime {
type DateType = NaiveDate;
fn date_floor(&self) -> NaiveDate {
self.date()
}
fn date_ceil(&self) -> NaiveDate {
if self.time().num_seconds_from_midnight() > 0 {
self.date() + Duration::days(1)
} else {
self.date()
}
}
fn earliest_after_date(date: NaiveDate) -> NaiveDateTime {
date.and_hms(0, 0, 0)
}
fn subtract(&self, other: &NaiveDateTime) -> Duration {
*self - *other
}
fn ymd(&self, year: i32, month: u32, date: u32) -> Self::DateType {
NaiveDate::from_ymd(year, month, date)
}
fn from_date(date: Self::DateType) -> Self {
date.and_hms(0, 0, 0)
}
}
/// The ranged coordinate for date
#[derive(Clone)]
pub struct RangedDate<D: Datelike>(D, D);
impl<D: Datelike> From<Range<D>> for RangedDate<D> {
fn from(range: Range<D>) -> Self {
Self(range.start, range.end)
}
}
impl<D> Ranged for RangedDate<D>
where
D: Datelike + TimeValue + Sub<D, Output = Duration> + Add<Duration, Output = D> + Clone,
{
type FormatOption = DefaultFormatting;
type ValueType = D;
fn range(&self) -> Range<D> {
self.0.clone()..self.1.clone()
}
fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
TimeValue::map_coord(value, &self.0, &self.1, limit)
}
fn key_points<HintType: KeyPointHint>(&self, hint: HintType) -> Vec<Self::ValueType> {
let max_points = hint.max_num_points();
let mut ret = vec![];
let total_days = (self.1.clone() - self.0.clone()).num_days();
let total_weeks = (self.1.clone() - self.0.clone()).num_weeks();
if total_days > 0 && total_days as usize <= max_points {
for day_idx in 0..=total_days {
ret.push(self.0.clone() + Duration::days(day_idx));
}
return ret;
}
if total_weeks > 0 && total_weeks as usize <= max_points {
for day_idx in 0..=total_weeks {
ret.push(self.0.clone() + Duration::weeks(day_idx));
}
return ret;
}
let week_per_point = ((total_weeks as f64) / (max_points as f64)).ceil() as usize;
for idx in 0..=(total_weeks as usize / week_per_point) {
ret.push(self.0.clone() + Duration::weeks((idx * week_per_point) as i64));
}
ret
}
}
impl<D> DiscreteRanged for RangedDate<D>
where
D: Datelike + TimeValue + Sub<D, Output = Duration> + Add<Duration, Output = D> + Clone,
{
fn size(&self) -> usize {
((self.1.clone() - self.0.clone()).num_days().max(-1) + 1) as usize
}
fn index_of(&self, value: &D) -> Option<usize> {
let ret = (value.clone() - self.0.clone()).num_days();
if ret < 0 {
return None;
}
Some(ret as usize)
}
fn from_index(&self, index: usize) -> Option<D> {
Some(self.0.clone() + Duration::days(index as i64))
}
}
impl<Z: TimeZone> AsRangedCoord for Range<Date<Z>> {
type CoordDescType = RangedDate<Date<Z>>;
type Value = Date<Z>;
}
impl AsRangedCoord for Range<NaiveDate> {
type CoordDescType = RangedDate<NaiveDate>;
type Value = NaiveDate;
}
/// Indicates the coord has a monthly resolution
///
/// Note: since month doesn't have a constant duration.
/// We can't use a simple granularity to describe it. Thus we have
/// this axis decorator to make it yield monthly key-points.
#[derive(Clone)]
pub struct Monthly<T: TimeValue>(Range<T>);
impl<T: TimeValue + Datelike + Clone> ValueFormatter<T> for Monthly<T> {
fn format(value: &T) -> String {
format!("{}-{}", value.year(), value.month())
}
}
impl<T: TimeValue + Clone> Monthly<T> {
fn bold_key_points<H: KeyPointHint>(&self, hint: &H) -> Vec<T> {
let max_points = hint.max_num_points();
let start_date = self.0.start.date_ceil();
let end_date = self.0.end.date_floor();
let mut start_year = start_date.year();
let mut start_month = start_date.month();
let start_day = start_date.day();
let end_year = end_date.year();
let end_month = end_date.month();
if start_day != 1 {
start_month += 1;
if start_month == 13 {
start_month = 1;
start_year += 1;
}
}
let total_month = (end_year - start_year) * 12 + end_month as i32 - start_month as i32;
fn generate_key_points<T: TimeValue>(
mut start_year: i32,
mut start_month: i32,
end_year: i32,
end_month: i32,
step: u32,
builder: &T,
) -> Vec<T> {
let mut ret = vec![];
while end_year > start_year || (end_year == start_year && end_month >= start_month) {
ret.push(T::earliest_after_date(builder.ymd(
start_year,
start_month as u32,
1,
)));
start_month += step as i32;
if start_month >= 13 {
start_year += start_month / 12;
start_month %= 12;
}
}
ret
}
if total_month as usize <= max_points {
// Monthly
return generate_key_points(
start_year,
start_month as i32,
end_year,
end_month as i32,
1,
&self.0.start,
);
} else if total_month as usize <= max_points * 3 {
// Quarterly
return generate_key_points(
start_year,
start_month as i32,
end_year,
end_month as i32,
3,
&self.0.start,
);
} else if total_month as usize <= max_points * 6 {
// Biyearly
return generate_key_points(
start_year,
start_month as i32,
end_year,
end_month as i32,
6,
&self.0.start,
);
}
// Otherwise we could generate the yearly keypoints
generate_yearly_keypoints(
max_points,
start_year,
start_month,
end_year,
end_month,
&self.0.start,
)
}
}
impl<T: TimeValue + Clone> Ranged for Monthly<T>
where
Range<T>: AsRangedCoord<Value = T>,
{
type FormatOption = NoDefaultFormatting;
type ValueType = T;
fn range(&self) -> Range<T> {
self.0.start.clone()..self.0.end.clone()
}
fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
T::map_coord(value, &self.0.start, &self.0.end, limit)
}
fn key_points<HintType: KeyPointHint>(&self, hint: HintType) -> Vec<Self::ValueType> {
if hint.weight().allow_light_points() && self.size() <= hint.bold_points() * 2 {
let coord: <Range<T> as AsRangedCoord>::CoordDescType = self.0.clone().into();
let normal = coord.key_points(hint.max_num_points());
return normal;
}
self.bold_key_points(&hint)
}
}
impl<T: TimeValue + Clone> DiscreteRanged for Monthly<T>
where
Range<T>: AsRangedCoord<Value = T>,
{
fn size(&self) -> usize {
let (start_year, start_month) = {
let ceil = self.0.start.date_ceil();
(ceil.year(), ceil.month())
};
let (end_year, end_month) = {
let floor = self.0.end.date_floor();
(floor.year(), floor.month())
};
((end_year - start_year).max(0) * 12
+ (1 - start_month as i32)
+ (end_month as i32 - 1)
+ 1)
.max(0) as usize
}
fn index_of(&self, value: &T) -> Option<usize> {
let this_year = value.date_floor().year();
let this_month = value.date_floor().month();
let start_year = self.0.start.date_ceil().year();
let start_month = self.0.start.date_ceil().month();
let ret = (this_year - start_year).max(0) * 12
+ (1 - start_month as i32)
+ (this_month as i32 - 1);
if ret >= 0 {
return Some(ret as usize);
}
None
}
fn from_index(&self, index: usize) -> Option<T> {
if index == 0 {
return Some(T::earliest_after_date(self.0.start.date_ceil()));
}
let index_from_start_year = index + (self.0.start.date_ceil().month() - 1) as usize;
let year = self.0.start.date_ceil().year() + index_from_start_year as i32 / 12;
let month = index_from_start_year % 12;
Some(T::earliest_after_date(self.0.start.ymd(
year,
month as u32 + 1,
1,
)))
}
}
/// Indicate the coord has a yearly granularity.
#[derive(Clone)]
pub struct Yearly<T: TimeValue>(Range<T>);
fn generate_yearly_keypoints<T: TimeValue>(
max_points: usize,
mut start_year: i32,
start_month: u32,
mut end_year: i32,
end_month: u32,
builder: &T,
) -> Vec<T> {
if start_month > end_month {
end_year -= 1;
}
let mut exp10 = 1;
while (end_year - start_year + 1) as usize / (exp10 * 10) > max_points {
exp10 *= 10;
}
let mut freq = exp10;
for try_freq in &[1, 2, 5, 10] {
freq = *try_freq * exp10;
if (end_year - start_year + 1) as usize / (exp10 * *try_freq) <= max_points {
break;
}
}
let mut ret = vec![];
while start_year <= end_year {
ret.push(T::earliest_after_date(builder.ymd(
start_year,
start_month,
1,
)));
start_year += freq as i32;
}
ret
}
impl<T: TimeValue + Datelike + Clone> ValueFormatter<T> for Yearly<T> {
fn format(value: &T) -> String {
format!("{}-{}", value.year(), value.month())
}
}
impl<T: TimeValue + Clone> Ranged for Yearly<T>
where
Range<T>: AsRangedCoord<Value = T>,
{
type FormatOption = NoDefaultFormatting;
type ValueType = T;
fn range(&self) -> Range<T> {
self.0.start.clone()..self.0.end.clone()
}
fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
T::map_coord(value, &self.0.start, &self.0.end, limit)
}
fn key_points<HintType: KeyPointHint>(&self, hint: HintType) -> Vec<Self::ValueType> {
if hint.weight().allow_light_points() && self.size() <= hint.bold_points() * 2 {
return Monthly(self.0.clone()).key_points(hint);
}
let max_points = hint.max_num_points();
let start_date = self.0.start.date_ceil();
let end_date = self.0.end.date_floor();
let mut start_year = start_date.year();
let mut start_month = start_date.month();
let start_day = start_date.day();
let end_year = end_date.year();
let end_month = end_date.month();
if start_day != 1 {
start_month += 1;
if start_month == 13 {
start_month = 1;
start_year += 1;
}
}
generate_yearly_keypoints(
max_points,
start_year,
start_month,
end_year,
end_month,
&self.0.start,
)
}
}
impl<T: TimeValue + Clone> DiscreteRanged for Yearly<T>
where
Range<T>: AsRangedCoord<Value = T>,
{
fn size(&self) -> usize {
let year_start = self.0.start.date_ceil().year();
let year_end = self.0.end.date_floor().year();
((year_end - year_start).max(-1) + 1) as usize
}
fn index_of(&self, value: &T) -> Option<usize> {
let year_start = self.0.start.date_ceil().year();
let year_value = value.date_floor().year();
let ret = year_value - year_start;
if ret < 0 {
return None;
}
Some(ret as usize)
}
fn from_index(&self, index: usize) -> Option<T> {
let year = self.0.start.date_ceil().year() + index as i32;
let ret = T::earliest_after_date(self.0.start.ymd(year, 1, 1));
if ret.date_ceil() <= self.0.start.date_floor() {
return Some(self.0.start.clone());
}
Some(ret)
}
}
/// The trait that converts a normal date coord into a yearly one
pub trait IntoMonthly<T: TimeValue> {
fn monthly(self) -> Monthly<T>;
}
/// The trait that converts a normal date coord into a yearly one
pub trait IntoYearly<T: TimeValue> {
fn yearly(self) -> Yearly<T>;
}
impl<T: TimeValue> IntoMonthly<T> for Range<T> {
fn monthly(self) -> Monthly<T> {
Monthly(self)
}
}
impl<T: TimeValue> IntoYearly<T> for Range<T> {
fn yearly(self) -> Yearly<T> {
Yearly(self)
}
}
/// The ranged coordinate for the date and time
#[derive(Clone)]
pub struct RangedDateTime<DT: Datelike + Timelike + TimeValue>(DT, DT);
impl<Z: TimeZone> AsRangedCoord for Range<DateTime<Z>> {
type CoordDescType = RangedDateTime<DateTime<Z>>;
type Value = DateTime<Z>;
}
impl<Z: TimeZone> From<Range<DateTime<Z>>> for RangedDateTime<DateTime<Z>> {
fn from(range: Range<DateTime<Z>>) -> Self {
Self(range.start, range.end)
}
}
impl From<Range<NaiveDateTime>> for RangedDateTime<NaiveDateTime> {
fn from(range: Range<NaiveDateTime>) -> Self {
Self(range.start, range.end)
}
}
impl<DT> Ranged for RangedDateTime<DT>
where
DT: Datelike + Timelike + TimeValue + Clone + PartialOrd,
DT: Add<Duration, Output = DT>,
DT: Sub<DT, Output = Duration>,
RangedDate<DT::DateType>: Ranged<ValueType = DT::DateType>,
{
type FormatOption = DefaultFormatting;
type ValueType = DT;
fn range(&self) -> Range<DT> {
self.0.clone()..self.1.clone()
}
fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
TimeValue::map_coord(value, &self.0, &self.1, limit)
}
fn key_points<HintType: KeyPointHint>(&self, hint: HintType) -> Vec<Self::ValueType> {
let max_points = hint.max_num_points();
let total_span = self.1.clone() - self.0.clone();
if let Some(total_ns) = total_span.num_nanoseconds() {
if let Some(actual_ns_per_point) =
compute_period_per_point(total_ns as u64, max_points, true)
{
let start_time_ns = u64::from(self.0.num_seconds_from_midnight()) * 1_000_000_000
+ u64::from(self.0.nanosecond());
let mut start_time = DT::from_date(self.0.date_floor())
+ Duration::nanoseconds(if start_time_ns % actual_ns_per_point > 0 {
start_time_ns + (actual_ns_per_point - start_time_ns % actual_ns_per_point)
} else {
start_time_ns
} as i64);
let mut ret = vec![];
while start_time < self.1 {
ret.push(start_time.clone());
start_time = start_time + Duration::nanoseconds(actual_ns_per_point as i64);
}
return ret;
}
}
// Otherwise, it actually behaves like a date
let date_range = RangedDate(self.0.date_ceil(), self.1.date_floor());
date_range
.key_points(max_points)
.into_iter()
.map(DT::from_date)
.collect()
}
}
/// The coordinate that for duration of time
#[derive(Clone)]
pub struct RangedDuration(Duration, Duration);
impl AsRangedCoord for Range<Duration> {
type CoordDescType = RangedDuration;
type Value = Duration;
}
impl From<Range<Duration>> for RangedDuration {
fn from(range: Range<Duration>) -> Self {
Self(range.start, range.end)
}
}
impl Ranged for RangedDuration {
type FormatOption = DefaultFormatting;
type ValueType = Duration;
fn range(&self) -> Range<Duration> {
self.0..self.1
}
fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
let total_span = self.1 - self.0;
let value_span = *value - self.0;
if let Some(total_ns) = total_span.num_nanoseconds() {
if let Some(value_ns) = value_span.num_nanoseconds() {
return limit.0
+ (f64::from(limit.1 - limit.0) * value_ns as f64 / total_ns as f64 + 1e-10)
as i32;
}
return limit.1;
}
let total_days = total_span.num_days();
let value_days = value_span.num_days();
limit.0
+ (f64::from(limit.1 - limit.0) * value_days as f64 / total_days as f64 + 1e-10) as i32
}
fn key_points<HintType: KeyPointHint>(&self, hint: HintType) -> Vec<Self::ValueType> {
let max_points = hint.max_num_points();
let total_span = self.1 - self.0;
if let Some(total_ns) = total_span.num_nanoseconds() {
if let Some(period) = compute_period_per_point(total_ns as u64, max_points, false) {
let mut start_ns = self.0.num_nanoseconds().unwrap();
if start_ns as u64 % period > 0 {
if start_ns > 0 {
start_ns += period as i64 - (start_ns % period as i64);
} else {
start_ns -= start_ns % period as i64;
}
}
let mut current = Duration::nanoseconds(start_ns);
let mut ret = vec![];
while current < self.1 {
ret.push(current);
current = current + Duration::nanoseconds(period as i64);
}
return ret;
}
}
let begin_days = self.0.num_days();
let end_days = self.1.num_days();
let mut days_per_tick = 1;
let mut idx = 0;
const MULTIPLIER: &[i32] = &[1, 2, 5];
while (end_days - begin_days) / i64::from(days_per_tick * MULTIPLIER[idx])
> max_points as i64
{
idx += 1;
if idx == MULTIPLIER.len() {
idx = 0;
days_per_tick *= 10;
}
}
days_per_tick *= MULTIPLIER[idx];
let mut ret = vec![];
let mut current = Duration::days(
self.0.num_days()
+ if Duration::days(self.0.num_days()) != self.0 {
1
} else {
0
},
);
while current < self.1 {
ret.push(current);
current = current + Duration::days(i64::from(days_per_tick));
}
ret
}
}
#[allow(clippy::inconsistent_digit_grouping)]
fn compute_period_per_point(total_ns: u64, max_points: usize, sub_daily: bool) -> Option<u64> {
let min_ns_per_point = total_ns as f64 / max_points as f64;
let actual_ns_per_point: u64 = (10u64).pow((min_ns_per_point as f64).log10().floor() as u32);
fn determine_actual_ns_per_point(
total_ns: u64,
mut actual_ns_per_point: u64,
units: &[u64],
base: u64,
max_points: usize,
) -> u64 {
let mut unit_per_point_idx = 0;
while total_ns / actual_ns_per_point > max_points as u64 * units[unit_per_point_idx] {
unit_per_point_idx += 1;
if unit_per_point_idx == units.len() {
unit_per_point_idx = 0;
actual_ns_per_point *= base;
}
}
units[unit_per_point_idx] * actual_ns_per_point
}
if actual_ns_per_point < 1_000_000_000 {
Some(determine_actual_ns_per_point(
total_ns as u64,
actual_ns_per_point,
&[1, 2, 5],
10,
max_points,
))
} else if actual_ns_per_point < 3600_000_000_000 {
Some(determine_actual_ns_per_point(
total_ns as u64,
1_000_000_000,
&[1, 2, 5, 10, 15, 20, 30],
60,
max_points,
))
} else if actual_ns_per_point < 3600_000_000_000 * 24 {
Some(determine_actual_ns_per_point(
total_ns as u64,
3600_000_000_000,
&[1, 2, 4, 8, 12],
24,
max_points,
))
} else if !sub_daily {
if actual_ns_per_point < 3600_000_000_000 * 24 * 10 {
Some(determine_actual_ns_per_point(
total_ns as u64,
3600_000_000_000 * 24,
&[1, 2, 5, 7],
10,
max_points,
))
} else {
Some(determine_actual_ns_per_point(
total_ns as u64,
3600_000_000_000 * 24 * 10,
&[1, 2, 5],
10,
max_points,
))
}
} else {
None
}
}
#[cfg(test)]
mod test {
use super::*;
use chrono::{TimeZone, Utc};
#[test]
fn test_date_range_long() {
let range = Utc.ymd(1000, 1, 1)..Utc.ymd(2999, 1, 1);
let ranged_coord = Into::<RangedDate<_>>::into(range);
assert_eq!(ranged_coord.map(&Utc.ymd(1000, 8, 10), (0, 100)), 0);
assert_eq!(ranged_coord.map(&Utc.ymd(2999, 8, 10), (0, 100)), 100);
let kps = ranged_coord.key_points(23);
assert!(kps.len() <= 23);
let max = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_days())
.max()
.unwrap();
let min = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_days())
.min()
.unwrap();
assert_eq!(max, min);
assert_eq!(max % 7, 0);
}
#[test]
fn test_date_range_short() {
let range = Utc.ymd(2019, 1, 1)..Utc.ymd(2019, 1, 21);
let ranged_coord = Into::<RangedDate<_>>::into(range);
let kps = ranged_coord.key_points(4);
assert_eq!(kps.len(), 3);
let max = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_days())
.max()
.unwrap();
let min = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_days())
.min()
.unwrap();
assert_eq!(max, min);
assert_eq!(max, 7);
let kps = ranged_coord.key_points(30);
assert_eq!(kps.len(), 21);
let max = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_days())
.max()
.unwrap();
let min = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_days())
.min()
.unwrap();
assert_eq!(max, min);
assert_eq!(max, 1);
}
#[test]
fn test_yearly_date_range() {
use crate::coord::ranged1d::BoldPoints;
let range = Utc.ymd(1000, 8, 5)..Utc.ymd(2999, 1, 1);
let ranged_coord = range.yearly();
assert_eq!(ranged_coord.map(&Utc.ymd(1000, 8, 10), (0, 100)), 0);
assert_eq!(ranged_coord.map(&Utc.ymd(2999, 8, 10), (0, 100)), 100);
let kps = ranged_coord.key_points(23);
assert!(kps.len() <= 23);
let max = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_days())
.max()
.unwrap();
let min = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_days())
.min()
.unwrap();
assert!(max != min);
assert!(kps.into_iter().all(|x| x.month() == 9 && x.day() == 1));
let range = Utc.ymd(2019, 8, 5)..Utc.ymd(2020, 1, 1);
let ranged_coord = range.yearly();
let kps = ranged_coord.key_points(BoldPoints(23));
assert!(kps.len() == 1);
}
#[test]
fn test_monthly_date_range() {
let range = Utc.ymd(2019, 8, 5)..Utc.ymd(2020, 9, 1);
let ranged_coord = range.monthly();
use crate::coord::ranged1d::BoldPoints;
let kps = ranged_coord.key_points(BoldPoints(15));
assert!(kps.len() <= 15);
assert!(kps.iter().all(|x| x.day() == 1));
assert!(kps.into_iter().any(|x| x.month() != 9));
let kps = ranged_coord.key_points(BoldPoints(5));
assert!(kps.len() <= 5);
assert!(kps.iter().all(|x| x.day() == 1));
let kps: Vec<_> = kps.into_iter().map(|x| x.month()).collect();
assert_eq!(kps, vec![9, 12, 3, 6, 9]);
// TODO: Investigate why max_point = 1 breaks the contract
let kps = ranged_coord.key_points(3);
assert!(kps.len() == 3);
assert!(kps.iter().all(|x| x.day() == 1));
let kps: Vec<_> = kps.into_iter().map(|x| x.month()).collect();
assert_eq!(kps, vec![9, 3, 9]);
}
#[test]
fn test_datetime_long_range() {
let coord: RangedDateTime<_> =
(Utc.ymd(1000, 1, 1).and_hms(0, 0, 0)..Utc.ymd(3000, 1, 1).and_hms(0, 0, 0)).into();
assert_eq!(
coord.map(&Utc.ymd(1000, 1, 1).and_hms(0, 0, 0), (0, 100)),
0
);
assert_eq!(
coord.map(&Utc.ymd(3000, 1, 1).and_hms(0, 0, 0), (0, 100)),
100
);
let kps = coord.key_points(23);
assert!(kps.len() <= 23);
let max = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_seconds())
.max()
.unwrap();
let min = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_seconds())
.min()
.unwrap();
assert!(max == min);
assert!(max % (24 * 3600 * 7) == 0);
}
#[test]
fn test_datetime_medium_range() {
let coord: RangedDateTime<_> =
(Utc.ymd(2019, 1, 1).and_hms(0, 0, 0)..Utc.ymd(2019, 1, 11).and_hms(0, 0, 0)).into();
let kps = coord.key_points(23);
assert!(kps.len() <= 23);
let max = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_seconds())
.max()
.unwrap();
let min = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_seconds())
.min()
.unwrap();
assert!(max == min);
assert_eq!(max, 12 * 3600);
}
#[test]
fn test_datetime_short_range() {
let coord: RangedDateTime<_> =
(Utc.ymd(2019, 1, 1).and_hms(0, 0, 0)..Utc.ymd(2019, 1, 2).and_hms(0, 0, 0)).into();
let kps = coord.key_points(50);
assert!(kps.len() <= 50);
let max = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_seconds())
.max()
.unwrap();
let min = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_seconds())
.min()
.unwrap();
assert!(max == min);
assert_eq!(max, 1800);
}
#[test]
fn test_datetime_nano_range() {
let start = Utc.ymd(2019, 1, 1).and_hms(0, 0, 0);
let end = start.clone() + Duration::nanoseconds(100);
let coord: RangedDateTime<_> = (start..end).into();
let kps = coord.key_points(50);
assert!(kps.len() <= 50);
let max = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_nanoseconds().unwrap())
.max()
.unwrap();
let min = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_nanoseconds().unwrap())
.min()
.unwrap();
assert!(max == min);
assert_eq!(max, 2);
}
#[test]
fn test_duration_long_range() {
let coord: RangedDuration = (Duration::days(-1000000)..Duration::days(1000000)).into();
assert_eq!(coord.map(&Duration::days(-1000000), (0, 100)), 0);
assert_eq!(coord.map(&Duration::days(1000000), (0, 100)), 100);
let kps = coord.key_points(23);
assert!(kps.len() <= 23);
let max = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_seconds())
.max()
.unwrap();
let min = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_seconds())
.min()
.unwrap();
assert!(max == min);
assert!(max % (24 * 3600 * 10000) == 0);
}
#[test]
fn test_duration_daily_range() {
let coord: RangedDuration = (Duration::days(0)..Duration::hours(25)).into();
let kps = coord.key_points(23);
assert!(kps.len() <= 23);
let max = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_seconds())
.max()
.unwrap();
let min = kps
.iter()
.zip(kps.iter().skip(1))
.map(|(p, n)| (*n - *p).num_seconds())
.min()
.unwrap();
assert!(max == min);
assert_eq!(max, 3600 * 2);
}
#[test]
fn test_date_discrete() {
let coord: RangedDate<Date<_>> = (Utc.ymd(2019, 1, 1)..Utc.ymd(2019, 12, 31)).into();
assert_eq!(coord.size(), 365);
assert_eq!(coord.index_of(&Utc.ymd(2019, 2, 28)), Some(31 + 28 - 1));
assert_eq!(coord.from_index(364), Some(Utc.ymd(2019, 12, 31)));
}
#[test]
fn test_monthly_discrete() {
let coord1 = (Utc.ymd(2019, 1, 10)..Utc.ymd(2019, 12, 31)).monthly();
let coord2 = (Utc.ymd(2019, 1, 10)..Utc.ymd(2020, 1, 1)).monthly();
assert_eq!(coord1.size(), 12);
assert_eq!(coord2.size(), 13);
for i in 1..=12 {
assert_eq!(coord1.from_index(i - 1).unwrap().month(), i as u32);
assert_eq!(
coord1.index_of(&coord1.from_index(i - 1).unwrap()).unwrap(),
i - 1
);
}
}
#[test]
fn test_yearly_discrete() {
let coord1 = (Utc.ymd(2000, 1, 10)..Utc.ymd(2019, 12, 31)).yearly();
assert_eq!(coord1.size(), 20);
for i in 0..20 {
assert_eq!(coord1.from_index(i).unwrap().year(), 2000 + i as i32);
assert_eq!(coord1.index_of(&coord1.from_index(i).unwrap()).unwrap(), i);
}
}
}