Check whether a "Recurring Date" lies between two DateTime instances - c#

We have a class called ABDate which contains among other things a Month, a nullable Year and a Day. When creating an instance of ABDate if the year is given as null, the date is considered recurring. There is an IsRecurring property as well.
Now we have a IList<ABDate>, and we are given a start and an end DateTime. How do we check to see whether any ABDate in the list lies in the start and end interval.

For the non-recurring case the solution is obvious. Given a collection abDates and start and end instances:
var anyInBetween = abDates
.Select(ab => new DateTime(ab.Year.Value, ab.Month, ab.Day))
.Any(d => d >= start && d <= end);
For the recurring case, we can observe that since no month has more than 31 days, a function such as f(month, day) = month * 100 + day will project each distinct (month, day) tuple into a unique integer. Comparing two such integers allows us to determine which of the source tuples represents a day earlier in the year:
// Note: m * 100 is "random". Multiplying by any factor > 31 will work as well.
Func<int, int, int> customTotalOrder = (m, d) => m * 100 + d;
var startIndex = customTotalOrder(start.Month, start.Day);
var endIndex = customTotalOrder(end.Month, end.Day);
var spanningYears = end.Year - start.Year;
var anyInBetween = false;
switch (spanningYears)
{
default:
case 2:
// start => end contains at least one whole year, so obviously...
anyInBetween = abDates.Any();
case 1:
var projection = abDates.Select(ab => customTotalOrder(ab.Month, ab.Day));
anyInBetween = startIndex < endIndex
? projection.Any(i => i >= startIndex && i <= endIndex)
: projection.Any(i => i >= startIndex || i <= endIndex);
case 0:
anyInBetween = abDates
.Select(ab => customTotalOrder(ab.Month, ab.Day))
.Any(i => i >= startIndex && i <= endIndex);
}

foreach (ABDate a in AbDateList)
{
for ( in checkYear = startDate.Year ;checkYear <= EndDate.Year; checkYear ++)
{
if ((new DateTime(checkYear ,a.Month,a.Day) >= startDate) && (new DateTime(checkYear ,a.Month,a.Day) <= endDate))
{
// I AM IN BETWEEN
}
}
}
}

Related

Calculate Date Ranges in Chunks by Year

