mirror of https://github.com/OpenRCT2/OpenRCT2.git
Create new templated format string
This commit is contained in:
parent
79c6b22600
commit
14377be487
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -220,6 +220,7 @@
|
|||
<ClInclude Include="localisation\Date.h" />
|
||||
<ClInclude Include="localisation\FormatCodes.h" />
|
||||
<ClInclude Include="localisation\Formatter.h" />
|
||||
<ClInclude Include="localisation\Formatting.h" />
|
||||
<ClInclude Include="localisation\Language.h" />
|
||||
<ClInclude Include="localisation\LanguagePack.h" />
|
||||
<ClInclude Include="localisation\Localisation.h" />
|
||||
|
@ -554,6 +555,7 @@
|
|||
<ClCompile Include="localisation\Currency.cpp" />
|
||||
<ClCompile Include="localisation\FormatCodes.cpp" />
|
||||
<ClCompile Include="localisation\Formatter.cpp" />
|
||||
<ClCompile Include="localisation\Formatting.cpp" />
|
||||
<ClCompile Include="localisation\Language.cpp" />
|
||||
<ClCompile Include="localisation\LanguagePack.cpp" />
|
||||
<ClCompile Include="localisation\Localisation.cpp" />
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include "FormatCodes.h"
|
||||
|
||||
#include "../common.h"
|
||||
#include "../core/String.hpp"
|
||||
#include "Localisation.h"
|
||||
|
||||
#include <iterator>
|
||||
|
@ -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)
|
||||
|
|
|
@ -11,7 +11,9 @@
|
|||
|
||||
#include "../common.h"
|
||||
|
||||
uint32_t format_get_code(const char* token);
|
||||
#include <string_view>
|
||||
|
||||
uint32_t format_get_code(std::string_view token);
|
||||
const char* format_get_token(uint32_t code);
|
||||
|
||||
enum
|
||||
|
|
|
@ -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 <cstdint>
|
||||
|
||||
namespace OpenRCT2
|
||||
{
|
||||
char GetDigitSeperator()
|
||||
{
|
||||
return ',';
|
||||
}
|
||||
|
||||
char GetDecimalSeperator()
|
||||
{
|
||||
return '.';
|
||||
}
|
||||
|
||||
template<size_t TDecimalPlace, bool TDigitSep, typename T> 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<int64_t>::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<typename T> void FormatArgument(std::stringstream& ss, FormatToken token, T arg)
|
||||
{
|
||||
switch (token)
|
||||
{
|
||||
case FORMAT_UINT16:
|
||||
case FORMAT_INT32:
|
||||
if constexpr (std::is_integral<T>())
|
||||
{
|
||||
FormatNumber<0, false>(ss, arg);
|
||||
}
|
||||
break;
|
||||
case FORMAT_COMMA16:
|
||||
case FORMAT_COMMA32:
|
||||
if constexpr (std::is_integral<T>())
|
||||
{
|
||||
FormatNumber<0, true>(ss, arg);
|
||||
}
|
||||
break;
|
||||
case FORMAT_COMMA1DP16:
|
||||
if constexpr (std::is_integral<T>())
|
||||
{
|
||||
FormatNumber<1, true>(ss, arg);
|
||||
}
|
||||
else if constexpr (std::is_floating_point<T>())
|
||||
{
|
||||
FormatNumber<1, true>(ss, std::round(arg * 10));
|
||||
}
|
||||
break;
|
||||
case FORMAT_COMMA2DP32:
|
||||
if constexpr (std::is_integral<T>())
|
||||
{
|
||||
FormatNumber<2, true>(ss, arg);
|
||||
}
|
||||
else if constexpr (std::is_floating_point<T>())
|
||||
{
|
||||
FormatNumber<2, true>(ss, std::round(arg * 100));
|
||||
}
|
||||
break;
|
||||
case FORMAT_VELOCITY:
|
||||
if constexpr (std::is_integral<T>())
|
||||
{
|
||||
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<T, const char*>())
|
||||
{
|
||||
ss << arg;
|
||||
}
|
||||
else if constexpr (std::is_same<T, const std::string&>())
|
||||
{
|
||||
ss << arg.c_str();
|
||||
}
|
||||
break;
|
||||
case FORMAT_STRINGID:
|
||||
case FORMAT_STRINGID2:
|
||||
if constexpr (std::is_integral<T>())
|
||||
{
|
||||
ss << language_get_string(arg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<std::string_view, uint32_t> 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
|
|
@ -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 <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace OpenRCT2
|
||||
{
|
||||
using FormatToken = uint32_t;
|
||||
|
||||
template<typename T> void FormatArgument(std::stringstream& ss, FormatToken token, T arg);
|
||||
|
||||
std::pair<std::string_view, uint32_t> 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<typename TArg0> 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<typename TArg0, typename... TArgs>
|
||||
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<typename... TArgs> 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<typename... TArgs> 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<typename... TArgs> 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
|
|
@ -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 <gtest/gtest.h>
|
||||
#include <openrct2/Context.h>
|
||||
#include <openrct2/OpenRCT2.h>
|
||||
#include <openrct2/config/Config.h>
|
||||
|
||||
using namespace OpenRCT2;
|
||||
|
||||
class FormattingTests : public testing::Test
|
||||
{
|
||||
private:
|
||||
static std::shared_ptr<IContext> _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<IContext> 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);
|
||||
}
|
|
@ -59,6 +59,7 @@
|
|||
<ClCompile Include="CircularBuffer.cpp" />
|
||||
<ClCompile Include="CryptTests.cpp" />
|
||||
<ClCompile Include="Endianness.cpp" />
|
||||
<ClCompile Include="FormattingTests.cpp" />
|
||||
<ClCompile Include="LanguagePackTest.cpp" />
|
||||
<ClCompile Include="ImageImporterTests.cpp" />
|
||||
<ClCompile Include="IniReaderTest.cpp" />
|
||||
|
|
Loading…
Reference in New Issue