Есть ли лучший способ объединить словарь с помощью LINQ? - PullRequest
8 голосов
/ 26 июля 2010

Я пытаюсь построить словарь из перечислимого, но мне нужен агрегатор для всех потенциально дублирующих ключей. Непосредственное использование ToDictionary () иногда приводило к дублированию ключей.

В этом случае у меня есть несколько записей времени ({DateTime Date, double Hours}), и если несколько записей времени происходят в один и тот же день, я хочу указать общее время для этого дня. То есть пользовательский агрегатор, который даст мне уникальный ключ для словарной статьи.

Есть ли лучший способ сделать это, чем этот?

(Это работает.)

    private static Dictionary<DateTime, double> CreateAggregatedDictionaryByDate( IEnumerable<TimeEntry> timeEntries )
    {
        return
            timeEntries
                .GroupBy(te => new {te.Date})
                .Select(group => new {group.Key.Date, Hours = group.Select(te => te.Hours).Sum()})
                .ToDictionary(te => te.Date, te => te.Hours);
    }

Мне кажется, я действительно ищу что-то вроде этого:

IEnumerable<T>.ToDictionary( 
    /* key selector : T -> TKey */, 
    /* value selector : T -> TValue */, 
    /* duplicate resolver : IEnumerable<TValue> -> TValue */ );

так ...

timeEntries.ToDictionary( 
    te => te.Date, 
    te => te.Hours, 
    duplicates => duplicates.Sum() );

'resolver' может быть .irst () или .Max () или что-то еще.

Или что-то подобное.


У меня была одна реализация ... и другая появилась в ответах, когда я работал над ней.

Mine:

    public static Dictionary<TKey, TValue> ToDictionary<T, TKey, TValue>(
        this IEnumerable<T> input, 
        Func<T, TKey> keySelector, 
        Func<T, TValue> valueSelector, 
        Func<IEnumerable<TValue>, TValue> duplicateResolver)
    {
        return input
            .GroupBy(keySelector)
            .Select(group => new { group.Key, Value = duplicateResolver(group.Select(valueSelector)) })
            .ToDictionary(k => k.Key, k => k.Value);
    }

Я надеялся, что что-то подобное уже было, но, думаю, нет. Это было бы хорошим дополнением.

Спасибо всем: -)

Ответы [ 5 ]

5 голосов
/ 26 июля 2010
public static Dictionary<KeyType, ValueType> ToDictionary
  <SourceType, KeyType, ValueType>
(
  this IEnumerable<SourceType> source,
  Func<SourceType, KeyType> KeySelector,
  Func<SourceType, ValueType> ValueSelector,
  Func<IGrouping<KeyType, ValueType>, ValueType> GroupHandler
)
{
  Dictionary<KeyType, ValueType> result = source
    .GroupBy(KeySelector, ValueSelector)
    .ToDictionary(g => g.Key, GroupHandler);
}

Вызывается:

Dictionary<DateTime, double> result = timeEntries.ToDictionary(
  te => te.Date,
  te => te.Hours,
  g => g.Sum()
);
3 голосов
/ 26 июля 2010

Если дублирующиеся ключи являются проблемой, возможно, вы имеете в виду ToLookup? Тот же принципал, но несколько значений на ключ ...

private static ILookup<DateTime, double> CreateAggregatedDictionaryByDate( IEnumerable<TimeEntry> timeEntries )
{
    return
        timeEntries
            .GroupBy(te => new {te.Date})
            .Select(group => new {group.Key.Date, Hours = group.Select(te => te.Hours).Sum()})
            .ToLookup(te => te.Date, te => te.Hours);
}

Тогда вы просто делаете что-то вроде:

var lookup = CreateAggregatedDictionaryByDate(...);
foreach(var grp in lookup) {
    Console.WriteLine(grp.Key); // the DateTime
    foreach(var hours in grp) { // the set of doubles per Key
        Console.WriteLine(hours)
    }
}

или используйте SelectMany конечно (from...from).

0 голосов
/ 26 июля 2010

Вы ищете что-то подобное?

private static Dictionary<DateTime, double> CreateAggregatedDictionaryByDate( IEnumerable<TimeEntry> timeEntries ) 
{ 
    return 
        (from te in timeEntries
        group te by te.Date into grp)
        .ToDictionary(grp => grp.Key, (from te in grp select te.Hours).Sum());
} 
0 голосов
/ 26 июля 2010

Мне нравится ваш метод, потому что он понятен, но если вы хотите сделать его более эффективным, вы можете сделать следующее, которое выполнит всю агрегацию и группировку в одном вызове Aggregate, хотя и немного запутанном.

private static Dictionary<DateTime, double> CreateAggregatedDictionaryByDate(IEnumerable<TimeEntry> timeEntries)
{
    return timeEntries.Aggregate(new Dictionary<DateTime, double>(),
                                 (accumulator, entry) =>
                                    {
                                        double value;
                                        accumulator.TryGetValue(entry.Date, out value);
                                        accumulator[entry.Date] = value + entry.Hours;
                                        return accumulator;
                                    });
}
0 голосов
/ 26 июля 2010

Если вы получаете доступ к индексатору словаря и там ничего нет, он позволяет вам установить, что он возвращает стандартную конструкцию типа данных, в случае double это будет 0. Я, возможно, сделал бы что-то вроде

public void blabla(List<TimeEntry> hoho)
{
    Dictionary<DateTime, double> timeEntries = new Dictionary<DateTime, double>();
    hoho.ForEach((timeEntry) =>
        {
            timeEntries[timeEntry.Day] = 0;
        });

    hoho.ForEach((timeEntry) =>
        {
            timeEntries[timeEntry.Day] += timeEntry.Hours;
        });

}

Просто использовал List, потому что по неизвестным причинам расширение .ForEach () не реализовано в ienumerable, даже если бы я предполагал, что реализация будет строкой для идентичной строки, но вы могли бы просто выполнить литерал foreach (), которыйв любом случае это то, что он делает под прикрытием.

Я думаю, с точки зрения читабельности, это намного упрощает понимание того, что делается, если только это не то, что вы пытались сделать ..

...