// MIT License - Copyright (c) 2023 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.Extension { using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Random; using WallstopStudios.UnityHelpers.Core.Helper; using WallstopStudios.UnityHelpers.Utils; /// /// Extension methods for IEnumerable providing collection operations, conversions, shuffling, and iteration utilities. /// /// /// Thread Safety: Most methods are not thread-safe unless noted otherwise. Concurrent enumeration requires external synchronization. /// Performance: Methods are optimized for common collection types (IList, IReadOnlyList, HashSet) with pattern matching. /// Allocations: Documented per method. Many methods use pooled buffers to minimize allocations. /// public static class IEnumerableExtensions { /// /// Converts an enumerable collection to a LinkedList. /// /// The type of elements in the collection. /// The source enumerable to convert. /// A new LinkedList containing all elements from the source. /// /// Null handling: Throws ArgumentNullException if source is null (thrown by LinkedList constructor). /// Thread safety: Not thread-safe. No Unity main thread requirement. /// Performance: O(n) where n is the number of elements in source. /// Allocations: Allocates a new LinkedList and a node for each element. /// Edge cases: Empty source results in an empty LinkedList. /// /// Thrown when source is null. public static LinkedList ToLinkedList(this IEnumerable source) { return new LinkedList(source); } /// /// Converts an enumerable to an IList, avoiding allocation if the enumerable is already an IList. /// /// The type of elements in the collection. /// The enumerable to convert. /// The original enumeration if it's already an IList, otherwise a new List containing all elements. /// /// Null handling: Throws NullReferenceException if enumeration is null when calling ToList(). /// Thread safety: Not thread-safe. No Unity main thread requirement. /// Performance: O(1) if enumeration is already IList, O(n) otherwise where n is the number of elements. /// Allocations: No allocation if enumeration is already an IList. Otherwise allocates a new List. /// Edge cases: Returns the original IList reference if enumeration is already an IList, enabling type-checking optimizations. /// public static IList AsList(this IEnumerable enumeration) { if (enumeration is IList list) { return list; } return enumeration.ToList(); } /// /// Sorts the elements of a sequence in ascending order using a comparison function. /// /// The type of the elements of enumeration. /// A sequence of values to order. /// A comparison function that returns negative if first is less than second, 0 if equal, positive if greater. /// A new List containing the sorted elements. /// /// Null handling: Comparer can be invoked with null elements depending on enumeration content. /// Thread safety: Not thread-safe. No Unity main thread requirement. /// Performance: O(n log n) where n is the number of elements (uses List.Sort). /// Allocations: Allocates a result List. Uses pooled buffer for intermediate work. /// Edge cases: Empty or single element collections return without sorting. /// /// Thrown when comparer is null (propagated from FuncBasedComparer). public static List OrderBy(this IEnumerable enumeration, Func comparer) { FuncBasedComparer typedComparer = new(comparer); using PooledResource> lease = Buffers.List.Get(out List buffer); buffer.AddRange(enumeration); if (buffer.Count <= 1) { return new List(buffer); } buffer.Sort(typedComparer); return new List(buffer); } /// /// Sorts the elements of a sequence in ascending order using their natural IComparable ordering. /// /// The type of the elements of enumerable. Must implement IComparable. /// A sequence of values to order. /// A new List containing the sorted elements in ascending order. /// /// Null handling: Behavior with null elements depends on T's CompareTo implementation. /// Thread safety: Not thread-safe. No Unity main thread requirement. /// Performance: O(n log n) where n is the number of elements (uses List.Sort). /// Allocations: Allocates a result List. Uses pooled buffer for intermediate work. /// Edge cases: Empty or single element collections return without sorting. /// public static List Ordered(this IEnumerable enumerable) where T : IComparable { using PooledResource> lease = Buffers.List.Get(out List buffer); buffer.AddRange(enumerable); if (buffer.Count <= 1) { return new List(buffer); } buffer.Sort(); return new List(buffer); } /// /// Returns a randomly shuffled version of the enumerable. /// /// The type of the elements of enumerable. /// A sequence of values to shuffle. /// The random number generator to use. If null, uses PRNG.Instance. /// A new List containing the same elements in random order. /// /// Null handling: If enumerable is null, will throw when enumerated. If random is null, uses PRNG.Instance. /// Thread safety: Not thread-safe if random is shared. No Unity main thread requirement. /// Performance: O(n) where n is the number of elements (uses Fisher-Yates shuffle via IListExtensions.Shuffle). /// Allocations: Allocates a result List. Uses pooled buffer for intermediate work. /// Edge cases: Empty or single element collections return unchanged. Shuffle quality depends on random implementation. /// public static List Shuffled(this IEnumerable enumerable, IRandom random = null) { using PooledResource> lease = Buffers.List.Get(out List buffer); buffer.AddRange(enumerable); buffer.Shuffle(random); return new List(buffer); } /// /// Creates an infinite repeating sequence from the given enumerable. /// /// The type of the elements of enumerable. /// A sequence of values to repeat infinitely. /// An infinite IEnumerable that cycles through the source elements repeatedly. /// /// Null handling: If enumerable is null, behavior is undefined. Empty enumerables immediately yield break. /// Thread safety: Not thread-safe. No Unity main thread requirement. /// Performance: O(1) amortized per element. Optimized for common collection types (IList, HashSet, etc). Unknown collection types use buffering on first iteration. /// Allocations: No allocations for known collection types. Unknown types allocate a pooled buffer during first enumeration. /// Edge cases: Empty collections immediately stop enumeration. Unknown collection types buffer all elements on first pass. The iterator will loop forever for non-empty collections. /// public static IEnumerable Infinite(this IEnumerable enumerable) { switch (enumerable) { case IReadOnlyList { Count: 0 }: { yield break; } case IReadOnlyList readonlyList: { while (true) { for (int i = 0; i < readonlyList.Count; ++i) { yield return readonlyList[i]; } } } case IList { Count: 0 }: { yield break; } case IList list: { while (true) { for (int i = 0; i < list.Count; ++i) { yield return list[i]; } } } case HashSet { Count: 0 }: { yield break; } case HashSet hashSet: { while (true) { foreach (T element in hashSet) { yield return element; } } } case Queue { Count: 0 }: { yield break; } case Queue queue: { while (true) { foreach (T element in queue) { yield return element; } } } case Stack { Count: 0 }: { yield break; } case Stack stack: { while (true) { foreach (T element in stack) { yield return element; } } } case SortedSet { Count: 0 }: { yield break; } case SortedSet sortedSet: { while (true) { foreach (T element in sortedSet) { yield return element; } } } case LinkedList { Count: 0 }: { yield break; } case LinkedList linkedList: { while (true) { foreach (T element in linkedList) { yield return element; } } } } using PooledResource> buffer = Buffers.List.Get(out List bufferList); foreach (T element in enumerable) { bufferList.Add(element); yield return element; } if (bufferList.Count == 0) { yield break; } while (true) { foreach (T element in bufferList) { yield return element; } } } /// /// Splits an enumerable into partitions of the specified size. /// /// The type of the elements of items. /// The sequence to partition. /// The maximum number of elements per partition. /// An IEnumerable of IEnumerable where each inner enumerable contains up to 'size' elements. /// /// Null handling: Throws NullReferenceException if items is null when GetEnumerator is called. /// Thread safety: Not thread-safe. No Unity main thread requirement. /// Performance: O(n) where n is the total number of elements. Lazy evaluation - partitions are created as enumerated. /// Allocations: Uses a pooled List buffer for each partition, reducing allocations. The buffer is reused across partitions. /// Edge cases: Last partition may have fewer than 'size' elements. Size less than 1 will cause infinite loop or no partitions. Empty items yields no partitions. /// public static IEnumerable> Partition(this IEnumerable items, int size) { if (size <= 0) { throw new ArgumentOutOfRangeException(nameof(size), size, "Size must be positive."); } using IEnumerator enumerator = items.GetEnumerator(); while (enumerator.MoveNext()) { // Allocate one independent list per partition to ensure the yielded value // is not mutated by subsequent iterations. List partition = new(size); int count = 0; do { partition.Add(enumerator.Current); } while (++count < size && enumerator.MoveNext()); yield return partition; } } /// /// Splits an enumerable into partitions of the specified size, returning pooled lists. /// Each yielded value must be disposed by the consumer to return the list to the pool. /// /// Element type. /// Sequence to partition. /// Maximum elements per partition. /// Sequence of pooled list resources; dispose each to return to pool. public static IEnumerable>> PartitionPooled( this IEnumerable items, int size ) { if (items == null) { throw new ArgumentNullException(nameof(items)); } if (size <= 0) { throw new ArgumentOutOfRangeException(nameof(size), size, "Size must be positive."); } return new PartitionPooledEnumerable(items, size); } private sealed class PartitionPooledEnumerable : IEnumerable>> { private readonly IEnumerable _items; private readonly int _size; public PartitionPooledEnumerable(IEnumerable items, int size) { _items = items; _size = size; } public IEnumerator>> GetEnumerator() { return new PartitionPooledEnumerator(_items.GetEnumerator(), _size); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } private sealed class PartitionPooledEnumerator : IEnumerator>> { private readonly IEnumerator _source; private readonly int _size; private readonly Dictionary, PooledResource>> _leases = new(); private readonly Action> _returnLeaseAction; private bool _disposed; public PartitionPooledEnumerator(IEnumerator source, int size) { _source = source; _size = size; _returnLeaseAction = ReturnLease; } public PooledResource> Current { get; private set; } object IEnumerator.Current => Current; public bool MoveNext() { if (_disposed) { return false; } if (!_source.MoveNext()) { Current = default; return false; } PooledResource> pooled = Buffers.GetList(_size, out List partition); partition.Add(_source.Current); int count = 1; while (count < _size && _source.MoveNext()) { partition.Add(_source.Current); count++; } _leases[partition] = pooled; Current = new PooledResource>(partition, _returnLeaseAction); return true; } public void Reset() { throw new NotSupportedException(); } public void Dispose() { if (_disposed) { return; } _disposed = true; _source.Dispose(); foreach (KeyValuePair, PooledResource>> lease in _leases) { lease.Value.Dispose(); } _leases.Clear(); } private void ReturnLease(List partition) { if (_leases.Remove(partition, out PooledResource> pooled)) { pooled.Dispose(); } } } } }