Writing a small application to calculate interest but the rate changes yearly. Needed to break the range into smaller date ranges when ever it crosses a year boundary. I wrote a little for loop to do it but it's rather clunky. Wondering if there are any built in functions to do this in C# (possible linq). Would basically be looking to return a list of date ranges with the corresponding base year (shortened code for readability).
static void Main(string[] args)
{
var dateStart = DateTime.Parse("2/10/2018");
var dateEnd = DateTime.Parse("3/10/2021");
var years = Years(dateStart, dateEnd);
var baseYear = dateStart.Year;
Console.WriteLine(baseYear);
var loopDateStart = dateStart;
var loopDateEnd = DateTime.Now;
for (int i = 0; i < years + 1; i++)
{
if (i < years) {
loopDateEnd = DateTime.Parse("1/1/" + (baseYear + 1));
Console.WriteLine(loopDateEnd + " ... " + loopDateStart);
Console.WriteLine((loopDateEnd - loopDateStart).Days);
loopDateStart = loopDateEnd;
baseYear++;
}
else {
loopDateEnd = dateEnd;
Console.WriteLine(loopDateEnd + " ... " + loopDateStart);
Console.WriteLine((loopDateEnd - loopDateStart).Days);
}
}
}
public static int Years(DateTime start, DateTime end)
{
return (end.Year - start.Year - 1) +
(((end.Month > start.Month) ||
((end.Month == start.Month) && (end.Day >= start.Day))) ? 1 : 0);
}
Sure, we can use LINQ:
var x = Enumerable.Range(dateStart.Year, (dateEnd.Year-dateStart.Year)+1)
.Select(y => new{
F = new[]{dateStart, new DateTime(y,1,1)}.Max(),
T = new[]{dateEnd, new DateTime(y,12,31)}.Min()
});
It generates an enumerable list of objects that have an F and a T property (from and to) that are your ranges.
It works by using Enumerable.Range to make a list of years: 2018,2019,2020,2021 by starting at 2108 and proceeding for 4 years (2018 to 2018 is one year entry, 2018 to 2021 is 4 year entries)
Then we just turn them into dates using new DateTime(year,amonth,aday) - when were making start dates, amonth and aday are 1 and 1, when making end dates they're 12 and 31
Then we just ask for every year y, "which date is greater, the startdate, or the 1-Jan-y" and "which date is lesser, the enddate or the 31-Dec-y " - for the initial and final date entry it's the startdate and the enddate that are greater and lesser. For other years it's the jan/dec dates. This gives the ranges you want
foreach(var xx in x){
Console.WriteLine(xx.F +" to "+xx.T);
}
2/10/2018 12:00:00 AM to 12/31/2018 12:00:00 AM
1/1/2019 12:00:00 AM to 12/31/2019 12:00:00 AM
1/1/2020 12:00:00 AM to 12/31/2020 12:00:00 AM
1/1/2021 12:00:00 AM to 3/10/2021 12:00:00 AM
If you want to do other work like the number of days between, you can do xx.T-xx.F in the loop, to make a timespan etc
Try:
var start = DateTime.Parse("4/5/2017");
var end = DateTime.Parse("3/1/2019");
DateTime chunkEnd;
for (var chunkStart = start; chunkStart < end; chunkStart = chunkEnd.AddDays(1))
{
var lastDay = new DateTime(chunkStart.Year, 12, 31);
chunkEnd = end > lastDay ? lastDay : end;
var days = (chunkEnd - chunkStart).Days;
Console.WriteLine($"{chunkStart:d} - {chunkEnd:d}; {days} days");
}
Produces:
4/5/2017 - 12/31/2017; 270 days
1/1/2018 - 12/31/2018; 364 days
1/1/2019 - 3/1/2019; 59 days
I came up with the following:
static IEnumerable<(DateTime,DateTime)> ChunkByYear(DateTime start, DateTime end)
{
// Splits <start,end> into chunks each belonging to a different year
while(start <= end)
{
var tempEnd = new DateTime(start.Year, 12, 31);
if(tempEnd >= end ) {
yield return (start, end);
yield break;
}
yield return (start, tempEnd);
start = tempEnd.AddDays(1);
}
}
Here are some results:
4/05/2017 to 3/01/2019:
4/05/2017->31/12/2017
1/01/2018->31/12/2018
1/01/2019->3/01/2019
4/05/2017 to 4/05/2017:
4/05/2017->4/05/2017
31/12/2017 to 31/12/2019:
31/12/2017->31/12/2017
1/01/2018->31/12/2018
1/01/2019->31/12/2019
31/12/2019 to 31/12/2019:
31/12/2019->31/12/2019
31/12/2018 to 1/01/2019:
31/12/2018->31/12/2018
1/01/2019->1/01/2019
Group by years:
using System.Collections.Generic;
using System.Linq;
...
static void Main(string[] args)
{
DateTime dateStart = DateTime.Parse("2/10/2018");
DateTime dateEnd = DateTime.Parse("3/10/2021");
// Group all possible dates by year
foreach(var group in GetDates(dateStart, dateEnd).GroupBy(date => date.Year))
{
Console.WriteLine(group.Key); // The key of the group is year
Console.WriteLine($"{group.Min()} ... {group.Max()}"); // Range: From minimum to maximum, order doesn't matter.
Console.WriteLine($"{group.First()} ... {group.Last()}"); //or Range version 2: From first to last, order matters.
Console.WriteLine(group.Count()); // Count days
}
}
/// <summary>
/// Get all days blindly, might need to pay attention to days on the boundaries
/// </summary>
private static IEnumerable<DateTime> GetDates(DateTime start, DateTime end)
{
// TODO: Check start <= end;
DateTime current = start;
while(current <= end)
{
yield return current;
current = current.AddDays(1);
}
}

