Summing hours under a threshold using linq - c#

I am working on a staff rota app and want to sum only the hours below a threshold of 2250 minutes (37.5 hours). I am struggling to isolate these hours however and the reason being is as follows. Firstly I'm new to LINQ.
Secondly, there are two different pay types that can be entered into the app, so I have to sum both pay types using .Sum() which is fine. The problem I'm having is isolating only the summed hours below 37.5 hours
I am grouping the results and running something like
g.Sum(x => x.Start >= start && x.End <= End ? x.Type1 : 0) +
g.Sum(x => x.Start >= start && x.End <= End ? x.Type2 : 0) <= 2250 ? .....
// then count the hours below here.
Now I get that this example will return zero if the count exceeds 2250, but how do I create a subset of the values below 2250 only?

Assuming that Type1 is minutes worked for pay type 1 and Type2 is minutes worked for pay type 2, you could go about it as follows:
Filter the relevant items based on your Start and End conditions
For each item, calculate the total minutes worked (Type1 + Type2)
Filter the total minutes by comparing them to the threshold
Summing total minutes items that are below the threshold
In Linq,
filtering can be done using .Where()
creating a new object for each item in the collection can be done using .Select()
Implementation:
int threshold = 2250;
int filteredTotalMinutes = g
.Where(x => x.Start >= start && x.End <= end)
.Select(x => x.Type1 + x.Type2)
.Where(minutesWorked => minutesWorked <= threshold)
.Sum();
Another possible approach is to do all the filtering first, and then sum the total minutes worked:
int filteredTotalMinutes = g
.Where(x => x.Start >= start && x.End <= end)
.Where(x => x.Type1 + x.Type2 <= threshold)
.Sum(x => x.Type1 + x.Type2);
These implementations will only take into account the work time of the employees that have worked less than or equal to the threshold.
If you rather need to include work time for all employees, but limit the maximum work time that is included in the calculation for each employee to the threshold (i.e. for each employee, use sum = x.Type1 + x.Type2 if sum is less than or equal to the threshold; else, use the threshold), you may utilize Math.Min() to get the lowest value of the total minutes worked (x.Type1 + x.Type2) and the threshold.
The implementation can now be simplified:
int filteredTotalMinutes = g
.Where(x => x.Start >= start && x.End <= end)
.Sum(x => Math.Min(x.Type1 + x.Type2, threshold));
If Type1 and/or Type2 are nullable (e.g. int?), you need to ensure that Math.Min() actually has int values to work with. You will then need to provide a fallback for each nullable value.
This can be achieved by replacing x.Type* with (x.Type* ?? 0), which reads:
Take the value of x.Type* if x.Type* is not null; else, take 0.
If both Type* properties are nullable, the implementation hence becomes:
int filteredTotalMinutes = g
.Where(x => x.Start >= start && x.End <= end)
.Sum(x => Math.Min((x.Type1 ?? 0) + (x.Type2 ?? 0), threshold));
If you cannot use Math.Min(), you could perhaps rather use a ternary operator to select the desired work time portion for each employee. I would then first calculate the total minutes worked for each employee, and then decide if the total minute amount or the threshold value should be used:
int filteredTotalMinutes = g
.Where(x => x.Start >= start && x.End <= end)
.Select(x => (x.Type1 ?? 0) + (x.Type2 ?? 0))
.Sum(minutesWorked => minutesWorked < threshold
? minutesWorked
: threshold);

Related

Group dateTime by hour range

I got a list like this:
class Article
{
...
Public DateTime PubTime{get;set}
...
}
List<Article> articles
Now I want to group this list with hour range :[0-5,6-11,12-17,18-23]
I know there is a cumbersome way to do this:
var firstRange = articles.Count(a => a.PubTime.Hour >= 0 && a.PubTime.Hour <= 5);
But I want to use a elegant way. How can I do that?Use Linq Or anything others?
Group by Hour / 6:
var grouped = articles.GroupBy(a => a.PubTime.Hour / 6);
IDictionary<int, int> CountsByHourGrouping = grouped.ToDictionary(g => g.Key, g => g.Count());
The key in the dictionary is the period (0 representing 0-5, 1 representing 6-11, 2 representing 12-17, and 3 representing 18-23). The value is the count of articles in that period.
Note that your dictionary will only contain values where those times existed in the source data, so it won't always contain 4 items.
You could write a CheckRange Function, which takes your values and returns a bool. To make your code more reusable and elegant.
Function Example:
bool CheckRange (this int number, int min, int max)
=> return (number >= min && number <= max);
You could now use this function to check if the PubTime.Hour is in the correct timelimit.
Implementation Example:
var firstRange = articles.Count(a => a.CheckRange(0, 5));

