patterncsharpMinor
Finding out which user has made the most progress
Viewed 0 times
themadeuserhasprogressfindingwhichoutmost
Problem
I have some code to accomplish the objective of finding out which user has made the most progress. Progress is measured in integer values, the higher the better.
The
Is this a proper way to accomplish this objective? Can it be improved?
Personally I would like to solve the issue a bit more functional, without declaring the variables above a foreach loop.
SubmittedAnswers consist of among others a int UserId and a int Progress.The
answers variable consist of a list of SubmittedAnswers as described above. To find out which user has made the most progress of this list I have the following algorithm:///
/// Method used to find out which user had made the most progress in a given time range.
///
///
///
public async Task MostProgressAsync(ITimeRange timeRange)
{
IList answers = await GetAnswersInRange(timeRange);
IEnumerable> answersOfUsers = answers.GroupBy(a => a.UserId);
int maxProgress = int.MinValue;
var maxProgressUserId = 0;
foreach (IGrouping answersOfUser in answersOfUsers)
{
int progress = answersOfUser.Sum(a => a.Progress);
if (progress > maxProgress)
{
maxProgress = progress;
maxProgressUserId = answersOfUser.Key;
}
}
return new User { UserId = maxProgressUserId };
}SubmittedAnswers and User looks as follows:public class SubmittedAnswer
{
public ObjectId Id { get; set; }
public int SubmittedAnswerId { get; set; }
public BsonDateTime SubmitDateTime { get; set; }
public bool Correct { get; set; }
public int Progress { get; set; }
}
public class User
{
public int UserId { get; set; }
}Is this a proper way to accomplish this objective? Can it be improved?
Personally I would like to solve the issue a bit more functional, without declaring the variables above a foreach loop.
Solution
The method's name can be improved. I like that you ended it with
This tells me
Now, returning an
LINQ came with C# 3.5, and that version introduced a little wonder to avoid declaring obscure types such as an
Explicit typing is actually quite annoying here:
Compare to:
There's a typo there that becomes immediately apparent: the loop variable is named exactly the same thing at the collection that's being iterated - that code wouldn't compile... so I'll assume you meant to have this:
Now now now... you have a grouping, and you're entering a
So we have all the answers in the time range we're interested in:
And what we're after, is the
Next, we want to know how much progress each user has - we can do this by projecting a selection into an anonymous type, with the
And this is where
What's next? Well, what we have now could be visualized as a table with a
And then, all we need to do is grab the first "row":
And then we can create the
...That's all nice, but it's excessively verbose:
The good news is that LINQ is awesome, and lets you chain all these methods, so you can actually do this:
Note that
Async, it makes it compliant with the convention for async Task methods - however there's another naming convention programmers use all the time: method names should start with a verb! FindUserWithMostProgressAsync might be a better name, unless that method is located in the User class itself (then it would be misplaced).IList answers = await GetAnswersInRange(timeRange);This tells me
GetAnswersInRange is an async function that isn't following the convention - should be GetAnswersInRangeAsync, but then it might be a better idea to just call it GetAnswersAsync and let overloading speak the "in range" part: parameterless returns all answers, overload taking an ITimeRange returns all answers within the specified time range - and that can be nicely documented with XML comments.Now, returning an
IList when all you really need is to iterate the results, is bad communication: IEnumerable would have worked better.IEnumerable> answersOfUsers = answers.GroupBy(a => a.UserId);LINQ came with C# 3.5, and that version introduced a little wonder to avoid declaring obscure types such as an
IEnumerable - implicit typing changes strictly nothing to how the code works, saves you a bunch of typing, and makes shorter, more readable code here:var answersOfUsers = answers.GroupBy(a => a.UserId);Explicit typing is actually quite annoying here:
foreach (IGrouping answersOfUser in answersOfUsers)Compare to:
foreach (var answersOfUser in answersOfUsers)There's a typo there that becomes immediately apparent: the loop variable is named exactly the same thing at the collection that's being iterated - that code wouldn't compile... so I'll assume you meant to have this:
foreach (var answerOfUser in answersOfUsers)Now now now... you have a grouping, and you're entering a
foreach loop, only to filter some more - that foreach loop could be converted to a LINQ query.So we have all the answers in the time range we're interested in:
var answers = await GetAnswersInRange(timeRange);And what we're after, is the
UserId for the user that has accumulated the most progress with these answers. Grouping by UserId was a good first step.var userAnswers = answers.GroupBy(answer => answer.UserId);Next, we want to know how much progress each user has - we can do this by projecting a selection into an anonymous type, with the
Select method:var userProgress = userAnswers.Select(userAnswer =>
new { UserId = userAnswer.Key, Progress = userAnswer.Sum(answer => answer.Progress) });And this is where
var has a clear advantage over explicit typing - exactly what is the type of userProgress here? It's an IQueryable, where T is an anonymous type with some int UserId and int Progress members: we don't need to care what the actual type is.What's next? Well, what we have now could be visualized as a table with a
UserId and a Progess column. The next step is to find out which user has the most progess, and we can sort the results to get that, in descending order so we have the top user, well, at the top:var sortedUserProgress = userProgress.OrderByDescending(progress => progress.Progress);And then, all we need to do is grab the first "row":
var topUserId = sortedUserProgress.First().UserId;And then we can create the
User object for the return value:return new User { UserId = topUserId };...That's all nice, but it's excessively verbose:
var answers = await GetAnswersInRange(timeRange);
var userAnswers = answers.GroupBy(answer => answer.UserId);
var userProgress = userAnswers.Select(userAnswer =>
new { UserId = userAnswer.Key, Progress = userAnswer.Sum(answer => answer.Progress) });
var sortedUserProgress = userProgress.OrderByDescending(progress => progress.Progress);
var topUserId = sortedUserProgress.First().UserId;The good news is that LINQ is awesome, and lets you chain all these methods, so you can actually do this:
var answers = await GetAnswersInRange(timeRange);
var userId = answers.GroupBy(answer => answer.UserId)
.Select(userAnswer => new
{
UserId = userAnswer.Key,
Progress = userAnswer.Sum(answer => answer.Progress)
})
.OrderByDescending(progress => progress.Progress)
.First().UserId;Note that
.First() will throw an exception if there is no element in the sorted results. You could use .FirstOrDefault() instead, but then the .UserId member call would have to go on a separate instruction, because FirstOrDefault() will return null when there's no "first element", which means you'd be calling .UserId on a null reference, which would obviously throw a NullReferenceException... and your current code Code Snippets
IList<SubmittedAnswer> answers = await GetAnswersInRange(timeRange);IEnumerable<IGrouping<int, SubmittedAnswer>> answersOfUsers = answers.GroupBy(a => a.UserId);var answersOfUsers = answers.GroupBy(a => a.UserId);foreach (IGrouping<int, SubmittedAnswer> answersOfUser in answersOfUsers)foreach (var answersOfUser in answersOfUsers)Context
StackExchange Code Review Q#98053, answer score: 5
Revisions (0)
No revisions yet.