Marty
Marty

Reputation: 3555

Linq Extension to Return Grouped results

I have the following code, and trying to avoid code duplication What I'd like to do is have a Custom Extension method say called "SelectGrouped"

that would accept a "Shift" object, and return the anonymous type (not sure if that possible)

var groupedFillingsCurrentShift = currentShift.FilingMeasurements
                    .Where(f => TimeSpan.Parse(f.MeasurementTime.MeasurementTime) == ShiftEnd)
                    .GroupBy(f => new { f.Medium, f.MeasurementTime })
                    .Select(t => new { t.Key.Medium, t.Key.MeasurementTime, VolumeInTanks = t.Sum(s => s.Filing) })
                    .ToList();


if (previousShift != null)
{
    var groupedFillingsPreviousShift = previousShift.FilingMeasurements
        .Where(f => TimeSpan.Parse(f.MeasurementTime.MeasurementTime) == previousShift.ShiftEnd)
        .GroupBy(f => new { f.Medium, f.MeasurementTime })
        .Select(t => new { t.Key.Medium, t.Key.MeasurementTime, VolumeInTanks = t.Sum(s => s.Filing) })
        .ToList();

What I'd like to achieve is to have it look like so

var groupedResultCurrentShift = 
   currentShift.SelectGrouped(t=>t.Medium, t.MeasurementTime, t.VolumenInTanks);

var groupedResultPreviousShift = 
   previousShift.SelectGrouped(t=>t.Medium, t.MeasurementTime, t.VolumenInTanks);

Upvotes: 0

Views: 94

Answers (2)

Jon Hanna
Jon Hanna

Reputation: 113242

var groupedResultCurrentShift = 
 currentShift.SelectGrouped(t=>t.Medium, t.MeasurementTime, t.VolumenInTanks);

Makes no sense, because the t of the second and third clause isn't defined, and VolumenInTanks relates to a property that you haven't created yet.

We also have a problem, in that the Select in your query relates to an anonymous object defined earlier in the same scope. This prevents us from defining a method that lets us pass in a lambda defining what should be selected.

However:

currentShift.FilingMeasurements
                .Where(f => TimeSpan.Parse(f.MeasurementTime.MeasurementTime) == ShiftEnd)
                .GroupBy(f => new { f.Medium, f.MeasurementTime })
                .Select(t => new { t.Key.Medium, t.Key.MeasurementTime, VolumeInTanks = t.Sum(s => s.Filing) })
                .ToList();

is equivalent to;

currentShift.FilingMeasurements
                .Where(f => TimeSpan.Parse(f.MeasurementTime.MeasurementTime) == ShiftEnd)
                .GroupBy(f => new { f.Medium, f.MeasurementTime }, s => s.Filing)
                .Select(t => new { t.Key.Medium, t.Key.MeasurementTime, VolumeInTanks = t.Sum() })
                .ToList();

Now, this still isn't quite there, but let's consider that we get the same information from:

currentShift.FilingMeasurements
                .Where(f => TimeSpan.Parse(f.MeasurementTime.MeasurementTime) == ShiftEnd)
                .GroupBy(f => new { f.Medium, f.MeasurementTime }, s => s.Filing)
                .Select(t => new { t.Key, VolumeInTanks = t.Sum() })
                .ToList();

So. If you'd be willing to have to do item.Key.Medium rather than item.Medium, then we're in business:

We need a return type, so we'll create one:

public class GroupedCount<T>
{
  public<T> Key{get;set;}
  public int Count{get;set;}//bit more of a general-purpose name than VolumeInTanks
}

Now, create our method:

public static List<GroupedCount<TKey>> ToGroupedCounts<TSource, TKey>(this IQueryable<TSource> source, Func<TSource, bool> pred, Func<TSource, TKey> keyGen, Func<TSource, int> tallyGen)
{
    currentShift.FilingMeasurements
                .Where(pred)
                .GroupBy(keyGen, tallyGen)
                .Select(t => new GroupedCount<TKey>{ Key = t.Key, Count = t.Sum() })
                .ToList();
}

We can now call:

currentShift.ToGroupedCounts(
  f => TimeSpan.Parse(f.MeasurementTime.MeasurementTime) == ShiftEnd,
  f => new { f.Medium, f.MeasurementTime },
  s => s.Filing
);

Since we want this to be general purpose, and ToList() shouldn't be called unless it's definitely needed in the specific case, it makes more sense to return an IQueryable:

public static IQueryable<GroupedCount<TKey>> ToGroupedCounts<TSource, TKey>(this IQueryable<TSource> source, Func<TSource, bool> pred, Func<TSource, TKey> keyGen, Func<TSource, int> tallyGen)
{
    currentShift.FilingMeasurements
                .Where(pred)
                .GroupBy(keyGen, tallyGen)
                .Select(t => new GroupedCount<TKey>{ Key = t.Key, Count = t.Sum() });
}

Upvotes: 0

KeithS
KeithS

Reputation: 71563

Unfortunately, what you want can't be done in its current form.

First off, the variable t only has value in the lambda. The commas separate parameters, and so t loses its scope for the other parameters.

Second and more importantly, anonymous types are locally scoped by definition, because you have to use "var" to imply the type of a variable that holds them. You can't pass them in as parameters nor return them as return values (well, technically you can using dynamic, but let's not go there, please).

If you were to define a named type that could be used to hold the final form of the projected data, this would work:

internal class VolumeData
{
   public string Medium;
   public DateTime MeasurementTime;
   public decimal VolumeInTanks;
}

public static List<VolumeData> GetVolumeData(this Shift shift)
{
   return shift.FilingMeasurements
               .Where(f => TimeSpan.Parse(f.MeasurementTime.MeasurementTime) == ShiftEnd)
               .GroupBy(f => new { f.Medium, f.MeasurementTime })
               .Select(t => new VolumeData{ 
                                Medium = t.Key.Medium, 
                                MeasurementTime = t.Key.MeasurementTime, 
                                VolumeInTanks = t.Sum(s => s.Filing) })
               .ToList();
}

...

var groupedFillingsCurrentShift = currentShift.GetVolumeData();


if (previousShift != null)
    var groupedFillingsPreviousShift = previousShift.GetVolumeData();

Upvotes: 2

Related Questions