use std::cmp::Ordering;
use std::fmt::{self, Display};
#[allow(clippy::derive_ord_xor_partial_ord, reason = "force edge case to equal")]
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
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 Ord for Percent {
fn cmp(&self, right: &Self) -> Ordering {
self.partial_cmp(right).unwrap_or(Ordering::Equal)
}
}
impl Display for Percent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0 < 1.0 {
write!(f, "{:.1}%", self.0)
}
else {
write!(f, "{:.0}%", self.0)
}
}
}
#[derive(Debug, Clone)]
pub struct TagPercent {
percent: Percent,
label: String
}
impl TagPercent {
#[rustfmt::skip]
pub fn new(label: &str, percent: f32) -> Option<Self> {
Some(
Self { percent: Percent::try_new(percent)?, label: label.to_string() }
)
}
pub fn label(&self) -> &str { &self.label }
pub fn display_label(&self) -> String { format!("{} - {}", self.percent, self.label) }
pub fn percent(&self) -> &Percent { &self.percent }
pub fn percent_val(&self) -> f32 { self.percent.val() }
}
impl PartialOrd for TagPercent {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(
self.percent
.cmp(&other.percent)
.reverse()
.then_with(|| self.label.cmp(&other.label))
)
}
}
impl PartialEq for TagPercent {
fn eq(&self, other: &Self) -> bool {
(self.percent == other.percent) && (self.label == other.label)
}
}
impl Eq for TagPercent {}
#[cfg(test)]
mod tests {
use assert2::{assert, let_assert};
use rstest::rstest;
use super::*;
#[test]
fn test_new() {
let_assert!(Some(tagper) = TagPercent::new("foo", 50.0));
assert!(tagper == TagPercent { percent: Percent(50.0), label: "foo".to_string() });
}
#[rstest]
#[case("foo", 100.0, "100% - foo")]
#[case("foo", 50.0, "50% - foo")]
#[case("foo", 4.0, "4% - foo")]
fn test_display_label(#[case]label: &str, #[case]pcnt: f32, #[case]expect: &str) {
let_assert!(Some(tagper) = TagPercent::new(label, pcnt));
assert!(tagper.display_label() == expect.to_string());
}
#[rstest]
#[case("foo", 0.0, "zero")]
#[case("foo", -10.0, "negative")]
#[case("foo", 100.1, "too large")]
#[rustfmt::skip]
fn test_new_zero(#[case]label: &str, #[case]pcnt: f32, #[case]msg: &str) {
assert!(TagPercent::new(label, pcnt).is_none(), "{msg}");
}
#[test]
fn test_accessors() {
let_assert!(Some(val) = TagPercent::new("bar", 37.5));
assert!(val.label() == "bar");
assert!(val.percent_val() == 37.5);
}
#[test]
fn test_order_by_percentage_same_label() {
let_assert!(Some(val1) = TagPercent::new("bar", 10.0));
let_assert!(Some(val2) = TagPercent::new("bar", 15.0));
assert!(Some(Ordering::Greater) == val1.partial_cmp(&val2));
assert!(Some(Ordering::Less) == val2.partial_cmp(&val1));
}
#[test]
fn test_order_by_percentage_diff_label() {
let_assert!(Some(val1) = TagPercent::new("bar", 10.0));
let_assert!(Some(val2) = TagPercent::new("foo", 15.0));
assert!(Some(Ordering::Greater) == val1.partial_cmp(&val2));
assert!(Some(Ordering::Less) == val2.partial_cmp(&val1));
}
#[test]
fn test_order_by_label_same_percentage() {
let_assert!(Some(val1) = TagPercent::new("bar", 10.0));
let_assert!(Some(val2) = TagPercent::new("foo", 10.0));
assert!(Some(Ordering::Less) == val1.partial_cmp(&val2));
assert!(Some(Ordering::Greater) == val2.partial_cmp(&val1));
}
}