I'm trying to build a Linq query that filter between 2 times of day.
First I need to filter between dates (ie.: 12/26/2013 to 01/26/2014) and after this search, the results may be filtered between times (ie.: records between 19:00 and 7:00).
I've tried with this query but it does not works:
orders = CurrentOrders.Where(o => o.ImportedOn >= dateFrom && o.ImportedOn <= dateTo);
orders = orders.Where(o => o.ImportedOn.TimeOfDay >= tFrom && o.ImportedOn.TimeOfDay <= tTo);
tFrom and tTo both are TimeSpan.
Any help?
Regards
Edit: Hi I'm editing my own question just to clarify the problem and attach a solution that I've found (I'm not sure if it's the most efficient) :
When I say "does not work" I mean "no records are returned that matched the criteria".
The previous query works only for time between the same day (ie: from 15:00 to 20:00). The problem is when the time span to the next day, for example form 19:00pm to 7:00am.
The solution that I propose is add a simple if to check if tFrom is less than tTo and otherwise add 1 to date in the search:
if (tFrom < tTo)
{
orders = orders.Where(o => o.ImportedOn.TimeOfDay >= tFrom && o.ImportedOn.TimeOfDay <= tTo);
}
else
{
orders = orders.Where(o => o.ImportedOn.TimeOfDay <= tFrom && o.ImportedOn.AddDays(1).TimeOfDay <= tTo);
}
I don't think your queries will give you results when the time range is 9 PM to 4 AM for example. Let's say that ImportedOn is 7 PM and you are checking that 7 PM is less than 9 PM, which is OK, and also checking that 7 PM is less than 4 AM, which is false. I don't see any difference if you add a day because you are only considering the time. Adding a date doesn't change the time.
My proposal is to create two time intervals when the time from is greater than time to (9 PM to 4 AM for example).
I created an extension method for DateTime so we can check if the date belongs to a time range.
public static bool IsInTimeRange(this DateTime obj, DateTime timeRangeFrom, DateTime timeRangeTo)
{
TimeSpan time = obj.TimeOfDay, t1From = timeRangeFrom.TimeOfDay, t1To = timeRangeTo.TimeOfDay;
// if the time from is smaller than the time to, just filter by range
if (t1From <= t1To)
{
return time >= t1From && time <= t1To;
}
// time from is greater than time to so two time intervals have to be created: one {timeFrom-12AM) and another one {12AM to timeTo}
TimeSpan t2From = TimeSpan.MinValue, t2To = t1To;
t1To = TimeSpan.MaxValue;
return (time >= t1From && time <= t1To) || (time >= t2From && time <= t2To);
}
Edited: Note that it is not necessary to compare time with t2From and t1To because the comparison is always gonna be true but it makes the code easier to read because it explicitly checks that the date belongs to one of the two intervals.
I also wrote these unit tests:
[TestMethod]
public void TimeRangeFilter_timeFrom_is_smaller_than_timeTo()
{
// arrange
List<DateTime> dates = new List<DateTime>()
{
DateTime.Today.AddHours(2), // 2 AM
DateTime.Today.AddHours(9), // 9 AM
DateTime.Today.AddHours(12), // 12 PM
DateTime.Today.AddHours(15), // 3 PM
DateTime.Today.AddHours(18), // 6 PM
DateTime.Today.AddHours(23).AddMinutes(50), // 11:50 PM
DateTime.Today, // 0 AM
};
// interval: 10 AM to 4 PM
DateTime timeFrom = DateTime.Today.AddHours(10), timeTo = DateTime.Today.AddHours(16);
// act
var datesInPeriod = dates.Where(p => p.IsInTimeRange(timeFrom, timeTo));
// assert
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 2));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 9));
Assert.IsTrue(datesInPeriod.Any(p => p.Hour == 12));
Assert.IsTrue(datesInPeriod.Any(p => p.Hour == 15));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 18));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 23));
}
[TestMethod]
public void TimeRangeFilter_timeFrom_is_greater_than_timeTo()
{
// arrange
List<DateTime> dates = new List<DateTime>()
{
DateTime.Today.AddHours(2), // 2 AM
DateTime.Today.AddHours(9), // 9 AM
DateTime.Today.AddHours(12), // 12 PM
DateTime.Today.AddHours(15), // 3 PM
DateTime.Today.AddHours(18), // 6 PM
DateTime.Today.AddHours(23).AddMinutes(50), // 11:50 PM
DateTime.Today, // 0 AM
};
// interval: 10 PM to 4 AM
DateTime timeFrom = DateTime.Today.AddHours(22), timeTo = DateTime.Today.AddHours(4);
// act
var datesInPeriod = dates.Where(p => p.IsInTimeRange(timeFrom, timeTo));
// assert
Assert.IsTrue(datesInPeriod.Any(p => p.Hour == 2));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 9));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 12));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 15));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 18));
Assert.IsTrue(datesInPeriod.Any(p => p.Hour == 23));
}
ImportedOn.TimeOfDay
returns just a Time. For example:
DateTime t = DateTime.Now;
System.Diagnostics.Debug.WriteLine(t.ToString());
var t2 = t.TimeOfDay;
System.Diagnostics.Debug.WriteLine(t2.ToString());
returns:
02.06.2014 11:48:33
11:48:33.6671525
So you can just compare the Dates, there is no need of TimeOfDay.
Linq to SQL: how to query a time range in a DateTime field
If you are tring to select on a table the records that have a DateTime field (in this example called DateFiled) between two DateTime value, if you specify:
DateFiled >= dateValue && DateFiled <= dateValue
you select all the record between this value, but if you need to select only the records that between these two dates correspond to a specific time range, you’ll need to add a column that computes the hour.
If you do not want to add it to the database, you will need to add it to your model and set it with the same value as the DateField field. For example:
public async Task<List<MyModel>> GetFilteredResultByDateAndTime
(DateTime startDate, DateTime endDate)
{
var result = from mytable in _context.MyTable
where
mytable.DateField >= startDate.Date
&& mytable.DateField <= endDate.Date
select new MyModel
{
DateField = mytable.DateField.Date,
DateFieldTime = mytable.DateField,
// Other fields of the model
};
// Now you can filter the result by the time
var filteredResult = from r in result
where
r.DateFieldTime.TimeOfDay >= startDate.TimeOfDay
&& r.DateFieldTime.TimeOfDay <= endDate.TimeOfDay
select r;
return await filteredResult.ToListAsync();
}
Related
A person is engaged in different works in different time duration which will start from Monday and end on friday as follows.Monday to Friday will be considered as 1 week.Any overlapping weeks be considered as 1 week.
Below are the scenario
"AssignedEngagementdate":[
{"Startdate":"01/03/2022","Enddate":"01/07/2022"},
{"Startdate":"01/10/2022","Enddate":"01/14/2022"},
{"Startdate":"01/10/2022","Enddate":"01/21/2022"},
{"Startdate":"02/14/2022","Enddate":"02/18/2022"}
]
Here I need to find the no of weeks assigned by this person and it should be 4 since one week is from 10th Jan to 14 Feb is overlapping in 2 engagement.
How I can do this in C# using linq. I was trying to fetch min start date and max end date from list and find the difference and converting in no of weeks but it has not given the actual result since date assigned is not consistent.
The trick is to convert each date range into an enumerated week range, or at least a list of distinct dates (such as Sundays) corresponding to each week or partial week. SelectMany() lets you gather them all together, after which a distinct count gives you an answer.
Try something like:
int numberOfWeeks = AssignedEngagementdate
.SelectMany(a => {
DateTime firstSunday = a.Startdate.AddDays(-(int)a.Startdate.DayOfWeek);
DateTime lastSunday = a.Enddate.AddDays(-(int)a.Enddate.DayOfWeek); // May be same as firstSunday
int weeks = (lastSunday - firstSunday).Days / 7 + 1;
// Enumerate one Sunday per week
return Enumerable.Range(0, weeks).Select(i => firstSunday.AddDays(7 * i));
})
.Distinct()
.Count();
You may still need to consider and test cases where assignments start or end on days other than Monday and Friday. The above should handle most such cases.
Without linq, you can try this Datetime extension :
public static int NumberOfWeeksBetween(this DateTime start, DateTime end)
{
TimeSpan span = end.Subtract(start);
if (span.Days <= 7)
{
if (start.DayOfWeek > end.DayOfWeek) return 2;
else return 1;
}
int days = span.Days - 7 + (int)start.DayOfWeek;
int weekCount = 1;
int dayCount = 0;
for (weekCount = 1; dayCount < days; weekCount++) dayCount += 7;
return weekCount;
}
This question already has answers here:
How to get the next working day, excluding weekends and holidays
(5 answers)
Closed 3 years ago.
I set up some methods to get holidays and used them to calculate next working day. This works fine but it only adds 1 day and gets the next non holiday weekday. I want to get the number of days by input and increment that date according to input and get the nex non holiday weekday. I tried some things but couldnt get exactly what i want.
Edit: I am using a method to get the next working day, excluding weekends and holidays. But my equation does that by incrementing the date only by 1 day. I want that number of days to be my input from user, and add that number to the date, exclude weekends and holidays and get the next working day. (My issue is on the adding that different number from my textbox)
public static DateTime GetWorkingDay(DateTime date, IList<Holiday> holidays, IList<DayOfWeek> weekendDays)
{
date = date.Date.AddDays(1);
var holidayDates = holidays.Select(x => x.GetDate(date.Year))
.Union(holidays.Select(x => x.GetDate(date.Year + 1)))
.Where(x => x != null)
.Select(x => x.Value)
.OrderBy(x => x).ToArray();
while (true)
{
if (weekendDays.Contains(date.DayOfWeek) || holidayDates.Contains(date))
date = date.AddDays(1);
else
return date;
}
}
You could change your while loop to (assuming numDays is your input):
while (numDays > 0)
{
date = date.AddDays(1);
if (!weekendDays.Contains(date.DayOfWeek) && !holidayDates.Contains(date))
numDays--; // Only decrement numDays if it's a working day.
}
Then return the date after the loop. You might want to assert that numDays isn't negative.
Here's an approach that might help. Or it might seem like total overkill. You decide.
Instead of trying to add one day at a time, figure out if it's a holiday, weekday, etc., just create a list of dates that you can query. That will make the logic much easier to understand, especially if you find yourself having to write more and more queries like this.
Ideally this is something you'd want to create once and keep around. It wouldn't make sense to keep creating this over and over.
Here's a stab at it:
public class Calendar
{
private readonly List<CalendarDate> _dates = new List<CalendarDate>();
public Calendar(
DateTime startDate,
int length,
ICollection<DateTime> holidays,
ICollection<DayOfWeek> weekDays)
{
if(length < 1)
throw new ArgumentOutOfRangeException(nameof(length), "The minimum length is one day.");
for (var x = 0; x < length; x++)
{
var addingDate = new CalendarDate
{
RepresentedDate = startDate.Date.AddDays(x),
};
addingDate.IsHoliday = holidays.Contains(addingDate.RepresentedDate);
addingDate.IsWeekday = weekDays.Contains(addingDate.RepresentedDate.DayOfWeek);
_dates.Add(addingDate);
}
FirstDay = startDate.Date;
LastDay = FirstDay.AddDays(length - 1);
}
public DateTime FirstDay { get; }
public DateTime LastDay { get; }
private class CalendarDate
{
internal DateTime RepresentedDate { get; set; }
internal bool IsHoliday { get; set; }
internal bool IsWeekday { get; set; }
internal bool IsWorkingDay => !(IsHoliday || IsWeekday);
}
public DateTime GetNextWorkingDay(DateTime date)
{
date = date.Date;
ValidateDateIsInRange(date);
var result = _dates.FirstOrDefault(d =>
d.RepresentedDate >= date && d.IsWorkingDay);
if (result != null) return result.RepresentedDate;
throw new InvalidOperationException("The result is outside the range of the calendar.");
}
private void ValidateDateIsInRange(DateTime date)
{
if(date < FirstDay || date > LastDay)
throw new InvalidOperationException("The specified date is outside the range of the calendar.");
}
}
It admittedly looks like more work and more code. But the result is that once the data is in place, the calculation you're trying to do becomes very easy. So do all sorts of other calculations and queries, because now such functions can be written to reflect the questions that you're asking, like "What is the first date that matches this criteria?"
I am trying to figure out the best way to search a list of objects for when a certain condition is met within a 24-hour window. I would prefer the 24-hour window be flexible enough that it isn't going by days but just 24-hours. My objects would look like something below.
public class Event {
public DateTime timestamp;
public string reason;
public long amount;
}
So it would search through this list for when the reason equalled something specific and it there was 10 of these within a 24-hour window it would return a list of those 10 along with any extra ones incase there was more.
To search for ALL 24-hour periods (not just 24 hours prior to now or an arbitrary range), you could do:
string reason = "???";
var query =
events.Where(ev => ev.Reason = reason)
.Select(ev => events.Where(ev2 => ev.Reason = reason &&
(ev.timestamp >= ev2.timestamp) &&
(ev.timestamp - ev2.timestamp).TotalHours <= 24))
.Where(g => g.Count() >= 10);
Transation: for each event, get all events within the 24 hours prior to it, and return all groups where that are at least 10 such events.
You can add hours to a DateTime instance and then compare your time
var startOfWindow = DateTime.Now \\ or however you get your window start
var endOfWindow = startOfWindow.AddHours(24);
var interestingEvents = events
.Where(e => e.reason == "reason")
.Where(e=> startOfWindow <= e.timestamp && e.timestamp <= endOfWindow)
.ToList();
Use LINQ
var search = events.Where(ev => ev.reason == "reason" && ev.timestamp >= DateTime.Now.AddHours(-24));
Have a method to filter and then return results like:
public IEnumerable<Event> GetEvents(List<Event> list, string yourReason)
{
var query = list.Where(e => e.reason == "your reason" &&
e.timestamp >= DateTime.Now.AddHours(-24));
if (query.Count() >= 10)
return query;
else
return null;
}
Maybe this will do the trick
public IEnumerable<Event> EventsInTimeWindow(
IEnumerable<Event> events,
DateTime from,
DateTime to,
string reason,
int maxNoOfResults)
{
return events
.Where(evt =>
from <= evt.timestamp &&
evt.timestamp <= to &&
evt.reason == reason)
.Take(maxNoOfResults);
}
Use:
DateTime now = DateTime.Now;
var events = EventsInTimeWindow(someCollection, now.AddHours(-24), now, 15);
I want to group my list by time step (hour, day, week etc.) and count sum for each group but starting from specific time.
Now I've got input list:
TIME VALUE
11:30 2
11:50 2
12:00 6
12:30 10
12:50 2
and hour step
var timeStep=new TimeSpan(1,0,0);
and I'm grouping my list with something like this
var myList = list.GroupBy(x =>
{
return x.Time.Ticks / timeStep.Ticks;
})
.Select(g => new { Time = new DateTime(g.Key * timeStep.Ticks), Value = g.Sum(x => x.Value) }).ToList();
It works fine (also for any other step, e.g. daily, weekly) and gives result:
TIME SUM
11:00 4
12:00 18
But now I have to group my list with hour step but starting from e.g. 30 minute of hour, so what can I do to have something like this:
TIME SUM
11:30 10
12:30 12
It is preferable to use a custom DateTme comparer:
internal class DateTimeComparer : IEqualityComparer<DateTime>
{
public bool Equals(DateTime x, DateTime y)
{
return GetHashCode(x) == GetHashCode(y);
// In general, this shouldn't be written (because GetHashCode(x) can equal GetHashCode(y) even if x != y (with the given comparer)).
// But here, we have: x == y <=> GetHashCode(x) == GetHashCode(y)
}
public int GetHashCode(DateTime obj)
{
return (int)((obj - new TimeSpan(0, 30, 0)).Ticks / new TimeSpan(1, 0, 0).Ticks);
}
}
with:
var myList = list.GroupBy(x => x.Time, new DateTimeComparer())
.Select(g => new { Time = g.Key, Value = g.Sum(x => x.Value) }).ToList();
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();