Create new templated format string

This commit is contained in:
Ted John 2020-10-09 00:50:08 +01:00
parent 79c6b22600
commit 14377be487
9 changed files with 466 additions and 8 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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" />

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}

View File

@ -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" />