use rust_decimal::{Decimal, MathematicalOps};
use std::collections::{BTreeMap, HashMap, HashSet};
pub const DEFAULT_CURRENCY: &str = "__default__";
#[derive(Debug, Clone, Default)]
struct Distribution {
hist: BTreeMap<u32, u32>,
}
impl Distribution {
fn update(&mut self, dp: u32) {
*self.hist.entry(dp).or_insert(0) += 1;
}
fn merge(&mut self, other: &Self) {
for (&dp, &count) in &other.hist {
*self.hist.entry(dp).or_insert(0) += count;
}
}
fn max(&self) -> Option<u32> {
self.hist.keys().next_back().copied()
}
fn mode(&self) -> Option<u32> {
let mut best: Option<(u32, u32)> = None; for (&dp, &count) in &self.hist {
if best.is_none_or(|(c, _)| count >= c) {
best = Some((count, dp));
}
}
best.map(|(_, dp)| dp)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Precision {
#[default]
MostCommon,
Maximum,
}
#[derive(Debug, Clone, Default)]
pub struct DisplayContext {
distributions: HashMap<String, Distribution>,
render_commas: bool,
fixed_precisions: HashMap<String, u32>,
precision: Precision,
}
impl DisplayContext {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn update(&mut self, number: Decimal, currency: &str) {
let dp = Self::decimal_precision(number);
self.distributions
.entry(currency.to_string())
.or_default()
.update(dp);
}
pub fn update_from(&mut self, other: &Self) {
for (currency, dist) in &other.distributions {
self.distributions
.entry(currency.clone())
.or_default()
.merge(dist);
}
for (currency, precision) in &other.fixed_precisions {
self.fixed_precisions
.entry(currency.clone())
.or_insert(*precision);
}
if other.render_commas {
self.render_commas = true;
}
}
pub const fn set_precision(&mut self, precision: Precision) {
self.precision = precision;
}
#[must_use]
pub const fn precision(&self) -> Precision {
self.precision
}
pub fn currencies(&self) -> impl Iterator<Item = &str> {
let mut seen: HashSet<&str> = HashSet::new();
let mut out: Vec<&str> = Vec::new();
for currency in self
.distributions
.keys()
.chain(self.fixed_precisions.keys())
.map(String::as_str)
{
if currency != DEFAULT_CURRENCY && seen.insert(currency) {
out.push(currency);
}
}
out.sort_unstable();
out.into_iter()
}
#[must_use]
pub fn histogram(&self, currency: &str) -> Vec<(u32, u32)> {
self.distributions.get(currency).map_or_else(Vec::new, |d| {
d.hist.iter().map(|(&dp, &c)| (dp, c)).collect()
})
}
#[must_use]
pub fn precision_under(&self, currency: &str, policy: Precision) -> Option<u32> {
if let Some(&fixed) = self.fixed_precisions.get(currency) {
return Some(fixed);
}
let dist = self.distributions.get(currency)?;
match policy {
Precision::MostCommon => dist.mode(),
Precision::Maximum => dist.max(),
}
}
#[must_use]
pub fn has_fixed_precision(&self, currency: &str) -> bool {
self.fixed_precisions.contains_key(currency)
}
pub const fn set_render_commas(&mut self, render_commas: bool) {
self.render_commas = render_commas;
}
#[must_use]
pub const fn render_commas(&self) -> bool {
self.render_commas
}
pub fn set_fixed_precision(&mut self, currency: &str, precision: u32) {
self.fixed_precisions
.insert(currency.to_string(), precision);
}
#[must_use]
pub fn get_precision(&self, currency: &str) -> Option<u32> {
if let Some(&precision) = self.fixed_precisions.get(currency) {
return Some(precision);
}
let dist = self.distributions.get(currency)?;
match self.precision {
Precision::MostCommon => dist.mode(),
Precision::Maximum => dist.max(),
}
}
#[must_use]
pub fn default_precision(&self) -> u32 {
if let Some(dp) = self.get_precision(DEFAULT_CURRENCY) {
return dp;
}
let mut max_dp: u32 = 0;
let mut seen: HashSet<&str> = HashSet::new();
for currency in self
.fixed_precisions
.keys()
.chain(self.distributions.keys())
.map(String::as_str)
{
if seen.insert(currency)
&& currency != DEFAULT_CURRENCY
&& let Some(dp) = self.get_precision(currency)
{
max_dp = max_dp.max(dp);
}
}
max_dp
}
#[must_use]
pub fn quantize(&self, number: Decimal, currency: &str) -> Decimal {
if let Some(dp) = self.get_precision(currency) {
let mut rounded = number.round_dp(dp);
rounded.rescale(dp);
rounded
} else {
number
}
}
#[must_use]
pub fn format(&self, number: Decimal, currency: &str) -> String {
let precision = self.get_precision(currency);
if let Some(dp) = precision {
let effective_dp = number.scale().max(dp);
let rounded = number.round_dp(effective_dp);
let formatted = format!("{rounded}");
let formatted = Self::ensure_decimal_places(&formatted, effective_dp);
if self.render_commas {
Self::add_commas(&formatted)
} else {
formatted
}
} else {
let formatted = number.normalize().to_string();
if self.render_commas {
Self::add_commas(&formatted)
} else {
formatted
}
}
}
#[must_use]
pub fn format_amount(&self, number: Decimal, currency: &str) -> String {
format!("{} {}", self.format_quantized(number, currency), currency)
}
#[must_use]
pub fn format_amount_number(&self, number: Decimal, currency: &str) -> String {
self.format_quantized(number, currency)
}
fn format_quantized(&self, number: Decimal, currency: &str) -> String {
let raw = match self.get_precision(currency) {
Some(dp) => {
let mut rounded = number.round_dp(dp);
rounded.rescale(dp);
rounded.to_string()
}
None => number.normalize().to_string(),
};
if self.render_commas {
Self::add_commas(&raw)
} else {
raw
}
}
#[must_use]
pub fn format_default(&self, number: Decimal) -> String {
const PYTHON_DECIMAL_PRECISION: u32 = 28;
let capped = Self::cap_significant_digits(number, PYTHON_DECIMAL_PRECISION);
let formatted = capped.to_string();
if self.render_commas {
Self::add_commas(&formatted)
} else {
formatted
}
}
fn cap_significant_digits(number: Decimal, max_sig: u32) -> Decimal {
let mantissa_abs = number.mantissa().unsigned_abs();
let digits = mantissa_abs.checked_ilog10().map_or(0, |x| x + 1);
if digits <= max_sig {
return number;
}
let excess = digits - max_sig;
if excess <= number.scale() {
return number.round_dp_with_strategy(
number.scale() - excess,
rust_decimal::RoundingStrategy::MidpointNearestEven,
);
}
let integer_excess = excess - number.scale();
let Some(factor) = Decimal::TEN.checked_powu(u64::from(integer_excess)) else {
return number;
};
let lifted = number / factor;
let rounded =
lifted.round_dp_with_strategy(0, rust_decimal::RoundingStrategy::MidpointNearestEven);
rounded * factor
}
const fn decimal_precision(number: Decimal) -> u32 {
number.scale()
}
fn ensure_decimal_places(s: &str, dp: u32) -> String {
if dp == 0 {
return s.split('.').next().unwrap_or(s).to_string();
}
let dp = dp as usize;
if let Some(dot_pos) = s.find('.') {
let current_decimals = s.len() - dot_pos - 1;
if current_decimals >= dp {
s.to_string()
} else {
let zeros_needed = dp - current_decimals;
format!("{s}{}", "0".repeat(zeros_needed))
}
} else {
format!("{s}.{}", "0".repeat(dp))
}
}
fn add_commas(s: &str) -> String {
let (integer_part, decimal_part) = match s.find('.') {
Some(pos) => (&s[..pos], Some(&s[pos..])),
None => (s, None),
};
let (sign, digits) = if let Some(stripped) = integer_part.strip_prefix('-') {
("-", stripped)
} else {
("", integer_part)
};
let mut result = String::with_capacity(digits.len() + digits.len() / 3);
for (i, c) in digits.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
result.push(',');
}
result.push(c);
}
let integer_with_commas: String = result.chars().rev().collect();
match decimal_part {
Some(dec) => format!("{sign}{integer_with_commas}{dec}"),
None => format!("{sign}{integer_with_commas}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_update_and_get_precision_most_common_default() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(100), "USD");
assert_eq!(ctx.get_precision("USD"), Some(0));
ctx.update(dec!(50.25), "USD");
assert_eq!(ctx.get_precision("USD"), Some(2));
ctx.update(dec!(1), "USD");
assert_eq!(ctx.get_precision("USD"), Some(0));
assert_eq!(ctx.get_precision("EUR"), None);
}
#[test]
fn test_update_and_get_precision_maximum_policy() {
let mut ctx = DisplayContext::new();
ctx.set_precision(Precision::Maximum);
ctx.update(dec!(100), "USD");
assert_eq!(ctx.get_precision("USD"), Some(0));
ctx.update(dec!(50.25), "USD");
assert_eq!(ctx.get_precision("USD"), Some(2));
ctx.update(dec!(1), "USD");
assert_eq!(ctx.get_precision("USD"), Some(2));
}
#[test]
fn test_default_precision_prefers_default_bucket_over_max_of_modes() {
let mut ctx = DisplayContext::new();
for _ in 0..5 {
ctx.update(dec!(1.23), "USD");
}
for _ in 0..5 {
ctx.update(dec!(1.234), "VBMPX");
}
assert_eq!(ctx.default_precision(), 3);
ctx.update(dec!(128.99), DEFAULT_CURRENCY);
ctx.update(dec!(131.73), DEFAULT_CURRENCY);
assert_eq!(ctx.default_precision(), 2);
}
#[test]
fn test_format_default_integer_column_stays_integer() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(1.23), "USD"); for n in [dec!(100), dec!(5), dec!(42)] {
ctx.update(n, DEFAULT_CURRENCY);
}
assert_eq!(ctx.format_default(dec!(100)), "100");
assert_eq!(ctx.format_default(dec!(5)), "5");
assert_eq!(ctx.format_default(dec!(7.5)), "7.5");
}
#[test]
fn test_default_precision_falls_back_when_default_bucket_empty() {
let mut ctx = DisplayContext::new();
for _ in 0..5 {
ctx.update(dec!(1.23), "USD");
}
assert_eq!(ctx.default_precision(), 2);
}
#[test]
fn test_currencies_skips_default_sentinel() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(1.23), "USD");
ctx.update(dec!(0.5), "EUR");
ctx.update(dec!(100), DEFAULT_CURRENCY); let cs: Vec<&str> = ctx.currencies().collect();
assert_eq!(cs, vec!["EUR", "USD"]); }
#[test]
fn test_currencies_includes_fixed_only_currencies() {
let mut ctx = DisplayContext::new();
ctx.set_fixed_precision("BTC", 8);
let cs: Vec<&str> = ctx.currencies().collect();
assert_eq!(cs, vec!["BTC"]);
}
#[test]
fn test_histogram_returns_ascending_pairs() {
let mut ctx = DisplayContext::new();
for _ in 0..5 {
ctx.update(dec!(1.23), "USD"); }
for _ in 0..2 {
ctx.update(dec!(1.234), "USD"); }
ctx.update(dec!(100), "USD"); let h = ctx.histogram("USD");
assert_eq!(h, vec![(0, 1), (2, 5), (3, 2)]);
}
#[test]
fn test_histogram_empty_for_unknown_currency() {
let ctx = DisplayContext::new();
assert!(ctx.histogram("XYZ").is_empty());
}
#[test]
fn test_precision_under_does_not_mutate_active_policy() {
let mut ctx = DisplayContext::new();
for _ in 0..5 {
ctx.update(dec!(100), "USD");
}
ctx.update(dec!(1.234), "USD");
assert_eq!(ctx.get_precision("USD"), Some(0));
assert_eq!(ctx.precision_under("USD", Precision::Maximum), Some(3));
assert_eq!(ctx.precision(), Precision::MostCommon);
assert_eq!(ctx.get_precision("USD"), Some(0));
}
#[test]
fn test_precision_under_returns_zero_when_fixed_is_zero() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(1.234), "JPY"); ctx.set_fixed_precision("JPY", 0); assert_eq!(ctx.precision_under("JPY", Precision::MostCommon), Some(0));
assert_eq!(ctx.precision_under("JPY", Precision::Maximum), Some(0));
assert_eq!(ctx.get_precision("JPY"), Some(0));
}
#[test]
fn test_precision_under_respects_fixed_override() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(1.234), "USD");
ctx.set_fixed_precision("USD", 2);
assert_eq!(ctx.precision_under("USD", Precision::MostCommon), Some(2));
assert_eq!(ctx.precision_under("USD", Precision::Maximum), Some(2));
}
#[test]
fn test_has_fixed_precision() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(1.23), "USD");
assert!(!ctx.has_fixed_precision("USD"));
ctx.set_fixed_precision("USD", 2);
assert!(ctx.has_fixed_precision("USD"));
}
#[test]
fn test_quantize_pads_scale_upward() {
let mut ctx = DisplayContext::new();
for _ in 0..10 {
ctx.update(dec!(0.0400), "USD"); }
for _ in 0..3 {
ctx.update(dec!(150.67), "USD"); }
assert_eq!(ctx.get_precision("USD"), Some(4));
let q = ctx.quantize(dec!(150.67), "USD");
assert_eq!(q.scale(), 4);
assert_eq!(q.to_string(), "150.6700");
}
#[test]
fn test_format_with_precision() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(100), "USD");
ctx.update(dec!(50.25), "USD");
assert_eq!(ctx.format(dec!(100), "USD"), "100.00");
assert_eq!(ctx.format(dec!(50.25), "USD"), "50.25");
assert_eq!(ctx.format(dec!(7.5), "USD"), "7.50");
}
#[test]
fn test_format_preserves_value_scale_above_tracked_precision() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(100.00), "USD");
ctx.update(dec!(50.25), "USD");
assert_eq!(ctx.get_precision("USD"), Some(2));
assert_eq!(ctx.format(dec!(1.234), "USD"), "1.234");
assert_eq!(ctx.format(dec!(-1202.00896), "USD"), "-1202.00896");
assert_eq!(ctx.format(dec!(0.00000), "USD"), "0.00000");
assert_eq!(ctx.format(dec!(7.5), "USD"), "7.50");
assert_eq!(ctx.format(dec!(0), "USD"), "0.00");
}
#[test]
fn test_format_vs_format_amount_split_semantics() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(100.00), "USD");
ctx.update(dec!(50.25), "USD");
assert_eq!(ctx.get_precision("USD"), Some(2));
assert_eq!(ctx.format(dec!(-1202.00896), "USD"), "-1202.00896");
assert_eq!(ctx.format(dec!(7.5), "USD"), "7.50");
assert_eq!(ctx.format_amount(dec!(-1202.00896), "USD"), "-1202.01 USD");
assert_eq!(ctx.format_amount(dec!(7.5), "USD"), "7.50 USD");
assert_eq!(
ctx.format_amount(dec!(170.16449234259784458309699376), "USD"),
"170.16 USD"
);
assert_eq!(
ctx.format_amount_number(dec!(-1202.00896), "USD"),
"-1202.01"
);
assert_eq!(ctx.format_amount_number(dec!(7.5), "USD"), "7.50");
}
#[test]
fn test_format_amount_untracked_currency_uses_natural_scale() {
let ctx = DisplayContext::new();
assert_eq!(ctx.format_amount(dec!(170.164), "USD"), "170.164 USD");
assert_eq!(ctx.format_amount(dec!(7.5), "USD"), "7.5 USD");
assert_eq!(ctx.format_amount(dec!(100), "USD"), "100 USD");
}
#[test]
fn test_format_unknown_currency() {
let ctx = DisplayContext::new();
assert_eq!(ctx.format(dec!(100), "EUR"), "100");
assert_eq!(ctx.format(dec!(50.25), "EUR"), "50.25");
}
#[test]
fn test_fixed_precision_override() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(100), "USD");
ctx.update(dec!(50.25), "USD");
assert_eq!(ctx.get_precision("USD"), Some(2));
ctx.set_fixed_precision("USD", 4);
assert_eq!(ctx.get_precision("USD"), Some(4));
assert_eq!(ctx.format(dec!(100), "USD"), "100.0000");
}
#[test]
fn test_mode_picks_most_common_dp() {
let mut ctx = DisplayContext::new();
for _ in 0..5 {
ctx.update(dec!(1.23), "USD"); }
for _ in 0..2 {
ctx.update(dec!(1.234), "USD"); }
assert_eq!(ctx.get_precision("USD"), Some(2));
}
#[test]
fn test_mode_tie_break_favors_larger_dp() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(1.23), "USD"); ctx.update(dec!(1.234), "USD"); ctx.update(dec!(1.2345), "USD"); assert_eq!(ctx.get_precision("USD"), Some(4));
}
#[test]
fn test_mode_outlier_does_not_dominate() {
let mut ctx = DisplayContext::new();
for _ in 0..5 {
ctx.update(dec!(100), "USD");
}
ctx.update(dec!(0.0000000000000000000000000001), "USD");
assert_eq!(ctx.get_precision("USD"), Some(0));
}
#[test]
fn test_switching_to_maximum_returns_max() {
let mut ctx = DisplayContext::new();
for _ in 0..5 {
ctx.update(dec!(100), "USD");
}
ctx.update(dec!(1.234567), "USD");
assert_eq!(ctx.get_precision("USD"), Some(0));
ctx.set_precision(Precision::Maximum);
assert_eq!(ctx.get_precision("USD"), Some(6));
ctx.set_precision(Precision::MostCommon);
assert_eq!(ctx.get_precision("USD"), Some(0));
}
#[test]
fn test_fixed_precision_overrides_both_policies() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(1.234), "USD");
ctx.set_fixed_precision("USD", 2);
assert_eq!(ctx.get_precision("USD"), Some(2));
ctx.set_precision(Precision::Maximum);
assert_eq!(ctx.get_precision("USD"), Some(2));
}
#[test]
fn test_update_from_merges_distributions_not_just_max() {
let mut a = DisplayContext::new();
for _ in 0..5 {
a.update(dec!(1.23), "USD"); }
let mut b = DisplayContext::new();
for _ in 0..10 {
b.update(dec!(1.234), "USD"); }
a.update_from(&b);
assert_eq!(a.get_precision("USD"), Some(3));
}
#[test]
fn test_update_from_is_not_idempotent_under_add_merge() {
let mut src = DisplayContext::new();
for _ in 0..10 {
src.update(dec!(1.23), "USD"); }
let mut dst1 = DisplayContext::new();
dst1.update_from(&src);
assert_eq!(dst1.histogram("USD"), vec![(2, 10)]);
let mut dst2 = DisplayContext::new();
dst2.update_from(&src);
dst2.update_from(&src);
assert_eq!(dst2.histogram("USD"), vec![(2, 20)]);
}
#[test]
fn test_update_from_does_not_propagate_precision_policy() {
let mut ledger = DisplayContext::new();
ledger.update(dec!(1.23), "USD");
let mut col = DisplayContext::new();
col.set_precision(Precision::Maximum);
col.update_from(&ledger);
assert_eq!(col.precision(), Precision::Maximum);
}
#[test]
fn test_render_commas() {
let mut ctx = DisplayContext::new();
ctx.set_render_commas(true);
ctx.update(dec!(1234567.89), "USD");
assert_eq!(ctx.format(dec!(1234567.89), "USD"), "1,234,567.89");
assert_eq!(ctx.format(dec!(1000), "USD"), "1,000.00");
}
#[test]
fn test_add_commas() {
assert_eq!(DisplayContext::add_commas("1234567"), "1,234,567");
assert_eq!(DisplayContext::add_commas("1234567.89"), "1,234,567.89");
assert_eq!(DisplayContext::add_commas("-1234567.89"), "-1,234,567.89");
assert_eq!(DisplayContext::add_commas("123"), "123");
assert_eq!(DisplayContext::add_commas("1"), "1");
}
#[test]
fn test_update_from() {
let mut ctx1 = DisplayContext::new();
ctx1.update(dec!(100), "USD");
let mut ctx2 = DisplayContext::new();
ctx2.update(dec!(50.25), "USD");
ctx2.update(dec!(1.5), "EUR");
ctx1.update_from(&ctx2);
assert_eq!(ctx1.get_precision("USD"), Some(2));
assert_eq!(ctx1.get_precision("EUR"), Some(1));
}
#[test]
fn test_update_from_propagates_fixed_precisions_and_render_commas() {
let mut ledger = DisplayContext::new();
ledger.update(dec!(1.234), "USD"); ledger.set_fixed_precision("USD", 2); ledger.set_fixed_precision("BTC", 8);
ledger.set_render_commas(true);
let mut col = DisplayContext::new();
col.update_from(&ledger);
assert_eq!(
col.distributions.get("USD").and_then(Distribution::mode),
Some(3)
);
assert_eq!(col.fixed_precisions.get("USD"), Some(&2));
assert_eq!(col.fixed_precisions.get("BTC"), Some(&8));
assert_eq!(col.get_precision("USD"), Some(2));
assert_eq!(col.get_precision("BTC"), Some(8));
assert!(col.render_commas);
}
#[test]
fn test_update_from_preserves_self_fixed_overrides() {
let mut ledger = DisplayContext::new();
ledger.set_fixed_precision("USD", 2);
let mut col = DisplayContext::new();
col.set_fixed_precision("USD", 4); col.update_from(&ledger);
assert_eq!(col.fixed_precisions.get("USD"), Some(&4));
}
#[test]
fn test_default_precision_respects_fixed_override_lower_than_inferred() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(1.2345), "USD"); ctx.set_fixed_precision("USD", 2);
assert_eq!(ctx.get_precision("USD"), Some(2));
assert_eq!(ctx.default_precision(), 2);
}
#[test]
fn test_default_precision_takes_max_across_currencies_with_overrides() {
let mut ctx = DisplayContext::new();
ctx.set_fixed_precision("USD", 2);
ctx.set_fixed_precision("EUR", 4);
assert_eq!(ctx.default_precision(), 4);
}
#[test]
fn test_format_amount() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(50.25), "USD");
assert_eq!(ctx.format_amount(dec!(100), "USD"), "100.00 USD");
}
#[test]
fn test_default_precision_picks_max_across_currencies() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(1.23), "USD"); ctx.update(dec!(1.2345), "EUR"); ctx.update(dec!(0.5), "GBP");
assert_eq!(ctx.default_precision(), 4);
}
#[test]
fn test_default_precision_includes_fixed_overrides() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(1.23), "USD");
ctx.set_fixed_precision("BTC", 8);
assert_eq!(ctx.default_precision(), 8);
}
#[test]
fn test_default_precision_empty_context_is_zero() {
let ctx = DisplayContext::new();
assert_eq!(ctx.default_precision(), 0);
}
#[test]
fn test_format_default_does_not_pad_scale_zero_to_column_precision() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(1.23), "USD");
ctx.update(dec!(1.2345), "EUR");
assert_eq!(ctx.format_default(dec!(0)), "0");
assert_eq!(ctx.format_default(dec!(100)), "100");
}
#[test]
fn test_format_default_preserves_natural_scale_for_overprecise_values() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(1.23), "USD");
assert_eq!(ctx.format_default(dec!(1.235)), "1.235");
}
#[test]
fn test_format_default_empty_context_natural() {
let ctx = DisplayContext::new();
assert_eq!(ctx.format_default(dec!(42)), "42");
assert_eq!(ctx.format_default(dec!(1.5)), "1.5");
}
#[test]
fn test_format_default_renders_commas() {
let mut ctx = DisplayContext::new();
ctx.update(dec!(1.23), "USD");
ctx.set_render_commas(true);
assert_eq!(ctx.format_default(dec!(1234567.89)), "1,234,567.89");
}
#[test]
fn test_format_default_caps_significant_digits_at_28() {
let ctx = DisplayContext::new();
let v = Decimal::from_str_exact("170.16449234259784458309699376").unwrap();
assert_eq!(v.scale(), 26, "test setup: input has scale 26");
assert_eq!(
ctx.format_default(v),
"170.1644923425978445830969938",
"should cap at 28 sig figs (3 integer + 25 fractional)"
);
}
#[test]
fn test_format_default_28_digit_or_fewer_passes_through_unchanged() {
let ctx = DisplayContext::new();
assert_eq!(ctx.format_default(dec!(170.16449)), "170.16449");
let v = Decimal::from_str_exact("1.234567890123456789012345678").unwrap();
assert_eq!(v.scale(), 27);
assert_eq!(
ctx.format_default(v),
"1.234567890123456789012345678",
"value at exactly 28 sig figs must pass through unchanged"
);
}
#[test]
fn test_format_default_cap_preserves_sign_and_integer_part() {
let ctx = DisplayContext::new();
let v = Decimal::from_str_exact("-1234.5678901234567890123456789").unwrap();
assert_eq!(
ctx.format_default(v),
"-1234.567890123456789012345679",
"negative + integer part preserved; fractional rounded half-even"
);
}
#[test]
fn test_format_default_caps_integer_only_excess() {
let ctx = DisplayContext::new();
let v = Decimal::from_str_exact("12345678901234567890123456789").unwrap();
assert_eq!(v.scale(), 0);
assert_eq!(
ctx.format_default(v),
"12345678901234567890123456790",
"29-digit integer must round to nearest 10 (28 sig figs), \
trailing 0 marks the rounded position"
);
}
#[test]
fn test_format_default_zero_preserves_intrinsic_scale() {
let ctx = DisplayContext::new();
assert_eq!(ctx.format_default(dec!(0)), "0", "Decimal(0) → \"0\"");
assert_eq!(
ctx.format_default(dec!(0.00)),
"0.00",
"Decimal(0.00) → \"0.00\" — the SUM-of-scale-2-zeros case from #954"
);
assert_eq!(
ctx.format_default(dec!(-0.0000)),
"0.0000",
"Decimal(-0.0000) — rust_decimal canonicalizes negative zero"
);
}
}