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)); }