linmath: For repr, use as much precision as needed for roundtrip

Will use as few digits as is necessary to ensure that round-tripping the same number with pstrtod will result in the same vector.  This prevents eg. 3.3 formatting as 3.29999995 while still ensuring that two floats that are not equal (other than nan) are guaranteed to have a different string representation.

Uses a hacky pftoa function that can be abandoned as soon as we adopt C++17, which has to_chars that does what we need

Fixes #1671
This commit is contained in:
rdb 2024-08-07 17:24:11 +02:00
parent ca7ba4eab3
commit 8c8cbeea98
20 changed files with 404 additions and 87 deletions

View File

@ -428,3 +428,161 @@ void pdtoa(double value, char *buffer) {
Prettify(buffer, length, K);
}
}
/**
* Version of pdtoa that tries hard to find the minimal string representation
* for a single-precision floating-point number.
*/
void pftoa(float value, char *buffer) {
#ifdef _MSC_VER
if (copysign(1.0f, value) < 0) {
#else
if (std::signbit(value)) {
#endif
*buffer++ = '-';
value = -value;
}
if (cinf(value)) {
buffer[0] = 'i';
buffer[1] = 'n';
buffer[2] = 'f';
buffer[3] = '\0';
} else if (cnan(value)) {
buffer[0] = 'n';
buffer[1] = 'a';
buffer[2] = 'n';
buffer[3] = '\0';
} else if (value == 0.0f) {
buffer[0] = '0';
buffer[1] = '.';
buffer[2] = '0';
buffer[3] = '\0';
} else if (value == 1.0f) {
buffer[0] = '1';
buffer[1] = '.';
buffer[2] = '0';
buffer[3] = '\0';
} else {
int length, k;
Grisu2(value, buffer, &length, &k);
const int kk = length + k; // 10^(kk-1) <= v < 10^kk
if (length <= kk && kk <= 21) {
// 1234e7 -> 12340000000
for (int i = length; i < kk; i++)
buffer[i] = '0';
buffer[kk] = '.';
buffer[kk + 1] = '0';
buffer[kk + 2] = '\0';
}
else if (0 < kk && kk <= 21) {
// 1234e-2 -> 12.34
memmove(&buffer[kk + 1], &buffer[kk], length - kk);
// We want the shortest possible representation, so keep reading digits
// until strtod would give the correct float value.
buffer[kk] = '\0';
double v = (double)atoi(buffer);
buffer[kk] = '.';
double multiplicand = 0.1;
for (int i = kk + 1; i <= length; ++i) {
double vplus = v + (buffer[i] - '0' + 1) * multiplicand;
v += (buffer[i] - '0') * multiplicand;
multiplicand *= 0.1;
if ((float)v == value) {
length = i;
break;
}
if (buffer[i] < '9' && (float)vplus == value) {
++buffer[i];
length = i;
break;
}
}
buffer[length + 1] = '\0';
}
else if (-6 < kk && kk <= 0) {
// 1234e-6 -> 0.001234
const int offset = 2 - kk;
memmove(&buffer[offset], &buffer[0], length);
buffer[0] = '0';
buffer[1] = '.';
// We want the shortest possible representation, so keep reading digits
// until strtod would give the correct float value.
double multiplicand = 1.0;
for (int i = 2; i < offset; i++) {
buffer[i] = '0';
multiplicand *= 0.1;
}
if ((float)multiplicand == value) {
length = 0;
buffer[offset - 1] = '1';
} else {
multiplicand *= 0.1;
double v = 0.0;
for (int i = offset; i < length + offset; ++i) {
double vplus = v + (buffer[i] - '0' + 1) * multiplicand;
v += (buffer[i] - '0') * multiplicand;
multiplicand *= 0.1;
if ((float)v == value) {
buffer[i + 1] = '\0';
break;
}
if (buffer[i] < '9' && (float)vplus == value) {
buffer[i]++;
buffer[i + 1] = '\0';
break;
}
}
}
buffer[length + offset] = '\0';
}
else if (length == 1) {
// 1e30
buffer[1] = 'e';
WriteExponent(kk - 1, &buffer[2]);
}
else {
// 1234e30 -> 1.234e33
memmove(&buffer[2], &buffer[1], length - 1);
buffer[1] = '.';
buffer[length + 1] = 'e';
double e_mult = pow(10.0, kk - 1);
if ((float)(10.0 * e_mult) == value) {
buffer[0] = '1';
buffer[1] = 'e';
WriteExponent(kk, &buffer[2]);
} else {
// We want the shortest possible representation, so keep reading
// digits until strtod would give the correct float value.
double v = buffer[0] - '0';
double multiplicand = 0.1;
for (int i = 2; i < length + 2; ++i) {
double vplus = v + (buffer[i] - '0' + 1) * multiplicand;
v += (buffer[i] - '0') * multiplicand;
multiplicand *= 0.1;
if ((float)(v * e_mult) == value) {
length = i;
buffer[i + 1] = 'e';
break;
}
if (buffer[i] < '9' && (float)(vplus * e_mult) == value) {
buffer[i]++;
length = i;
buffer[i + 1] = 'e';
break;
}
}
WriteExponent(kk - 1, &buffer[0 + length + 2]);
}
}
}
}

View File

@ -16,6 +16,7 @@ extern "C" {
#endif
EXPCL_DTOOL_DTOOLBASE void pdtoa(double value, char *buffer);
EXPCL_DTOOL_DTOOLBASE void pftoa(float value, char *buffer);
#ifdef __cplusplus
}; /* end of extern "C" */

View File

@ -32,7 +32,7 @@ format_string(bool value) {
INLINE std::string
format_string(float value) {
char buffer[32];
pdtoa((double)value, buffer);
pftoa(value, buffer);
return std::string(buffer);
}

View File

@ -33,6 +33,7 @@
#undef FLOATTYPE_IS_INT
#undef STRINGIFY
#undef FLOATNAME_STR
#undef FLOATTYPE_REPR
#define FLOATTYPE double
#define FLOATNAME(ARG) ARG##d
@ -41,3 +42,15 @@
#define STRINGIFY(ARG) #ARG
#define FLOATNAME_STR(ARG) STRINGIFY(ARG##d)
#define FLOATTYPE_REPR(v, str) do { \
double v_copy = (v); \
char *into_str = (str); \
if ((double)(long long)v_copy == v_copy) { \
snprintf(into_str, 32, "%lld", (long long)v_copy); \
} else { \
pdtoa(v_copy, into_str); \
} \
} while (0)
#include "pdtoa.h"

View File

@ -33,6 +33,7 @@
#undef FLOATTYPE_IS_INT
#undef STRINGIFY
#undef FLOATNAME_STR
#undef FLOATTYPE_REPR
#define FLOATTYPE float
#define FLOATNAME(ARG) ARG##f
@ -41,3 +42,15 @@
#define STRINGIFY(ARG) #ARG
#define FLOATNAME_STR(ARG) STRINGIFY(ARG##f)
#define FLOATTYPE_REPR(v, str) do { \
float v_copy = (v); \
char *into_str = (str); \
if ((float)(int)v_copy == v_copy) { \
snprintf(into_str, 32, "%d", (int)v_copy); \
} else { \
pftoa(v_copy, into_str); \
} \
} while (0)
#include "pdtoa.h"

