diff --git a/Foundation/include/Poco/Config.h b/Foundation/include/Poco/Config.h index e0610ccf03..6e953e2426 100644 --- a/Foundation/include/Poco/Config.h +++ b/Foundation/include/Poco/Config.h @@ -205,6 +205,29 @@ #define POCO_HAVE_ATOMIC_SHARED_PTR false #endif +// Float std::to_chars/from_chars support detection. +// - GCC 11+ (libstdc++): full support since GCC 11 +// - MSVC 19.24+: full support since VS 2019 16.4 +// - Apple Clang: requires macOS 26.0+ deployment target (availability annotations) +// - Non-Apple libc++ (Android NDK, FreeBSD, Emscripten): requires libc++ 20+ +// (LLVM 20); libc++ 17-19 have float from_chars overloads explicitly deleted. +// On macOS, even non-Apple LLVM (Homebrew) uses system libc++ headers with +// Apple availability annotations, so __APPLE__ must also be excluded here. +#if defined(__APPLE__) +#include +#endif +#if defined(__cpp_lib_to_chars) && __cpp_lib_to_chars >= 202306L + #define POCO_HAS_FLOAT_CHARCONV 1 +#elif defined(_MSC_VER) && _MSC_VER >= 1924 + #define POCO_HAS_FLOAT_CHARCONV 1 +#elif defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 11 + #define POCO_HAS_FLOAT_CHARCONV 1 +#elif defined(__APPLE__) && defined(MAC_OS_X_VERSION_MIN_REQUIRED) && MAC_OS_X_VERSION_MIN_REQUIRED >= 260000 + #define POCO_HAS_FLOAT_CHARCONV 1 +#elif defined(_LIBCPP_VERSION) && _LIBCPP_VERSION >= 200000 && !defined(__APPLE__) + #define POCO_HAS_FLOAT_CHARCONV 1 +#endif + // Option to silence deprecation warnings. #ifndef POCO_SILENCE_DEPRECATED #define POCO_DEPRECATED(reason) [[deprecated(reason)]] diff --git a/Foundation/include/Poco/NumberFormatter.h b/Foundation/include/Poco/NumberFormatter.h index 79549f409e..0d0fb9ec0f 100644 --- a/Foundation/include/Poco/NumberFormatter.h +++ b/Foundation/include/Poco/NumberFormatter.h @@ -690,7 +690,7 @@ inline bool NumberFormatter::isEnabled(Options options, Options opt) inline std::string NumberFormatter::format(int value) { std::string result; - intToStr(value, 10, result); + if (!intToStr(value, 10, result)) result.clear(); return result; } @@ -698,7 +698,7 @@ inline std::string NumberFormatter::format(int value) inline std::string NumberFormatter::format(int value, int width) { std::string result; - intToStr(value, 10, result, false, width, ' '); + if (!intToStr(value, 10, result, false, width, ' ')) result.clear(); return result; } @@ -706,7 +706,7 @@ inline std::string NumberFormatter::format(int value, int width) inline std::string NumberFormatter::format0(int value, int width) { std::string result; - intToStr(value, 10, result, false, width, '0'); + if (!intToStr(value, 10, result, false, width, '0')) result.clear(); return result; } @@ -726,7 +726,7 @@ inline std::string NumberFormatter::formatHex(int value, int width, Options opti inline std::string NumberFormatter::format(unsigned value) { std::string result; - intToStr(value, 10, result); + if (!intToStr(value, 10, result)) result.clear(); return result; } @@ -734,7 +734,7 @@ inline std::string NumberFormatter::format(unsigned value) inline std::string NumberFormatter::format(unsigned value, int width) { std::string result; - intToStr(value, 10, result, false, width, ' '); + if (!intToStr(value, 10, result, false, width, ' ')) result.clear(); return result; } @@ -742,7 +742,7 @@ inline std::string NumberFormatter::format(unsigned value, int width) inline std::string NumberFormatter::format0(unsigned int value, int width) { std::string result; - intToStr(value, 10, result, false, width, '0'); + if (!intToStr(value, 10, result, false, width, '0')) result.clear(); return result; } @@ -750,7 +750,7 @@ inline std::string NumberFormatter::format0(unsigned int value, int width) inline std::string NumberFormatter::formatHex(unsigned value, Options options) { std::string result; - intToStr(value, 0x10, result, isEnabled(options, Options::HEX_PREFIX), -1, ' ', 0, isEnabled(options, Options::HEX_LOWERCASE)); + if (!intToStr(value, 0x10, result, isEnabled(options, Options::HEX_PREFIX), -1, ' ', 0, isEnabled(options, Options::HEX_LOWERCASE))) result.clear(); return result; } @@ -758,7 +758,7 @@ inline std::string NumberFormatter::formatHex(unsigned value, Options options) inline std::string NumberFormatter::formatHex(unsigned value, int width, Options options) { std::string result; - intToStr(value, 0x10, result, isEnabled(options, Options::HEX_PREFIX), width, '0', 0, isEnabled(options, Options::HEX_LOWERCASE)); + if (!intToStr(value, 0x10, result, isEnabled(options, Options::HEX_PREFIX), width, '0', 0, isEnabled(options, Options::HEX_LOWERCASE))) result.clear(); return result; } @@ -766,7 +766,7 @@ inline std::string NumberFormatter::formatHex(unsigned value, int width, Options inline std::string NumberFormatter::format(long value) { std::string result; - intToStr(value, 10, result); + if (!intToStr(value, 10, result)) result.clear(); return result; } @@ -774,7 +774,7 @@ inline std::string NumberFormatter::format(long value) inline std::string NumberFormatter::format(long value, int width) { std::string result; - intToStr(value, 10, result, false, width, ' '); + if (!intToStr(value, 10, result, false, width, ' ')) result.clear(); return result; } @@ -782,7 +782,7 @@ inline std::string NumberFormatter::format(long value, int width) inline std::string NumberFormatter::format0(long value, int width) { std::string result; - intToStr(value, 10, result, false, width, '0'); + if (!intToStr(value, 10, result, false, width, '0')) result.clear(); return result; } @@ -802,7 +802,7 @@ inline std::string NumberFormatter::formatHex(long value, int width, Options opt inline std::string NumberFormatter::format(unsigned long value) { std::string result; - intToStr(value, 10, result); + if (!intToStr(value, 10, result)) result.clear(); return result; } @@ -810,7 +810,7 @@ inline std::string NumberFormatter::format(unsigned long value) inline std::string NumberFormatter::format(unsigned long value, int width) { std::string result; - intToStr(value, 10, result, false, width, ' '); + if (!intToStr(value, 10, result, false, width, ' ')) result.clear(); return result; } @@ -818,7 +818,7 @@ inline std::string NumberFormatter::format(unsigned long value, int width) inline std::string NumberFormatter::format0(unsigned long value, int width) { std::string result; - intToStr(value, 10, result, false, width, '0'); + if (!intToStr(value, 10, result, false, width, '0')) result.clear(); return result; } @@ -826,7 +826,7 @@ inline std::string NumberFormatter::format0(unsigned long value, int width) inline std::string NumberFormatter::formatHex(unsigned long value, Options options) { std::string result; - intToStr(value, 0x10, result, isEnabled(options, Options::HEX_PREFIX), -1, ' ', 0, isEnabled(options, Options::HEX_LOWERCASE)); + if (!intToStr(value, 0x10, result, isEnabled(options, Options::HEX_PREFIX), -1, ' ', 0, isEnabled(options, Options::HEX_LOWERCASE))) result.clear(); return result; } @@ -834,7 +834,7 @@ inline std::string NumberFormatter::formatHex(unsigned long value, Options optio inline std::string NumberFormatter::formatHex(unsigned long value, int width, Options options) { std::string result; - intToStr(value, 0x10, result, isEnabled(options, Options::HEX_PREFIX), width, '0', 0, isEnabled(options, Options::HEX_LOWERCASE)); + if (!intToStr(value, 0x10, result, isEnabled(options, Options::HEX_PREFIX), width, '0', 0, isEnabled(options, Options::HEX_LOWERCASE))) result.clear(); return result; } @@ -845,7 +845,7 @@ inline std::string NumberFormatter::formatHex(unsigned long value, int width, Op inline std::string NumberFormatter::format(long long value) { std::string result; - intToStr(value, 10, result); + if (!intToStr(value, 10, result)) result.clear(); return result; } @@ -853,7 +853,7 @@ inline std::string NumberFormatter::format(long long value) inline std::string NumberFormatter::format(long long value, int width) { std::string result; - intToStr(value, 10, result, false, width, ' '); + if (!intToStr(value, 10, result, false, width, ' ')) result.clear(); return result; } @@ -861,7 +861,7 @@ inline std::string NumberFormatter::format(long long value, int width) inline std::string NumberFormatter::format0(long long value, int width) { std::string result; - intToStr(value, 10, result, false, width, '0'); + if (!intToStr(value, 10, result, false, width, '0')) result.clear(); return result; } @@ -881,7 +881,7 @@ inline std::string NumberFormatter::formatHex(long long value, int width, Option inline std::string NumberFormatter::format(unsigned long long value) { std::string result; - intToStr(value, 10, result); + if (!intToStr(value, 10, result)) result.clear(); return result; } @@ -889,7 +889,7 @@ inline std::string NumberFormatter::format(unsigned long long value) inline std::string NumberFormatter::format(unsigned long long value, int width) { std::string result; - intToStr(value, 10, result, false, width, ' '); + if (!intToStr(value, 10, result, false, width, ' ')) result.clear(); return result; } @@ -897,7 +897,7 @@ inline std::string NumberFormatter::format(unsigned long long value, int width) inline std::string NumberFormatter::format0(unsigned long long value, int width) { std::string result; - intToStr(value, 10, result, false, width, '0'); + if (!intToStr(value, 10, result, false, width, '0')) result.clear(); return result; } @@ -905,7 +905,7 @@ inline std::string NumberFormatter::format0(unsigned long long value, int width) inline std::string NumberFormatter::formatHex(unsigned long long value, Options options) { std::string result; - intToStr(value, 0x10, result, isEnabled(options, Options::HEX_PREFIX), -1, ' ', 0, isEnabled(options, Options::HEX_LOWERCASE)); + if (!intToStr(value, 0x10, result, isEnabled(options, Options::HEX_PREFIX), -1, ' ', 0, isEnabled(options, Options::HEX_LOWERCASE))) result.clear(); return result; } @@ -913,7 +913,7 @@ inline std::string NumberFormatter::formatHex(unsigned long long value, Options inline std::string NumberFormatter::formatHex(unsigned long long value, int width, Options options) { std::string result; - intToStr(value, 0x10, result, isEnabled(options, Options::HEX_PREFIX), width, '0', 0, isEnabled(options, Options::HEX_LOWERCASE)); + if (!intToStr(value, 0x10, result, isEnabled(options, Options::HEX_PREFIX), width, '0', 0, isEnabled(options, Options::HEX_LOWERCASE))) result.clear(); return result; } @@ -924,7 +924,7 @@ inline std::string NumberFormatter::formatHex(unsigned long long value, int widt inline std::string NumberFormatter::format(Int64 value) { std::string result; - intToStr(value, 10, result); + if (!intToStr(value, 10, result)) result.clear(); return result; } @@ -932,7 +932,7 @@ inline std::string NumberFormatter::format(Int64 value) inline std::string NumberFormatter::format(Int64 value, int width) { std::string result; - intToStr(value, 10, result, false, width, ' '); + if (!intToStr(value, 10, result, false, width, ' ')) result.clear(); return result; } @@ -940,7 +940,7 @@ inline std::string NumberFormatter::format(Int64 value, int width) inline std::string NumberFormatter::format0(Int64 value, int width) { std::string result; - intToStr(value, 10, result, false, width, '0'); + if (!intToStr(value, 10, result, false, width, '0')) result.clear(); return result; } @@ -960,7 +960,7 @@ inline std::string NumberFormatter::formatHex(long long value, int width, Option inline std::string NumberFormatter::format(UInt64 value) { std::string result; - intToStr(value, 10, result); + if (!intToStr(value, 10, result)) result.clear(); return result; } @@ -968,7 +968,7 @@ inline std::string NumberFormatter::format(UInt64 value) inline std::string NumberFormatter::format(UInt64 value, int width) { std::string result; - intToStr(value, 10, result, false, width, ' '); + if (!intToStr(value, 10, result, false, width, ' ')) result.clear(); return result; } @@ -976,7 +976,7 @@ inline std::string NumberFormatter::format(UInt64 value, int width) inline std::string NumberFormatter::format0(UInt64 value, int width) { std::string result; - intToStr(value, 10, result, false, width, '0'); + if (!intToStr(value, 10, result, false, width, '0')) result.clear(); return result; } @@ -984,7 +984,7 @@ inline std::string NumberFormatter::format0(UInt64 value, int width) inline std::string NumberFormatter::formatHex(UInt64 value, Options options) { std::string result; - intToStr(value, 0x10, result, isEnabled(options, Options::HEX_PREFIX), -1, ' ', 0, isEnabled(options, Options::HEX_LOWERCASE)); + if (!intToStr(value, 0x10, result, isEnabled(options, Options::HEX_PREFIX), -1, ' ', 0, isEnabled(options, Options::HEX_LOWERCASE))) result.clear(); return result; } @@ -992,7 +992,7 @@ inline std::string NumberFormatter::formatHex(UInt64 value, Options options) inline std::string NumberFormatter::formatHex(UInt64 value, int width, Options options) { std::string result; - intToStr(value, 0x10, result, isEnabled(options, Options::HEX_PREFIX), width, '0', 0, isEnabled(options, Options::HEX_LOWERCASE)); + if (!intToStr(value, 0x10, result, isEnabled(options, Options::HEX_PREFIX), width, '0', 0, isEnabled(options, Options::HEX_LOWERCASE))) result.clear(); return result; } diff --git a/Foundation/include/Poco/NumericString.h b/Foundation/include/Poco/NumericString.h index 1ab79df46c..e738d03426 100644 --- a/Foundation/include/Poco/NumericString.h +++ b/Foundation/include/Poco/NumericString.h @@ -19,7 +19,7 @@ #include "Poco/Foundation.h" -#include "Poco/Buffer.h" +#include "Poco/Exception.h" #include "Poco/FPEnvironment.h" #ifdef min #undef min @@ -27,33 +27,28 @@ #ifdef max #undef max #endif -#include -#include #include +#include +#include +#include +#include #if !defined(POCO_NO_LOCALE) #include #endif +#include #include -#if defined(POCO_NOINTMAX) -typedef Poco::UInt64 uintmax_t; -typedef Poco::Int64 intmax_t; -#endif -#if !defined (INTMAX_MAX) -#define INTMAX_MAX std::numeric_limits::max() -#endif -#ifdef POCO_COMPILER_MSVC -#pragma warning(push) -#pragma warning(disable : 4146) -#endif // POCO_COMPILER_MSVC -// binary numbers are supported, thus 64 (bits) + 1 (string terminating zero) + 2 (hex prefix) -#define POCO_MAX_INT_STRING_LEN (67) -// value from strtod.cc (double_conversion::kMaxSignificantDecimalDigits) -#define POCO_MAX_FLT_STRING_LEN 780 +/// Maximum length of an integer formatted as a string. +/// 64 binary digits + sign + NUL + padding margin = 67. +inline constexpr int POCO_MAX_INT_STRING_LEN = 67; + +/// Maximum length of a floating-point formatted string. +/// Matches double_conversion::kMaxSignificantDecimalDigits. +inline constexpr int POCO_MAX_FLT_STRING_LEN = 780; -#define POCO_FLT_INF "inf" -#define POCO_FLT_NAN "nan" -#define POCO_FLT_EXP 'e' +inline constexpr char POCO_FLT_INF[] = "inf"; +inline constexpr char POCO_FLT_NAN[] = "nan"; +inline constexpr char POCO_FLT_EXP = 'e'; namespace Poco { @@ -74,25 +69,23 @@ inline bool isIntOverflow(From val) { poco_assert_dbg (std::numeric_limits::is_integer); poco_assert_dbg (std::numeric_limits::is_integer); - bool ret; - if (std::numeric_limits::is_signed) + if constexpr (std::numeric_limits::is_signed) { - ret = (!std::numeric_limits::is_signed && + return (!std::numeric_limits::is_signed && (uintmax_t)val > (uintmax_t)INTMAX_MAX) || (intmax_t)val < (intmax_t)std::numeric_limits::min() || (intmax_t)val > (intmax_t)std::numeric_limits::max(); } else { - ret = isNegative(val) || + return isNegative(val) || (uintmax_t)val > (uintmax_t)std::numeric_limits::max(); } - return ret; } template -bool safeMultiply(R& result, F f, S s) +[[nodiscard]] bool safeMultiply(R& result, F f, S s) { using CT = std::common_type_t; auto cast = [](auto v) { return static_cast(v); }; @@ -103,56 +96,64 @@ bool safeMultiply(R& result, F f, S s) return true; } - if (f > 0) + if constexpr (std::is_signed_v || std::is_signed_v) { - if (s > 0) + if (f > 0) { - if (cast(f) > (cast(std::numeric_limits::max()) / cast(s))) - return false; + if (s > 0) + { + if (cast(f) > (cast(std::numeric_limits::max()) / cast(s))) + return false; + } + else + { + if (cast(s) < (cast(std::numeric_limits::min()) / cast(f))) + return false; + } } else { - if (cast(s) < (cast(std::numeric_limits::min()) / cast(f))) - return false; + if (s > 0) + { + if (cast(f) < (cast(std::numeric_limits::min()) / cast(s))) + return false; + } + else + { + if (cast(s) < (cast(std::numeric_limits::max()) / cast(f))) + return false; + } } } else { - if (s > 0) - { - if (cast(f) < (cast(std::numeric_limits::min()) / cast(s))) - return false; - } - else - { - if (cast(s) < (cast(std::numeric_limits::max()) / cast(f))) - return false; - } + // Both f and s are unsigned (and non-zero at this point) + if (cast(f) > (cast(std::numeric_limits::max()) / cast(s))) + return false; } result = f * s; return true; } -template -inline T& isSafeIntCast(F from) +template +[[nodiscard]] inline bool isSafeIntCast(From from) /// Returns true if it is safe to cast - /// integer from F to T. + /// integer from From to To. { - if (!isIntOverflow(from)) return true; - return false; + return !isIntOverflow(from); } -template -inline T& safeIntCast(F from, T& to) +template +inline To& safeIntCast(From from, To& to) /// Returns cast value if it is safe - /// to cast integer from F to T, + /// to cast integer from From to To, /// otherwise throws BadCastException. { - if (!isIntOverflow(from)) + if (!isIntOverflow(from)) { - to = static_cast(from); + to = static_cast(from); return to; } throw BadCastException("safeIntCast: Integer overflow"); @@ -187,8 +188,9 @@ inline char thousandSeparator() // template -bool strToInt(const char* pStr, I& outResult, short base, char thSep = ',') - /// Converts zero-terminated character array to integer number; +[[nodiscard]] bool strToInt(const char* begin, const char* end, I& outResult, short base, char thSep = ',') + /// Converts character range [begin, end) to integer number; + /// begin must point past any leading whitespace. /// Thousand separators are recognized for base10 and current locale; /// they are silently skipped and not verified for correct positioning. /// It is not allowed to convert a negative number to anything except @@ -200,81 +202,66 @@ bool strToInt(const char* pStr, I& outResult, short base, char thSep = ',') { poco_assert_dbg (base == 2 || base == 8 || base == 10 || base == 16); - if (!pStr) return false; - while (std::isspace(*pStr)) ++pStr; - if ((*pStr == '\0') || ((*pStr == '-') && ((base != 10) || (std::is_unsigned::value)))) + if (begin >= end) return false; + if ((*begin == '-') && ((base != 10) || std::is_unsigned_v)) return false; - bool negative = false; - if ((base == 10) && (*pStr == '-')) - { - if (!std::numeric_limits::is_signed) return false; - negative = true; - ++pStr; - } - else if (*pStr == '+') ++pStr; - - // numbers are parsed as unsigned, for negative numbers the sign is applied after parsing - // overflow is checked in every parse step - uintmax_t limitCheck = std::numeric_limits::max(); - if (negative) ++limitCheck; - uintmax_t result = 0; - unsigned char add = 0; - for (; *pStr != '\0'; ++pStr) + // Skip '+' sign (std::from_chars doesn't accept it) + const char* start = begin; + if (*start == '+') ++start; + if (start >= end) return false; // reject bare sign without digits + + // Try std::from_chars -- handles sign, overflow, and all bases. + const auto [ptr, ec] = std::from_chars(start, end, outResult, base); + if (ec == std::errc() && ptr == end) + return true; + + // If from_chars reported overflow, invalid input, or no thSep is configured, + // there's nothing the slow path can do differently. + if (ec != std::errc() || !thSep || base != 10) + return false; + + // from_chars consumed some digits then stopped at a non-digit character. + // The input may contain thousand separators -- strip them and retry. + char cleaned[POCO_MAX_INT_STRING_LEN]; + char* dst = cleaned; + for (const char* src = start; src < end; ++src) { - if (*pStr == thSep) + if (*src != thSep) { - if (base == 10) continue; - // thousand separators only allowed for base 10 - return false; + if (static_cast(dst - cleaned) >= sizeof(cleaned) - 1) + return false; // input too long + *dst++ = *src; } - if (result > (limitCheck / base)) return false; - if (!safeMultiply(result, result, base)) return false; - switch (*pStr) - { - case '0': case '1': case '2': case '3': - case '4': case '5': case '6': case '7': - add = (*pStr - '0'); - break; - - case '8': case '9': - if ((base == 10) || (base == 0x10)) add = (*pStr - '0'); - else return false; - break; - - case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': - if (base != 0x10) return false; - add = (*pStr - 'a') + 10; - break; - - case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': - if (base != 0x10) return false; - add = (*pStr - 'A') + 10; - break; - - default: - return false; - } - if ((limitCheck - static_cast(result)) < add) return false; - result += add; } - if (negative && (base == 10)) - outResult = static_cast(-result); - else - outResult = static_cast(result); + if (dst == cleaned) return false; // nothing left after stripping - return true; + const auto [ptr2, ec2] = std::from_chars(cleaned, dst, outResult, base); + return ec2 == std::errc() && ptr2 == dst; } template -bool strToInt(const std::string& str, I& result, short base, char thSep = ',') - /// Converts string to integer number; - /// This is a wrapper function, for details see see the - /// bool strToInt(const char*, I&, short, char) implementation. +[[nodiscard]] bool strToInt(const char* pStr, I& outResult, short base, char thSep = ',') + /// Converts zero-terminated character array to integer number. + /// Skips leading whitespace, then delegates to the range-based overload. { - return strToInt(str.c_str(), result, base, thSep); + if (!pStr) return false; + while (std::isspace(static_cast(*pStr))) ++pStr; + return strToInt(pStr, pStr + std::strlen(pStr), outResult, base, thSep); +} + + +template +[[nodiscard]] bool strToInt(const std::string& str, I& result, short base, char thSep = ',') + /// Converts string to integer number. + /// Avoids strlen by using the known string length directly. +{ + const char* const end = str.data() + str.size(); + const char* begin = str.data(); + while (begin < end && std::isspace(static_cast(*begin))) ++begin; + return strToInt(begin, end, result, base, thSep); } @@ -350,13 +337,38 @@ namespace Impl { const char* _beg; char* _cur; const char* _end; -}; + }; + +/// Digit-pairs lookup table for the "two-digits-at-a-time" integer-to-string +/// technique. Instead of extracting one digit per iteration (value / 10), +/// this approach extracts two digits at once (value / 100) and uses the +/// remainder (0-99) as an index into this 200-byte table, halving the +/// number of expensive integer divisions. +/// +/// The technique is widely used in high-performance formatting libraries +/// including {fmt}, jemalloc, and Facebook Folly. +/// See also: https://github.com/miloyip/itoa-benchmark +inline constexpr char kDigitPairs[201] = + "00010203040506070809" + "10111213141516171819" + "20212223242526272829" + "30313233343536373839" + "40414243444546474849" + "50515253545556575859" + "60616263646566676869" + "70717273747576777879" + "80818283848586878889" + "90919293949596979899"; + +/// Direct digit tables for non-base-10 paths +inline constexpr char kDigitsUpper[] = "0123456789ABCDEF"; +inline constexpr char kDigitsLower[] = "0123456789abcdef"; } // namespace Impl template -bool intToStr(T value, +[[nodiscard]] bool intToStr(T value, unsigned short base, char* result, std::size_t& size, @@ -376,7 +388,7 @@ bool intToStr(T value, { if constexpr (std::is_signed_v) { - poco_assert_dbg (((value < 0) && (base == 10)) || (value >= 0)); + poco_assert_dbg(((value < 0) && (base == 10)) || (value >= 0)); } if (base < 2 || base > 0x10) @@ -385,61 +397,189 @@ bool intToStr(T value, return false; } - Impl::Ptr ptr(result, size); - int thCount = 0; - T tmpVal; - do + // size is used as buffer capacity (in) and written length (out). + const std::size_t capacity = size; + // Guard against buffer overflow in padding loops (phases 3-5). + if (width >= static_cast(capacity)) + throw RangeException(); + + // --- Phase 1: extract sign, convert to unsigned --- + // This avoids UB for T_MIN (e.g. -2^63) where -value overflows signed. + using U = std::make_unsigned_t; + + [[maybe_unused]] bool negative = false; + U uval; + if constexpr (std::is_signed_v) { - tmpVal = value; - value /= base; - *ptr++ = (lowercase ? "fedcba9876543210123456789abcdef" : "FEDCBA9876543210123456789ABCDEF")[15 + (tmpVal - value * base)]; - if (thSep && (base == 10) && (++thCount == 3)) + if (value < 0) { - *ptr++ = thSep; - thCount = 0; + negative = true; + // Two-step cast avoids signed overflow UB for T_MIN: + // cast to unsigned first, then negate in unsigned arithmetic. + uval = static_cast(0) - static_cast(value); } - } while (value); + else + { + uval = static_cast(value); + } + } + else + { + uval = value; + } + + // --- Phase 2: extract digits backwards into result buffer --- + // We write directly into 'result' (backwards), then reverse at the end. + char* ptr = result; - if ('0' == fill) + if (base == 10) + { + // ---- Fast path: base 10, two digits at a time ---- + // This halves the number of expensive divisions. + if (thSep) [[unlikely]] + { + // With thousand separators: track digit count for separator insertion. + int thCount = 0; + + while (uval >= 100) + { + const U q = uval / 100; + const unsigned r = static_cast(uval - q * 100); + uval = q; + + // Lower digit (ones place of the pair) + *ptr++ = Impl::kDigitPairs[r * 2 + 1]; + if (++thCount == 3) + { + *ptr++ = thSep; + thCount = 0; + } + + // Upper digit (tens place of the pair) + *ptr++ = Impl::kDigitPairs[r * 2]; + if (++thCount == 3 && uval != 0) + { + *ptr++ = thSep; + thCount = 0; + } + } + // Remaining 1 or 2 digits + if (uval >= 10) + { + const unsigned r = static_cast(uval); + *ptr++ = Impl::kDigitPairs[r * 2 + 1]; + if (++thCount == 3) + { + *ptr++ = thSep; + thCount = 0; + } + *ptr++ = Impl::kDigitPairs[r * 2]; + } + else + { + *ptr++ = static_cast('0' + uval); + } + } + else + { + // No thousand separators: pure speed path. + while (uval >= 100) + { + const U q = uval / 100; + const unsigned r = static_cast(uval - q * 100); + uval = q; + *ptr++ = Impl::kDigitPairs[r * 2 + 1]; + *ptr++ = Impl::kDigitPairs[r * 2]; + } + if (uval >= 10) + { + const unsigned r = static_cast(uval); + *ptr++ = Impl::kDigitPairs[r * 2 + 1]; + *ptr++ = Impl::kDigitPairs[r * 2]; + } + else + { + *ptr++ = static_cast('0' + uval); + } + } + } + else { - if constexpr (std::is_signed_v) + // ---- Generic path: base 2..9, 11..16 ---- + const char* digits = lowercase ? Impl::kDigitsLower : Impl::kDigitsUpper; + + // For power-of-two bases, use shift+mask instead of division. + if ((base & (base - 1)) == 0) + { + unsigned shift; + switch (base) + { + case 2: shift = 1; break; + case 4: shift = 2; break; + case 8: shift = 3; break; + case 16: shift = 4; break; + default: poco_bugcheck(); shift = 0; break; // unreachable for valid power-of-2 bases + } + const U mask = (static_cast(1) << shift) - 1; + do + { + *ptr++ = digits[uval & mask]; + uval >>= shift; + } while (uval); + } + else [[unlikely]] { - if (tmpVal < 0) --width; + // Non-power-of-two: base 3, 5, 6, 7, 9, 11..15 + do + { + const U q = uval / base; + *ptr++ = digits[uval - q * base]; + uval = q; + } while (uval); } - if (prefix && base == 010) --width; - if (prefix && base == 0x10) width -= 2; - while ((ptr - result) < width) *ptr++ = fill; } - if (prefix && base == 010) *ptr++ = '0'; + // --- Phase 3: zero-fill padding (inserted backwards, gets reversed) --- + if (fill == '0') + { + int w = width; + if (negative) --w; + if (prefix && base == 010) --w; + if (prefix && base == 0x10) w -= 2; + while (static_cast(ptr - result) < w) *ptr++ = '0'; + } + + // --- Phase 4: prefix and sign (appended backwards, reversed later) --- + if (prefix && base == 010) + { + *ptr++ = '0'; + } else if (prefix && base == 0x10) { *ptr++ = 'x'; *ptr++ = '0'; } - if constexpr (std::is_signed_v) - { - if (tmpVal < 0) *ptr++ = '-'; - } + if (negative) *ptr++ = '-'; - if ('0' != fill) + // --- Phase 5: non-zero-fill padding --- + if (fill != '0') { - while ((ptr - result) < width) *ptr++ = fill; + while (static_cast(ptr - result) < width) *ptr++ = fill; } - size = ptr - result; - poco_assert_dbg (size <= ptr.span()); - poco_assert_dbg ((-1 == width) || (size >= size_t(width))); + // --- Phase 6: finalize and reverse --- + size = static_cast(ptr - result); + if (size >= capacity) + throw RangeException(); *ptr-- = '\0'; - char* ptrr = result; - char tmp; - while(ptrr < ptr) + char* lo = result; + while (lo < ptr) { - tmp = *ptr; - *ptr-- = *ptrr; - *ptrr++ = tmp; + char tmp = *ptr; + *ptr-- = *lo; + *lo++ = tmp; } return true; @@ -464,15 +604,14 @@ bool uIntToStr(T value, /// If prefix is true and base is octal or hexadecimal, respective prefix ('0' for octal, /// "0x" for hexadecimal) is prepended. For all other bases, prefix argument is ignored. /// Formatted string has at least [width] total length. - /// - /// This function is deprecated; use intToStr instead. + /// @deprecated Use intToStr instead. { return intToStr(value, base, result, size, prefix, width, fill, thSep, lowercase); } template -bool intToStr (T number, +[[nodiscard]] bool intToStr(T number, unsigned short base, std::string& result, bool prefix = false, @@ -485,7 +624,7 @@ bool intToStr (T number, { char res[POCO_MAX_INT_STRING_LEN] = {0}; std::size_t size = POCO_MAX_INT_STRING_LEN; - bool ret = intToStr(number, base, res, size, prefix, width, fill, thSep, lowercase); + const bool ret = intToStr(number, base, res, size, prefix, width, fill, thSep, lowercase); result.assign(res, size); return ret; } @@ -493,7 +632,7 @@ bool intToStr (T number, template POCO_DEPRECATED("use intToStr instead") -bool uIntToStr (T number, +bool uIntToStr(T number, unsigned short base, std::string& result, bool prefix = false, @@ -503,17 +642,18 @@ bool uIntToStr (T number, bool lowercase = false) /// Converts unsigned integer to string; This is a wrapper function, for details see the /// bool uIntToStr(T, unsigned short, char*, int, int, char, char) implementation. - /// - /// This function is deprecated; use intToStr instead. + /// @deprecated Use intToStr instead. { return intToStr(number, base, result, prefix, width, fill, thSep, lowercase); } // -// Wrappers for double-conversion library (http://code.google.com/p/double-conversion/). -// -// Library is the implementation of the algorithm described in Florian Loitsch's paper: +// Floating-point to/from string conversions. +// Uses std::to_chars/from_chars when available (POCO_HAS_FLOAT_CHARCONV), +// otherwise falls back to the bundled double-conversion library +// (http://code.google.com/p/double-conversion/), which implements the +// algorithm described in Florian Loitsch's paper: // http://florian.loitsch.com/publications/dtoa-pldi2010.pdf // @@ -639,9 +779,4 @@ Foundation_API bool strToDouble(const std::string& str, double& result, } // namespace Poco -#ifdef POCO_COMPILER_MSVC -#pragma warning(pop) -#endif // POCO_COMPILER_MSVC - - #endif // Foundation_NumericString_INCLUDED diff --git a/Foundation/src/NumberFormatter.cpp b/Foundation/src/NumberFormatter.cpp index 7ec7bdd801..eaf28a0d7a 100644 --- a/Foundation/src/NumberFormatter.cpp +++ b/Foundation/src/NumberFormatter.cpp @@ -60,8 +60,8 @@ void NumberFormatter::append(std::string& str, int value) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz); - str.append(result, sz); + if (intToStr(value, 10, result, sz)) + str.append(result, sz); } @@ -69,8 +69,8 @@ void NumberFormatter::append(std::string& str, int value, int width) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz, false, width); - str.append(result, sz); + if (intToStr(value, 10, result, sz, false, width)) + str.append(result, sz); } @@ -78,8 +78,8 @@ void NumberFormatter::append0(std::string& str, int value, int width) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz, false, width, '0'); - str.append(result, sz); + if (intToStr(value, 10, result, sz, false, width, '0')) + str.append(result, sz); } @@ -87,8 +87,8 @@ void NumberFormatter::appendHex(std::string& str, int value, bool lowercase) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(static_cast(value), 0x10, result, sz, false, -1, ' ', 0, lowercase); - str.append(result, sz); + if (intToStr(static_cast(value), 0x10, result, sz, false, -1, ' ', 0, lowercase)) + str.append(result, sz); } @@ -96,8 +96,8 @@ void NumberFormatter::appendHex(std::string& str, int value, int width, bool low { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(static_cast(value), 0x10, result, sz, false, width, '0', 0, lowercase); - str.append(result, sz); + if (intToStr(static_cast(value), 0x10, result, sz, false, width, '0', 0, lowercase)) + str.append(result, sz); } @@ -105,8 +105,8 @@ void NumberFormatter::append(std::string& str, unsigned value) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz); - str.append(result, sz); + if (intToStr(value, 10, result, sz)) + str.append(result, sz); } @@ -114,8 +114,8 @@ void NumberFormatter::append(std::string& str, unsigned value, int width) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz, false, width); - str.append(result, sz); + if (intToStr(value, 10, result, sz, false, width)) + str.append(result, sz); } @@ -123,8 +123,8 @@ void NumberFormatter::append0(std::string& str, unsigned int value, int width) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz, false, width, '0'); - str.append(result, sz); + if (intToStr(value, 10, result, sz, false, width, '0')) + str.append(result, sz); } @@ -132,8 +132,8 @@ void NumberFormatter::appendHex(std::string& str, unsigned value, bool lowercase { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 0x10, result, sz, false, -1, ' ', 0, lowercase); - str.append(result, sz); + if (intToStr(value, 0x10, result, sz, false, -1, ' ', 0, lowercase)) + str.append(result, sz); } @@ -141,8 +141,8 @@ void NumberFormatter::appendHex(std::string& str, unsigned value, int width, boo { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 0x10, result, sz, false, width, '0', 0, lowercase); - str.append(result, sz); + if (intToStr(value, 0x10, result, sz, false, width, '0', 0, lowercase)) + str.append(result, sz); } @@ -150,8 +150,8 @@ void NumberFormatter::append(std::string& str, long value) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz); - str.append(result, sz); + if (intToStr(value, 10, result, sz)) + str.append(result, sz); } @@ -159,8 +159,8 @@ void NumberFormatter::append(std::string& str, long value, int width) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz, false, width); - str.append(result, sz); + if (intToStr(value, 10, result, sz, false, width)) + str.append(result, sz); } @@ -168,8 +168,8 @@ void NumberFormatter::append0(std::string& str, long value, int width) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz, false, width, '0'); - str.append(result, sz); + if (intToStr(value, 10, result, sz, false, width, '0')) + str.append(result, sz); } @@ -177,8 +177,8 @@ void NumberFormatter::appendHex(std::string& str, long value, bool lowercase) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(static_cast(value), 0x10, result, sz, false, -1, ' ', 0, lowercase); - str.append(result, sz); + if (intToStr(static_cast(value), 0x10, result, sz, false, -1, ' ', 0, lowercase)) + str.append(result, sz); } @@ -186,8 +186,8 @@ void NumberFormatter::appendHex(std::string& str, long value, int width, bool lo { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(static_cast(value), 0x10, result, sz, false, width, '0', 0, lowercase); - str.append(result, sz); + if (intToStr(static_cast(value), 0x10, result, sz, false, width, '0', 0, lowercase)) + str.append(result, sz); } @@ -195,8 +195,8 @@ void NumberFormatter::append(std::string& str, unsigned long value) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz); - str.append(result, sz); + if (intToStr(value, 10, result, sz)) + str.append(result, sz); } @@ -204,8 +204,8 @@ void NumberFormatter::append(std::string& str, unsigned long value, int width) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz, false, width, '0'); - str.append(result, sz); + if (intToStr(value, 10, result, sz, false, width)) + str.append(result, sz); } @@ -213,8 +213,8 @@ void NumberFormatter::append0(std::string& str, unsigned long value, int width) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz, false, width, '0'); - str.append(result, sz); + if (intToStr(value, 10, result, sz, false, width, '0')) + str.append(result, sz); } @@ -222,8 +222,8 @@ void NumberFormatter::appendHex(std::string& str, unsigned long value, bool lowe { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 0x10, result, sz, false, -1, ' ', 0, lowercase); - str.append(result, sz); + if (intToStr(value, 0x10, result, sz, false, -1, ' ', 0, lowercase)) + str.append(result, sz); } @@ -231,8 +231,8 @@ void NumberFormatter::appendHex(std::string& str, unsigned long value, int width { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 0x10, result, sz, false, width, '0', 0, lowercase); - str.append(result, sz); + if (intToStr(value, 0x10, result, sz, false, width, '0', 0, lowercase)) + str.append(result, sz); } @@ -244,8 +244,8 @@ void NumberFormatter::append(std::string& str, long long value) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz); - str.append(result, sz); + if (intToStr(value, 10, result, sz)) + str.append(result, sz); } @@ -253,8 +253,8 @@ void NumberFormatter::append(std::string& str, long long value, int width) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz, false, width, '0'); - str.append(result, sz); + if (intToStr(value, 10, result, sz, false, width)) + str.append(result, sz); } @@ -262,8 +262,8 @@ void NumberFormatter::append0(std::string& str, long long value, int width) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz, false, width, '0'); - str.append(result, sz); + if (intToStr(value, 10, result, sz, false, width, '0')) + str.append(result, sz); } @@ -271,8 +271,8 @@ void NumberFormatter::appendHex(std::string& str, long long value, bool lowercas { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(static_cast(value), 0x10, result, sz, false, -1, ' ', 0, lowercase); - str.append(result, sz); + if (intToStr(static_cast(value), 0x10, result, sz, false, -1, ' ', 0, lowercase)) + str.append(result, sz); } @@ -280,8 +280,8 @@ void NumberFormatter::appendHex(std::string& str, long long value, int width, bo { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(static_cast(value), 0x10, result, sz, false, width, '0', 0, lowercase); - str.append(result, sz); + if (intToStr(static_cast(value), 0x10, result, sz, false, width, '0', 0, lowercase)) + str.append(result, sz); } @@ -289,8 +289,8 @@ void NumberFormatter::append(std::string& str, unsigned long long value) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz); - str.append(result, sz); + if (intToStr(value, 10, result, sz)) + str.append(result, sz); } @@ -298,8 +298,8 @@ void NumberFormatter::append(std::string& str, unsigned long long value, int wid { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz, false, width, '0'); - str.append(result, sz); + if (intToStr(value, 10, result, sz, false, width)) + str.append(result, sz); } @@ -307,8 +307,8 @@ void NumberFormatter::append0(std::string& str, unsigned long long value, int wi { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz, false, width, '0'); - str.append(result, sz); + if (intToStr(value, 10, result, sz, false, width, '0')) + str.append(result, sz); } @@ -316,8 +316,8 @@ void NumberFormatter::appendHex(std::string& str, unsigned long long value, bool { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 0x10, result, sz, false, -1, ' ', 0, lowercase); - str.append(result, sz); + if (intToStr(value, 0x10, result, sz, false, -1, ' ', 0, lowercase)) + str.append(result, sz); } @@ -325,8 +325,8 @@ void NumberFormatter::appendHex(std::string& str, unsigned long long value, int { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 0x10, result, sz, false, width, '0', 0, lowercase); - str.append(result, sz); + if (intToStr(value, 0x10, result, sz, false, width, '0', 0, lowercase)) + str.append(result, sz); } @@ -337,8 +337,8 @@ void NumberFormatter::append(std::string& str, Int64 value) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz); - str.append(result, sz); + if (intToStr(value, 10, result, sz)) + str.append(result, sz); } @@ -346,8 +346,8 @@ void NumberFormatter::append(std::string& str, Int64 value, int width) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz, false, width, '0'); - str.append(result, sz); + if (intToStr(value, 10, result, sz, false, width)) + str.append(result, sz); } @@ -355,8 +355,8 @@ void NumberFormatter::append0(std::string& str, Int64 value, int width) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz, false, width, '0'); - str.append(result, sz); + if (intToStr(value, 10, result, sz, false, width, '0')) + str.append(result, sz); } @@ -364,8 +364,8 @@ void NumberFormatter::appendHex(std::string& str, Int64 value, bool lowercase) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(static_cast(value), 0x10, result, sz, false, -1, ' ', 0, lowercase); - str.append(result, sz); + if (intToStr(static_cast(value), 0x10, result, sz, false, -1, ' ', 0, lowercase)) + str.append(result, sz); } @@ -373,8 +373,8 @@ void NumberFormatter::appendHex(std::string& str, Int64 value, int width, bool l { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(static_cast(value), 0x10, result, sz, false, width, '0', 0, lowercase); - str.append(result, sz); + if (intToStr(static_cast(value), 0x10, result, sz, false, width, '0', 0, lowercase)) + str.append(result, sz); } @@ -382,8 +382,8 @@ void NumberFormatter::append(std::string& str, UInt64 value) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz); - str.append(result, sz); + if (intToStr(value, 10, result, sz)) + str.append(result, sz); } @@ -391,8 +391,8 @@ void NumberFormatter::append(std::string& str, UInt64 value, int width) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz, false, width, '0'); - str.append(result, sz); + if (intToStr(value, 10, result, sz, false, width)) + str.append(result, sz); } @@ -400,8 +400,8 @@ void NumberFormatter::append0(std::string& str, UInt64 value, int width) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 10, result, sz, false, width, '0'); - str.append(result, sz); + if (intToStr(value, 10, result, sz, false, width, '0')) + str.append(result, sz); } @@ -409,8 +409,8 @@ void NumberFormatter::appendHex(std::string& str, UInt64 value, bool lowercase) { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 0x10, result, sz, false, -1, ' ', 0, lowercase); - str.append(result, sz); + if (intToStr(value, 0x10, result, sz, false, -1, ' ', 0, lowercase)) + str.append(result, sz); } @@ -418,8 +418,8 @@ void NumberFormatter::appendHex(std::string& str, UInt64 value, int width, bool { char result[NF_MAX_INT_STRING_LEN]; std::size_t sz = NF_MAX_INT_STRING_LEN; - intToStr(value, 0x10, result, sz, false, width, '0', 0, lowercase); - str.append(result, sz); + if (intToStr(value, 0x10, result, sz, false, width, '0', 0, lowercase)) + str.append(result, sz); } diff --git a/Foundation/src/NumberParser.cpp b/Foundation/src/NumberParser.cpp index ab3ec14f84..04935cd56c 100644 --- a/Foundation/src/NumberParser.cpp +++ b/Foundation/src/NumberParser.cpp @@ -50,7 +50,7 @@ int NumberParser::parse(const std::string& s, char thSep) bool NumberParser::tryParse(const std::string& s, int& value, char thSep) { - return strToInt(s.c_str(), value, NUM_BASE_DEC, thSep); + return strToInt(s, value, NUM_BASE_DEC, thSep); } @@ -66,7 +66,7 @@ unsigned NumberParser::parseUnsigned(const std::string& s, char thSep) bool NumberParser::tryParseUnsigned(const std::string& s, unsigned& value, char thSep) { - return strToInt(s.c_str(), value, NUM_BASE_DEC, thSep); + return strToInt(s, value, NUM_BASE_DEC, thSep); } @@ -82,9 +82,11 @@ unsigned NumberParser::parseHex(const std::string& s) bool NumberParser::tryParseHex(const std::string& s, unsigned& value) { - int offset = 0; - if (s.size() > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) offset = 2; - return strToInt(s.c_str() + offset, value, NUM_BASE_HEX); + const char* begin = s.data(); + const char* end = s.data() + s.size(); + while (begin < end && std::isspace(static_cast(*begin))) ++begin; + if (end - begin > 2 && begin[0] == '0' && (begin[1] == 'x' || begin[1] == 'X')) begin += 2; + return strToInt(begin, end, value, NUM_BASE_HEX); } @@ -100,7 +102,7 @@ unsigned NumberParser::parseOct(const std::string& s) bool NumberParser::tryParseOct(const std::string& s, unsigned& value) { - return strToInt(s.c_str(), value, NUM_BASE_OCT); + return strToInt(s, value, NUM_BASE_OCT); } @@ -119,7 +121,7 @@ Int64 NumberParser::parse64(const std::string& s, char thSep) bool NumberParser::tryParse64(const std::string& s, Int64& value, char thSep) { - return strToInt(s.c_str(), value, NUM_BASE_DEC, thSep); + return strToInt(s, value, NUM_BASE_DEC, thSep); } @@ -135,7 +137,7 @@ UInt64 NumberParser::parseUnsigned64(const std::string& s, char thSep) bool NumberParser::tryParseUnsigned64(const std::string& s, UInt64& value, char thSep) { - return strToInt(s.c_str(), value, NUM_BASE_DEC, thSep); + return strToInt(s, value, NUM_BASE_DEC, thSep); } @@ -151,9 +153,11 @@ UInt64 NumberParser::parseHex64(const std::string& s) bool NumberParser::tryParseHex64(const std::string& s, UInt64& value) { - int offset = 0; - if (s.size() > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) offset = 2; - return strToInt(s.c_str() + offset, value, NUM_BASE_HEX); + const char* begin = s.data(); + const char* end = s.data() + s.size(); + while (begin < end && std::isspace(static_cast(*begin))) ++begin; + if (end - begin > 2 && begin[0] == '0' && (begin[1] == 'x' || begin[1] == 'X')) begin += 2; + return strToInt(begin, end, value, NUM_BASE_HEX); } @@ -169,7 +173,7 @@ UInt64 NumberParser::parseOct64(const std::string& s) bool NumberParser::tryParseOct64(const std::string& s, UInt64& value) { - return strToInt(s.c_str(), value, NUM_BASE_OCT); + return strToInt(s, value, NUM_BASE_OCT); } @@ -188,7 +192,7 @@ double NumberParser::parseFloat(const std::string& s, char decSep, char thSep) bool NumberParser::tryParseFloat(const std::string& s, double& value, char decSep, char thSep) { - return strToDouble(s.c_str(), value, decSep, thSep); + return strToDouble(s, value, decSep, thSep); } diff --git a/Foundation/src/NumericString.cpp b/Foundation/src/NumericString.cpp index 8e9da998ec..bbf7d70dde 100644 --- a/Foundation/src/NumericString.cpp +++ b/Foundation/src/NumericString.cpp @@ -15,8 +15,11 @@ #include "Poco/Bugcheck.h" #include "Poco/NumericString.h" +#ifndef POCO_HAS_FLOAT_CHARCONV // +++ double conversion +++ -// don't collide with standalone double_conversion library +// Unity-build: .cc files are #included directly, not compiled separately. +// The namespace rename avoids symbol collisions if the application also +// links a standalone double-conversion library. #define double_conversion poco_double_conversion #define UNIMPLEMENTED poco_bugcheck #include "double-conversion.h" @@ -29,11 +32,12 @@ #include "double-to-string.cc" #include "string-to-double.cc" // --- double conversion --- - poco_static_assert(POCO_MAX_FLT_STRING_LEN == double_conversion::kMaxSignificantDecimalDigits); +#endif // !POCO_HAS_FLOAT_CHARCONV + #include "Poco/String.h" -#include #include +#include namespace { @@ -57,13 +61,13 @@ void pad(std::string& str, std::string::size_type precision, std::string::size_t std::string::size_type frac = str.length() - decSepPos - 1; - std::string::size_type ePos = str.find_first_of("eE"); - std::unique_ptr eStr; + const std::string::size_type ePos = str.find_first_of("eE"); + std::string eStr; if (ePos != std::string::npos) { - eStr.reset(new std::string(str.substr(ePos, std::string::npos))); - frac -= eStr->length(); - str = str.substr(0, str.length() - eStr->length()); + eStr = str.substr(ePos); + frac -= eStr.length(); + str.erase(ePos); } if (frac != precision) @@ -111,7 +115,7 @@ void pad(std::string& str, std::string::size_type precision, std::string::size_t } } - if (eStr.get()) str += *eStr; + if (!eStr.empty()) str += eStr; if (width && (str.length() < width)) str.insert(str.begin(), width - str.length(), prefix); } @@ -122,11 +126,11 @@ void insertThousandSep(std::string& str, char thSep, char decSep = '.') /// Used only internally. { poco_assert (decSep != thSep); - if (str.size() == 0) return; + if (str.empty()) return; std::string::size_type exPos = str.find('e'); if (exPos == std::string::npos) exPos = str.find('E'); - std::string::size_type decPos = str.find(decSep); + const std::string::size_type decPos = str.find(decSep); // there's no rinsert, using forward iterator to go backwards std::string::iterator it = str.end(); if (exPos != std::string::npos) it -= str.size() - exPos; @@ -163,13 +167,167 @@ void insertThousandSep(std::string& str, char thSep, char decSep = '.') namespace Poco { +#ifdef POCO_HAS_FLOAT_CHARCONV + +namespace { + +/// Format a floating-point value using std::to_chars with shortest representation. +/// Chooses fixed or scientific notation based on the value's exponent and the +/// [lowDec, highDec] range, matching double-conversion's ToShortest behavior. +template +void toShortestStr(char* buffer, int bufferSize, T value, int lowDec, int highDec) +{ + // Handle NaN, infinity, and -0 directly -- log10 is undefined for these. + if (!std::isfinite(value) || value == T(0)) + { + auto [ptr, ec] = std::to_chars(buffer, buffer + bufferSize, value); + if (ec != std::errc()) { buffer[0] = '\0'; return; } + *ptr = '\0'; + return; + } + + // Compute the base-10 exponent robustly. std::floor(std::log10(x)) can + // be off by one near exact powers of 10 due to floating-point rounding + // (e.g. log10(1000.0) may return 2.999..., floor gives 2 instead of 3). + // Verify with a power-of-10 multiplication to correct off-by-one. + const T absVal = value < 0 ? -value : value; + int exp10 = static_cast(std::floor(std::log10(absVal))); + // Correct off-by-one: if 10^(exp10+1) <= absVal, we underestimated + T threshold = 1; + for (int i = 0; i < exp10 + 1 && i < 20; ++i) threshold *= 10; + if (threshold <= absVal) ++exp10; + + const bool useFixed = (exp10 >= lowDec && exp10 <= highDec); + + if (useFixed) + { + // Fixed notation with trailing-zero trimming for shortest output + auto [ptr, ec] = std::to_chars(buffer, buffer + bufferSize, value, std::chars_format::fixed); + if (ec != std::errc()) { buffer[0] = '\0'; return; } + *ptr = '\0'; + + char* dot = static_cast(std::memchr(buffer, '.', ptr - buffer)); + if (dot != nullptr) + { + char* last = ptr - 1; + while (last > dot && *last == '0') --last; + *(last == dot ? last : last + 1) = '\0'; + } + } + else + { + auto [ptr, ec] = std::to_chars(buffer, buffer + bufferSize, value); + if (ec != std::errc()) { buffer[0] = '\0'; return; } + *ptr = '\0'; + } +} + +/// Adjust a fixed-notation string to the requested precision by truncating +/// or zero-padding the fractional digits. Uses round-half-up rounding to +/// match double-conversion's behavior. +void adjustPrecision(char* buffer, char* end, int precision) +{ + char* dot = static_cast(std::memchr(buffer, '.', end - buffer)); + if (!dot) + { + if (precision <= 0) return; // no decimal point needed + // Append decimal point and pad with zeros + dot = end; + *end++ = '.'; + for (int i = 0; i < precision; ++i) *end++ = '0'; + *end = '\0'; + return; + } + + // First digit position (skip sign if present) + char* const firstDigit = (buffer[0] == '-') ? buffer + 1 : buffer; + + // Propagate carry backward through digits, skipping '.' and '-'. + // Returns true if carry overflows past the first digit. + auto propagateCarry = [&](char* from) -> bool { + for (char* p = from; p >= firstDigit; --p) + { + if (*p == '.') continue; + if (*p < '9') { ++(*p); return false; } + *p = '0'; + } + return true; // carry past first digit + }; + + // Insert a leading '1' after the sign (if any), shifting digits right. + auto insertLeadingOne = [&](char* upTo) { + std::memmove(firstDigit + 1, firstDigit, upTo - firstDigit); + *firstDigit = '1'; + }; + + if (precision <= 0) + { + // Round to integer: check first fractional digit + if (dot + 1 < end && *(dot + 1) >= '5') + { + if (propagateCarry(dot - 1)) + { + insertLeadingOne(dot); + ++dot; + } + } + *dot = '\0'; + return; + } + + const int frac = static_cast(end - dot - 1); + if (frac <= precision) + { + // Pad with trailing zeros + for (int i = frac; i < precision; ++i) *end++ = '0'; + *end = '\0'; + } + else + { + // Truncate with round-half-up + char* cutPos = dot + 1 + precision; + if (*cutPos >= '5') + { + if (propagateCarry(cutPos - 1)) + { + insertLeadingOne(cutPos); + cutPos[1] = '\0'; + return; + } + } + *cutPos = '\0'; + } +} + +} // namespace + +void floatToStr(char* buffer, int bufferSize, float value, int lowDec, int highDec) +{ + toShortestStr(buffer, bufferSize, value, lowDec, highDec); +} + + +// std::to_chars(fixed, precision) is 15-20% slower than std::to_chars(fixed) +// without precision on both Apple Clang and GCC (benchmarked 2M random values): +// auto [ptr, ec] = std::to_chars(buffer, buffer + bufferSize, value, std::chars_format::fixed, precision); +// We use the faster no-precision overload and manually adjust to the requested +// precision with round-half-up rounding, matching double-conversion's behavior. +void floatToFixedStr(char* buffer, int bufferSize, float value, int precision) +{ + auto [ptr, ec] = std::to_chars(buffer, buffer + bufferSize, value, std::chars_format::fixed); + if (ec != std::errc()) { buffer[0] = '\0'; return; } + *ptr = '\0'; + adjustPrecision(buffer, ptr, precision); +} + +#else // !POCO_HAS_FLOAT_CHARCONV + void floatToStr(char* buffer, int bufferSize, float value, int lowDec, int highDec) { using namespace double_conversion; StringBuilder builder(buffer, bufferSize); - int flags = DoubleToStringConverter::UNIQUE_ZERO | - DoubleToStringConverter::EMIT_POSITIVE_EXPONENT_SIGN; + int flags = DoubleToStringConverter::UNIQUE_ZERO | DoubleToStringConverter::EMIT_POSITIVE_EXPONENT_SIGN; DoubleToStringConverter dc(flags, POCO_FLT_INF, POCO_FLT_NAN, POCO_FLT_EXP, lowDec, highDec, 0, 0); dc.ToShortestSingle(value, &builder); builder.Finalize(); @@ -181,57 +339,91 @@ void floatToFixedStr(char* buffer, int bufferSize, float value, int precision) using namespace double_conversion; StringBuilder builder(buffer, bufferSize); - int flags = DoubleToStringConverter::UNIQUE_ZERO | - DoubleToStringConverter::EMIT_POSITIVE_EXPONENT_SIGN; + int flags = DoubleToStringConverter::UNIQUE_ZERO | DoubleToStringConverter::EMIT_POSITIVE_EXPONENT_SIGN; DoubleToStringConverter dc(flags, POCO_FLT_INF, POCO_FLT_NAN, POCO_FLT_EXP, -std::numeric_limits::digits10, std::numeric_limits::digits10, 0, 0); dc.ToFixed(value, precision, &builder); builder.Finalize(); } +#endif // POCO_HAS_FLOAT_CHARCONV -std::string& floatToStr(std::string& str, float value, int precision, int width, char thSep, char decSep) + +namespace { + +/// Common implementation for float/double-to-string overloads. +/// When precisionApplied is true (fixed-precision path), the char* overload +/// already produced the correct precision via adjustPrecision, so only +/// width-padding is applied. When false (shortest path), pad() handles +/// both precision and width. +template +std::string& floatToStrCommon(std::string& str, T value, int precision, int width, + char thSep, char decSep, bool precisionApplied, CharConvFn charConvFn) { if (!decSep) decSep = '.'; if (precision == 0) value = std::floor(value); char buffer[POCO_MAX_FLT_STRING_LEN]; - floatToStr(buffer, POCO_MAX_FLT_STRING_LEN, value); + charConvFn(buffer, POCO_MAX_FLT_STRING_LEN, value); str = buffer; if (decSep && (decSep != '.') && (str.find('.') != std::string::npos)) replaceInPlace(str, '.', decSep); if (thSep) insertThousandSep(str, thSep, decSep); - if (precision > 0 || width) pad(str, precision, width, ' ', decSep ? decSep : '.'); + + if (precisionApplied) + { + if (width && str.length() < static_cast(width)) + str.insert(str.begin(), width - str.length(), ' '); + } + else + { + if (precision > 0 || width) pad(str, precision, width, ' ', decSep ? decSep : '.'); + } return str; } +} // namespace + + +std::string& floatToStr(std::string& str, float value, int precision, int width, char thSep, char decSep) +{ + return floatToStrCommon(str, value, precision, width, thSep, decSep, false, + [](char* buf, int sz, float v) { floatToStr(buf, sz, v); }); +} + std::string& floatToFixedStr(std::string& str, float value, int precision, int width, char thSep, char decSep) { - if (!decSep) decSep = '.'; - if (precision == 0) value = std::floor(value); + return floatToStrCommon(str, value, precision, width, thSep, decSep, true, + [precision](char* buf, int sz, float v) { floatToFixedStr(buf, sz, v, precision); }); +} - char buffer[POCO_MAX_FLT_STRING_LEN]; - floatToFixedStr(buffer, POCO_MAX_FLT_STRING_LEN, value, precision); - str = buffer; - if (decSep && (decSep != '.') && (str.find('.') != std::string::npos)) - replaceInPlace(str, '.', decSep); +#ifdef POCO_HAS_FLOAT_CHARCONV - if (thSep) insertThousandSep(str, thSep, decSep); - if (precision > 0 || width) pad(str, precision, width, ' ', decSep ? decSep : '.'); - return str; +void doubleToStr(char* buffer, int bufferSize, double value, int lowDec, int highDec) +{ + toShortestStr(buffer, bufferSize, value, lowDec, highDec); +} + + +void doubleToFixedStr(char* buffer, int bufferSize, double value, int precision) +{ + auto [ptr, ec] = std::to_chars(buffer, buffer + bufferSize, value, std::chars_format::fixed); + if (ec != std::errc()) { buffer[0] = '\0'; return; } + *ptr = '\0'; + adjustPrecision(buffer, ptr, precision); } +#else // !POCO_HAS_FLOAT_CHARCONV void doubleToStr(char* buffer, int bufferSize, double value, int lowDec, int highDec) { using namespace double_conversion; StringBuilder builder(buffer, bufferSize); - int flags = DoubleToStringConverter::UNIQUE_ZERO | - DoubleToStringConverter::EMIT_POSITIVE_EXPONENT_SIGN; + int flags = DoubleToStringConverter::UNIQUE_ZERO | DoubleToStringConverter::EMIT_POSITIVE_EXPONENT_SIGN; DoubleToStringConverter dc(flags, POCO_FLT_INF, POCO_FLT_NAN, POCO_FLT_EXP, lowDec, highDec, 0, 0); dc.ToShortest(value, &builder); builder.Finalize(); @@ -243,52 +435,94 @@ void doubleToFixedStr(char* buffer, int bufferSize, double value, int precision) using namespace double_conversion; StringBuilder builder(buffer, bufferSize); - int flags = DoubleToStringConverter::UNIQUE_ZERO | - DoubleToStringConverter::EMIT_POSITIVE_EXPONENT_SIGN; + int flags = DoubleToStringConverter::UNIQUE_ZERO | DoubleToStringConverter::EMIT_POSITIVE_EXPONENT_SIGN; DoubleToStringConverter dc(flags, POCO_FLT_INF, POCO_FLT_NAN, POCO_FLT_EXP, -std::numeric_limits::digits10, std::numeric_limits::digits10, 0, 0); dc.ToFixed(value, precision, &builder); builder.Finalize(); } +#endif // POCO_HAS_FLOAT_CHARCONV + std::string& doubleToStr(std::string& str, double value, int precision, int width, char thSep, char decSep) { - if (!decSep) decSep = '.'; - if (precision == 0) value = std::floor(value); + return floatToStrCommon(str, value, precision, width, thSep, decSep, false, + [](char* buf, int sz, double v) { doubleToStr(buf, sz, v); }); +} - char buffer[POCO_MAX_FLT_STRING_LEN]; - doubleToStr(buffer, POCO_MAX_FLT_STRING_LEN, value); - str = buffer; +std::string& doubleToFixedStr(std::string& str, double value, int precision, int width, char thSep, char decSep) +{ + return floatToStrCommon(str, value, precision, width, thSep, decSep, true, + [precision](char* buf, int sz, double v) { doubleToFixedStr(buf, sz, v, precision); }); +} - if (decSep && (decSep != '.') && (str.find('.') != std::string::npos)) - replaceInPlace(str, '.', decSep); - if (thSep) insertThousandSep(str, thSep, decSep); - if (precision > 0 || width) pad(str, precision, width, ' ', decSep ? decSep : '.'); - return str; -} +#ifdef POCO_HAS_FLOAT_CHARCONV +namespace { -std::string& doubleToFixedStr(std::string& str, double value, int precision, int width, char thSep, char decSep) +/// Helper: skip whitespace and check for inf/nan strings. +/// std::from_chars recognizes "inf"/"infinity"/"nan" on its own, but +/// double-conversion only matches the exact strings passed as parameters. +/// We replicate double-conversion behavior: only the exact inf/nan parameter +/// strings are recognized; everything else that isn't numeric returns NaN. +template +T strToFloatImpl(const char* str, const char* inf, const char* nan) { - if (!decSep) decSep = '.'; - if (precision == 0) value = std::floor(value); + while (std::isspace(static_cast(*str))) ++str; + const char* end = str + std::strlen(str); + while (end > str && std::isspace(static_cast(*(end - 1)))) --end; + + if (str == end) return std::numeric_limits::quiet_NaN(); + + const bool negative = (*str == '-'); + const char* const p = negative ? str + 1 : str; + const std::size_t plen = static_cast(end - p); + + // Check for exact inf/nan match (must consume entire remaining string). + // This must be done before from_chars because from_chars accepts + // "inf", "infinity", and "nan" which may not match the caller's strings. + const std::size_t infLen = std::strlen(inf); + const std::size_t nanLen = std::strlen(nan); + if (plen == infLen && std::strncmp(p, inf, infLen) == 0) + return negative ? -std::numeric_limits::infinity() : std::numeric_limits::infinity(); + if (plen == nanLen && std::strncmp(p, nan, nanLen) == 0) + return std::numeric_limits::quiet_NaN(); + + // Reject strings that start with letters (from_chars would parse "inf"/"nan") + if (plen > 0 && std::isalpha(static_cast(*p))) + return std::numeric_limits::quiet_NaN(); + + T result{}; + const auto [ptr, ec] = std::from_chars(str, end, result); + if (ptr != end) + return std::numeric_limits::quiet_NaN(); + if (ec == std::errc()) + return result; + // errc::result_out_of_range means underflow or overflow. + // from_chars may leave result unmodified on range error (GCC behavior), + // so fall back to strtod which correctly returns 0 or +/-HUGE_VAL. + if (ec == std::errc::result_out_of_range) + return static_cast(std::strtod(str, nullptr)); + return std::numeric_limits::quiet_NaN(); +} - char buffer[POCO_MAX_FLT_STRING_LEN]; - doubleToFixedStr(buffer, POCO_MAX_FLT_STRING_LEN, value, precision); +} // namespace - str = buffer; +float strToFloat(const char* str, const char* inf, const char* nan) +{ + return strToFloatImpl(str, inf, nan); +} - if (decSep && (decSep != '.') && (str.find('.') != std::string::npos)) - replaceInPlace(str, '.', decSep); - if (thSep) insertThousandSep(str, thSep, decSep); - if (precision > 0 || width) pad(str, precision, width, ' ', decSep ? decSep : '.'); - return str; +double strToDouble(const char* str, const char* inf, const char* nan) +{ + return strToFloatImpl(str, inf, nan); } +#else // !POCO_HAS_FLOAT_CHARCONV float strToFloat(const char* str, const char* inf, const char* nan) { @@ -314,36 +548,46 @@ double strToDouble(const char* str, const char* inf, const char* nan) return result; } +#endif // POCO_HAS_FLOAT_CHARCONV -bool strToFloat(const std::string& str, float& result, char decSep, char thSep, const char* inf, const char* nan) + +namespace { + +/// Common implementation for string-to-float/double overloads. +/// Strips whitespace, thousand separators, and the 'f' suffix, replaces +/// the decimal separator, then delegates to the char* parsing overload. +/// The 'f' suffix stripping allows C/C++ float literals ("1.23f") to be +/// parsed. It is also applied for double parsing for backward compatibility. +template +bool strToFloatImpl(const std::string& str, T& result, char decSep, char thSep, + const char* inf, const char* nan, ParseFn parseFn) { - using namespace double_conversion; + if (str.empty()) return false; std::string tmp(str); trimInPlace(tmp); removeInPlace(tmp, thSep); removeInPlace(tmp, 'f'); replaceInPlace(tmp, decSep, '.'); - result = strToFloat(tmp.c_str(), inf, nan); + result = parseFn(tmp.c_str(), inf, nan); return !FPEnvironment::isInfinite(result) && !FPEnvironment::isNaN(result); } +} // namespace -bool strToDouble(const std::string& str, double& result, char decSep, char thSep, const char* inf, const char* nan) + +bool strToFloat(const std::string& str, float& result, char decSep, char thSep, const char* inf, const char* nan) { - if (str.empty()) return false; + return strToFloatImpl(str, result, decSep, thSep, inf, nan, + [](const char* s, const char* i, const char* n) -> float { return strToFloat(s, i, n); }); +} - using namespace double_conversion; - std::string tmp(str); - trimInPlace(tmp); - removeInPlace(tmp, thSep); - replaceInPlace(tmp, decSep, '.'); - removeInPlace(tmp, 'f'); - result = strToDouble(tmp.c_str(), inf, nan); - return !FPEnvironment::isInfinite(result) && - !FPEnvironment::isNaN(result); +bool strToDouble(const std::string& str, double& result, char decSep, char thSep, const char* inf, const char* nan) +{ + return strToFloatImpl(str, result, decSep, thSep, inf, nan, + [](const char* s, const char* i, const char* n) -> double { return strToDouble(s, i, n); }); } diff --git a/Foundation/testsuite/src/NumberParserTest.cpp b/Foundation/testsuite/src/NumberParserTest.cpp index 76ceaead5b..35af8408db 100644 --- a/Foundation/testsuite/src/NumberParserTest.cpp +++ b/Foundation/testsuite/src/NumberParserTest.cpp @@ -14,7 +14,6 @@ #include "Poco/Exception.h" #include "Poco/Types.h" #include "Poco/Format.h" -#include "Poco/NumericString.h" #include "Poco/MemoryStream.h" #include "Poco/Stopwatch.h" #include @@ -232,12 +231,12 @@ void NumberParserTest::testParseError() failmsg("must throw SyntaxException"); } catch (SyntaxException&) { } - try + // Leading whitespace is accepted (stripped before parsing) { - NumberParser::parse(" 123"); - NumberParser::parseBool(""); - failmsg("must throw SyntaxException"); - } catch (SyntaxException&) { } + int ws = 0; + assertTrue(NumberParser::tryParse(" 123", ws)); + assertEqual(123, ws); + } try { diff --git a/Foundation/testsuite/src/StringTest.cpp b/Foundation/testsuite/src/StringTest.cpp index 4bd2f05df9..c4955c7259 100644 --- a/Foundation/testsuite/src/StringTest.cpp +++ b/Foundation/testsuite/src/StringTest.cpp @@ -19,13 +19,19 @@ #include "Poco/String.h" #include "Poco/JSONString.h" #include "Poco/Format.h" -#include "Poco/MemoryStream.h" #include "Poco/Stopwatch.h" +#include "Poco/MemoryStream.h" #include "Poco/FPEnvironment.h" #include "Poco/Exception.h" +#include "Poco/NumberFormatter.h" +#include "Poco/NumberParser.h" #include #include #include +#include +#include +#include +#include #include #include #include @@ -66,12 +72,10 @@ using Poco::decimalSeparator; using Poco::format; using Poco::toJSON; using Poco::CILess; -using Poco::MemoryInputStream; using Poco::Stopwatch; using Poco::RangeException; using Poco::isIntOverflow; using Poco::safeMultiply; -using Poco::isSafeIntCast; using Poco::safeIntCast; using Poco::FPEnvironment; @@ -1038,7 +1042,7 @@ void StringTest::testIntToString() { char pResult[POCO_MAX_INT_STRING_LEN]; std::size_t sz = POCO_MAX_INT_STRING_LEN; - intToStr(0, 10, pResult, sz, false, (int) sz + 1, ' '); + (void) intToStr(0, 10, pResult, sz, false, (int) sz + 1, ' '); fail ("must throw RangeException"); } catch (RangeException&) { } } @@ -1095,9 +1099,10 @@ void StringTest::testNumericStringLimit() catch(Poco::BadCastException&){} signed char sc = 0, st = -1; + char ct = -1; assertTrue(!isIntOverflow(sc)); - assertTrue(safeIntCast(sc, st) == sc); - assertTrue(st == sc); + assertTrue(safeIntCast(sc, ct) == sc); + assertTrue(ct == sc); short ss = SHRT_MAX; assertTrue(isIntOverflow(ss)); @@ -1122,10 +1127,11 @@ void StringTest::testNumericStringLimit() assertTrue(safeIntCast(sc, st) == c); assertTrue(st == sc); - unsigned char uc = 0, ut = -1; + unsigned char uc = 0; + ct = -1; assertTrue(!isIntOverflow(uc)); - assertTrue(safeIntCast(uc, ut) == uc); - assertTrue(ut == uc); + assertTrue(safeIntCast(uc, ct) == uc); + assertTrue(ct == static_cast(uc)); ss = SHRT_MAX; assertTrue(isIntOverflow(ss)); @@ -1252,6 +1258,7 @@ void StringTest::testNumericStringLimit() } + void formatStream(double value, std::string& str) { char buffer[128]; @@ -1364,195 +1371,290 @@ void StringTest::testJSONString() void StringTest::conversionBenchmarks() { - std::cout << std::endl << "===================" << std::endl; + benchmarkIntToStr(); benchmarkFloatToStr(); - std::cout << "===================" << std::endl << std::endl; - std::cout << "===================" << std::endl; - benchmarkStrToFloat(); - std::cout << "===================" << std::endl << std::endl; - std::cout << "===================" << std::endl; benchmarkStrToInt(); - std::cout << "===================" << std::endl << std::endl; + benchmarkStrToFloat(); } void StringTest::benchmarkFloatToStr() { + using Poco::NumberFormatter; + + constexpr int N = 1000000; Poco::Stopwatch sw; - double val = 1.0372157551632929e-112; - std::cout << "The Number: " << std::setprecision(std::numeric_limits::digits10) << val << std::endl; std::string str; - sw.start(); - for (int i = 0; i < 1000000; ++i) formatStream(val, str); - sw.stop(); - std::cout << "formatStream Number: " << str << std::endl; - double timeStream = sw.elapsed() / 1000.0; - - // standard sprintf - str = ""; - sw.restart(); - for (int i = 0; i < 1000000; ++i) formatSprintf(val, str); - sw.stop(); - std::cout << "std::sprintf Number: " << str << std::endl; - double timeSprintf = sw.elapsed() / 1000.0; - - // POCO Way (via double-conversion) - // no padding - sw.restart(); - char buffer[POCO_MAX_FLT_STRING_LEN]; - for (int i = 0; i < 1000000; ++i) doubleToStr(buffer, POCO_MAX_FLT_STRING_LEN, val); - sw.stop(); - std::cout << "doubleToStr(char) Number: " << buffer << std::endl; - double timeDoubleToStrChar = sw.elapsed() / 1000.0; - - // with padding - str = ""; - sw.restart(); - for (int i = 0; i < 1000000; ++i) doubleToStr(str, val); - sw.stop(); - std::cout << "doubleToStr(std::string) Number: " << str << std::endl; - double timeDoubleToStrString = sw.elapsed() / 1000.0; - - int graph; - std::cout << std::endl << "Timing and speedup relative to I/O stream:" << std::endl << std::endl; - std::cout << std::setw(14) << "Stream:\t" << std::setw(10) << std::setfill(' ') << std::setprecision(4) << timeStream << "[ms]" << std::endl; - - std::cout << std::setw(14) << "sprintf:\t" << std::setw(10) << std::setfill(' ') << timeSprintf << "[ms]" << - std::setw(10) << std::setfill(' ') << "Speedup: " << (timeStream / timeSprintf) << '\t' ; - graph = (int) (timeStream / timeSprintf); for (int i = 0; i < graph; ++i) std::cout << '#'; - - std::cout << std::endl << std::setw(14) << "doubleToChar:\t" << std::setw(10) << std::setfill(' ') << timeDoubleToStrChar << "[ms]" << - std::setw(10) << std::setfill(' ') << "Speedup: " << (timeStream / timeDoubleToStrChar) << '\t' ; - graph = (int) (timeStream / timeDoubleToStrChar); for (int i = 0; i < graph; ++i) std::cout << '#'; - - std::cout << std::endl << std::setw(14) << "doubleToString:\t" << std::setw(10) << std::setfill(' ') << timeDoubleToStrString << "[ms]" << - std::setw(10) << std::setfill(' ') << "Speedup: " << (timeStream / timeDoubleToStrString) << '\t' ; - graph = (int) (timeStream / timeDoubleToStrString); for (int i = 0; i < graph; ++i) std::cout << '#'; - - std::cout << std::endl; + + std::mt19937 rng(42); + std::vector dvals(N); + std::vector fvals(N); + for (int i = 0; i < N; ++i) + { + const double d = static_cast(static_cast(rng())) / 1000.0; + dvals[i] = d; + fvals[i] = static_cast(d); + } + + auto bench = [&](const char* label, auto fn) { + for (int i = 0; i < 1000; ++i) fn(i % N); + sw.restart(); + for (int i = 0; i < N; ++i) fn(i); + sw.stop(); + const double ms = sw.elapsed() / 1000.0; + std::cout << std::setw(40) << std::setfill(' ') << label + << std::setw(10) << std::fixed << std::setprecision(2) << ms << " ms" << std::endl; + }; + + // Validate that format() round-trips: format → parse must recover the original value + for (int i = 0; i < N; ++i) + { + const std::string formatted = NumberFormatter::format(dvals[i]); + const double parsed = std::strtod(formatted.c_str(), nullptr); + assertEqualDelta(dvals[i], parsed, std::abs(dvals[i]) * 1e-10); + } + + std::cout << std::endl << "NumberFormatter float/double (" << N << " random values)" << std::endl; + std::cout << std::string(55, '-') << std::endl; + + bench("format(double)", [&](int i){ str = NumberFormatter::format(dvals[i]); }); + bench("format(double, prec=2)", [&](int i){ str = NumberFormatter::format(dvals[i], 2); }); + bench("format(double, prec=6)", [&](int i){ str = NumberFormatter::format(dvals[i], 6); }); + bench("format(double, w=15, prec=4)", [&](int i){ str = NumberFormatter::format(dvals[i], 15, 4); }); + bench("format(float)", [&](int i){ str = NumberFormatter::format(fvals[i]); }); + bench("format(float, prec=2)", [&](int i){ str = NumberFormatter::format(fvals[i], 2); }); + + std::cout << std::endl << " baselines:" << std::endl; + bench("std::to_string(double)", [&](int i){ str = std::to_string(dvals[i]); }); + { + char buf[64]; + bench("snprintf(%g, double)", [&](int i){ std::snprintf(buf, sizeof(buf), "%g", dvals[i]); str = buf; }); + bench("snprintf(%.2f, double)", [&](int i){ std::snprintf(buf, sizeof(buf), "%.2f", dvals[i]); str = buf; }); + bench("snprintf(%.6f, double)", [&](int i){ std::snprintf(buf, sizeof(buf), "%.6f", dvals[i]); str = buf; }); + bench("snprintf(%15.4f, double)", [&](int i){ std::snprintf(buf, sizeof(buf), "%15.4f", dvals[i]); str = buf; }); + bench("snprintf(%g, float)", [&](int i){ std::snprintf(buf, sizeof(buf), "%g", static_cast(fvals[i])); str = buf; }); + } + bench("stream(double)", [&](int i){ formatStream(dvals[i], str); }); } void StringTest::benchmarkStrToInt() { + using Poco::NumberParser; + + constexpr int N = 1000000; Poco::Stopwatch sw; - std::string num = "123456789"; - int res[4] = {}; - sw.start(); - for (int i = 0; i < 1000000; ++i) parseStream(num, res[0]); - sw.stop(); - std::cout << "parseStream Number: " << res[0] << std::endl; - double timeStream = sw.elapsed() / 1000.0; - - char* pC = nullptr; - sw.restart(); - for (int i = 0; i < 1000000; ++i) res[1] = std::strtol(num.c_str(), &pC, 10); - sw.stop(); - std::cout << "std::strtol Number: " << res[1] << std::endl; - double timeStrtol = sw.elapsed() / 1000.0; - - sw.restart(); - for (int i = 0; i < 1000000; ++i) strToInt(num.c_str(), res[2], 10); - sw.stop(); - std::cout << "strToInt Number: " << res[2] << std::endl; - double timeStrToInt = sw.elapsed() / 1000.0; - - sw.restart(); - for (int i = 0; i < 1000000; ++i) std::sscanf(num.c_str(), "%d", &res[3]); - sw.stop(); - std::cout << "sscanf Number: " << res[3] << std::endl; - double timeScanf = sw.elapsed() / 1000.0; - - assertEqual (res[0], res[1]); - assertEqual (res[1], res[2]); - assertEqual (res[2], res[3]); - - int graph; - std::cout << std::endl << "Timing and speedup relative to I/O stream:" << std::endl << std::endl; - std::cout << std::setw(14) << "Stream:\t" << std::setw(10) << std::setfill(' ') << timeStream << "[ms]" << std::endl; - - std::cout << std::setw(14) << "std::strtol:\t" << std::setw(10) << std::setfill(' ') << timeStrtol << "[ms]" << - std::setw(10) << std::setfill(' ') << "Speedup: " << (timeStream / timeStrtol) << '\t' ; - graph = (int) (timeStream / timeStrtol); for (int i = 0; i < graph; ++i) std::cout << '|'; - - std::cout << std::endl << std::setw(14) << "strToInt:\t" << std::setw(10) << std::setfill(' ') << timeStrToInt << "[ms]" << - std::setw(10) << std::setfill(' ') << "Speedup: " << (timeStream / timeStrToInt) << '\t' ; - graph = (int) (timeStream / timeStrToInt); for (int i = 0; i < graph; ++i) std::cout << '|'; - - std::cout << std::endl << std::setw(14) << "std::sscanf:\t" << std::setw(10) << std::setfill(' ') << timeScanf << "[ms]" << - std::setw(10) << std::setfill(' ') << "Speedup: " << (timeStream / timeScanf) << '\t' ; - graph = (int) (timeStream / timeScanf); for (int i = 0; i < graph; ++i) std::cout << '|'; - std::cout << std::endl; + static volatile int gSink = 0; + + std::mt19937 rng(42); + std::vector smallStrs(N), decStrs(N), hexStrs(N), hexPrefStrs(N); + std::vector negStrs(N), bigStrs(N); + for (int i = 0; i < N; ++i) + { + const unsigned v = rng(); + smallStrs[i] = std::to_string(v % 1000); + decStrs[i] = std::to_string(v); + char hbuf[32]; std::snprintf(hbuf, sizeof(hbuf), "%X", v); + hexStrs[i] = hbuf; + hexPrefStrs[i] = std::string("0x") + hbuf; + negStrs[i] = std::to_string(static_cast(-(static_cast(v) % INT_MAX))); + const auto big = (static_cast(rng()) << 32) | rng(); + bigStrs[i] = std::to_string(big); + } + + auto bench = [&](const char* label, auto fn) { + for (int i = 0; i < 1000; ++i) fn(i % N); + sw.restart(); + for (int i = 0; i < N; ++i) fn(i); + sw.stop(); + const double ms = sw.elapsed() / 1000.0; + std::cout << std::setw(40) << std::setfill(' ') << label + << std::setw(10) << std::fixed << std::setprecision(2) << ms << " ms" << std::endl; + }; + + // Validate that parse() agrees with strtoul/strtol for all test data + for (int i = 0; i < N; ++i) + { + assertEqual(static_cast(std::strtoul(decStrs[i].c_str(), nullptr, 10)), + NumberParser::parseUnsigned(decStrs[i])); + assertEqual(static_cast(std::strtol(negStrs[i].c_str(), nullptr, 10)), + NumberParser::parse(negStrs[i])); + } + + std::cout << std::endl << "NumberParser integer (" << N << " random values)" << std::endl; + std::cout << std::string(55, '-') << std::endl; + + { + int r; unsigned ur; Poco::UInt64 ur64; + + bench("parse(small dec)", [&](int i){ r = NumberParser::parse(smallStrs[i % N]); gSink += r; }); + bench("parse(uint dec)", [&](int i){ ur = NumberParser::parseUnsigned(decStrs[i % N]); gSink += ur; }); + bench("parse(int neg)", [&](int i){ r = NumberParser::parse(negStrs[i % N]); gSink += r; }); + bench("parseHex(uint)", [&](int i){ ur = NumberParser::parseHex(hexStrs[i % N]); gSink += ur; }); + bench("parseHex(0x-prefixed)", [&](int i){ ur = NumberParser::parseHex(hexPrefStrs[i % N]); gSink += ur; }); + bench("parseUnsigned64(uint64)", [&](int i){ ur64 = NumberParser::parseUnsigned64(bigStrs[i % N]); gSink += static_cast(ur64); }); + + std::cout << std::endl << " baselines:" << std::endl; + bench("std::strtol(small dec)", [&](int i){ gSink += static_cast(std::strtol(smallStrs[i % N].c_str(), nullptr, 10)); }); + bench("std::strtol(dec)", [&](int i){ gSink += static_cast(std::strtol(decStrs[i % N].c_str(), nullptr, 10)); }); + bench("std::strtol(neg)", [&](int i){ gSink += static_cast(std::strtol(negStrs[i % N].c_str(), nullptr, 10)); }); + bench("std::strtoul(hex)", [&](int i){ gSink += static_cast(std::strtoul(hexStrs[i % N].c_str(), nullptr, 16)); }); + bench("std::strtoull(uint64)", [&](int i){ gSink += static_cast(std::strtoull(bigStrs[i % N].c_str(), nullptr, 10)); }); + { + int v; + bench("sscanf(%d)", [&](int i){ std::sscanf(decStrs[i % N].c_str(), "%d", &v); gSink += v; }); + bench("sscanf(%x)", [&](int i){ std::sscanf(hexStrs[i % N].c_str(), "%x", reinterpret_cast(&v)); gSink += v; }); + } + } +} + + +void StringTest::benchmarkIntToStr() +{ + using Poco::NumberFormatter; + + constexpr int N = 1000000; + Poco::Stopwatch sw; + std::string str; + + std::mt19937 rng(42); + std::vector ivals(N); + std::vector uvals(N); + std::vector i64vals(N); + std::vector u64vals(N); + std::vector ismall(N); + std::vector usmall(N); + for (int i = 0; i < N; ++i) + { + ivals[i] = static_cast(rng()); + uvals[i] = static_cast(rng()); + i64vals[i] = static_cast((static_cast(rng()) << 32) | rng()); + u64vals[i] = (static_cast(rng()) << 32) | rng(); + ismall[i] = static_cast(rng() % 1000); + usmall[i] = static_cast(rng() % 1000); + } + + auto bench = [&](const char* label, auto fn) { + for (int i = 0; i < 1000; ++i) fn(i % N); + sw.restart(); + for (int i = 0; i < N; ++i) fn(i); + sw.stop(); + const double ms = sw.elapsed() / 1000.0; + std::cout << std::setw(40) << std::setfill(' ') << label + << std::setw(10) << std::fixed << std::setprecision(2) << ms << " ms" << std::endl; + }; + + // Validate format() round-trip: format → strtol must recover the original value + for (int i = 0; i < N; ++i) + { + const std::string formatted = NumberFormatter::format(ivals[i]); + const long parsed = std::strtol(formatted.c_str(), nullptr, 10); + assertEqual(static_cast(ivals[i]), parsed); + } + + std::cout << std::endl << "NumberFormatter integer (" << N << " random values)" << std::endl; + std::cout << std::string(55, '-') << std::endl; + + std::cout << std::endl << " small int (0-999):" << std::endl; + bench("format(small)", [&](int i){ str = NumberFormatter::format(ismall[i]); }); + bench("formatHex(small)", [&](int i){ str = NumberFormatter::formatHex(static_cast(usmall[i])); }); + + std::cout << std::endl << " int:" << std::endl; + bench("format(int)", [&](int i){ str = NumberFormatter::format(ivals[i]); }); + bench("format(int, width=15)", [&](int i){ str = NumberFormatter::format(ivals[i], 15); }); + bench("format0(int, width=15)", [&](int i){ str = NumberFormatter::format0(ivals[i], 15); }); + bench("format(-int)", [&](int i){ str = NumberFormatter::format(ivals[i] > 0 ? -ivals[i] : ivals[i]); }); + bench("formatHex(int)", [&](int i){ str = NumberFormatter::formatHex(ivals[i]); }); + bench("formatHex(int, width=10)", [&](int i){ str = NumberFormatter::formatHex(ivals[i], 10); }); + bench("formatHex(int, prefix)", [&](int i){ str = NumberFormatter::formatHex(ivals[i], true); }); + + std::cout << std::endl << " unsigned:" << std::endl; + bench("format(unsigned)", [&](int i){ str = NumberFormatter::format(uvals[i]); }); + bench("formatHex(unsigned)", [&](int i){ str = NumberFormatter::formatHex(uvals[i]); }); + +#ifdef POCO_HAVE_INT64 + std::cout << std::endl << " Int64 / UInt64:" << std::endl; + bench("format(Int64)", [&](int i){ str = NumberFormatter::format(i64vals[i]); }); + bench("format(UInt64)", [&](int i){ str = NumberFormatter::format(u64vals[i]); }); + bench("formatHex(Int64)", [&](int i){ str = NumberFormatter::formatHex(i64vals[i]); }); + bench("formatHex(UInt64, w=20)", [&](int i){ str = NumberFormatter::formatHex(u64vals[i], 20); }); +#endif + + std::cout << std::endl << " baselines:" << std::endl; + bench("std::to_string(int)", [&](int i){ str = std::to_string(ivals[i]); }); + bench("std::to_string(unsigned)", [&](int i){ str = std::to_string(uvals[i]); }); +#ifdef POCO_HAVE_INT64 + bench("std::to_string(Int64)", [&](int i){ str = std::to_string(i64vals[i]); }); +#endif + { + char buf[32]; + bench("snprintf(%d, int)", [&](int i){ std::snprintf(buf, sizeof(buf), "%d", ivals[i]); str = buf; }); + bench("snprintf(%15d, int)", [&](int i){ std::snprintf(buf, sizeof(buf), "%15d", ivals[i]); str = buf; }); + bench("snprintf(%015d, int)", [&](int i){ std::snprintf(buf, sizeof(buf), "%015d", ivals[i]); str = buf; }); + bench("snprintf(%X, int)", [&](int i){ std::snprintf(buf, sizeof(buf), "%X", uvals[i]); str = buf; }); + } } void StringTest::benchmarkStrToFloat() { - double res[5] = {}; + using Poco::NumberParser; + using Poco::NumberFormatter; + + constexpr int N = 1000000; Poco::Stopwatch sw; - std::string num = "1.0372157551632929e-112"; - std::cout << "The Number: " << num << std::endl; - sw.start(); - for (int i = 0; i < 1000000; ++i) parseStream(num, res[0]); - sw.stop(); - std::cout << "parseStream Number: " << std::setprecision(std::numeric_limits::digits10) << res[0] << std::endl; - double timeStream = sw.elapsed() / 1000.0; - - // standard strtod - char* pC = nullptr; - sw.restart(); - for (int i = 0; i < 1000000; ++i) res[1] = std::strtod(num.c_str(), &pC); - sw.stop(); - std::cout << "std::strtod Number: " << res[1] << std::endl; - double timeStdStrtod = sw.elapsed() / 1000.0; - - // POCO Way - sw.restart(); - char ou = 0; - for (int i = 0; i < 1000000; ++i) strToDouble(num, res[2], ou); - sw.stop(); - std::cout << "Poco::strToDouble(const string&, double&) Number: " << res[2] << std::endl; - double timeStrToDouble = sw.elapsed() / 1000.0; - - sw.restart(); - for (int i = 0; i < 1000000; ++i) res[3] = strToDouble(num.c_str()); - sw.stop(); - std::cout << "Poco::strToDouble(const char*) Number: " << res[3] << std::endl; - double timeStrtoD = sw.elapsed() / 1000.0; - - // standard sscanf - sw.restart(); - for (int i = 0; i < 1000000; ++i) std::sscanf(num.c_str(), "%lf", &res[4]); - sw.stop(); - std::cout << "sscanf Number: " << res[4] << std::endl; - double timeScanf = sw.elapsed() / 1000.0; - - assertEqual (res[0], res[1]); - assertEqual (res[1], res[2]); - assertEqual (res[2], res[3]); - assertEqual (res[3], res[4]); - - int graph; - std::cout << std::endl << "Timing and speedup relative to I/O stream:" << std::endl << std::endl; - std::cout << std::setw(14) << "Stream:\t" << std::setw(10) << std::setfill(' ') << std::setprecision(4) << timeStream << "[ms]" << std::endl; - - std::cout << std::setw(14) << "std::strtod:\t" << std::setw(10) << std::setfill(' ') << timeStdStrtod << "[ms]" << - std::setw(10) << std::setfill(' ') << "Speedup: " << (timeStream / timeStdStrtod) << '\t' ; - graph = (int) (timeStream / timeStdStrtod); for (int i = 0; i < graph; ++i) std::cout << '#'; - - std::cout << std::endl << std::setw(14) << "strToDouble:\t" << std::setw(10) << std::setfill(' ') << timeStrToDouble << "[ms]" << - std::setw(10) << std::setfill(' ') << "Speedup: " << (timeStream / timeStrToDouble) << '\t' ; - graph = (int) (timeStream / timeStrToDouble); for (int i = 0; i < graph; ++i) std::cout << '#'; - - std::cout << std::endl << std::setw(14) << "strToDouble:\t" << std::setw(10) << std::setfill(' ') << timeScanf << "[ms]" << - std::setw(10) << std::setfill(' ') << "Speedup: " << (timeStream / timeStrtoD) << '\t' ; - graph = (int) (timeStream / timeStrtoD); for (int i = 0; i < graph; ++i) std::cout << '#'; - - std::cout << std::endl << std::setw(14) << "std::sscanf:\t" << std::setw(10) << std::setfill(' ') << timeScanf << "[ms]" << - std::setw(10) << std::setfill(' ') << "Speedup: " << (timeStream / timeScanf) << '\t' ; - graph = (int) (timeStream / timeScanf); for (int i = 0; i < graph; ++i) std::cout << '#'; - - std::cout << std::endl; + static volatile int gSink = 0; + + std::mt19937 rng(42); + std::vector dvals(N); + std::vector fvals(N); + std::vector dblStrs(N), fltStrs(N), dblFixedStrs(N); + for (int i = 0; i < N; ++i) + { + const double d = static_cast(static_cast(rng())) / 1000.0; + dvals[i] = d; + fvals[i] = static_cast(d); + dblStrs[i] = NumberFormatter::format(dvals[i]); + fltStrs[i] = NumberFormatter::format(fvals[i]); + dblFixedStrs[i] = NumberFormatter::format(dvals[i], 4); + } + + auto bench = [&](const char* label, auto fn) { + for (int i = 0; i < 1000; ++i) fn(i % N); + sw.restart(); + for (int i = 0; i < N; ++i) fn(i); + sw.stop(); + const double ms = sw.elapsed() / 1000.0; + std::cout << std::setw(40) << std::setfill(' ') << label + << std::setw(10) << std::fixed << std::setprecision(2) << ms << " ms" << std::endl; + }; + + // Validate that POCO parsing agrees with strtod/strtof for all test data + for (int i = 0; i < N; ++i) + { + double pocoD; + const double strtodD = std::strtod(dblStrs[i].c_str(), nullptr); + assertTrue(NumberParser::tryParseFloat(dblStrs[i], pocoD)); + assertEqualDelta(strtodD, pocoD, std::abs(strtodD) * 1e-10); + } + + std::cout << std::endl << "NumberParser float/double (" << N << " random values)" << std::endl; + std::cout << std::string(55, '-') << std::endl; + + { + double d; + bench("tryParseFloat(float str)", [&](int i){ NumberParser::tryParseFloat(fltStrs[i % N], d); gSink += static_cast(d); }); + bench("tryParseFloat(double str)", [&](int i){ NumberParser::tryParseFloat(dblStrs[i % N], d); gSink += static_cast(d); }); + bench("tryParseFloat(fixed str)", [&](int i){ NumberParser::tryParseFloat(dblFixedStrs[i % N], d); gSink += static_cast(d); }); + + std::cout << std::endl << " baselines:" << std::endl; + bench("std::strtod", [&](int i){ gSink += static_cast(std::strtod(dblStrs[i % N].c_str(), nullptr)); }); + bench("std::strtof", [&](int i){ gSink += static_cast(std::strtof(fltStrs[i % N].c_str(), nullptr)); }); + { + double v; + bench("sscanf(%lf)", [&](int i){ std::sscanf(dblStrs[i % N].c_str(), "%lf", &v); gSink += static_cast(v); }); + } + } } diff --git a/Foundation/testsuite/src/StringTest.h b/Foundation/testsuite/src/StringTest.h index 9f4f264aad..40313a4fcc 100644 --- a/Foundation/testsuite/src/StringTest.h +++ b/Foundation/testsuite/src/StringTest.h @@ -16,8 +16,6 @@ #include "Poco/Foundation.h" #include "CppUnit/TestCase.h" -#include "Poco/NumericString.h" -#include "Poco/MemoryStream.h" #include "Poco/NumberFormatter.h" #include @@ -62,6 +60,7 @@ class StringTest: public CppUnit::TestCase void benchmarkFloatToStr(); void benchmarkStrToFloat(); void benchmarkStrToInt(); + void benchmarkIntToStr(); void testJSONString(); @@ -165,16 +164,6 @@ class StringTest: public CppUnit::TestCase assertFalse (Poco::safeMultiply(t, f, m)); } - template - bool parseStream(const std::string& s, T& value) - { - Poco::MemoryInputStream istr(s.data(), s.size()); -#if !defined(POCO_NO_LOCALE) - istr.imbue(std::locale::classic()); -#endif - istr >> value; - return istr.eof() && !istr.fail(); - } };