Skip to content

Commit 9bfa113

Browse files
committed
Rewrite the float-to-string conversion
Instead of committing to a number of decimal places, the new algorithm targets a certain number of significant digits. Since the mantissa has to fit in a 32-bit integer, the number of significant digits is limited to 9.
1 parent 6cc8b31 commit 9bfa113

File tree

4 files changed

+84
-66
lines changed

4 files changed

+84
-66
lines changed

extras/tests/JsonSerializer/JsonVariant.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,15 @@ TEST_CASE("serializeJson(JsonVariant)") {
5151

5252
SECTION("double") {
5353
CHECK(serialize(0.0) == "0");
54+
CHECK(serialize(-0.0) == "0");
55+
CHECK(serialize(10.0) == "10");
56+
CHECK(serialize(100.0) == "100");
57+
CHECK(serialize(0.1) == "0.1");
58+
CHECK(serialize(0.01) == "0.01");
5459
CHECK(serialize(3.1415927) == "3.1415927");
5560
CHECK(serialize(-3.1415927) == "-3.1415927");
56-
CHECK(serialize(1.7976931348623157E+308) == "1.797693135e308");
57-
CHECK(serialize(4.94065645841247e-324) == "4.940656458e-324");
61+
CHECK(serialize(1.7976931348623157E+308) == "1.79769313e308");
62+
CHECK(serialize(4.94065645841247e-324) == "4.94065646e-324");
5863
}
5964

6065
SECTION("float") {

extras/tests/TextFormatter/writeFloat.cpp

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ static std::string toString(TFloat input) {
2424

2525
TEST_CASE("TextFormatter::writeFloat(double)") {
2626
SECTION("Pi") {
27-
REQUIRE(toString(3.14159265359) == "3.141592654");
27+
REQUIRE(toString(3.14159265359) == "3.14159265");
2828
}
2929

3030
SECTION("Signaling NaN") {
@@ -49,13 +49,13 @@ TEST_CASE("TextFormatter::writeFloat(double)") {
4949
}
5050

5151
SECTION("Espilon") {
52-
REQUIRE(toString(2.2250738585072014E-308) == "2.225073859e-308");
53-
REQUIRE(toString(-2.2250738585072014E-308) == "-2.225073859e-308");
52+
REQUIRE(toString(2.2250738585072014E-308) == "2.22507386e-308");
53+
REQUIRE(toString(-2.2250738585072014E-308) == "-2.22507386e-308");
5454
}
5555

5656
SECTION("Max double") {
57-
REQUIRE(toString(1.7976931348623157E+308) == "1.797693135e308");
58-
REQUIRE(toString(-1.7976931348623157E+308) == "-1.797693135e308");
57+
REQUIRE(toString(1.7976931348623157E+308) == "1.79769313e308");
58+
REQUIRE(toString(-1.7976931348623157E+308) == "-1.79769313e308");
5959
}
6060

6161
SECTION("Big exponent") {
@@ -72,10 +72,10 @@ TEST_CASE("TextFormatter::writeFloat(double)") {
7272
}
7373

7474
SECTION("Exponentation when >= 1e7") {
75-
REQUIRE(toString(9999999.999) == "9999999.999");
75+
REQUIRE(toString(9999999.99) == "9999999.99");
7676
REQUIRE(toString(10000000.0) == "1e7");
7777

78-
REQUIRE(toString(-9999999.999) == "-9999999.999");
78+
REQUIRE(toString(-9999999.99) == "-9999999.99");
7979
REQUIRE(toString(-10000000.0) == "-1e7");
8080
}
8181

@@ -85,12 +85,20 @@ TEST_CASE("TextFormatter::writeFloat(double)") {
8585
REQUIRE(toString(0.9999999996) == "1");
8686
}
8787

88+
SECTION("9 decimal places") {
89+
REQUIRE(toString(0.10000001) == "0.10000001");
90+
REQUIRE(toString(0.99999999) == "0.99999999");
91+
92+
REQUIRE(toString(9.00000001) == "9.00000001");
93+
REQUIRE(toString(9.99999999) == "9.99999999");
94+
}
95+
8896
SECTION("9 decimal places") {
8997
REQUIRE(toString(0.100000001) == "0.100000001");
9098
REQUIRE(toString(0.999999999) == "0.999999999");
9199

92-
REQUIRE(toString(9.000000001) == "9.000000001");
93-
REQUIRE(toString(9.999999999) == "9.999999999");
100+
REQUIRE(toString(9.000000001) == "9");
101+
REQUIRE(toString(9.999999999) == "10");
94102
}
95103

96104
SECTION("10 decimal places") {

src/ArduinoJson/Json/TextFormatter.hpp

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,16 @@ class TextFormatter {
6666

6767
template <typename T>
6868
void writeFloat(T value) {
69-
writeFloat(JsonFloat(value), sizeof(T) >= 8 ? 9 : 6);
69+
writeFloat(JsonFloat(value), sizeof(T) >= 8 ? 9 : 7);
7070
}
7171

7272
void writeFloat(JsonFloat value, int8_t decimalPlaces) {
7373
if (isnan(value))
7474
return writeRaw(ARDUINOJSON_ENABLE_NAN ? "NaN" : "null");
7575

76+
if (!value)
77+
return writeRaw("0");
78+
7679
#if ARDUINOJSON_ENABLE_INFINITY
7780
if (value < 0.0) {
7881
writeRaw('-');
@@ -93,9 +96,28 @@ class TextFormatter {
9396

9497
auto parts = decomposeFloat(value, decimalPlaces);
9598

96-
writeInteger(parts.integral);
97-
if (parts.decimalPlaces)
98-
writeDecimals(parts.decimal, parts.decimalPlaces);
99+
// buffer should be big enough for all digits and the dot
100+
char buffer[32];
101+
char* end = buffer + sizeof(buffer);
102+
char* begin = end;
103+
104+
// write the string in reverse order
105+
while (parts.mantissa != 0 || parts.pointIndex > 0) {
106+
*--begin = char(parts.mantissa % 10 + '0');
107+
parts.mantissa /= 10;
108+
if (parts.pointIndex == 1) {
109+
*--begin = '.';
110+
}
111+
parts.pointIndex--;
112+
}
113+
114+
// Avoid a leading dot
115+
if (parts.pointIndex == 0) {
116+
*--begin = '0';
117+
}
118+
119+
// and dump it in the right order
120+
writeRaw(begin, end);
99121

100122
if (parts.exponent) {
101123
writeRaw('e');
@@ -132,23 +154,6 @@ class TextFormatter {
132154
writeRaw(begin, end);
133155
}
134156

135-
void writeDecimals(uint32_t value, int8_t width) {
136-
// buffer should be big enough for all digits and the dot
137-
char buffer[16];
138-
char* end = buffer + sizeof(buffer);
139-
char* begin = end;
140-
141-
// write the string in reverse order
142-
while (width--) {
143-
*--begin = char(value % 10 + '0');
144-
value /= 10;
145-
}
146-
*--begin = '.';
147-
148-
// and dump it in the right order
149-
writeRaw(begin, end);
150-
}
151-
152157
void writeRaw(const char* s) {
153158
writer_.write(reinterpret_cast<const uint8_t*>(s), strlen(s));
154159
}

src/ArduinoJson/Numbers/FloatParts.hpp

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,32 @@
1313
ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE
1414

1515
struct FloatParts {
16-
uint32_t integral;
17-
uint32_t decimal;
16+
uint32_t mantissa;
1817
int16_t exponent;
19-
int8_t decimalPlaces;
18+
int8_t pointIndex;
2019
};
2120

2221
constexpr uint32_t pow10(int exponent) {
2322
return (exponent == 0) ? 1 : 10 * pow10(exponent - 1);
2423
}
2524

26-
inline FloatParts decomposeFloat(JsonFloat value, int8_t decimalPlaces) {
27-
ARDUINOJSON_ASSERT(value >= 0);
28-
ARDUINOJSON_ASSERT(decimalPlaces >= 0);
25+
inline FloatParts decomposeFloat(JsonFloat value, int8_t significantDigits) {
26+
ARDUINOJSON_ASSERT(value > 0);
27+
ARDUINOJSON_ASSERT(significantDigits > 1);
28+
ARDUINOJSON_ASSERT(significantDigits <= 9); // to prevent uint32_t overflow
2929

3030
using traits = FloatTraits<JsonFloat>;
3131

32-
uint32_t maxDecimalPart = pow10(decimalPlaces);
32+
bool useScientificNotation =
33+
value >= ARDUINOJSON_POSITIVE_EXPONENTIATION_THRESHOLD ||
34+
value <= ARDUINOJSON_NEGATIVE_EXPONENTIATION_THRESHOLD;
3335

3436
int16_t exponent = 0;
3537
int8_t index = traits::binaryPowersOfTenArraySize - 1;
3638
int bit = 1 << index;
3739

38-
if (value >= ARDUINOJSON_POSITIVE_EXPONENTIATION_THRESHOLD) {
40+
// Normalize value to range [1..10) and compute exponent
41+
if (value > 1) {
3942
for (; index >= 0; index--) {
4043
if (value >= traits::positiveBinaryPowersOfTen()[index]) {
4144
value *= traits::negativeBinaryPowersOfTen()[index];
@@ -44,8 +47,8 @@ inline FloatParts decomposeFloat(JsonFloat value, int8_t decimalPlaces) {
4447
bit >>= 1;
4548
}
4649
}
47-
48-
if (value > 0 && value <= ARDUINOJSON_NEGATIVE_EXPONENTIATION_THRESHOLD) {
50+
ARDUINOJSON_ASSERT(value < 10);
51+
if (value < 1) {
4952
for (; index >= 0; index--) {
5053
if (value < traits::negativeBinaryPowersOfTen()[index] * 10) {
5154
value *= traits::positiveBinaryPowersOfTen()[index];
@@ -54,39 +57,36 @@ inline FloatParts decomposeFloat(JsonFloat value, int8_t decimalPlaces) {
5457
bit >>= 1;
5558
}
5659
}
60+
ARDUINOJSON_ASSERT(value >= 1);
61+
// ARDUINOJSON_ASSERT(value < 10);
5762

58-
uint32_t integral = uint32_t(value);
59-
// reduce number of decimal places by the number of integral places
60-
for (uint32_t tmp = integral; tmp >= 10; tmp /= 10) {
61-
maxDecimalPart /= 10;
62-
decimalPlaces--;
63-
}
63+
value *= JsonFloat(pow10(significantDigits - 1));
6464

65-
JsonFloat remainder =
66-
(value - JsonFloat(integral)) * JsonFloat(maxDecimalPart);
65+
auto mantissa = uint32_t(value);
66+
ARDUINOJSON_ASSERT(mantissa > 0);
6767

68-
uint32_t decimal = uint32_t(remainder);
69-
remainder = remainder - JsonFloat(decimal);
68+
// rounding
69+
auto remainder = value - JsonFloat(mantissa);
70+
if (remainder >= 0.5)
71+
mantissa++;
7072

71-
// rounding:
72-
// increment by 1 if remainder >= 0.5
73-
decimal += uint32_t(remainder * 2);
74-
if (decimal >= maxDecimalPart) {
75-
decimal = 0;
76-
integral++;
77-
if (exponent && integral >= 10) {
78-
exponent++;
79-
integral = 1;
80-
}
73+
auto pointIndex = int8_t(significantDigits - 1);
74+
75+
if (!useScientificNotation) {
76+
pointIndex = int8_t(pointIndex - int8_t(exponent));
77+
exponent = 0;
8178
}
8279

8380
// remove trailing zeros
84-
while (decimal % 10 == 0 && decimalPlaces > 0) {
85-
decimal /= 10;
86-
decimalPlaces--;
81+
while (mantissa % 10 == 0 && (useScientificNotation || pointIndex > 0)) {
82+
mantissa /= 10;
83+
if (pointIndex > 0)
84+
pointIndex--;
85+
else
86+
exponent++;
8787
}
8888

89-
return {integral, decimal, exponent, decimalPlaces};
89+
return {mantissa, exponent, pointIndex};
9090
}
9191

9292
ARDUINOJSON_END_PRIVATE_NAMESPACE

0 commit comments

Comments
 (0)