View File

@ -33,6 +33,7 @@
#undef FLOATTYPE_IS_INT
#undef STRINGIFY
#undef FLOATNAME_STR
#undef FLOATTYPE_REPR
#define FLOATTYPE int
#define FLOATNAME(ARG) ARG##i
@ -42,3 +43,5 @@
#define STRINGIFY(ARG) #ARG
#define FLOATNAME_STR(ARG) STRINGIFY(ARG##i)
#define FLOATTYPE_REPR(v, str) (snprintf((str), 12, "%d", (v)))

View File

@ -39,18 +39,21 @@ __reduce__(PyObject *self) const {
*/
INLINE_LINMATH std::string Extension<FLOATNAME(LMatrix3)>::
__repr__() const {
std::ostringstream out;
out << "LMatrix3" << FLOATTOKEN << "("
<< MAYBE_ZERO(_this->_m(0, 0)) << ", "
<< MAYBE_ZERO(_this->_m(0, 1)) << ", "
<< MAYBE_ZERO(_this->_m(0, 2)) << ", "
char buf[32 * 17] = "LMatrix4";
char *p = buf + strlen(buf);
*(p++) = FLOATTOKEN;
*(p++) = '(';
FLOATTYPE_REPR(_this->_m(0, 0), p);
p += strlen(p);
<< MAYBE_ZERO(_this->_m(1, 0)) << ", "
<< MAYBE_ZERO(_this->_m(1, 1)) << ", "
<< MAYBE_ZERO(_this->_m(1, 2)) << ", "
for (int i = 1; i < 9; ++i) {
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->get_data()[i], p);
p += strlen(p);
}
<< MAYBE_ZERO(_this->_m(2, 0)) << ", "
<< MAYBE_ZERO(_this->_m(2, 1)) << ", "
<< MAYBE_ZERO(_this->_m(2, 2)) << ")";
return out.str();
*(p++) = ')';
*p = '\0';
return std::string(buf, p - buf);
}