Compare C# string with Sql Server decimal type

I have a scenario that I have string value in c# and in SQL server the data type is decimal and requirement is that i have to filter value like we do in string which includes (startswith,endswith,contains etc) filters. I am trying like this :
customers = customers.Where(x => Convert.ToString(x.CrmCustomerNumber).Contains(value));
but it's give me error because you can't use Convert.tostring in IQuerable. I know that I can do that
customers.ToList().Where(x => Convert.ToString(x.CrmCustomerNumber).Contains(value));
but I am doing Customer.ToList() at the end after applying all filters. is there any solution of my problem?
If the numbers are of a known size, for instance if they are all six digits, then you could just query for a value range.
Example: I'm looking for all six-digit numbers that start with 123. That's the same as saying "WHERE Value BETWEEN 123000 AND 123999". So you could query .Where(x => x >= 123000 && x <= 123999).
IF the numbers aren't all a consistent size, but at least have some practical limit, you could extend this to say .Where(x => x == 123 || (x >= 1230 && x <= 1239) || (x >= 12300 && x <= 12399) || (x >=123000 && x <= 123999). etc.
With a little math, you could make this work for any number. (x >= n * 10 && x <= ((n * 10) + 9))
EndsWith can be done using modulo math.
Contains... well... you're stuck with a table scan. In that case, you might seriously consider adding a second, perhaps computed column to the table, one that stores the same value as a string, and then indexing that column. Then, use the new column to do the searches.

Sort on anonymous type in DBQuery

I'm struggling with an issue I have to be able to retrieve data from the database efficiently.
I'm using EF 4.3/c#.
Here's my EF logic that pulls back data:
IEnumerable<Offer> GetHotOffers()
{
using (var uow = new UnitOfWork(Connection.Products))
{
var r = new Repository<Offer>(uow.Context);
return r.Find(o =>
o.OfferPopularity != null &&
o.OfferPopularity.TotalVotes > 5 && o.OfferPopularity.PctUpvotes >= 55)
.Include(o => o.OfferPopularity)
.ToList() <= this needs to be removed
.OrderByDescending(o => ** an in memory method **)
.Take(20)
.ToList();
}
}
The Find() method is simple a repository wrapper - it returns DBQuery<T>.
In the database I have 2 pieces of data:
PctUpvotes - a decimal figure showing the % up upvotes (as opposed to
downvotes). Total of both = 100.
TotalVotes - the total number of votes cast.
As you can see from the query, I'm able to narrow down the selection somewhat in the Find() method, which means its done at the database.
However, I want to order the list using data that is not readily available, but can be assumed. I decided not to put it into a computed field because it was tieing changable logic into the database.
Here's the logic I need to incorporate, it's abit like a traffic light system:
Min PctUpvotes 55%, Min TotalVotes: 5 = level 1
Min PctUpvotes 60%, Min TotalVotes: 10 = level 2
Min PctUpvotes 65%, Min TotalVotes: 15 = level 3
Min PctUpvotes 70%, Min TotalVotes: 20 = level 4
So, once I've made the base selection, need to order them by level (descending) then PctUpvotes (descending).
I could do this once the query has been converted to a list, simply by calling a method in the linq logic after the ToList(), something like this:
public static int AdjVotes(int votes, decimal pctUpvotes)
{
if (votes >= 20 && pctUpvotes > 70) return 4;
if (votes >= 15 && pctUpvotes > 65) return 3;
if (votes >= 10 && pctUpvotes > 60) return 2;
return 1;
}
However, it's done in memory, not at the database.
The question I have is: can the logic above be incorporate into the DBQuery logic so that I don't have to call ToList() twice, and have the whole thing run against the database?
Operator (bool)?(trueValue):(falseValue) is compiled to native db query; it will look a little messy though:
return r.Find(o =>
o.OfferPopularity != null &&
o.OfferPopularity.TotalVotes > 5 && o.OfferPopularity.PctUpvotes >= 55)
.Include(o => o.OfferPopularity)
.OrderByDescending(o =>
(o.OfferPopularity.TotalVotes >= 20 && o.OfferPopularity.TotalVotes > 70 ? 4 :
(o.OfferPopularity.TotalVotes >= 15 && o.OfferPopularity.TotalVotes > 65 ? 3 :
(o.OfferPopularity.TotalVotes >= 10 && o.OfferPopularity.TotalVotes > 60 ? 2 : 1))))
.Take(20)
.ToList();

