use crate::types::BrightDateError;
pub const DEFAULT_BD_PRECISION: usize = 3;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BrightLabel {
BD(f64),
PBD(f64),
}
pub fn format_bd(bd: f64, precision: usize) -> Result<String, BrightDateError> {
if !bd.is_finite() {
return Err(BrightDateError::InvalidNumber(format!(
"format_bd: value must be finite, got {bd}"
)));
}
let v = if bd == 0.0 { 0.0_f64 } else { bd };
let prefix = if v < 0.0 { "PBD" } else { "BD" };
let magnitude = v.abs();
Ok(format!("{prefix} {:.*}", precision, magnitude))
}
pub fn format_bd_label(
label: BrightLabel,
precision: usize,
) -> Result<String, BrightDateError> {
match label {
BrightLabel::BD(value) if value < 0.0 => Err(BrightDateError::InvalidInput(
format!("format_bd_label: BD value must be non-negative, got {value}"),
)),
BrightLabel::PBD(value) if value <= 0.0 => Err(BrightDateError::InvalidInput(
format!(
"format_bd_label: PBD value must be strictly positive, got {value}"
),
)),
BrightLabel::BD(value) => Ok(format!("BD {:.*}", precision, value)),
BrightLabel::PBD(value) => Ok(format!("PBD {:.*}", precision, value)),
}
}
pub fn parse_bd(label: &str) -> Result<f64, BrightDateError> {
let trimmed = label.trim();
let (prefix, rest) = if let Some(rest) = trimmed.strip_prefix("BD") {
("BD", rest)
} else if let Some(rest) = trimmed.strip_prefix("PBD") {
("PBD", rest)
} else {
return Err(BrightDateError::ParseError(format!(
"parse_bd: not a recognised label, expected \"BD <n>\" or \"PBD <n>\", got {label:?}"
)));
};
let body = rest.trim_start();
let value: f64 = body.parse().map_err(|_| {
BrightDateError::ParseError(format!(
"parse_bd: numeric body did not parse, got {body:?}"
))
})?;
if !value.is_finite() {
return Err(BrightDateError::ParseError(format!(
"parse_bd: numeric body must be finite, got {body:?}"
)));
}
match prefix {
"BD" => {
if value < 0.0 {
Err(BrightDateError::InvalidInput(format!(
"parse_bd: BD value must be non-negative, got {value}"
)))
} else {
Ok(value)
}
}
"PBD" => {
if value <= 0.0 {
Err(BrightDateError::InvalidInput(format!(
"parse_bd: PBD value must be strictly positive, got {value}"
)))
} else {
Ok(-value)
}
}
_ => unreachable!(),
}
}
pub fn parse_bd_label(label: &str) -> Result<BrightLabel, BrightDateError> {
let scalar = parse_bd(label)?;
if scalar >= 0.0 {
Ok(BrightLabel::BD(scalar))
} else {
Ok(BrightLabel::PBD(-scalar))
}
}
pub fn compare_bd_labels(a: BrightLabel, b: BrightLabel) -> std::cmp::Ordering {
use std::cmp::Ordering;
match (a, b) {
(BrightLabel::BD(_), BrightLabel::PBD(_)) => Ordering::Greater,
(BrightLabel::PBD(_), BrightLabel::BD(_)) => Ordering::Less,
(BrightLabel::BD(x), BrightLabel::BD(y)) => {
x.partial_cmp(&y).unwrap_or(Ordering::Equal)
}
(BrightLabel::PBD(x), BrightLabel::PBD(y)) => {
y.partial_cmp(&x).unwrap_or(Ordering::Equal)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_bd_renders_zero_as_canonical_bd() {
assert_eq!(format_bd(0.0, DEFAULT_BD_PRECISION).unwrap(), "BD 0.000");
}
#[test]
fn format_bd_never_produces_pbd_zero() {
assert_eq!(format_bd(-0.0, DEFAULT_BD_PRECISION).unwrap(), "BD 0.000");
}
#[test]
fn format_bd_renders_positive_with_bd_prefix() {
assert_eq!(format_bd(9622.504, 3).unwrap(), "BD 9622.504");
assert_eq!(format_bd(1.0, 3).unwrap(), "BD 1.000");
}
#[test]
fn format_bd_renders_negative_with_pbd_prefix() {
assert_eq!(format_bd(-11125.154, 3).unwrap(), "PBD 11125.154");
assert_eq!(format_bd(-1.0, 0).unwrap(), "PBD 1");
}
#[test]
fn format_bd_rejects_non_finite() {
assert!(format_bd(f64::NAN, 3).is_err());
assert!(format_bd(f64::INFINITY, 3).is_err());
}
#[test]
fn parse_bd_parses_bd_labels() {
assert_eq!(parse_bd("BD 0").unwrap(), 0.0);
assert_eq!(parse_bd("BD 9622.504").unwrap(), 9622.504);
}
#[test]
fn parse_bd_parses_pbd_labels() {
assert_eq!(parse_bd("PBD 1").unwrap(), -1.0);
assert_eq!(parse_bd("PBD 11125.154").unwrap(), -11125.154);
}
#[test]
fn parse_bd_tolerates_whitespace() {
assert_eq!(parse_bd(" BD 1.0 ").unwrap(), 1.0);
assert_eq!(parse_bd("PBD 2.5").unwrap(), -2.5);
}
#[test]
fn parse_bd_rejects_pbd_zero() {
assert!(parse_bd("PBD 0").is_err());
assert!(parse_bd("PBD 0.0").is_err());
}
#[test]
fn parse_bd_rejects_negative_bodies() {
assert!(parse_bd("BD -1").is_err());
assert!(parse_bd("PBD -1").is_err());
}
#[test]
fn parse_bd_rejects_unrecognised_input() {
assert!(parse_bd("9622.504").is_err());
assert!(parse_bd("XBD 1").is_err());
assert!(parse_bd("").is_err());
}
#[test]
fn round_trip_format_then_parse() {
let cases = [0.0_f64, 1.0, 9622.504, -1.0, -11125.154, 1e6, -1e6];
for v in cases {
let label = format_bd(v, 9).unwrap();
let back = parse_bd(&label).unwrap();
assert!((back - v).abs() < 1e-9, "{v} -> {label} -> {back}");
}
}
#[test]
fn label_round_trip() {
let bd = BrightLabel::BD(9622.504);
let s = format_bd_label(bd, 3).unwrap();
assert_eq!(s, "BD 9622.504");
assert_eq!(parse_bd_label(&s).unwrap(), bd);
let pbd = BrightLabel::PBD(11125.154);
let s = format_bd_label(pbd, 3).unwrap();
assert_eq!(s, "PBD 11125.154");
assert_eq!(parse_bd_label(&s).unwrap(), pbd);
}
#[test]
fn label_format_rejects_invalid_combinations() {
assert!(format_bd_label(BrightLabel::BD(-1.0), 3).is_err());
assert!(format_bd_label(BrightLabel::PBD(0.0), 3).is_err());
assert!(format_bd_label(BrightLabel::PBD(-1.0), 3).is_err());
}
#[test]
fn parse_bd_label_returns_bd_for_zero() {
assert_eq!(parse_bd_label("BD 0").unwrap(), BrightLabel::BD(0.0));
}
#[test]
fn compare_bd_orders_correctly() {
use std::cmp::Ordering;
let bd0 = BrightLabel::BD(0.0);
let bd9k = BrightLabel::BD(9622.504);
let pbd1 = BrightLabel::PBD(1.0);
let pbd11k = BrightLabel::PBD(11125.154);
assert_eq!(compare_bd_labels(bd0, pbd1), Ordering::Greater);
assert_eq!(compare_bd_labels(pbd11k, bd0), Ordering::Less);
assert_eq!(compare_bd_labels(bd9k, bd0), Ordering::Greater);
assert_eq!(compare_bd_labels(pbd1, pbd11k), Ordering::Greater);
assert_eq!(compare_bd_labels(bd9k, BrightLabel::BD(9622.504)), Ordering::Equal);
}
#[test]
fn compare_bd_agrees_with_native_numeric_comparison() {
let labels = [
(0.0_f64, BrightLabel::BD(0.0)),
(9622.504, BrightLabel::BD(9622.504)),
(-1.0, BrightLabel::PBD(1.0)),
(-11125.154, BrightLabel::PBD(11125.154)),
];
for (sa, la) in labels.iter() {
for (sb, lb) in labels.iter() {
let label_ord = compare_bd_labels(*la, *lb);
let scalar_ord = sa.partial_cmp(sb).unwrap();
assert_eq!(
label_ord, scalar_ord,
"mismatch comparing {sa} vs {sb}"
);
}
}
}
}