From 36ad6131f4d292f62db64236d1d7f9f70eb26b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87?= Date: Sat, 31 Jan 2026 18:45:04 -0800 Subject: [PATCH] Added: bx::formatHumanNumber. (#368) --- include/bx/string.h | 10 +++++++ src/string.cpp | 69 ++++++++++++++++++++++++++++++++++++++++++- tests/string_test.cpp | 38 ++++++++++++++++++++++-- 3 files changed, 114 insertions(+), 3 deletions(-) diff --git a/include/bx/string.h b/include/bx/string.h index 4180177..4f99a97 100644 --- a/include/bx/string.h +++ b/include/bx/string.h @@ -450,6 +450,16 @@ namespace bx template void stringPrintf(Ty& _out, const char* _format, ...); + /// Format number to human readable representation. + /// + /// @param[out] _out Output string. + /// @param[in] _count Maximum output string count. + /// @param[in] _value Value. + /// @param[in] _numFrac Number of fraction digits. + /// @returns Length of output string. + /// + int32_t formatHumanNumber(char* _out, int32_t _count, double _value, int32_t _numFrac); + /// Convert size in bytes to human readable string kibi units. int32_t prettify(char* _out, int32_t _count, uint64_t _value, Units::Enum _units = Units::Kibi); diff --git a/src/string.cpp b/src/string.cpp index 7c50df1..1f2e9fc 100644 --- a/src/string.cpp +++ b/src/string.cpp @@ -1280,6 +1280,70 @@ namespace bx return total; } + int32_t formatHumanNumber(char* _out, int32_t _count, double _value, int32_t _numFrac) + { + char temp[64]; + int32_t len = snprintf(temp, sizeof(temp), "%.*f", _numFrac, _value); + int32_t intPartLen = len; + + if (len >= _numFrac+1 + && '.' == temp[len-_numFrac-1]) + { + intPartLen = len-_numFrac-1; + + bool zero = true; + + for (int32_t ii = _numFrac; 0 < ii && zero; --ii) + { + zero &= temp[len-ii] == '0'; + } + + if (zero) + { + temp[len-_numFrac-1] = '\0'; + len = intPartLen; + } + } + + const int32_t fracPartLen = len - intPartLen; + const int32_t commas = (intPartLen > 3) ? (intPartLen - 1) / 3 : 0; + const int32_t total = intPartLen + fracPartLen + commas; + + if (_count < total) + { + if (0 < _count) + { + _out[0] = '\0'; + } + + return 0; + } + + char* out = _out + total; + *out = '\0'; + + if (0 != fracPartLen) + { + out -= fracPartLen; + memCopy(out, &temp[intPartLen], fracPartLen); + } + + int32_t group = 0; + for (int32_t ii = intPartLen - 1; ii >= 0; --ii) + { + *--out = temp[ii]; + + if (3 == ++group + && 0 < ii) + { + *--out = ','; + group = 0; + } + } + + return total; + } + static const char s_units[] = { 'B', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' }; template @@ -1295,7 +1359,10 @@ namespace bx ++idx; } - return snprintf(_out, _count, "%0.2f %c%c%c", value + char human[32]; + formatHumanNumber(human, sizeof(human), value, 2); + + return snprintf(_out, _count, "%s %c%c%c", human , fn(s_units[idx]) , idx > 0 ? KiloCh0 : '\0' , KiloCh1 diff --git a/tests/string_test.cpp b/tests/string_test.cpp index 03d40e1..fcbfcc7 100644 --- a/tests/string_test.cpp +++ b/tests/string_test.cpp @@ -24,10 +24,10 @@ TEST_CASE("prettify", "[string]") { char tmp[1024]; prettify(tmp, BX_COUNTOF(tmp), 4000, bx::Units::Kilo); - REQUIRE(0 == bx::strCmp(tmp, "4.00 kB") ); + REQUIRE(0 == bx::strCmp(tmp, "4 kB") ); prettify(tmp, BX_COUNTOF(tmp), 4096, bx::Units::Kibi); - REQUIRE(0 == bx::strCmp(tmp, "4.00 KiB") ); + REQUIRE(0 == bx::strCmp(tmp, "4 KiB") ); } TEST_CASE("chars", "[string]") @@ -762,3 +762,37 @@ TEST(tinystl_string_assign) CHECK( other.size() == 0 ); } } + +bool testFormatHumanNumber(bx::StringView _expected, double _value, int32_t _numFrac, int32_t _bufferSize = 32) +{ + char* tmp = (char*)BX_STACK_ALLOC(_bufferSize); + int32_t total = bx::formatHumanNumber(tmp, _bufferSize, _value, _numFrac); + + bx::StringView human(tmp, total); + + const bool result = 0 == bx::strCmp(human, _expected); + + if (!result) + { + DBG( + "expected: '%S' (len: %d), human: '%S' (len: %d)" + , &_expected + , _expected.getLength() + , &human + , human.getLength() + ); + } + + return result; +} + +TEST_CASE("formatHumanNumber", "[string]") +{ + REQUIRE(testFormatHumanNumber("1,389,983,113.89", 1389983113.891389, 2) ); + REQUIRE(testFormatHumanNumber("13,899,831.1389", 13899831.1389, 4) ); + REQUIRE(testFormatHumanNumber("1,389.10", 1389.1, 2) ); + REQUIRE(testFormatHumanNumber("1,389", 1389.0, 8) ); + REQUIRE(testFormatHumanNumber("0", 0.0, 2) ); + + REQUIRE(testFormatHumanNumber("", 1389983113.891389, 2, 4) ); +}