Last active
August 13, 2021 11:18
-
-
Save Quackward/e79c9716a3003e8a30dea7e97995a89b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// [email protected] \(v ` >`)_v | |
// feel free to use as you see fit, attribution appreciated but not required, not fit for any purpose | |
// if `lexicalCheck is false` | |
// returns TRUE if the printed version of a float is being approximated when we limit the print | |
// to a certain number of decimal places, as defined by `decimalPlaceLimit`, | |
// returns FALSE if the printed version is 100% accurate to the float itself. | |
// if `lexicalCheck is true` (expensive) | |
// adds the following check after the prior check, if the function is about to return TRUE: | |
// the value is printed using the decimal rounding, then converted back to a float; | |
// if this parsed value is the same as the input value, the function returns FALSE instead of TRUE | |
// NOTE: This WILL return true for values like 0.1 without `lexicalCheck`, as they are approximated | |
// at extremely small fractional parts in floats: 0.1 is actually 0.100000000000000005551115... | |
// but the lexical check will avoid this situation by checking if the displayed value is mapped | |
// to the floating representation during the string-to-float process | |
// truth table: | |
// f( val, dec ) -> ret | |
// ----------------------- | |
// f( 0.5, 3 ) -> 0 | |
// f( 0.25, 3 ) -> 0 | |
// f( 0.125, 3 ) -> 0 | |
// f( 0.0625, 3 ) -> 1 | |
bool isFloatRoundingApprox(float value, uint32_t decimalPlaceLimit, bool lexicalCheck = false) { | |
// 0.0 is special, it's the only value for which our assumption about the exponent kind of backfires | |
// 1.0 and 0.5 are... not special, these are just kind of common, may as well make quick outs for them | |
if(value == 0.f || value == 1.f || (value == 0.5f && decimalPlaceLimit > 0)) | |
return false; | |
uint32_t bits = *(uint32_t*)(&value); | |
uint32_t exponent = (bits>>23)&0xFF; | |
uint32_t mantissa = bits&0x7FFFFF; | |
uint32_t lsb; | |
#ifdef _MSC_VER | |
{ | |
unsigned long result; // has to be this type | |
_BitScanForward(&result, mantissa); | |
lsb = result; | |
} | |
#elif __GNUC__ | |
#pragma message ("branch untested, if it works, feel free to delete this!") | |
lsb = __builtin_ffs(mantissa); | |
#else | |
#pragma message ("branch untested, if it works, feel free to delete this!") | |
#pragma message ("using an unoptimized fallback") | |
lsb = 0; // treat as lsb until the end | |
for (uint32_t i = 0; i < 23; ++i) { | |
if((mantissa>>i)&1u) | |
break; | |
++lsb; | |
} | |
lsb = (lsb + 1) % 23; // 0 to 22 lsb | |
#endif | |
// invert so we get the distance from the left instead of the right | |
lsb = (23 - lsb); | |
if(lsb == 23) | |
lsb = 0; | |
// takes advantage of a property I noticed (that hopefully is true?) regarding float representation, | |
// where: | |
// [index of the least set bit in the mantissa] - ([exponent] - 127) | |
// defines how many digits "deep" into the fractional part our value's entropy lies (or uh something like that?) | |
// we can use this to DIRECTLY gather if our decimal point is being rounded due to the fact that... | |
// once we touch a digit, it can NEVER become insignificant again (this however does NOT hold for 0.0) | |
int32_t depth = int32_t(lsb) - (int32_t(exponent) - 127); | |
bool result = depth > int32_t(decimalPlaceLimit); | |
if (result && lexicalCheck) { | |
char format[8]; | |
format[0] = '%'; | |
format[1] = '.'; | |
if(decimalPlaceLimit >= 10){ | |
format[2] = '0' + decimalPlaceLimit/10; | |
format[3] = '0' + decimalPlaceLimit%10; | |
format[4] = 'f'; | |
format[5] = 0; | |
} else { | |
format[2] = '0' + decimalPlaceLimit; | |
format[3] = 'f'; | |
format[4] = 0; | |
} | |
char buffer[32]; | |
snprintf(buffer, 32, format, value); | |
result = !(value == strtof(buffer, NULL)); | |
} | |
return result; | |
} | |
// you can disable thread safety by setting this to 0 instead of 1, it may improve performance if you don't need it | |
#define THREAD_SAFE 1 | |
// this prints a float to a string, appending '~' if rounding occurs beyond `decimalPlaces`, appends ' ' otherwise | |
// NOTE: returned string is valid until next time this function is called within this thread (or among all threads if THREAD_SAFE is 0) | |
// if you need it longer than that, you'll need to copy it somewhere | |
char * floatApproxToString(float value, bool plusSign, bool leadWithZeroes, bool alignLeft, uint32 minIntDigitCount, uint32 decimalPlaces, bool lexicalCheck = false) { | |
static_assert(std::numeric_limits<float>::is_iec559, "Hello from 20XX"); | |
assert(minIntDigitCount <= 99); // limits impact how format string are constructed, if these change the impl must be updated | |
assert(decimalPlaces <= 99); | |
const uint32 formatSize = 16; | |
#if THREAD_SAFE | |
thread_local int size = 0; | |
thread_local char * buffer = NULL; | |
#else | |
static int size = 0; | |
static char * buffer = NULL; | |
#endif | |
char format[formatSize]; | |
// building the format string byte by byte | |
uint32 n = 1; | |
format[0] = '%'; | |
if(alignLeft) | |
format[n++] = '-'; | |
if(leadWithZeroes) | |
format[n++] = '0'; | |
if((2+minIntDigitCount+decimalPlaces) / 10) | |
format[n++] = '0' + (2+minIntDigitCount+decimalPlaces)/10; | |
format[n++] = '0' + (2+minIntDigitCount+decimalPlaces)%10; | |
format[n++] = '.'; | |
if(decimalPlaces > 10) | |
format[n++] = '0' + decimalPlaces/10; | |
format[n++] = '0' + decimalPlaces%10; | |
format[n++] = 'f'; | |
bool approx = isFloatRoundingApprox(value, decimalPlaces, lexicalCheck); | |
if(approx) | |
format[n++] = '~'; | |
else | |
format[n++] = ' '; | |
format[n] = '\0'; | |
assert(n < formatSize); | |
// resize our buffer if we're lacking enough room. | |
int newSize = snprintf(buffer, size, format, value); | |
if (newSize + 1 > size) { | |
// didn't have enough room; resize to fit then use snprintf() again | |
size = newSize + 1; | |
if(buffer != NULL) | |
delete [] buffer; | |
buffer = new (std::nothrow) char[size]; | |
assert(buffer != NULL); | |
newSize = snprintf(buffer, size, format, value); // second time should succeed without fail | |
assert(newSize > 0); | |
} | |
// if aligning left, we need to shift the ~ left a bit | |
if (alignLeft && approx) { | |
n = decimalPlaces; | |
while (buffer[n] != ' ' && buffer[n] != 0) | |
++n; | |
if(buffer[n] == ' ') | |
buffer[n] = '~'; | |
++n; | |
while (buffer[n] != '~' && buffer[n] != 0) | |
++n; | |
if(buffer[n] == '~') | |
buffer[n] = ' '; | |
} | |
return buffer; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment