use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Headline {
Amount,
Percent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetricHeadline {
Percent,
Value,
}
impl MetricHeadline {
pub fn as_str(self) -> &'static str {
match self {
Self::Percent => "percent",
Self::Value => "value",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DisplayPrefs {
pub display_limit: Option<f64>,
pub headline: Headline,
}
impl Default for DisplayPrefs {
fn default() -> Self {
Self {
display_limit: None,
headline: Headline::Percent,
}
}
}
impl DisplayPrefs {
pub fn balance(display_limit: Option<f64>, headline: Headline) -> Self {
Self {
display_limit,
headline,
}
}
}
pub fn denominator(api_limit: Option<f64>, display_limit: Option<f64>) -> Option<f64> {
api_limit
.filter(usable)
.or_else(|| display_limit.filter(usable))
}
fn usable(limit: &f64) -> bool {
limit.is_finite() && *limit > 0.0
}
pub fn consumed_pct(limit: f64, remaining: f64) -> u16 {
if !usable(&limit) {
return 0;
}
crate::format::clamp_pct((limit - remaining) / limit * 100.0)
}
pub fn resolve_headline(choice: Headline, denominator: Option<f64>) -> MetricHeadline {
match choice {
Headline::Amount => MetricHeadline::Value,
Headline::Percent if denominator.is_some() => MetricHeadline::Percent,
Headline::Percent => MetricHeadline::Value,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_api_limit_wins_over_the_users_display_limit() {
assert_eq!(denominator(Some(50.0), Some(200.0)), Some(50.0));
}
#[test]
fn display_limit_is_used_only_when_the_api_states_no_limit() {
assert_eq!(denominator(None, Some(200.0)), Some(200.0));
assert_eq!(denominator(Some(50.0), None), Some(50.0));
assert_eq!(denominator(None, None), None);
}
#[test]
fn an_unusable_limit_counts_as_absent_at_either_position() {
for bad in [0.0, -1.0, f64::NAN, f64::INFINITY] {
assert_eq!(denominator(Some(bad), Some(200.0)), Some(200.0), "{bad}");
assert_eq!(denominator(None, Some(bad)), None, "{bad}");
assert_eq!(denominator(Some(bad), None), None, "{bad}");
}
}
#[test]
fn percent_is_consumed_not_remaining() {
assert_eq!(consumed_pct(200.0, 200.0), 0);
assert_eq!(consumed_pct(200.0, 150.0), 25);
assert_eq!(consumed_pct(200.0, 100.0), 50);
assert_eq!(consumed_pct(200.0, 0.0), 100);
}
#[test]
fn a_balance_above_the_cap_reads_as_zero_percent_used() {
assert_eq!(consumed_pct(200.0, 250.0), 0);
assert_eq!(consumed_pct(20.0, 1_000.0), 0);
}
#[test]
fn an_overdrawn_balance_stops_at_one_hundred() {
assert_eq!(consumed_pct(200.0, -0.01), 100);
assert_eq!(consumed_pct(200.0, -5_000.0), 100);
}
#[test]
fn percent_rounds_to_the_nearest_whole() {
assert_eq!(consumed_pct(20.0, 14.5), 28);
assert_eq!(consumed_pct(20.0, 15.1), 25);
}
#[test]
fn a_limit_that_cannot_divide_yields_zero_rather_than_a_nonsense_percent() {
for bad in [0.0, -10.0, f64::NAN, f64::INFINITY] {
assert_eq!(consumed_pct(bad, 5.0), 0, "{bad}");
}
assert_eq!(consumed_pct(200.0, f64::NAN), 0);
}
#[test]
fn amount_keeps_the_money_figure_on_the_bar_even_with_a_tank() {
assert_eq!(
resolve_headline(Headline::Amount, Some(200.0)),
MetricHeadline::Value
);
assert_eq!(
resolve_headline(Headline::Amount, None),
MetricHeadline::Value
);
}
#[test]
fn percent_needs_a_denominator_and_otherwise_keeps_the_amount() {
assert_eq!(
resolve_headline(Headline::Percent, Some(200.0)),
MetricHeadline::Percent
);
assert_eq!(
resolve_headline(Headline::Percent, None),
MetricHeadline::Value
);
}
#[test]
fn the_report_words_are_percent_and_value() {
assert_eq!(MetricHeadline::Percent.as_str(), "percent");
assert_eq!(MetricHeadline::Value.as_str(), "value");
}
#[test]
fn the_default_prefs_are_the_quota_shape() {
let prefs = DisplayPrefs::default();
assert_eq!(prefs.display_limit, None);
assert_eq!(prefs.headline, Headline::Percent);
}
#[test]
fn headline_parses_from_the_config_words_and_rejects_anything_else() {
#[derive(Deserialize)]
struct Wrapper {
headline: Headline,
}
let parse = |body: &str| toml::from_str::<Wrapper>(body).map(|w| w.headline);
assert_eq!(parse("headline = \"amount\"").unwrap(), Headline::Amount);
assert_eq!(parse("headline = \"percent\"").unwrap(), Headline::Percent);
assert!(parse("headline = \"dollars\"").is_err());
assert!(parse("headline = \"Amount\"").is_err());
}
}