aboutsummaryrefslogtreecommitdiff
path: root/src/zencore/string.cpp
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2023-05-02 10:01:47 +0200
committerGitHub <[email protected]>2023-05-02 10:01:47 +0200
commit075d17f8ada47e990fe94606c3d21df409223465 (patch)
treee50549b766a2f3c354798a54ff73404217b4c9af /src/zencore/string.cpp
parentfix: bundle shouldn't append content zip to zen (diff)
downloadzen-075d17f8ada47e990fe94606c3d21df409223465.tar.xz
zen-075d17f8ada47e990fe94606c3d21df409223465.zip
moved source directories into `/src` (#264)
* moved source directories into `/src` * updated bundle.lua for new `src` path * moved some docs, icon * removed old test trees
Diffstat (limited to 'src/zencore/string.cpp')
-rw-r--r--src/zencore/string.cpp1004
1 files changed, 1004 insertions, 0 deletions
diff --git a/src/zencore/string.cpp b/src/zencore/string.cpp
new file mode 100644
index 000000000..ad6ee78fc
--- /dev/null
+++ b/src/zencore/string.cpp
@@ -0,0 +1,1004 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zencore/memory.h>
+#include <zencore/string.h>
+#include <zencore/testing.h>
+
+#include <inttypes.h>
+#include <math.h>
+#include <stdio.h>
+#include <exception>
+#include <ostream>
+#include <stdexcept>
+
+#include <utf8.h>
+
+template<typename u16bit_iterator>
+void
+utf16to8_impl(u16bit_iterator StartIt, u16bit_iterator EndIt, ::zen::StringBuilderBase& OutString)
+{
+ while (StartIt != EndIt)
+ {
+ uint32_t cp = utf8::internal::mask16(*StartIt++);
+ // Take care of surrogate pairs first
+ if (utf8::internal::is_lead_surrogate(cp))
+ {
+ uint32_t trail_surrogate = utf8::internal::mask16(*StartIt++);
+ cp = (cp << 10) + trail_surrogate + utf8::internal::SURROGATE_OFFSET;
+ }
+ OutString.AppendCodepoint(cp);
+ }
+}
+
+template<typename u32bit_iterator>
+void
+utf32to8_impl(u32bit_iterator StartIt, u32bit_iterator EndIt, ::zen::StringBuilderBase& OutString)
+{
+ for (; StartIt != EndIt; ++StartIt)
+ {
+ wchar_t cp = *StartIt;
+ OutString.AppendCodepoint(cp);
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+namespace zen {
+
+bool
+ToString(std::span<char> Buffer, uint64_t Num)
+{
+ snprintf(Buffer.data(), Buffer.size(), "%" PRIu64, Num);
+
+ return true;
+}
+bool
+ToString(std::span<char> Buffer, int64_t Num)
+{
+ snprintf(Buffer.data(), Buffer.size(), "%" PRId64, Num);
+
+ return true;
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+const char*
+FilepathFindExtension(const std::string_view& Path, const char* ExtensionToMatch)
+{
+ const size_t PathLen = Path.size();
+
+ if (ExtensionToMatch)
+ {
+ size_t ExtLen = strlen(ExtensionToMatch);
+
+ if (ExtLen > PathLen)
+ return nullptr;
+
+ const char* PathExtension = Path.data() + PathLen - ExtLen;
+
+ if (StringEquals(PathExtension, ExtensionToMatch))
+ return PathExtension;
+
+ return nullptr;
+ }
+
+ if (PathLen == 0)
+ return nullptr;
+
+ // Look for extension introducer ('.')
+
+ for (int64_t i = PathLen - 1; i >= 0; --i)
+ {
+ if (Path[i] == '.')
+ return Path.data() + i;
+ }
+
+ return nullptr;
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+void
+Utf8ToWide(const char8_t* Str8, WideStringBuilderBase& OutString)
+{
+ Utf8ToWide(std::u8string_view(Str8), OutString);
+}
+
+void
+Utf8ToWide(const std::string_view& Str8, WideStringBuilderBase& OutString)
+{
+ Utf8ToWide(std::u8string_view{reinterpret_cast<const char8_t*>(Str8.data()), Str8.size()}, OutString);
+}
+
+std::wstring
+Utf8ToWide(const std::string_view& Wstr)
+{
+ ExtendableWideStringBuilder<128> String;
+ Utf8ToWide(Wstr, String);
+
+ return String.c_str();
+}
+
+void
+Utf8ToWide(const std::u8string_view& Str8, WideStringBuilderBase& OutString)
+{
+ const char* str = (const char*)Str8.data();
+ const size_t strLen = Str8.size();
+
+ const char* endStr = str + strLen;
+ size_t ByteCount = 0;
+ size_t CurrentOutChar = 0;
+
+ for (; str != endStr; ++str)
+ {
+ unsigned char Data = static_cast<unsigned char>(*str);
+
+ if (!(Data & 0x80))
+ {
+ // ASCII
+ OutString.Append(wchar_t(Data));
+ continue;
+ }
+ else if (!ByteCount)
+ {
+ // Start of multi-byte sequence. Figure out how
+ // many bytes we're going to consume
+
+ size_t Count = 0;
+
+ for (size_t Temp = Data; Temp & 0x80; Temp <<= 1)
+ ++Count;
+
+ ByteCount = Count - 1;
+ CurrentOutChar = Data & (0xff >> (Count + 1));
+ }
+ else
+ {
+ --ByteCount;
+
+ if ((Data & 0xc0) != 0x80)
+ {
+ break;
+ }
+
+ CurrentOutChar = (CurrentOutChar << 6) | (Data & 0x3f);
+
+ if (!ByteCount)
+ {
+ OutString.Append(wchar_t(CurrentOutChar));
+ CurrentOutChar = 0;
+ }
+ }
+ }
+}
+
+void
+WideToUtf8(const wchar_t* Wstr, StringBuilderBase& OutString)
+{
+ WideToUtf8(std::wstring_view{Wstr}, OutString);
+}
+
+void
+WideToUtf8(const std::wstring_view& Wstr, StringBuilderBase& OutString)
+{
+#if ZEN_SIZEOF_WCHAR_T == 2
+ utf16to8_impl(begin(Wstr), end(Wstr), OutString);
+#else
+ utf32to8_impl(begin(Wstr), end(Wstr), OutString);
+#endif
+}
+
+std::string
+WideToUtf8(const wchar_t* Wstr)
+{
+ ExtendableStringBuilder<128> String;
+ WideToUtf8(std::wstring_view{Wstr}, String);
+
+ return String.c_str();
+}
+
+std::string
+WideToUtf8(const std::wstring_view Wstr)
+{
+ ExtendableStringBuilder<128> String;
+ WideToUtf8(std::wstring_view{Wstr.data(), Wstr.size()}, String);
+
+ return String.c_str();
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+enum NicenumFormat
+{
+ kNicenum1024 = 0, // Print kilo, mega, tera, peta, exa..
+ kNicenumBytes = 1, // Print single bytes ("13B"), kilo, mega, tera...
+ kNicenumTime = 2, // Print nanosecs, microsecs, millisecs, seconds...
+ kNicenumRaw = 3, // Print the raw number without any formatting
+ kNicenumRawTime = 4 // Same as RAW, but print dashes ('-') for zero.
+};
+
+namespace {
+ static const char* UnitStrings[3][7] = {
+ /* kNicenum1024 */ {"", "K", "M", "G", "T", "P", "E"},
+ /* kNicenumBytes */ {"B", "K", "M", "G", "T", "P", "E"},
+ /* kNicenumTime */ {"ns", "us", "ms", "s", "?", "?", "?"}};
+
+ static const int UnitsLen[] = {
+ /* kNicenum1024 */ 6,
+ /* kNicenumBytes */ 6,
+ /* kNicenumTime */ 3};
+
+ static const uint64_t KiloUnit[] = {
+ /* kNicenum1024 */ 1024,
+ /* kNicenumBytes */ 1024,
+ /* kNicenumTime */ 1000};
+} // namespace
+
+/*
+ * Convert a number to an appropriately human-readable output.
+ */
+int
+NiceNumGeneral(uint64_t Num, std::span<char> Buffer, NicenumFormat Format)
+{
+ switch (Format)
+ {
+ case kNicenumRaw:
+ return snprintf(Buffer.data(), Buffer.size(), "%" PRIu64, (uint64_t)Num);
+
+ case kNicenumRawTime:
+ if (Num > 0)
+ {
+ return snprintf(Buffer.data(), Buffer.size(), "%" PRIu64, (uint64_t)Num);
+ }
+ else
+ {
+ return snprintf(Buffer.data(), Buffer.size(), "%s", "-");
+ }
+ break;
+
+ case kNicenum1024:
+ case kNicenumBytes:
+ case kNicenumTime:
+ default:
+ break;
+ }
+
+ // Bring into range and select unit
+
+ int Index = 0;
+ uint64_t n = Num;
+
+ {
+ const uint64_t Unit = KiloUnit[Format];
+ const int maxIndex = UnitsLen[Format];
+
+ while (n >= Unit && Index < maxIndex)
+ {
+ n /= Unit;
+ Index++;
+ }
+ }
+
+ const char* u = UnitStrings[Format][Index];
+
+ if ((Index == 0) || ((Num % (uint64_t)powl((int)KiloUnit[Format], Index)) == 0))
+ {
+ /*
+ * If this is an even multiple of the base, always display
+ * without any decimal precision.
+ */
+ return snprintf(Buffer.data(), Buffer.size(), "%" PRIu64 "%s", (uint64_t)n, u);
+ }
+ else
+ {
+ /*
+ * We want to choose a precision that reflects the best choice
+ * for fitting in 5 characters. This can get rather tricky when
+ * we have numbers that are very close to an order of magnitude.
+ * For example, when displaying 10239 (which is really 9.999K),
+ * we want only a single place of precision for 10.0K. We could
+ * develop some complex heuristics for this, but it's much
+ * easier just to try each combination in turn.
+ */
+
+ int StrLen = 0;
+
+ for (int i = 2; i >= 0; i--)
+ {
+ double Value = (double)Num / (uint64_t)powl((int)KiloUnit[Format], Index);
+
+ /*
+ * Don't print floating point values for time. Note,
+ * we use floor() instead of round() here, since
+ * round can result in undesirable results. For
+ * example, if "num" is in the range of
+ * 999500-999999, it will print out "1000us". This
+ * doesn't happen if we use floor().
+ */
+ if (Format == kNicenumTime)
+ {
+ StrLen = snprintf(Buffer.data(), Buffer.size(), "%d%s", (unsigned int)floor(Value), u);
+
+ if (StrLen <= 5)
+ break;
+ }
+ else
+ {
+ StrLen = snprintf(Buffer.data(), Buffer.size(), "%.*f%s", i, Value, u);
+
+ if (StrLen <= 5)
+ break;
+ }
+ }
+
+ return StrLen;
+ }
+}
+
+size_t
+NiceNumToBuffer(uint64_t Num, std::span<char> Buffer)
+{
+ return NiceNumGeneral(Num, Buffer, kNicenum1024);
+}
+
+size_t
+NiceBytesToBuffer(uint64_t Num, std::span<char> Buffer)
+{
+ return NiceNumGeneral(Num, Buffer, kNicenumBytes);
+}
+
+size_t
+NiceByteRateToBuffer(uint64_t Num, uint64_t ElapsedMs, std::span<char> Buffer)
+{
+ size_t n = 0;
+
+ if (ElapsedMs)
+ {
+ n = NiceNumGeneral(Num * 1000 / ElapsedMs, Buffer, kNicenumBytes);
+ }
+ else
+ {
+ Buffer[n++] = '0';
+ Buffer[n++] = 'B';
+ }
+
+ Buffer[n++] = '/';
+ Buffer[n++] = 's';
+ Buffer[n++] = '\0';
+
+ return n;
+}
+
+size_t
+NiceLatencyNsToBuffer(uint64_t Nanos, std::span<char> Buffer)
+{
+ return NiceNumGeneral(Nanos, Buffer, kNicenumTime);
+}
+
+size_t
+NiceTimeSpanMsToBuffer(uint64_t Millis, std::span<char> Buffer)
+{
+ if (Millis < 1000)
+ {
+ return snprintf(Buffer.data(), Buffer.size(), "%" PRIu64 "ms", Millis);
+ }
+ else if (Millis < 10000)
+ {
+ return snprintf(Buffer.data(), Buffer.size(), "%.2fs", Millis / 1000.0);
+ }
+ else if (Millis < 60000)
+ {
+ return snprintf(Buffer.data(), Buffer.size(), "%.1fs", Millis / 1000.0);
+ }
+ else if (Millis < 60 * 60000)
+ {
+ return snprintf(Buffer.data(), Buffer.size(), "%" PRIu64 "m%02" PRIu64 "s", Millis / 60000, (Millis / 1000) % 60);
+ }
+ else
+ {
+ return snprintf(Buffer.data(), Buffer.size(), "%" PRIu64 "h%02" PRIu64 "m", Millis / 3600000, (Millis / 60000) % 60);
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+template<typename C>
+StringBuilderImpl<C>::~StringBuilderImpl()
+{
+ if (m_IsDynamic)
+ {
+ FreeBuffer(m_Base, m_End - m_Base);
+ }
+}
+
+template<typename C>
+void
+StringBuilderImpl<C>::Extend(size_t extraCapacity)
+{
+ if (!m_IsExtendable)
+ {
+ Fail("exceeded capacity");
+ }
+
+ const size_t oldCapacity = m_End - m_Base;
+ const size_t newCapacity = NextPow2(oldCapacity + extraCapacity);
+
+ C* newBase = (C*)AllocBuffer(newCapacity);
+
+ size_t pos = m_CurPos - m_Base;
+ memcpy(newBase, m_Base, pos * sizeof(C));
+
+ if (m_IsDynamic)
+ {
+ FreeBuffer(m_Base, oldCapacity);
+ }
+
+ m_Base = newBase;
+ m_CurPos = newBase + pos;
+ m_End = newBase + newCapacity;
+ m_IsDynamic = true;
+}
+
+template<typename C>
+void*
+StringBuilderImpl<C>::AllocBuffer(size_t byteCount)
+{
+ return Memory::Alloc(byteCount * sizeof(C));
+}
+
+template<typename C>
+void
+StringBuilderImpl<C>::FreeBuffer(void* buffer, size_t byteCount)
+{
+ ZEN_UNUSED(byteCount);
+
+ Memory::Free(buffer);
+}
+
+template<typename C>
+[[noreturn]] void
+StringBuilderImpl<C>::Fail(const char* reason)
+{
+ throw std::runtime_error(reason);
+}
+
+// Instantiate templates once
+
+template class StringBuilderImpl<char>;
+template class StringBuilderImpl<wchar_t>;
+
+//////////////////////////////////////////////////////////////////////////
+//
+// Unit tests
+//
+
+#if ZEN_WITH_TESTS
+
+TEST_CASE("niceNum")
+{
+ char Buffer[16];
+
+ SUBCASE("raw")
+ {
+ NiceNumGeneral(1, Buffer, kNicenumRaw);
+ CHECK(StringEquals(Buffer, "1"));
+
+ NiceNumGeneral(10, Buffer, kNicenumRaw);
+ CHECK(StringEquals(Buffer, "10"));
+
+ NiceNumGeneral(100, Buffer, kNicenumRaw);
+ CHECK(StringEquals(Buffer, "100"));
+
+ NiceNumGeneral(1000, Buffer, kNicenumRaw);
+ CHECK(StringEquals(Buffer, "1000"));
+
+ NiceNumGeneral(10000, Buffer, kNicenumRaw);
+ CHECK(StringEquals(Buffer, "10000"));
+
+ NiceNumGeneral(100000, Buffer, kNicenumRaw);
+ CHECK(StringEquals(Buffer, "100000"));
+ }
+
+ SUBCASE("1024")
+ {
+ NiceNumGeneral(1, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "1"));
+
+ NiceNumGeneral(10, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "10"));
+
+ NiceNumGeneral(100, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "100"));
+
+ NiceNumGeneral(1000, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "1000"));
+
+ NiceNumGeneral(10000, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "9.77K"));
+
+ NiceNumGeneral(100000, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "97.7K"));
+
+ NiceNumGeneral(1000000, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "977K"));
+
+ NiceNumGeneral(10000000, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "9.54M"));
+
+ NiceNumGeneral(100000000, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "95.4M"));
+
+ NiceNumGeneral(1000000000, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "954M"));
+
+ NiceNumGeneral(10000000000, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "9.31G"));
+
+ NiceNumGeneral(100000000000, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "93.1G"));
+
+ NiceNumGeneral(1000000000000, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "931G"));
+
+ NiceNumGeneral(10000000000000, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "9.09T"));
+
+ NiceNumGeneral(100000000000000, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "90.9T"));
+
+ NiceNumGeneral(1000000000000000, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "909T"));
+
+ NiceNumGeneral(10000000000000000, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "8.88P"));
+
+ NiceNumGeneral(100000000000000000, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "88.8P"));
+
+ NiceNumGeneral(1000000000000000000, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "888P"));
+
+ NiceNumGeneral(10000000000000000000ull, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "8.67E"));
+
+ // pow2
+
+ NiceNumGeneral(0, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "0"));
+
+ NiceNumGeneral(1, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "1"));
+
+ NiceNumGeneral(1024, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "1K"));
+
+ NiceNumGeneral(1024 * 1024, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "1M"));
+
+ NiceNumGeneral(1024 * 1024 * 1024, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "1G"));
+
+ NiceNumGeneral(1024llu * 1024 * 1024 * 1024, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "1T"));
+
+ NiceNumGeneral(1024llu * 1024 * 1024 * 1024 * 1024, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "1P"));
+
+ NiceNumGeneral(1024llu * 1024 * 1024 * 1024 * 1024 * 1024, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "1E"));
+
+ // pow2-1
+
+ NiceNumGeneral(1023, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "1023"));
+
+ NiceNumGeneral(2047, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "2.00K"));
+
+ NiceNumGeneral(9 * 1024 - 1, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "9.00K"));
+
+ NiceNumGeneral(10 * 1024 - 1, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "10.0K"));
+
+ NiceNumGeneral(10 * 1024 - 5, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "10.0K"));
+
+ NiceNumGeneral(10 * 1024 - 6, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "9.99K"));
+
+ NiceNumGeneral(10 * 1024 - 10, Buffer, kNicenum1024);
+ CHECK(StringEquals(Buffer, "9.99K"));
+ }
+
+ SUBCASE("time")
+ {
+ NiceNumGeneral(1, Buffer, kNicenumTime);
+ CHECK(StringEquals(Buffer, "1ns"));
+
+ NiceNumGeneral(100, Buffer, kNicenumTime);
+ CHECK(StringEquals(Buffer, "100ns"));
+
+ NiceNumGeneral(1000, Buffer, kNicenumTime);
+ CHECK(StringEquals(Buffer, "1us"));
+
+ NiceNumGeneral(10000, Buffer, kNicenumTime);
+ CHECK(StringEquals(Buffer, "10us"));
+
+ NiceNumGeneral(100000, Buffer, kNicenumTime);
+ CHECK(StringEquals(Buffer, "100us"));
+
+ NiceNumGeneral(1000000, Buffer, kNicenumTime);
+ CHECK(StringEquals(Buffer, "1ms"));
+
+ NiceNumGeneral(10000000, Buffer, kNicenumTime);
+ CHECK(StringEquals(Buffer, "10ms"));
+
+ NiceNumGeneral(100000000, Buffer, kNicenumTime);
+ CHECK(StringEquals(Buffer, "100ms"));
+
+ NiceNumGeneral(1000000000, Buffer, kNicenumTime);
+ CHECK(StringEquals(Buffer, "1s"));
+
+ NiceNumGeneral(10000000000, Buffer, kNicenumTime);
+ CHECK(StringEquals(Buffer, "10s"));
+
+ NiceNumGeneral(100000000000, Buffer, kNicenumTime);
+ CHECK(StringEquals(Buffer, "100s"));
+
+ NiceNumGeneral(1000000000000, Buffer, kNicenumTime);
+ CHECK(StringEquals(Buffer, "1000s"));
+
+ NiceNumGeneral(10000000000000, Buffer, kNicenumTime);
+ CHECK(StringEquals(Buffer, "10000s"));
+
+ NiceNumGeneral(100000000000000, Buffer, kNicenumTime);
+ CHECK(StringEquals(Buffer, "100000s"));
+ }
+
+ SUBCASE("bytes")
+ {
+ NiceNumGeneral(1, Buffer, kNicenumBytes);
+ CHECK(StringEquals(Buffer, "1B"));
+
+ NiceNumGeneral(10, Buffer, kNicenumBytes);
+ CHECK(StringEquals(Buffer, "10B"));
+
+ NiceNumGeneral(100, Buffer, kNicenumBytes);
+ CHECK(StringEquals(Buffer, "100B"));
+
+ NiceNumGeneral(1000, Buffer, kNicenumBytes);
+ CHECK(StringEquals(Buffer, "1000B"));
+
+ NiceNumGeneral(10000, Buffer, kNicenumBytes);
+ CHECK(StringEquals(Buffer, "9.77K"));
+ }
+
+ SUBCASE("byteRate")
+ {
+ NiceByteRateToBuffer(1, 1, Buffer);
+ CHECK(StringEquals(Buffer, "1000B/s"));
+
+ NiceByteRateToBuffer(1000, 1000, Buffer);
+ CHECK(StringEquals(Buffer, "1000B/s"));
+
+ NiceByteRateToBuffer(1024, 1, Buffer);
+ CHECK(StringEquals(Buffer, "1000K/s"));
+
+ NiceByteRateToBuffer(1024, 1000, Buffer);
+ CHECK(StringEquals(Buffer, "1K/s"));
+ }
+
+ SUBCASE("timespan")
+ {
+ NiceTimeSpanMsToBuffer(1, Buffer);
+ CHECK(StringEquals(Buffer, "1ms"));
+
+ NiceTimeSpanMsToBuffer(900, Buffer);
+ CHECK(StringEquals(Buffer, "900ms"));
+
+ NiceTimeSpanMsToBuffer(1000, Buffer);
+ CHECK(StringEquals(Buffer, "1.00s"));
+
+ NiceTimeSpanMsToBuffer(1900, Buffer);
+ CHECK(StringEquals(Buffer, "1.90s"));
+
+ NiceTimeSpanMsToBuffer(19000, Buffer);
+ CHECK(StringEquals(Buffer, "19.0s"));
+
+ NiceTimeSpanMsToBuffer(60000, Buffer);
+ CHECK(StringEquals(Buffer, "1m00s"));
+
+ NiceTimeSpanMsToBuffer(600000, Buffer);
+ CHECK(StringEquals(Buffer, "10m00s"));
+
+ NiceTimeSpanMsToBuffer(3600000, Buffer);
+ CHECK(StringEquals(Buffer, "1h00m"));
+
+ NiceTimeSpanMsToBuffer(36000000, Buffer);
+ CHECK(StringEquals(Buffer, "10h00m"));
+
+ NiceTimeSpanMsToBuffer(360000000, Buffer);
+ CHECK(StringEquals(Buffer, "100h00m"));
+ }
+}
+
+void
+string_forcelink()
+{
+}
+
+TEST_CASE("StringBuilder")
+{
+ StringBuilder<64> sb;
+
+ SUBCASE("Empty init")
+ {
+ const char* str = sb.c_str();
+
+ CHECK(StringLength(str) == 0);
+ }
+
+ SUBCASE("Append single character")
+ {
+ sb.Append('a');
+
+ const char* str = sb.c_str();
+ CHECK(StringLength(str) == 1);
+ CHECK(str[0] == 'a');
+
+ sb.Append('b');
+ str = sb.c_str();
+ CHECK(StringLength(str) == 2);
+ CHECK(str[0] == 'a');
+ CHECK(str[1] == 'b');
+ }
+
+ SUBCASE("Append string")
+ {
+ sb.Append("a");
+
+ const char* str = sb.c_str();
+ CHECK(StringLength(str) == 1);
+ CHECK(str[0] == 'a');
+
+ sb.Append("b");
+ str = sb.c_str();
+ CHECK(StringLength(str) == 2);
+ CHECK(str[0] == 'a');
+ CHECK(str[1] == 'b');
+
+ sb.Append("cdefghijklmnopqrstuvwxyz");
+ CHECK(sb.Size() == 26);
+
+ sb.Append("abcdefghijklmnopqrstuvwxyz");
+ CHECK(sb.Size() == 52);
+
+ sb.Append("abcdefghijk");
+ CHECK(sb.Size() == 63);
+ }
+}
+
+TEST_CASE("ExtendableStringBuilder")
+{
+ ExtendableStringBuilder<16> sb;
+
+ SUBCASE("Empty init")
+ {
+ const char* str = sb.c_str();
+
+ CHECK(StringLength(str) == 0);
+ }
+
+ SUBCASE("Short append")
+ {
+ sb.Append("abcd");
+ CHECK(sb.IsDynamic() == false);
+ }
+
+ SUBCASE("Short+long append")
+ {
+ sb.Append("abcd");
+ CHECK(sb.IsDynamic() == false);
+ // This should trigger a dynamic buffer allocation since the required
+ // capacity exceeds the internal fixed buffer.
+ sb.Append("abcdefghijklmnopqrstuvwxyz");
+ CHECK(sb.IsDynamic() == true);
+ CHECK(sb.Size() == 30);
+ CHECK(sb.Size() == StringLength(sb.c_str()));
+ }
+}
+
+TEST_CASE("WideStringBuilder")
+{
+ WideStringBuilder<64> sb;
+
+ SUBCASE("Empty init")
+ {
+ const wchar_t* str = sb.c_str();
+
+ CHECK(StringLength(str) == 0);
+ }
+
+ SUBCASE("Append single character")
+ {
+ sb.Append(L'a');
+
+ const wchar_t* str = sb.c_str();
+ CHECK(StringLength(str) == 1);
+ CHECK(str[0] == L'a');
+
+ sb.Append(L'b');
+ str = sb.c_str();
+ CHECK(StringLength(str) == 2);
+ CHECK(str[0] == L'a');
+ CHECK(str[1] == L'b');
+ }
+
+ SUBCASE("Append string")
+ {
+ sb.Append(L"a");
+
+ const wchar_t* str = sb.c_str();
+ CHECK(StringLength(str) == 1);
+ CHECK(str[0] == L'a');
+
+ sb.Append(L"b");
+ str = sb.c_str();
+ CHECK(StringLength(str) == 2);
+ CHECK(str[0] == L'a');
+ CHECK(str[1] == L'b');
+
+ sb.Append(L"cdefghijklmnopqrstuvwxyz");
+ CHECK(sb.Size() == 26);
+
+ sb.Append(L"abcdefghijklmnopqrstuvwxyz");
+ CHECK(sb.Size() == 52);
+
+ sb.Append(L"abcdefghijk");
+ CHECK(sb.Size() == 63);
+ }
+}
+
+TEST_CASE("ExtendableWideStringBuilder")
+{
+ ExtendableWideStringBuilder<16> sb;
+
+ SUBCASE("Empty init")
+ {
+ CHECK(sb.Size() == 0);
+
+ const wchar_t* str = sb.c_str();
+ CHECK(StringLength(str) == 0);
+ }
+
+ SUBCASE("Short append")
+ {
+ sb.Append(L"abcd");
+ CHECK(sb.IsDynamic() == false);
+ }
+
+ SUBCASE("Short+long append")
+ {
+ sb.Append(L"abcd");
+ CHECK(sb.IsDynamic() == false);
+ // This should trigger a dynamic buffer allocation since the required
+ // capacity exceeds the internal fixed buffer.
+ sb.Append(L"abcdefghijklmnopqrstuvwxyz");
+ CHECK(sb.IsDynamic() == true);
+ CHECK(sb.Size() == 30);
+ CHECK(sb.Size() == StringLength(sb.c_str()));
+ }
+}
+
+TEST_CASE("utf8")
+{
+ SUBCASE("utf8towide")
+ {
+ // TODO: add more extensive testing here - this covers a very small space
+
+ WideStringBuilder<32> wout;
+ Utf8ToWide(u8"abcdefghi", wout);
+ CHECK(StringEquals(L"abcdefghi", wout.c_str()));
+
+ wout.Reset();
+
+ Utf8ToWide(u8"abc���", wout);
+ CHECK(StringEquals(L"abc���", wout.c_str()));
+ }
+
+ SUBCASE("widetoutf8")
+ {
+ // TODO: add more extensive testing here - this covers a very small space
+
+ StringBuilder<32> out;
+
+ WideToUtf8(L"abcdefghi", out);
+ CHECK(StringEquals("abcdefghi", out.c_str()));
+
+ out.Reset();
+
+ WideToUtf8(L"abc���", out);
+ CHECK(StringEquals(u8"abc���", out.c_str()));
+ }
+}
+
+TEST_CASE("filepath")
+{
+ CHECK(FilepathFindExtension("foo\\bar\\baz.txt", ".txt") != nullptr);
+ CHECK(FilepathFindExtension("foo\\bar\\baz.txt", ".zap") == nullptr);
+
+ CHECK(FilepathFindExtension("foo\\bar\\baz.txt") != nullptr);
+ CHECK(FilepathFindExtension("foo\\bar\\baz.txt") == std::string_view(".txt"));
+
+ CHECK(FilepathFindExtension(".txt") == std::string_view(".txt"));
+}
+
+TEST_CASE("string")
+{
+ using namespace std::literals;
+
+ SUBCASE("hash_djb2")
+ {
+ CHECK(HashStringAsLowerDjb2("AbcdZ"sv) == HashStringDjb2("abcdz"sv));
+ CHECK(HashStringAsLowerDjb2("aBCd"sv) == HashStringDjb2("abcd"sv));
+ CHECK(HashStringAsLowerDjb2("aBCd"sv) == HashStringDjb2(ToLower("aBCd"sv)));
+ }
+
+ SUBCASE("tolower")
+ {
+ CHECK_EQ(ToLower("te!st"sv), "te!st"sv);
+ CHECK_EQ(ToLower("TE%St"sv), "te%st"sv);
+ }
+
+ SUBCASE("StrCaseCompare")
+ {
+ CHECK(StrCaseCompare("foo", "FoO") == 0);
+ CHECK(StrCaseCompare("Bar", "bAs") < 0);
+ CHECK(StrCaseCompare("bAr", "Bas") < 0);
+ CHECK(StrCaseCompare("BBr", "Bar") > 0);
+ CHECK(StrCaseCompare("Bbr", "BAr") > 0);
+ CHECK(StrCaseCompare("foo", "FoO", 3) == 0);
+ CHECK(StrCaseCompare("Bar", "bAs", 3) < 0);
+ CHECK(StrCaseCompare("BBr", "Bar", 2) > 0);
+ }
+
+ SUBCASE("ForEachStrTok")
+ {
+ const auto Tokens = "here,is,my,different,tokens"sv;
+ int32_t ExpectedTokenCount = 5;
+ int32_t TokenCount = 0;
+ StringBuilder<512> Sb;
+
+ TokenCount = ForEachStrTok(Tokens, ',', [&Sb](const std::string_view& Token) {
+ if (Sb.Size())
+ {
+ Sb << ",";
+ }
+ Sb << Token;
+ return true;
+ });
+
+ CHECK(TokenCount == ExpectedTokenCount);
+ CHECK(Sb.ToString() == Tokens);
+
+ ExpectedTokenCount = 1;
+ const auto Str = "mosdef"sv;
+
+ Sb.Reset();
+ TokenCount = ForEachStrTok(Str, ' ', [&Sb](const std::string_view& Token) {
+ Sb << Token;
+ return true;
+ });
+ CHECK(Sb.ToString() == Str);
+ CHECK(TokenCount == ExpectedTokenCount);
+
+ ExpectedTokenCount = 0;
+ TokenCount = ForEachStrTok(""sv, ',', [](const std::string_view&) { return true; });
+ CHECK(TokenCount == ExpectedTokenCount);
+ }
+}
+
+#endif
+
+} // namespace zen