Created
May 13, 2024 15:01
-
-
Save Gh61/4b0cc2ac6337fe72826436942ec25a9c to your computer and use it in GitHub Desktop.
Works the same as standard LINQ .GroupBy, but is evaluating items, as you need them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Linq; | |
namespace Gh61 | |
{ | |
public static class LazyLinq | |
{ | |
/// <summary> | |
/// Works the same as standard LINQ .GroupBy, but is evaluating items, as you need them. | |
/// Groups the elements of a sequence according to a specified key selector function. | |
/// </summary> | |
/// <param name="source">An <see cref="T:System.Collections.Generic.IEnumerable`1" /> whose elements to group.</param> | |
/// <param name="keySelector">A function to extract the key for each element.</param> | |
/// <typeparam name="TSource">The type of the elements of <paramref name="source" />.</typeparam> | |
/// <typeparam name="TKey">The type of the key returned by <paramref name="keySelector" />.</typeparam> | |
/// <returns>An IEnumerable<IGrouping<TKey, TSource>> in C# or IEnumerable(Of IGrouping(Of TKey, TSource)) in Visual Basic where each <see cref="T:System.Linq.IGrouping`2" /> object contains a sequence of objects and a key.</returns> | |
/// <exception cref="T:System.ArgumentNullException"> | |
/// <paramref name="source" /> or <paramref name="keySelector" /> is <see langword="null" />. | |
/// </exception> | |
public static IEnumerable<IGrouping<TKey, TSource>> LazyGroupBy<TSource, TKey>( | |
this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) | |
{ | |
if (source == null) throw new ArgumentNullException(nameof(source)); | |
if (keySelector == null) throw new ArgumentNullException(nameof(keySelector)); | |
// dictionary for already created groups | |
var dict = new Dictionary<TKey, LazyGroup<TKey, TSource>>(); | |
// result from | |
var groups = new List<LazyGroup<TKey, TSource>>(); | |
var groupIndex = 0; | |
var sourceEnumerator = new DisposableEnumerator<TSource>(source.GetEnumerator()); | |
// iterate all accumulated groups | |
enumerate: | |
while (groupIndex < groups.Count) | |
{ | |
yield return groups[groupIndex++]; | |
} | |
// search for another group | |
if (FindAnother(FindType.Group, default, dict, groups, sourceEnumerator, keySelector)) | |
{ | |
// another group was found, enumerate it | |
goto enumerate; | |
} | |
} | |
private static bool FindAnother<TKey, TSource>(FindType findType, TKey itemKey, | |
Dictionary<TKey, LazyGroup<TKey, TSource>> dict, List<LazyGroup<TKey, TSource>> groups, | |
DisposableEnumerator<TSource> enumerator, Func<TSource, TKey> keySelector) | |
{ | |
// need to find another group | |
while (!enumerator.IsDisposed && enumerator.MoveNext()) | |
{ | |
var item = enumerator.Current; | |
var key = keySelector(item); | |
// if the group exists, add the item to it | |
if (dict.TryGetValue(key, out var group)) | |
{ | |
group.AddItem(item); | |
// search for item with this key was successful | |
if (findType == FindType.Item && key.Equals(itemKey)) | |
{ | |
return true; | |
} | |
} | |
// the group does not exist - create and return new one | |
else | |
{ | |
// ReSharper disable once AccessToDisposedClosure | |
group = new LazyGroup<TKey, TSource>(key, item, (g) => FindAnother(FindType.Item, g.Key, dict, groups, enumerator, keySelector)); | |
dict.Add(key, group); | |
groups.Add(group); | |
// search for new group was successful | |
if (findType == FindType.Group) | |
{ | |
return true; | |
} | |
} | |
} | |
// enumerator is at the end - dispose it | |
enumerator.Dispose(); | |
// new item or group was not found | |
return false; | |
} | |
private enum FindType | |
{ | |
Group, | |
Item | |
} | |
private class LazyGroup<TKey, TSource> : IGrouping<TKey, TSource> | |
{ | |
private readonly List<TSource> _items; | |
private bool _isOpen = true; | |
private readonly Func<LazyGroup<TKey, TSource>, bool> _requestItem; | |
public LazyGroup(TKey key, TSource first, Func<LazyGroup<TKey, TSource>, bool> requestItem) | |
{ | |
Key = key; | |
_items = new List<TSource>(1) | |
{ | |
first | |
}; | |
_requestItem = requestItem; | |
} | |
public TKey Key { get; } | |
IEnumerator IEnumerable.GetEnumerator() | |
{ | |
return GetEnumerator(); | |
} | |
public IEnumerator<TSource> GetEnumerator() | |
{ | |
var index = 0; | |
// iterate all accumulated items | |
enumerate: | |
while (index < _items.Count) | |
{ | |
yield return _items[index]; | |
index++; | |
} | |
// if the group is still open, request another item | |
if (_isOpen) | |
{ | |
// if the request was successful, group is still open | |
_isOpen = _requestItem(this); | |
// if success, return more item(s) | |
if (_isOpen) | |
{ | |
goto enumerate; | |
} | |
} | |
} | |
/// <summary> | |
/// Will add another item to this group. | |
/// </summary> | |
public void AddItem(TSource item) | |
{ | |
_items.Add(item); | |
} | |
} | |
private class DisposableEnumerator<T> : IEnumerator<T> | |
{ | |
private readonly IEnumerator<T> _source; | |
public DisposableEnumerator(IEnumerator<T> source) | |
{ | |
_source = source; | |
} | |
public bool IsDisposed | |
{ | |
get; | |
private set; | |
} | |
public void Dispose() | |
{ | |
if (IsDisposed) | |
return; | |
_source.Dispose(); | |
IsDisposed = true; | |
} | |
public bool MoveNext() | |
{ | |
if (IsDisposed) | |
throw new ObjectDisposedException(nameof(DisposableEnumerator<T>), "Enumerator is already disposed"); | |
return _source.MoveNext(); | |
} | |
public void Reset() | |
{ | |
if (IsDisposed) | |
throw new ObjectDisposedException(nameof(DisposableEnumerator<T>), "Enumerator is already disposed"); | |
_source.Reset(); | |
} | |
object IEnumerator.Current => Current; | |
public T Current => _source.Current; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment