use bigdecimal::{BigDecimal, ToPrimitive};
use serde_json::Value;
const F64_SAFE_DIGITS: u64 = 15;
pub(crate) fn bigdecimal_to_json(value: &BigDecimal) -> Value {
let normalized = value.normalized();
if normalized.is_integer()
&& let Some(as_i64) = normalized.to_i64()
{
return Value::from(as_i64);
}
let scale = normalized.fractional_digit_count();
let canonical_digits = normalized.digits() + scale.min(0).unsigned_abs();
if canonical_digits <= F64_SAFE_DIGITS
&& let Some(as_f64) = normalized.to_f64()
&& as_f64 != 0.0
&& let Some(num) = serde_json::Number::from_f64(as_f64)
{
return Value::from(num);
}
Value::String(normalized.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn dec(s: &str) -> BigDecimal {
BigDecimal::from_str(s).expect("valid decimal literal")
}
#[test]
fn small_fixed_point_is_number() {
assert_eq!(bigdecimal_to_json(&dec("0.5")), Value::from(0.5));
assert_eq!(bigdecimal_to_json(&dec("42")), Value::from(42));
assert_eq!(bigdecimal_to_json(&dec("-1.25")), Value::from(-1.25));
}
#[test]
fn trailing_zeros_normalize_consistently() {
let a = bigdecimal_to_json(&dec("1.20"));
let b = bigdecimal_to_json(&dec("1.2"));
assert_eq!(a, b);
}
#[test]
fn value_beyond_f64_precision_is_string() {
let v = bigdecimal_to_json(&dec("12345678901234567890.1234567890"));
assert_eq!(v, Value::String("12345678901234567890.123456789".to_string()));
}
#[test]
fn small_decimal_with_many_significant_digits_is_string() {
let v = bigdecimal_to_json(&dec("0.123456789012345678901234567890"));
let Value::String(s) = v else {
panic!("expected string for high-precision small decimal");
};
assert!(s.starts_with("0.12345678901234567890"));
}
#[test]
fn shape_is_deterministic_per_value() {
let v1 = bigdecimal_to_json(&dec("99999999999999999999.99"));
let v2 = bigdecimal_to_json(&dec("99999999999999999999.99"));
assert_eq!(v1, v2);
assert!(matches!(v1, Value::String(_)));
}
#[test]
fn integer_value_uses_integer_branch_regardless_of_digit_count() {
let v = bigdecimal_to_json(&dec(&i64::MAX.to_string()));
assert_eq!(v, Value::Number(i64::MAX.into()));
}
#[test]
fn boundary_15_digit_fraction_is_number() {
let v = bigdecimal_to_json(&dec("12345678901234.5"));
assert!(matches!(v, Value::Number(_)));
}
#[test]
fn boundary_16_digit_fraction_is_string() {
let v = bigdecimal_to_json(&dec("12345678901234.56"));
assert!(matches!(v, Value::String(_)));
}
#[test]
fn small_fraction_with_few_digits_is_number() {
assert_eq!(bigdecimal_to_json(&dec("0.1")), Value::from(0.1));
assert_eq!(bigdecimal_to_json(&dec("0.10")), Value::from(0.1));
assert_eq!(bigdecimal_to_json(&dec("0.30")), Value::from(0.3));
}
#[test]
fn high_precision_fraction_is_string() {
let v = bigdecimal_to_json(&dec("0.123456789012345678"));
assert!(matches!(v, Value::String(_)));
}
#[test]
fn huge_magnitude_with_few_significant_digits_is_string() {
let v = bigdecimal_to_json(&dec("1000000000000000000000000000000"));
assert!(matches!(v, Value::String(_)));
}
#[test]
fn zero_is_integer_zero() {
assert_eq!(bigdecimal_to_json(&dec("0")), Value::from(0));
assert_eq!(bigdecimal_to_json(&dec("0.0")), Value::from(0));
assert_eq!(bigdecimal_to_json(&dec("0.0000")), Value::from(0));
}
#[test]
fn negative_zero_normalizes_to_zero() {
let v = bigdecimal_to_json(&dec("-0"));
assert_eq!(v, Value::from(0));
let v = bigdecimal_to_json(&dec("-0.000"));
assert_eq!(v, Value::from(0));
}
#[test]
fn i64_min_uses_integer_branch() {
let v = bigdecimal_to_json(&dec(&i64::MIN.to_string()));
assert_eq!(v, Value::Number(i64::MIN.into()));
}
#[test]
fn integer_one_past_i64_max_is_string() {
let v = bigdecimal_to_json(&dec("9223372036854775808"));
assert_eq!(v, Value::String("9223372036854775808".to_string()));
}
#[test]
fn very_large_integer_is_string() {
let v = bigdecimal_to_json(&dec("1234567890123456789012345"));
let Value::String(s) = v else {
panic!("expected string for huge integer");
};
assert_eq!(s, "1234567890123456789012345");
}
#[test]
fn tiny_fraction_within_digit_budget_is_number() {
let v = bigdecimal_to_json(&dec("1e-30"));
let Value::Number(n) = v else {
panic!("expected JSON number for tiny fraction");
};
let f = n.as_f64().expect("f64 representable");
assert!((f - 1e-30).abs() < 1e-40);
}
#[test]
fn integer_15_digits_uses_integer_branch() {
let v = bigdecimal_to_json(&dec("123456789012345"));
assert_eq!(v, Value::Number(123_456_789_012_345_i64.into()));
}
#[test]
fn f64_underflow_falls_back_to_string() {
let v = bigdecimal_to_json(&dec("1e-1000"));
let Value::String(s) = v else {
panic!("expected string for f64-underflow tiny fraction, got {v:?}");
};
let lower = s.to_ascii_lowercase();
assert!(
lower.contains("e-1000") || lower.starts_with("0.0"),
"must preserve magnitude: {s}"
);
}
#[test]
fn negative_f64_underflow_falls_back_to_string() {
let v = bigdecimal_to_json(&dec("-1e-500"));
let Value::String(s) = v else {
panic!("expected string for negative-tiny underflow, got {v:?}");
};
assert!(s.starts_with('-'), "negative sign preserved: {s}");
}
#[test]
fn near_underflow_within_f64_range_is_number() {
let v = bigdecimal_to_json(&dec("1e-300"));
let Value::Number(n) = v else {
panic!("1e-300 must emit as JSON number, got {v:?}");
};
let f = n.as_f64().expect("number is f64");
assert!((f / 1e-300 - 1.0).abs() < 1e-10, "round-trip preserved: {f}");
}
#[test]
fn non_integer_with_negative_scale_is_handled() {
let v = bigdecimal_to_json(&dec("1.23e5"));
assert_eq!(v, Value::from(123_000));
}
}