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 @@
+