Skip to content

Instantly share code, notes, and snippets.

@Quackward
Last active August 13, 2021 11:18
Show Gist options
  • Save Quackward/e79c9716a3003e8a30dea7e97995a89b to your computer and use it in GitHub Desktop.
Save Quackward/e79c9716a3003e8a30dea7e97995a89b to your computer and use it in GitHub Desktop.
// Clairvoire@gmail.com \(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