From eefd4abd5e636f4d998a33e8fa00cb1271471499 Mon Sep 17 00:00:00 2001 From: Romain Reignier Date: Thu, 16 Oct 2025 15:06:49 +0200 Subject: [PATCH] Emitter: produce string with trailing '.0' for floating point value with only integral part For example the number 1.0 will be converted to "1.0" instead of "1". This allows to keep the correct type information when parsing back the YAML. Fix #226 #412 #1016 --- src/fptostring.cpp | 10 ++- test/fptostring_test.cpp | 113 ++++++++++++++++++++---------- test/integration/emitter_test.cpp | 5 +- test/node/node_test.cpp | 2 +- 4 files changed, 91 insertions(+), 39 deletions(-) diff --git a/src/fptostring.cpp b/src/fptostring.cpp index 3026cbd08..d5c1ca26c 100644 --- a/src/fptostring.cpp +++ b/src/fptostring.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include @@ -86,7 +88,8 @@ std::string FpToString(T v, int precision = 0) { if (v == 0 || std::isinf(v) || std::isnan(v)) { std::stringstream ss; ss.imbue(std::locale::classic()); - ss << v; + // force the presecence of one decimal digit for 0 + ss << std::fixed << std::setprecision(1) << v; return ss.str(); } @@ -204,6 +207,11 @@ std::string FpToString(T v, int precision = 0) { for (;digits_iter < digits_end; ++digits_iter) { *(output_ptr++) = *digits_iter; } + } else { + // if no digits after decimal point, print .0 + // to make sure it is recognized as a floating point number + *(output_ptr++) = '.'; + *(output_ptr++) = '0'; } } *output_ptr = '\0'; diff --git a/test/fptostring_test.cpp b/test/fptostring_test.cpp index 3f377abf2..553e0e7be 100644 --- a/test/fptostring_test.cpp +++ b/test/fptostring_test.cpp @@ -1,3 +1,7 @@ +#include +#include +#include + #include "yaml-cpp/fptostring.h" #include "gtest/gtest.h" @@ -7,14 +11,23 @@ namespace { /** * Helper function, that converts double to string as std::stringstream would do */ -template +template ::value, + bool>::type = true> static std::string convert_with_stringstream(T v, size_t precision = 0) { std::stringstream ss; if (precision > 0) { ss << std::setprecision(precision); } ss << v; - return ss.str(); + std::string result = ss.str(); + // Check if already in scientific notation or already has a decimal point + // This logic mimics the logic in FpToString + if (std::isfinite(v) && result.find('e') == std::string::npos && + result.find('.') == std::string::npos) { + // Add .0 to whole numbers not in scientific notation + result += ".0"; + } + return result; } // Caution: Test involving 'convert_with_stringstream' are based on std::stringstream @@ -51,13 +64,13 @@ TEST(FpToStringTest, conversion_double) { EXPECT_EQ(convert_with_stringstream(1e9), FpToString(1e9)); // Print by default values below 1e6 without scientific notation - EXPECT_EQ("1", FpToString(1.)); - EXPECT_EQ("1", FpToString(1e0)); - EXPECT_EQ("10", FpToString(1e1)); - EXPECT_EQ("100", FpToString(1e2)); - EXPECT_EQ("1000", FpToString(1e3)); - EXPECT_EQ("10000", FpToString(1e4)); - EXPECT_EQ("100000", FpToString(1e5)); + EXPECT_EQ("1.0", FpToString(1.)); + EXPECT_EQ("1.0", FpToString(1e0)); + EXPECT_EQ("10.0", FpToString(1e1)); + EXPECT_EQ("100.0", FpToString(1e2)); + EXPECT_EQ("1000.0", FpToString(1e3)); + EXPECT_EQ("10000.0", FpToString(1e4)); + EXPECT_EQ("100000.0", FpToString(1e5)); EXPECT_EQ("1e+06", FpToString(1e6)); EXPECT_EQ("1e+07", FpToString(1e7)); EXPECT_EQ("1e+08", FpToString(1e8)); @@ -77,8 +90,8 @@ TEST(FpToStringTest, conversion_double) { EXPECT_EQ(convert_with_stringstream(1e-9), FpToString(1e-9)); // Print by default values above 1e-5 without scientific notation - EXPECT_EQ("1", FpToString(1.)); - EXPECT_EQ("1", FpToString(1e-0)); + EXPECT_EQ("1.0", FpToString(1.)); + EXPECT_EQ("1.0", FpToString(1e-0)); EXPECT_EQ("0.1", FpToString(1e-1)); EXPECT_EQ("0.01", FpToString(1e-2)); EXPECT_EQ("0.001", FpToString(1e-3)); @@ -96,9 +109,9 @@ TEST(FpToStringTest, conversion_double) { EXPECT_EQ(convert_with_stringstream(1234567e-9, 7), FpToString(1234567e-9, 7)); EXPECT_EQ(convert_with_stringstream(1234567e-9, 1), FpToString(1234567e-9, 1)); - // known example that is difficult to round - EXPECT_EQ("1", FpToString(0.9999, 2)); - EXPECT_EQ("-1", FpToString(-0.9999, 2)); + // known example that is difficult to round + EXPECT_EQ("1.0", FpToString(0.9999, 2)); + EXPECT_EQ("-1.0", FpToString(-0.9999, 2)); // some more random tests EXPECT_EQ("1.25", FpToString(1.25)); @@ -110,13 +123,13 @@ TEST(FpToStringTest, conversion_double) { EXPECT_EQ("1e-20", FpToString(0.1e-19)); EXPECT_EQ("1.1e-20", FpToString(0.11e-19)); - EXPECT_EQ("19", FpToString(18.9, 2)); - EXPECT_EQ("20", FpToString(19.9, 2)); + EXPECT_EQ("19.0", FpToString(18.9, 2)); + EXPECT_EQ("20.0", FpToString(19.9, 2)); EXPECT_EQ("2e+01", FpToString(19.9, 1)); EXPECT_EQ("1.2e-05", FpToString(1.234e-5, 2)); EXPECT_EQ("1.3e-05", FpToString(1.299e-5, 2)); - EXPECT_EQ("-1", FpToString(-1.)); + EXPECT_EQ("-1.0", FpToString(-1.)); EXPECT_EQ("-1.25", FpToString(-1.25)); EXPECT_EQ("-34.34", FpToString(-34.34)); EXPECT_EQ("-1e+20", FpToString(-1e+20)); @@ -126,11 +139,25 @@ TEST(FpToStringTest, conversion_double) { EXPECT_EQ("-1e-20", FpToString(-0.1e-19)); EXPECT_EQ("-1.1e-20", FpToString(-0.11e-19)); - EXPECT_EQ("-19", FpToString(-18.9, 2)); - EXPECT_EQ("-20", FpToString(-19.9, 2)); + EXPECT_EQ("-19.0", FpToString(-18.9, 2)); + EXPECT_EQ("-20.0", FpToString(-19.9, 2)); EXPECT_EQ("-2e+01", FpToString(-19.9, 1)); EXPECT_EQ("-1.2e-05", FpToString(-1.234e-5, 2)); EXPECT_EQ("-1.3e-05", FpToString(-1.299e-5, 2)); + + // special values: 0, inf, -inf, NaN + EXPECT_EQ("0.0", FpToString(0.0)); + EXPECT_EQ("inf", FpToString(std::numeric_limits::infinity())); + EXPECT_EQ("-inf", FpToString(-std::numeric_limits::infinity())); + EXPECT_EQ("nan", FpToString(std::numeric_limits::quiet_NaN())); + + // prints the same way as std::stringstream + EXPECT_EQ(convert_with_stringstream(std::numeric_limits::infinity()), + FpToString(std::numeric_limits::infinity())); + EXPECT_EQ(convert_with_stringstream(-std::numeric_limits::infinity()), + FpToString(-std::numeric_limits::infinity())); + EXPECT_EQ(convert_with_stringstream(std::numeric_limits::quiet_NaN()), + FpToString(std::numeric_limits::quiet_NaN())); } TEST(FpToStringTest, conversion_float) { @@ -156,13 +183,13 @@ TEST(FpToStringTest, conversion_float) { EXPECT_EQ(convert_with_stringstream(1e9f), FpToString(1e9f)); // Print by default values below 1e6 without scientific notation - EXPECT_EQ("1", FpToString(1.f)); - EXPECT_EQ("1", FpToString(1e0f)); - EXPECT_EQ("10", FpToString(1e1f)); - EXPECT_EQ("100", FpToString(1e2f)); - EXPECT_EQ("1000", FpToString(1e3f)); - EXPECT_EQ("10000", FpToString(1e4f)); - EXPECT_EQ("100000", FpToString(1e5f)); + EXPECT_EQ("1.0", FpToString(1.f)); + EXPECT_EQ("1.0", FpToString(1e0f)); + EXPECT_EQ("10.0", FpToString(1e1f)); + EXPECT_EQ("100.0", FpToString(1e2f)); + EXPECT_EQ("1000.0", FpToString(1e3f)); + EXPECT_EQ("10000.0", FpToString(1e4f)); + EXPECT_EQ("100000.0", FpToString(1e5f)); EXPECT_EQ("1e+06", FpToString(1e6f)); EXPECT_EQ("1e+07", FpToString(1e7f)); EXPECT_EQ("1e+08", FpToString(1e8f)); @@ -182,8 +209,8 @@ TEST(FpToStringTest, conversion_float) { EXPECT_EQ(convert_with_stringstream(1e-9f), FpToString(1e-9f)); // Print by default values above 1e-5 without scientific notation - EXPECT_EQ("1", FpToString(1.f)); - EXPECT_EQ("1", FpToString(1e-0f)); + EXPECT_EQ("1.0", FpToString(1.f)); + EXPECT_EQ("1.0", FpToString(1e-0f)); EXPECT_EQ("0.1", FpToString(1e-1f)); EXPECT_EQ("0.01", FpToString(1e-2f)); EXPECT_EQ("0.001", FpToString(1e-3f)); @@ -201,9 +228,9 @@ TEST(FpToStringTest, conversion_float) { EXPECT_EQ(convert_with_stringstream(1234567e-9f, 7), FpToString(1234567e-9f, 7)); EXPECT_EQ(convert_with_stringstream(1234567e-9f, 1), FpToString(1234567e-9f, 1)); - // known example that is difficult to round - EXPECT_EQ("1", FpToString(0.9999f, 2)); - EXPECT_EQ("-1", FpToString(-0.9999f, 2)); + // known example that is difficult to round + EXPECT_EQ("1.0", FpToString(0.9999f, 2)); + EXPECT_EQ("-1.0", FpToString(-0.9999f, 2)); // some more random tests EXPECT_EQ("1.25", FpToString(1.25f)); @@ -215,13 +242,13 @@ TEST(FpToStringTest, conversion_float) { EXPECT_EQ("1e-20", FpToString(0.1e-19f)); EXPECT_EQ("1.1e-20", FpToString(0.11e-19f)); - EXPECT_EQ("19", FpToString(18.9f, 2)); - EXPECT_EQ("20", FpToString(19.9f, 2)); + EXPECT_EQ("19.0", FpToString(18.9f, 2)); + EXPECT_EQ("20.0", FpToString(19.9f, 2)); EXPECT_EQ("2e+01", FpToString(19.9f, 1)); EXPECT_EQ("1.2e-05", FpToString(1.234e-5f, 2)); EXPECT_EQ("1.3e-05", FpToString(1.299e-5f, 2)); - EXPECT_EQ("-1", FpToString(-1.f)); + EXPECT_EQ("-1.0", FpToString(-1.f)); EXPECT_EQ("-1.25", FpToString(-1.25f)); EXPECT_EQ("-34.34", FpToString(-34.34f)); EXPECT_EQ("-1e+20", FpToString(-1e+20f)); @@ -231,11 +258,25 @@ TEST(FpToStringTest, conversion_float) { EXPECT_EQ("-1e-20", FpToString(-0.1e-19f)); EXPECT_EQ("-1.1e-20", FpToString(-0.11e-19f)); - EXPECT_EQ("-19", FpToString(-18.9f, 2)); - EXPECT_EQ("-20", FpToString(-19.9f, 2)); + EXPECT_EQ("-19.0", FpToString(-18.9f, 2)); + EXPECT_EQ("-20.0", FpToString(-19.9f, 2)); EXPECT_EQ("-2e+01", FpToString(-19.9f, 1)); EXPECT_EQ("-1.2e-05", FpToString(-1.234e-5f, 2)); EXPECT_EQ("-1.3e-05", FpToString(-1.299e-5f, 2)); + + // special values: 0, inf, -inf, NaN + EXPECT_EQ("0.0", FpToString(0.0f)); + EXPECT_EQ("inf", FpToString(std::numeric_limits::infinity())); + EXPECT_EQ("-inf", FpToString(-std::numeric_limits::infinity())); + EXPECT_EQ("nan", FpToString(std::numeric_limits::quiet_NaN())); + + // prints the same way as std::stringstream + EXPECT_EQ(convert_with_stringstream(std::numeric_limits::infinity()), + FpToString(std::numeric_limits::infinity())); + EXPECT_EQ(convert_with_stringstream(-std::numeric_limits::infinity()), + FpToString(-std::numeric_limits::infinity())); + EXPECT_EQ(convert_with_stringstream(std::numeric_limits::quiet_NaN()), + FpToString(std::numeric_limits::quiet_NaN())); } } // namespace diff --git a/test/integration/emitter_test.cpp b/test/integration/emitter_test.cpp index 4bfc136df..302538d5a 100644 --- a/test/integration/emitter_test.cpp +++ b/test/integration/emitter_test.cpp @@ -121,14 +121,17 @@ TEST_F(EmitterTest, NumberPrecision) { out.SetFloatPrecision(3); out.SetDoublePrecision(2); out << BeginSeq; + out << 1.0f; out << 3.1425926f; + out << 0.0; + out << 1.0; out << 53.5893; out << 2384626.4338; out << 1999926.4338; out << 9999926.4338; out << EndSeq; - ExpectEmit("- 3.14\n- 54\n- 2.4e+06\n- 2e+06\n- 1e+07"); + ExpectEmit("- 1.0\n- 3.14\n- 0.0\n- 1.0\n- 54.0\n- 2.4e+06\n- 2e+06\n- 1e+07"); } TEST_F(EmitterTest, SimpleSeq) { diff --git a/test/node/node_test.cpp b/test/node/node_test.cpp index f889059a7..a34a61b47 100644 --- a/test/node/node_test.cpp +++ b/test/node/node_test.cpp @@ -765,7 +765,7 @@ TEST_F(NodeEmitterTest, SimpleFlowSeqNode) { node.push_back(4000.); node.push_back(1.5474251e+26f); - ExpectOutput("[1.5, 2.25, 3.125, 34.34, 56.56, 12.12, 78.78, 0.0003, 4000, 1.5474251e+26]", node); + ExpectOutput("[1.5, 2.25, 3.125, 34.34, 56.56, 12.12, 78.78, 0.0003, 4000.0, 1.5474251e+26]", node); } TEST_F(NodeEmitterTest, NestFlowSeqNode) {