Skip to content

Instantly share code, notes, and snippets.

@Gh61
Created May 13, 2024 15:01
Show Gist options
  • Save Gh61/4b0cc2ac6337fe72826436942ec25a9c to your computer and use it in GitHub Desktop.
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.
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&lt;IGrouping&lt;TKey, TSource&gt;&gt; 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