From 3859070d534c0f8c1051c9d5f8c155c01ff50f48 Mon Sep 17 00:00:00 2001 From: Redstone1024 <2824517378@qq.com> Date: Tue, 12 Nov 2024 17:21:36 +0800 Subject: [PATCH] feat(string): add string trimming functions --- .../Source/Private/Testing/StringTesting.cpp | 24 ++- .../Source/Public/String/Conversion.h.inl | 84 +++++---- .../Source/Public/String/String.h | 164 +++++++++++++++++- .../Source/Public/String/StringView.h | 138 +++++++++++++-- 4 files changed, 356 insertions(+), 54 deletions(-) diff --git a/Redcraft.Utility/Source/Private/Testing/StringTesting.cpp b/Redcraft.Utility/Source/Private/Testing/StringTesting.cpp index cc44e49..f33366f 100644 --- a/Redcraft.Utility/Source/Private/Testing/StringTesting.cpp +++ b/Redcraft.Utility/Source/Private/Testing/StringTesting.cpp @@ -175,13 +175,21 @@ void TestStringView() always_check(View.FindLastNotOf(LITERAL(T, '!')) == 27); } + { + always_check(LITERAL_VIEW(T, " ABC ").TrimStart() == LITERAL(T, "ABC ")); + always_check(LITERAL_VIEW(T, " ABC ").TrimEnd() == LITERAL(T, " ABC" )); + always_check(LITERAL_VIEW(T, " ABC ").TrimStartAndEnd() == LITERAL(T, "ABC" )); + + always_check(LITERAL_VIEW(T, " A\0C ").TrimToNullTerminator() == LITERAL(T, " A")); + } + { always_check( LITERAL_VIEW(T, "012345678900").IsASCII()); always_check(!LITERAL_VIEW(T, "\u4E38\u8FA3").IsASCII()); - always_check( LITERAL_VIEW(T, "012345678900").IsNumeric()); - always_check(!LITERAL_VIEW(T, "\u4E38\u8FA3").IsNumeric()); - always_check(!LITERAL_VIEW(T, "0123456789AB").IsNumeric()); - always_check( LITERAL_VIEW(T, "0123456789AB").IsNumeric(16)); + always_check( LITERAL_VIEW(T, "012345678900").IsInteger()); + always_check(!LITERAL_VIEW(T, "\u4E38\u8FA3").IsInteger()); + always_check(!LITERAL_VIEW(T, "0123456789AB").IsInteger()); + always_check( LITERAL_VIEW(T, "0123456789AB").IsInteger(16)); } }; @@ -404,6 +412,14 @@ void TestTemplateString() always_check(Str.FindLastNotOf(LITERAL(T, '!')) == 27); } + { + always_check(TString(LITERAL(T, " ABC ")).TrimStart() == LITERAL(T, "ABC ")); + always_check(TString(LITERAL(T, " ABC ")).TrimEnd() == LITERAL(T, " ABC" )); + always_check(TString(LITERAL(T, " ABC ")).TrimStartAndEnd() == LITERAL(T, "ABC" )); + + always_check(TString(LITERAL(T, " A\0C ")).TrimToNullTerminator() == LITERAL(T, " A")); + } + { always_check(TString(LITERAL(T, "\u4E38\u8FA3")).ToString() == TEXT("\u4E38\u8FA3")); always_check(TString(LITERAL(T, "\u4E38\u8FA3")).ToWString() == WTEXT("\u4E38\u8FA3")); diff --git a/Redcraft.Utility/Source/Public/String/Conversion.h.inl b/Redcraft.Utility/Source/Public/String/Conversion.h.inl index 3653b7c..2a3d640 100644 --- a/Redcraft.Utility/Source/Public/String/Conversion.h.inl +++ b/Redcraft.Utility/Source/Public/String/Conversion.h.inl @@ -457,7 +457,7 @@ struct TStringHelper if (IndexLength != 0) { - if (!PlaceholderIndex.IsNumeric()) + if (!PlaceholderIndex.IsInteger(10, false)) { checkf(false, TEXT("Invalid placeholder index.")); @@ -550,27 +550,23 @@ NAMESPACE_PRIVATE_END template Allocator> template -TString TString::Format(TStringView Fmt, const Ts&... Args) +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; - - Result.Reserve(ReserveBufferSize); + TString> Result; NAMESPACE_PRIVATE::TStringHelper::Do(Result, Fmt, ForwardAsTuple(Args...)); - return Result; + Append(Result.Begin(), Result.End()); } template template -size_t TStringView::Parse(TStringView Fmt, Ts&... Args) const +size_t TStringView::ParseAndTrim(TStringView Fmt, Ts&... Args) { - TStringView View = *this; - - return NAMESPACE_PRIVATE::TStringHelper::Do(View, Fmt, ForwardAsTuple(Args...)); + return NAMESPACE_PRIVATE::TStringHelper::Do(*this, Fmt, ForwardAsTuple(Args...)); } #undef LEFT_BRACE @@ -580,43 +576,55 @@ size_t TStringView::Parse(TStringView Fmt, Ts&... Args) const #undef ESCAPE_RIGHT_BRACE template -constexpr bool TStringView::ToBool() const +constexpr bool TStringView::ToBoolAndTrim() { - if (StartsWith(LITERAL(T, '1')) - || StartsWith(LITERAL(T, "true")) - || StartsWith(LITERAL(T, "True")) - || StartsWith(LITERAL(T, "TRUE"))) + if (this->IsEmpty()) return false; + + if (this->Front() == LITERAL(T, '1')) { + RemovePrefix(1); return true; } - if (StartsWith(LITERAL(T, '0')) - || StartsWith(LITERAL(T, "false")) - || StartsWith(LITERAL(T, "False")) - || StartsWith(LITERAL(T, "FALSE"))) + if (this->Front() == LITERAL(T, '0')) { + RemovePrefix(1); return false; } - return ToInt() != 0; + if (StartsWith(LITERAL(T, "true")) + || StartsWith(LITERAL(T, "True")) + || StartsWith(LITERAL(T, "TRUE"))) + { + RemovePrefix(4); + return true; + } + + if (StartsWith(LITERAL(T, "false")) + || StartsWith(LITERAL(T, "False")) + || StartsWith(LITERAL(T, "FALSE"))) + { + RemovePrefix(5); + return false; + } + + return ToIntAndTrim() != 0; } template template requires (!CSameAs && !CConst && !CVolatile) -constexpr U TStringView::ToInt(unsigned Base) const +constexpr U TStringView::ToIntAndTrim(unsigned Base) { checkf(Base >= 2 && Base <= 36, TEXT("Illegal base. Please check the base.")); bool bNegative = false; - TStringView View = *this; - if constexpr (CSigned) { - if (View.StartsWith(LITERAL(ElementType, '-'))) + if (StartsWith(LITERAL(ElementType, '-'))) { bNegative = true; - View.RemovePrefix(1); + RemovePrefix(1); } } @@ -630,9 +638,13 @@ constexpr U TStringView::ToInt(unsigned Base) const UnsignedU LastValue = 0; UnsignedU Value = 0; - for (ElementType Char : View) + bool bOverflow = false; + + while (!this->IsEmpty()) { - auto Digit = TChar::ToDigit(Char); + auto Digit = TChar::ToDigit(this->Front()); + + RemovePrefix(1); if (Digit >= Base) break; @@ -640,14 +652,16 @@ constexpr U TStringView::ToInt(unsigned Base) const Value = static_cast(LastValue * Base + Digit); - if (Value < LastValue) + if (Value < LastValue) bOverflow = true; + } + + if (bOverflow) + { + if constexpr (CSigned) { - if constexpr (CSigned) - { - return bNegative ? SignedMinimum : SignedMaximum; - } - else return UnsignedMaximum; + return bNegative ? SignedMinimum : SignedMaximum; } + else return UnsignedMaximum; } if constexpr (CSigned) @@ -663,7 +677,7 @@ constexpr U TStringView::ToInt(unsigned Base) const template template requires (!CConst && !CVolatile) -constexpr U TStringView::ToFloat(bool bFixed, bool bScientific) const +constexpr U TStringView::ToFloatAndTrim(bool bFixed, bool bScientific) { NAMESPACE_STD::chars_format Format; @@ -768,6 +782,8 @@ constexpr U TStringView::ToFloat(bool bFixed, bool bScientific) const } else ConvertResult = NAMESPACE_STD::from_chars(ToAddress(this->Begin()), ToAddress(this->End()), Result, Format); + RemovePrefix(Iter - this->Begin()); + if (ConvertResult.ec == NAMESPACE_STD::errc::result_out_of_range) { if (!bNegativeMantissa && !bNegativeExponent) return NAMESPACE_STD::numeric_limits::infinity(); diff --git a/Redcraft.Utility/Source/Public/String/String.h b/Redcraft.Utility/Source/Public/String/String.h index a620380..a7c2dc8 100644 --- a/Redcraft.Utility/Source/Public/String/String.h +++ b/Redcraft.Utility/Source/Public/String/String.h @@ -338,6 +338,78 @@ 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: + + /** Shrinks the view by moving its start forward. */ + FORCEINLINE constexpr TString& RemovePrefix(size_t Count, bool bAllowShrinking = true) + { + checkf(Count <= this->Num(), TEXT("Illegal subview range. Please check Count.")); + + Erase(0, Count, bAllowShrinking); + + return *this; + } + + /** Shrinks the view by moving its end backward. */ + FORCEINLINE constexpr TString& RemoveSuffix(size_t Count, bool bAllowShrinking = true) + { + checkf(Count <= this->Num(), TEXT("Illegal subview range. Please check Count.")); + + this->SetNum(this->Num() - Count, bAllowShrinking); + + return *this; + } + + /** Removes whitespace characters from the start of this string. */ + FORCEINLINE constexpr TString& TrimStart(bool bAllowShrinking = true) + { + auto Index = Find([](ElementType Char) { return !TChar::IsSpace(Char); }); + + if (Index != INDEX_NONE) + { + RemovePrefix(Index, bAllowShrinking); + } + else this->Reset(bAllowShrinking); + + return *this; + } + + /** Removes whitespace characters from the end of this string. */ + FORCEINLINE constexpr TString& TrimEnd(bool bAllowShrinking = true) + { + auto Index = RFind([](ElementType Char) { return !TChar::IsSpace(Char); }); + + if (Index != INDEX_NONE) + { + this->SetNum(Index + 1, bAllowShrinking); + } + else this->Reset(bAllowShrinking); + + return *this; + } + + /** Removes whitespace characters from the start and end of this string. */ + FORCEINLINE constexpr TString& TrimStartAndEnd(bool bAllowShrinking = true) + { + TrimStart(false); + TrimEnd(bAllowShrinking); + + return *this; + } + + /** Removes characters after the first null-terminator. */ + FORCEINLINE constexpr TString& TrimToNullTerminator(bool bAllowShrinking = true) + { + auto Index = Find(LITERAL(ElementType, '\0')); + + if (Index != INDEX_NONE) + { + this->SetNum(Index, bAllowShrinking); + } + + return *this; + } + public: /** @return true if the string view starts with the given prefix, false otherwise. */ @@ -1033,10 +1105,22 @@ public: return TStringView(*this).IsASCII(); } - /** @return true if the string only contains numeric characters, false otherwise. */ - NODISCARD FORCEINLINE bool IsNumeric(unsigned Base = 10) const + /** @return true if the string can be fully represented as a boolean value, false otherwise. */ + NODISCARD FORCEINLINE bool IsBoolean() const { - return TStringView(*this).IsNumeric(Base); + return TStringView(*this).IsBoolean(); + } + + /** @return true if the string can be fully represented as an integer value, false otherwise. */ + NODISCARD FORCEINLINE bool IsInteger(unsigned Base = 10, bool bSigned = true) const + { + return TStringView(*this).IsInteger(Base, bSigned); + } + + /** @return true if the string can be fully represented as a floating-point value, false otherwise. */ + NODISCARD FORCEINLINE bool IsFloatingPoint(bool bFixed = true, bool bScientific = true, bool bSigned = true) const + { + return TStringView(*this).IsFloatingPoint(bFixed, bScientific, bSigned); } public: @@ -1207,10 +1291,54 @@ public: return TStringView(*this).template ToFloat(bFixed, bScientific); } + /** Converts a string into a boolean value and remove the parsed substring. */ + NODISCARD FORCEINLINE bool ToBoolAndTrim() + { + TStringView View = *this; + + bool Result = View.ToBool(); + + size_t TrimNum = this->Num() - View.Num(); + + if (TrimNum > 0) Erase(0, TrimNum); + + return Result; + } + + /** Converts a string into an integer value and remove the parsed substring. */ + template requires (!CSameAs && !CConst && !CVolatile) + NODISCARD FORCEINLINE U ToIntAndTrim(unsigned Base = 10) + { + TStringView View = *this; + + U Result = View.template ToInt(Base); + + size_t TrimNum = this->Num() - View.Num(); + + if (TrimNum > 0) Erase(0, TrimNum); + + return Result; + } + + /** Converts a string into a floating-point value and remove the parsed substring. */ + template requires (!CConst && !CVolatile) + NODISCARD FORCEINLINE U ToFloatAndTrim(bool bFixed = true, bool bScientific = true) + { + TStringView View = *this; + + U Result = View.template ToFloat(bFixed, bScientific); + + size_t TrimNum = this->Num() - View.Num(); + + if (TrimNum > 0) Erase(0, TrimNum); + + return Result; + } + public: /** - * Format some objects using a format string and append to the string. + * Format some objects using a format string. * * @param Fmt - The format string. * @param Args - The objects to format. @@ -1218,7 +1346,18 @@ public: * @return The formatted string containing the objects. */ template - NODISCARD static TString Format(TStringView Fmt, const Ts&... Args); + NODISCARD static FORCEINLINE TString Format(TStringView Fmt, const Ts&... Args) + { + TString Result; + + Result.AppendFormat(Fmt, Args...); + + return Result; + } + + /** Format some objects using a format string and append to the string. */ + template + void AppendFormat(TStringView Fmt, const Ts&... Args); /** * Parse a string using a format string to objects. @@ -1234,6 +1373,21 @@ public: return TStringView(*this).Parse(Fmt, Args...); } + /** Parse a string using a format string to objects and remove the parsed substring. */ + template + FORCEINLINE size_t ParseAndTrim(TStringView Fmt, Ts&... Args) + { + TStringView View = *this; + + size_t Result = View.ParseAndTrim(Fmt, Args...); + + size_t TrimNum = this->Num() - View.Num(); + + if (TrimNum > 0) Erase(0, TrimNum); + + return Result; + } + public: /** Overloads the GetTypeHash algorithm for TString. */ diff --git a/Redcraft.Utility/Source/Public/String/StringView.h b/Redcraft.Utility/Source/Public/String/StringView.h index 5b12e49..b350ab9 100644 --- a/Redcraft.Utility/Source/Public/String/StringView.h +++ b/Redcraft.Utility/Source/Public/String/StringView.h @@ -113,22 +113,80 @@ public: NODISCARD friend constexpr auto operator<=>(TStringView LHS, ElementType RHS) { return LHS <=> TStringView(&RHS, 1); } NODISCARD friend constexpr auto operator<=>(ElementType LHS, TStringView RHS) { return TStringView(&LHS, 1) <=> RHS; } +public: + /** Shrinks the view by moving its start forward. */ - FORCEINLINE constexpr void RemovePrefix(size_t Count) + FORCEINLINE constexpr TStringView RemovePrefix(size_t Count) { checkf(Count <= this->Num(), TEXT("Illegal subview range. Please check Count.")); *this = Substr(Count); + + return *this; } /** Shrinks the view by moving its end backward. */ - FORCEINLINE constexpr void RemoveSuffix(size_t Count) + FORCEINLINE constexpr TStringView RemoveSuffix(size_t Count) { checkf(Count <= this->Num(), TEXT("Illegal subview range. Please check Count.")); *this = Substr(0, this->Num() - Count); + + return *this; } + /** Removes whitespace characters from the start of this string. */ + FORCEINLINE constexpr TStringView TrimStart() + { + auto Index = Find([](ElementType Char) { return !TChar::IsSpace(Char); }); + + if (Index != INDEX_NONE) + { + RemovePrefix(Index); + } + else *this = TStringView(); + + return *this; + } + + /** Removes whitespace characters from the end of this string. */ + FORCEINLINE constexpr TStringView TrimEnd() + { + auto Index = RFind([](ElementType Char) { return !TChar::IsSpace(Char); }); + + if (Index != INDEX_NONE) + { + RemoveSuffix(this->Num() - Index - 1); + } + else *this = TStringView(); + + return *this; + } + + /** Removes whitespace characters from the start and end of this string. */ + FORCEINLINE constexpr TStringView TrimStartAndEnd() + { + TrimStart(); + TrimEnd(); + + return *this; + } + + /** Removes characters after the first null-terminator. */ + FORCEINLINE constexpr TStringView TrimToNullTerminator() + { + auto Index = Find(LITERAL(ElementType, '\0')); + + if (Index != INDEX_NONE) + { + *this = Substr(0, Index); + } + + return *this; + } + +public: + /** Copies the elements of this string view to the destination buffer without null-termination. */ FORCEINLINE constexpr size_t Copy(ElementType* Dest, size_t Count = DynamicExtent, size_t Offset = 0) const { @@ -401,15 +459,46 @@ public: return true; } - /** @return true if the string only contains numeric characters, false otherwise. */ - NODISCARD constexpr bool IsNumeric(unsigned Base = 10) const + /** @return true if the string can be fully represented as a boolean value, false otherwise. */ + NODISCARD FORCEINLINE constexpr bool IsBoolean() const { - for (ElementType Char : *this) + TStringView View = *this; + + Ignore = View.ToBoolAndTrim(); + + return View.IsEmpty(); + } + + /** @return true if the string can be fully represented as an integer value, false otherwise. */ + NODISCARD FORCEINLINE constexpr bool IsInteger(unsigned Base = 10, bool bSigned = true) const + { + TStringView View = *this; + + if (View.StartsWith(LITERAL(ElementType, '-'))) { - if (!TChar::IsDigit(Char, Base)) return false; + if (bSigned) View.RemovePrefix(1); + else return false; } - return true; + Ignore = View.ToIntAndTrim(Base); + + return View.IsEmpty(); + } + + /** @return true if the string can be fully represented as a floating-point value, false otherwise. */ + NODISCARD FORCEINLINE constexpr bool IsFloatingPoint(bool bFixed = true, bool bScientific = true, bool bSigned = true) const + { + TStringView View = *this; + + if (View.StartsWith(LITERAL(ElementType, '-'))) + { + if (bSigned) View.RemovePrefix(1); + else return false; + } + + Ignore = View.ToFloatAndTrim(bFixed, bScientific); + + return View.IsEmpty(); } public: @@ -422,7 +511,10 @@ public: * * @return The boolean value. */ - NODISCARD constexpr bool ToBool() const; + NODISCARD constexpr bool ToBool() const + { + return TStringView(*this).ToBoolAndTrim(); + } /** * Converts a string into an integer value. @@ -438,7 +530,10 @@ public: * @return The integer value. */ template requires (!CSameAs && !CConst && !CVolatile) - NODISCARD constexpr U ToInt(unsigned Base = 10) const; + NODISCARD constexpr U ToInt(unsigned Base = 10) const + { + return TStringView(*this).ToIntAndTrim(Base); + } /** * Converts a string into a floating-point value. @@ -456,7 +551,21 @@ public: * @return The floating-point value. */ template requires (!CConst && !CVolatile) - NODISCARD constexpr U ToFloat(bool bFixed = true, bool bScientific = true) const; + NODISCARD constexpr U ToFloat(bool bFixed = true, bool bScientific = true) const + { + return TStringView(*this).ToFloatAndTrim(bFixed, bScientific); + } + + /** Converts a string into a boolean value and remove the parsed substring. */ + NODISCARD constexpr bool ToBoolAndTrim(); + + /** Converts a string into an integer value and remove the parsed substring. */ + template requires (!CSameAs && !CConst && !CVolatile) + NODISCARD constexpr U ToIntAndTrim(unsigned Base = 10); + + /** Converts a string into a floating-point value and remove the parsed substring. */ + template requires (!CConst && !CVolatile) + NODISCARD constexpr U ToFloatAndTrim(bool bFixed = true, bool bScientific = true); public: @@ -469,7 +578,14 @@ public: * @return The number of objects successfully parsed. */ template - size_t Parse(TStringView Fmt, Ts&... Args) const; + size_t Parse(TStringView Fmt, Ts&... Args) const + { + return TStringView(*this).ParseAndTrim(Fmt, Args...); + } + + /** Parse a string using a format string to objects and remove the parsed substring. */ + template + size_t ParseAndTrim(TStringView Fmt, Ts&... Args); public: