rtimelog 1.1.1

System for tracking time in a text-log-based format.
Documentation
//! Define color handling for charts.

use std::collections::HashMap;

use crate::chart::TagPercent;

// Built from Colorbrewer 2.0, with help from Blonde
const COLORS: [&str; 8] = [
    "#1f78b4", "#a6cee3", "#33a02c", "#b2df8a", "#aa3133", "#fb9a99", "#ff7f00", "#fdbf6f"
];
const LAST_COLOR: usize = COLORS.len() - 1;

/// Iterator over the available colors.
#[derive(Clone)]
pub struct ColorIter<'a> {
    // Current index into the colors
    idx:    usize,
    // Slice holding the colors
    colors: &'a [&'static str],
    // Index of the last item in the color slice
    last:   usize
}

impl<'a> ColorIter<'a> {
    /// Create a [`ColorIter`] based on the supplied slice of a color container.
    pub fn new(colors: &'a [&'static str]) -> Self {
        Self { idx: 0, colors, last: colors.len() - 1 }
    }

    /// Convert a slice of [`TagPercent`] into a Vector of [`TagPercent`]s limited
    /// to the number of colors. If there are more percent entries than colors, the
    /// remaining items are combined with the entry for the last color.
    pub fn limit_percents(&self, percents: &[TagPercent], other: &str) -> Vec<TagPercent> {
        if percents.len() <= self.colors.len() {
            percents.to_vec()
        }
        else {
            let len = self.colors.len() - 1;
            let percent: f32 = percents.iter().skip(len).map(TagPercent::percent_val).sum();
            let mut percents: Vec<TagPercent> = percents.iter().take(len).cloned().collect();

            // Since the percents are all calculated from the sum, there should be none that
            // fail to new.
            if let Some(perc) = TagPercent::new(other, percent) {
                percents.push(perc);
            }
            percents
        }
    }
}

impl<'a> Iterator for ColorIter<'a> {
    type Item = &'static str;

    /// Return the next color, repeating the last as long as needed.
    fn next(&mut self) -> Option<Self::Item> {
        let idx = self.idx;
        if idx < self.last {
            self.idx += 1;
        }
        Some(self.colors[idx])
    }
}

impl<'a> Default for ColorIter<'a> {
    /// Create a [`ColorIter`] based on the default set of colors
    fn default() -> Self { Self::new(&COLORS) }
}

