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, count: usize, } impl TimeBucket { fn new(start: DateTime) -> Self { Self { 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, min: DateTime, max: DateTime, 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]) -> Self { let mut vec = Vec::::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 = Self { 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]) { 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) { 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) -> Option { 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::() )), )?; writeln!(f, "{horizontal_scale}")?; let ts_fmt = date_fmt_string(self.step.num_seconds()); for row in &self.vec { 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 vec = vec![ DateTime::parse_from_rfc3339("2021-04-15T04:25:00+00:00").unwrap(), DateTime::parse_from_rfc3339("2022-04-15T04:25:00+00:00").unwrap(), DateTime::parse_from_rfc3339("2022-04-15T04:25:00+00:00").unwrap(), DateTime::parse_from_rfc3339("2022-04-15T04:25:00+00:00").unwrap(), 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 vec = vec![ DateTime::parse_from_rfc3339("2022-04-15T04:25:00.001+00:00").unwrap(), DateTime::parse_from_rfc3339("2022-04-15T04:25:00.002+00:00").unwrap(), 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 vec = vec![ DateTime::parse_from_rfc3339("2022-04-15T04:25:00.001+00:00").unwrap(), 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")); } }