rtimelog 0.51.0

System for tracking time in a text-log-based format.
Documentation
//! The [`PieData`] type represents the data in a pie chart.
//!
//! # Examples
//!
//! # Description
//!
//! The [`PieData`] holds the data used to construct a pie chart. It supports
//! adding data to build up the data set.

use std::cmp::Ordering;
use std::collections::HashMap;

#[doc(inline)]
use crate::chart::{Percentages, TagPercent};

/// Representation of the data used to construct the pie chart.
#[derive(Debug)]
pub struct PieData {
    total: f32,
    data:  HashMap<String, f32>
}

impl PieData {
    /// Add the supplied number of seconds to the supplied label.
    pub fn add_secs(&mut self, label: &str, secs: u64) {
        #![allow(clippy::cast_precision_loss)]
        let secs = secs as f32;
        self.total += secs;
        let entry = self.data.entry(label.to_string()).or_insert(0.0);
        *entry += secs;
    }

    /// Return the percentage associated with the supplied label, if any.
    pub fn get_percent(&self, label: &str) -> Option<f32> {
        self.data.get(label).map(|&s| self.cent(s))
    }

    /// Return `true` if no data has been added to the [`PieData`]
    pub fn is_empty(&self) -> bool { self.data.is_empty() }

    /// Return the total sum of the values in the data
    pub fn total(&self) -> f32 { self.total }

    /// Return a sorted list of the labels in the [`PieData`].
    pub fn labels(&self) -> Vec<&String> {
        let mut labels: Vec<&String> = self.data.keys().collect();
        labels.sort();
        labels
    }

    /// Return list of [`TagPercent`] objects sorted by descending percentage
    /// and ascending label.
    pub fn percentages(&self) -> Percentages {
        let mut percents: Percentages = self
            .data
            .iter()
            .filter_map(|(l, p)| TagPercent::new(l, self.cent(*p)))
            .collect();

        percents.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
        percents
    }

    // Utility method for converting a stored value into a percentage.
    fn cent(&self, val: f32) -> f32 { 100.0 * val / self.total }
}

impl Default for PieData {
    /// Create a brand-new [`PieData`] object with no data.
    fn default() -> Self { Self { total: 0.0, data: HashMap::new() } }
}

#[cfg(test)]
mod tests {
    use spectral::prelude::*;

    use super::*;

    #[test]
    fn test_default() {
        let pd = PieData::default();

        assert_that!(pd.is_empty()).is_true();
        assert_that!(pd.total()).is_equal_to(&0.0);
        assert_that!(pd.labels().len()).is_equal_to(&0);
        assert_that!(pd.percentages().len()).is_equal_to(&0);
    }

    #[test]
    fn test_add_secs() {
        let mut pd = PieData::default();

        pd.add_secs("foo", 300);

        assert_that!(pd.is_empty()).is_false();
        assert_that!(pd.total()).is_equal_to(&300.0);
        assert_that!(pd.labels().len()).is_equal_to(&1);
        assert_that!(pd.labels()).is_equal_to(&vec![&("foo".to_string())]);
        assert_that!(pd.percentages()).is_equal_to(&vec![TagPercent::new("foo", 100.0).unwrap()]);
    }

    #[test]
    fn test_add_secs_multiple() {
        let mut pd = PieData::default();

        #[rustfmt::skip]
        let input = [
            ("david",   400),
            ("kirsten", 300),
            ("mark",    200),
            ("connie",  100)
        ];
        for (label, secs) in &input {
            pd.add_secs(label, *secs);
        }

        assert_that!(pd.total()).is_equal_to(&1000.0);
        let expect: Vec<String> = ["connie", "david", "kirsten", "mark"]
            .iter()
            .map(|s| s.to_string())
            .collect();

        assert_that!(pd.labels()).is_equal_to(&expect.iter().collect::<Vec<&String>>());
        let expect: Vec<TagPercent> = input
            .iter()
            .map(|(l, s)| TagPercent::new(l, *s as f32 / 10.0).unwrap())
            .collect();
        assert_that!(pd.percentages()).is_equal_to(&expect);
    }
}