/// A mapping of labels to colors.
pub struct ColorMap(HashMap<String, &'static str>);

impl ColorMap {
    /// Create a [`ColorMap`] from the supplied slice of [`TagPercent`] with the default colors.
    pub fn new(percents: &[TagPercent]) -> Self {
        Self::new_with_colors(percents, ColorIter::default())
    }

    /// Create a [`ColorMap`] from the supplied slice of [`TagPercent`] and [`ColorIter`].
    pub fn new_with_colors(percents: &[TagPercent], colors: ColorIter) -> Self {
        Self(
            percents
                .iter()
                .zip(colors)
                .map(|(tp, clr)| (tp.label().to_string(), clr))
                .collect()
        )
    }

    /// Return the color associated with the supplied label.
    pub fn get(&self, label: &str) -> Option<&'static str> { self.0.get(label).copied() }

    /// Declare a label for the final color.
    pub fn set_default(&mut self, label: &str) {
        self.0.insert(label.to_string(), COLORS[LAST_COLOR]);
    }

    /// Declare a label for the final color.
    pub fn set_default_to_color(&mut self, label: &str, clr: &'static str) {
        self.0.insert(label.to_string(), clr);
    }
}

#[cfg(test)]
mod tests {
    use assert2::{assert, let_assert};

    use super::*;

    #[test]
    fn test_new_iter() {
        let mut iter = ColorIter::new(&COLORS);
        let_assert!(Some(_color) = iter.next());
    }

    #[test]
    fn test_color_iter_base() {
        for (clr, expect) in ColorIter::default().zip(&COLORS) {
            assert!(&clr == expect);
        }
    }

    #[test]
    fn test_color_iter_tail() {
        let mut iter = ColorIter::default().skip(COLORS.len());
        for _ in 0..10 {
            let_assert!(Some(color) = iter.next());
            assert!(color == COLORS[LAST_COLOR]);
        }
    }

    #[test]
    fn test_new() {
        let colors = [
            "black", "brown", "red", "orange", "yellow", "green", "blue", "violet", "grey", "white"
        ];
        let iter = ColorIter::new(&colors);

        for (actual, expect) in iter.zip(&colors) {
            assert!(&actual == expect);
        }
    }

    #[test]
    fn color_map_few() {
        #[rustfmt::skip]
        let percents = [
            TagPercent::new("david",   40.0).expect("Hardcoded value"),
            TagPercent::new("connie",  20.0).expect("Hardcoded value"),
            TagPercent::new("mark",    10.0).expect("Hardcoded value"),
            TagPercent::new("kirsten", 10.0).expect("Hardcoded value"),
            TagPercent::new("fred",     5.0).expect("Hardcoded value"),
            TagPercent::new("bianca",   5.0).expect("Hardcoded value"),
        ];

        let cmap = ColorMap::new(&percents);
        for (label, clr) in percents
            .iter()
            .map(|tp| tp.label())
            .zip(ColorIter::default())
        {
            let_assert!(Some(tagpcnt) = cmap.get(label));
            assert!(tagpcnt == clr);
        }
    }

    #[test]
    fn color_map_with_default() {
        #[rustfmt::skip]
        let percents = [
            TagPercent::new("david",   40.0).expect("Hardcoded value"),
            TagPercent::new("connie",  20.0).expect("Hardcoded value"),
            TagPercent::new("mark",    10.0).expect("Hardcoded value"),
            TagPercent::new("kirsten", 10.0).expect("Hardcoded value"),
            TagPercent::new("fred",     5.0).expect("Hardcoded value"),
            TagPercent::new("bianca",   5.0).expect("Hardcoded value"),
        ];

        let mut cmap = ColorMap::new(&percents);
        assert!(None == cmap.get("Other"), "no default");

        cmap.set_default("Other");
        let_assert!(Some(color) = cmap.get("Other"), "has default");
        assert!(color == COLORS[LAST_COLOR]);
    }

    #[test]
    fn color_map_many() {
        #[rustfmt::skip]
        let percents = [
            TagPercent::new("david",   40.0).expect("Hardcoded value"),
            TagPercent::new("connie",  20.0).expect("Hardcoded value"),
            TagPercent::new("mark",    10.0).expect("Hardcoded value"),
            TagPercent::new("kirsten", 10.0).expect("Hardcoded value"),
            TagPercent::new("fred",     5.0).expect("Hardcoded value"),
            TagPercent::new("bianca",   5.0).expect("Hardcoded value"),
            TagPercent::new("aramis",   1.0).expect("Hardcoded value"),
            TagPercent::new("bryan",    1.0).expect("Hardcoded value"),
            TagPercent::new("serpent",  1.0).expect("Hardcoded value"),
            TagPercent::new("mobile",   1.0).expect("Hardcoded value"),
        ];

        let cmap = ColorMap::new(&percents);
        for (label, clr) in percents
            .iter()
            .map(|tp| tp.label())
            .zip(ColorIter::default())
        {
            let_assert!(Some(color) = cmap.get(label));
            assert!(color == clr);
        }
    }

    #[test]
    fn limit_percents_few() {
        #[rustfmt::skip]
        let percents = [
            TagPercent::new("david",   40.0).expect("Hardcoded value"),
            TagPercent::new("connie",  20.0).expect("Hardcoded value"),
            TagPercent::new("mark",    10.0).expect("Hardcoded value"),
            TagPercent::new("kirsten", 10.0).expect("Hardcoded value"),
            TagPercent::new("fred",     5.0).expect("Hardcoded value"),
            TagPercent::new("bianca",   5.0).expect("Hardcoded value"),
        ];

        let colors = ColorIter::default();
        for (a, b) in colors.limit_percents(&percents, "Other").iter().zip(percents.iter()) {
            assert!(a == b);
        }
    }

    #[test]
    fn limit_percents_many() {
        #[rustfmt::skip]
        let percents = [
            TagPercent::new("david",   40.0).expect("Hardcoded value"),
            TagPercent::new("connie",  20.0).expect("Hardcoded value"),
            TagPercent::new("mark",    10.0).expect("Hardcoded value"),
            TagPercent::new("kirsten", 10.0).expect("Hardcoded value"),
            TagPercent::new("fred",     5.0).expect("Hardcoded value"),
            TagPercent::new("bianca",   5.0).expect("Hardcoded value"),
            TagPercent::new("aramis",   1.0).expect("Hardcoded value"),
            TagPercent::new("bryan",    1.0).expect("Hardcoded value"),
            TagPercent::new("serpent",  1.0).expect("Hardcoded value"),
            TagPercent::new("mobile",   1.0).expect("Hardcoded value"),
        ];

        #[rustfmt::skip]
        let expected = [
            TagPercent::new("david",   40.0).expect("Hardcoded value"),
            TagPercent::new("connie",  20.0).expect("Hardcoded value"),
            TagPercent::new("mark",    10.0).expect("Hardcoded value"),
            TagPercent::new("kirsten", 10.0).expect("Hardcoded value"),
            TagPercent::new("fred",     5.0).expect("Hardcoded value"),
            TagPercent::new("bianca",   5.0).expect("Hardcoded value"),
            TagPercent::new("aramis",   1.0).expect("Hardcoded value"),
            TagPercent::new("Other",    3.0).expect("Hardcoded value"),
        ];

        let colors = ColorIter::default();
        for (a, b) in colors.limit_percents(&percents, "Other").iter().zip(expected.iter()) {
            assert!(a == b);
        }
    }
}