Is this LINQ query with averaging and grouping by hour written efficiently? - c#

This is my first real-world LINQ-to-SQL query. I was wondering if I am making any large, obvious mistakes.
I have a medium-large sized (2M+ records, adding 13k a day) table with data, dataTypeID, machineID, and dateStamp. I'd like to get the average, min, and max of data from all machines and of a specific dataType within a 4 hour period, going back for 28 days.
E.g
DateTime Avg Min Max
1/1/10 12:00AM 74.2 72.1 75.7
1/1/10 04:00AM 74.5 73.1 76.2
1/1/10 08:00AM 73.7 71.5 74.2
1/1/10 12:00PM 73.2 71.2 76.1
etc..
1/28/10 12:00AM 73.1 71.3 75.5
So far I have only been able to group the averages by 1 hour increments, but I could probably deal with that if the alternatives are overly messy.
Code:
var q =
from d in DataPointTable
where d.dateStamp > DateTime.Now.AddDays(-28) && (d.dataTypeID == (int)dataType + 1)
group d by new {
d.dateStamp.Year,
d.dateStamp.Month,
d.dateStamp.Day,
d.dateStamp.Hour
} into groupedData
orderby groupedData.Key.Year, groupedData.Key.Month, groupedData.Key.Day, groupedData.Key.Hour ascending
select new {
date = Convert.ToDateTime(
groupedData.Key.Year.ToString() + "-" +
groupedData.Key.Month.ToString() + "-" +
groupedData.Key.Day.ToString() + " " +
groupedData.Key.Hour.ToString() + ":00"
),
avg = groupedData.Average(d => d.data),
max = groupedData.Max(d => d.data),
min = groupedData.Min(d => d.data)
};

If you want 4 hour increments divide the hour by 4 (using integer division) and then multiply by 4 when creating the new datetime element. Note you can simply use the constructor that takes year, month, day, hour, minute, and second instead of constructing a string and converting it.
var q =
from d in DataPointTable
where d.dateStamp > DateTime.Now.AddDays(-28) && (d.dataTypeID == (int)dataType + 1)
group d by new {
d.dateStamp.Year,
d.dateStamp.Month,
d.dateStamp.Day,
Hour = d.dateStamp.Hour / 4
} into groupedData
orderby groupedData.Key.Year, groupedData.Key.Month, groupedData.Key.Day, groupedData.Key.Hour ascending
select new {
date = new DateTime(
groupedData.Key.Year,
groupedData.Key.Month,
groupedData.Key.Day,
(groupedData.Key.Hour * 4),
0, 0),
avg = groupedData.Average(d => d.data),
max = groupedData.Max(d => d.data),
min = groupedData.Min(d => d.data)
};
To improve efficiency you might want to consider adding an index on the dateStamp column. Given that you're only selecting a potentially small range of the dates, using an index should be a significant advantage. I would expect the query plan to do an index seek for the first date, making it even faster.

Related

How to group an SQL date column stored as int as Year Month using LINQ

I have a column in SQL with a date value (CreDt) stored as an int.
the below Linq2Sql query syntax will group my table by the CreDt date. i.e 20220109 which is not what I want, I want to group it by Year or by YearMonth. i.e 202201
how can I do that?
var yymm = from p in Order
group p by new { ym = p.CreDt } into d
select new { YearMonth = d.Key.ym};
Dropping digits from a number is easy, just divide the date by 100 when you group. The day will be truncated due to integer division.
20220109 / 100 = 202201.09
= 202201
group p by new { ym = p.CreDt / 100 } into d
If you want to group by year, divide by 10000.

c# Create list of selected time intervals

I'm trying to create a downtown that will display times from 00:00 to 24:59 at a selected number of intervals (and this could change from 5 minutes to 10 minutes etc
so for example a list of
00:10
00:20
00:30
or could be
00:15
00:30
I'm using the following which works, but only for a selected number of iterations (33):
List<string> query = Enumerable
.Range(0, 33)
.Select(i => DateTime.Today
.AddHours(0)
.AddMinutes(i * (double)IntervalParameter)
.ToString())
.ToList();
*IntervalParameter = 10 for the example above.
I'm looking to adapt this so it runs the full 24 hours time frame. Just looking for the most efficient way to do this.
Why not compute the number of items?
int IntervalParameter = 5;
// .Range(0, 1440 / IntervalParameter) - see Zohar Peled's comment -
// is brief, but less readable
List<string> query = Enumerable
.Range(0, (int) (new TimeSpan(24, 0, 0).TotalMinutes / IntervalParameter))
.Select(i => DateTime.Today
.AddMinutes(i * (double)IntervalParameter) // AddHours is redundant
.ToString("HH:mm")) // Let's provide HH:mm format
.ToList();
Something like this?
public static IEnumerable<TimeSpan> Intervals(TimeSpan inclusiveStart, TimeSpan exclusiveEnd, TimeSpan increment)
{
for (var time = inclusiveStart; time < exclusiveEnd; time += increment)
yield return time;
}
Example usage:
foreach (var time in Intervals(TimeSpan.Zero, TimeSpan.FromDays(1), TimeSpan.FromMinutes(15)))
{
Console.WriteLine(time);
}

How do I move a Period to the other side of a comparison?

