You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
179 lines
6.2 KiB
179 lines
6.2 KiB
use std::fmt; |
|
|
|
use chrono::{DateTime, Duration, FixedOffset}; |
|
use yansi::Color::Blue; |
|
|
|
use crate::format::HorizontalScale; |
|
use crate::plot::date_fmt_string; |
|
|
|
#[derive(Debug)] |
|
struct TimeBucket { |
|
start: DateTime<FixedOffset>, |
|
count: usize, |
|
} |
|
|
|
impl TimeBucket { |
|
fn new(start: DateTime<FixedOffset>) -> TimeBucket { |
|
TimeBucket { start, count: 0 } |
|
} |
|
|
|
fn inc(&mut self) { |
|
self.count += 1; |
|
} |
|
} |
|
|
|
#[derive(Debug)] |
|
/// A struct holding data to plot a TimeHistogram of timestamp data. |
|
pub struct TimeHistogram { |
|
vec: Vec<TimeBucket>, |
|
min: DateTime<FixedOffset>, |
|
max: DateTime<FixedOffset>, |
|
step: Duration, |
|
top: usize, |
|
last: usize, |
|
nanos: u64, |
|
} |
|
|
|
impl TimeHistogram { |
|
/// Creates a Histogram from a vector of DateTime elements. |
|
/// |
|
/// `size` is the number of histogram buckets to display. |
|
pub fn new(size: usize, ts: &[DateTime<FixedOffset>]) -> TimeHistogram { |
|
let mut vec = Vec::<TimeBucket>::with_capacity(size); |
|
let min = *ts.iter().min().unwrap(); |
|
let max = *ts.iter().max().unwrap(); |
|
let step = max - min; |
|
let inc = step / size as i32; |
|
for i in 0..size { |
|
vec.push(TimeBucket::new(min + (inc * i as i32))); |
|
} |
|
let mut timehist = TimeHistogram { |
|
vec, |
|
min, |
|
max, |
|
step, |
|
top: 0, |
|
last: size - 1, |
|
nanos: (max - min).num_microseconds().unwrap() as u64, |
|
}; |
|
timehist.load(ts); |
|
timehist |
|
} |
|
|
|
/// Add to the `TimeHistogram` data the values of a slice of DateTime |
|
/// elements. Elements not in the initial range (the one passed to `new`) |
|
/// will be silently discarded. |
|
pub fn load(&mut self, vec: &[DateTime<FixedOffset>]) { |
|
for x in vec { |
|
self.add(*x); |
|
} |
|
} |
|
|
|
/// Add to the `TimeHistogram` another DateTime element. If element is not |
|
/// in the initial range (the one passed to `new`), it will be silently |
|
/// discarded. |
|
pub fn add(&mut self, ts: DateTime<FixedOffset>) { |
|
if let Some(slot) = self.find_slot(ts) { |
|
self.vec[slot].inc(); |
|
self.top = self.top.max(self.vec[slot].count); |
|
} |
|
} |
|
|
|
fn find_slot(&self, ts: DateTime<FixedOffset>) -> Option<usize> { |
|
if ts < self.min || ts > self.max { |
|
None |
|
} else { |
|
let x = (ts - self.min).num_microseconds().unwrap() as u64; |
|
if self.nanos == 0 { |
|
// All timestamps are the same. We will have a degenrate plot |
|
// (as opposed to failing hard). |
|
Some(0) |
|
} else { |
|
Some(((x * self.vec.len() as u64 / self.nanos) as usize).min(self.last)) |
|
} |
|
} |
|
} |
|
} |
|
|
|
impl fmt::Display for TimeHistogram { |
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
|
let width = f.width().unwrap_or(100); |
|
let horizontal_scale = HorizontalScale::new(self.top / width); |
|
let width_count = format!("{}", self.top).len(); |
|
writeln!( |
|
f, |
|
"Matches: {}.", |
|
Blue.paint(format!( |
|
"{}", |
|
self.vec.iter().map(|r| r.count).sum::<usize>() |
|
)), |
|
)?; |
|
writeln!(f, "{}", horizontal_scale)?; |
|
let ts_fmt = date_fmt_string(self.step.num_seconds()); |
|
for row in self.vec.iter() { |
|
writeln!( |
|
f, |
|
"[{label}] [{count}] {bar}", |
|
label = Blue.paint(format!("{}", row.start.format(ts_fmt))), |
|
count = horizontal_scale.get_count(row.count, width_count), |
|
bar = horizontal_scale.get_bar(row.count) |
|
)?; |
|
} |
|
Ok(()) |
|
} |
|
} |
|
|
|
#[cfg(test)] |
|
mod tests { |
|
use super::*; |
|
use yansi::Paint; |
|
|
|
#[test] |
|
fn test_big_time_interval() { |
|
Paint::disable(); |
|
let mut vec = Vec::<DateTime<FixedOffset>>::new(); |
|
vec.push(DateTime::parse_from_rfc3339("2021-04-15T04:25:00+00:00").unwrap()); |
|
vec.push(DateTime::parse_from_rfc3339("2022-04-15T04:25:00+00:00").unwrap()); |
|
vec.push(DateTime::parse_from_rfc3339("2022-04-15T04:25:00+00:00").unwrap()); |
|
vec.push(DateTime::parse_from_rfc3339("2022-04-15T04:25:00+00:00").unwrap()); |
|
vec.push(DateTime::parse_from_rfc3339("2023-04-15T04:25:00+00:00").unwrap()); |
|
let th = TimeHistogram::new(3, &vec); |
|
let display = format!("{}", th); |
|
assert!(display.contains("Matches: 5")); |
|
assert!(display.contains("represents a count of 1")); |
|
assert!(display.contains("[2021-04-15 04:25:00] [1] ∎\n")); |
|
assert!(display.contains("[2021-12-14 12:25:00] [3] ∎∎∎\n")); |
|
assert!(display.contains("[2022-08-14 20:25:00] [1] ∎\n")); |
|
} |
|
|
|
#[test] |
|
fn test_small_time_interval() { |
|
Paint::disable(); |
|
let mut vec = Vec::<DateTime<FixedOffset>>::new(); |
|
vec.push(DateTime::parse_from_rfc3339("2022-04-15T04:25:00.001+00:00").unwrap()); |
|
vec.push(DateTime::parse_from_rfc3339("2022-04-15T04:25:00.002+00:00").unwrap()); |
|
vec.push(DateTime::parse_from_rfc3339("2022-04-15T04:25:00.006+00:00").unwrap()); |
|
let th = TimeHistogram::new(4, &vec); |
|
let display = format!("{}", th); |
|
assert!(display.contains("Matches: 3")); |
|
assert!(display.contains("represents a count of 1")); |
|
assert!(display.contains("[04:25:00.001000] [2] ∎∎\n")); |
|
assert!(display.contains("[04:25:00.002250] [0] \n")); |
|
assert!(display.contains("[04:25:00.003500] [0] \n")); |
|
assert!(display.contains("[04:25:00.004750] [1] ∎\n")); |
|
} |
|
|
|
#[test] |
|
fn test_single_timestamp() { |
|
Paint::disable(); |
|
let mut vec = Vec::<DateTime<FixedOffset>>::new(); |
|
vec.push(DateTime::parse_from_rfc3339("2022-04-15T04:25:00.001+00:00").unwrap()); |
|
vec.push(DateTime::parse_from_rfc3339("2022-04-15T04:25:00.001+00:00").unwrap()); |
|
let th = TimeHistogram::new(4, &vec); |
|
let display = format!("{}", th); |
|
assert!(display.contains("Matches: 2")); |
|
assert!(display.contains("represents a count of 1")); |
|
assert!(display.contains("[04:25:00.001000] [2] ∎∎\n")); |
|
assert!(display.contains("[04:25:00.001000] [0] \n")); |
|
} |
|
}
|
|
|