use std::fmt;
use std::ops::Deref;
use std::str::FromStr;
use rust_decimal::Decimal;
use rust_decimal::Error as DecimalError;
use rust_decimal::prelude::ToPrimitive;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use specta::datatype::reference::Reference;
use specta::{DataType, Generics, Type, TypeCollection};
pub fn decimal_to_scaled_i64(value: Decimal, scale: i64) -> Option<i64> {
let scaled = value * Decimal::from(scale);
scaled.to_i64()
}
pub fn decimal_to_scaled_u64(value: Decimal, scale: i64) -> Option<u64> {
let scaled = value * Decimal::from(scale);
scaled.to_u64()
}
pub fn f64_to_scaled_u64(value: f64, scale: i64) -> u64 {
if !value.is_finite() || value <= 0.0 {
return 0;
}
let scaled = value * scale as f64;
if !scaled.is_finite() || scaled <= 0.0 {
return 0;
}
scaled as u64
}
pub fn decimal_abs_to_scaled_u64(value: Decimal, scale: i64) -> u64 {
let scaled = value.abs() * Decimal::from(scale);
scaled.to_u64().unwrap_or(0)
}
pub fn safe_decimal_from_f64(val: f64) -> Option<Decimal> {
if val.is_finite() {
Decimal::from_f64_retain(val)
} else {
None
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct SpectaDecimal(#[schemars(with = "String")] pub Decimal);
impl Type for SpectaDecimal {
fn inline(type_map: &mut TypeCollection, generics: Generics<'_>) -> DataType {
String::inline(type_map, generics)
}
fn reference(type_map: &mut TypeCollection, generics: &[DataType]) -> Reference {
String::reference(type_map, generics)
}
}
impl Deref for SpectaDecimal {
type Target = Decimal;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<Decimal> for SpectaDecimal {
fn from(decimal: Decimal) -> Self {
Self(decimal)
}
}
impl From<SpectaDecimal> for Decimal {
fn from(wrapper: SpectaDecimal) -> Self {
wrapper.0
}
}
impl fmt::Display for SpectaDecimal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for SpectaDecimal {
type Err = DecimalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Decimal::from_str(s).map(Self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::Decimal;
#[test]
fn decimal_to_scaled_i64_positive_value() {
let value = Decimal::new(123_456_789, 4); assert_eq!(decimal_to_scaled_i64(value, 100), Some(1_234_567));
}
#[test]
fn decimal_to_scaled_i64_negative_value() {
let value = Decimal::new(-123_456_789, 4); assert_eq!(decimal_to_scaled_i64(value, 100), Some(-1_234_567));
}
#[test]
fn decimal_to_scaled_i64_zero() {
assert_eq!(decimal_to_scaled_i64(Decimal::ZERO, 1_000), Some(0));
}
#[test]
fn decimal_to_scaled_i64_truncates_fractional_remainder() {
let value = Decimal::new(12345, 4); assert_eq!(decimal_to_scaled_i64(value, 100), Some(123));
}
#[test]
fn decimal_to_scaled_i64_overflow_returns_none() {
let huge = Decimal::new(i64::MAX, 0);
assert_eq!(decimal_to_scaled_i64(huge, 1_000_000), None);
}
#[test]
fn decimal_to_scaled_i64_negative_overflow_returns_none() {
let huge = Decimal::new(i64::MIN, 0);
assert_eq!(decimal_to_scaled_i64(huge, 1_000_000), None);
}
#[test]
fn decimal_to_scaled_u64_positive_value() {
let value = Decimal::new(123_456_789, 4); assert_eq!(decimal_to_scaled_u64(value, 100), Some(1_234_567));
}
#[test]
fn decimal_to_scaled_u64_zero() {
assert_eq!(decimal_to_scaled_u64(Decimal::ZERO, 1_000), Some(0));
}
#[test]
fn decimal_to_scaled_u64_negative_returns_none() {
let value = Decimal::new(-1, 0);
assert_eq!(decimal_to_scaled_u64(value, 1), None);
}
#[test]
fn decimal_to_scaled_u64_overflow_returns_none() {
let huge = Decimal::new(i64::MAX, 0);
assert_eq!(decimal_to_scaled_u64(huge, 1_000_000), None);
}
#[test]
fn f64_to_scaled_u64_positive_finite() {
assert_eq!(f64_to_scaled_u64(1.5, 100), 150);
}
#[test]
fn f64_to_scaled_u64_zero_returns_zero() {
assert_eq!(f64_to_scaled_u64(0.0, 100), 0);
}
#[test]
fn f64_to_scaled_u64_negative_returns_zero() {
assert_eq!(f64_to_scaled_u64(-1.5, 100), 0);
}
#[test]
fn f64_to_scaled_u64_nan_returns_zero() {
assert_eq!(f64_to_scaled_u64(f64::NAN, 100), 0);
}
#[test]
fn f64_to_scaled_u64_infinity_returns_zero() {
assert_eq!(f64_to_scaled_u64(f64::INFINITY, 100), 0);
}
#[test]
fn f64_to_scaled_u64_neg_infinity_returns_zero() {
assert_eq!(f64_to_scaled_u64(f64::NEG_INFINITY, 100), 0);
}
#[test]
fn f64_to_scaled_u64_scaled_overflow_returns_zero() {
assert_eq!(f64_to_scaled_u64(f64::MAX, 1_000_000), 0);
}
#[test]
fn f64_to_scaled_u64_scaled_to_negative_via_overflow_returns_zero() {
assert_eq!(f64_to_scaled_u64(-f64::MAX, 1_000_000), 0);
}
#[test]
fn decimal_abs_to_scaled_u64_positive() {
let value = Decimal::new(123_456_789, 4); assert_eq!(decimal_abs_to_scaled_u64(value, 100), 1_234_567);
}
#[test]
fn decimal_abs_to_scaled_u64_negative_takes_absolute_value() {
let value = Decimal::new(-123_456_789, 4); assert_eq!(decimal_abs_to_scaled_u64(value, 100), 1_234_567);
}
#[test]
fn decimal_abs_to_scaled_u64_zero() {
assert_eq!(decimal_abs_to_scaled_u64(Decimal::ZERO, 1_000), 0);
}
#[test]
fn decimal_abs_to_scaled_u64_overflow_returns_zero() {
let huge = Decimal::new(i64::MAX, 0);
assert_eq!(decimal_abs_to_scaled_u64(huge, 1_000_000), 0);
}
#[test]
fn decimal_abs_to_scaled_u64_negative_overflow_returns_zero() {
let huge = Decimal::new(i64::MIN, 0);
assert_eq!(decimal_abs_to_scaled_u64(huge, 1_000_000), 0);
}
#[test]
fn safe_decimal_from_f64_finite_value() {
let result = safe_decimal_from_f64(1.5);
assert_eq!(result, Some(Decimal::new(15, 1)));
}
#[test]
fn safe_decimal_from_f64_negative_finite_value() {
let result = safe_decimal_from_f64(-2.25);
assert_eq!(result, Some(Decimal::new(-225, 2)));
}
#[test]
fn safe_decimal_from_f64_zero() {
assert_eq!(safe_decimal_from_f64(0.0), Some(Decimal::ZERO));
}
#[test]
fn safe_decimal_from_f64_nan_returns_none() {
assert_eq!(safe_decimal_from_f64(f64::NAN), None);
}
#[test]
fn safe_decimal_from_f64_infinity_returns_none() {
assert_eq!(safe_decimal_from_f64(f64::INFINITY), None);
}
#[test]
fn safe_decimal_from_f64_neg_infinity_returns_none() {
assert_eq!(safe_decimal_from_f64(f64::NEG_INFINITY), None);
}
#[test]
fn specta_decimal_from_str_valid() {
let parsed: SpectaDecimal = "3.14".parse().unwrap();
assert_eq!(parsed.0, Decimal::new(314, 2));
}
#[test]
fn specta_decimal_from_str_invalid() {
assert!("not-a-number".parse::<SpectaDecimal>().is_err());
}
#[test]
fn specta_decimal_from_and_into_decimal_roundtrip() {
let d = Decimal::new(42, 0);
let wrapped: SpectaDecimal = d.into();
let back: Decimal = wrapped.into();
assert_eq!(back, d);
}
#[test]
fn specta_decimal_display_matches_inner() {
let d = Decimal::new(123, 2); let wrapped = SpectaDecimal(d);
assert_eq!(format!("{wrapped}"), format!("{d}"));
}
#[test]
fn specta_decimal_deref_exposes_inner() {
let d = Decimal::new(7, 0);
let wrapped = SpectaDecimal(d);
assert_eq!(*wrapped, d);
}
#[test]
fn decimal_scaled_conversions_accept_zero_and_negative_scales() {
let value = Decimal::new(125, 2);
assert_eq!(decimal_to_scaled_i64(value, 0), Some(0));
assert_eq!(decimal_to_scaled_i64(value, -10), Some(-12));
assert_eq!(decimal_to_scaled_u64(value, 0), Some(0));
assert_eq!(decimal_to_scaled_u64(value, -10), None);
}
#[test]
fn decimal_to_scaled_u64_truncates_fractional_remainder() {
assert_eq!(decimal_to_scaled_u64(Decimal::new(12345, 4), 100), Some(123));
}
#[test]
fn decimal_scaled_conversions_accept_exact_integer_boundaries() {
assert_eq!(decimal_to_scaled_i64(Decimal::from(i64::MAX), 1), Some(i64::MAX));
assert_eq!(decimal_to_scaled_i64(Decimal::from(i64::MIN), 1), Some(i64::MIN));
assert_eq!(decimal_to_scaled_u64(Decimal::from(u64::MAX), 1), Some(u64::MAX));
}
#[test]
#[should_panic]
fn decimal_scaled_multiplication_overflow_panics() {
let _ = decimal_to_scaled_i64(Decimal::MAX, i64::MAX);
}
#[test]
fn f64_scaled_conversion_saturates_finite_values_and_truncates_subunits() {
assert_eq!(f64_to_scaled_u64(f64::MAX, 1), u64::MAX);
assert_eq!(f64_to_scaled_u64(0.9, 1), 0);
assert_eq!(f64_to_scaled_u64(-0.0, 10), 0);
}
#[test]
fn safe_decimal_from_f64_rejects_finite_out_of_range_and_retains_precision() {
assert_eq!(safe_decimal_from_f64(f64::MAX), None);
let retained = safe_decimal_from_f64(0.1).unwrap();
assert_eq!(retained, Decimal::from_f64_retain(0.1).unwrap());
assert_ne!(retained, Decimal::new(1, 1));
}
#[test]
fn specta_decimal_serde_serializes_as_string_and_roundtrips_values() {
let wrapped = SpectaDecimal(Decimal::new(314, 2));
let json = serde_json::to_string(&wrapped).unwrap();
assert_eq!(json, "\"3.14\"");
assert_eq!(serde_json::from_str::<SpectaDecimal>(&json).unwrap(), wrapped);
assert_eq!(serde_json::from_str::<SpectaDecimal>("3.14").unwrap(), wrapped);
assert!(serde_json::from_str::<SpectaDecimal>("\"not-a-decimal\"").is_err());
assert!(serde_json::from_str::<SpectaDecimal>("null").is_err());
assert!(serde_json::from_str::<SpectaDecimal>("{}").is_err());
}
#[test]
fn specta_decimal_declares_string_schema_and_type() {
let schema = schemars::schema_for!(SpectaDecimal);
assert_eq!(schema.as_value()["type"], "string");
let projected = SpectaDecimal::inline(&mut TypeCollection::default(), Generics::NONE);
let string_type = String::inline(&mut TypeCollection::default(), Generics::NONE);
assert_eq!(projected, string_type);
}
}