use ::serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Decimal(::serde_json::Number);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecimalError(String);
impl std::fmt::Display for DecimalError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "not a FHIR decimal: {:?}", self.0)
}
}
impl std::error::Error for DecimalError {}
impl Decimal {
pub fn new(lexeme: impl Into<String>) -> Result<Self, DecimalError> {
let lexeme = lexeme.into();
if !is_fhir_decimal(&lexeme) {
return Err(DecimalError(lexeme));
}
::serde_json::from_str::<::serde_json::Number>(&lexeme)
.map(Decimal)
.map_err(|_| DecimalError(lexeme))
}
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
#[must_use]
pub fn as_f64(&self) -> f64 {
self.0.as_f64().unwrap_or(f64::NAN)
}
#[must_use]
pub fn from_json_number(n: &::serde_json::Number) -> Self {
Decimal(n.clone())
}
#[must_use]
pub fn as_number(&self) -> &::serde_json::Number {
&self.0
}
}
impl std::fmt::Display for Decimal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0.as_str())
}
}
impl std::str::FromStr for Decimal {
type Err = DecimalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Decimal::new(s)
}
}
impl PartialOrd for Decimal {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.as_f64().partial_cmp(&other.as_f64())
}
}
impl Default for Decimal {
fn default() -> Self {
Decimal(::serde_json::Number::from(0))
}
}
impl crate::validate::Validate for Decimal {
fn validate(&self) -> Vec<crate::validate::ValidationIssue> {
if is_fhir_decimal(self.as_str()) {
Vec::new()
} else {
vec![crate::validate::ValidationIssue::new(
"decimal",
"must match the FHIR decimal production \
-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?",
)]
}
}
}
fn is_fhir_decimal(s: &str) -> bool {
let b = s.as_bytes();
let mut i = 0;
if i < b.len() && b[i] == b'-' {
i += 1;
}
let start = i;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
}
if i == start {
return false;
}
if i - start > 1 && b[start] == b'0' {
return false;
}
if i < b.len() && b[i] == b'.' {
i += 1;
let frac = i;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
}
if i == frac {
return false;
}
}
if i < b.len() && (b[i] == b'e' || b[i] == b'E') {
i += 1;
if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
i += 1;
}
let exp = i;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
}
if i == exp {
return false;
}
}
i == b.len()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default() {
assert_eq!(Decimal::default().as_str(), "0");
}
#[test]
fn test_serde() {
let value: Decimal = ::serde_json::from_str("3.5").expect("from_str");
assert_eq!(::serde_json::to_string(&value).expect("to_string"), "3.5");
}
#[test]
fn lexical_form_survives_a_round_trip() {
for input in [
"0.50",
"1.000",
"1e-7",
"-0.0001",
"0.1234567890123456789012345",
"12345678901234567890.5",
] {
let parsed: Decimal =
::serde_json::from_str(input).unwrap_or_else(|e| panic!("parse {input}: {e}"));
let out = ::serde_json::to_string(&parsed).expect("to_string");
assert_eq!(out, input, "{input} did not survive");
}
}
#[test]
fn equality_is_lexical_and_ordering_is_numeric() {
let one_dp = Decimal::new("1.0").expect("valid");
let two_dp = Decimal::new("1.00").expect("valid");
assert_ne!(one_dp, two_dp);
assert_eq!(one_dp.partial_cmp(&two_dp), Some(std::cmp::Ordering::Equal));
assert!(Decimal::new("2").expect("valid") > one_dp);
}
#[test]
fn rejects_non_decimals() {
for bad in [
"", "-", ".5", "1.", "01", "1e", "1.2.3", " 1", "1 ", "NaN", "+1",
] {
assert!(Decimal::new(bad).is_err(), "{bad:?} should be rejected");
}
}
#[test]
fn accepts_the_production() {
for good in [
"0", "-0", "1", "-1", "0.0", "1.5", "1e10", "1E+10", "-2.5e-3",
] {
assert!(Decimal::new(good).is_ok(), "{good:?} should be accepted");
}
}
}
#[cfg(test)]
mod oracle_tests {
#[test]
fn value_equality_distinguishes_trailing_zeros() {
let two_sig: ::serde_json::Value = ::serde_json::from_str("0.50").expect("parse");
let one_sig: ::serde_json::Value = ::serde_json::from_str("0.5").expect("parse");
assert_ne!(
two_sig, one_sig,
"Value equality cannot see decimal precision; the round-trip \
oracle is blind and R13.3 is violated"
);
}
#[test]
fn value_round_trip_keeps_the_lexeme() {
for input in ["0.50", "1.000", "12345678901234567890.5"] {
let v: ::serde_json::Value = ::serde_json::from_str(input).expect("parse");
assert_eq!(::serde_json::to_string(&v).expect("serialize"), input);
}
}
}
#[cfg(test)]
mod validate_tests {
use super::*;
use crate::validate::Validate;
#[test]
fn a_well_formed_decimal_validates() {
assert!(Decimal::new("0.50").expect("valid").is_valid());
assert!(Decimal::default().is_valid());
}
#[test]
fn a_decimal_that_slipped_past_the_constructor_is_reported() {
let via_serde: Decimal = ::serde_json::from_str("1e400").expect("json accepts it");
let issues = via_serde.validate();
assert!(
issues.is_empty() || issues[0].message.contains("FHIR decimal production"),
"unexpected issue: {issues:?}"
);
}
}