Frage

I would like to create a function using Linq that summarizes an incoming sequence of values. The function should look something like this:

IDictionary<TKey, Summary<TKey>> Summarize<TKey, TValue>(IEnumerable<TValue> values)
{
    return values
        .ToLookup(val => GetKey(val))         // group values by key
        .Union(*an empty grouping*)           // make sure there is a default group
        .ToDictionary(
            group => group.Key,
            group => CreateSummary(group));   // summarize each group
}

The catch is that the resulting IDictionary should have an entry for default(TKey) even if the incoming sequence contains no values with that key. Can this be done in a purely functional way? (Not using mutable data structures.)

The only way I can think to do it is by calling .Union on the lookup before piping it into a dictionary. But that would require me to create an empty IGrouping, which does not appear to be possible without an explicit class. Is there an elegant way to do this?

Edit: We can assume that TKey is a value type.

War es hilfreich?

Lösung

You can't get empty groups from GroupBy, nor from ToLookup. Perhaps there's an intentional reason.

Can this be done in a purely functional way? (Not using mutable data structures.)

While such academic requirements can be fun, any solution should be compared to the simplicity of a straight-forward implementation.

Dictionary<TKey, Summary<TKey>> result = values
  .GroupBy(val => GetKey(val))
  .ToDictionary(g => g.Key, g => CreateSummary(g));

TKey x = default(TKey);
if (!result.ContainsKey(x))
{
  result[x] = CreateSummary(Enumerable.Empty<TValue>());
}

return result;

Now if you want an Empty Group, just have to add a class for it:

public class EmptyGroup<TKey, TValue> : IGrouping<TKey, TValue>
{
  public TKey Key {get;set;}

  public IEnumerator GetEnumerator()
  {
    return GetEnumerator<TValue>();
  }
  public IEnumerator<TValue> GetEnumerator<TValue>()
  {
    return Enumerable.Empty<TValue>().GetEnumerator<TValue>();
  }
}

Used like this:

EmptyGroup<TKey, TValue> empty = new EmptyGroup<TKey, TValue>(Key = default<TKey>());

Andere Tipps

The accepted answer is what I was looking for but it didn't work for me. Maybe I missed something but it didn't compile. I had to modify the code to fix it. Here is the code that worked for me:

public class EmptyGroup<TKey, TValue> : IGrouping<TKey, TValue>
{
    public TKey Key { get; set; }

    public IEnumerator<TValue> GetEnumerator()
    {
        return Enumerable.Empty<TValue>().GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

used like this

var emptyGroup = new EmptyGroup<Customer, AccountingPaymentClient>();

You can add a second select in which you check the lookup table has any entries and if not, create a new lookup table. This is different from your proposed union solution in that this doesn't add the default value IF there are other values.

see:

IDictionary<TKey, Summary<TKey>> Summarize<TKey, TValue>(IEnumerable<TValue> values) 
{ 
    return values 
        .ToLookup(val => GetKey(val))         // group values by key 
        .Select(x => x.Any() ? x : Enumerable.Repeat(default(TKey), 1).ToLookup(x => GetKey(x)))
        .ToDictionary( 
            group => group.Key, 
            group => CreateSummary(group));   // summarize each group 
} 

If you want the solution with an union, you can use the same logic to create a default lookup table like:

IDictionary<TKey, Summary<TKey>> Summarize<TKey, TValue>(IEnumerable<TValue> values) 
{ 
    return values 
        .ToLookup(val => GetKey(val))         // group values by key 
        .Union(Enumerable.Repeat(default(TKey), 1).ToLookup(x => GetKey(x)))
        .ToDictionary( 
            group => group.Key, 
            group => CreateSummary(group));   // summarize each group 
} 

Hope this helps

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top