Igor
Igor

Reputation: 4003

Better way calculate percentage from class properties using LINQ

We have the following definition LINQ:

myList.Select(s=> new DtoTest()
{
    TotalSamples = myList.Count(c=> c.UserId == s.UserId),
    EvaluatedSamples = myList.Count(c=> c.UserId == s.UserId && c.Status == Status.OK)
    PercentageRealized = (myList.Count(c=> c.UserId == s.UserId) / myList.Count(c=> c.UserId == s.UserId && c.Status == Status.OK)) * 100
});

Is there a way where you can assign the property value "PercentageRealized" without using the same functions previously used in: "TotalSamples" & "EvaluatedSamples"?

Something like that:

myList.Select(s=> new DtoTest()
{
    TotalSamples = myList.Count(c=> c.UserId == s.UserId),
    EvaluatedSamples = myList.Count(c=> c.UserId == s.UserId && c.Status == Status.OK)
    PercentageRealized = (TotalSamples / EvaluatedSamples) * 100 //<-!Not possible!
});

Any other tips?

Upvotes: 1

Views: 813

Answers (5)

Derrick Moeller
Derrick Moeller

Reputation: 4950

If you were using an anonymous type this would be more complicated, but since DtoTest is a class, you could always move your math into the property.

public class DtoTest
{
    public float PercentageRealized
    {
        get { return (TotalSamples / EvaluatedSamples) * 100; }
    }
}

Upvotes: 3

NetMage
NetMage

Reputation: 26917

What you are doing seems extremely questionable to me - you are making multiple passes over the source data to re-compute the same thing over and over again (per occurrence of UserId), when it seems like what you should want is to compute once per UserId, like so:

var ans2 = myList.GroupBy(s => s.UserId)
                .Select(sg => {
                    var ts = sg.Count();
                    var es = sg.Count(c => c.Status == Status.OK);
                    return new DtoTest { UserId = sg.Key, TotalSamples = ts, EvaluatedSamples = es, PercentageRealized = (int)(100.0 * ts / es) };
                });

Also, your percentage calculation will use C# integer division and not be close to correct, unless you convert to double first. You can cast back to int after the math.

If you really intended to return multiple results, and want to be efficient (and since I love extension methods), creating an extension method to count chained predicates for one pass:

public static class IEnumerableExt {
    public static (int Cond1Count, int Cond2Count) Count2Chained<T>(this IEnumerable<T> src, Func<T, bool> cond1, Func<T, bool> cond2) {
        int cond1Count = 0;
        int cond2Count = 0;
        foreach (var s in src) {
            if (cond1(s)) {
                ++cond1Count;
                if (cond2(s))
                    ++cond2Count;
            }
        }
        return (cond1Count, cond2Count);
    }
}

Now you can count the sub-values in one pass and compute the third value:

var ans3 = myList.Select(s => {
            var (ts, es) = myList.Count2Chained(c => c.UserId == s.UserId, c => c.Status == Status.OK);
            return new DtoTest { UserId = s.UserId, TotalSamples = ts, EvaluatedSamples = es, PercentageRealized = (int)(100.0 * ts / es) };
        });

Of course, depending on the size of myList, you may still be better off calculating each answer once and then repeating them for the final answer:

var ansd = myList.GroupBy(s => s.UserId)
                 .Select(sg => {
                     var ts = sg.Count();
                     var es = sg.Count(c => c.Status == Status.OK);
                     return new { sg.Key, ts, es };
                 })
                 .ToDictionary(ste => ste.Key, ste => new DtoTest {
                    UserId = ste.Key,
                    TotalSamples = ste.ts,
                    EvaluatedSamples = ste.es,
                    PercentageRealized = (int)(100.0 * ste.ts / ste.es) });
var ans4 = myList.Select(s => ansd[s.UserId]);

Upvotes: 1

Ousmane D.
Ousmane D.

Reputation: 56433

first project to a tuple then project to your custom object:

 myList.Select(s => (tSample: myList.Count(c=> c.UserId == s.UserId), 
       eSample : myList.Count(c=> c.UserId == s.UserId && c.Status == Status.OK)))
      .Select(x => new DtoTest
      {
             TotalSamples = x.tSample,
             EvaluatedSamples = x.eSample,
             PercentageRealized = (x.tSample / x.eSample) * 100 
     });

or use an anonymous type:

myList.Select(s => new 
      {
        tSample = myList.Count(c=> c.UserId == s.UserId),
        eSample = myList.Count(c=> c.UserId == s.UserId && c.Status == Status.OK)
      })
      .Select(x => new DtoTest
      {
            TotalSamples = x.tSample,
            EvaluatedSamples = x.eSample,
            PercentageRealized = (x.tSample / x.eSample) * 100 
     });

Upvotes: 1

Nkosi
Nkosi

Reputation: 247153

Change the function delegate to use the already calculated values

myList.Select(s => {
    var result = new DtoTest() {
        TotalSamples = myList.Count(c => c.UserId == s.UserId),
        EvaluatedSamples = myList.Count(c => c.UserId == s.UserId && c.Status == Status.OK)
    };
    result.PercentageRealized = (result.TotalSamples / result.EvaluatedSamples) * 100;
    return result;
});

Upvotes: 3

Amr Elgarhy
Amr Elgarhy

Reputation: 68932

If PercentageRealized is that simple calculations, why not have the calculation in the property in the class, something like this inside the class DTOTest:

public float PercentageRealized => (TotalSamples / EvaluatedSamples) * 100;

Upvotes: 2

Related Questions