From 3c026aea4040dc5ca8e3d80588fbb2a2a35931cb Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 18 Feb 2026 09:45:44 -0600 Subject: [PATCH] Add float formatting functions and update serializer for improved precision - Introduced `trim_float`, `format_f32`, and `format_f64` functions to handle float formatting, ensuring consistent representation of numeric values. - Updated the `serialize_f32` and `serialize_f64` methods in `CompactJson5Serializer` and `Json5ValueSerializer` to utilize the new formatting functions, enhancing output accuracy. - Adjusted the handling of float values in `UnresolvedValue` to improve data consistency and precision during serialization. - Added unit tests for the new formatting functions to validate their behavior and ensure correctness. These changes enhance the precision of float serialization in JSON5, addressing previous inconsistencies and improving overall data integrity. --- src/json.rs | 102 ++++++++++++++++++++++++++---------------- src/resolution.rs | 110 ++++++++++++---------------------------------- 2 files changed, 92 insertions(+), 120 deletions(-) diff --git a/src/json.rs b/src/json.rs index 8303c91e..dadb4f85 100644 --- a/src/json.rs +++ b/src/json.rs @@ -126,6 +126,24 @@ pub fn from_slice_with_context( from_str_with_context(text, context) } +fn trim_float(s: &str) -> String { + if !s.contains('.') { return s.to_string(); } + let s = s.trim_end_matches('0').trim_end_matches('.'); + if s == "-0" { "0".to_string() } else { s.to_string() } +} + +fn format_f32(v: f32) -> String { + if v.is_nan() { return "NaN".to_string(); } + if v.is_infinite() { return if v > 0.0 { "Infinity" } else { "-Infinity" }.to_string(); } + trim_float(&format!("{:.6}", v)) +} + +fn format_f64(v: f64) -> String { + if v.is_nan() { return "NaN".to_string(); } + if v.is_infinite() { return if v > 0.0 { "Infinity" } else { "-Infinity" }.to_string(); } + trim_float(&format!("{:.15}", v)) +} + /// A JSON5 value that uses BTreeMap for sorted keys and supports NaN/Infinity. /// Uses String for numbers to preserve exact representation (including scientific notation). #[derive(Debug, Clone, PartialEq)] @@ -154,7 +172,7 @@ impl Json5Value { Json5Value::Array(arr) => { if arr.is_empty() { output.push_str("[]"); - } else if arr.len() < 12 + } else if arr.len() <= 20 && arr.iter().all(|v| { matches!( v, @@ -360,27 +378,12 @@ impl<'a> serde::Serializer for &mut CompactJson5Serializer<'a> { } fn serialize_f32(self, v: f32) -> Result<(), Self::Error> { - self.serialize_f64(v as f64) + self.output.push_str(&format_f32(v)); + Ok(()) } fn serialize_f64(self, v: f64) -> Result<(), Self::Error> { - if v.is_nan() { - self.output.push_str("NaN"); - } else if v.is_infinite() { - if v.is_sign_positive() { - self.output.push_str("Infinity"); - } else { - self.output.push_str("-Infinity"); - } - } else { - // Use scientific notation for very large or very small numbers - let abs = v.abs(); - if abs != 0.0 && !(1e-6..1e15).contains(&abs) { - self.output.push_str(&format!("{:e}", v)); - } else { - self.output.push_str(&v.to_string()); - } - } + self.output.push_str(&format_f64(v)); Ok(()) } @@ -708,29 +711,11 @@ impl serde::Serializer for Json5ValueSerializer { } fn serialize_f32(self, v: f32) -> Result { - self.serialize_f64(v as f64) + Ok(Json5Value::Number(format_f32(v))) } fn serialize_f64(self, v: f64) -> Result { - let s = if v.is_nan() { - "NaN".to_string() - } else if v.is_infinite() { - if v.is_sign_positive() { - "Infinity".to_string() - } else { - "-Infinity".to_string() - } - } else { - // Use scientific notation for very large or very small numbers - // to ensure they can be parsed back by json5 - let abs = v.abs(); - if abs != 0.0 && !(1e-6..1e15).contains(&abs) { - format!("{:e}", v) - } else { - v.to_string() - } - }; - Ok(Json5Value::Number(s)) + Ok(Json5Value::Number(format_f64(v))) } fn serialize_char(self, v: char) -> Result { @@ -1029,6 +1014,45 @@ mod tests { use super::*; use serde::Deserialize; + #[test] + fn test_trim_float() { + assert_eq!(trim_float("6.670000"), "6.67"); + assert_eq!(trim_float("6.000000"), "6"); + assert_eq!(trim_float("-0.000000"), "0"); + assert_eq!(trim_float("0.000000"), "0"); + assert_eq!(trim_float("1.500000"), "1.5"); + assert_eq!(trim_float("-3.140000"), "-3.14"); + assert_eq!(trim_float("100"), "100"); + } + + #[test] + fn test_format_f32() { + assert_eq!(format_f32(6.67), "6.67"); + assert_eq!(format_f32(6.0), "6"); + assert_eq!(format_f32(0.0), "0"); + assert_eq!(format_f32(-0.0), "0"); + assert_eq!(format_f32(0.5), "0.5"); + assert_eq!(format_f32(-3.14), "-3.14"); + assert_eq!(format_f32(1.0 / 3.0), "0.333333"); + assert_eq!(format_f32(f32::NAN), "NaN"); + assert_eq!(format_f32(f32::INFINITY), "Infinity"); + assert_eq!(format_f32(f32::NEG_INFINITY), "-Infinity"); + } + + #[test] + fn test_format_f64() { + assert_eq!(format_f64(6.67), "6.67"); + assert_eq!(format_f64(6.0), "6"); + assert_eq!(format_f64(0.0), "0"); + assert_eq!(format_f64(-0.0), "0"); + assert_eq!(format_f64(0.5), "0.5"); + assert_eq!(format_f64(-3.14), "-3.14"); + assert_eq!(format_f64(1.0 / 3.0), "0.333333333333333"); + assert_eq!(format_f64(f64::NAN), "NaN"); + assert_eq!(format_f64(f64::INFINITY), "Infinity"); + assert_eq!(format_f64(f64::NEG_INFINITY), "-Infinity"); + } + #[test] fn test_parse_value() { let value = parse_value(r#"{"foo": "bar"}"#).unwrap(); diff --git a/src/resolution.rs b/src/resolution.rs index 6d6b5eeb..add8aa20 100644 --- a/src/resolution.rs +++ b/src/resolution.rs @@ -10,65 +10,6 @@ use serde::{Deserialize, Serialize}; use crate::REF_PATH_ATTRIBUTE_PREFIX; -/// Rounds float components of Variant types to 6 decimal places. -/// Uses format-and-reparse for exact decimal representation in json5 output. -fn truncate_variant(v: Variant) -> Variant { - fn t(v: f32) -> f32 { - format!("{:.6}", v).parse::().unwrap_or(v) - } - - match v { - Variant::CFrame(cf) => Variant::CFrame(CFrame { - position: Vector3::new(t(cf.position.x), t(cf.position.y), t(cf.position.z)), - orientation: Matrix3::new( - Vector3::new( - t(cf.orientation.x.x), - t(cf.orientation.x.y), - t(cf.orientation.x.z), - ), - Vector3::new( - t(cf.orientation.y.x), - t(cf.orientation.y.y), - t(cf.orientation.y.z), - ), - Vector3::new( - t(cf.orientation.z.x), - t(cf.orientation.z.y), - t(cf.orientation.z.z), - ), - ), - }), - Variant::OptionalCFrame(Some(cf)) => Variant::OptionalCFrame(Some(CFrame { - position: Vector3::new(t(cf.position.x), t(cf.position.y), t(cf.position.z)), - orientation: Matrix3::new( - Vector3::new( - t(cf.orientation.x.x), - t(cf.orientation.x.y), - t(cf.orientation.x.z), - ), - Vector3::new( - t(cf.orientation.y.x), - t(cf.orientation.y.y), - t(cf.orientation.y.z), - ), - Vector3::new( - t(cf.orientation.z.x), - t(cf.orientation.z.y), - t(cf.orientation.z.z), - ), - ), - })), - Variant::Vector3(v) => Variant::Vector3(Vector3::new(t(v.x), t(v.y), t(v.z))), - Variant::Vector2(v) => Variant::Vector2(Vector2::new(t(v.x), t(v.y))), - Variant::UDim(u) => Variant::UDim(rbx_dom_weak::types::UDim::new(t(u.scale), u.offset)), - Variant::UDim2(u) => Variant::UDim2(rbx_dom_weak::types::UDim2::new( - rbx_dom_weak::types::UDim::new(t(u.x.scale), u.x.offset), - rbx_dom_weak::types::UDim::new(t(u.y.scale), u.y.offset), - )), - other => other, - } -} - /// A user-friendly version of `Variant` that supports specifying ambiguous /// values. Ambiguous values need a reflection database to be resolved to a /// usable value. @@ -100,7 +41,6 @@ impl UnresolvedValue { /// Creates an `UnresolvedValue` from a variant, using a class and property /// name to potentially allow for ambiguous Enum variants. pub fn from_variant(variant: Variant, class_name: &str, prop_name: &str) -> Self { - let variant = truncate_variant(variant); let descriptor = find_descriptor(class_name, prop_name); if descriptor.is_some() { // We can only use an ambiguous syntax if the property is known @@ -124,7 +64,7 @@ impl UnresolvedValue { return Self::FullyQualified(variant); } Variant::Bool(bool) => AmbiguousValue::Bool(bool), - Variant::Float32(n) => AmbiguousValue::Number(n as f64), + Variant::Float32(n) => AmbiguousValue::Number32(n), Variant::Float64(n) => AmbiguousValue::Number(n), Variant::Int32(n) => AmbiguousValue::Number(n as f64), Variant::Int64(n) => AmbiguousValue::Number(n as f64), @@ -139,27 +79,27 @@ impl UnresolvedValue { }, Variant::ContentId(content) => AmbiguousValue::String(content.into_string()), Variant::Vector2(vector) => { - AmbiguousValue::Array2([vector.x as f64, vector.y as f64]) + AmbiguousValue::Array2([vector.x, vector.y]) } Variant::Vector3(vector) => { - AmbiguousValue::Array3([vector.x as f64, vector.y as f64, vector.z as f64]) + AmbiguousValue::Array3([vector.x, vector.y, vector.z]) } Variant::Color3(color) => { - AmbiguousValue::Array3([color.r as f64, color.g as f64, color.b as f64]) + AmbiguousValue::Array3([color.r, color.g, color.b]) } Variant::CFrame(cf) => AmbiguousValue::Array12([ - cf.position.x as f64, - cf.position.y as f64, - cf.position.z as f64, - cf.orientation.x.x as f64, - cf.orientation.x.y as f64, - cf.orientation.x.z as f64, - cf.orientation.y.x as f64, - cf.orientation.y.y as f64, - cf.orientation.y.z as f64, - cf.orientation.z.x as f64, - cf.orientation.z.y as f64, - cf.orientation.z.z as f64, + cf.position.x, + cf.position.y, + cf.position.z, + cf.orientation.x.x, + cf.orientation.x.y, + cf.orientation.x.z, + cf.orientation.y.x, + cf.orientation.y.y, + cf.orientation.y.z, + cf.orientation.z.x, + cf.orientation.z.y, + cf.orientation.z.z, ]), Variant::Attributes(attr) => AmbiguousValue::Attributes(attr), Variant::Font(font) => AmbiguousValue::Font(font), @@ -176,7 +116,6 @@ impl UnresolvedValue { /// Creates an `UnresolvedValue` from a variant, only returning ambiguous /// values if they're able to be resolved in a context-free environment. pub fn from_variant_unambiguous(variant: Variant) -> Self { - let variant = truncate_variant(variant); match variant { Variant::String(str) => Self::Ambiguous(AmbiguousValue::String(str)), Variant::Float64(number) => Self::Ambiguous(AmbiguousValue::Number(number)), @@ -199,10 +138,11 @@ pub enum AmbiguousValue { String(String), StringArray(Vec), Number(f64), - Array2([f64; 2]), - Array3([f64; 3]), - Array4([f64; 4]), - Array12([f64; 12]), + Number32(f32), + Array2([f32; 2]), + Array3([f32; 3]), + Array4([f32; 4]), + Array12([f32; 12]), Attributes(Attributes), Font(Font), MaterialColors(MaterialColors), @@ -257,6 +197,7 @@ impl AmbiguousValue { DataType::Value(variant_ty) => match (variant_ty, self) { (VariantType::Bool, AmbiguousValue::Bool(value)) => Ok(value.into()), + (VariantType::Float32, AmbiguousValue::Number32(value)) => Ok((value as f32).into()), (VariantType::Float32, AmbiguousValue::Number(value)) => Ok((value as f32).into()), (VariantType::Float64, AmbiguousValue::Number(value)) => Ok(value.into()), (VariantType::Int32, AmbiguousValue::Number(value)) => Ok((value as i32).into()), @@ -341,6 +282,7 @@ impl AmbiguousValue { AmbiguousValue::String(_) => "a string", AmbiguousValue::StringArray(_) => "an array of strings", AmbiguousValue::Number(_) => "a number", + AmbiguousValue::Number32(_) => "a number", AmbiguousValue::Array2(_) => "an array of two numbers", AmbiguousValue::Array3(_) => "an array of three numbers", AmbiguousValue::Array4(_) => "an array of four numbers", @@ -476,6 +418,12 @@ mod test { assert_eq!(resolve("Part", "Transparency", "1"), Variant::Float32(1.0)); assert_eq!(resolve("NumberValue", "Value", "1"), Variant::Float64(1.0)); + assert_eq!( + UnresolvedValue::from_variant(Variant::Float32(0.125), "Part", "Transparency") + .resolve("Part", "Transparency") + .unwrap(), + Variant::Float32(0.125), + ); assert_eq!(resolve_unambiguous("12.5"), Variant::Float64(12.5)); }