Files
UnrealEngine/Engine/Source/Editor/PropertyEditor/Private/StringPrefixTree.cpp
2025-05-18 13:04:45 +08:00

383 lines
7.1 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "StringPrefixTree.h"
FStringPrefixTree::~FStringPrefixTree()
{
Clear();
}
void FStringPrefixTree::Insert(FStringView ToAdd)
{
if (ToAdd.IsEmpty())
{
Root.IsLeaf = true;
return;
}
Root.Insert(ToAdd);
}
void FStringPrefixTree::InsertAll(const TArray<FString>& Array)
{
for (const FString& Entry : Array)
{
Insert(Entry);
}
}
void FStringPrefixTree::InsertAll(const TArray<FStringView>& Array)
{
for (const FStringView& Entry : Array)
{
Insert(Entry);
}
}
void FStringPrefixTree::Remove(FStringView ToRemove)
{
if (ToRemove.IsEmpty())
{
Root.IsLeaf = false;
return;
}
Root.Remove(ToRemove);
}
void FStringPrefixTree::Clear()
{
Root.Children.Reset();
Root.IsLeaf = false;
Root.Prefix = FString();
}
bool FStringPrefixTree::Contains(FStringView ToFind) const
{
if (ToFind.IsEmpty())
{
return Root.IsLeaf;
}
return Root.Contains(ToFind);
}
bool FStringPrefixTree::AnyStartsWith(FStringView Prefix) const
{
if (Prefix.IsEmpty())
{
return Root.IsLeaf || Root.Children.Num() > 0;
}
return Root.AnyStartsWith(Prefix);
}
int32 FStringPrefixTree::Size() const
{
return Root.Size();
}
int32 FStringPrefixTree::NumNodes() const
{
return Root.NumNodes();
}
TArray<FString> FStringPrefixTree::GetAllEntries() const
{
TArray<FString> Entries;
Root.GetAllEntries(FString(), Entries);
return MoveTemp(Entries);
}
FString FStringPrefixTree::DumpToString() const
{
TStringBuilder<512> Builder;
TArray<bool, TInlineAllocator<32>> DrawLines;
Root.DumpToString(Builder, DrawLines, false);
return Builder.ToString();
}
FStringPrefixTree::FNode::~FNode()
{
Children.Reset();
Prefix.Reset();
IsLeaf = false;
}
static int32 GetCommonPrefixLength(FStringView A, FStringView B)
{
int32 CharIdx = 0;
for (; CharIdx < A.Len() && CharIdx < B.Len(); ++CharIdx)
{
if (A[CharIdx] != B[CharIdx])
{
break;
}
}
return CharIdx;
}
void FStringPrefixTree::FNode::Insert(FStringView ToAdd)
{
bool bAdded = false;
// find a child that shares a prefix
for (FNode& Child : Children)
{
int32 CharIdx = GetCommonPrefixLength(Child.Prefix, ToAdd);
if (CharIdx > 0)
{
// the paths share a common prefix, there can be only one such node
bAdded = true;
if (CharIdx == Child.Prefix.Len())
{
// the prefix is the whole child path, we don't need to split the child node, but rather just add the postfix as a child
if (CharIdx == ToAdd.Len())
{
// the paths are identical, we don't need to add a node, just mark this as a leaf node
Child.IsLeaf = true;
break;
}
// eg. Child = Foo, ToAdd = Foo.Bar
Child.Insert(ToAdd.RightChop(CharIdx));
}
// this is a partial prefix of the child
// eg. Child = Foobar, ToAdd = Foo
else if (CharIdx < Child.Prefix.Len())
{
// modify the child node such that we take the common prefix and insert a node above it
// eg. Child = "Foobar", ToAdd = "Foo",
// Child -> "Foo", Split -> "bar"
FNode Split;
Split.Prefix = Child.Prefix.RightChop(CharIdx);
check(Split.Prefix.Len() > 0);
Split.Children = MoveTemp(Child.Children);
Split.IsLeaf = Child.IsLeaf;
Child.Prefix.LeftInline(CharIdx);
check(Child.Prefix.Len() > 0);
Child.Children.Reset();
Child.Children.Add(MoveTemp(Split));
if (CharIdx < ToAdd.Len())
{
// this was a partial match that still has leftover chars,
// eg. Child = "Foo", ToAdd = "Foz"
// Child -> "Fo", Split -> "o", Added -> "z"
FNode& AddedNode = Child.Children.AddDefaulted_GetRef();
AddedNode.Prefix = ToAdd.RightChop(CharIdx);
check(AddedNode.Prefix.Len() > 0);
AddedNode.IsLeaf = true;
Child.IsLeaf = false;
}
else
{
// this was a full match, eg. Child = "Foobar", ToAdd = "Foo"
// this is a leaf because this represents ToAdd now
Child.IsLeaf = true;
}
}
break;
}
}
if (!bAdded)
{
FNode& NewChild = Children.AddDefaulted_GetRef();
NewChild.Prefix = ToAdd;
check(NewChild.Prefix.Len() > 0);
NewChild.IsLeaf = true;
}
}
/**
* Remove an entry. This can fragment the tree and should be avoided.
*/
void FStringPrefixTree::FNode::Remove(FStringView ToRemove)
{
for (int32 Idx = 0; Idx < Children.Num(); ++Idx)
{
FNode& Child = Children[Idx];
if (ToRemove.StartsWith(Child.Prefix))
{
if (ToRemove.Len() == Child.Prefix.Len())
{
// matched the entire string
Child.IsLeaf = false;
}
else
{
Child.Remove(ToRemove.RightChop(Child.Prefix.Len()));
}
if (Child.CanPrune())
{
Children.RemoveAt(Idx);
}
break;
}
}
}
bool FStringPrefixTree::FNode::Contains(FStringView ToFind) const
{
for (const FNode& Child : Children)
{
if (ToFind.StartsWith(Child.Prefix))
{
if (Child.Prefix.Len() == ToFind.Len())
{
// matched the entire string
return Child.IsLeaf;
}
return Child.Contains(ToFind.RightChop(Child.Prefix.Len()));
}
}
return false;
}
bool FStringPrefixTree::FNode::AnyStartsWith(FStringView ToFind) const
{
for (const FNode& Child : Children)
{
int32 CharIdx = GetCommonPrefixLength(Child.Prefix, ToFind);
if (CharIdx > 0)
{
if (CharIdx <= Child.Prefix.Len())
{
// identical strings or child is longer than the prefix we're searching for
return true;
}
return Child.AnyStartsWith(ToFind.RightChop(Child.Prefix.Len()));
}
}
return false;
}
void FStringPrefixTree::FNode::GetAllEntries(const FString& CurrentPrefix, TArray<FString>& OutEntries) const
{
FString NewPrefix = CurrentPrefix + Prefix;
if (IsLeaf)
{
OutEntries.Add(NewPrefix);
}
for (const FNode& Child : Children)
{
Child.GetAllEntries(NewPrefix, OutEntries);
}
}
int32 FStringPrefixTree::FNode::Size() const
{
int32 Result = 0;
for (const FNode& Child : Children)
{
// only leaf children count
if (Child.IsLeaf)
{
++Result;
}
Result += Child.Size();
}
return Result;
}
int32 FStringPrefixTree::FNode::NumNodes() const
{
int32 Result = 1;
for (const FNode& Child : Children)
{
Result += Child.NumNodes();
}
return Result;
}
void FStringPrefixTree::FNode::DumpToString(FStringBuilderBase& Builder, TArray<bool, TInlineAllocator<32>>& DrawLines, bool bLastChild) const
{
for (int32 Level = 0; Level < DrawLines.Num(); ++Level)
{
if (DrawLines[Level])
{
Builder += TEXT("|");
}
else
{
Builder += TEXT(" ");
}
if (Level != DrawLines.Num() - 1)
{
Builder += TEXT(" ");
}
}
if (bLastChild)
{
DrawLines.Last() = false;
}
if (Prefix.IsEmpty())
{
Builder += TEXT("\"\"");
}
else
{
Builder += TEXT("- ");
Builder += Prefix;
}
if (IsLeaf)
{
Builder += TEXT("*");
}
Builder += TEXT("\n");
for (int32 Idx = 0; Idx < Children.Num(); ++Idx)
{
DrawLines.Push(true);
Children[Idx].DumpToString(Builder, DrawLines, Idx == Children.Num() - 1);
DrawLines.Pop();
}
}
// we can prune any node that is not a leaf and does not contain any children which are leaves anymore
bool FStringPrefixTree::FNode::CanPrune() const
{
if (IsLeaf)
{
return false;
}
for (const FNode& Child : Children)
{
if (!Child.CanPrune())
{
return false;
}
}
return true;
}