How to execute while loop given number of times?

I have this while loop to get next working day excluding holidays and sundays.
But it calculates by adding 1 day. And i want that number of day to be given by the user. I get that input from the below TextBox (TboxIzin).
How can execute that while loop to do the calculation for given number of times?
int i = 1;
int sayi;
IzinIslem i1 = new IzinIslem();
int.TryParse(i1.TboxIzin.Text, out sayi);
public static DateTime GetNextWeekDay(DateTime date,
IList<Holiday> holidays, IList<DayOfWeek> weekendDays)
{
int i = 1;
int sayi;
IzinIslem i1 = new IzinIslem();
int.TryParse(i1.TboxIzin.Text, out sayi);
// always start with tomorrow, and truncate time to be safe
date = date.Date.AddDays(1);
// calculate holidays for both this year and next year
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();
// increment to get working day
while (true)
{
if (weekendDays.Contains(date.DayOfWeek) ||
holidayDates.Contains(date))
date = date.AddDays(1);
else
return date;
}
}
I get not all code paths return a value
error when i try nesting while loops.
while is a conditional loop. Here you put a non-condition in the clause and immediately follow up with a condition. Put the condition in the while clause:
while(weekendDays.Contains(date.DayOfWeek) || holidayDates.Contains(date)) {
date = date.AddDays(1);
}
return date;
The actual reason you're getting the error is that the compiler cannot predict if your if condition will ever resolve to false. If it doesn't, then your function never returns.
With the modified while loop, that may still happen, but it will result in an infinite loop, and the compiler is fine if you shoot yourself in the foot that way.
You can change your else clause to break out of the loop. And then return out of the loop.
while (true)
{
if (weekendDays.Contains(date.DayOfWeek) ||
holidayDates.Contains(date))
date = date.AddDays(1);
else
break;
}
return date;
Let's get rif of all UI in the GetNextWeekDay (like int.TryParse(i1.TboxIzin.Text, out sayi);):
public static DateTime GetNextWeekDay(DateTime date,
IEnumerable<Holiday> holidays,
IEnumerable<DayOfWeek> weekendDays) {
// public method arguments validation
if (null == holidays)
throw new ArgumentNullException(nameof(holidays));
else if (null == weekendDays)
throw new ArgumentNullException(nameof(weekendDays));
HashSet<DayOfWeek> wends = new HashSet<DayOfWeek>(weekendDays);
// Current Year and Next years - .AddYear(1)
HashSet<DateTime> hds = new HashSet<DateTime>(holidays
.Select(item => item.Date)
.Concate(holidays.Select(item => item.Date.AddYear(1))));
for (var day = date.Date.AddDays(1); ; day = day.AddDays(1))
if (!wends.Contains(day.DayOfWeek) && ! hds.Contains(day))
return day;
}
Or if you prefer Linq, the loop can be rewritten as
return Enumerable
.Range(1, 1000)
.Select(day => date.Date.AddDays(day))
.TakeWhile(item => item.Year - date.Year <= 1)
.First(item => !wends.Contains(item.DayOfWeek) && !hds.Contains(item));

Get List of Records between two times C# Linq To Entities

