use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum PriceDisplayMode {
#[default]
Absolute,
Percentage {
reference_price: Option<i64>, },
Points {
reference_price: Option<i64>,
},
BasisPoints { reference_price: Option<i64> },
}
impl PriceDisplayMode {
pub fn format(&self, price: f64, reference: Option<f64>) -> String {
match self {
Self::Absolute => {
format!("{price:.2}")
}
Self::Percentage { reference_price } => {
let ref_price = reference_price
.map(|r| r as f64)
.or(reference)
.unwrap_or(price);
if ref_price == 0.0 {
return "N/A".to_string();
}
let change = ((price - ref_price) / ref_price) * 100.0;
if change >= 0.0 {
format!("+{change:.2}%")
} else {
format!("{change:.2}%")
}
}
Self::Points { reference_price } => {
let ref_price = reference_price
.map(|r| r as f64)
.or(reference)
.unwrap_or(price);
let change = price - ref_price;
if change >= 0.0 {
format!("+{change:.2} pts")
} else {
format!("{change:.2} pts")
}
}
Self::BasisPoints { reference_price } => {
let ref_price = reference_price
.map(|r| r as f64)
.or(reference)
.unwrap_or(price);
if ref_price == 0.0 {
return "N/A".to_string();
}
let change = ((price - ref_price) / ref_price) * 10000.0;
if change >= 0.0 {
format!("+{change:.0} bps")
} else {
format!("{change:.0} bps")
}
}
}
}
pub fn to_display_val(&self, price: f64, reference: Option<f64>) -> f64 {
match self {
Self::Absolute => price,
Self::Percentage { reference_price } => {
let ref_price = reference_price
.map(|r| r as f64)
.or(reference)
.unwrap_or(price);
if ref_price == 0.0 {
return 0.0;
}
((price - ref_price) / ref_price) * 100.0
}
Self::Points { reference_price } => {
let ref_price = reference_price
.map(|r| r as f64)
.or(reference)
.unwrap_or(price);
price - ref_price
}
Self::BasisPoints { reference_price } => {
let ref_price = reference_price
.map(|r| r as f64)
.or(reference)
.unwrap_or(price);
if ref_price == 0.0 {
return 0.0;
}
((price - ref_price) / ref_price) * 10000.0
}
}
}
}
impl fmt::Display for PriceDisplayMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Absolute => write!(f, "Absolute"),
Self::Percentage { .. } => write!(f, "Percentage"),
Self::Points { .. } => write!(f, "Points"),
Self::BasisPoints { .. } => write!(f, "Basis Points"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum AutoScaleMode {
AllData,
#[default]
VisibleData,
VisibleDataWithPadding {
padding_percent: u8, },
Custom {
min: i64, max: i64,
},
Locked,
}
impl AutoScaleMode {
pub fn calculate_range(
&self,
visible_min: f64,
visible_max: f64,
all_data_min: f64,
all_data_max: f64,
) -> (f64, f64) {
match self {
Self::AllData => (all_data_min, all_data_max),
Self::VisibleData => (visible_min, visible_max),
Self::VisibleDataWithPadding { padding_percent } => {
let padding = *padding_percent as f64 / 100.0;
let range = visible_max - visible_min;
let padding_amount = range * padding;
(visible_min - padding_amount, visible_max + padding_amount)
}
Self::Custom { min, max } => (*min as f64, *max as f64),
Self::Locked => (visible_min, visible_max),
}
}
pub fn visible_with_standard_padding() -> Self {
Self::VisibleDataWithPadding {
padding_percent: 10,
}
}
}
impl fmt::Display for AutoScaleMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AllData => write!(f, "All Data"),
Self::VisibleData => write!(f, "Visible Data"),
Self::VisibleDataWithPadding { padding_percent } => {
write!(f, "Visible + {padding_percent}% padding")
}
Self::Custom { .. } => write!(f, "Custom Range"),
Self::Locked => write!(f, "Locked"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_price_display_absolute() {
let mode = PriceDisplayMode::Absolute;
assert_eq!(mode.format(50123.45, None), "50123.45");
}
#[test]
fn test_price_display_percentage() {
let mode = PriceDisplayMode::Percentage {
reference_price: Some(50000),
};
assert_eq!(mode.format(51000.0, None), "+2.00%");
assert_eq!(mode.format(49000.0, None), "-2.00%");
}
#[test]
fn test_price_display_points() {
let mode = PriceDisplayMode::Points {
reference_price: Some(50000),
};
assert_eq!(mode.format(50150.0, None), "+150.00 pts");
assert_eq!(mode.format(49925.0, None), "-75.00 pts");
}
#[test]
fn test_price_display_basis_points() {
let mode = PriceDisplayMode::BasisPoints {
reference_price: Some(50000),
};
let formatted = mode.format(51000.0, None);
assert!(formatted.contains("200"));
assert!(formatted.contains("bps"));
}
#[test]
fn test_auto_scale_visible_with_padding() {
let mode = AutoScaleMode::VisibleDataWithPadding {
padding_percent: 10,
};
let (min, max) = mode.calculate_range(100.0, 200.0, 50.0, 250.0);
assert_eq!(min, 90.0); assert_eq!(max, 210.0); }
#[test]
fn test_auto_scale_all_data() {
let mode = AutoScaleMode::AllData;
let (min, max) = mode.calculate_range(100.0, 200.0, 50.0, 250.0);
assert_eq!(min, 50.0);
assert_eq!(max, 250.0);
}
#[test]
fn test_auto_scale_custom() {
let mode = AutoScaleMode::Custom {
min: 1000,
max: 2000,
};
let (min, max) = mode.calculate_range(100.0, 200.0, 50.0, 250.0);
assert_eq!(min, 1000.0);
assert_eq!(max, 2000.0);
}
}