1
\$\begingroup\$

I have the following snippet of code which gets a load of time intervals (every hour, every 2 hours etc), then groups the results for each of the intervals (e.g, the max value for each hour). This data needs to go into an array (which will be sent to a Chart).

The user can select if they wish to group my Min, Max or Average.

At the moment I have a switch statement that has the same code in each branch except the Max/Min/Average bit at the end.

Can I avoid duplicating the code whilst still having the Min/Max/Average conditional?

The code as it is:

var vm = new MyViewModel();
var times = getTimeIntervals();
var results = getResults();

vm.GroupedData =
   times.Select(x =>
      new object[] {
         x.Start.ToInstant().Ticks,
         results.Data
             .Where(i => x.Intersects(i.Date))
             .Select(v => v)
             .DefaultIfEmpty()
             .Max(i => i == null ? 0m : decimal.Parse(i.Value))
             //.Min(i => i == null ? 0m : decimal.Parse(i.Value))
             //.Average(i => i == null ? 0m : decimal.Parse(i.Value))
   });

The code I'd like:

vm.GroupedData =
           times.Select(x =>
              new object[] {
                 x.Start.ToInstant().Ticks,
                 results.Data
                     .Where(i => x.Intersects(i.Date))
                     .Select(v => v)
                     .DefaultIfEmpty()
                     .If(Avg)
                         .Average(i => i == null ? 0m : decimal.Parse(i.Value))
                     .ElseIf(Max)
                         .Max(i => i == null ? 0m : decimal.Parse(i.Value))
                     .ElseIf(Min)
                         .Min(i => i == null ? 0m : decimal.Parse(i.Value))
           });
\$\endgroup\$
3
\$\begingroup\$

First of all you can simplify your Code to:

vm.GroupedData =
   times.Select(time =>
      new { Ticks = time.Start.ToInstant().Ticks,
            Aggregate = results.Data
             .Where(data => time.Intersects(data.Date))
             .Select(data => data == null ? 0m : decimal.Parse(data.Value))
             .DefaultIfEmpty()
             .Max()
             //.Min()
             //.Average()
   });

1.) Remove the Select(v => v)

The Select doesn't actually do anythin in your case, you will get the same result without it. Select is used to project the single elements in your result into another format. E.g. get only a list of the Values insted of the whole object.

2.) Remove the DefaultIfEmpty()

With the DefaultIfEmpty you get a List containing only one "null" element. which would produce the same result as calling Max, Min or Average on an empty list.

3.) Transform the Results before using aggregates

Transform your result list to a list of all the values as decimal with a Select instead of doing this in each of the subfunctions.

4.) Give x and i meaningful Names

5.) Use annonymous Type instead of object array or even better create own type for the tick-aggregate pair


To your actual question: Instead of calculating the Aggregate you could store the IEnumerable in your result object and then do a second select on that result with your desired aggregate function. As long as you store the query as IEnumerable the query shouldn't be executed until you want to access the actual values of the result.

var myDataRowsPerTime = times.Select(time =>
      new { Tick = time.Start.ToInstant().Ticks,
            DataRow = results.Data
             .Where(data => time.Intersects(data.Date))
             .Select(data => data == null ? 0m : decimal.Parse(data.Value))
             .DefaultIfEmpty()
   }); 

switch(aggregationMethod)
{
    case AggregationMethod.Avg:
        vm.Groupdata = myDataRowsPerTime.Select(item =>
             new { Tick = item.Tick, Aggregate = item.DataRow.Avg() });
        break;
   case AggregationMethod.Max:
         ...

You should define a own class for the last result, there maybe a problem with accessing the Data of the annonymous Type in the viewmodel. The viewmodel doesn't know the structure of this annonymous type.

\$\endgroup\$
4
  • \$\begingroup\$ Brilliant answer. Really appreciated. Thanks. \$\endgroup\$ – ETFairfax May 12 '16 at 13:33
  • 1
    \$\begingroup\$ thanks. you should consider if no values realy count as zero or if you want to exclude this values. by only taking non null values into account. To exclude null values you could add a check for non null in the where method \$\endgroup\$ – BoeseB May 12 '16 at 14:08
  • 1
    \$\begingroup\$ Calling Max, Min, or Average on an empty list results in an exception. \$\endgroup\$ – Risky Martin May 12 '16 at 18:59
  • \$\begingroup\$ Thanks for your comment @RiskyMartin. I corrected the answer and sorry for the wrong informaiton. \$\endgroup\$ – BoeseB May 13 '16 at 6:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.