In my application, I am wanting to fill a list with records from the database that match specific conditions:
public ActionResult SelectYearGraph(int year)
{
var lstAllSummaries = db.Summaries.ToList();
var lstMonths =
lstAllSummaries.Where(x => x.FlightDay.Year == year)
.Select(x => x.TestDay.Month)
.Distinct()
.OrderBy(x => x)
.ToList();
List<string> lstMonthNames = new List<string>();
List<int> lstCountSummaries = new List<int>();
List<int> lstCountDailySummariesDifferentTime = new List<int>();
var tsSix = new TimeSpan(6, 0, 0);
var tsTen = new TimeSpan(22, 0, 0);
foreach (var item in lstMonths)
{
var monthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(item);
lstMonthNames.Add(monthName);
foreach (var item in lstMonths)
{
var monthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(item);
lstMonthNames.Add(monthName);
lstCountDailySummaries.Add(lstAllSummaries.Count(x => x.FlightDay.Month == item && x.FlightDay.Year == year
&& (x.FlightDay.TimeOfDay >= tsSix && x.FlightDay.TimeOfDay <= tsTen)
&& !x.deleted));
lstCountDailySummariesDifferentTime.Add(lstAllSummaries.Count(x => x.FlightDay.Month == item && x.FlightDay.Year == year
&& (x.FlightDay.TimeOfDay > tsTen || x.FlightDay.TimeOfDay < tsSix)
&& !x.deleted));
}
}
... // more down here but not relevant to question
}
When I run this, I get a runtime error:
The specified type member 'TimeOfDay' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.
I have tried Date and Time Canonical Functions for LINQ-to-Entities, but I am receiving the same error.
How do I get all records between 6am and 10pm?
You don't need TimeOfDay just reading Hour, Minute, Second of your DateTime will give you the same result in your case.
the TimeOfDay property returns a TimeSpan value that represents a DateTime value's time component.
So just change your code to:
...
&& (DbFunctions.CreateTime(x.TestDay.Hour, x.TestDay.Minute,x.TestDay.Second) > DbFunctions.CreateTime(6,0,0)
&& (DbFunctions.CreateTime(x.TestDay.Hour, x.TestDay.Minute, x.TestDay.Second) < DbFunctions.CreateTime(22,0,0))
...
The problem with looking for times which cross midnight is that whereas a time can be both after 6am AND before 10pm the same is not true for after 10pm AND before 6am.
You'll need to change your AND to an OR
...
&& (
(DbFunctions.CreateTime(x.TestDay.Hour, x.TestDay.Minute,x.TestDay.Second) > DbFunctions.CreateTime(22,0,0)
|| (DbFunctions.CreateTime(x.TestDay.Hour, x.TestDay.Minute, x.TestDay.Second) < DbFunctions.CreateTime(6,0,0))
)
...
var query = from x in db.Summaries
let time = DbFunctions.CreateTime(x.TestDay.Hour, x.TestDay.Minute, x.TestDay.Second)
where x.FlightDay.Year == year
&& !x.deleted
&& (time < DbFunctions.CreateTime(6, 0, 0))
&& (time > DbFunctions.CreateTime(22, 0, 0))
group x by x.FlightDay.Month into summaries
let month = summaries.Key
select new
{
Month = month,
Count = summaries.Count(),
DifferentCount = (from d in db.DailySummaries
let timed = DbFunctions.CreateTime(d.TestDay.Hour, d.TestDay.Minute, d.TestDay.Second)
where d.FlightDay.Year == year && d.FlightDay.Month == month
&& !d.deleted
&& (timed < DbFunctions.CreateTime(6, 0, 0))
&& (timed > DbFunctions.CreateTime(22, 0, 0))
select d).Count(),
};

LINQ Where clause inline for loop?

I have database values called start and length.
start is the start time of a booking (1->09:00, 2->10:00 etc) and length is the length in hours.
i then have an array of start times and end times. I want to be able to check whether each start and end pair are already booked. I so far have it figured that if the start times are the same, it is booked, or if the end times are the same, it is also booked. But if the start and end time are inbetween the comparison times, it will return not booked, which is false.
I am trying to write a LINQ query to test whether a booking is already in the database. So far I have
var proposedRequest = db.requests.Include(r => r.rooms);
proposedRequest = proposedRequest.Where(r => r.booked.Equals(1));
proposedRequest = proposedRequest.Where(r => r.roundID.Equals(roundID));
proposedRequest = proposedRequest.Where(r => r.day.Equals(day));
int[] startTimes = new int[length];
int[] endTimes = new int[length];
for(var q=0;q<length;q++)
{
startTimes[q] = time + q;
endTimes[q] = time + q + 1;
}
proposedRequest = proposedRequest.Where(s => startTimes.Contains(s.start) || endTimes.Contains(s.start+s.length));
Now, this only works for if the new booking starts at the same time as the booking already in the DB, or if it ends at the same time. This doesn't look at the following case
there is a records in the db where start -> 2 and length ->3.
so this booking runs from 10:00->13:00.
but say I am checking this against an entry that starts at 11:00 and ends at 12. It would not come back as booked already because the start and end times do not match.
What is the best way to solve this?
the only way i could see fit is to loop through my startTime and endTime arrays and have another clause for each pair that would produce something like the following:
.Where((s => s.startTime<startTime[i] && (s.startTime + s.Length) > endTime[i]) || (s => s.startTime<startTime[i+1] && (s.startTime + s.Length) > endTime[i+1]))
but i dont think this is possible.
Based on this answer, two ranges overlap if (StartA <= EndB) and (EndA >= StartB)
In your case:
StartA = s.start
EndA = s.start + s.length
StartB = time
EndB = time + length
So your last condition should be like this:
proposedRequest = proposedRequest.Where(s => s.start <= time + length &&
s.start + s.length >= time);
This will return objects that have your StartTime, EndTime, and a boolean that signifies if it booked already.
var proposedRequest = db.requests
.Include(r => r.rooms)
.Where(r => r.booked.Equals(1))
.Where(r => r.roundID.Equals(roundID))
.Where(r => r.day.Equals(day))
.ToList();
//int[] startTimes = new int[length];
//int[] endTimes = new int[length];
//for(var q=0;q<length;q++)
//{
// startTimes[q] = time + q;
// endTimes[q] = time + q + 1;
//}
var times=Enumerable
.Range(time,length)
.Select(r=>
new {
StartTime=r,
EndTime=r+1,
Booked=proposedRequest.Any(pr=>pr.StartTime<=r && pr.StartTime+pr.Length>r)
}).ToList();

Get weekday hours between two dates?

How would I get the number of weekday hours between two dates? (There's a lot of business days calculations, but doesn't seem to be much on weekday hours - not business/opening hours, just hours that aren't weekends).
This is my stab at it - is there a better way?
void Main()
{
// Works
DateTime start = new DateTime(2013,6,15,0,0,0); // Saturday
DateTime end = new DateTime(2013,6,17,10,0,0); // Monday
// Result = 10 (OK)
GetBusinessHours(start, end).Dump();
// Bugs
start = new DateTime(2013,6,14,0,0,0); // Friday
end = new DateTime(2013,6,15,0,0,0); // Saturday
// Result = 0 (Bug) - should be 24
GetBusinessHours(start, end).Dump();
}
public double GetBusinessHours(DateTime start, DateTime end)
{
double result = (end - start).TotalHours;
int weekendDays = Enumerable.Range(0, 1 + end.Subtract(start).Days).Select(offset => start.AddDays(offset)).Count(d => d.DayOfWeek == DayOfWeek.Sunday || d.DayOfWeek == DayOfWeek.Saturday);
double weekendDeltaHours = 0;
if (start.DayOfWeek == DayOfWeek.Saturday ||
start.DayOfWeek == DayOfWeek.Sunday)
{
weekendDays--;
weekendDeltaHours = start.Date.AddDays(1).Subtract(start).TotalHours;
}
else if (end.DayOfWeek == DayOfWeek.Saturday ||
end.DayOfWeek == DayOfWeek.Sunday)
{
weekendDeltaHours = (end - end.Date).TotalHours;
}
result = result - (weekendDays * 24) - weekendDeltaHours;
return result;
}
(Props to Ani for Enumerable.Range trick).
Not that this will be the most efficient method, it will work for what you require.
var end = DateTime.Now;
var start = end.AddDays(-100);
var weekend = new[] { DayOfWeek.Saturday, DayOfWeek.Sunday };
var count = Enumerable.Range(0, Convert.ToInt32(end.Subtract(start).TotalHours))
.Count(offset => !weekend.Contains(start.AddHours(offset).DayOfWeek));
It might be worth putting a check that the number of hours isn't too big for an int. However this would mean a date range of > 245,000 years!

Categories

Resources