Repository: ron-rs/ron
Pull request #237 (2020-05-23 00:58:27)

Optionally always include float decimals
This adds the option to always include the decimal point in floats.

Currently ron always serializes `5.0` as `5`. I have a use case that makes the `5` vs `5.0` distinction important. My rust type is a dynamic collection that can contain either floats or ints. Round tripping a value of `5.0` with ron results in an integer output of `5` when its source was a float.

cart: Also, I'd love to use rust's fmt syntax to tack on the `.0`, but I can't find a way to use the minimum amount of precision required. Ex: `write!(w, "{:.5}", 1.0)` will always include the decimal, but it will always use a precision of exactly 5 decimal places: `1.00000`. I don't think rounding here is desirable.
kvark: Note: clippy isn't happy with float comparisons (red on CI)
cart: i opted for the clippy recommended solution: ``` (v - v.floor()).abs() < f32::EPSILON```
cart: oops :)
cart: It looks like EPSILON associated constants aren't in 1.34. Is backward compat desirable? I can either inline the epsilon values or someone will need to bump that version to 1.43. What should I do here?
kvark: Let's not raise the Rust version just yet. Debian and other distros are typically much behind the top of the tree.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index da0ef05b..3a409104 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## Next
+- Added `decimal_floats` PrettyConfig option, which always includes decimals in floats (`1.0` vs `1`) ([#237](https://github.com/ron-rs/ron/pull/237))
 
 ## [0.6.0] - 2020-05-21
 ### Additions
diff --git a/src/ser/mod.rs b/src/ser/mod.rs
index 53e11f65..a4a98eb1 100644
--- a/src/ser/mod.rs
+++ b/src/ser/mod.rs
@@ -76,6 +76,9 @@ pub struct PrettyConfig {
     /// Enumerate array items in comments
     #[serde(default = "default_enumerate_arrays")]
     pub enumerate_arrays: bool,
+    /// Always include the decimal in floats
+    #[serde(default = "default_decimal_floats")]
+    pub decimal_floats: bool,
     /// Enable extensions. Only configures 'implicit_some' for now.
     pub extensions: Extensions,
     /// Private field to ensure adding a field is non-breaking.
@@ -141,6 +144,17 @@ impl PrettyConfig {
         self
     }
 
+    /// Configures whether floats should always include a decimal.
+    /// When false `1.0` will serialize as `1`
+    /// When true `1.0` will serialize as `1.0`
+    ///
+    /// Default: `false`
+    pub fn with_decimal_floats(mut self, decimal_floats: bool) -> Self {
+        self.decimal_floats = decimal_floats;
+
+        self
+    }
+
     /// Configures extensions
     ///
     /// Default: Extensions::empty()
@@ -164,6 +178,10 @@ fn default_new_line() -> String {
     new_line
 }
 
+fn default_decimal_floats() -> bool {
+    false
+}
+
 fn default_indentor() -> String {
     "    ".to_string()
 }
@@ -185,6 +203,7 @@ impl Default for PrettyConfig {
             separate_tuple_members: default_separate_tuple_members(),
             enumerate_arrays: default_enumerate_arrays(),
             extensions: Extensions::default(),
+            decimal_floats: default_decimal_floats(),
             _future_proof: (),
         }
     }
@@ -241,6 +260,12 @@ impl<W: io::Write> Serializer<W> {
             .map_or(false, |&(ref config, _)| config.separate_tuple_members)
     }
 
+    fn decimal_floats(&self) -> bool {
+        self.pretty
+            .as_ref()
+            .map_or(false, |&(ref config, _)| config.decimal_floats)
+    }
+
     fn extensions(&self) -> Extensions {
         self.pretty
             .as_ref()
@@ -363,11 +388,21 @@ impl<'a, W: io::Write> ser::Serializer for &'a mut Serializer<W> {
 
     fn serialize_f32(self, v: f32) -> Result<()> {
         write!(self.output, "{}", v)?;
+        // TODO: use f32::EPSILON when minimum supported rust version is 1.43
+        pub const EPSILON: f32 = 1.19209290e-07_f32;
+        if self.decimal_floats() && (v - v.floor()).abs() < EPSILON {
+            write!(self.output, ".0")?;
+        }
         Ok(())
     }
 
     fn serialize_f64(self, v: f64) -> Result<()> {
         write!(self.output, "{}", v)?;
+        // TODO: use f64::EPSILON when minimum supported rust version is 1.43
+        pub const EPSILON: f64 = 2.2204460492503131e-16_f64;
+        if self.decimal_floats() && (v - v.floor()).abs() < EPSILON {
+            write!(self.output, ".0")?;
+        }
         Ok(())
     }
 


Test patch:
diff --git a/tests/floats.rs b/tests/floats.rs
index f01c414a..e19726b1 100644
--- a/tests/floats.rs
+++ b/tests/floats.rs
@@ -1,4 +1,7 @@
-use ron::de::from_str;
+use ron::{
+    de::from_str,
+    ser::{to_string_pretty, PrettyConfig},
+};
 
 #[test]
 fn test_inf_and_nan() {
@@ -6,3 +9,22 @@ fn test_inf_and_nan() {
     assert_eq!(from_str("-inf"), Ok(std::f64::NEG_INFINITY));
     assert_eq!(from_str::<f64>("NaN").map(|n| n.is_nan()), Ok(true))
 }
+
+#[test]
+fn decimal_floats() {
+    let pretty = PrettyConfig::new().with_decimal_floats(false);
+    let without_decimal = to_string_pretty(&1.0, pretty).unwrap();
+    assert_eq!(without_decimal, "1");
+
+    let pretty = PrettyConfig::new().with_decimal_floats(false);
+    let without_decimal = to_string_pretty(&1.1, pretty).unwrap();
+    assert_eq!(without_decimal, "1.1");
+
+    let pretty = PrettyConfig::new().with_decimal_floats(true);
+    let with_decimal = to_string_pretty(&1.0, pretty).unwrap();
+    assert_eq!(with_decimal, "1.0");
+
+    let pretty = PrettyConfig::new().with_decimal_floats(true);
+    let with_decimal = to_string_pretty(&1.1, pretty).unwrap();
+    assert_eq!(with_decimal, "1.1");
+}