rtimelog 1.1.1

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::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<&str> {
        let mut labels: Vec<&str> = self.data.keys().map(String::as_str).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(std::cmp::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 assert2::{assert, let_assert};

    use super::*;

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

        assert!(pd.is_empty());
        assert!(pd.total() == 0.0);
        assert!(pd.labels().len() == 0);
        assert!(pd.percentages().len() == 0);
    }

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

        pd.add_secs("foo", 300);

        assert!(!pd.is_empty());
        assert!(pd.total() == 300.0);
        assert!(pd.labels().len() == 1);
        assert!(pd.labels() == vec!["foo"]);
        let_assert!(Some(expected_pcnt) = TagPercent::new("foo", 100.0));
        assert!(pd.percentages() == vec![expected_pcnt]);
    }

    #[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!(pd.total() == 1000.0);
        let expect: Vec<&str> = vec!["connie", "david", "kirsten", "mark"];

        assert!(pd.labels() == expect);
        let expect: Vec<TagPercent> = input
            .iter()
            .map(|(l, s)| TagPercent::new(l, *s as f32 / 10.0).expect("Hardcoded value"))
            .collect();
        assert!(pd.percentages() == expect);
    }
}