mirror of
https://github.com/open-goal/jak-project.git
synced 2024-11-23 06:09:57 +00:00
Use dragonbox to print floats (#481)
* add new float printer * more includes * compare as floats
This commit is contained in:
parent
790e65a78c
commit
60c670df3a
@ -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)
|
||||||
|
@ -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
130
common/util/print_float.cpp
Normal 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;
|
||||||
|
}
|
6
common/util/print_float.h
Normal file
6
common/util/print_float.h
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
std::string float_to_string(float value);
|
||||||
|
int float_to_cstr(float value, char* buffer);
|
@ -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
3307
third-party/dragonbox.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user