When it comes to Periods, - is not supposed to be the inverse of +: adding one month to March 31 produces April 30. Subtracting one month from April 30 produces March 30, not March 31.
I've got a filter on database records that involves periods:
priorDate + period < today
Here, priorDate is derived from a database column, period can be configured by the user, and I need to find all records for which the condition is true.
If the period doesn't involve months or years, I can transform this into
priorDate < today - period
which allows the comparison to be moved from the client side to the database: it allows me to avoid retrieving all records just to discard the ones that don't meet the criteria.
How do I do this if the period does involve months or years?
I can assume the Gregorian calendar, and that that period is non-negative. (Specifically: I can assume priorDate + period >= priorDate for all values of priorDate, but if possible, I'd like to not rule out periods "one month minus one day" just yet.)
If I've got a period of one month, and today is April 30, then I want to figure out that the expression should become priorDate < new LocalDate(2018, 3, 30), which is easy: that's what today - period produces.
If I've got a period of one month, and today is March 30 2018, then today - period will produce February 28, but the expression should instead become a comparison to March 1: if priorDate is exactly new LocalDate(2018, 2, 28), then priorDate + period < new LocalDate(2018, 3, 30) will be true, but priorDate < new LocalDate(2018, 2, 28) will be false.
Given a LocalDate value d and a Period p:
If p only includes months or years:
If naïve addition or subtraction of p would produce an invalid date, the result is reduced to the end of the month. The resulting date will never be "rounded up" to the next month. It will have its year/month components increased exactly by the amount specified in p. Therefore:
d - p will produce the lowest x such that x + p == d, if there is such an x.
In this case, v + p < d is equivalent to v < x.
Otherwise, d - p will produce largest x such that x + p < d.
In this case, v + p < d is equivalent to v <= x, or v < x + Period.FromDays(1).
Which of these two applies can be detected by comparing d - p + p to d.
So priorDate + period < refDate is equivalent to priorDate < F(period, refDate), where F is defined as.
LocalDate F(Period period, LocalDate refDate)
{
var result = refDate - period;
if (result + period != refDate)
result += Period.FromDays(1);
return result;
}
If p includes both days/weeks and months/years:
Adding or subtracting p will add or subtract the month/year components first, and the day/week components next. Moving the period to the other side of the comparison should subtract or add the day/week components first, the month/year components last. The above F doesn't work for e.g. priorDate == new LocalDate(2000, 1, 30), period == Period.FromMonths(1) + Period.FromDays(1), refDate == new LocalDate(2000, 3, 1): here, priorDate + period == refDate (because first a month is added to produce Feb 29, then a day is added to produce Mar 1) but priorDate < F(period, refDate) (because first a month is subtracted to produce Feb 1, then a day is subtracted to produce Jan 31). For that, it's important to subtract the days component first, completely contrary to how Period arithmetic normally works.
So priorDate + period < refDate is equivalent to priorDate < G(period, refDate), where G is defined as.
LocalDate G(Period period, LocalDate refDate)
{
var result =
refDate
- new PeriodBuilder {Weeks = period.Weeks, Days = period.Days}.Build()
- new PeriodBuilder {Years = period.Years, Months = period.Months}.Build();
if (result + period != refDate)
result += Period.FromDays(1);
return result;
}
Note: the subtraction of new PeriodBuilder {Years = period.Years, Months = period.Months}.Build() subtracts years first, months second. This order must not get reversed, unless another fixup is added. A test case that would otherwise fail is d1 == new LocalDate(2000, 2, 29), p == Period.FromYears(1) + Period.FromMonths(1), d2 == new LocalDate(2001, 3, 31). Subtracting a month from d2 produces Feb 28 2001, then subtracting a year would produce Feb 28 2000, and adding a day produces Feb 29 2000. Subtracting a year first from d2 produces Mar 31 2000, then subtracting a month produces Feb 29 2000, and adding a day produces Mar 1 2000, which is the correct result.
It looks like the assumption in my question that period is non-negative is unnecessary.

Sum of 3 numbers representing Hours in linq query or c#

I have column with spent hours values like (10.30,1.45 ...)
I want to sum those numbers to get the total spent hours.
Example:
Values 11.11,
0.50, 0.59
Total spent hours 11 and the sum of minutes
(11 + 50 + 59) = 120 minutes = 2 hour
So my final output should be 11 + 2 = 13.00 hour
but in my c# query I am getting 11.11 + 0.50 + 0.59 = 12.2 which is wrong as i considered it as hour format.
How can get the result 13.00 hours without splitting the numbers.
I Tried:
db.myTable.Where(t => t.is_deleted == false).Sum(t => t.time_spent)
which is giving me 12.2 but i want 13.00 as i considered it as time.
EDIT:
I used
List<string> hoursTemp1 = Model.tblName.Where(t => t.is_deleted == false).Select(p => p.time_spent.ToString()).ToList();
var total_effort_hr = new TimeSpan(hoursTemp1.Sum(x => TimeSpan.ParseExact(x, "h\\.mm", System.Globalization.CultureInfo.InvariantCulture).Ticks)).ToString(#"hh\:mm");
But now I am Getting OverflowException for Hour value Greater then 24 and minute value greater then 60
So can anyone please help me how to resolve this error and get proper result?
Any help will be apreciated.
var times = new string[] { "11.11", "0.50", "0.59" };
var totalTime = new TimeSpan(times.Sum(x => TimeSpan.ParseExact(x, "h\\.mm", CultureInfo.InvariantCulture).Ticks));
Console.WriteLine(totalTime); //13:00:00
public decimal GetHours(IEnumerable<decimal> args)
{
return args.Sum(x=> Math.Truncate(x) + (x-Math.Truncate(x))*100/60);
}
Or with one truncate:
public decimal GetHours(IEnumerable<decimal> args)
{
return args.Sum(x=> (5*x-2*Math.Truncate(x))/3);
}
But seriously, I will cutoff hand for such datetime handling. Almost every database has type that represents date, time or both. Why not to use them to avoid this kind of crap?

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