View File

@ -40,26 +40,21 @@ __reduce__(PyObject *self) const {
*/
INLINE_LINMATH std::string Extension<FLOATNAME(LMatrix4)>::
__repr__() const {
std::ostringstream out;
out << "LMatrix4" << FLOATTOKEN << "("
<< MAYBE_ZERO(_this->_m(0, 0)) << ", "
<< MAYBE_ZERO(_this->_m(0, 1)) << ", "
<< MAYBE_ZERO(_this->_m(0, 2)) << ", "
<< MAYBE_ZERO(_this->_m(0, 3)) << ", "
char buf[32 * 17] = "LMatrix4";
char *p = buf + strlen(buf);
*(p++) = FLOATTOKEN;
*(p++) = '(';
FLOATTYPE_REPR(_this->_m(0, 0), p);
p += strlen(p);
<< MAYBE_ZERO(_this->_m(1, 0)) << ", "
<< MAYBE_ZERO(_this->_m(1, 1)) << ", "
<< MAYBE_ZERO(_this->_m(1, 2)) << ", "
<< MAYBE_ZERO(_this->_m(1, 3)) << ", "
for (int i = 1; i < 16; ++i) {
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->get_data()[i], p);
p += strlen(p);
}
<< MAYBE_ZERO(_this->_m(2, 0)) << ", "
<< MAYBE_ZERO(_this->_m(2, 1)) << ", "
<< MAYBE_ZERO(_this->_m(2, 2)) << ", "
<< MAYBE_ZERO(_this->_m(2, 3)) << ", "
<< MAYBE_ZERO(_this->_m(3, 0)) << ", "
<< MAYBE_ZERO(_this->_m(3, 1)) << ", "
<< MAYBE_ZERO(_this->_m(3, 2)) << ", "
<< MAYBE_ZERO(_this->_m(3, 3)) << ")";
return out.str();
*(p++) = ')';
*p = '\0';
return std::string(buf, p - buf);
}

View File

