Files
ProjectEleri/Plugins/DirectiveUtilities/Source/DirectiveUtilitiesRuntime/Private/Libraries/DirectiveUtilStringFunctionLibrary.cpp

542 lines
14 KiB
C++

// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilStringFunctionLibrary.h"
#include "Misc/Base64.h"
#include "Misc/Crc.h"
#include "Misc/Paths.h"
#include "Misc/SecureHash.h"
namespace
{
TArray<uint8> StringToUtf8Bytes(const FString& String)
{
const FTCHARToUTF8 Converter(*String, String.Len());
return TArray<uint8>(reinterpret_cast<const uint8*>(Converter.Get()), Converter.Length());
}
int32 CalculateLevenshteinDistance(
FStringView Left,
FStringView Right,
TArray<int32>& PreviousRow,
TArray<int32>& CurrentRow)
{
int32 Start = 0;
while (Start < Left.Len() && Start < Right.Len() && Left[Start] == Right[Start])
{
++Start;
}
int32 LeftEnd = Left.Len();
int32 RightEnd = Right.Len();
while (LeftEnd > Start && RightEnd > Start && Left[LeftEnd - 1] == Right[RightEnd - 1])
{
--LeftEnd;
--RightEnd;
}
int32 LeftLength = LeftEnd - Start;
int32 RightLength = RightEnd - Start;
int32 LeftStart = Start;
int32 RightStart = Start;
if (RightLength > LeftLength)
{
Swap(Left, Right);
Swap(LeftLength, RightLength);
Swap(LeftStart, RightStart);
}
if (RightLength == 0)
{
return LeftLength;
}
PreviousRow.SetNumUninitialized(RightLength + 1);
CurrentRow.SetNumUninitialized(RightLength + 1);
for (int32 ColumnIndex = 0; ColumnIndex <= RightLength; ++ColumnIndex)
{
PreviousRow[ColumnIndex] = ColumnIndex;
}
for (int32 RowIndex = 1; RowIndex <= LeftLength; ++RowIndex)
{
CurrentRow[0] = RowIndex;
for (int32 ColumnIndex = 1; ColumnIndex <= RightLength; ++ColumnIndex)
{
const int32 SubstitutionCost = Left[LeftStart + RowIndex - 1] == Right[RightStart + ColumnIndex - 1] ? 0 : 1;
CurrentRow[ColumnIndex] = FMath::Min3(
PreviousRow[ColumnIndex] + 1,
CurrentRow[ColumnIndex - 1] + 1,
PreviousRow[ColumnIndex - 1] + SubstitutionCost);
}
Swap(PreviousRow, CurrentRow);
}
return PreviousRow[RightLength];
}
float CalculateStringSimilarity(
const FStringView Left,
const FStringView Right,
const int32 MaxLength,
TArray<int32>& PreviousRow,
TArray<int32>& CurrentRow)
{
if (MaxLength == 0)
{
return 1.0f;
}
const int32 Distance = CalculateLevenshteinDistance(Left, Right, PreviousRow, CurrentRow);
return 1.0f - static_cast<float>(Distance) / static_cast<float>(MaxLength);
}
}
bool UDirectiveUtilStringFunctionLibrary::ContainsLetters(const FString& String)
{
for (const TCHAR& Char : String)
{
if (FChar::IsAlpha(Char))
{
return true;
}
}
return false;
}
bool UDirectiveUtilStringFunctionLibrary::ContainsNumbers(const FString& String)
{
for (const TCHAR& Char : String)
{
if (FChar::IsDigit(Char))
{
return true;
}
}
return false;
}
bool UDirectiveUtilStringFunctionLibrary::ContainsSpaces(const FString& String)
{
for (const TCHAR& Char : String)
{
if (FChar::IsWhitespace(Char))
{
return true;
}
}
return false;
}
bool UDirectiveUtilStringFunctionLibrary::ContainsSpecialCharacters(const FString& String)
{
for (const TCHAR& Char : String)
{
if (FChar::IsPunct(Char))
{
return true;
}
}
return false;
}
FString UDirectiveUtilStringFunctionLibrary::FilterCharacters(
const FString& String,
const bool bLetters,
const bool bNumbers,
const bool bSpecialCharacters,
const bool bSpaces)
{
FString NewString;
NewString.Reserve(String.Len());
for (const TCHAR& Char : String)
{
if (bLetters && FChar::IsAlpha(Char)) continue;
if (bNumbers && FChar::IsDigit(Char)) continue;
if (bSpecialCharacters && FChar::IsPunct(Char)) continue;
if (bSpaces && FChar::IsWhitespace(Char)) continue;
NewString.AppendChar(Char);
}
return NewString;
}
FString UDirectiveUtilStringFunctionLibrary::TruncateString(const FString& String, const int32 MaxLength, const FString& Suffix)
{
if (String.Len() <= MaxLength)
{
return String;
}
return String.Left(FMath::Max(0, MaxLength - Suffix.Len())) + Suffix;
}
FString UDirectiveUtilStringFunctionLibrary::ToTitleCase(const FString& String)
{
FString Result;
Result.Reserve(String.Len());
bool bCapitalizeNext = true;
for (const TCHAR& Char : String)
{
if (FChar::IsWhitespace(Char))
{
bCapitalizeNext = true;
Result.AppendChar(Char);
}
else if (bCapitalizeNext && FChar::IsAlpha(Char))
{
Result.AppendChar(FChar::ToUpper(Char));
bCapitalizeNext = false;
}
else
{
Result.AppendChar(FChar::ToLower(Char));
bCapitalizeNext = false;
}
}
return Result;
}
TArray<FString> UDirectiveUtilStringFunctionLibrary::SplitIntoWords(const FString& String)
{
TArray<FString> Words;
FString CurrentWord;
for (int32 Index = 0; Index < String.Len(); ++Index)
{
const TCHAR Char = String[Index];
if (!FChar::IsAlnum(Char))
{
if (!CurrentWord.IsEmpty())
{
Words.Add(MoveTemp(CurrentWord));
CurrentWord.Reset();
}
continue;
}
if (!CurrentWord.IsEmpty())
{
const TCHAR Previous = CurrentWord[CurrentWord.Len() - 1];
const bool bNextIsLower = Index + 1 < String.Len() && FChar::IsLower(String[Index + 1]);
const bool bBoundary =
(FChar::IsLower(Previous) && FChar::IsUpper(Char)) ||
(FChar::IsUpper(Previous) && FChar::IsUpper(Char) && bNextIsLower) ||
(FChar::IsAlpha(Previous) && FChar::IsDigit(Char)) ||
(FChar::IsDigit(Previous) && FChar::IsAlpha(Char));
if (bBoundary)
{
Words.Add(MoveTemp(CurrentWord));
CurrentWord.Reset();
}
}
CurrentWord.AppendChar(Char);
}
if (!CurrentWord.IsEmpty())
{
Words.Add(MoveTemp(CurrentWord));
}
return Words;
}
FString UDirectiveUtilStringFunctionLibrary::ToCamelCase(const FString& String)
{
const TArray<FString> Words = SplitIntoWords(String);
FString Result;
for (int32 Index = 0; Index < Words.Num(); ++Index)
{
FString Word = Words[Index].ToLower();
if (Index > 0)
{
Word[0] = FChar::ToUpper(Word[0]);
}
Result += Word;
}
return Result;
}
FString UDirectiveUtilStringFunctionLibrary::ToPascalCase(const FString& String)
{
FString Result;
for (const FString& Word : SplitIntoWords(String))
{
FString Cased = Word.ToLower();
Cased[0] = FChar::ToUpper(Cased[0]);
Result += Cased;
}
return Result;
}
FString UDirectiveUtilStringFunctionLibrary::ToSnakeCase(const FString& String)
{
return FString::Join(SplitIntoWords(String), TEXT("_")).ToLower();
}
FString UDirectiveUtilStringFunctionLibrary::ToKebabCase(const FString& String)
{
return FString::Join(SplitIntoWords(String), TEXT("-")).ToLower();
}
TArray<FString> UDirectiveUtilStringFunctionLibrary::SortStringArray(TArray<FString> StringArray)
{
Algo::Sort(StringArray);
return StringArray;
}
TArray<FString> UDirectiveUtilStringFunctionLibrary::GetSortedStringArray(const TArray<FString> StringArray)
{
TArray<FString> SortedArray = StringArray;
Algo::Sort(SortedArray);
return SortedArray;
}
int32 UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(const FString& A, const FString& B, const bool bCaseSensitive)
{
const FString NormalizedA = bCaseSensitive ? FString() : A.ToLower();
const FString NormalizedB = bCaseSensitive ? FString() : B.ToLower();
const FStringView ViewA = bCaseSensitive ? FStringView(A) : FStringView(NormalizedA);
const FStringView ViewB = bCaseSensitive ? FStringView(B) : FStringView(NormalizedB);
TArray<int32> PreviousRow;
TArray<int32> CurrentRow;
return CalculateLevenshteinDistance(ViewA, ViewB, PreviousRow, CurrentRow);
}
float UDirectiveUtilStringFunctionLibrary::GetStringSimilarity(const FString& A, const FString& B, const bool bCaseSensitive)
{
const FString NormalizedA = bCaseSensitive ? FString() : A.ToLower();
const FString NormalizedB = bCaseSensitive ? FString() : B.ToLower();
TArray<int32> PreviousRow;
TArray<int32> CurrentRow;
return CalculateStringSimilarity(
bCaseSensitive ? FStringView(A) : FStringView(NormalizedA),
bCaseSensitive ? FStringView(B) : FStringView(NormalizedB),
FMath::Max(A.Len(), B.Len()),
PreviousRow,
CurrentRow);
}
bool UDirectiveUtilStringFunctionLibrary::ContainsAny(const FString& Source, const TArray<FString>& SearchTerms, const bool bCaseSensitive)
{
const ESearchCase::Type SearchCase = bCaseSensitive ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase;
for (const FString& Term : SearchTerms)
{
if (!Term.IsEmpty() && Source.Contains(Term, SearchCase))
{
return true;
}
}
return false;
}
bool UDirectiveUtilStringFunctionLibrary::FindFirstOfAny(const FString& Source, const TArray<FString>& SearchTerms, const bool bCaseSensitive, int32& OutFoundIndex, int32& OutTermIndex)
{
OutFoundIndex = INDEX_NONE;
OutTermIndex = INDEX_NONE;
const ESearchCase::Type SearchCase = bCaseSensitive ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase;
for (int32 TermIndex = 0; TermIndex < SearchTerms.Num(); ++TermIndex)
{
const FString& Term = SearchTerms[TermIndex];
if (Term.IsEmpty())
{
continue;
}
const int32 FoundIndex = Source.Find(Term, SearchCase, ESearchDir::FromStart);
if (FoundIndex != INDEX_NONE && (OutFoundIndex == INDEX_NONE || FoundIndex < OutFoundIndex))
{
OutFoundIndex = FoundIndex;
OutTermIndex = TermIndex;
}
}
return OutFoundIndex != INDEX_NONE;
}
FString UDirectiveUtilStringFunctionLibrary::Base64Encode(const FString& Source)
{
return FBase64::Encode(Source);
}
bool UDirectiveUtilStringFunctionLibrary::Base64Decode(const FString& Source, FString& OutDecoded)
{
OutDecoded.Reset();
return FBase64::Decode(Source, OutDecoded);
}
FString UDirectiveUtilStringFunctionLibrary::HexEncode(const FString& String)
{
return HexEncodeBytes(StringToUtf8Bytes(String));
}
bool UDirectiveUtilStringFunctionLibrary::HexDecode(const FString& Hex, FString& OutString)
{
OutString.Reset();
TArray<uint8> Bytes;
if (!HexDecodeBytes(Hex, Bytes))
{
return false;
}
const FUTF8ToTCHAR Converter(reinterpret_cast<const ANSICHAR*>(Bytes.GetData()), Bytes.Num());
OutString = FString(Converter.Length(), Converter.Get());
return true;
}
FString UDirectiveUtilStringFunctionLibrary::HexEncodeBytes(const TArray<uint8>& Bytes)
{
return BytesToHexLower(Bytes.GetData(), Bytes.Num());
}
bool UDirectiveUtilStringFunctionLibrary::HexDecodeBytes(const FString& Hex, TArray<uint8>& OutBytes)
{
OutBytes.Reset();
if (Hex.Len() % 2 != 0)
{
return false;
}
OutBytes.Reserve(Hex.Len() / 2);
for (int32 Index = 0; Index < Hex.Len(); Index += 2)
{
const TCHAR High = Hex[Index];
const TCHAR Low = Hex[Index + 1];
if (!CheckTCharIsHex(High) || !CheckTCharIsHex(Low))
{
OutBytes.Reset();
return false;
}
OutBytes.Add(static_cast<uint8>((TCharToNibble(High) << 4) | TCharToNibble(Low)));
}
return true;
}
FString UDirectiveUtilStringFunctionLibrary::Md5HashString(const FString& String)
{
return Md5HashBytes(StringToUtf8Bytes(String));
}
FString UDirectiveUtilStringFunctionLibrary::Md5HashBytes(const TArray<uint8>& Bytes)
{
return FMD5::HashBytes(Bytes.GetData(), Bytes.Num());
}
FString UDirectiveUtilStringFunctionLibrary::Sha1HashString(const FString& String)
{
return Sha1HashBytes(StringToUtf8Bytes(String));
}
FString UDirectiveUtilStringFunctionLibrary::Sha1HashBytes(const TArray<uint8>& Bytes)
{
uint8 Digest[20];
FSHA1::HashBuffer(Bytes.GetData(), Bytes.Num(), Digest);
return BytesToHexLower(Digest, UE_ARRAY_COUNT(Digest));
}
int32 UDirectiveUtilStringFunctionLibrary::Crc32String(const FString& String)
{
return Crc32Bytes(StringToUtf8Bytes(String));
}
int32 UDirectiveUtilStringFunctionLibrary::Crc32Bytes(const TArray<uint8>& Bytes)
{
return static_cast<int32>(FCrc::MemCrc32(Bytes.GetData(), Bytes.Num()));
}
namespace
{
bool IsReservedDeviceFileName(const FString& FileName)
{
FString Stem = FileName;
int32 DotIndex = INDEX_NONE;
if (FileName.FindChar(TEXT('.'), DotIndex))
{
Stem = FileName.Left(DotIndex);
}
while (Stem.EndsWith(TEXT(".")) || Stem.EndsWith(TEXT(" ")))
{
Stem.LeftChopInline(1);
}
if (Stem.IsEmpty())
{
return false;
}
static const TCHAR* ReservedNames[] = {
TEXT("CON"), TEXT("PRN"), TEXT("AUX"), TEXT("CLOCK$"), TEXT("NUL"),
TEXT("COM1"), TEXT("COM2"), TEXT("COM3"), TEXT("COM4"), TEXT("COM5"),
TEXT("COM6"), TEXT("COM7"), TEXT("COM8"), TEXT("COM9"),
TEXT("LPT1"), TEXT("LPT2"), TEXT("LPT3"), TEXT("LPT4"), TEXT("LPT5"),
TEXT("LPT6"), TEXT("LPT7"), TEXT("LPT8"), TEXT("LPT9")
};
for (const TCHAR* ReservedName : ReservedNames)
{
if (Stem.Equals(ReservedName, ESearchCase::IgnoreCase))
{
return true;
}
}
return false;
}
}
bool UDirectiveUtilStringFunctionLibrary::IsValidFileName(const FString& String)
{
return !String.IsEmpty()
&& String != TEXT(".")
&& String != TEXT("..")
&& FPaths::GetCleanFilename(String) == String
&& SanitizeFileName(String) == String;
}
FString UDirectiveUtilStringFunctionLibrary::SanitizeFileName(const FString& String, const FString& Replacement)
{
FString Result = FPaths::MakeValidFileName(String, Replacement.IsEmpty() ? TEXT('\0') : Replacement[0]);
while (Result.EndsWith(TEXT(".")) || Result.EndsWith(TEXT(" ")))
{
Result.LeftChopInline(1);
}
if (IsReservedDeviceFileName(Result))
{
Result = FString(TEXT("_")) + Result;
}
return Result;
}
int32 UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(const FString& Input, const TArray<FString>& Candidates, float& OutSimilarity, const bool bCaseSensitive)
{
OutSimilarity = 0.0f;
int32 BestIndex = INDEX_NONE;
const FString NormalizedInput = bCaseSensitive ? FString() : Input.ToLower();
const FStringView InputView = bCaseSensitive ? FStringView(Input) : FStringView(NormalizedInput);
TArray<int32> PreviousRow;
TArray<int32> CurrentRow;
for (int32 Index = 0; Index < Candidates.Num(); ++Index)
{
const FString NormalizedCandidate = bCaseSensitive ? FString() : Candidates[Index].ToLower();
const FStringView CandidateView = bCaseSensitive
? FStringView(Candidates[Index])
: FStringView(NormalizedCandidate);
const float Similarity = CalculateStringSimilarity(
InputView,
CandidateView,
FMath::Max(Input.Len(), Candidates[Index].Len()),
PreviousRow,
CurrentRow);
if (BestIndex == INDEX_NONE || Similarity > OutSimilarity)
{
BestIndex = Index;
OutSimilarity = Similarity;
if (Similarity == 1.0f)
{
break;
}
}
}
return BestIndex;
}