c# - ¿Hay una mejor manera de agregar un diccionario usando LINQ?
aggregate group-by (5)
Estoy intentando construir un diccionario a partir de un enumerable, pero necesito un agregador para todas las claves potencialmente duplicadas. El uso de ToDictionary () directamente causaba ocasionalmente claves duplicadas.
En este caso, tengo un montón de entradas de tiempo ({DateTime Date, double Hours}), y si se producen varias entradas de tiempo en el mismo día, quiero el tiempo total para ese día. Es decir, un agregador personalizado, que me dará una clave única para una entrada de diccionario.
¿Hay una mejor manera de hacerlo que esto?
(Esto funciona)
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);
}
Creo que realmente estoy buscando algo como esto:
IEnumerable<T>.ToDictionary(
/* key selector : T -> TKey */,
/* value selector : T -> TValue */,
/* duplicate resolver : IEnumerable<TValue> -> TValue */ );
asi que...
timeEntries.ToDictionary(
te => te.Date,
te => te.Hours,
duplicates => duplicates.Sum() );
El ''resolver'' podría ser .Primero () o .Max () o lo que sea.
O algo similar.
Tuve una implementación ... y apareció otra en las respuestas mientras estaba trabajando en eso.
Mía:
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);
}
Esperaba que ya hubiera algo así, pero supongo que no. Eso sería un buen complemento.
Gracias a todos :-)
¿Estás buscando algo así?
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());
}
Me gusta su método porque está claro, pero si desea hacerlo más eficiente, puede hacer lo siguiente, que hará toda la agregación y la agrupación en una única llamada Aggregate
, aunque sea un poco intrincada.
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;
});
}
Si accedes al indexador de un diccionario y no hay nada allí, te permite establecer que devuelve una construcción predeterminada del tipo de datos, en el caso de un doble será 0. Tal vez haría algo así como
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;
});
}
Solo usé List porque, por razones desconocidas, la extensión .ForEach () no está implementada en ienumerable, aunque supongo que la implementación sería line for line idéntica, pero podrías hacer un literal foreach () que es lo que hace bajo las cubiertas de todos modos.
Creo que desde el punto de vista de la legibilidad, esto hace que el punto sea mucho más fácil de lo que se está haciendo, a menos que esto no sea lo que estabas tratando de hacer.
Si las claves duplicadas son un problema, ¿quizás te refieres a ToLookup
? Mismo principio, pero valores múltiples por clave ...
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);
}
Entonces simplemente haces algo como:
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)
}
}
o use SelectMany
por supuesto ( from...from
).
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);
}
Llamado por:
Dictionary<DateTime, double> result = timeEntries.ToDictionary(
te => te.Date,
te => te.Hours,
g => g.Sum()
);