diff --git a/Redcraft.Utility/Source/Private/Testing/Strings.cpp b/Redcraft.Utility/Source/Private/Testing/Strings.cpp index 8c91c3b..2a2201c 100644 --- a/Redcraft.Utility/Source/Private/Testing/Strings.cpp +++ b/Redcraft.Utility/Source/Private/Testing/Strings.cpp @@ -565,29 +565,37 @@ void TestConvert() Test(InPlaceType); } -void TestStringConversion() +void TestFormatting() { auto Test = [](TInPlaceType) { - always_check(TString::Format(LITERAL(T, "#{}#"), true ) == LITERAL(T, "#True#" )); - always_check(TString::Format(LITERAL(T, "#{}#"), false) == LITERAL(T, "#False#")); + always_check(TString::Format(LITERAL(T, "#{0}#"), LITERAL(T, "Hello, World!")) == LITERAL(T, "#Hello, World!#")); - always_check(TString::Format(LITERAL(T, "#{}#"), +0) == LITERAL(T, "#0#")); - always_check(TString::Format(LITERAL(T, "#{}#"), 0) == LITERAL(T, "#0#")); - always_check(TString::Format(LITERAL(T, "#{}#"), -0) == LITERAL(T, "#0#")); + always_check(TString::Format(LITERAL(T, "#{0}#"), true ) == LITERAL(T, "#True#" )); + always_check(TString::Format(LITERAL(T, "#{0}#"), false) == LITERAL(T, "#False#")); - always_check(TString::Format(LITERAL(T, "#{}#"), 42) == LITERAL(T, "#42#")); + always_check(TString::Format(LITERAL(T, "#{0}#"), +0) == LITERAL(T, "#0#")); + always_check(TString::Format(LITERAL(T, "#{0}#"), 0) == LITERAL(T, "#0#")); + always_check(TString::Format(LITERAL(T, "#{0}#"), -0) == LITERAL(T, "#0#")); - always_check(TString::Format(LITERAL(T, "#{}#"), +0.0) == LITERAL(T, "#0.000000#")); - always_check(TString::Format(LITERAL(T, "#{}#"), 0.0) == LITERAL(T, "#0.000000#")); - always_check(TString::Format(LITERAL(T, "#{}#"), -0.0) == LITERAL(T, "#-0.000000#")); + always_check(TString::Format(LITERAL(T, "#{0}#"), 42) == LITERAL(T, "#42#")); - always_check(TString::Format(LITERAL(T, "#{}#"), 3.14) == LITERAL(T, "#3.140000#")); + always_check(TString::Format(LITERAL(T, "#{0}#"), +0.0) == LITERAL(T, "#0#")); + always_check(TString::Format(LITERAL(T, "#{0}#"), 0.0) == LITERAL(T, "#0#")); + always_check(TString::Format(LITERAL(T, "#{0}#"), -0.0) == LITERAL(T, "#-0#")); - always_check(TString::Format(LITERAL(T, "#{}#"), +TNumericLimits::Infinity()) == LITERAL(T, "#Infinity#")); - always_check(TString::Format(LITERAL(T, "#{}#"), -TNumericLimits::Infinity()) == LITERAL(T, "#-Infinity#")); - always_check(TString::Format(LITERAL(T, "#{}#"), +TNumericLimits::QuietNaN()) == LITERAL(T, "#NaN#")); - always_check(TString::Format(LITERAL(T, "#{}#"), -TNumericLimits::QuietNaN()) == LITERAL(T, "#-NaN#")); + always_check(TString::Format(LITERAL(T, "#{0}#"), 3.14) == LITERAL(T, "#3.14#")); + + always_check(TString::Format(LITERAL(T, "#{0:.6F}#"), +0.0) == LITERAL(T, "#0.000000#")); + always_check(TString::Format(LITERAL(T, "#{0:.6F}#"), 0.0) == LITERAL(T, "#0.000000#")); + always_check(TString::Format(LITERAL(T, "#{0:.6F}#"), -0.0) == LITERAL(T, "#-0.000000#")); + + always_check(TString::Format(LITERAL(T, "#{0:.6F}#"), 3.14) == LITERAL(T, "#3.140000#")); + + always_check(TString::Format(LITERAL(T, "#{0}#"), +TNumericLimits::Infinity()) == LITERAL(T, "#Infinity#")); + always_check(TString::Format(LITERAL(T, "#{0}#"), -TNumericLimits::Infinity()) == LITERAL(T, "#-Infinity#")); + always_check(TString::Format(LITERAL(T, "#{0}#"), +TNumericLimits::QuietNaN()) == LITERAL(T, "#NaN#")); + always_check(TString::Format(LITERAL(T, "#{0}#"), -TNumericLimits::QuietNaN()) == LITERAL(T, "#-NaN#")); { always_check(TString::FromBool(true ) == LITERAL(T, "True" )); @@ -632,7 +640,7 @@ void TestString() NAMESPACE_PRIVATE::TestStringView(); NAMESPACE_PRIVATE::TestString(); NAMESPACE_PRIVATE::TestConvert(); - NAMESPACE_PRIVATE::TestStringConversion(); + NAMESPACE_PRIVATE::TestFormatting(); } NAMESPACE_END(Testing) diff --git a/Redcraft.Utility/Source/Public/Strings/Conversion.h.inl b/Redcraft.Utility/Source/Public/Strings/Conversion.h.inl deleted file mode 100644 index e07eaba..0000000 --- a/Redcraft.Utility/Source/Public/Strings/Conversion.h.inl +++ /dev/null @@ -1,1650 +0,0 @@ -#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 -#include - -#pragma warning(push) -#pragma warning(disable : 4146 4244) - -NAMESPACE_REDCRAFT_BEGIN -NAMESPACE_MODULE_BEGIN(Redcraft) -NAMESPACE_MODULE_BEGIN(Utility) - -// TODO: Refactor the conversion tool by more elegant way. - -// The conversion tool uses a string to describe the object format. -// -// The format string consists of the following parts: -// -// - A pair of braces: The object placeholder. -// - A escaped brace: The brace is formatted or parsed as-is -// - A general character: The character is formatted or parsed as-is. -// - A space character: The character is formatted as-is or all leading space characters are consumed when parsing. -// -// About the object placeholder: -// -// Use the ':' character to separate the different layers of object placeholders, for a normal object he has only two layers, -// for a string or a character he may have three layers to represent the format of the escape character, -// for a container he may have many layers to represent the format of the elements. -// -// The first level is the object index. -// The other levels are the object format, which is used to format or parse the object. -// -// The object format contains a common optional fill-and-align consisting of the following parts: -// -// i. A fill character: The character is used to fill width of the object. It is optional. -// It should be representable as a single unicode otherwise it is undefined behavior. -// ii. A alignment option: The character is used to indicate the direction of alignment. It is optional if it does not create ambiguity. -// '<' for left, '>' for right, '^' for center. If cannot absolute centering, offset to the left. -// iii. A width number: The number is used to specify the width of the object. -// It should be a decimal number without any sign. -// -// The width is limits the minimum number of characters in formatting and the maximum number of characters in parsing. -// The fill character is treated as a space character in parsing. -// -// After the fill-and-align, the object format contains type-specific options. -// -// Specially, only strings and characters that agree with the main character type are considered string values and character values. -// -// For string values: -// -// 1. The type indicators part: -// -// - none: Indicates the as-is formatting. -// - 'S': Indicates uppercase formatting if case indicators is '!', otherwise as-is formatting. -// - 's': Indicates lowercase formatting if case indicators is '!', otherwise as-is formatting. -// -// 2. The case indicators part: -// -// - none: Indicates the as-is formatting. -// - '!': Indicates the case as the type indicators case. -// -// 3. The escape indicators part: -// -// - none: Indicates the as-is formatting. -// - '?': Indicates the escape formatting. -// -// For character values: -// -// 1. The type indicators part: -// -// - none: Indicates the as-is formatting. -// - 'C': Indicates uppercase formatting if case indicators is '!', otherwise as-is formatting. -// - 'c': Indicates lowercase formatting if case indicators is '!', otherwise as-is formatting. -// - 's' or 'S': Indicates that characters should be treated as strings. -// See the string values section for additional formatting. -// - 'B', 'D', 'O', 'X': Indicates that characters should be treated as integer values. -// See the integer values section for additional formatting. -// -// 2. The case indicators part: -// -// - none: Indicates the as-is formatting. -// - '!': Indicates the case as the type indicators case. -// -// 3. The escape indicators part: -// -// - none: Indicates the as-is formatting. -// - '?': Indicates the escape formatting. -// -// For boolean values: -// -// 1. The type indicators part: -// -// - none or 'S': Indicates that boolean value should be treated as string 'True' or 'False'. -// See the string values section for additional formatting. -// - 'C': Indicates that boolean value should be treated as character 'T' or 'F'. -// See the character values section for additional formatting. -// - 'B', 'D', 'O', 'X': Indicates that boolean value should be treated as integer 1 or 0. -// See the integer values section for additional formatting. -// -// For integer values: -// -// 1. The positive indicators part: -// -// - none or '-': Indicates hide the sign of the positive number. -// - '+': Indicates show the '+' of the positive number. -// - ' ': Indicates show the ' ' of the positive number. -// -// 2. The prefix indicators part: -// -// - none: Indicates hide the prefix of the number. -// - '#': Indicates show the prefix of the number. Indicates auto-detect the base in parsing. -// -// 3. The '0' padded width indicators part: -// -// - none: Indicates that the '0' padded width is 0. -// - '0N': Indicates that the '0' padded width is N. -// -// 4. The base indicators part: -// -// - none or '_0': Indicates decimal in formatting. Indicates auto-detect the base in parsing. -// - '_N': Indicates that the base is N, between [2, 36]. -// -// 5. The type indicators part: -// -// - none or 'D': Indicates decimal formatting. Same as '_10I'. -// - 'B': Indicates binary formatting. Same as '_2I'. -// - 'O': Indicates octal formatting. Same as '_8I'. -// - 'X': Indicates hexadecimal formatting. Same as '_16I'. -// - 'I': Indicates specified formatting by base indicators. -// -// For floating-point values: -// -// 1. The positive indicators part: -// -// - none or '-': Indicates hide the sign of the positive number. -// - '+': Indicates show the '+' of the positive number. -// - ' ': Indicates show the ' ' of the positive number. -// -// 2. The prefix indicators part: -// -// - none: Indicates hide the prefix of the number. -// - '#': Indicates show the prefix of the number. Indicates auto-detect the hex scientific in parsing. -// -// 3. The precision indicators part: -// -// - none: Indicates six decimal for fixed-point in formatting. Indicates auto-detect the precision in parsing. -// - '.N': Indicates that the precision is N, It should be a decimal number without any sign. -// -// 4. The type indicators part: -// -// - none or 'F': Indicates fixed-point formatting. -// - 'G': Indicates general formatting. -// - 'E': Indicates scientific formatting. -// - 'A': Indicates hex scientific formatting. -// -// For pointer values: -// -// 1. The type indicators part: -// -// - none or 'P': The pointer value is formatted as hexadecimal with prefix and fill-and-align. -// Same as '#X'. The default width depends on the platform. -// -// For tuple values: -// -// 1. The type indicators part: -// -// - none: Indicates general formatting. Same as 'T(_, _)'. -// - 'M': Indicates map formatting. Same as 'T_: _'. -// - 'N': Indicates none formatting. Same as 'T__'. -// - 'T': Indicates user-defined formatting. -// -// 2. The user-defined part: -// -// i. A begin string: Indicates the begin string of the tuple. Cannot contain '_' or ':' character. -// ii. '_': Indicates a placeholder. -// iii. A separator string: Indicates the separator string of the tuple. Cannot contain '_' character. -// iv. '_': Indicates a placeholder. -// v. An end string: Indicates the end string of the tuple. Cannot contain '_' or ':' character. -// -// For container values: -// -// 1. The type indicators part: -// -// - none: Indicates general formatting. Same as 'T[_, _]'. -// - 'N': Indicates none formatting. Same as 'T__'. -// - 'T': Indicates user-defined formatting. -// -// 2. The user-defined part: -// -// i. A begin string: Indicates the begin string of the container. Cannot contain '_' or ':' character. -// ii. '_': Indicates a placeholder. -// iii. A separator string: Indicates the separator string of the container. Cannot contain '_' character. -// iv. '_': Indicates a placeholder. -// v. An end string: Indicates the end string of the container. Cannot contain '_' or ':' character. -// -// For the type indicator part of the boolean, integer, and floating-point values, -// The case of letter indicates the case of the first letter or number part, -// and other parts can also be uppercase by appended the '!' mark. -// -// Specially, the case of letters is ignored by default in parsing, -// and can be forced to match the required case by appending the '=' mark. -// -// Tuples of pointers and containers cannot be parsed. -// -// Examples: -// -// - '{:}': Parse the integer value in decimal without positive sign. -// - '{:+D}': Parse the integer value in decimal with optional positive sign. -// - '{:+#I}': Parse the integer value in any formatting. -// - '{:}': Parse the floating-point value in fixed-point without positive sign. -// - '{:+F}': Parse the floating-point value in fixed-point with optional positive sign. -// - '{:+#G}': Parse the floating-point value in any formatting. - -// NOTE: These functions are recommended for debug programs. - -NAMESPACE_PRIVATE_BEGIN - -// In private, conversion tools use structured parameters to describe the object format. -// The structured parameter is an object with specific public members: -// -// - DigitStyle: A signed integer that represents the letter case of the first part or the digit part. -// Less than 0 for lowercase, greater than 0 for uppercase, 0 for default or any in parsing. -// It is valid for boolean, integer and floating-point values. -// -// - OtherStyle: A signed integer that represents the letter case of the other part. -// Less than 0 for lowercase, greater than 0 for uppercase, 0 for default or any in parsing. -// It is valid for boolean, integer and floating-point values. -// -// - bSign: A boolean that represents whether to show the sign of the number if it is positive. -// It is valid for integer and floating-point values. -// -// - bPrefix: A boolean that represents whether to show the prefix of the number. -// Legal only when base is binary octal decimal and hexadecimal -// For parsing, together with the following parameters, it also determines whether to automatically detect the base. -// It is valid for integer and floating-point values. -// -// - Padding: A unsigned integer that represents the '0' padded width of the number. -// It is valid for integer values. -// -// - Base: A unsigned integer that represents the base of the number, between [2, 36]. -// However, when parsed and prefixed, 0 is allowed to indicate auto-detection. -// It is valid for integer values. -// -// - bFixed: A boolean that represents whether to use the decimal fixed-point format. -// - bScientific: A boolean that represents whether to use the decimal scientific format. -// These two parameters together determine the format of the floating-point value. -// When both are false, represents the hex scientific is format. -// However, when parsed and prefixed, any values allows auto-detection hex scientific format. -// It is valid for floating-point values. -// -// - Precision: A signed integer that represents the number of digits after the decimal point. Negative value means ignore. -// For parsing, it is used to determine the maximum number of digits after the decimal point. -// It is valid for floating-point values. - -template -struct TStringObjectFormatter -{ - static bool Do(auto& Result, auto& Object, auto Param) - { - // ReSharper disable once CppInconsistentNaming - using U = TRemoveCVRef; - - if constexpr (!CConst>) - { - checkf(false, TEXT("Unsafe formatting for a variable that is non-const.")); - - return false; - } - - // Parse format string if parameter is TStringView, otherwise use the structured parameters directly. - if constexpr (requires { { Param.Fmt } -> CConvertibleTo>; }) - { - TStringView Fmt = Param.Fmt; - - // Parse the fill-and-align part and reserve the space for the result. - auto ParseFillAndAlign = [&Result, &Fmt] - { - TStringView FillCharacter = LITERAL(T, " "); - - T AlignmentOption = CIntegral || CFloatingPoint ? LITERAL(T, '>') : LITERAL(T, '<'); - - size_t AlignmentWidth = 0; - - // Parse the fill-and-align part of the object format. - if (!Fmt.IsEmpty()) - { - size_t Index = Fmt.FindFirstOf(LITERAL(T, "123456789")); - - if (Index != INDEX_NONE) - { - // Create a temporary view to avoid modifying the original view. - TStringView TrimmedFmt = Fmt; - - TStringView FillAndAlign = TrimmedFmt.First(Index); - - TrimmedFmt.RemovePrefix(Index); - - TStringView View = TrimmedFmt.Substr(0, TrimmedFmt.FindFirstNotOf(LITERAL(T, "0123456789"))); - - TrimmedFmt.RemovePrefix(View.Num()); - - size_t PossibleWidth = View.template ToInt(); - - bool bIsValid = true; - - if (!FillAndAlign.IsEmpty()) - { - if (FillAndAlign.Back() == LITERAL(T, '<')) { FillAndAlign.RemoveSuffix(1); AlignmentOption = LITERAL(T, '<'); } - else if (FillAndAlign.Back() == LITERAL(T, '>')) { FillAndAlign.RemoveSuffix(1); AlignmentOption = LITERAL(T, '>'); } - else if (FillAndAlign.Back() == LITERAL(T, '^')) { FillAndAlign.RemoveSuffix(1); AlignmentOption = LITERAL(T, '^'); } - else - { - if (FillAndAlign.Num() != 1) - { - // If the string contains ASCII then it must not be represented as a single unicode. - for (T Char : FillAndAlign) if (TChar::IsASCII(Char)) bIsValid = false; - } - else if (FillAndAlign.Front() == LITERAL(T, '.')) bIsValid = false; // Ambiguously with the precision indicator. - else if (FillAndAlign.Front() == LITERAL(T, '_')) bIsValid = false; // Ambiguously with the base indicator. - } - } - - if (bIsValid) - { - if (!FillAndAlign.IsEmpty()) FillCharacter = FillAndAlign; - - AlignmentWidth = PossibleWidth; - - Fmt = TrimmedFmt; - } - } - } - - Result.Reserve(Result.Num() + AlignmentWidth * FillCharacter.Num()); - - return MakeTuple(FillCharacter, AlignmentOption, AlignmentWidth, Result.Num()); - }; - - // Apply the fill-and-align part to the result. - auto ApplyFillAndAlign = [&Result](auto FillAndAlign) - { - auto [FillCharacter, AlignmentOption, AlignmentWidth, OriginalNum] = FillAndAlign; - - const size_t AppendedNum = Result.Num() - OriginalNum; - - if (AlignmentWidth > AppendedNum) - { - size_t LeftWidth = 0; - size_t RightWidth = 0; - - switch (AlignmentOption) - { - case LITERAL(T, '<'): RightWidth = AlignmentWidth - AppendedNum; break; - case LITERAL(T, '>'): LeftWidth = AlignmentWidth - AppendedNum; break; - case LITERAL(T, '^'): - { - LeftWidth = (AlignmentWidth - AppendedNum) / 2; - RightWidth = AlignmentWidth - AppendedNum - LeftWidth; - break; - } - default: check_no_entry(); - } - - if (LeftWidth != 0) - { - Result.SetNum(Result.Num() + LeftWidth * FillCharacter.Num(), false); - - for (size_t Index = 0; Index != AppendedNum; ++Index) - { - Result[Result.Num() - Index - 1] = Result[OriginalNum + AppendedNum - Index - 1]; - } - - for (size_t Index = 0; Index != LeftWidth * FillCharacter.Num(); ++Index) - { - Result[OriginalNum + Index] = FillCharacter[Index % FillCharacter.Num()]; - } - } - - if (RightWidth != 0) - { - for (size_t Index = 0; Index < RightWidth; ++Index) - { - Result += FillCharacter; - } - } - } - }; - - // Format the string value by format string. - if constexpr (requires { TStringView(Object); }) - { - auto FillAndAlign = ParseFillAndAlign(); - - bool bNeedToCase = false; - bool bStringLowercase = false; - bool bNeedToEscape = false; - bool bEscapeLowercase = false; - - if (Fmt.StartsWith(LITERAL(T, 'S'))) { bStringLowercase = false; Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, 's'))) { bStringLowercase = true; Fmt.RemovePrefix(1); } - - if (Fmt.StartsWith(LITERAL(T, '!'))) { bNeedToCase = true; Fmt.RemovePrefix(1); } - if (Fmt.StartsWith(LITERAL(T, '?'))) { bNeedToEscape = true; Fmt.RemovePrefix(1); } - - if (Fmt.StartsWith(LITERAL(T, '='))) Fmt.RemovePrefix(1); - - if (bNeedToEscape && Fmt.StartsWith(LITERAL(T, ':'))) - { - Fmt.RemovePrefix(1); - - if (Fmt.StartsWith(LITERAL(T, 'X'))) { bEscapeLowercase = false; Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, 'x'))) { bEscapeLowercase = true; Fmt.RemovePrefix(1); } - - if (Fmt.StartsWith(LITERAL(T, '='))) Fmt.RemovePrefix(1); - } - - if (!Fmt.IsEmpty()) - { - checkf(false, TEXT("Illegal format string. Redundant unknown characters.")); - return false; - } - - TStringView String = Object; - - if (bNeedToEscape) Result += LITERAL(T, '\"'); - - if (bNeedToCase || bNeedToEscape) - { - for (T Char : String) - { - if (bNeedToCase) - { - if (bStringLowercase) Char = TChar::ToLower(Char); - else Char = TChar::ToUpper(Char); - } - - if (bNeedToEscape) - { - switch (Char) - { - case LITERAL(T, '\"'): Result += LITERAL(T, "\\\""); break; - case LITERAL(T, '\\'): Result += LITERAL(T, "\\\\"); break; - case LITERAL(T, '\a'): Result += LITERAL(T, "\\a"); break; - case LITERAL(T, '\b'): Result += LITERAL(T, "\\b"); break; - case LITERAL(T, '\f'): Result += LITERAL(T, "\\f"); break; - case LITERAL(T, '\n'): Result += LITERAL(T, "\\n"); break; - case LITERAL(T, '\r'): Result += LITERAL(T, "\\r"); break; - case LITERAL(T, '\t'): Result += LITERAL(T, "\\t"); break; - case LITERAL(T, '\v'): Result += LITERAL(T, "\\v"); break; - default: - { - if (!TChar::IsASCII(Char) || !TChar::IsPrint(Char)) - { - Result += LITERAL(T, "\\x"); - - const TMakeUnsigned IntValue = static_cast>(Char); - - struct { int DigitStyle; unsigned Padding; unsigned Base; } DigitParam = { bEscapeLowercase ? -1 : 1, sizeof(T) * 2, 16}; - - verify(TStringObjectFormatter::Do(Result, IntValue, DigitParam)); - } - else Result += Char; - } - } - } - else Result += Char; - } - } - else Result += String; - - if (bNeedToEscape) Result += LITERAL(T, '\"'); - - ApplyFillAndAlign(FillAndAlign); - - return true; - } - - // Format the character value by format string. - else if constexpr (CCharType) - { - if (Fmt.FindFirstOf(LITERAL(T, "Ss")) != INDEX_NONE) - { - const TStringView StringValue(&Object, 1); - - return TStringObjectFormatter::Do(Result, StringValue, Param); - } - - if (Fmt.FindFirstOf(LITERAL(T, "BbDdOoXxIi")) != INDEX_NONE) - { - const TMakeUnsigned IntValue = static_cast>(Object); - - return TStringObjectFormatter::Do(Result, IntValue, Param); - } - - auto FillAndAlign = ParseFillAndAlign(); - - bool bNeedToCase = false; - bool bStringLowercase = false; - bool bNeedToEscape = false; - bool bEscapeLowercase = false; - - if (Fmt.StartsWith(LITERAL(T, 'C'))) { bStringLowercase = false; Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, 'c'))) { bStringLowercase = true; Fmt.RemovePrefix(1); } - - if (Fmt.StartsWith(LITERAL(T, '!'))) { bNeedToCase = true; Fmt.RemovePrefix(1); } - if (Fmt.StartsWith(LITERAL(T, '?'))) { bNeedToEscape = true; Fmt.RemovePrefix(1); } - - if (Fmt.StartsWith(LITERAL(T, '='))) Fmt.RemovePrefix(1); - - if (bNeedToEscape && Fmt.StartsWith(LITERAL(T, ':'))) - { - Fmt.RemovePrefix(1); - - if (Fmt.StartsWith(LITERAL(T, 'X'))) { bEscapeLowercase = false; Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, 'x'))) { bEscapeLowercase = true; Fmt.RemovePrefix(1); } - - if (Fmt.StartsWith(LITERAL(T, '='))) Fmt.RemovePrefix(1); - } - - if (!Fmt.IsEmpty()) - { - checkf(false, TEXT("Illegal format string. Redundant unknown characters.")); - return false; - } - - T Char = Object; - - if (bNeedToEscape) Result += LITERAL(T, '\''); - - if (bNeedToCase || bNeedToEscape) - { - if (bNeedToCase) - { - if (bStringLowercase) Char = TChar::ToLower(Char); - else Char = TChar::ToUpper(Char); - } - - if (bNeedToEscape) - { - switch (Char) - { - case LITERAL(T, '\''): Result += LITERAL(T, "\\\'"); break; - case LITERAL(T, '\\'): Result += LITERAL(T, "\\\\"); break; - case LITERAL(T, '\a'): Result += LITERAL(T, "\\a"); break; - case LITERAL(T, '\b'): Result += LITERAL(T, "\\b"); break; - case LITERAL(T, '\f'): Result += LITERAL(T, "\\f"); break; - case LITERAL(T, '\n'): Result += LITERAL(T, "\\n"); break; - case LITERAL(T, '\r'): Result += LITERAL(T, "\\r"); break; - case LITERAL(T, '\t'): Result += LITERAL(T, "\\t"); break; - case LITERAL(T, '\v'): Result += LITERAL(T, "\\v"); break; - default: - { - if (!TChar::IsASCII(Char) || !TChar::IsPrint(Char)) - { - Result += LITERAL(T, "\\x"); - - const TMakeUnsigned IntValue = static_cast>(Char); - - struct { int DigitStyle; unsigned Padding; unsigned Base; } DigitParam = { bEscapeLowercase ? -1 : 1, sizeof(T) * 2, 16 }; - - verify(TStringObjectFormatter::Do(Result, IntValue, DigitParam)); - } - else Result += Char; - } - } - } - else Result += Char; - } - else Result += Char; - - if (bNeedToEscape) Result += LITERAL(T, '\''); - - ApplyFillAndAlign(FillAndAlign); - - return true; - } - - // Format the boolean value by format string. - else if constexpr (CSameAs) - { - if (Fmt.IsEmpty()) return TStringObjectFormatter::Do(Result, Object, Invalid); - - if (Fmt.FindFirstOf(LITERAL(T, 'S')) != INDEX_NONE) - { - const TStringView StringValue = Object ? LITERAL(T, "True") : LITERAL(T, "False"); - - return TStringObjectFormatter::Do(Result, StringValue, Param); - } - - if (Fmt.FindFirstOf(LITERAL(T, 's')) != INDEX_NONE) - { - const TStringView StringValue = Object ? LITERAL(T, "true") : LITERAL(T, "false"); - - return TStringObjectFormatter::Do(Result, StringValue, Param); - } - - if (Fmt.FindFirstOf(LITERAL(T, 'C')) != INDEX_NONE) - { - const T CharacterValue = Object ? LITERAL(T, 'T') : LITERAL(T, 'F'); - - return TStringObjectFormatter::Do(Result, CharacterValue, Param); - } - - if (Fmt.FindFirstOf(LITERAL(T, 'c')) != INDEX_NONE) - { - const T CharacterValue = Object ? LITERAL(T, 't') : LITERAL(T, 'f'); - - return TStringObjectFormatter::Do(Result, CharacterValue, Param); - } - - if (Fmt.FindFirstOf(LITERAL(T, "BbDdOoXxIi")) != INDEX_NONE) - { - const int IntValue = Object ? 1 : 0; - - return TStringObjectFormatter::Do(Result, IntValue, Param); - } - - checkf(false, TEXT("Illegal format string. Redundant unknown characters.")); - return false; - } - - // Format the integer value by format string. - else if constexpr (CIntegral && !CSameAs) - { - if (Fmt.IsEmpty()) return TStringObjectFormatter::Do(Result, Object, Invalid); - - auto FillAndAlign = ParseFillAndAlign(); - - T PositiveIndicator = LITERAL(T, '-'); - - bool bPrefix = false; - - unsigned Padding = 0; - - bool bHasBase = false; - - unsigned Base = 10; - - bool bDigitLowercase = false; - bool bOtherLowercase = true; - - if (Fmt.StartsWith(LITERAL(T, '-'))) { PositiveIndicator = LITERAL(T, '-'); Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, '+'))) { PositiveIndicator = LITERAL(T, '+'); Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, ' '))) { PositiveIndicator = LITERAL(T, ' '); Fmt.RemovePrefix(1); } - - if (Fmt.StartsWith(LITERAL(T, '#'))) { bPrefix = true; Fmt.RemovePrefix(1); } - - if (Fmt.StartsWith(LITERAL(T, '0')) && Fmt.Num() > 1 && TChar::IsDigit(Fmt[1]) && Fmt[1] != LITERAL(T, '0')) - { - Fmt.RemovePrefix(1); - - TStringView View = Fmt.Substr(0, Fmt.FindFirstNotOf(LITERAL(T, "0123456789"))); - - Fmt.RemovePrefix(View.Num()); - - Padding = View.template ToInt(); - } - - if (Fmt.StartsWith(LITERAL(T, '_')) && Fmt.Num() > 1 && TChar::IsDigit(Fmt[1])) - { - Fmt.RemovePrefix(1); - - bHasBase = true; - - TStringView View = Fmt.Substr(0, Fmt.FindFirstNotOf(LITERAL(T, "0123456789"))); - - Fmt.RemovePrefix(View.Num()); - - Base = View.template ToInt(); - } - - if ( Fmt.StartsWith(LITERAL(T, 'I'))) { bDigitLowercase = false; Fmt.RemovePrefix(1); } - else if ( Fmt.StartsWith(LITERAL(T, 'i'))) { bDigitLowercase = true; Fmt.RemovePrefix(1); } - else if (!bHasBase && Fmt.StartsWith(LITERAL(T, 'D'))) { Base = 10; bDigitLowercase = false; Fmt.RemovePrefix(1); } - else if (!bHasBase && Fmt.StartsWith(LITERAL(T, 'd'))) { Base = 10; bDigitLowercase = true; Fmt.RemovePrefix(1); } - else if (!bHasBase && Fmt.StartsWith(LITERAL(T, 'B'))) { Base = 2; bDigitLowercase = false; Fmt.RemovePrefix(1); } - else if (!bHasBase && Fmt.StartsWith(LITERAL(T, 'b'))) { Base = 2; bDigitLowercase = true; Fmt.RemovePrefix(1); } - else if (!bHasBase && Fmt.StartsWith(LITERAL(T, 'O'))) { Base = 8; bDigitLowercase = false; Fmt.RemovePrefix(1); } - else if (!bHasBase && Fmt.StartsWith(LITERAL(T, 'o'))) { Base = 8; bDigitLowercase = true; Fmt.RemovePrefix(1); } - else if (!bHasBase && Fmt.StartsWith(LITERAL(T, 'X'))) { Base = 16; bDigitLowercase = false; Fmt.RemovePrefix(1); } - else if (!bHasBase && Fmt.StartsWith(LITERAL(T, 'x'))) { Base = 16; bDigitLowercase = true; Fmt.RemovePrefix(1); } - - if (Fmt.StartsWith(LITERAL(T, '!'))) { bOtherLowercase = false; Fmt.RemovePrefix(1); } - - if (Fmt.StartsWith(LITERAL(T, '='))) Fmt.RemovePrefix(1); - - if (!Fmt.IsEmpty()) - { - checkf(false, TEXT("Illegal format string. Redundant unknown characters.")); - return false; - } - - struct { int DigitStyle; int OtherStyle; T PositiveSign; bool bPrefix; unsigned Padding; unsigned Base; } IntParam = - { - bDigitLowercase ? -1 : 1, - bOtherLowercase ? -1 : 1, - PositiveIndicator, - bPrefix, - Padding, - Base == 0 ? 10 : Base, - }; - - verify(TStringObjectFormatter::Do(Result, Object, IntParam)); - - ApplyFillAndAlign(FillAndAlign); - - return true; - } - - // Format the floating-point value by format string. - else if constexpr (CFloatingPoint) - { - if (Fmt.IsEmpty()) - { - struct { bool bFixed; bool bScientific; unsigned Precision; } FloatParam = { true, false, 6 }; - - return TStringObjectFormatter::Do(Result, Object, FloatParam); - } - - auto FillAndAlign = ParseFillAndAlign(); - - T PositiveIndicator = LITERAL(T, '-'); - - bool bPrefix = false; - - int Precision = -1; - - bool bDigitLowercase = false; - bool bOtherLowercase = true; - - bool bFixed = true; - bool bScientific = false; - - if (Fmt.StartsWith(LITERAL(T, '-'))) { PositiveIndicator = LITERAL(T, '-'); Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, '+'))) { PositiveIndicator = LITERAL(T, '+'); Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, ' '))) { PositiveIndicator = LITERAL(T, ' '); Fmt.RemovePrefix(1); } - - if (Fmt.StartsWith(LITERAL(T, '#'))) { bPrefix = true; Fmt.RemovePrefix(1); } - - if (Fmt.StartsWith(LITERAL(T, '.')) && Fmt.Num() > 1 && TChar::IsDigit(Fmt[1])) - { - Fmt.RemovePrefix(1); - - TStringView View = Fmt.Substr(0, Fmt.FindFirstNotOf(LITERAL(T, "0123456789"))); - - Fmt.RemovePrefix(View.Num()); - - Precision = View.template ToInt(); - } - - if (Fmt.StartsWith(LITERAL(T, 'F'))) { bFixed = true; bScientific = false; bDigitLowercase = false; Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, 'f'))) { bFixed = true; bScientific = false; bDigitLowercase = true; Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, 'G'))) { bFixed = true; bScientific = true; bDigitLowercase = false; Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, 'g'))) { bFixed = true; bScientific = true; bDigitLowercase = true; Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, 'E'))) { bFixed = false; bScientific = true; bDigitLowercase = false; Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, 'e'))) { bFixed = false; bScientific = true; bDigitLowercase = true; Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, 'A'))) { bFixed = false; bScientific = false; bDigitLowercase = false; Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, 'a'))) { bFixed = false; bScientific = false; bDigitLowercase = true; Fmt.RemovePrefix(1); } - - if (Fmt.StartsWith(LITERAL(T, '!'))) { bOtherLowercase = false; Fmt.RemovePrefix(1); } - - if (Fmt.StartsWith(LITERAL(T, '='))) Fmt.RemovePrefix(1); - - if (!Fmt.IsEmpty()) - { - checkf(false, TEXT("Illegal format string. Redundant unknown characters.")); - return false; - } - - if (Precision == -1 && bFixed && !bScientific) Precision = 6; - - struct { bool bFixed; bool bScientific; int Precision; int DigitStyle; int OtherStyle; T PositiveSign; bool bPrefix; } FloatParam = - { - bFixed, - bScientific, - Precision, - bDigitLowercase ? -1 : 1, - bOtherLowercase ? -1 : 1, - PositiveIndicator, - bPrefix, - }; - - verify(TStringObjectFormatter::Do(Result, Object, FloatParam)); - - ApplyFillAndAlign(FillAndAlign); - - return true; - } - - // Format the pointer value by format string. - else if constexpr (CNullPointer || TPointerTraits::bIsPointer) - { - void* Ptr = nullptr; if constexpr (!CNullPointer) Ptr = ToAddress(Object); - - auto FillAndAlign = ParseFillAndAlign(); - - bool bDigitLowercase = false; - bool bOtherLowercase = true; - - if (Fmt.StartsWith(LITERAL(T, 'P'))) { bDigitLowercase = false; Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, 'p'))) { bDigitLowercase = true; Fmt.RemovePrefix(1); } - - if (Fmt.StartsWith(LITERAL(T, '!'))) { bOtherLowercase = false; Fmt.RemovePrefix(1); } - - if (Fmt.StartsWith(LITERAL(T, '='))) Fmt.RemovePrefix(1); - - if (!Fmt.IsEmpty()) - { - checkf(false, TEXT("Illegal format string. Redundant unknown characters.")); - return false; - } - - const uintptr IntValue = reinterpret_cast(Ptr); - - struct { int DigitStyle; int OtherStyle; bool bPrefix; unsigned Padding; unsigned Base; } IntParam = - { - bDigitLowercase ? -1 : 1, - bOtherLowercase ? -1 : 1, - true, - sizeof(uintptr) * 2, - 16, - }; - - verify(TStringObjectFormatter::Do(Result, IntValue, IntParam)); - - ApplyFillAndAlign(FillAndAlign); - - return true; - } - - // Format the tuple value by format string. - else if constexpr (CTTuple) - { - auto FillAndAlign = ParseFillAndAlign(); - - TStringView Begin = LITERAL(T, "("); - TStringView Separator = LITERAL(T, ", "); - TStringView End = LITERAL(T, ")"); - - if (Fmt.StartsWith(LITERAL(T, 'T')) || Fmt.StartsWith(LITERAL(T, 't'))) - { - Fmt.RemovePrefix(1); - - const size_t PlaceholderA = Fmt.FindFirstOf(LITERAL(T, '_')); - const size_t PlaceholderB = Fmt.FindFirstOf(LITERAL(T, '_'), PlaceholderA + 1); - - if (PlaceholderA == INDEX_NONE || PlaceholderB == INDEX_NONE || PlaceholderA == PlaceholderB) - { - checkf(false, TEXT("Illegal format string. Expect placeholders.")); - return false; - } - - size_t UserDefinedEnd = Fmt.FindFirstOf(LITERAL(T, ':'), PlaceholderB + 1); - - if (UserDefinedEnd == INDEX_NONE) UserDefinedEnd = Fmt.Num(); - - Begin = Fmt.First(PlaceholderA); - Separator = Fmt.Substr(PlaceholderA + 1, PlaceholderB - PlaceholderA - 1); - End = Fmt.Substr(PlaceholderB + 1, UserDefinedEnd - PlaceholderB - 1); - - Fmt.RemovePrefix(UserDefinedEnd); - } - else if (Fmt.StartsWith(LITERAL(T, 'M'))) { Begin = LITERAL(T, ""); Separator = LITERAL(T, ": "); End = LITERAL(T, ""); Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, 'm'))) { Begin = LITERAL(T, ""); Separator = LITERAL(T, ": "); End = LITERAL(T, ""); Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, 'N'))) { Begin = LITERAL(T, ""); Separator = LITERAL(T, ""); End = LITERAL(T, ""); Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, 'n'))) { Begin = LITERAL(T, ""); Separator = LITERAL(T, ""); End = LITERAL(T, ""); Fmt.RemovePrefix(1); } - - if (!Fmt.IsEmpty()) - { - checkf(false, TEXT("Illegal format string. Redundant unknown characters.")); - return false; - } - - if (Object.IsEmpty()) - { - Result += Begin; - Result += End; - - ApplyFillAndAlign(FillAndAlign); - - return true; - } - - TString> Buffer; - - struct { TStringView Fmt; } Empty = { LITERAL(T, "") }; - - bool bIsSuccessful = TStringObjectFormatter::Do(Buffer, Object.template GetValue<0>(), Empty); - - bIsSuccessful = [=, &Object, &Buffer](TIndexSequence) -> bool - { - return (bIsSuccessful && ... && (Buffer += Separator, TStringObjectFormatter::Do(Buffer, Object.template GetValue(), Empty))); - } - (TMakeIndexSequence()); - - if (!bIsSuccessful) - { - checkf(false, TEXT("Failed to fully format tuple value.")); - return false; - } - - Result += Begin; - Result += Buffer; - Result += End; - - ApplyFillAndAlign(FillAndAlign); - - return true; - } - - // Format the container value by format string. - else if constexpr (requires { Ranges::Begin(Object); Ranges::End(Object); }) - { - auto FillAndAlign = ParseFillAndAlign(); - - TStringView Begin = LITERAL(T, "["); - TStringView Separator = LITERAL(T, ", "); - TStringView End = LITERAL(T, "]"); - - TStringView Subfmt = LITERAL(T, ""); - - if (Fmt.StartsWith(LITERAL(T, 'T')) || Fmt.StartsWith(LITERAL(T, 't'))) - { - Fmt.RemovePrefix(1); - - const size_t PlaceholderA = Fmt.FindFirstOf(LITERAL(T, '_')); - const size_t PlaceholderB = Fmt.FindFirstOf(LITERAL(T, '_'), PlaceholderA + 1); - - if (PlaceholderA == INDEX_NONE || PlaceholderB == INDEX_NONE || PlaceholderA == PlaceholderB) - { - checkf(false, TEXT("Illegal format string. Expect placeholders.")); - return false; - } - - size_t UserDefinedEnd = Fmt.FindFirstOf(LITERAL(T, ':'), PlaceholderB + 1); - - if (UserDefinedEnd == INDEX_NONE) UserDefinedEnd = Fmt.Num(); - - Begin = Fmt.First(PlaceholderA); - Separator = Fmt.Substr(PlaceholderA + 1, PlaceholderB - PlaceholderA - 1); - End = Fmt.Substr(PlaceholderB + 1, UserDefinedEnd - PlaceholderB - 1); - - Fmt.RemovePrefix(UserDefinedEnd); - } - else if (Fmt.StartsWith(LITERAL(T, 'N'))) { Begin = LITERAL(T, ""); Separator = LITERAL(T, ""); End = LITERAL(T, ""); Fmt.RemovePrefix(1); } - else if (Fmt.StartsWith(LITERAL(T, 'n'))) { Begin = LITERAL(T, ""); Separator = LITERAL(T, ""); End = LITERAL(T, ""); Fmt.RemovePrefix(1); } - - if (Fmt.StartsWith(LITERAL(T, ':'))) - { - Fmt.RemovePrefix(1); - - Subfmt = Fmt; - - Fmt = LITERAL(T, ""); - } - - if (!Fmt.IsEmpty()) - { - checkf(false, TEXT("Illegal format string. Redundant unknown characters.")); - return false; - } - - if (Ranges::Begin(Object) == Ranges::End(Object)) - { - Result += Begin; - Result += End; - - ApplyFillAndAlign(FillAndAlign); - - return true; - } - - TString> Buffer; - - struct { TStringView Fmt; } ElementParam = { Subfmt }; - - // It is assumed that if the first element is successfully formatted, all elements will succeed. - bool bIsSuccessful = TStringObjectFormatter::Do(Buffer, *Ranges::Begin(Object), ElementParam); - - if (!bIsSuccessful) - { - checkf(false, TEXT("Failed to fully format container value.")); - return false; - } - - Result += Begin; - Result += Buffer; - - auto Sentinel = Ranges::End(Object); - - for (auto Iter = ++Ranges::Begin(Object); Iter != Sentinel; ++Iter) - { - Result += Separator; - - verify(TStringObjectFormatter::Do(Result, *Iter, ElementParam)); - } - - Result += End; - - ApplyFillAndAlign(FillAndAlign); - - return true; - } - - else static_assert(sizeof(U) == -1, "Unsupported object type."); - } - else - { - // Format the boolean value by structured parameters. - if constexpr (CSameAs) - { - constexpr bool bHasDigitStyle = requires { { Param.DigitStyle } -> CConvertibleTo; }; - constexpr bool bHasOtherStyle = requires { { Param.OtherStyle } -> CConvertibleTo; }; - - if constexpr (bHasDigitStyle || bHasOtherStyle) - { - Result.Reserve(Result.Num() + 5); - - bool bDigitLowercase = false; if constexpr (bHasDigitStyle) bDigitLowercase = Param.DigitStyle < 0; - bool bOtherLowercase = true; if constexpr (bHasOtherStyle) bOtherLowercase = Param.OtherStyle <= 0; - - if (bDigitLowercase) - { - if (Object) Result += LITERAL(T, 't'); - else Result += LITERAL(T, 'f'); - } - else - { - if (Object) Result += LITERAL(T, 'T'); - else Result += LITERAL(T, 'F'); - } - - if (bOtherLowercase) - { - if (Object) Result += LITERAL(T, "RUE"); - else Result += LITERAL(T, "ALSE"); - } - else - { - if (Object) Result += LITERAL(T, "rue"); - else Result += LITERAL(T, "alse"); - } - - return true; - } - - if (Object) Result += LITERAL(T, "True"); - else Result += LITERAL(T, "False"); - return true; - } - - // Format the integer value by structured parameters. - else if constexpr (CIntegral && !CSameAs) - { - constexpr bool bHasDigitStyle = requires { { Param.DigitStyle } -> CConvertibleTo; }; - constexpr bool bHasOtherStyle = requires { { Param.OtherStyle } -> CConvertibleTo; }; - - constexpr bool bHasSign = requires { { Param.PositiveSign } -> CConvertibleTo; }; - - constexpr bool bHasPrefix = requires { { Param.bPrefix } -> CBooleanTestable; }; - constexpr bool bHasBase = requires { { Param.Base } -> CConvertibleTo; }; - - constexpr bool bHasPadding = requires { { Param.Padding } -> CConvertibleTo; }; - - static_assert(TChar::IsASCII()); - - // If the value should be formatted with prefix, the value must be binary, octal, decimal or hexadecimal. - if constexpr (bHasPrefix && bHasBase) if (Param.bPrefix) - { - if (Param.Base != 2 && Param.Base != 8 && Param.Base != 10 && Param.Base != 16) - { - checkf(false, TEXT("Prefix is only supported for binary, octal, decimal and hexadecimal value.")); - - return false; - } - } - - using FUnsignedU = TMakeUnsigned; - - FUnsignedU Unsigned = static_cast(Object); - - bool bNegative = false; - - if constexpr (CSigned) - { - if (Object < 0) - { - bNegative = true; - - Unsigned = static_cast(-Unsigned); - } - } - - constexpr size_t BufferSize = sizeof(FUnsignedU) * 8 + 4; - - T Buffer[BufferSize]; - - T* DigitEnd = Buffer + BufferSize; - - T* Iter = Buffer + BufferSize; - - // Reverse append the digits to the buffer. - if constexpr (bHasBase) - { - checkf(Param.Base >= 2 && Param.Base <= 36, TEXT("Illegal base.")); - - if constexpr (bHasDigitStyle) - { - const bool bLowercase = Param.DigitStyle < 0; - - switch (Param.Base) - { - case 0x02: do { *--Iter = static_cast('0' + (Unsigned & 0b00001)); Unsigned >>= 1; } while (Unsigned != 0); break; - case 0x04: do { *--Iter = static_cast('0' + (Unsigned & 0b00011)); Unsigned >>= 2; } while (Unsigned != 0); break; - case 0x08: do { *--Iter = static_cast('0' + (Unsigned & 0b00111)); Unsigned >>= 3; } while (Unsigned != 0); break; - case 0x10: do { *--Iter = TChar::FromDigit(Unsigned & 0b01111, bLowercase); Unsigned >>= 4; } while (Unsigned != 0); break; - case 0X20: do { *--Iter = TChar::FromDigit(Unsigned & 0b11111, bLowercase); Unsigned >>= 5; } while (Unsigned != 0); break; - - case 3: - case 5: - case 6: - case 7: - case 9: - case 10: do { *--Iter = static_cast('0' + Unsigned % Param.Base); Unsigned = static_cast(Unsigned / Param.Base); } while (Unsigned != 0); break; - default: do { *--Iter = TChar::FromDigit(Unsigned % Param.Base, bLowercase); Unsigned = static_cast(Unsigned / Param.Base); } while (Unsigned != 0); break; - } - } - else - { - switch (Param.Base) - { - case 0x02: do { *--Iter = static_cast('0' + (Unsigned & 0b00001)); Unsigned >>= 1; } while (Unsigned != 0); break; - case 0x04: do { *--Iter = static_cast('0' + (Unsigned & 0b00011)); Unsigned >>= 2; } while (Unsigned != 0); break; - case 0x08: do { *--Iter = static_cast('0' + (Unsigned & 0b00111)); Unsigned >>= 3; } while (Unsigned != 0); break; - case 0x10: do { *--Iter = TChar::FromDigit(Unsigned & 0b01111); Unsigned >>= 4; } while (Unsigned != 0); break; - case 0X20: do { *--Iter = TChar::FromDigit(Unsigned & 0b11111); Unsigned >>= 5; } while (Unsigned != 0); break; - - case 3: - case 5: - case 6: - case 7: - case 9: - case 10: do { *--Iter = static_cast('0' + Unsigned % Param.Base); Unsigned = static_cast(Unsigned / Param.Base); } while (Unsigned != 0); break; - default: do { *--Iter = TChar::FromDigit(Unsigned % Param.Base); Unsigned = static_cast(Unsigned / Param.Base); } while (Unsigned != 0); break; - } - } - } - else do { *--Iter = static_cast('0' + Unsigned % 10); Unsigned = static_cast(Unsigned / 10); } while (Unsigned != 0); - - T* DigitBegin = Iter; - - // Handle the width parameter. - if constexpr (bHasPadding) if (Param.Padding > DigitEnd - DigitBegin) - { - const size_t Padding = Param.Padding - (DigitEnd - DigitBegin); - - if (Param.Padding < sizeof(FUnsignedU) * 8) for (size_t Index = 0; Index != Padding; ++Index) *--Iter = LITERAL(T, '0'); - } - - // Append the prefix to the buffer. - if constexpr (bHasPrefix && bHasBase) if (Param.bPrefix && Param.Base != 10) - { - bool bOtherLowercase = true; if constexpr (bHasOtherStyle) bOtherLowercase = Param.OtherStyle <= 0; - - const T PrefixBin = bOtherLowercase ? LITERAL(T, 'b') : LITERAL(T, 'B'); - const T PrefixHex = bOtherLowercase ? LITERAL(T, 'x') : LITERAL(T, 'X'); - - if (Param.Base == 2) { *--Iter = PrefixBin; *--Iter = LITERAL(T, '0'); } - if (Param.Base == 8) { if (Object != 0) *--Iter = LITERAL(T, '0'); } - if (Param.Base == 16) { *--Iter = PrefixHex; *--Iter = LITERAL(T, '0'); } - } - - // Append the negative sign to the buffer. - if constexpr (CSigned) if (bNegative) *--Iter = LITERAL(T, '-'); - - // Append the positive sign to the buffer. - if constexpr (bHasSign) if (!bNegative && Param.PositiveSign != LITERAL(T, '-')) *--Iter = Param.PositiveSign; - - // Handle the width parameter. - if constexpr (bHasPadding) if (Param.Padding > DigitEnd - DigitBegin) - { - const size_t Padding = Param.Padding - (DigitEnd - DigitBegin); - - if (Param.Padding > sizeof(FUnsignedU) * 8) - { - Result.Reserve(Result.Num() + (DigitBegin - Iter) + Param.Padding); - - Result.Append(Iter, DigitBegin); - - for (size_t Index = 0; Index != Padding; ++Index) Result += LITERAL(T, '0'); - - Result.Append(DigitBegin, DigitEnd); - - return true; - } - } - - Result.Append(Iter, DigitEnd); - - return true; - } - - // Format the floating-point value by structured parameters. - else if constexpr (CFloatingPoint) - { - constexpr bool bHasDigitStyle = requires { { Param.DigitStyle } -> CConvertibleTo; }; - constexpr bool bHasOtherStyle = requires { { Param.OtherStyle } -> CConvertibleTo; }; - - constexpr bool bHasSign = requires { { Param.PositiveSign } -> CConvertibleTo; }; - - constexpr bool bHasPrefix = requires { { Param.bPrefix } -> CBooleanTestable; }; - constexpr bool bHasPrecision = requires { { Param.Precision } -> CConvertibleTo; }; - - constexpr bool bHasFormat = - requires - { - { Param.bFixed } -> CBooleanTestable; - { Param.bScientific } -> CBooleanTestable; - }; - - NAMESPACE_STD::chars_format Format = NAMESPACE_STD::chars_format::general; - - if constexpr (bHasFormat) if ( Param.bFixed && !Param.bScientific) Format = NAMESPACE_STD::chars_format::fixed; - if constexpr (bHasFormat) if (!Param.bFixed && Param.bScientific) Format = NAMESPACE_STD::chars_format::scientific; - if constexpr (bHasFormat) if (!Param.bFixed && !Param.bScientific) Format = NAMESPACE_STD::chars_format::hex; - - constexpr size_t StartingBufferSize = 64; - - // Create a buffer with a starting size. - TArray> Buffer(StartingBufferSize / 2); - - // Formatting strings using the standard library until successful - NAMESPACE_STD::to_chars_result ConvertResult; - - do - { - Buffer.SetNum(Buffer.Num() * 2); - - if constexpr (bHasPrecision) - { - if (Param.Precision >= 0) ConvertResult = NAMESPACE_STD::to_chars(ToAddress(Buffer.Begin()), ToAddress(Buffer.End()), Object, Format, Param.Precision); - else ConvertResult = NAMESPACE_STD::to_chars(ToAddress(Buffer.Begin()), ToAddress(Buffer.End()), Object, Format); - } - else if constexpr (bHasFormat) ConvertResult = NAMESPACE_STD::to_chars(ToAddress(Buffer.Begin()), ToAddress(Buffer.End()), Object, Format); - else ConvertResult = NAMESPACE_STD::to_chars(ToAddress(Buffer.Begin()), ToAddress(Buffer.End()), Object); - } - while (ConvertResult.ec == NAMESPACE_STD::errc::value_too_large); - - // Set the buffer size to the number of characters written. - Buffer.SetNum(ConvertResult.ptr - Buffer.GetData()); - - const bool bNegative = Buffer[0] == '-'; - - const char* Iter = Buffer.GetData() + (bNegative ? 1 : 0); - - bool bDigitLowercase = false; if constexpr (bHasDigitStyle) bDigitLowercase = Param.DigitStyle < 0; - bool bOtherLowercase = true; if constexpr (bHasOtherStyle) bOtherLowercase = Param.OtherStyle <= 0; - - // Handle the infinity values. - if (*Iter == 'i') - { - Result.Reserve(Result.Num() + 9); - - if (bNegative) Result.Append(LITERAL(T, "-")); - - // Append the positive sign to the buffer. - else if constexpr (bHasSign) if (Param.PositiveSign != LITERAL(T, '-')) Result += Param.PositiveSign; - - if constexpr (bHasDigitStyle || bHasOtherStyle) - { - if (bDigitLowercase) Result += LITERAL(T, 'i'); - else Result += LITERAL(T, 'I'); - - if (bOtherLowercase) Result += LITERAL(T, "nfinity"); - else Result += LITERAL(T, "NFINITY"); - - return true; - } - - Result += LITERAL(T, "Infinity"); - return true; - } - - // Handle the NaN values. - if (*Iter == 'n') - { - Result.Reserve(Result.Num() + 4); - - if (bNegative) Result.Append(LITERAL(T, "-")); - - // Append the positive sign to the buffer. - else if constexpr (bHasSign) if (Param.PositiveSign != LITERAL(T, '-')) Result += Param.PositiveSign; - - if constexpr (bHasDigitStyle || bHasOtherStyle) - { - if (bDigitLowercase) Result += LITERAL(T, 'n'); - else Result += LITERAL(T, 'N'); - - if (bOtherLowercase) Result += LITERAL(T, "a"); - else Result += LITERAL(T, "A"); - - if (bDigitLowercase) Result += LITERAL(T, 'n'); - else Result += LITERAL(T, 'N'); - - return true; - } - - Result += LITERAL(T, "NaN"); - return true; - } - - Result.Reserve(Result.Num() + Buffer.Num() + 4); - - // Append the positive sign to the buffer. - if constexpr (bHasSign) if (Param.PositiveSign != LITERAL(T, '-')) Result += Param.PositiveSign; - - // Handle the prefix. - if constexpr (bHasPrefix) if (Param.bPrefix) - { - if (Format == NAMESPACE_STD::chars_format::hex) - { - if (bOtherLowercase) Result += LITERAL(T, "0x"); - else Result += LITERAL(T, "0X"); - } - } - - // Handle the lowercase or uppercase characters. - if constexpr (bHasFormat || bHasDigitStyle || bHasOtherStyle) - { - const unsigned Base = Format == NAMESPACE_STD::chars_format::hex ? 16 : 10; - - if (Base == 16 && !bDigitLowercase) - { - for (char& Char : Buffer) if (FChar::ToDigit(Char) < Base) Char = FChar::ToUpper(Char); - } - - if (!bOtherLowercase) - { - for (char& Char : Buffer) if (FChar::ToDigit(Char) >= Base) Char = FChar::ToUpper(Char); - } - } - - Result.Append(Buffer.Begin(), Buffer.End()); - - return true; - } - - else static_assert(sizeof(U) == -1, "Unsupported object type."); - } - - checkf(false, TEXT("Unsupported type for formatting.")); - - return false; - } -}; - -template -struct TStringFormatOrParseHelper -{ - static constexpr T LeftBrace = LITERAL(T, '{'); - static constexpr T RightBrace = LITERAL(T, '}'); - - static inline const TStringView EscapeLeftBrace = LITERAL(T, "[{"); - static inline const TStringView EscapeRightBrace = LITERAL(T, "}]"); - - static FORCEINLINE 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(EscapeLeftBrace)) - { - Fmt.RemovePrefix(EscapeLeftBrace.Num()); - - if constexpr (!bIsFormat) - { - if (!String.StartsWith(LeftBrace)) return false; - - String.RemovePrefix(1); - } - else String += LeftBrace; - - continue; - } - - if (Fmt.StartsWith(EscapeRightBrace)) - { - Fmt.RemovePrefix(EscapeRightBrace.Num()); - - if constexpr (!bIsFormat) - { - if (!String.StartsWith(RightBrace)) return false; - - String.RemovePrefix(1); - } - else String += RightBrace; - - continue; - } - - if (Fmt.StartsWith(LeftBrace)) - { - 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(LeftBrace, PlaceholderBegin + 1); - - if (PlaceholderBegin == INDEX_NONE) break; - - if (Fmt.First(PlaceholderBegin + 1).EndsWith(EscapeLeftBrace)) - { - ++PlaceholderBegin; - ++SubplaceholderNum; - } - else break; - } - - while (true) - { - PlaceholderEnd = Fmt.FindFirstOf(RightBrace, PlaceholderEnd + 1); - - if (PlaceholderEnd == INDEX_NONE) break; - - if (Fmt.Substr(PlaceholderEnd).StartsWith(EscapeRightBrace)) - { - ++PlaceholderEnd; - ++SubplaceholderNum; - } - 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 = TStringFormatOrParseHelper::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.template IsInteger(10)) - { - checkf(false, TEXT("Invalid placeholder index.")); - - if constexpr (bIsFormat) - { - String += LeftBrace; - String += Subfmt; - String += RightBrace; - - bIsFullyFormatted = false; - } - else return false; - - continue; - } - else Index = PlaceholderIndex.template ToInt(); - } - else Index = ArgsIndex++; - - checkf(Index < ArgsTuple.Num(), TEXT("Argument not found.")); - - bIsSuccessful = ArgsTuple.Visit( - [&String, Subfmt = PlaceholderSubfmt](auto& Object) mutable - { - struct { TStringView Fmt; } Param = { Subfmt }; - - if constexpr (bIsFormat) return TStringObjectFormatter::Do(String, Object, Param); - - else static_assert(bIsFormat, "Parsing is not supported."); - }, - Index - ); - } - - if (!bIsSuccessful) - { - if constexpr (bIsFormat) - { - String += LeftBrace; - String += Subfmt; - String += RightBrace; - - bIsFullyFormatted = false; - } - else return false; - } - else ++FormattedObjectNum; - - continue; - } - - check_code({ if (Fmt.StartsWith(RightBrace)) 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 -void TString::AppendFormat(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; - - NAMESPACE_PRIVATE::TStringFormatOrParseHelper::Do(Result, Fmt, ForwardAsTuple(Args...)); - - Append(Result.Begin(), Result.End()); -} - -template Allocator> -void TString::AppendBool(bool Value) -{ - NAMESPACE_PRIVATE::TStringObjectFormatter::Do(*this, AsConst(Value), Invalid); -} - -template Allocator> -template requires (!CSameAs && !CConst && !CVolatile) -void TString::AppendInt(U Value, unsigned Base) -{ - checkf(Base >= 2 && Base <= 36, TEXT("Illegal base. Please check the base.")); - - struct { unsigned Base; } Param = { Base }; - - NAMESPACE_PRIVATE::TStringObjectFormatter::Do(*this, AsConst(Value), Param); -} - -template Allocator> template requires (!CConst && !CVolatile) -void TString::AppendFloat(U Value) -{ - NAMESPACE_PRIVATE::TStringObjectFormatter::Do(*this, AsConst(Value), Invalid); -} - -template Allocator> template requires (!CConst && !CVolatile) -void TString::AppendFloat(U Value, bool bFixed, bool bScientific) -{ - struct { bool bFixed; bool bScientific; } Param = { bFixed, bScientific }; - - NAMESPACE_PRIVATE::TStringObjectFormatter::Do(*this, AsConst(Value), Param); -} - -template Allocator> template requires (!CConst && !CVolatile) -void TString::AppendFloat(U Value, bool bFixed, bool bScientific, unsigned Precision) -{ - struct { bool bFixed; bool bScientific; unsigned Precision; } Param = { bFixed, bScientific, Precision }; - - NAMESPACE_PRIVATE::TStringObjectFormatter::Do(*this, AsConst(Value), Param); -} - -NAMESPACE_MODULE_END(Utility) -NAMESPACE_MODULE_END(Redcraft) -NAMESPACE_REDCRAFT_END - -#pragma warning(pop) diff --git a/Redcraft.Utility/Source/Public/Strings/String.h b/Redcraft.Utility/Source/Public/Strings/String.h index 10b54b3..3d8f4c3 100644 --- a/Redcraft.Utility/Source/Public/Strings/String.h +++ b/Redcraft.Utility/Source/Public/Strings/String.h @@ -9,12 +9,14 @@ #include "Containers/Array.h" #include "Containers/ArrayView.h" #include "Iterators/Utility.h" -#include "Iterators/BasicIterator.h" #include "Iterators/Sentinel.h" +#include "Iterators/BasicIterator.h" +#include "Iterators/InsertIterator.h" #include "Ranges/Utility.h" #include "Ranges/Factory.h" #include "Strings/Char.h" #include "Strings/StringView.h" +#include "Strings/Formatting.h" #include "Miscellaneous/AssertionMacros.h" #include @@ -1231,7 +1233,7 @@ public: * @return The string containing the integer value. */ template requires (!CSameAs && !CConst && !CVolatile) - NODISCARD static FORCEINLINE TString FromInt(U Value, unsigned Base = 10) + NODISCARD static FORCEINLINE TString FromInt(U Value, uint Base = 10) { checkf(Base >= 2 && Base <= 36, TEXT("Illegal base. Please check the base.")); @@ -1289,7 +1291,7 @@ public: * @return */ template requires (!CConst && !CVolatile) - NODISCARD static FORCEINLINE TString FromFloat(U Value, bool bFixed, bool bScientific, unsigned Precision) + NODISCARD static FORCEINLINE TString FromFloat(U Value, bool bFixed, bool bScientific, uint Precision) { TString Result; @@ -1299,23 +1301,84 @@ public: } /** Converts a boolean value into a string and appends it to the string. */ - void AppendBool(bool Value); + FORCEINLINE void AppendBool(bool Value) + { + auto Inserter = Ranges::View(MakeBackInserter(*this), UnreachableSentinel); + + Algorithms::Format(Inserter, LITERAL_VIEW(FElementType, "{0}"), Value); + } /** Converts an integer value into a string and appends it to the string. */ template requires (!CSameAs && !CConst && !CVolatile) - void AppendInt(U Value, unsigned Base = 10); + FORCEINLINE void AppendInt(U Value, uint Base = 10) + { + auto Inserter = Ranges::View(MakeBackInserter(*this), UnreachableSentinel); + + Algorithms::Format(Inserter, LITERAL_VIEW(FElementType, "{0:_{1}I}"), Value, Base); + } /** Converts a floating-point value into a string and appends it to the string. */ template requires (!CConst && !CVolatile) - void AppendFloat(U Value); + FORCEINLINE void AppendFloat(U Value) + { + auto Inserter = Ranges::View(MakeBackInserter(*this), UnreachableSentinel); + + Algorithms::Format(Inserter, LITERAL_VIEW(FElementType, "{0}"), Value); + } /** Converts a floating-point value into a string and appends it to the string. */ template requires (!CConst && !CVolatile) - void AppendFloat(U Value, bool bFixed, bool bScientific); + FORCEINLINE void AppendFloat(U Value, bool bFixed, bool bScientific) + { + auto Inserter = Ranges::View(MakeBackInserter(*this), UnreachableSentinel); + + if (bFixed && bScientific) + { + Algorithms::Format(Inserter, LITERAL_VIEW(FElementType, "{0:G}"), Value); + } + + else if (bFixed) + { + Algorithms::Format(Inserter, LITERAL_VIEW(FElementType, "{0:F}"), Value); + } + + else if (bScientific) + { + Algorithms::Format(Inserter, LITERAL_VIEW(FElementType, "{0:E}"), Value); + } + + else + { + Algorithms::Format(Inserter, LITERAL_VIEW(FElementType, "{0:A}"), Value); + } + } /** Converts a floating-point value into a string and appends it to the string. */ template requires (!CConst && !CVolatile) - void AppendFloat(U Value, bool bFixed, bool bScientific, unsigned Precision); + FORCEINLINE void AppendFloat(U Value, bool bFixed, bool bScientific, uint Precision) + { + auto Inserter = Ranges::View(MakeBackInserter(*this), UnreachableSentinel); + + if (bFixed && bScientific) + { + Algorithms::Format(Inserter, LITERAL_VIEW(FElementType, "{0:.{1}G}"), Value, Precision); + } + + else if (bFixed) + { + Algorithms::Format(Inserter, LITERAL_VIEW(FElementType, "{0:.{1}F}"), Value, Precision); + } + + else if (bScientific) + { + Algorithms::Format(Inserter, LITERAL_VIEW(FElementType, "{0:.{1}E}"), Value, Precision); + } + + else + { + Algorithms::Format(Inserter, LITERAL_VIEW(FElementType, "{0:.{1}A}"), Value, Precision); + } + } public: @@ -1339,7 +1402,12 @@ public: /** Format some objects using a format string and append to the string. */ template - void AppendFormat(TStringView Fmt, const Ts&... Args); + FORCEINLINE void AppendFormat(TStringView Fmt, const Ts&... Args) + { + auto Inserter = Ranges::View(MakeBackInserter(*this), UnreachableSentinel); + + Algorithms::Format(Inserter, Fmt, Args...); + } public: @@ -1379,5 +1447,3 @@ template template constexpr TStringView::T NAMESPACE_MODULE_END(Utility) NAMESPACE_MODULE_END(Redcraft) NAMESPACE_REDCRAFT_END - -#include "Strings/Conversion.h.inl"