// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License. #include "Libraries/DirectiveUtilArrayFunctionLibrary.h" #include "Async/ParallelFor.h" #include "Tests/DirectiveUtilTestObject.h" #include #include "Misc/AutomationTest.h" namespace { TArray BuildReferenceSample(const int32 SourceCount, const int32 Count, const int32 Seed) { TArray AvailableIndices; AvailableIndices.SetNumUninitialized(SourceCount); for (int32 Index = 0; Index < SourceCount; ++Index) { AvailableIndices[Index] = Index; } FRandomStream RandomStream(Seed); TArray Result; const int32 SampleCount = FMath::Min(SourceCount, Count); Result.Reserve(SampleCount); for (int32 SampleIndex = 0; SampleIndex < SampleCount; ++SampleIndex) { const int32 SelectedIndex = RandomStream.RandRange(0, AvailableIndices.Num() - 1); Result.Add(AvailableIndices[SelectedIndex]); AvailableIndices.RemoveAtSwap(SelectedIndex, 1, EAllowShrinking::No); } return Result; } TArray BuildReferenceDistinct(const TArray& Values) { TArray Result; for (const int32 Value : Values) { if (!Result.Contains(Value)) { Result.Add(Value); } } return Result; } TArray BuildReferenceRemoval( const TArray& Values, const TArray& Indices) { TArray Result; Result.Reserve(Values.Num()); for (int32 Index = 0; Index < Values.Num(); ++Index) { if (!Indices.Contains(Index)) { Result.Add(Values[Index]); } } return Result; } } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilArrayFunctionLibraryTest, "DirectiveUtilities.ArrayFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter) bool FDirectiveUtilArrayFunctionLibraryTest::RunTest(const FString& Parameters) { UDirectiveUtilTestObject* TestObject = NewObject(); TestObject->TestArray = {1, 2, 3, 4, 5}; FArrayProperty* ArrayProperty = FindFProperty(UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestArray)); const int32 NextIndex = UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, 2, false); const int32 PreviousIndex = UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, 2, false); TestEqual("Array_NextIndex should return the next index in the array", NextIndex, 3); TestEqual("Array_PreviousIndex should return the previous index in the array", PreviousIndex, 1); const int32 NextIndexLooped = UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, 4, true); const int32 PreviousIndexLooped = UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, 0, true); TestEqual("Array_NextIndex should return the first index when looping", NextIndexLooped, 0); TestEqual("Array_PreviousIndex should return the last index when looping", PreviousIndexLooped, 4); const int32 NextIndexNonLooped = UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, 4, false); const int32 PreviousIndexNonLooped = UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, 0, false); TestEqual("Array_NextIndex should return the last index when not looping", NextIndexNonLooped, 4); TestEqual("Array_PreviousIndex should return the first index when not looping", PreviousIndexNonLooped, 0); const int32 NextIndexOutOfBounds = UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, 5, false); const int32 PreviousIndexOutOfBounds = UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, -1, false); TestEqual("Array_NextIndex should return the last index when out of bounds", NextIndexOutOfBounds, 4); TestEqual("Array_PreviousIndex should return the first index when out of bounds", PreviousIndexOutOfBounds, 0); TestEqual("Array_NextIndex should clamp a negative input index to 0 (no loop)", UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, -2, false), 0); TestEqual("Array_NextIndex should clamp a negative input index to 0 (loop)", UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, -2, true), 0); TestEqual("Array_NextIndex should wrap a large input index to 0 when looping", UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, 10, true), 0); TestEqual("Array_NextIndex should clamp a large input index to the last index when not looping", UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, 10, false), 4); TestEqual("Array_PreviousIndex should clamp a large input index to the last index (no loop)", UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, 10, false), 4); TestEqual("Array_PreviousIndex should clamp a large input index to the last index (loop)", UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, 10, true), 4); TestEqual("Array_PreviousIndex should wrap a negative input index to the last index when looping", UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, -1, true), 4); TestEqual("Array_NextIndex should handle the maximum integer index", UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, MAX_int32, false), 4); TestEqual("Array_PreviousIndex should handle the minimum integer index", UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, MIN_int32, false), 0); TestObject->TestArray = {1, 2, 3, 4, 5}; int32 FirstItem = -1; const bool bGotFirst = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetFirstItem(&TestObject->TestArray, ArrayProperty, &FirstItem); TestTrue("GetFirstItem should succeed on a non-empty array", bGotFirst); TestEqual("GetFirstItem should return the first element", FirstItem, 1); int32 LastItem = -1; const bool bGotLast = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetLastItem(&TestObject->TestArray, ArrayProperty, &LastItem); TestTrue("GetLastItem should succeed on a non-empty array", bGotLast); TestEqual("GetLastItem should return the last element", LastItem, 5); int32 IndexItem = -1; const bool bGotIndex = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetItemAtIndex(&TestObject->TestArray, ArrayProperty, 2, &IndexItem); TestTrue("GetItemAtIndex should succeed for a valid index", bGotIndex); TestEqual("GetItemAtIndex should return the element at the index", IndexItem, 3); int32 OutOfRangeItem = 777; const bool bGotOutOfRange = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetItemAtIndex(&TestObject->TestArray, ArrayProperty, 10, &OutOfRangeItem); TestFalse("GetItemAtIndex should fail for an out-of-range index", bGotOutOfRange); TestEqual("GetItemAtIndex should reset the output to default on failure", OutOfRangeItem, 0); int32 RandomItem = -1; int32 RandomIndex = -1; const bool bGotRandom = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetRandomItem(&TestObject->TestArray, ArrayProperty, &RandomItem, &RandomIndex); TestTrue("GetRandomItem should succeed on a non-empty array", bGotRandom); TestTrue("GetRandomItem should return a valid index", TestObject->TestArray.IsValidIndex(RandomIndex)); if (TestObject->TestArray.IsValidIndex(RandomIndex)) { TestEqual("GetRandomItem item should match the element at the returned index", RandomItem, TestObject->TestArray[RandomIndex]); } TestObject->TestArray.Empty(); int32 EmptyItem = 999; TestFalse("GetFirstItem should fail on an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetFirstItem(&TestObject->TestArray, ArrayProperty, &EmptyItem)); TestEqual("GetFirstItem should reset the output to default on an empty array", EmptyItem, 0); EmptyItem = 999; TestFalse("GetLastItem should fail on an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetLastItem(&TestObject->TestArray, ArrayProperty, &EmptyItem)); TestEqual("GetLastItem should reset the output to default on an empty array", EmptyItem, 0); int32 EmptyRandomItem = 999; int32 EmptyRandomIndex = 5; TestFalse("GetRandomItem should fail on an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetRandomItem(&TestObject->TestArray, ArrayProperty, &EmptyRandomItem, &EmptyRandomIndex)); TestEqual("GetRandomItem should return INDEX_NONE on an empty array", EmptyRandomIndex, static_cast(INDEX_NONE)); TestEqual("GetRandomItem should reset the output to default on an empty array", EmptyRandomItem, 0); TestObject->TestArray = {1, 2, 3}; int32 PoppedItem = -1; const bool bPopped = UDirectiveUtilArrayFunctionLibrary::GenericArray_Pop(&TestObject->TestArray, ArrayProperty, &PoppedItem); TestTrue("Pop should succeed on a non-empty array", bPopped); TestEqual("Pop should return the last element", PoppedItem, 3); TestEqual("Pop should shrink the array by one", TestObject->TestArray.Num(), 2); TestEqual("Pop should leave the new last element intact", TestObject->TestArray.Last(), 2); TestObject->TestArray = {1, 2, 3}; int32 PoppedFirst = -1; const bool bPoppedFirst = UDirectiveUtilArrayFunctionLibrary::GenericArray_PopFirst(&TestObject->TestArray, ArrayProperty, &PoppedFirst); TestTrue("PopFirst should succeed on a non-empty array", bPoppedFirst); TestEqual("PopFirst should return the first element", PoppedFirst, 1); TestEqual("PopFirst should shrink the array by one", TestObject->TestArray.Num(), 2); TestEqual("PopFirst should shift the remaining elements down", TestObject->TestArray[0], 2); TestObject->TestArray.Empty(); int32 PoppedEmpty = 888; TestFalse("Pop should fail on an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_Pop(&TestObject->TestArray, ArrayProperty, &PoppedEmpty)); TestEqual("Pop should reset the output to default on an empty array", PoppedEmpty, 0); TestFalse("PopFirst should fail on an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_PopFirst(&TestObject->TestArray, ArrayProperty, &PoppedEmpty)); TestObject->TestArray = {10, 20, 30, 40}; const bool bRemoved = UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtSwap(&TestObject->TestArray, ArrayProperty, 1); TestTrue("RemoveAtSwap should succeed for a valid index", bRemoved); TestEqual("RemoveAtSwap should shrink the array by one", TestObject->TestArray.Num(), 3); TestFalse("RemoveAtSwap should have removed the target element", TestObject->TestArray.Contains(20)); TestEqual("RemoveAtSwap should move the previously-last element into the removed slot", TestObject->TestArray[1], 40); const bool bRemovedOutOfRange = UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtSwap(&TestObject->TestArray, ArrayProperty, 99); TestFalse("RemoveAtSwap should fail for an out-of-range index", bRemovedOutOfRange); TestEqual("RemoveAtSwap should not change the array on failure", TestObject->TestArray.Num(), 3); TestObject->TestArray = {10, 20, 30}; const bool bRemovedLast = UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtSwap(&TestObject->TestArray, ArrayProperty, 2); TestTrue("RemoveAtSwap should succeed when removing the last element", bRemovedLast); TestEqual("RemoveAtSwap on the last element should shrink the array", TestObject->TestArray.Num(), 2); TestEqual("RemoveAtSwap on the last element should preserve the order of the rest", TestObject->TestArray[1], 20); int32 ItemToRemove = 2; TestObject->TestArray = {1, 2, 3, 2, 4, 2}; TestTrue( "RemoveAllOccurrences should report matching values", UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(&TestObject->TestArray, ArrayProperty, &ItemToRemove)); TestEqual( "RemoveAllOccurrences should remove every match and preserve survivor order", TestObject->TestArray, TArray({1, 3, 4})); ItemToRemove = 9; const TArray UnchangedValues = TestObject->TestArray; TestFalse( "RemoveAllOccurrences should report an absent value", UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(&TestObject->TestArray, ArrayProperty, &ItemToRemove)); TestEqual("RemoveAllOccurrences should not change an array without matches", TestObject->TestArray, UnchangedValues); TestObject->TestArray.Empty(); TestFalse( "RemoveAllOccurrences should report false for an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(&TestObject->TestArray, ArrayProperty, &ItemToRemove)); ItemToRemove = 5; TestObject->TestArray = {5, 5, 5}; TestTrue( "RemoveAllOccurrences should remove an all-matching array", UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(&TestObject->TestArray, ArrayProperty, &ItemToRemove)); TestTrue("RemoveAllOccurrences should leave an all-matching array empty", TestObject->TestArray.IsEmpty()); TestObject->TestArray = {0, 1, 2, 3, 4, 5, 6}; TestEqual( "RemoveAtIndices should remove each valid index once", UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtIndices( &TestObject->TestArray, ArrayProperty, TArray({5, 1, 5, -1, 99, 3})), 3); TestEqual( "RemoveAtIndices should preserve survivor order", TestObject->TestArray, TArray({0, 2, 4, 6})); const TArray BeforeInvalidIndices = TestObject->TestArray; TestEqual( "RemoveAtIndices should report zero for invalid indices", UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtIndices( &TestObject->TestArray, ArrayProperty, TArray({-5, 10, 10})), 0); TestEqual( "RemoveAtIndices should not modify the target when no indices are valid", TestObject->TestArray, BeforeInvalidIndices); TestObject->TestArray = {10, 20, 30}; TestEqual( "RemoveAtIndices should remove the full array", UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtIndices( &TestObject->TestArray, ArrayProperty, TArray({2, 0, 1, 1})), 3); TestTrue("RemoveAtIndices should leave the array empty when every index is removed", TestObject->TestArray.IsEmpty()); TestObject->TestArray = {0, 2, 4}; TestEqual( "RemoveAtIndices should support using the target array as the index array", UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtIndices( &TestObject->TestArray, ArrayProperty, TestObject->TestArray), 2); TestEqual( "RemoveAtIndices should read aliased indices before modifying the target", TestObject->TestArray, TArray({2})); TestTrue("Integer arrays should use the bulk append path", ArrayProperty->Inner->HasAnyPropertyFlags(CPF_IsPlainOldData)); const TArray IntegerAppendSource = {3, 4, 5}; TestObject->TestArray = {1, 2}; UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized( &TestObject->TestArray, ArrayProperty, &IntegerAppendSource, ArrayProperty); TestEqual( "AppendOptimized should append POD values in order", TestObject->TestArray, TArray({1, 2, 3, 4, 5})); TestEqual( "AppendOptimized should not modify the source array", IntegerAppendSource, TArray({3, 4, 5})); TestObject->TestArray.Empty(); UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized( &TestObject->TestArray, ArrayProperty, &IntegerAppendSource, ArrayProperty); TestEqual( "AppendOptimized should append into an empty target array", TestObject->TestArray, IntegerAppendSource); const TArray EmptyAppendSource; const TArray BeforeEmptyAppend = TestObject->TestArray; UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized( &TestObject->TestArray, ArrayProperty, &EmptyAppendSource, ArrayProperty); TestEqual("AppendOptimized should ignore an empty source array", TestObject->TestArray, BeforeEmptyAppend); TestObject->TestArray = {7, 8, 9}; UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized( &TestObject->TestArray, ArrayProperty, &TestObject->TestArray, ArrayProperty); TestEqual( "AppendOptimized should support appending an array to itself", TestObject->TestArray, TArray({7, 8, 9, 7, 8, 9})); const TArray IntegerInsertSource = {7, 8}; TestObject->TestArray = {1, 2, 3}; TestTrue( "InsertOptimized should insert POD values", UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized( &TestObject->TestArray, ArrayProperty, &IntegerInsertSource, ArrayProperty, 1)); TestEqual( "InsertOptimized should preserve target and source order", TestObject->TestArray, TArray({1, 7, 8, 2, 3})); TestEqual( "InsertOptimized should not modify the source array", IntegerInsertSource, TArray({7, 8})); TestObject->TestArray = {1, 2, 3}; TestTrue( "InsertOptimized should insert at the front", UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized( &TestObject->TestArray, ArrayProperty, &IntegerInsertSource, ArrayProperty, 0)); TestEqual( "InsertOptimized should preserve order at the front", TestObject->TestArray, TArray({7, 8, 1, 2, 3})); TestObject->TestArray = {1, 2, 3}; TestTrue( "InsertOptimized should insert at the end", UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized( &TestObject->TestArray, ArrayProperty, &IntegerInsertSource, ArrayProperty, TestObject->TestArray.Num())); TestEqual( "InsertOptimized should preserve order at the end", TestObject->TestArray, TArray({1, 2, 3, 7, 8})); const TArray EmptyInsertSource; const TArray BeforeEmptyInsert = TestObject->TestArray; TestFalse( "InsertOptimized should report an empty source", UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized( &TestObject->TestArray, ArrayProperty, &EmptyInsertSource, ArrayProperty, 1)); TestEqual("InsertOptimized should ignore an empty source", TestObject->TestArray, BeforeEmptyInsert); TestFalse( "InsertOptimized should reject a negative index", UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized( &TestObject->TestArray, ArrayProperty, &IntegerInsertSource, ArrayProperty, -1)); TestEqual("InsertOptimized should not modify the target for a negative index", TestObject->TestArray, BeforeEmptyInsert); TestFalse( "InsertOptimized should reject an index beyond the array end", UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized( &TestObject->TestArray, ArrayProperty, &IntegerInsertSource, ArrayProperty, TestObject->TestArray.Num() + 1)); TestEqual("InsertOptimized should not modify the target for a large index", TestObject->TestArray, BeforeEmptyInsert); TestObject->TestArray = {4, 5, 6}; TestTrue( "InsertOptimized should support inserting an array into itself", UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized( &TestObject->TestArray, ArrayProperty, &TestObject->TestArray, ArrayProperty, 1)); TestEqual( "InsertOptimized should preserve self-inserted values", TestObject->TestArray, TArray({4, 4, 5, 6, 5, 6})); TestObject->TestArray = {1, 2, 2, 3, 1, 4}; UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestArray, ArrayProperty); TestEqual("RemoveDuplicates should remove all duplicate entries", TestObject->TestArray.Num(), 4); if (TestObject->TestArray.Num() == 4) { TestEqual("RemoveDuplicates should keep the first occurrence (index 0)", TestObject->TestArray[0], 1); TestEqual("RemoveDuplicates should keep the first occurrence (index 1)", TestObject->TestArray[1], 2); TestEqual("RemoveDuplicates should keep the first occurrence (index 2)", TestObject->TestArray[2], 3); TestEqual("RemoveDuplicates should keep the first occurrence (index 3)", TestObject->TestArray[3], 4); } TestObject->TestArray = {5, 5, 5}; UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestArray, ArrayProperty); TestEqual("RemoveDuplicates should collapse an all-duplicates array to one element", TestObject->TestArray.Num(), 1); if (TestObject->TestArray.Num() == 1) { TestEqual("RemoveDuplicates should keep the single remaining value", TestObject->TestArray[0], 5); } TestObject->TestArray.Empty(); UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestArray, ArrayProperty); TestEqual("RemoveDuplicates on an empty array should leave it empty", TestObject->TestArray.Num(), 0); TestObject->TestArray = {10, 20, 30, 40, 50}; TArray SliceOut; UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(&TestObject->TestArray, ArrayProperty, 1, 3, &SliceOut, ArrayProperty); TestEqual("Slice should copy a contiguous range", SliceOut, TArray({20, 30, 40})); UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(&TestObject->TestArray, ArrayProperty, 3, 99, &SliceOut, ArrayProperty); TestEqual("Slice should clamp Count to the available elements", SliceOut, TArray({40, 50})); UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(&TestObject->TestArray, ArrayProperty, -5, 2, &SliceOut, ArrayProperty); TestEqual("Slice should clamp a negative start index to 0", SliceOut, TArray({10, 20})); UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(&TestObject->TestArray, ArrayProperty, 0, 0, &SliceOut, ArrayProperty); TestEqual("Slice with Count 0 should be empty", SliceOut.Num(), 0); TestObject->TestArray = {10, 20, 30, 40}; UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(&TestObject->TestArray, ArrayProperty, 1, 2, &TestObject->TestArray, ArrayProperty); TestEqual("Slice should support using the source array as its output", TestObject->TestArray, TArray({20, 30})); TestObject->TestArray = {1, 2, 3, 4, 5}; UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&TestObject->TestArray, ArrayProperty, 2); TestEqual("Rotate by +2 should rotate toward the end", TestObject->TestArray, TArray({4, 5, 1, 2, 3})); TestObject->TestArray = {1, 2, 3, 4, 5}; UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&TestObject->TestArray, ArrayProperty, -1); TestEqual("Rotate by -1 should rotate toward the start", TestObject->TestArray, TArray({2, 3, 4, 5, 1})); TestObject->TestArray = {1, 2, 3}; UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&TestObject->TestArray, ArrayProperty, 3); TestEqual("Rotate by Length should be a no-op", TestObject->TestArray, TArray({1, 2, 3})); TestObject->TestArray = {1, 2, 2, 3, 1, 4}; TArray DistinctOut; UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(&TestObject->TestArray, ArrayProperty, &DistinctOut, ArrayProperty); TestEqual("GetDistinct should keep first occurrences in order", DistinctOut, TArray({1, 2, 3, 4})); TestEqual("GetDistinct should not modify the source array", TestObject->TestArray.Num(), 6); TestObject->TestArray = {1, 2, 2, 3, 1}; UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(&TestObject->TestArray, ArrayProperty, &TestObject->TestArray, ArrayProperty); TestEqual("GetDistinct should support using the source array as its output", TestObject->TestArray, TArray({1, 2, 3})); FArrayProperty* DistinctStringArrayProperty = FindFProperty(UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestStringArray)); TestNotNull("TestStringArray property should be found", DistinctStringArrayProperty); if (DistinctStringArrayProperty) { TestObject->TestStringArray = {TEXT("Alpha"), TEXT("Beta"), TEXT("Beta")}; UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct( &TestObject->TestStringArray, DistinctStringArrayProperty, &TestObject->TestStringArray, DistinctStringArrayProperty); TestEqual("GetDistinct should preserve in-place string values", TestObject->TestStringArray, TArray({TEXT("Alpha"), TEXT("Beta")})); } TestObject->TestArray = {5, 1, 5, 2, 5, 3}; int32 ItemToCount = 5; TestEqual("CountOccurrences should count matches", UDirectiveUtilArrayFunctionLibrary::GenericArray_CountOccurrences(&TestObject->TestArray, ArrayProperty, &ItemToCount), 3); int32 MissingItem = 99; TestEqual("CountOccurrences should return 0 for an absent item", UDirectiveUtilArrayFunctionLibrary::GenericArray_CountOccurrences(&TestObject->TestArray, ArrayProperty, &MissingItem), 0); TestObject->TestArray = {7, 7, 8, 7, 9, 8}; int32 MostCommonItem = -1; int32 MostCommonCount = -1; const bool bGotMostCommon = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon(&TestObject->TestArray, ArrayProperty, &MostCommonItem, &MostCommonCount); TestTrue("GetMostCommon should succeed on a non-empty array", bGotMostCommon); TestEqual("GetMostCommon should return the most frequent element", MostCommonItem, 7); TestEqual("GetMostCommon should return the occurrence count", MostCommonCount, 3); TestObject->TestArray.Empty(); int32 EmptyMostItem = 5; int32 EmptyMostCount = 5; TestFalse("GetMostCommon should fail on an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon(&TestObject->TestArray, ArrayProperty, &EmptyMostItem, &EmptyMostCount)); TestEqual("GetMostCommon should reset the count on an empty array", EmptyMostCount, 0); const TArray MutatingFunctions = { GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveDuplicates), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_AppendOptimized), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_InsertOptimized), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_Pop), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_PopFirst), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveAtSwap), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveAtIndices), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveAllOccurrences), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_Rotate) }; for (const FName FunctionName : MutatingFunctions) { const UFunction* Function = UDirectiveUtilArrayFunctionLibrary::StaticClass()->FindFunctionByName(FunctionName); const FArrayProperty* TargetArrayProperty = Function ? FindFProperty(Function, TEXT("TargetArray")) : nullptr; TestNotNull(*FString::Printf(TEXT("%s should expose a TargetArray parameter"), *FunctionName.ToString()), TargetArrayProperty); if (TargetArrayProperty) { TestFalse(*FString::Printf(TEXT("%s should expose TargetArray as mutable"), *FunctionName.ToString()), TargetArrayProperty->HasAnyPropertyFlags(CPF_ConstParm)); } #if WITH_EDITOR if (Function) { TestFalse( *FString::Printf(TEXT("%s should not advertise inert Blueprint thread safety"), *FunctionName.ToString()), Function->HasMetaData(TEXT("BlueprintThreadSafe"))); } #endif } TArray ParallelResults; ParallelResults.SetNumZeroed(64); ParallelFor(ParallelResults.Num(), [&ParallelResults, ArrayProperty](const int32 TaskIndex) { const int32 RemovedValue = TaskIndex + 1000; TArray Values = {RemovedValue, 7, RemovedValue, 11}; const TArray Appended = {13, RemovedValue}; UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized( &Values, ArrayProperty, &Appended, ArrayProperty); const bool bRemoved = UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences( &Values, ArrayProperty, &RemovedValue); ParallelResults[TaskIndex] = bRemoved && Values == TArray({7, 11, 13}) ? 1 : 0; }); TestTrue("Independent array operations should remain correct on worker tasks", ParallelResults.Find(0) == INDEX_NONE); TestObject->TestArray = {10, 20, 30, 40, 50}; TArray SampledValues; FRandomStream FirstStream(1337); UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 3, false, &FirstStream, &SampledValues, ArrayProperty); TestEqual("Sample without replacement should return the requested count", SampledValues.Num(), 3); TestEqual("Sample without replacement should contain unique values", TSet(SampledValues).Num(), 3); TArray RepeatedSample; FRandomStream SecondStream(1337); UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 3, false, &SecondStream, &RepeatedSample, ArrayProperty); TestEqual("Sample from stream should be deterministic", RepeatedSample, SampledValues); FRandomStream ClampedStream(42); UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 20, false, &ClampedStream, &SampledValues, ArrayProperty); TestEqual("Sample without replacement should clamp to the source length", SampledValues.Num(), TestObject->TestArray.Num()); FRandomStream ReplacementStream(7); UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 20, true, &ReplacementStream, &SampledValues, ArrayProperty); TestEqual("Sample with replacement should return the requested count", SampledValues.Num(), 20); TestObject->TestArray = {99}; FRandomStream SingleValueStream(7); UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 4, true, &SingleValueStream, &SampledValues, ArrayProperty); TestEqual("Sample with replacement should repeat the only available value", SampledValues, TArray({99, 99, 99, 99})); TestObject->TestArray.Empty(); UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 4, false, nullptr, &SampledValues, ArrayProperty); TestTrue("Sampling an empty array should return an empty array", SampledValues.IsEmpty()); TestObject->TestArray = {10, 20, 30, 40, 50}; UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 0, false, nullptr, &SampledValues, ArrayProperty); TestTrue("Sampling zero values should return an empty array", SampledValues.IsEmpty()); UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 1'000'001, true, nullptr, &SampledValues, ArrayProperty); TestTrue("Sampling should reject an unsafe output count", SampledValues.IsEmpty()); const TArray DeterministicWeights = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; TArray WeightedSample; TArray RepeatedWeightedSample; FRandomStream FirstWeightedStream(90210); FRandomStream SecondWeightedStream(90210); TestTrue( "Weighted sampling should accept matching inputs", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, DeterministicWeights, 4, true, &FirstWeightedStream, &WeightedSample, ArrayProperty)); TestTrue( "Weighted sampling from a stream should accept matching inputs", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, DeterministicWeights, 4, true, &SecondWeightedStream, &RepeatedWeightedSample, ArrayProperty)); TestEqual("Weighted sampling from a stream should be deterministic", RepeatedWeightedSample, WeightedSample); TestFalse( "Weighted sampling should reject an unsafe output count", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, DeterministicWeights, 1'000'001, true, nullptr, &WeightedSample, ArrayProperty)); TestTrue("Rejected weighted sampling should clear the output", WeightedSample.IsEmpty()); FRandomStream FirstUniqueWeightedStream(31415); FRandomStream SecondUniqueWeightedStream(31415); TestTrue( "Weighted sampling without replacement should accept matching inputs", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, DeterministicWeights, 5, false, &FirstUniqueWeightedStream, &WeightedSample, ArrayProperty)); TestTrue( "Repeated weighted sampling without replacement should accept matching inputs", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, DeterministicWeights, 5, false, &SecondUniqueWeightedStream, &RepeatedWeightedSample, ArrayProperty)); TestEqual("Weighted sampling without replacement should be deterministic", RepeatedWeightedSample, WeightedSample); TestEqual("Weighted sampling without replacement should select each available value once", TSet(WeightedSample).Num(), 5); FRandomStream UnchangedWeightedStream(8675309); const int32 InitialWeightedSeed = UnchangedWeightedStream.GetCurrentSeed(); TestTrue( "A zero weighted sample should succeed", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, DeterministicWeights, 0, false, &UnchangedWeightedStream, &WeightedSample, ArrayProperty)); TestEqual("A zero weighted sample should not advance its random stream", UnchangedWeightedStream.GetCurrentSeed(), InitialWeightedSeed); TestObject->TestArray = {0, 1}; FRandomStream DistributionStream(1187); TestTrue( "Weighted sampling should accept proportional weights", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, TArray({1.0f, 9.0f}), 10000, true, &DistributionStream, &WeightedSample, ArrayProperty)); int32 HeavySelectionCount = 0; for (const int32 Value : WeightedSample) { HeavySelectionCount += Value == 1 ? 1 : 0; } TestTrue("Weighted sampling should follow the supplied proportions", HeavySelectionCount > 8500 && HeavySelectionCount < 9500); TestObject->TestArray = {10, 20, 30, 40, 50}; const TArray SparseWeights = { 0.0f, 1.0f, -1.0f, std::numeric_limits::quiet_NaN(), std::numeric_limits::infinity() }; FRandomStream SparseStream(17); TestTrue( "Weighted sampling should ignore non-positive and non-finite weights", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, SparseWeights, 8, true, &SparseStream, &WeightedSample, ArrayProperty)); TestEqual("Weighted sampling should select the only positive-weight value", WeightedSample, TArray({20, 20, 20, 20, 20, 20, 20, 20})); const TArray TwoPositiveWeights = {1.0f, 0.0f, 2.0f, 0.0f, 0.0f}; FRandomStream UniqueWeightedStream(44); TestTrue( "Weighted sampling without replacement should succeed", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, TwoPositiveWeights, 8, false, &UniqueWeightedStream, &WeightedSample, ArrayProperty)); TestEqual("Weighted sampling without replacement should clamp to positive-weight entries", WeightedSample.Num(), 2); TestEqual("Weighted sampling without replacement should not repeat source indices", TSet(WeightedSample).Num(), 2); TestTrue("Weighted sampling without replacement should include the first weighted value", WeightedSample.Contains(10)); TestTrue("Weighted sampling without replacement should include the second weighted value", WeightedSample.Contains(30)); WeightedSample = {999}; TestFalse( "Weighted sampling should reject a mismatched weights array", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, TArray({1.0f}), 1, false, nullptr, &WeightedSample, ArrayProperty)); TestTrue("Weighted sampling should clear output after a weights mismatch", WeightedSample.IsEmpty()); TestFalse( "Weighted sampling should reject a negative count", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, TwoPositiveWeights, -1, false, nullptr, &WeightedSample, ArrayProperty)); TestTrue("Weighted sampling should clear output after a negative count", WeightedSample.IsEmpty()); TestTrue( "Weighted sampling should accept a zero count", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, TwoPositiveWeights, 0, false, nullptr, &WeightedSample, ArrayProperty)); TestTrue("Weighted sampling should return an empty zero-count result", WeightedSample.IsEmpty()); const TArray EmptyWeights = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; TestFalse( "Weighted sampling should reject a positive count when all weights are zero", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, EmptyWeights, 1, false, nullptr, &WeightedSample, ArrayProperty)); TestTrue("Weighted sampling should clear output when no selectable entries exist", WeightedSample.IsEmpty()); TestObject->TestArray.Empty(); TestTrue( "Weighted sampling should accept an empty source for a zero count", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, TArray(), 0, false, nullptr, &WeightedSample, ArrayProperty)); TestFalse( "Weighted sampling should reject a positive count for an empty source", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, TArray(), 1, false, nullptr, &WeightedSample, ArrayProperty)); TestTrue("A failed empty weighted sample should leave an empty output", WeightedSample.IsEmpty()); TestObject->TestArray = {10, 20, 30, 40}; FRandomStream LeadingZeroStream(101); TestTrue( "Weighted sampling should handle leading zero weights", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, TArray({0.0f, 0.0f, 0.0f, 1.0f}), 4, true, &LeadingZeroStream, &WeightedSample, ArrayProperty)); TestEqual("Leading zero weights should not select an unavailable value", WeightedSample, TArray({40, 40, 40, 40})); FRandomStream ExtremeWeightStream(2026); TestTrue( "Weighted sampling should handle maximum finite weights", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, TArray({std::numeric_limits::max(), std::numeric_limits::max(), 0.0f, 0.0f}), 2, false, &ExtremeWeightStream, &WeightedSample, ArrayProperty)); TestEqual("Maximum finite weights should return both selectable values", WeightedSample.Num(), 2); TestTrue("Maximum finite weights should include the first value", WeightedSample.Contains(10)); TestTrue("Maximum finite weights should include the second value", WeightedSample.Contains(20)); TestObject->TestArray = {10, 20, 30}; FRandomStream MixedRangeWeightStream(2027); TestTrue( "Weighted sampling should handle mixed finite weight ranges", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, TArray({std::numeric_limits::max(), 1.0f, 2.0f}), 3, false, &MixedRangeWeightStream, &WeightedSample, ArrayProperty)); TestEqual("Mixed finite weights should return every selectable value", WeightedSample.Num(), 3); TestEqual("Mixed finite weights should not repeat values", TSet(WeightedSample).Num(), 3); TestTrue("Mixed finite weights should include the first value", WeightedSample.Contains(10)); TestTrue("Mixed finite weights should include the second value", WeightedSample.Contains(20)); TestTrue("Mixed finite weights should include the third value", WeightedSample.Contains(30)); TestObject->TestArray = {1, 2, 3, 4}; FRandomStream AliasedWeightedStream(81); TestTrue( "Weighted sampling should support the source array as output", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, TArray({1.0f, 1.0f, 1.0f, 1.0f}), 2, false, &AliasedWeightedStream, &TestObject->TestArray, ArrayProperty)); TestEqual("Aliased weighted sampling should return the requested count", TestObject->TestArray.Num(), 2); TestObject->TestArray = {10, 20, 30, 40, 50}; TArray Page; int32 PageCount = 0; TestTrue("GetPage should return an available page", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetPage(&TestObject->TestArray, ArrayProperty, 1, 2, &Page, ArrayProperty, &PageCount)); TestEqual("GetPage should return the second page", Page, TArray({30, 40})); TestEqual("GetPage should return the total page count", PageCount, 3); TestFalse("GetPage should reject a negative page", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetPage(&TestObject->TestArray, ArrayProperty, -1, 2, &Page, ArrayProperty, &PageCount)); TestTrue("GetPage should clear output for a negative page", Page.IsEmpty()); TestFalse("GetPage should reject a non-positive page size", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetPage(&TestObject->TestArray, ArrayProperty, 0, 0, &Page, ArrayProperty, &PageCount)); TestEqual("GetPage should report zero pages for an invalid page size", PageCount, 0); TestObject->TestArray = {1, 2, 3, 4}; FRandomStream InPlaceStream(11); UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 2, false, &InPlaceStream, &TestObject->TestArray, ArrayProperty); TestEqual("Sample should support the same source and output array", TestObject->TestArray.Num(), 2); TestObject->TestArray = {1, 2, 3, 4}; TestTrue("GetPage should support the same source and output array", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetPage(&TestObject->TestArray, ArrayProperty, 1, 2, &TestObject->TestArray, ArrayProperty, &PageCount)); TestEqual("In-place GetPage should return the requested values", TestObject->TestArray, TArray({3, 4})); TArray NaturalStrings = {TEXT("Item10"), TEXT("Item2"), TEXT("Item1"), TEXT("アイテム2")}; UDirectiveUtilArrayFunctionLibrary::NaturalSortStringArray(NaturalStrings); TestEqual("Natural string sort should compare embedded numbers", NaturalStrings[0], FString(TEXT("Item1"))); TestEqual("Natural string sort should place Item2 before Item10", NaturalStrings[1], FString(TEXT("Item2"))); TestEqual("Natural string sort should place Item10 after Item2", NaturalStrings[2], FString(TEXT("Item10"))); TestEqual("Natural string sort should preserve Unicode strings", NaturalStrings[3], FString(TEXT("アイテム2"))); UDirectiveUtilArrayFunctionLibrary::NaturalSortStringArray(NaturalStrings, true); TestEqual("Natural string sort should support descending order", NaturalStrings.Last(), FString(TEXT("Item1"))); TArray NaturalNames = {FName(TEXT("Actor12")), FName(TEXT("Actor3")), FName(TEXT("Actor1"))}; UDirectiveUtilArrayFunctionLibrary::NaturalSortNameArray(NaturalNames); TestEqual("Natural name sort should compare embedded numbers", NaturalNames, TArray({FName(TEXT("Actor1")), FName(TEXT("Actor3")), FName(TEXT("Actor12"))})); TestObject->TestArray = {1, 2, 3, 4, 5}; UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(&TestObject->TestArray, ArrayProperty, 1, 3, &TestObject->TestArray, ArrayProperty); TestEqual("Slice should support the same source and output array", TestObject->TestArray, TArray({2, 3, 4})); TestObject->TestArray = {3, 1, 3, 2, 1}; UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(&TestObject->TestArray, ArrayProperty, &TestObject->TestArray, ArrayProperty); TestEqual("GetDistinct should support the same source and output array", TestObject->TestArray, TArray({3, 1, 2})); FArrayProperty* StringArrayProperty = FindFProperty( UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestStringArray)); FArrayProperty* TextArrayProperty = FindFProperty( UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestTextArray)); FArrayProperty* BoolArrayProperty = FindFProperty( UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestBoolArray)); FArrayProperty* CollisionArrayProperty = FindFProperty( UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestCollisionArray)); FArrayProperty* PodArrayProperty = FindFProperty( UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestPodArray)); FArrayProperty* ObjectArrayProperty = FindFProperty( UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestObjectArray)); TestNotNull("String array property should be available", StringArrayProperty); TestNotNull("Text array property should be available", TextArrayProperty); TestNotNull("Boolean array property should be available", BoolArrayProperty); TestNotNull("Collision array property should be available", CollisionArrayProperty); TestNotNull("POD struct array property should be available", PodArrayProperty); TestNotNull("Object array property should be available", ObjectArrayProperty); TestFalse("String arrays should use the property-aware append path", StringArrayProperty->Inner->HasAnyPropertyFlags(CPF_IsPlainOldData)); const TArray StringAppendSource = {TEXT("Three"), TEXT("Four")}; TestObject->TestStringArray = {TEXT("One"), TEXT("Two")}; UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized( &TestObject->TestStringArray, StringArrayProperty, &StringAppendSource, StringArrayProperty); TestEqual( "AppendOptimized should preserve non-trivial values", TestObject->TestStringArray, TArray({TEXT("One"), TEXT("Two"), TEXT("Three"), TEXT("Four")})); TestObject->TestStringArray = {TEXT("Alpha"), TEXT("Beta")}; UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized( &TestObject->TestStringArray, StringArrayProperty, &TestObject->TestStringArray, StringArrayProperty); TestEqual( "AppendOptimized should support self-appending non-trivial values", TestObject->TestStringArray, TArray({TEXT("Alpha"), TEXT("Beta"), TEXT("Alpha"), TEXT("Beta")})); const TArray StringInsertSource = {TEXT("Two"), TEXT("Three")}; TestObject->TestStringArray = {TEXT("One"), TEXT("Four")}; TestTrue( "InsertOptimized should support non-trivial values", UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized( &TestObject->TestStringArray, StringArrayProperty, &StringInsertSource, StringArrayProperty, 1)); TestEqual( "InsertOptimized should preserve non-trivial value order", TestObject->TestStringArray, TArray({TEXT("One"), TEXT("Two"), TEXT("Three"), TEXT("Four")})); TestObject->TestStringArray = {TEXT("Alpha"), TEXT("Beta"), TEXT("Gamma")}; TestTrue( "InsertOptimized should self-insert non-trivial values", UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized( &TestObject->TestStringArray, StringArrayProperty, &TestObject->TestStringArray, StringArrayProperty, 2)); TestEqual( "InsertOptimized should preserve self-inserted non-trivial values", TestObject->TestStringArray, TArray({ TEXT("Alpha"), TEXT("Beta"), TEXT("Alpha"), TEXT("Beta"), TEXT("Gamma"), TEXT("Gamma") })); TestObject->TestStringArray = { TEXT("Zero"), TEXT("One"), TEXT("Two"), TEXT("Three"), TEXT("Four") }; TestEqual( "RemoveAtIndices should remove non-trivial values", UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtIndices( &TestObject->TestStringArray, StringArrayProperty, TArray({3, 1, 3})), 2); TestEqual( "RemoveAtIndices should preserve non-trivial survivor order", TestObject->TestStringArray, TArray({TEXT("Zero"), TEXT("Two"), TEXT("Four")})); const TArray BoolAppendSource = {false, true, false}; TestObject->TestBoolArray = {true}; UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized( &TestObject->TestBoolArray, BoolArrayProperty, &BoolAppendSource, BoolArrayProperty); TestEqual( "AppendOptimized should preserve Boolean values", TestObject->TestBoolArray, TArray({true, false, true, false})); TestObject->TestBoolArray = {true, false, false}; UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized( &TestObject->TestBoolArray, BoolArrayProperty, &TestObject->TestBoolArray, BoolArrayProperty); TestEqual( "AppendOptimized should support self-appending Boolean values", TestObject->TestBoolArray, TArray({true, false, false, true, false, false})); auto MakePodValue = [](const int32 Index, const float Weight) { FDirectiveUtilPodValue Value; Value.Index = Index; Value.Weight = Weight; return Value; }; TestTrue("POD struct arrays should use the bulk append path", PodArrayProperty->Inner->HasAnyPropertyFlags(CPF_IsPlainOldData)); const TArray PodAppendSource = { MakePodValue(2, 2.5f), MakePodValue(3, 3.5f) }; TestObject->TestPodArray = {MakePodValue(1, 1.5f)}; UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized( &TestObject->TestPodArray, PodArrayProperty, &PodAppendSource, PodArrayProperty); TestEqual("AppendOptimized should append POD structs", TestObject->TestPodArray.Num(), 3); for (int32 Index = 0; Index < TestObject->TestPodArray.Num(); ++Index) { TestEqual("AppendOptimized should preserve POD struct indices", TestObject->TestPodArray[Index].Index, Index + 1); TestEqual("AppendOptimized should preserve POD struct weights", TestObject->TestPodArray[Index].Weight, static_cast(Index) + 1.5f); } TestEqual("AppendOptimized should not modify the POD struct source", PodAppendSource.Num(), 2); if (PodAppendSource.Num() == 2) { TestEqual("AppendOptimized should preserve the first POD source index", PodAppendSource[0].Index, 2); TestEqual("AppendOptimized should preserve the second POD source index", PodAppendSource[1].Index, 3); } TestObject->TestPodArray = {MakePodValue(4, 4.5f), MakePodValue(5, 5.5f)}; UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized( &TestObject->TestPodArray, PodArrayProperty, &TestObject->TestPodArray, PodArrayProperty); TestEqual("AppendOptimized should self-append POD structs", TestObject->TestPodArray.Num(), 4); if (TestObject->TestPodArray.Num() == 4) { TestEqual("AppendOptimized should preserve the first self-appended POD struct", TestObject->TestPodArray[2].Index, 4); TestEqual("AppendOptimized should preserve the second self-appended POD struct", TestObject->TestPodArray[3].Index, 5); } TestFalse("POD struct grouping should use the reflected-value hash path", PodArrayProperty->Inner->HasAllPropertyFlags(CPF_HasGetValueTypeHash)); TestObject->TestPodArray = { MakePodValue(1, 1.5f), MakePodValue(2, 2.5f), MakePodValue(1, 1.5f), MakePodValue(3, 3.5f), MakePodValue(1, 1.5f) }; UDirectiveUtilTestObject* PodResultObject = NewObject(); UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct( &TestObject->TestPodArray, PodArrayProperty, &PodResultObject->TestPodArray, PodArrayProperty); TestEqual("GetDistinct should group unhashable POD structs", PodResultObject->TestPodArray.Num(), 3); if (PodResultObject->TestPodArray.Num() == 3) { TestEqual("GetDistinct should retain the first POD value", PodResultObject->TestPodArray[0].Index, 1); TestEqual("GetDistinct should retain the second POD value", PodResultObject->TestPodArray[1].Index, 2); TestEqual("GetDistinct should retain the third POD value", PodResultObject->TestPodArray[2].Index, 3); } FDirectiveUtilPodValue MostCommonPod = MakePodValue(INDEX_NONE, 0.0f); int32 MostCommonPodCount = 0; TestTrue( "GetMostCommon should group unhashable POD structs", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon( &TestObject->TestPodArray, PodArrayProperty, &MostCommonPod, &MostCommonPodCount)); TestEqual("GetMostCommon should return the repeated POD value", MostCommonPod.Index, 1); TestEqual("GetMostCommon should report the POD value count", MostCommonPodCount, 3); TestObject->TestStringArray = {TEXT("Zero"), TEXT("Selected"), TEXT("Never")}; TArray WeightedStrings; FRandomStream WeightedStringStream(55); TestTrue( "Weighted sampling should preserve non-trivial array values", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestStringArray, StringArrayProperty, TArray({0.0f, 1.0f, 0.0f}), 3, true, &WeightedStringStream, &WeightedStrings, StringArrayProperty)); TestEqual( "Weighted sampling should copy the selected non-trivial value", WeightedStrings, TArray({TEXT("Selected"), TEXT("Selected"), TEXT("Selected")})); TestObject->TestStringArray = {TEXT("One"), TEXT("Two"), TEXT("Three"), TEXT("Four")}; UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&TestObject->TestStringArray, StringArrayProperty, 2); TestEqual( "Rotate should preserve non-trivial values", TestObject->TestStringArray, TArray({TEXT("Three"), TEXT("Four"), TEXT("One"), TEXT("Two")})); UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&TestObject->TestStringArray, StringArrayProperty, -2); TestEqual( "Rotate should support negative shifts for non-trivial values", TestObject->TestStringArray, TArray({TEXT("One"), TEXT("Two"), TEXT("Three"), TEXT("Four")})); UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice( &TestObject->TestStringArray, StringArrayProperty, 1, 2, &TestObject->TestStringArray, StringArrayProperty); TestEqual("Aliased Slice should preserve string values", TestObject->TestStringArray, TArray({TEXT("Two"), TEXT("Three")})); TestObject->TestStringArray = {TEXT("Remove"), TEXT("Keep A"), TEXT("Remove"), TEXT("Keep B")}; const FString StringToRemove = TEXT("Remove"); TestTrue( "RemoveAllOccurrences should support strings", UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences( &TestObject->TestStringArray, StringArrayProperty, &StringToRemove)); TestEqual( "RemoveAllOccurrences should preserve string survivor order", TestObject->TestStringArray, TArray({TEXT("Keep A"), TEXT("Keep B")})); UObject* FirstObject = NewObject(TestObject); UObject* SecondObject = NewObject(TestObject); UObject* ThirdObject = NewObject(TestObject); const TArray> ObjectAppendSource = {SecondObject, ThirdObject}; TestObject->TestObjectArray = {FirstObject}; UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized( &TestObject->TestObjectArray, ObjectArrayProperty, &ObjectAppendSource, ObjectArrayProperty); TestEqual("AppendOptimized should preserve object references", TestObject->TestObjectArray.Num(), 3); if (TestObject->TestObjectArray.Num() == 3) { TestEqual("AppendOptimized should retain the target object", TestObject->TestObjectArray[0].Get(), FirstObject); TestEqual("AppendOptimized should append the first source object", TestObject->TestObjectArray[1].Get(), SecondObject); TestEqual("AppendOptimized should append the second source object", TestObject->TestObjectArray[2].Get(), ThirdObject); } TestObject->TestObjectArray = {FirstObject, SecondObject, FirstObject}; UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestObjectArray, ObjectArrayProperty); TestEqual("RemoveDuplicates should preserve object references", TestObject->TestObjectArray.Num(), 2); if (TestObject->TestObjectArray.Num() == 2) { TestEqual("RemoveDuplicates should retain the first object", TestObject->TestObjectArray[0].Get(), FirstObject); TestEqual("RemoveDuplicates should retain the second object", TestObject->TestObjectArray[1].Get(), SecondObject); } TestObject->TestObjectArray = {FirstObject, SecondObject, ThirdObject}; TestTrue("Aliased object GetPage should succeed", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetPage( &TestObject->TestObjectArray, ObjectArrayProperty, 1, 2, &TestObject->TestObjectArray, ObjectArrayProperty, &PageCount)); TestEqual("Aliased object GetPage should preserve the selected reference", TestObject->TestObjectArray.Num(), 1); if (TestObject->TestObjectArray.Num() == 1) { TestEqual("Aliased object GetPage should return the final object", TestObject->TestObjectArray[0].Get(), ThirdObject); } TestObject->TestObjectArray = {FirstObject, nullptr, SecondObject, FirstObject, nullptr}; TestTrue( "RemoveAllOccurrences should support object references", UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences( &TestObject->TestObjectArray, ObjectArrayProperty, &FirstObject)); TestEqual("RemoveAllOccurrences should retain object and null survivors", TestObject->TestObjectArray.Num(), 3); if (TestObject->TestObjectArray.Num() == 3) { TestNull("RemoveAllOccurrences should retain the first null", TestObject->TestObjectArray[0].Get()); TestEqual("RemoveAllOccurrences should retain the unmatched object", TestObject->TestObjectArray[1].Get(), SecondObject); TestNull("RemoveAllOccurrences should retain the second null", TestObject->TestObjectArray[2].Get()); } UObject* NullObject = nullptr; TestTrue( "RemoveAllOccurrences should remove null object references", UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences( &TestObject->TestObjectArray, ObjectArrayProperty, &NullObject)); TestEqual("RemoveAllOccurrences should leave the non-null object", TestObject->TestObjectArray.Num(), 1); TestObject->TestObjectArray = {FirstObject, nullptr, SecondObject, ThirdObject}; TestEqual( "RemoveAtIndices should preserve object references", UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtIndices( &TestObject->TestObjectArray, ObjectArrayProperty, TArray({1, 3})), 2); TestEqual("RemoveAtIndices should retain two object references", TestObject->TestObjectArray.Num(), 2); if (TestObject->TestObjectArray.Num() == 2) { TestEqual("RemoveAtIndices should retain the first object", TestObject->TestObjectArray[0].Get(), FirstObject); TestEqual("RemoveAtIndices should retain the second object", TestObject->TestObjectArray[1].Get(), SecondObject); } TestObject->TestStringArray = {TEXT("unchanged")}; UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized( &TestObject->TestStringArray, StringArrayProperty, &TestObject->TestArray, ArrayProperty); TestEqual( "AppendOptimized should reject mismatched array types without changing the target", TestObject->TestStringArray, TArray({TEXT("unchanged")})); TestFalse( "InsertOptimized should reject mismatched array types", UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized( &TestObject->TestStringArray, StringArrayProperty, &TestObject->TestArray, ArrayProperty, 0)); TestEqual( "InsertOptimized should not change a target with a mismatched source type", TestObject->TestStringArray, TArray({TEXT("unchanged")})); UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice( &TestObject->TestArray, ArrayProperty, 0, 1, &TestObject->TestStringArray, StringArrayProperty); TestEqual("Slice should reject mismatched array types without changing output", TestObject->TestStringArray, TArray({TEXT("unchanged")})); TestFalse( "Weighted sampling should reject mismatched output types", UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted( &TestObject->TestArray, ArrayProperty, TArray({1.0f, 1.0f, 1.0f}), 1, false, nullptr, &TestObject->TestStringArray, StringArrayProperty)); TestEqual("Weighted sampling should leave a mismatched output unchanged", TestObject->TestStringArray, TArray({TEXT("unchanged")})); const FText AlphaText = FText::FromString(TEXT("Alpha")); const FText BetaText = FText::FromString(TEXT("Beta")); TestObject->TestTextArray = {AlphaText, BetaText, AlphaText}; UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestTextArray, TextArrayProperty); TestEqual("RemoveDuplicates should support unhashable property types", TestObject->TestTextArray.Num(), 2); if (TestObject->TestTextArray.Num() == 2) { TestTrue("RemoveDuplicates should retain the first unhashable value", TestObject->TestTextArray[0].IdenticalTo(AlphaText)); TestTrue("RemoveDuplicates should retain the second unhashable value", TestObject->TestTextArray[1].IdenticalTo(BetaText)); } TestObject->TestTextArray = {AlphaText, BetaText, AlphaText, BetaText}; TestTrue( "RemoveAllOccurrences should support text values", UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences( &TestObject->TestTextArray, TextArrayProperty, &AlphaText)); TestEqual("RemoveAllOccurrences should remove matching text values", TestObject->TestTextArray.Num(), 2); if (TestObject->TestTextArray.Num() == 2) { TestTrue("RemoveAllOccurrences should retain the first text survivor", TestObject->TestTextArray[0].IdenticalTo(BetaText)); TestTrue("RemoveAllOccurrences should retain the second text survivor", TestObject->TestTextArray[1].IdenticalTo(BetaText)); } TestObject->TestBoolArray = {true, false, true, false, true}; const bool bBoolToRemove = true; TestTrue( "RemoveAllOccurrences should support Boolean values", UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences( &TestObject->TestBoolArray, BoolArrayProperty, &bBoolToRemove)); TestEqual("RemoveAllOccurrences should remove matching Boolean values", TestObject->TestBoolArray, TArray({false, false})); auto AddCollisionValue = [&TestObject](const int32 Value) { FDirectiveUtilCollisionValue& Entry = TestObject->TestCollisionArray.AddDefaulted_GetRef(); Entry.Value = Value; }; TestObject->TestCollisionArray.Reset(); for (const int32 Value : {1, 2, 1, 3, 2}) { AddCollisionValue(Value); } TestTrue("Collision test property should expose a value hash", CollisionArrayProperty->Inner->HasAllPropertyFlags(CPF_HasGetValueTypeHash)); UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestCollisionArray, CollisionArrayProperty); TestEqual("RemoveDuplicates should resolve hash collisions", TestObject->TestCollisionArray.Num(), 3); if (TestObject->TestCollisionArray.Num() == 3) { TestEqual("Hash collision result should retain the first value", TestObject->TestCollisionArray[0].Value, 1); TestEqual("Hash collision result should retain the second value", TestObject->TestCollisionArray[1].Value, 2); TestEqual("Hash collision result should retain the third value", TestObject->TestCollisionArray[2].Value, 3); } TestObject->TestCollisionArray.Reset(); for (const int32 Value : {1, 2, 1, 3, 1, 4}) { AddCollisionValue(Value); } FDirectiveUtilCollisionValue CollisionToRemove; CollisionToRemove.Value = 1; TestTrue( "RemoveAllOccurrences should support struct equality", UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences( &TestObject->TestCollisionArray, CollisionArrayProperty, &CollisionToRemove)); TestEqual("RemoveAllOccurrences should keep struct survivors", TestObject->TestCollisionArray.Num(), 3); if (TestObject->TestCollisionArray.Num() == 3) { TestEqual("RemoveAllOccurrences should retain the first struct survivor", TestObject->TestCollisionArray[0].Value, 2); TestEqual("RemoveAllOccurrences should retain the second struct survivor", TestObject->TestCollisionArray[1].Value, 3); TestEqual("RemoveAllOccurrences should retain the third struct survivor", TestObject->TestCollisionArray[2].Value, 4); } TestObject->TestCollisionArray.Reset(); for (const int32 Value : {4, 5, 5, 4}) { AddCollisionValue(Value); } FDirectiveUtilCollisionValue MostCommonCollision; MostCommonCount = 0; TestTrue("GetMostCommon should resolve hash collisions", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon( &TestObject->TestCollisionArray, CollisionArrayProperty, &MostCommonCollision, &MostCommonCount)); TestEqual("GetMostCommon should use first occurrence to break collision ties", MostCommonCollision.Value, 4); TestEqual("GetMostCommon should report the collision value count", MostCommonCount, 2); TestObject->TestArray.SetNumUninitialized(100); for (int32 Index = 0; Index < TestObject->TestArray.Num(); ++Index) { TestObject->TestArray[Index] = Index; } for (const int32 Count : {10, 75}) { FRandomStream CompatibilityStream(9917); UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample( &TestObject->TestArray, ArrayProperty, Count, false, &CompatibilityStream, &SampledValues, ArrayProperty); TestEqual( FString::Printf(TEXT("Sampling %d values should match dense Fisher-Yates"), Count), SampledValues, BuildReferenceSample(TestObject->TestArray.Num(), Count, 9917)); } TArray SelectionCounts; SelectionCounts.Init(0, 5); TestObject->TestArray = {0, 1, 2, 3, 4}; FRandomStream UniformityStream(77123); for (int32 Iteration = 0; Iteration < 5000; ++Iteration) { UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample( &TestObject->TestArray, ArrayProperty, 1, false, &UniformityStream, &SampledValues, ArrayProperty); if (SampledValues.Num() == 1 && SelectionCounts.IsValidIndex(SampledValues[0])) { ++SelectionCounts[SampledValues[0]]; } } for (int32 Value = 0; Value < SelectionCounts.Num(); ++Value) { TestTrue( FString::Printf(TEXT("Sampling frequency for value %d should remain within tolerance"), Value), FMath::Abs(SelectionCounts[Value] - 1000) <= 150); } FRandomStream FuzzStream(18181); for (int32 Iteration = 0; Iteration < 100; ++Iteration) { TArray SourceValues; const int32 ValueCount = FuzzStream.RandRange(0, 128); SourceValues.Reserve(ValueCount); for (int32 Index = 0; Index < ValueCount; ++Index) { SourceValues.Add(FuzzStream.RandRange(-12, 12)); } const TArray ExpectedDistinct = BuildReferenceDistinct(SourceValues); TestObject->TestArray = SourceValues; UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestArray, ArrayProperty); TestEqual(FString::Printf(TEXT("RemoveDuplicates fuzz case %d"), Iteration), TestObject->TestArray, ExpectedDistinct); TestObject->TestArray = SourceValues; UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(&TestObject->TestArray, ArrayProperty, &DistinctOut, ArrayProperty); TestEqual(FString::Printf(TEXT("GetDistinct fuzz case %d"), Iteration), DistinctOut, ExpectedDistinct); TArray InsertValues; const int32 InsertCount = FuzzStream.RandRange(0, 32); InsertValues.Reserve(InsertCount); for (int32 Index = 0; Index < InsertCount; ++Index) { InsertValues.Add(FuzzStream.RandRange(-20, 20)); } const int32 InsertIndex = FuzzStream.RandRange(0, SourceValues.Num()); TArray ExpectedInsert = SourceValues; ExpectedInsert.Insert(InsertValues, InsertIndex); TestObject->TestArray = SourceValues; TestEqual( FString::Printf(TEXT("InsertOptimized fuzz result %d"), Iteration), UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized( &TestObject->TestArray, ArrayProperty, &InsertValues, ArrayProperty, InsertIndex), !InsertValues.IsEmpty()); TestEqual( FString::Printf(TEXT("InsertOptimized fuzz values %d"), Iteration), TestObject->TestArray, ExpectedInsert); TArray RemovalIndices; const int32 RemovalCount = FuzzStream.RandRange(0, 48); RemovalIndices.Reserve(RemovalCount); for (int32 Index = 0; Index < RemovalCount; ++Index) { RemovalIndices.Add(FuzzStream.RandRange(-8, SourceValues.Num() + 8)); } const TArray ExpectedRemoval = BuildReferenceRemoval(SourceValues, RemovalIndices); TestObject->TestArray = SourceValues; const int32 RemovedCount = UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtIndices( &TestObject->TestArray, ArrayProperty, RemovalIndices); TestEqual( FString::Printf(TEXT("RemoveAtIndices fuzz count %d"), Iteration), RemovedCount, SourceValues.Num() - ExpectedRemoval.Num()); TestEqual( FString::Printf(TEXT("RemoveAtIndices fuzz values %d"), Iteration), TestObject->TestArray, ExpectedRemoval); } TArray StableNaturalStrings = {TEXT("ItemA"), TEXT("itema"), TEXT("ItemB"), TEXT("itemb")}; UDirectiveUtilArrayFunctionLibrary::NaturalSortStringArray(StableNaturalStrings); TestEqual( "Natural string sort should preserve equivalent-key order", StableNaturalStrings, TArray({TEXT("ItemA"), TEXT("itema"), TEXT("ItemB"), TEXT("itemb")})); UDirectiveUtilArrayFunctionLibrary::NaturalSortStringArray(StableNaturalStrings, true); TestEqual( "Descending natural string sort should preserve equivalent-key order", StableNaturalStrings, TArray({TEXT("ItemB"), TEXT("itemb"), TEXT("ItemA"), TEXT("itema")})); return true; }