@ -16,11 +16,19 @@
*/
INLINE_LINMATH std::string Extension<FLOATNAME(LPoint2)>::
__repr__() const {
std::ostringstream out;
out << "LPoint2" << FLOATTOKEN << "("
<< MAYBE_ZERO(_this->_v(0)) << ", "
<< MAYBE_ZERO(_this->_v(1)) << ")";
return out.str();
char buf[96] = "LPoint2";
char *p = buf + strlen(buf);
*(p++) = FLOATTOKEN;
*(p++) = '(';
FLOATTYPE_REPR(_this->_v(0), p);
p += strlen(p);
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->_v(1), p);
p += strlen(p);
*(p++) = ')';
*p = '\0';
return std::string(buf, p - buf);
}
/**

View File

@ -16,12 +16,23 @@
*/
INLINE_LINMATH std::string Extension<FLOATNAME(LPoint3)>::
__repr__() const {
std::ostringstream out;
out << "LPoint3" << FLOATTOKEN << "("
<< MAYBE_ZERO(_this->_v(0)) << ", "
<< MAYBE_ZERO(_this->_v(1)) << ", "
<< MAYBE_ZERO(_this->_v(2)) << ")";
return out.str();
char buf[128] = "LPoint3";
char *p = buf + strlen(buf);
*(p++) = FLOATTOKEN;
*(p++) = '(';
FLOATTYPE_REPR(_this->_v(0), p);
p += strlen(p);
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->_v(1), p);
p += strlen(p);
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->_v(2), p);
p += strlen(p);
*(p++) = ')';
*p = '\0';
return std::string(buf, p - buf);
}
/**

View File

@ -16,13 +16,27 @@
*/
INLINE_LINMATH std::string Extension<FLOATNAME(LPoint4)>::
__repr__() const {
std::ostringstream out;
out << "LPoint4" << FLOATTOKEN << "("
<< MAYBE_ZERO(_this->_v(0)) << ", "
<< MAYBE_ZERO(_this->_v(1)) << ", "
<< MAYBE_ZERO(_this->_v(2)) << ", "
<< MAYBE_ZERO(_this->_v(3)) << ")";
return out.str();
char buf[160] = "LPoint4";
char *p = buf + strlen(buf);
*(p++) = FLOATTOKEN;
*(p++) = '(';
FLOATTYPE_REPR(_this->_v(0), p);
p += strlen(p);
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->_v(1), p);
p += strlen(p);
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->_v(2), p);
p += strlen(p);
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->_v(3), p);
p += strlen(p);
*(p++) = ')';
*p = '\0';
return std::string(buf, p - buf);
}
/**

View File

@ -29,11 +29,19 @@
*/
INLINE_LINMATH std::string Extension<FLOATNAME(LVecBase2)>::
__repr__() const {
std::ostringstream out;
out << "LVecBase2" << FLOATTOKEN << "("
<< MAYBE_ZERO(_this->_v(0)) << ", "
<< MAYBE_ZERO(_this->_v(1)) << ")";
return out.str();
char buf[96] = "LVecBase2";
char *p = buf + strlen(buf);
*(p++) = FLOATTOKEN;
*(p++) = '(';
FLOATTYPE_REPR(_this->_v(0), p);
p += strlen(p);
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->_v(1), p);
p += strlen(p);
*(p++) = ')';
*p = '\0';
return std::string(buf, p - buf);
}
/**

View File

@ -29,12 +29,23 @@
*/
INLINE_LINMATH std::string Extension<FLOATNAME(LVecBase3)>::
__repr__() const {
std::ostringstream out;
out << "LVecBase3" << FLOATTOKEN << "("
<< MAYBE_ZERO(_this->_v(0)) << ", "
<< MAYBE_ZERO(_this->_v(1)) << ", "
<< MAYBE_ZERO(_this->_v(2)) << ")";
return out.str();
char buf[128] = "LVecBase3";
char *p = buf + strlen(buf);
*(p++) = FLOATTOKEN;
*(p++) = '(';
FLOATTYPE_REPR(_this->_v(0), p);
p += strlen(p);
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->_v(1), p);
p += strlen(p);
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->_v(2), p);
p += strlen(p);
*(p++) = ')';
*p = '\0';
return std::string(buf, p - buf);
}
/**
@ -356,3 +367,4 @@ __ceil__(PyObject *self) const {
#undef PYNUMBER_FLOATTYPE
#undef PY_AS_FLOATTYPE
#undef FLOATTYPE_TO_STR

View File

@ -29,13 +29,27 @@
*/
INLINE_LINMATH std::string Extension<FLOATNAME(LVecBase4)>::
__repr__() const {
std::ostringstream out;
out << "LVecBase4" << FLOATTOKEN << "("
<< MAYBE_ZERO(_this->_v(0)) << ", "
<< MAYBE_ZERO(_this->_v(1)) << ", "
<< MAYBE_ZERO(_this->_v(2)) << ", "
<< MAYBE_ZERO(_this->_v(3)) << ")";
return out.str();
char buf[160] = "LVecBase4";
char *p = buf + strlen(buf);
*(p++) = FLOATTOKEN;
*(p++) = '(';
FLOATTYPE_REPR(_this->_v(0), p);
p += strlen(p);
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->_v(1), p);
p += strlen(p);
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->_v(2), p);
p += strlen(p);
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->_v(3), p);
p += strlen(p);
*(p++) = ')';
*p = '\0';
return std::string(buf, p - buf);
}
/**

View File

@ -16,11 +16,19 @@
*/
INLINE_LINMATH std::string Extension<FLOATNAME(LVector2)>::
__repr__() const {
std::ostringstream out;
out << "LVector2" << FLOATTOKEN << "("
<< MAYBE_ZERO(_this->_v(0)) << ", "
<< MAYBE_ZERO(_this->_v(1)) << ")";
return out.str();
char buf[96] = "LVector2";
char *p = buf + strlen(buf);
*(p++) = FLOATTOKEN;
*(p++) = '(';
FLOATTYPE_REPR(_this->_v(0), p);
p += strlen(p);
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->_v(1), p);
p += strlen(p);
*(p++) = ')';
*p = '\0';
return std::string(buf, p - buf);
}
/**

View File

@ -16,12 +16,23 @@
*/
INLINE_LINMATH std::string Extension<FLOATNAME(LVector3)>::
__repr__() const {
std::ostringstream out;
out << "LVector3" << FLOATTOKEN << "("
<< MAYBE_ZERO(_this->_v(0)) << ", "
<< MAYBE_ZERO(_this->_v(1)) << ", "
<< MAYBE_ZERO(_this->_v(2)) << ")";
return out.str();
char buf[128] = "LVector3";
char *p = buf + strlen(buf);
*(p++) = FLOATTOKEN;
*(p++) = '(';
FLOATTYPE_REPR(_this->_v(0), p);
p += strlen(p);
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->_v(1), p);
p += strlen(p);
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->_v(2), p);
p += strlen(p);
*(p++) = ')';
*p = '\0';
return std::string(buf, p - buf);
}
/**

View File

@ -16,13 +16,27 @@
*/
INLINE_LINMATH std::string Extension<FLOATNAME(LVector4)>::
__repr__() const {
std::ostringstream out;
out << "LVector4" << FLOATTOKEN << "("
<< MAYBE_ZERO(_this->_v(0)) << ", "
<< MAYBE_ZERO(_this->_v(1)) << ", "
<< MAYBE_ZERO(_this->_v(2)) << ", "
<< MAYBE_ZERO(_this->_v(3)) << ")";
return out.str();
char buf[160] = "LVector4";
char *p = buf + strlen(buf);
*(p++) = FLOATTOKEN;
*(p++) = '(';
FLOATTYPE_REPR(_this->_v(0), p);
p += strlen(p);
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->_v(1), p);
p += strlen(p);
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->_v(2), p);
p += strlen(p);
*(p++) = ',';
*(p++) = ' ';
FLOATTYPE_REPR(_this->_v(3), p);
p += strlen(p);
*(p++) = ')';
*p = '\0';
return std::string(buf, p - buf);
}
/**

View File

@ -142,3 +142,14 @@ def test_vec2_floordiv(type):
v = type(i)
v //= -j
assert v.x == i // -j
def test_vec2_repr():
assert repr(Vec2F(0.1, 0.2)) == "LVector2f(0.1, 0.2)"
assert repr(Vec2F(0.3, 0.4)) == "LVector2f(0.3, 0.4)"
assert repr(Vec2F(-0.9999999403953552, 1.00000001)) == "LVector2f(-0.99999994, 1)"
assert repr(Vec2F(0.00000001, 0.0)) == "LVector2f(1e-8, 0)"
assert repr(Vec2D(0.1, 0.2)) == "LVector2d(0.1, 0.2)"
assert repr(Vec2D(0.3, 0.4)) == "LVector2d(0.3, 0.4)"
assert repr(Vec2D(-0.9999999403953552, 1.00000001)) == "LVector2d(-0.9999999403953552, 1.00000001)"
assert repr(Vec2D(0.00000001, 0.0)) == "LVector2d(1e-8, 0)"

View File

@ -127,3 +127,16 @@ def test_vec3_floordiv(type):
v = type(i)
v //= -j
assert v.x == i // -j
def test_vec3_repr():
assert repr(Vec3F(0.1, 0.2, 0.3)) == "LVector3f(0.1, 0.2, 0.3)"
assert repr(Vec3F(-0.9999999403953552, 1.00000001, 1)) == "LVector3f(-0.99999994, 1, 1)"
assert repr(Vec3F(-9.451235e29, 9.451234e-19, 1e-11)) == "LVector3f(-9.451235e29, 9.451234e-19, 1e-11)"
assert repr(Vec3F(0.001, 0.0001, 0.00001)) == "LVector3f(0.001, 0.0001, 0.00001)"
assert repr(Vec3F(-0.001, -0.0001, -0.00001)) == "LVector3f(-0.001, -0.0001, -0.00001)"
assert repr(Vec3D(0.1, 0.2, 0.3)) == "LVector3d(0.1, 0.2, 0.3)"
assert repr(Vec3D(-0.9999999403953552, 1.00000001, 1)) == "LVector3d(-0.9999999403953552, 1.00000001, 1)"
assert repr(Vec3D(-9.451235e29, 9.451234e-19, 1e-11)) == "LVector3d(-9.451235e29, 9.451234e-19, 1e-11)"
assert repr(Vec3D(0.001, 0.0001, 0.00001)) == "LVector3d(0.001, 0.0001, 0.00001)"
assert repr(Vec3D(-0.001, -0.0001, -0.00001)) == "LVector3d(-0.001, -0.0001, -0.00001)"

View File

@ -143,3 +143,10 @@ def test_vec4_floordiv(type):
v = type(i)
v //= -j
assert v.x == i // -j
def test_vec4_repr():
assert repr(Vec4F(0.1, 0.2, 0.3, 0.4)) == "LVector4f(0.1, 0.2, 0.3, 0.4)"
assert repr(Vec4F(-0.9999999403953552, 1.000001, 1e-8, 0.0)) == "LVector4f(-0.99999994, 1.000001, 1e-8, 0)"
assert repr(Vec4D(0.1, 0.2, 0.3, 0.4)) == "LVector4d(0.1, 0.2, 0.3, 0.4)"
assert repr(Vec4D(-0.9999999403953552, 1.00000001, 1e-8, 0.0)) == "LVector4d(-0.9999999403953552, 1.00000001, 1e-8, 0)"