diff options
-rw-r--r-- | src/common/Utilities/StringConvert.h | 86 | ||||
-rw-r--r-- | src/server/game/Chat/ChatCommands/ChatCommandArgs.h | 30 | ||||
-rw-r--r-- | tests/common/StringConvert.cpp | 25 | ||||
-rw-r--r-- | tests/game/ChatCommand.cpp | 27 |
4 files changed, 136 insertions, 32 deletions
diff --git a/src/common/Utilities/StringConvert.h b/src/common/Utilities/StringConvert.h index f264c0222fa..4ec3324fb33 100644 --- a/src/common/Utilities/StringConvert.h +++ b/src/common/Utilities/StringConvert.h @@ -39,7 +39,6 @@ namespace Trinity::Impl::StringConvertImpl */ }; - // @todo relax this once proper std::from_chars support exists template <typename T> struct For<T, std::enable_if_t<std::is_integral_v<T> && !std::is_same_v<T, bool>>> { @@ -171,6 +170,91 @@ namespace Trinity::Impl::StringConvertImpl return (val ? "1" : "0"); } }; + +#if TRINITY_COMPILER == TRINITY_COMPILER_MICROSOFT + template <typename T> + struct For<T, std::enable_if_t<std::is_floating_point_v<T>>> + { + static Optional<T> FromString(std::string_view str, std::chars_format fmt = std::chars_format()) + { + if (str.empty()) + return std::nullopt; + + if (fmt == std::chars_format()) + { + if (StringEqualI(str.substr(0, 2), "0x")) + { + fmt = std::chars_format::hex; + str.remove_prefix(2); + } + else + fmt = std::chars_format::general; + + if (str.empty()) + return std::nullopt; + } + + char const* const start = str.data(); + char const* const end = (start + str.length()); + + T val; + std::from_chars_result const res = std::from_chars(start, end, val, fmt); + if ((res.ptr == end) && (res.ec == std::errc())) + return val; + else + return std::nullopt; + } + + // this allows generic converters for all numeric types (easier templating!) + static Optional<T> FromString(std::string_view str, int base) + { + if (base == 16) + return FromString(str, std::chars_format::hex); + else if (base == 10) + return FromString(str, std::chars_format::general); + else + return FromString(str, std::chars_format()); + } + + static std::string ToString(T val) + { + return std::to_string(val); + } + }; +#else + // @todo replace this once libc++ supports double args to from_chars + template <typename T> + struct For<T, std::enable_if_t<std::is_floating_point_v<T>>> + { + static Optional<T> FromString(std::string_view str, int base = 0) + { + try { + if (str.empty()) + return std::nullopt; + + if ((base == 10) && StringEqualI(str.substr(0, 2), "0x")) + return std::nullopt; + + std::string tmp; + if (base == 16) + tmp.append("0x"); + tmp.append(str); + + size_t n; + T val = static_cast<T>(std::stold(tmp, &n)); + if (n != tmp.length()) + return std::nullopt; + return val; + } + catch (...) { return std::nullopt; } + } + + static std::string ToString(T val) + { + return std::to_string(val); + } + }; +#endif } namespace Trinity diff --git a/src/server/game/Chat/ChatCommands/ChatCommandArgs.h b/src/server/game/Chat/ChatCommands/ChatCommandArgs.h index 583edcd40e2..9f10013989c 100644 --- a/src/server/game/Chat/ChatCommands/ChatCommandArgs.h +++ b/src/server/game/Chat/ChatCommands/ChatCommandArgs.h @@ -45,9 +45,9 @@ namespace Trinity::Impl::ChatCommands template <typename T, typename = void> struct ArgInfo { static_assert(!std::is_same_v<T,T>, "Invalid command parameter type - see ChatCommandArgs.h for possible types"); }; -// catch-all for integral types +// catch-all for number types template <typename T> -struct ArgInfo<T, std::enable_if_t<std::is_integral_v<T>>> +struct ArgInfo<T, std::enable_if_t<std::is_integral_v<T> || std::is_floating_point_v<T>>> { static Optional<std::string_view> TryConsume(T& val, std::string_view args) { @@ -59,34 +59,14 @@ struct ArgInfo<T, std::enable_if_t<std::is_integral_v<T>>> val = *v; else return std::nullopt; - return tail; - } -}; - -// catch-all for floating point types -template <typename T> -struct ArgInfo<T, std::enable_if_t<std::is_floating_point_v<T>>> -{ - static Optional<std::string_view> TryConsume(T& val, std::string_view args) - { - auto [token, tail] = tokenize(args); - if (token.empty()) - return std::nullopt; - try + if constexpr (std::is_floating_point_v<T>) { - // @todo replace this once libc++ supports double args to from_chars for required minimum - size_t processedChars = 0; - val = std::stold(std::string(token), &processedChars); - if (processedChars != token.length()) + if (!std::isfinite(val)) return std::nullopt; } - catch (...) { return std::nullopt; } - if (std::isfinite(val)) - return tail; - else - return std::nullopt; + return tail; } }; diff --git a/tests/common/StringConvert.cpp b/tests/common/StringConvert.cpp index d25e28f3d19..c1a6f5da7f8 100644 --- a/tests/common/StringConvert.cpp +++ b/tests/common/StringConvert.cpp @@ -119,7 +119,7 @@ TEST_CASE("String to smaller integer types", "[StringConvert]") REQUIRE(Trinity::StringTo<int16>("-1", 0) == -1); } -TEST_CASE("Test.String to boolean", "[StringConvert]") +TEST_CASE("String to boolean", "[StringConvert]") { REQUIRE(Trinity::StringTo<bool>("true") == true); REQUIRE(Trinity::StringTo<bool>("false") == false); @@ -129,3 +129,26 @@ TEST_CASE("Test.String to boolean", "[StringConvert]") REQUIRE(Trinity::StringTo<bool>("1", 10) == true); REQUIRE(Trinity::StringTo<bool>("0", 10) == false); } + +TEST_CASE("String to double", "[StringConvert]") +{ + using namespace Catch::literals; + REQUIRE(Trinity::StringTo<double>("0.5") == 0.5); + REQUIRE(Trinity::StringTo<double>("0.1") == 0.1_a); + REQUIRE(Trinity::StringTo<double>("1.2.3") == std::nullopt); + REQUIRE(Trinity::StringTo<double>("1e+5") == 100000.0); + REQUIRE(Trinity::StringTo<double>("1e+3+5") == std::nullopt); + REQUIRE(Trinity::StringTo<double>("a1.5") == std::nullopt); + REQUIRE(Trinity::StringTo<double>("1.5tail") == std::nullopt); + REQUIRE(Trinity::StringTo<double>("0x0") == 0.0); + REQUIRE(Trinity::StringTo<double>("0x0", 16) == std::nullopt); + REQUIRE(Trinity::StringTo<double>("0", 16) == 0.0); + REQUIRE(Trinity::StringTo<double>("0x1.BC70A3D70A3D7p+6") == 0x1.BC70A3D70A3D7p+6); + REQUIRE(Trinity::StringTo<double>("0x1.BC70A3D70A3D7p+6", 10) == std::nullopt); + REQUIRE(Trinity::StringTo<double>("0x1.BC70A3D70A3D7p+6", 16) == std::nullopt); + REQUIRE(Trinity::StringTo<double>("1.BC70A3D70A3D7p+6", 16) == 0x1.BC70A3D70A3D7p+6); + REQUIRE(Trinity::StringTo<double>("0x1.2.3") == std::nullopt); + REQUIRE(Trinity::StringTo<double>("0x1.AAAp+1-3") == std::nullopt); + REQUIRE(Trinity::StringTo<double>("1.2.3", 16) == std::nullopt); + REQUIRE(Trinity::StringTo<double>("1.AAAp+1-3", 16) == std::nullopt); +} diff --git a/tests/game/ChatCommand.cpp b/tests/game/ChatCommand.cpp index f484b5806c5..2f73b579f84 100644 --- a/tests/game/ChatCommand.cpp +++ b/tests/game/ChatCommand.cpp @@ -48,13 +48,30 @@ TEST_CASE("Command argument parsing", "[ChatCommand]") TestChatCommand("true", [](ChatHandler*, uint32) { return true; }, false); } - SECTION("std::vector<uint8>") + SECTION("Floating point argument") { - TestChatCommand("1 2 3 4 5 6 7 8 9 10", [](ChatHandler*, std::vector<uint8> v) + TestChatCommand("0.5", [](ChatHandler*, float f) { - REQUIRE(v.size() == 10); - for (size_t i = 0; i < 10; ++i) - REQUIRE(v[i] == (i + 1)); + REQUIRE(f == 0.5); + return true; + }); + TestChatCommand("true", [](ChatHandler*, float) { return true; }, false); + } + + SECTION("std::vector<uint16>") + { + TestChatCommand("1 2 3 4 5 6 7 8 9 10", [](ChatHandler*, std::vector<uint16> v) + { + REQUIRE(v == std::vector<uint16>{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + return true; + }); + } + + SECTION("std::array<uint16>") + { + TestChatCommand("1 2 3 4 5 6 7 8 9 10", [](ChatHandler*, std::array<uint16, 10> v) + { + REQUIRE(v == std::array<uint16, 10>{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); return true; }); } |