Why do the calculations not work?

In the Linq code below, the count is 16 and the sum is 21 which are correct. However, the score always shows as 100. It should be 76.19. What is happening?
Also, I tried score = sum/count, but I can't seem to use the variable inside the new section. Any suggestions?
.GroupBy(g => g.YR_MNTH)
.Select(x =>
new
{
count = x.Count(),
sum = x.Sum(i=>i.SCORE >= 95? 1:0),
score = (decimal)Math.Round((decimal)(x.Sum(i => i.SCORE >= 95 ? 1 : 0) / x.Count()) * 100, 2)
});
Performing math on integers results in integers. So if you do something like this:
1 / 2
The result will not be 0.5, it will just be 0. So this:
x.Sum(i => i.SCORE >= 95 ? 1 : 0) / x.Count()
Will result in an integer. Later casting that integer to a decimal won't change its value after the fact. You need to cast the individual values before performing math on them:
(decimal)x.Sum(i => i.SCORE >= 95 ? 1 : 0) / (decimal)x.Count()
The problem is that x.Count() is an int and x.Sum(i=>i.SCORE >= 95? 1:0) is an int. An int divided by an int is an int. 21 divided by 16 in integer division is 1 which you are then multiplying by 100. You need to move your decimal cast and place it on one of you operands inside the parenthesis; like this, for example: (decimal)x.Sum(i => i.SCORE >= 95 ? 1 : 0). A decimal divided by an int will result in a decimal so you will be back in business.
On a side note performing these aggregations multiple times is not the most efficient thing to do.

Grouping by every n minutes

I'm playing around with LINQ and I was wondering how easy could it be to group by minutes, but instead of having each minute, I would like to group by every 5 minutes.
For example, currently I have:
var q = (from cr in JK_ChallengeResponses
where cr.Challenge_id == 114
group cr.Challenge_id
by new { cr.Updated_date.Date, cr.Updated_date.Hour, cr.Updated_date.Minute }
into g
select new {
Day = new DateTime(g.Key.Date.Year, g.Key.Date.Month, g.Key.Date.Day, g.Key.Hour, g.Key.Minute, 0),
Total = g.Count()
}).OrderBy(x => x.Day);
What do I have to do to group my result for each 5 minutes?
To group by n of something, you can use the following formula to create "buckets":
((int)(total / bucket_size)) * bucket_size
This will take the total, divide it, cast to integer to drop off any decimals, and then multiply again, which ends up with multiples of bucket_size. So for instance (/ is integer division, so casting isn't necessary):
group cr.Challenge_id
by new { cr.Updated_Date.Year, cr.Updated_Date.Month,
cr.Updated_Date.Day, cr.Updated_Date.Hour,
Minute = (cr.Updated_Date.Minute / 5) * 5 }
//Data comes for every 3 Hours
where (Convert.ToDateTime(capcityprogressrow["Data Captured Date"].ToString()).Date != DateTime.Now.Date || (Convert.ToDateTime(capcityprogressrow["Data Captured Date"].ToString()).Date == DateTime.Now.Date && (Convert.ToInt16(capcityprogressrow["Data Captured Time"])) % 3 == 0))
group capcityprogressrow by new { WCGID = Convert.ToInt32(Conversions.GetIntEntityValue("WCGID", capcityprogressrow)), WCGName = Conversions.GetEntityValue("WIRECENTERGROUPNAME", capcityprogressrow).ToString(), DueDate = Convert.ToDateTime(capcityprogressrow["Data Captured Date"]), DueTime = capcityprogressrow["Data Captured Time"].ToString() } into WCGDateGroup
// For orderby with acsending order
.OrderBy(x => x.WcgName).ThenBy(x => x.DataCapturedDateTime).ThenBy(x => x.DataCapturedTime).ToList();

Categories