diff --git a/src/iceberg/util/string_util.h b/src/iceberg/util/string_util.h index 3528204ee..fc202f0e6 100644 --- a/src/iceberg/util/string_util.h +++ b/src/iceberg/util/string_util.h @@ -20,10 +20,12 @@ #pragma once #include +#include #include #include #include #include +#include #include #include @@ -32,6 +34,9 @@ namespace iceberg { +template +concept FromChars = requires(const char* p, T& v) { std::from_chars(p, p, v); }; + class ICEBERG_EXPORT StringUtils { public: static std::string ToLower(std::string_view str) { @@ -68,7 +73,7 @@ class ICEBERG_EXPORT StringUtils { } template - requires std::is_arithmetic_v && (!std::same_as) + requires std::is_arithmetic_v && FromChars && (!std::same_as) static Result ParseNumber(std::string_view str) { T value = 0; auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), value); @@ -85,6 +90,35 @@ class ICEBERG_EXPORT StringUtils { } std::unreachable(); } + + template + requires std::is_floating_point_v && (!FromChars) + static Result ParseNumber(std::string_view str) { + T value{}; + // strto* require null-terminated input; string_view does not guarantee it. + std::string owned(str); + const char* start = owned.c_str(); + char* end = nullptr; + errno = 0; + + if constexpr (std::same_as) { + value = std::strtof(start, &end); + } else if constexpr (std::same_as) { + value = std::strtod(start, &end); + } else { + value = std::strtold(start, &end); + } + + if (end == start || end != start + static_cast(owned.size())) { + return InvalidArgument("Failed to parse {} from string '{}': invalid argument", + typeid(T).name(), str); + } + if (errno == ERANGE) { + return InvalidArgument("Failed to parse {} from string '{}': value out of range", + typeid(T).name(), str); + } + return value; + } }; /// \brief Transparent hash function that supports std::string_view as lookup key