Use dragonbox to print floats (#481)

* add new float printer

* more includes

* compare as floats
This commit is contained in:
water111 2021-05-13 21:05:05 -04:00 committed by GitHub
parent 790e65a78c
commit 60c670df3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 3486 additions and 42 deletions

View File

@ -22,6 +22,7 @@ add_library(common
util/FileUtil.cpp util/FileUtil.cpp
util/json_util.cpp util/json_util.cpp
util/Timer.cpp util/Timer.cpp
util/print_float.cpp
) )
target_link_libraries(common fmt lzokay replxx) target_link_libraries(common fmt lzokay replxx)

View File

@ -42,6 +42,7 @@
#include "Object.h" #include "Object.h"
#include "common/util/FileUtil.h" #include "common/util/FileUtil.h"
#include "third-party/fmt/core.h" #include "third-party/fmt/core.h"
#include "common/util/print_float.h"
namespace goos { namespace goos {
@ -83,51 +84,13 @@ std::string object_type_to_string(ObjectType type) {
} }
/*! /*!
* Special case to print a float with the %g format specifier * Special case to print a float
*/ */
template <> template <>
std::string fixed_to_string(FloatType x) { std::string fixed_to_string(FloatType x) {
char buff[256]; auto result = float_to_string(x);
s64 rounded = x; assert((float)x == (float)std::stod(result));
bool exact_int = ((float)rounded) == x; return result;
// it's an integer number, so let's just get this over with asap
if (exact_int) {
sprintf(buff, "%" PRId64 ".0", rounded);
return {buff};
} else {
// not an integer - see how many decimal cases we need
// i'm not sure what happens if x is a NaN/inf...
// buffer for format string
char fmt_buf[256];
// we are going to try our hardest to make sure the output is re-parseable
// for what it's worth, the lowest representable 32-bit floating point number has almost 50
// decimal cases, although:
// - by that point we should just be using scientific notation instead?
// - the PS2 DOES NOT DENORMALIZE FLOATS, or handle NaNs/infs! so the representation wouldn't
// be accurate anyway
// - we might not ever encounter numbers like that. the pretty printer has a "banned" floats
// list just in case
// 99 might seem high, but we are gonna get the result in under 10 or so most of the time, so
// it's fine.
// maybe there's some math principles or other tricks to optimize this?
for (int i = 1; i <= 99; ++i) {
// generate the format string
sprintf(fmt_buf, "%%.%df", i);
sprintf(buff, fmt_buf, x);
auto float_from_string = float(std::stod(buff));
float value_as_float = float(x);
bool are_they_equal = float_from_string == value_as_float;
if (are_they_equal)
return {buff};
}
throw std::runtime_error("a float could not be represented accurately");
}
} }
/*! /*!

130
common/util/print_float.cpp Normal file
View File

@ -0,0 +1,130 @@
#include <cmath>
#include "third-party/fmt/core.h"
#include "common/common_types.h"
#include "third-party/dragonbox.h"
#include "print_float.h"
/*!
* Convert a float to a string. The string is _always_ in this format:
* [negative_sign] [at least 1 digit] [decimal point] [at least 1 digit]
* and, if you trust the dragonbox library, should be the shortest possible representation
* that round-trips through a properly implemented string -> float conversion.
*/
std::string float_to_string(float value) {
constexpr int buff_size = 128;
char buff[buff_size];
float_to_cstr(value, buff);
return {buff};
}
int float_to_cstr(float value, char* buffer) {
assert(std::isfinite(value));
// dragonbox gives us:
// - an integer, representing the decimal value
// - sign
// - exponent
int i = 0;
// the exponent/significand representation of dragonbox is ambiguous with how it represents 0,
// so just handle that as a special case
if (value == 0) {
buffer[i++] = '0';
buffer[i++] = '.';
buffer[i++] = '0';
buffer[i++] = '\0';
}
auto decimal = jkj::dragonbox::to_decimal(value);
// in all cases, we need to convert the decimal to characters.
char digit_buff[64];
int num_digits = 0;
u64 significand = decimal.significand;
while (significand) {
digit_buff[num_digits++] = '0' + (significand % 10);
significand /= 10;
}
if (decimal.exponent >= 0) {
// needs 0 or more trailing zeros before decimal (no nonzeros after decimal).
// print in four parts:
// negative sign | digits | 000's | .0
// part 1
if (decimal.is_negative) {
buffer[i++] = '-';
}
// part 2
for (int digit = 0; digit < num_digits; digit++) {
buffer[i++] = digit_buff[num_digits - (digit + 1)];
}
// part 3
for (int j = 0; j < decimal.exponent; j++) {
buffer[i++] = '0';
}
// part 4
buffer[i++] = '.';
buffer[i++] = '0';
buffer[i++] = '\0';
} else {
// some nonzero digits after decimal.
if (num_digits <= -decimal.exponent) {
// all after the decimal
// negative sign | 0. | 000's | digits
// part 1
if (decimal.is_negative) {
buffer[i++] = '-';
}
// part 2
buffer[i++] = '0';
buffer[i++] = '.';
// part 3
int zeros = -decimal.exponent - num_digits;
for (int j = 0; j < zeros; j++) {
buffer[i++] = '0';
}
// part 4
for (int digit = 0; digit < num_digits; digit++) {
buffer[i++] = digit_buff[num_digits - (digit + 1)];
}
buffer[i++] = '\0';
} else {
// some before, some after.
// negative sign | digits | . | digits
// part 1
if (decimal.is_negative) {
buffer[i++] = '-';
}
// part 2
int digits_before_decimal = num_digits + decimal.exponent;
int digit = 0;
for (; digit < digits_before_decimal; digit++) {
buffer[i++] = digit_buff[num_digits - (digit + 1)];
}
// part 3
buffer[i++] = '.';
// part 4
for (; digit < num_digits; digit++) {
buffer[i++] = digit_buff[num_digits - (digit + 1)];
}
buffer[i++] = '\0';
}
}
return i;
}

View File

@ -0,0 +1,6 @@
#pragma once
#include <string>
std::string float_to_string(float value);
int float_to_cstr(float value, char* buffer);

View File

@ -1,5 +1,6 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include <limits>
#include "common/util/FileUtil.h" #include "common/util/FileUtil.h"
#include "common/util/Trie.h" #include "common/util/Trie.h"
@ -9,6 +10,7 @@
#include "common/util/json_util.h" #include "common/util/json_util.h"
#include "common/util/Range.h" #include "common/util/Range.h"
#include "third-party/fmt/core.h" #include "third-party/fmt/core.h"
#include "common/util/print_float.h"
TEST(CommonUtil, get_file_path) { TEST(CommonUtil, get_file_path) {
std::vector<std::string> test = {"cabbage", "banana", "apple"}; std::vector<std::string> test = {"cabbage", "banana", "apple"};
@ -94,4 +96,39 @@ TEST(CommonUtil, BitRange) {
EXPECT_EQ(get_bit_range(UINT64_MAX), Range<int>(0, 64)); EXPECT_EQ(get_bit_range(UINT64_MAX), Range<int>(0, 64));
EXPECT_EQ(get_bit_range(UINT64_MAX - 1), Range<int>(1, 64)); EXPECT_EQ(get_bit_range(UINT64_MAX - 1), Range<int>(1, 64));
EXPECT_EQ(get_bit_range(UINT64_MAX / 2), Range<int>(0, 63)); EXPECT_EQ(get_bit_range(UINT64_MAX / 2), Range<int>(0, 63));
}
TEST(CommonUtil, FloatToString) {
float test_floats[] = {0.f,
1.f,
-1.f,
0.1f,
-0.1f,
1234,
12340,
123400,
-1234000,
0.00342f,
-0.003423f,
std::numeric_limits<float>::min(),
std::numeric_limits<float>::max(),
std::numeric_limits<float>::lowest(),
std::numeric_limits<float>::epsilon(),
std::numeric_limits<float>::denorm_min(),
-std::numeric_limits<float>::min(),
-std::numeric_limits<float>::max(),
-std::numeric_limits<float>::lowest(),
-std::numeric_limits<float>::epsilon(),
-std::numeric_limits<float>::denorm_min()};
for (auto x : test_floats) {
EXPECT_TRUE(x == (float)std::stod(float_to_string(x)));
}
// all three of these constants should become _exactly_ 1460961.25 when converted to a float.
// to break a tie, dragonbox defaults to round to even, which is nice because that's the
// default rounding mode.
EXPECT_EQ("1460961.2", float_to_string(1460961.25));
EXPECT_EQ("1460961.2", float_to_string(1460961.20));
EXPECT_EQ("1460961.2", float_to_string(1460961.30));
} }

3307
third-party/dragonbox.h vendored Normal file

File diff suppressed because it is too large Load Diff