diff --git a/src/openrct2/core/String.cpp b/src/openrct2/core/String.cpp index b77aa005db..8cc4e44548 100644 --- a/src/openrct2/core/String.cpp +++ b/src/openrct2/core/String.cpp @@ -154,6 +154,32 @@ namespace String } } + bool Equals(const std::string_view& a, const std::string_view& b, bool ignoreCase) + { + if (ignoreCase) + { + if (a.size() == b.size()) + { + for (size_t i = 0; i < a.size(); i++) + { + if (tolower(a[i]) != tolower(b[i])) + { + return false; + } + } + return true; + } + else + { + return false; + } + } + else + { + return a == b; + } + } + bool Equals(const std::string& a, const std::string& b, bool ignoreCase) { return Equals(a.c_str(), b.c_str(), ignoreCase); diff --git a/src/openrct2/core/String.hpp b/src/openrct2/core/String.hpp index af6e612d31..b1c6a9f4dd 100644 --- a/src/openrct2/core/String.hpp +++ b/src/openrct2/core/String.hpp @@ -42,6 +42,7 @@ namespace String bool IsNullOrEmpty(const utf8* str); int32_t Compare(const std::string& a, const std::string& b, bool ignoreCase = false); int32_t Compare(const utf8* a, const utf8* b, bool ignoreCase = false); + bool Equals(const std::string_view& a, const std::string_view& b, bool ignoreCase); bool Equals(const std::string& a, const std::string& b, bool ignoreCase = false); bool Equals(const utf8* a, const utf8* b, bool ignoreCase = false); bool StartsWith(const utf8* str, const utf8* match, bool ignoreCase = false); diff --git a/src/openrct2/libopenrct2.vcxproj b/src/openrct2/libopenrct2.vcxproj index 2dd9b0e4ea..4f655f918d 100644 --- a/src/openrct2/libopenrct2.vcxproj +++ b/src/openrct2/libopenrct2.vcxproj @@ -220,6 +220,7 @@ + @@ -554,6 +555,7 @@ + diff --git a/src/openrct2/localisation/FormatCodes.cpp b/src/openrct2/localisation/FormatCodes.cpp index 150fa21271..4fba694267 100644 --- a/src/openrct2/localisation/FormatCodes.cpp +++ b/src/openrct2/localisation/FormatCodes.cpp @@ -10,6 +10,7 @@ #include "FormatCodes.h" #include "../common.h" +#include "../core/String.hpp" #include "Localisation.h" #include @@ -76,14 +77,12 @@ static constexpr const format_code_token format_code_tokens[] = { }; // clang-format on -uint32_t format_get_code(const char* token) +uint32_t format_get_code(std::string_view token) { - for (uint32_t i = 0; i < std::size(format_code_tokens); i++) - { - if (_strcmpi(token, format_code_tokens[i].token) == 0) - return format_code_tokens[i].code; - } - return 0; + auto result = std::find_if(std::begin(format_code_tokens), std::end(format_code_tokens), [token](auto& fct) { + return String::Equals(token, fct.token, true); + }); + return result != std::end(format_code_tokens) ? result->code : 0; } const char* format_get_token(uint32_t code) diff --git a/src/openrct2/localisation/FormatCodes.h b/src/openrct2/localisation/FormatCodes.h index 96335d60b3..7e2ed2f43e 100644 --- a/src/openrct2/localisation/FormatCodes.h +++ b/src/openrct2/localisation/FormatCodes.h @@ -11,7 +11,9 @@ #include "../common.h" -uint32_t format_get_code(const char* token); +#include + +uint32_t format_get_code(std::string_view token); const char* format_get_token(uint32_t code); enum diff --git a/src/openrct2/localisation/Formatting.cpp b/src/openrct2/localisation/Formatting.cpp new file mode 100644 index 0000000000..ec18d92577 --- /dev/null +++ b/src/openrct2/localisation/Formatting.cpp @@ -0,0 +1,207 @@ +/***************************************************************************** + * Copyright (c) 2014-2020 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#include "Formatting.h" + +#include "../config/Config.h" +#include "../util/Util.h" +#include "FormatCodes.h" +#include "Language.h" +#include "StringIds.h" + +#include + +namespace OpenRCT2 +{ + char GetDigitSeperator() + { + return ','; + } + + char GetDecimalSeperator() + { + return '.'; + } + + template void FormatNumber(std::stringstream& ss, T value) + { + char buffer[32]; + int32_t i = 0; + + size_t num; + if (value < 0) + { + // TODO handle edge case: std::numeric_limits::min(); + num = -value; + ss << '-'; + } + else + { + num = value; + } + + // Decimal digits + if constexpr (TDecimalPlace > 0) + { + while (num != 0 && i < sizeof(buffer) && i < TDecimalPlace) + { + buffer[i++] = (char)('0' + (num % 10)); + num /= 10; + } + buffer[i++] = GetDecimalSeperator(); + } + + // Whole digits + size_t groupLen = 0; + do + { + if constexpr (TDigitSep) + { + if (groupLen >= 3) + { + groupLen = 0; + buffer[i++] = GetDigitSeperator(); + } + } + buffer[i++] = (char)('0' + (num % 10)); + num /= 10; + if constexpr (TDigitSep) + { + groupLen++; + } + } while (num != 0 && i < sizeof(buffer)); + + // Finally reverse append the string + for (int32_t j = i - 1; j >= 0; j--) + { + ss << buffer[j]; + } + } + + template void FormatArgument(std::stringstream& ss, FormatToken token, T arg) + { + switch (token) + { + case FORMAT_UINT16: + case FORMAT_INT32: + if constexpr (std::is_integral()) + { + FormatNumber<0, false>(ss, arg); + } + break; + case FORMAT_COMMA16: + case FORMAT_COMMA32: + if constexpr (std::is_integral()) + { + FormatNumber<0, true>(ss, arg); + } + break; + case FORMAT_COMMA1DP16: + if constexpr (std::is_integral()) + { + FormatNumber<1, true>(ss, arg); + } + else if constexpr (std::is_floating_point()) + { + FormatNumber<1, true>(ss, std::round(arg * 10)); + } + break; + case FORMAT_COMMA2DP32: + if constexpr (std::is_integral()) + { + FormatNumber<2, true>(ss, arg); + } + else if constexpr (std::is_floating_point()) + { + FormatNumber<2, true>(ss, std::round(arg * 100)); + } + break; + case FORMAT_VELOCITY: + if constexpr (std::is_integral()) + { + switch (gConfigGeneral.measurement_format) + { + default: + case MeasurementFormat::Imperial: + FormatStringId(ss, STR_UNIT_SUFFIX_MILES_PER_HOUR, arg); + break; + case MeasurementFormat::Metric: + FormatStringId(ss, STR_UNIT_SUFFIX_KILOMETRES_PER_HOUR, mph_to_kmph(arg)); + break; + case MeasurementFormat::SI: + FormatStringId(ss, STR_UNIT_SUFFIX_METRES_PER_SECOND, mph_to_dmps(arg)); + break; + } + } + break; + case FORMAT_STRING: + if constexpr (std::is_same()) + { + ss << arg; + } + else if constexpr (std::is_same()) + { + ss << arg.c_str(); + } + break; + case FORMAT_STRINGID: + case FORMAT_STRINGID2: + if constexpr (std::is_integral()) + { + ss << language_get_string(arg); + } + break; + } + } + + std::pair FormatNextPart(std::string_view& fmt) + { + if (fmt.size() > 0) + { + for (size_t i = 0; i < fmt.size() - 1; i++) + { + if (fmt[i] == '{' && fmt[i + 1] != '}') + { + if (i == 0) + { + // Find end brace + for (size_t j = i + 1; j < fmt.size(); j++) + { + if (fmt[j] == '}') + { + auto result = fmt.substr(0, j + 1); + fmt = fmt.substr(j + 1); + return { result, format_get_code(result.substr(1, result.size() - 2)) }; + } + } + } + else + { + auto result = fmt.substr(0, i); + fmt = fmt.substr(i); + return { result, 0 }; + } + } + } + } + { + auto result = fmt; + fmt = {}; + return { result, 0 }; + } + } + + bool CanFormatToken(FormatToken t) + { + return t == FORMAT_COMMA1DP16 || (t >= FORMAT_COMMA32 && t <= FORMAT_LENGTH); + } + + template void FormatArgument(std::stringstream&, uint32_t, int32_t); + template void FormatArgument(std::stringstream&, uint32_t, const char*); +} // namespace OpenRCT2 diff --git a/src/openrct2/localisation/Formatting.h b/src/openrct2/localisation/Formatting.h new file mode 100644 index 0000000000..6233af63bf --- /dev/null +++ b/src/openrct2/localisation/Formatting.h @@ -0,0 +1,102 @@ +/***************************************************************************** + * Copyright (c) 2014-2020 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#pragma once + +#include "../common.h" +#include "Language.h" + +#include +#include +#include +#include + +namespace OpenRCT2 +{ + using FormatToken = uint32_t; + + template void FormatArgument(std::stringstream& ss, FormatToken token, T arg); + + std::pair FormatNextPart(std::string_view& fmt); + bool CanFormatToken(FormatToken t); + + inline void FormatString(std::stringstream& ss, std::string_view& fmtc) + { + while (!fmtc.empty()) + { + auto [part, token] = FormatNextPart(fmtc); + if (!CanFormatToken(token)) + { + ss << part; + FormatString(ss, fmtc); + } + } + } + + template static void FormatString(std::stringstream& ss, std::string_view& fmtc, TArg0 arg0) + { + if (!fmtc.empty()) + { + auto [part, token] = FormatNextPart(fmtc); + if (CanFormatToken(token)) + { + FormatArgument(ss, token, arg0); + FormatString(ss, fmtc); + } + else + { + ss << part; + FormatString(ss, fmtc, arg0); + } + } + } + + template + static void FormatString(std::stringstream& ss, std::string_view& fmtc, TArg0 arg0, TArgs&&... argN) + { + if (!fmtc.empty()) + { + auto [part, token] = FormatNextPart(fmtc); + if (CanFormatToken(token)) + { + FormatArgument(ss, token, arg0); + return FormatString(ss, fmtc, argN...); + } + else + { + ss << part; + return FormatString(ss, fmtc, arg0, argN...); + } + } + } + + template std::string FormatString(std::string_view fmt, TArgs&&... argN) + { + thread_local std::stringstream ss; + // Reset the buffer (reported as most efficient way) + std::stringstream().swap(ss); + FormatString(ss, fmt, argN...); + return ss.str(); + } + + template static void FormatStringId(std::stringstream& ss, rct_string_id fmt, TArgs&&... argN) + { + auto lang = language_get_string(fmt); + auto fmtsz = language_convert_string_to_tokens(lang); + auto fmtc = std::string_view(fmtsz); + FormatString(ss, fmtc, argN...); + } + + template std::string FormatStringId(rct_string_id fmt, TArgs&&... argN) + { + auto lang = language_get_string(fmt); + auto fmtc = language_convert_string_to_tokens(lang); + return FormatString(fmtc, argN...); + } +} // namespace OpenRCT2 diff --git a/test/tests/FormattingTests.cpp b/test/tests/FormattingTests.cpp new file mode 100644 index 0000000000..a6339d6d3f --- /dev/null +++ b/test/tests/FormattingTests.cpp @@ -0,0 +1,118 @@ +/***************************************************************************** + * Copyright (c) 2014-2020 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#include "openrct2/localisation/Formatting.h" + +#include +#include +#include +#include + +using namespace OpenRCT2; + +class FormattingTests : public testing::Test +{ +private: + static std::shared_ptr _context; + +protected: + static void SetUpTestCase() + { + gOpenRCT2Headless = true; + gOpenRCT2NoGraphics = true; + _context = CreateContext(); + bool initialised = _context->Initialise(); + ASSERT_TRUE(initialised); + + // load_from_sv6(parkPath.c_str()); + // game_load_init(); + + // Changed in some tests. Store to restore its value + // _gScreenFlags = gScreenFlags; + SUCCEED(); + } + + static void TearDownTestCase() + { + if (_context) + _context.reset(); + + // gScreenFlags = _gScreenFlags; + } +}; + +std::shared_ptr FormattingTests::_context; +TEST_F(FormattingTests, no_args) +{ + auto actual = FormatString("test string"); + ASSERT_EQ("test string", actual); +} + +TEST_F(FormattingTests, missing_arg) +{ + auto actual = FormatString("test {STRING} arg"); + ASSERT_EQ("test arg", actual); +} + +TEST_F(FormattingTests, integer) +{ + auto actual = FormatString("Guests: {INT32}", 32); + ASSERT_EQ("Guests: 32", actual); +} + +TEST_F(FormattingTests, integer_integer) +{ + auto actual = FormatString("Guests: {INT32}, Staff: {INT32}", 32, 10); + ASSERT_EQ("Guests: 32, Staff: 10", actual); +} + +TEST_F(FormattingTests, comma) +{ + auto actual = FormatString("Guests: {COMMA16}", 12534); + ASSERT_EQ("Guests: 12,534", actual); +} + +TEST_F(FormattingTests, comma_0) +{ + auto actual = FormatString("Guests: {COMMA16}", 0); + ASSERT_EQ("Guests: 0", actual); +} + +TEST_F(FormattingTests, string) +{ + auto actual = FormatString("{RED}{STRING} has broken down.", "Woodchip"); + ASSERT_EQ("{RED}Woodchip has broken down.", actual); +} + +TEST_F(FormattingTests, escaped_braces) +{ + auto actual = FormatString("--{{ESCAPED}}--", 0); + ASSERT_EQ("--{{ESCAPED}}--", actual); +} + +TEST_F(FormattingTests, velocity_mph) +{ + gConfigGeneral.measurement_format = MeasurementFormat::Imperial; + auto actual = FormatString("Train is going at {VELOCITY}.", 1024); + ASSERT_EQ("Train is going at 1,024 mph.", actual); +} + +TEST_F(FormattingTests, velocity_kph) +{ + gConfigGeneral.measurement_format = MeasurementFormat::Metric; + auto actual = FormatString("Train is going at {VELOCITY}.", 1024); + ASSERT_EQ("Train is going at 1,648 km/h.", actual); +} + +TEST_F(FormattingTests, velocity_mps) +{ + gConfigGeneral.measurement_format = MeasurementFormat::SI; + auto actual = FormatString("Train is going at {VELOCITY}.", 1024); + ASSERT_EQ("Train is going at 457.7 m/s.", actual); +} diff --git a/test/tests/tests.vcxproj b/test/tests/tests.vcxproj index 75678eb34e..574ac7a22b 100644 --- a/test/tests/tests.vcxproj +++ b/test/tests/tests.vcxproj @@ -59,6 +59,7 @@ +