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/json_util.cpp
|
||||
util/Timer.cpp
|
||||
util/print_float.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(common fmt lzokay replxx)
|
||||
|
@ -42,6 +42,7 @@
|
||||
#include "Object.h"
|
||||
#include "common/util/FileUtil.h"
|
||||
#include "third-party/fmt/core.h"
|
||||
#include "common/util/print_float.h"
|
||||
|
||||
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 <>
|
||||
std::string fixed_to_string(FloatType x) {
|
||||
char buff[256];
|
||||
s64 rounded = x;
|
||||
bool exact_int = ((float)rounded) == x;
|
||||
|
||||
// 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");
|
||||
}
|
||||
auto result = float_to_string(x);
|
||||
assert((float)x == (float)std::stod(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
/*!
|
||||
|
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 <vector>
|
||||
#include <limits>
|
||||
|
||||
#include "common/util/FileUtil.h"
|
||||
#include "common/util/Trie.h"
|
||||
@ -9,6 +10,7 @@
|
||||
#include "common/util/json_util.h"
|
||||
#include "common/util/Range.h"
|
||||
#include "third-party/fmt/core.h"
|
||||
#include "common/util/print_float.h"
|
||||
|
||||
TEST(CommonUtil, get_file_path) {
|
||||
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 - 1), Range<int>(1, 64));
|
||||
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