Files
dungeons/Assets/UltimateXR/Runtime/Scripts/Extensions/System/Collections/EnumerableExt.cs
2024-08-06 21:58:35 +02:00

318 lines
14 KiB
C#

// --------------------------------------------------------------------------------------------------------------------
// <copyright file="EnumerableExt.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UltimateXR.Core;
using UltimateXR.Core.Settings;
using UnityEngine;
using Random = UnityEngine.Random;
namespace UltimateXR.Extensions.System.Collections
{
/// <summary>
/// <see cref="IEnumerable{T}" /> extensions.
/// </summary>
public static class EnumerableExt
{
#region Public Methods
/// <summary>
/// Compares two IEnumerable for equality, considering the order of elements.
/// For dictionaries, compares key-value pairs regardless of their order.
/// </summary>
/// <param name="enumerableA">The first collection to compare</param>
/// <param name="enumerableB">The second collection to compare</param>
/// <returns>True if the collections are equal; otherwise, false</returns>
public static bool ContentEqual(IEnumerable enumerableA, IEnumerable enumerableB)
{
return ContentEqual(enumerableA, enumerableB, (a, b) => a.ValuesEqual(b));
}
/// <summary>
/// Compares two IEnumerable for equality, considering the order of elements.
/// For dictionaries, compares key-value pairs regardless of their order.
/// Values are compared using a floating point precision threshold used by
/// <see cref="ObjectExt.ValuesEqual(object,object,float)" />.
/// </summary>
/// <param name="enumerableA">The first collection to compare</param>
/// <param name="enumerableB">The second collection to compare</param>
/// <param name="precisionThreshold">
/// The precision threshold for float comparisons in types supported by
/// <see cref="ObjectExt.ValuesEqual(object,object,float)" />.
/// </param>
/// <returns>True if the collections are equal; otherwise, false</returns>
public static bool ContentEqual(IEnumerable enumerableA, IEnumerable enumerableB, float precisionThreshold)
{
return ContentEqual(enumerableA, enumerableB, (a, b) => a.ValuesEqual(b, precisionThreshold));
}
/// <summary>
/// Returns a random element from the collection.
/// </summary>
/// <param name="list">Collection to get the random element from</param>
/// <typeparam name="TIn">Element type</typeparam>
/// <returns>Random element from the collection</returns>
/// <remarks>
/// Uses Unity's random number generator (<see cref="UnityEngine.Random.Range(int,int)" />).
/// </remarks>
public static TIn RandomElement<TIn>(this IEnumerable<TIn> list)
{
return list.Any() ? list.ElementAt(Random.Range(0, list.Count())) : default(TIn);
}
/// <summary>
/// Applies an <see cref="Action" /> on all elements in a collection.
/// </summary>
/// <param name="list">Elements to apply the action on</param>
/// <param name="action">Action to apply</param>
/// <typeparam name="TIn">Element type</typeparam>
/// <exception cref="ArgumentException">Any of the parameters was null</exception>
public static void ForEach<TIn>(this IEnumerable<TIn> list, Action<TIn> action)
{
if (list == null)
{
throw new ArgumentException("Argument cannot be null.", nameof(list));
}
if (action == null)
{
throw new ArgumentException("Argument cannot be null.", nameof(action));
}
foreach (TIn value in list)
{
action(value);
}
}
/// <summary>
/// Asynchronously applies a function on all elements in a collection.
/// </summary>
/// <param name="list">Elements to apply the function on</param>
/// <param name="function">Function to apply</param>
/// <typeparam name="TIn">Element type</typeparam>
/// <returns>An awaitable task wrapping the Task.WhenAll applying the function on all elements in a collection</returns>
public static Task ForEachAsync<TIn>(this IEnumerable<TIn> list, Func<TIn, Task> function)
{
return Task.WhenAll(list.Select(function));
}
/// <summary>
/// Asynchronously applies a function to all elements in a collection.
/// </summary>
/// <param name="list">Elements to apply the function on</param>
/// <param name="function">Function to apply</param>
/// <typeparam name="TIn">Element type</typeparam>
/// <typeparam name="TOut">Function return type</typeparam>
/// <returns>An awaitable task wrapping the Task.WhenAll applying the function on all elements in a collection</returns>
public static Task<TOut[]> ForEachAsync<TIn, TOut>(this IEnumerable<TIn> list, Func<TIn, Task<TOut>> function)
{
return Task.WhenAll(list.Select(function));
}
/// <summary>
/// Asynchronously applies an action on all elements in a collection.
/// </summary>
/// <param name="list">Elements to apply the action on</param>
/// <param name="action">Action to apply</param>
/// <typeparam name="TIn">Element type</typeparam>
/// <returns>Task wrapping the Task.WhenAll applying the action on all elements in a collection</returns>
public static Task ForEachThreaded<TIn>(this IEnumerable<TIn> list, Action<TIn> action)
{
void OnFaulted(Task runTask, int itemIndex)
{
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Warnings)
{
Debug.LogWarning($"{UxrConstants.CoreModule} ForEachThreaded::Item[{itemIndex}] faulted (see reason below):");
}
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Errors)
{
Debug.LogException(runTask.Exception);
}
}
return Task.WhenAll(list.Select((item, index) => Task.Run(() => action(item)).ContinueWith(runTask => OnFaulted(runTask, index), TaskContinuationOptions.OnlyOnFaulted)));
}
/// <summary>
/// Asynchronously applies a function on all elements in a collection.
/// </summary>
/// <param name="list">Elements to apply the function on</param>
/// <param name="function">Function to apply</param>
/// <typeparam name="TIn">Element type</typeparam>
/// <typeparam name="TOut">Function return type</typeparam>
/// <returns>Task wrapping the Task.WhenAll applying the function on all elements in a collection</returns>
public static Task<TOut[]> ForEachThreaded<TIn, TOut>(this IEnumerable<TIn> list, Func<TIn, TOut> function)
{
TOut OnFaulted(Task<TOut> t, int itemIndex)
{
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Warnings)
{
Debug.LogWarning($"{UxrConstants.CoreModule} ForEachThreaded::Item[{itemIndex}] faulted (see reason below):");
}
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Errors)
{
Debug.LogException(t.Exception);
}
return default;
}
return Task.WhenAll(list.Select((item, index) => Task.Run(() => function(item)).ContinueWith(t => OnFaulted(t, index), TaskContinuationOptions.OnlyOnFaulted)));
}
/// <summary>
/// Returns the maximal element of the given sequence, based on the given projection.
/// </summary>
/// <remarks>
/// This overload uses the default comparer for the projected type. This operator uses deferred execution. The results
/// are evaluated and cached on first use to returned sequence.
/// </remarks>
/// <typeparam name="TSource">Type of the source sequence</typeparam>
/// <typeparam name="TKey">Type of the projected element</typeparam>
/// <param name="source">Source sequence</param>
/// <param name="selector">Selector to use to pick the results to compare</param>
/// <returns>The maximal element, according to the projection.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source" /> or <paramref name="selector" /> is null</exception>
public static TSource MaxBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> selector)
{
return source.MaxBy(selector, null!);
}
/// <summary>
/// Returns the maximal element of the given sequence, based on the given projection and the specified comparer for
/// projected values.
/// </summary>
/// <remarks>
/// This operator uses deferred execution. The results are evaluated and cached on first use to returned sequence.
/// </remarks>
/// <typeparam name="TSource">Type of the source sequence</typeparam>
/// <typeparam name="TKey">Type of the projected element</typeparam>
/// <param name="source">Source sequence</param>
/// <param name="selector">Selector to use to pick the results to compare</param>
/// <param name="comparer">Comparer to use to compare projected values</param>
/// <returns>The maximal element, according to the projection.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="source" />, <paramref name="selector" /> or <paramref name="comparer" /> is null
/// </exception>
/// <exception cref="T:System.Exception">A delegate callback throws an exception.</exception>
public static TSource MaxBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> selector, IComparer<TKey> comparer)
{
source.ThrowIfNull(nameof(source));
selector.ThrowIfNull(nameof(selector));
TSource result = default;
TKey keyMax = default;
comparer ??= Comparer<TKey>.Default;
bool isFirst = true;
foreach (TSource s in source)
{
TKey key = selector(s);
if (isFirst)
{
result = s;
keyMax = key;
isFirst = false;
}
else if (comparer.Compare(key, keyMax) > 0)
{
result = s;
keyMax = key;
}
}
return result;
}
/// <summary>
/// Splits a list of strings using CamelCase.
/// </summary>
/// <param name="strings">List of strings</param>
/// <returns>List of strings with added spacing</returns>
public static IEnumerable<string> SplitCamelCase(this IEnumerable<string> strings)
{
foreach (string element in strings)
{
yield return element.SplitCamelCase();
}
}
#endregion
#region Private Methods
/// <summary>
/// Compares two IEnumerable for equality, considering the order of elements.
/// For dictionaries, compares key-value pairs regardless of their order.
/// </summary>
/// <param name="enumerableA">The first collection to compare</param>
/// <param name="enumerableB">The second collection to compare</param>
/// <param name="comparer">Comparison function</param>
/// <returns>True if the collections are equal; otherwise, false</returns>
private static bool ContentEqual(IEnumerable enumerableA, IEnumerable enumerableB, Func<object, object, bool> comparer)
{
// If the collections are dictionaries, compare key-value pairs
if (enumerableA is IDictionary dictionaryA && enumerableB is IDictionary dictionaryB)
{
// Ensure both dictionaries have the same number of elements
if (dictionaryA.Count != dictionaryB.Count)
{
return false;
}
// Compare key-value pairs regardless of order
foreach (DictionaryEntry entryA in dictionaryA)
{
if (!dictionaryB.Contains(entryA.Key) || !comparer(entryA.Value, dictionaryB[entryA.Key]))
{
return false;
}
}
return true;
}
// If the collections are lists, do a quick test to check if they have different number of elements
if (enumerableA is IList listA && enumerableB is IList listB)
{
if (listA.Count != listB.Count)
{
return false;
}
}
// If the collections are HashSets, compare elements using SetEquals
if (enumerableA is HashSet<object> hashSetA && enumerableB is HashSet<object> hashSetB)
{
return hashSetA.SetEquals(hashSetB);
}
// For non-dictionary, non-HashSet collections, compare elements
IEnumerator enumeratorA = enumerableA.GetEnumerator();
IEnumerator enumeratorB = enumerableB.GetEnumerator();
while (enumeratorA.MoveNext())
{
if (!enumeratorB.MoveNext() || !comparer(enumeratorA.Current, enumeratorB.Current))
{
return false;
}
}
// Ensure both collections have the same number of elements
return !enumeratorB.MoveNext();
}
#endregion
}
}