rtimelog 0.51.0

System for tracking time in a text-log-based format.
Documentation
//! Data struct representing a label and percentage.
//!
//! # Examples
//!
//! ```rust
//! use timelog::chart::TagPercent;
//!
//! # fn main() {
//! let tp = TagPercent::new("One tenth", 10.0).unwrap();
//!
//! if tp.percent_val() < 50.0 {
//!     println!("'{}' is less than half", tp.label());
//! }
//!
//! println!("{}", tp.display_label());
//! # }
//! ```

use std::cmp::Ordering;
use std::fmt::{self, Display};

/// Floating point percentage from 0.0 to 100.0
#[derive(Clone, Debug, Default)]
pub struct Percent(f32);

impl Percent {
    #[rustfmt::skip]
    pub fn try_new(val: f32) -> Option<Self> {
        (0.0 < val && val <= 100.0).then_some(Percent(val))
    }

    pub fn val(&self) -> f32 { self.0 }
}

impl Eq for Percent {}

impl PartialEq for Percent {
    /// Compare two [`Percent`] values, return true if they are equal.
    fn eq(&self, right: &Self) -> bool { self.0 == right.0 }
}

impl PartialOrd for Percent {
    /// Compare two [`Percent`] values, returning appropriate Ordering variant
    fn partial_cmp(&self, right: &Self) -> Option<Ordering> {
        self.0.partial_cmp(&right.0)
    }
}

impl Ord for Percent {
    /// Compare two [`Percent`] values, returning appropriate Ordering variant
    fn cmp(&self, right: &Self) -> Ordering {
        // Safe to convert unwrap_or because of the data limits and no non-numbers
        self.partial_cmp(right).unwrap_or(Ordering::Equal)
    }
}

impl Display for Percent {
    /// Format the percent, assuming a positive floating point number less than or equal to 100.0.
    /// If you use a number outside that range, the formatting may not be appropriate.
    ///
    /// - Numbers less than 1 will be formatted with one decimal place.
    /// - Numbers greater than or equal to 1 will be formatted as whole numbers.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        (self.0 < 1.0)
            .then(|| write!(f, "{:.1}%", self.0))
            .unwrap_or_else(|| write!(f, "{:.0}%", self.0))
    }
}

/// Structure holding a percent value with a label
#[derive(Debug, Clone)]
pub struct TagPercent {
    percent: Percent,
    label:   String
}

impl TagPercent {
    /// Create a new [`TagPercent`] object if the percentage is between 0 and 100
    /// inclusive, otherwise `None`.
    #[rustfmt::skip]
    pub fn new(label: &str, percent: f32) -> Option<Self> {
        Some(
            Self { percent: Percent::try_new(percent)?, label: label.to_string() }
        )
    }

    /// Return a reference to the label.
    pub fn label(&self) -> &str { &self.label }

    /// Return a display label
    pub fn display_label(&self) -> String { format!("{} - {}", self.percent, self.label) }

    /// Return the percentage value.
    pub fn percent(&self) -> &Percent { &self.percent }

    /// Return the percentage value.
    pub fn percent_val(&self) -> f32 { self.percent.val() }
}

impl PartialOrd for TagPercent {
    /// Compare two [`TagPercent`] values, returning appropriate Ordering variant.
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(
            other.percent
                .cmp(&self.percent)
                .then_with(|| self.label.cmp(&other.label))
        )
    }
}

impl PartialEq for TagPercent {
    /// Return `true` if two [`TagPercent`] values are equal.
    fn eq(&self, other: &Self) -> bool {
        (self.percent == other.percent) && (self.label == other.label)
    }
}

impl Eq for TagPercent {}

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

    use super::*;

    #[test]
    fn test_new() {
        assert_that!(TagPercent::new("foo", 50.0))
            .contains(TagPercent { percent: Percent(50.0), label: "foo".to_string() });
    }

    #[test]
    fn test_display_label() {
        assert_that!(TagPercent::new("foo", 100.0).unwrap().display_label())
            .is_equal_to("100% - foo".to_string());
        assert_that!(TagPercent::new("foo", 50.0).unwrap().display_label())
            .is_equal_to("50% - foo".to_string());
        assert_that!(TagPercent::new("foo", 4.0).unwrap().display_label())
            .is_equal_to("4% - foo".to_string());
    }

    #[test]
    #[rustfmt::skip]
    fn test_new_zero() {
        assert_that!(TagPercent::new("foo", 0.0)).is_none();
    }

    #[test]
    #[rustfmt::skip]
    fn test_new_negative() {
        assert_that!(TagPercent::new("foo", -10.0)).is_none();
    }

    #[test]
    #[rustfmt::skip]
    fn test_new_too_large() {
        assert_that!(TagPercent::new("foo", 100.1)).is_none();
    }

    #[test]
    fn test_accessors() {
        let val = TagPercent::new("bar", 37.5).unwrap();
        assert_that!(val.label()).is_equal_to("bar");
        assert_that!(val.percent_val()).is_equal_to(37.5);
    }

    #[test]
    fn test_order_by_percentage_same_label() {
        let val1 = TagPercent::new("bar", 10.0).unwrap();
        let val2 = TagPercent::new("bar", 15.0).unwrap();
        assert_that!(val1.partial_cmp(&val2))
            .is_some()
            .is_equal_to(Ordering::Greater);
        assert_that!(val2.partial_cmp(&val1))
            .is_some()
            .is_equal_to(Ordering::Less);
    }

    #[test]
    fn test_order_by_percentage_diff_label() {
        let val1 = TagPercent::new("bar", 10.0).unwrap();
        let val2 = TagPercent::new("foo", 15.0).unwrap();
        assert_that!(val1.partial_cmp(&val2))
            .is_some()
            .is_equal_to(Ordering::Greater);
        assert_that!(val2.partial_cmp(&val1))
            .is_some()
            .is_equal_to(Ordering::Less);
    }

    #[test]
    fn test_order_by_label_same_percentage() {
        let val1 = TagPercent::new("bar", 10.0).unwrap();
        let val2 = TagPercent::new("foo", 10.0).unwrap();
        assert_that!(val1.partial_cmp(&val2))
            .is_some()
            .is_equal_to(Ordering::Less);
        assert_that!(val2.partial_cmp(&val1))
            .is_some()
            .is_equal_to(Ordering::Greater);
    }
}