From 211a30525ebc49ca48fb9da73f45c7efeacd021f Mon Sep 17 00:00:00 2001 From: Redstone1024 <2824517378@qq.com> Date: Fri, 8 Nov 2024 20:38:19 +0800 Subject: [PATCH] feat(string): add string parsing and formatting functions --- .../Source/Public/String/Conversion.h.inl | 318 ++++++++++++++++++ .../Source/Public/String/String.h | 37 ++ .../Source/Public/String/StringView.h | 15 + 3 files changed, 370 insertions(+) create mode 100644 Redcraft.Utility/Source/Public/String/Conversion.h.inl diff --git a/Redcraft.Utility/Source/Public/String/Conversion.h.inl b/Redcraft.Utility/Source/Public/String/Conversion.h.inl new file mode 100644 index 0000000..48a2e5e --- /dev/null +++ b/Redcraft.Utility/Source/Public/String/Conversion.h.inl @@ -0,0 +1,318 @@ +#pragma once + +// NOTE: This file is not intended to be included directly, it is included by 'String/String.h'. + +#include "Templates/Tuple.h" +#include "Templates/Utility.h" +#include "TypeTraits/TypeTraits.h" + +#include +#include + +#pragma warning(push) +#pragma warning(disable : 4146) + +NAMESPACE_REDCRAFT_BEGIN +NAMESPACE_MODULE_BEGIN(Redcraft) +NAMESPACE_MODULE_BEGIN(Utility) + +// NOTE: These functions are used to format an object to a string and parse a string to an object. +// If the user-defined overloads a function with the 'Fmt' parameter, fill-and-align needs to be handled. +// The formatting function should produce a string that can be parsed by the parsing function, if the parsing function exists. + +// NOTE: These functions are recommended for debug programs. + +#define LEFT_BRACE LITERAL(T, '{') +#define RIGHT_BRACE LITERAL(T, '}') + +#define ESCAPE_LEFT_BRACE TStringView(LITERAL(T, "#{")) +#define ESCAPE_RIGHT_BRACE TStringView(LITERAL(T, "}#")) + +NAMESPACE_PRIVATE_BEGIN + +template +struct TStringHelper +{ + FORCEINLINE static bool FormatObject(auto& Result, TStringView Fmt, auto& Object) requires (bIsFormat) + { + using U = TRemoveCVRef; + + if constexpr (!CConst>) + { + checkf(false, TEXT("Unsafe formatting for a variable that is non-const.")); + + return false; + } + + return false; + } + + FORCEINLINE static bool ParseObject(TStringView& View, TStringView Fmt, auto& Object) requires (!bIsFormat) + { + using U = TRemoveCVRef; + + if constexpr (CConst>) + { + checkf(false, TEXT("Cannot assign to a variable that is const.")); + + return false; + } + + return false; + } + + FORCEINLINE static size_t Do(auto& Result, TStringView Fmt, auto ArgsTuple) + { + size_t FormattedObjectNum = 0; + + size_t ArgsIndex = 0; + + auto ParseFormat = [&FormattedObjectNum, &ArgsIndex, ArgsTuple](auto& Self, auto& String, TStringView& Fmt) -> bool + { + bool bIsFullyFormatted = true; + + while (!Fmt.IsEmpty()) + { + if (Fmt.StartsWith(ESCAPE_LEFT_BRACE)) + { + Fmt.RemovePrefix(ESCAPE_LEFT_BRACE.Num()); + + if constexpr (!bIsFormat) + { + if (!String.StartsWith(LEFT_BRACE)) return false; + + String.RemovePrefix(1); + } + else String += LEFT_BRACE; + + continue; + } + + if (Fmt.StartsWith(ESCAPE_RIGHT_BRACE)) + { + Fmt.RemovePrefix(ESCAPE_RIGHT_BRACE.Num()); + + if constexpr (!bIsFormat) + { + if (!String.StartsWith(RIGHT_BRACE)) return false; + + String.RemovePrefix(1); + } + else String += RIGHT_BRACE; + + continue; + } + + if (Fmt.StartsWith(LEFT_BRACE)) + { + Fmt.RemovePrefix(1); + + int SubplaceholderNum = -1; + + size_t PlaceholderBegin = -1; + size_t PlaceholderEnd = -1; + + // Find the end of the placeholder. + do + { + while (true) + { + PlaceholderBegin = Fmt.FindFirstOf(LEFT_BRACE, PlaceholderBegin + 1); + + if (PlaceholderBegin == INDEX_NONE) break; + + if (Fmt.First(PlaceholderBegin + 1).EndsWith(ESCAPE_LEFT_BRACE)) + { + ++PlaceholderBegin; + } + else break; + } + + while (true) + { + PlaceholderEnd = Fmt.FindFirstOf(RIGHT_BRACE, PlaceholderEnd + 1); + + if (PlaceholderEnd == INDEX_NONE) break; + + if (Fmt.Substr(PlaceholderEnd).StartsWith(ESCAPE_RIGHT_BRACE)) + { + ++PlaceholderEnd; + } + else break; + } + + if (PlaceholderEnd == INDEX_NONE) + { + checkf(false, TEXT("Unmatched '{' in format string.")); + + if constexpr (bIsFormat) String += Fmt; + + Fmt = LITERAL(T, ""); + + return false; + } + + ++SubplaceholderNum; + } + while (PlaceholderBegin != INDEX_NONE && PlaceholderBegin < PlaceholderEnd); + + TStringView Subfmt = Fmt.First(PlaceholderEnd); + + Fmt.RemovePrefix(PlaceholderEnd + 1); + + bool bIsSuccessful = true; + + // The subformat string size are usually smaller than 16. + TString> FormattedSubfmt; + + // Recursively format the subformat string. + if (SubplaceholderNum > 0) + { + if constexpr (bIsFormat) bIsSuccessful = Self(Self, FormattedSubfmt, Subfmt); + + else bIsSuccessful = TStringHelper::Do(FormattedSubfmt, Subfmt, ArgsTuple); + + Subfmt = FormattedSubfmt; + } + + if (bIsSuccessful) + { + // Find the placeholder index delimiter. + size_t IndexLength = Subfmt.FindFirstOf(LITERAL(T, ':')); + + if (IndexLength == INDEX_NONE) IndexLength = Subfmt.Num(); + + TStringView PlaceholderIndex = Subfmt.First(IndexLength); + TStringView PlaceholderSubfmt = IndexLength != Subfmt.Num() ? Subfmt.Substr(IndexLength + 1) : LITERAL(T, ""); + + size_t Index; + + if (IndexLength != 0) + { + if (PlaceholderIndex.FindFirstNotOf(LITERAL(T, "0123456789")) != INDEX_NONE) + { + checkf(false, TEXT("Invalid placeholder index.")); + + if constexpr (bIsFormat) + { + String += LEFT_BRACE; + String += Subfmt; + String += RIGHT_BRACE; + + bIsFullyFormatted = false; + } + else return false; + + continue; + } + + verify(PlaceholderIndex.Parse(LITERAL(T, "{}"), Index) == 1); + } + else Index = ArgsIndex++; + + checkf(Index < ArgsTuple.Num(), TEXT("Argument not found.")); + + bIsSuccessful = ArgsTuple.Visit( + [&String, Subfmt = PlaceholderSubfmt](auto& Object) mutable + { + if (Subfmt.StartsWith(LITERAL(T, ':'))) Subfmt.RemovePrefix(1); + + if constexpr (bIsFormat) return TStringHelper::FormatObject(String, Subfmt, Object); + + else return TStringHelper::ParseObject(String, Subfmt, Object); + }, + Index + ); + } + + if (!bIsSuccessful) + { + if constexpr (bIsFormat) + { + String += LEFT_BRACE; + String += Subfmt; + String += RIGHT_BRACE; + + bIsFullyFormatted = false; + } + else return false; + } + else ++FormattedObjectNum; + + continue; + } + + check_code({ if (Fmt.StartsWith(RIGHT_BRACE)) check_no_entry(); }); + + if constexpr (!bIsFormat) + { + if (TChar::IsSpace(Fmt.Front())) + { + Fmt.RemovePrefix(1); + + while (TChar::IsSpace(String.Front())) + { + String.RemovePrefix(1); + } + + continue; + } + + if (!String.StartsWith(Fmt.Front())) return false; + + String.RemovePrefix(1); + } + else String += Fmt.Front(); + + Fmt.RemovePrefix(1); + } + + return bIsFullyFormatted; + }; + + bool bIsSuccessful = ParseFormat(ParseFormat, Result, Fmt); + + if constexpr (bIsFormat) return bIsSuccessful; + + return FormattedObjectNum; + } +}; + +NAMESPACE_PRIVATE_END + +template Allocator> +template +TString TString::Format(TStringView Fmt, const Ts&... Args) +{ + // The Unreal Engine says that the starting buffer size catches 99.97% of printf calls. + constexpr size_t ReserveBufferSize = 512; + + TString Result; + + Result.Reserve(ReserveBufferSize); + + NAMESPACE_PRIVATE::TStringHelper::Do(Result, Fmt, ForwardAsTuple(Args...)); + + return Result; +} + +template +template +size_t TStringView::Parse(TStringView Fmt, Ts&... Args) const +{ + TStringView View = *this; + + return NAMESPACE_PRIVATE::TStringHelper::Do(View, Fmt, ForwardAsTuple(Args...)); +} + +#undef LEFT_BRACE +#undef RIGHT_BRACE + +#undef ESCAPE_LEFT_BRACE +#undef ESCAPE_RIGHT_BRACE + +NAMESPACE_MODULE_END(Utility) +NAMESPACE_MODULE_END(Redcraft) +NAMESPACE_REDCRAFT_END + +#pragma warning(pop) diff --git a/Redcraft.Utility/Source/Public/String/String.h b/Redcraft.Utility/Source/Public/String/String.h index 7747dd4..0a1f297 100644 --- a/Redcraft.Utility/Source/Public/String/String.h +++ b/Redcraft.Utility/Source/Public/String/String.h @@ -149,6 +149,8 @@ public: NODISCARD friend FORCEINLINE auto operator<=>( ElementType LHS, const TString& RHS) { return LHS <=> TStringView(RHS); } NODISCARD friend FORCEINLINE auto operator<=>(const ElementType* LHS, const TString& RHS) { return LHS <=> TStringView(RHS); } +public: + /** Inserts 'InValue' before 'Index' in the string. */ FORCEINLINE Iterator Insert(size_t Index, ElementType InValue) { @@ -336,6 +338,8 @@ public: NODISCARD friend FORCEINLINE TString operator+( TStringView LHS, TString&& RHS) { RHS.Insert(0, LHS); return RHS; } NODISCARD friend FORCEINLINE TString operator+(const TString& LHS, TString&& RHS) { RHS.Insert(0, LHS); return RHS; } +public: + /** @return true if the string view starts with the given prefix, false otherwise. */ NODISCARD FORCEINLINE bool StartsWith(TStringView Prefix) const { @@ -626,6 +630,8 @@ public: return TStringView(*this).FindLastNotOf(Char, Index); } +public: + /** Try to decode the given character using the U-encoded to a string using the T-encoded. */ template bool DecodeFrom(U Char, bool bAllowShrinking = true) @@ -1013,6 +1019,35 @@ public: return AsConst(*this).GetData(); } +public: + + /** + * Format some objects using a format string and append to the string. + * + * @param Fmt - The format string. + * @param Args - The objects to format. + * + * @return The formatted string containing the objects. + */ + template + NODISCARD static TString Format(TStringView Fmt, const Ts&... Args); + + /** + * Parse a string using a format string to objects. + * + * @param Fmt - The format string. + * @param Args - The objects to parse. + * + * @return The number of objects successfully parsed. + */ + template + size_t Parse(TStringView Fmt, Ts&... Args) const + { + return TStringView(*this).Parse(Fmt, Args...); + } + +public: + /** Overloads the GetTypeHash algorithm for TString. */ NODISCARD friend FORCEINLINE size_t GetTypeHash(const TString& A) { return GetTypeHash(TStringView(A)); } @@ -1046,3 +1081,5 @@ template template constexpr TStringView::T NAMESPACE_MODULE_END(Utility) NAMESPACE_MODULE_END(Redcraft) NAMESPACE_REDCRAFT_END + +#include "String/Conversion.h.inl" diff --git a/Redcraft.Utility/Source/Public/String/StringView.h b/Redcraft.Utility/Source/Public/String/StringView.h index 5be2ebb..820269f 100644 --- a/Redcraft.Utility/Source/Public/String/StringView.h +++ b/Redcraft.Utility/Source/Public/String/StringView.h @@ -377,6 +377,21 @@ public: return RFind([Char](ElementType C) { return C != Char; }, Index); } +public: + + /** + * Parse a string using a format string to objects. + * + * @param Fmt - The format string. + * @param Args - The objects to parse. + * + * @return The number of objects successfully parsed. + */ + template + size_t Parse(TStringView Fmt, Ts&... Args) const; + +public: + /** Overloads the GetTypeHash algorithm for TStringView. */ NODISCARD friend FORCEINLINE constexpr size_t GetTypeHash(TStringView A) { return GetTypeHash(static_cast(A)); }