I am wondering if anyone can help me, I have a collection of work start times and end times within a single day. I want to show the opposite also, so the times to which the person has not worked on that day. The issue I am having with my code is that it is carrying onto the next day whereas I just want that day.
public class WorkDay
{
public DateTime Day { get; set; }
public List<Worked> WorkingTimes { get; set; }
}
public class Worked
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
}
static class Program
{
static void Main()
{
var now = DateTime.Now;
var year = now.Year;
var month = now.Month;
var day = now.Day;
var Day = new WorkDay
{
Day = now,
WorkingTimes = new List<Worked>
{
new Worked { Start = new DateTime(year,month, day,8,30,0), End = new DateTime(year,month, day,10,30,0) },
new Worked { Start = new DateTime(year,month, day,10,45,0), End = new DateTime(year,month, day,14,30,0) },
new Worked { Start = new DateTime(year,month, day,14,50,0), End = new DateTime(year,month, day,14,50,0).AddHours(10) }
}
};
foreach (var time in Day.WorkingTimes)
Console.WriteLine($"Start {time.Start} " + $"End {time.End}");
Day.WorkingTimes = Day.WorkingTimes.OrderBy(x => x.Start).ToList();
var opposite = Opposite(Day);
foreach (var time in opposite)
Console.WriteLine($"Start {time.Start} " + $"End {time.End}");
Console.WriteLine("Hello World!");
}
public static IEnumerable<Worked> Opposite(WorkDay workDay)
{
var rested = new List<Worked>();
for (var i = workDay.WorkingTimes.Count(); i-- > 0;)
if (i - 1 != -1 && workDay.WorkingTimes[i - 1].End != workDay.WorkingTimes[i].Start)
rested.Add(new Worked { Start = workDay.WorkingTimes[i - 1].End, End = workDay.WorkingTimes[i].Start });
rested = rested.OrderBy(x => x.Start).ToList();
var lastEntry = rested.Last().End.Date;
var lastTime = new DateTime(lastEntry.Year, lastEntry.Month, lastEntry.Day, 23, 59, 59, 59);
if (lastTime > rested.Last().End)
rested.Add(new Worked
{ Start = workDay.WorkingTimes.Last().End, End = lastTime });
return rested;
}
}
So the output would be:
Worked:
Start 21/09/2020 08:30:00 End 21/09/2020 10:30:00
Start 21/09/2020 10:45:00 End 21/09/2020 14:30:00
Start 21/09/2020 14:50:00 End 22/09/2020 00:50:00
Opposite:
Start 21/09/2020 10:30:00 End 21/09/2020 10:45:00
Start 21/09/2020 14:30:00 End 21/09/2020 14:50:00
Start **22/09/2020** 00:50:00 End 21/09/2020 23:59:59
What I am trying to do is not go into the next day calculating the difference. So in the WorkDay class there is the Day and all dates must be from that day.
So that the 24 hours of that day is accounted for (either worked or not), so if they did not work it should be accounted for within the result of the Opposite method.
I would tackle this by modelling a period of time and then provide a method that could Cut out a section of time from that period. Then it becomes trivial to work out the remainder of the day.
Let's start with the Period class:
private sealed class Period : IEquatable<Period>
{
public DateTime StartTime { get; private set; }
public DateTime EndTime { get; private set; }
public Period(DateTime startTime, DateTime endTime)
{
this.StartTime = startTime;
this.EndTime = endTime;
}
public override bool Equals(object obj)
{
if (obj is Period)
return Equals((Period)obj);
return false;
}
public bool Equals(Period obj)
{
if (obj == null)
return false;
if (!EqualityComparer<DateTime>.Default.Equals(this.StartTime, obj.StartTime))
return false;
if (!EqualityComparer<DateTime>.Default.Equals(this.EndTime, obj.EndTime))
return false;
return true;
}
public override int GetHashCode()
{
int hash = 0;
hash ^= EqualityComparer<DateTime>.Default.GetHashCode(this.StartTime);
hash ^= EqualityComparer<DateTime>.Default.GetHashCode(this.EndTime);
return hash;
}
public override string ToString()
{
return String.Format("{{ StartTime = {0}, EndTime = {1} }}",
this.StartTime, this.EndTime);
}
public IEnumerable<Period> Cut(Period that)
{
if (that.StartTime <= this.StartTime)
{
if (that.EndTime <= this.StartTime)
{
yield return this;
}
else if (that.EndTime < this.EndTime)
{
yield return new Period(that.EndTime, this.EndTime);
}
}
else if (that.StartTime < this.EndTime)
{
if (that.EndTime < this.EndTime)
{
yield return new Period(this.StartTime, that.StartTime);
yield return new Period(that.EndTime, this.EndTime);
}
else
{
yield return new Period(this.StartTime, that.StartTime);
}
}
else
{
yield return this;
}
}
}
Now we can express the current working times like this:
var workingTimes = new[]
{
new Period(new DateTime(year, month, day, 8, 30, 0), new DateTime(year, month, day, 10, 30, 0)),
new Period(new DateTime(year, month, day, 10, 45, 0), new DateTime(year, month, day, 14, 30, 0)),
new Period(new DateTime(year, month, day, 14, 50, 0), new DateTime(year, month, day, 14, 50, 0).AddHours(10)),
};
Then the whole day is:
var whole = new Period(now.Date, now.Date.AddDays(1.0));
Now we can compute the remainder of the day simply like this:
var result = new [] { whole };
foreach (var d in workingTimes)
{
result = result.SelectMany(r => r.Cut(d)).ToArray();
}
My end result is:
2020/09/21 00:00 - 2020/09/21 08:30
2020/09/21 10:30 - 2020/09/21 10:45
2020/09/21 14:30 - 2020/09/21 14:50
A simple solution might be to filter the rested result by only those values where the Start and End match the Day
// last line of method "Opposite"
return rested.Where(o => o.Start.Date == workDay.Day.Date && o.End.Date == workDay.Day.Date);
Related
How can I calculate the end date for a given period of time, based on a start date, today's date, and the definition of "period of time"?
Example 1:
A tenant pays rent every week (ie. "period of time" = 7 days).
He stops paying rent and has paid up until 2022-01-07, rent is due on 2022-01-08 (and every 7 days after that too).
This means rent is due on 2022-01-08 for the period up to and including 2022-01-14.
The next period is 2022-01-15 to 2022-01-21.
The one after that is 2022-01-22 to 2022-01-28 etc.
Today's date is 2022-01-16, this means the current rent period end date is 2022-01-21.
Similar to the weekly rent period, there is daily and fortnightly.
However, there are also monthly, quarterly and yearly which do not have a defined number of days as it depends on the number of days in the months.
Example 2:
The tenant has paid to 2022-01-05.
Today's date is 2022-07-02.
The rent period is quarterly.
First rent period:
2022-01-06 -> 2022-04-05
Second rent period:
2022-04-06 -> 2022-07-05 etc
Today's date falls in the 2nd rent period, so the answer is 2022-07-05.
Let's get this really simple.
Start with an infinite DateTime generator:
public IEnumerable<DateTime> GetPaymentDates(DateTime paidTo, DatePeriod rentPeriod)
{
var n = 0;
while (true)
{
yield return _nextDate[rentPeriod](paidTo, n++);
}
}
This relies on a dictionary that computes the _nextDate based on the DatePeriod provided. Here that is:
private Dictionary<DatePeriod, Func<DateTime, int, DateTime>> _nextDate =
new Dictionary<DatePeriod, Func<DateTime, int, DateTime>>()
{
{ DatePeriod.Day, (d, n) => d.AddDays(n) },
{ DatePeriod.Week, (d, n) => d.AddDays(7.0 * n) },
{ DatePeriod.Fortnight, (d, n) => d.AddDays(14.0 * n) },
{ DatePeriod.Month, (d, n) => d.AddMonths(n) },
{ DatePeriod.Quarter, (d, n) => d.AddMonths(3 * n) },
{ DatePeriod.Year, (d, n) => d.AddYears(n) },
};
Now computing GetCurrentRentPeriodEndDate is super simple:
private DateTime GetCurrentRentPeriodEndDate(DateTime paidTo, DateTime today, DatePeriod rentPeriod) =>
GetPaymentDates(paidTo, rentPeriod).FirstOrDefault(x => x >= today);
I tested this against the code you posted with this query:
var paidTo = new DateTime(2022, 1, 7);
var today = new DateTime(2022, 1, 16);
var query =
from rentPeriod in new[]
{
DatePeriod.Day, DatePeriod.Week, DatePeriod.Fortnight,
DatePeriod.Month, DatePeriod.Quarter, DatePeriod.Year,
}
select new
{
rentPeriod,
endDate = GetCurrentRentPeriodEndDate(paidTo, today, rentPeriod)
};
The results I got from both sets of code are the same and are this:
The following will calculate the end date for the current period of time, based on a paidTo date (one day before the period start date), today's date, and a rentPeriod.
using System;
namespace ConsoleApp1
{
class Program
{
private const int NumOfMonthsInQuarter = 3;
static void Main(string[] args)
{
//paidTo is one day before the start of the period
var paidTo = new DateTime(2022, 1, 7);
var today = new DateTime(2022, 1, 16);
var rentPeriod = DatePeriod.Week;
var endDate = GetCurrentRentPeriodEndDate(paidTo, today, rentPeriod); // 2022-01-21
}
private static DateTime GetCurrentRentPeriodEndDate(DateTime paidTo, DateTime today, DatePeriod rentPeriod)
{
switch (rentPeriod)
{
case DatePeriod.Day:
return today;
case DatePeriod.Week:
case DatePeriod.Fortnight:
return GetEndDateForWeekFortnightlyRentPeriod(paidTo, today, rentPeriod);
case DatePeriod.Month:
return GetEndDateForMonthlyRentPeriod(paidTo, today);
case DatePeriod.Quarter:
return GetEndDateForQuarterlyRentPeriod(paidTo, today);
case DatePeriod.Year:
return GetEndDateForYearlyRentPeriod(paidTo, today);
default:
throw new ArgumentException($"{nameof(rentPeriod)} value of {rentPeriod} not recognised");
}
}
private static DateTime GetEndDateForWeekFortnightlyRentPeriod(DateTime paidTo, DateTime today, DatePeriod rentPeriod)
{
var daysInPeriod = GetDaysInPeriod(rentPeriod);
var daysToAdd = ((int)((today - paidTo).TotalDays / daysInPeriod) + 1) * daysInPeriod;
return paidTo.AddDays(daysToAdd);
}
private static int GetDaysInPeriod(DatePeriod rentPeriod)
{
switch (rentPeriod)
{
case DatePeriod.Day:
return 1;
case DatePeriod.Week:
return 7;
case DatePeriod.Fortnight:
return 14;
default:
throw new ArgumentException($"Cannot calculate days in {rentPeriod}.");
}
}
private static DateTime GetEndDateForMonthlyRentPeriod(DateTime paidTo, DateTime today)
{
if (today.Day < paidTo.Day)
{
return new DateTime(today.Year, today.Month, paidTo.Day);
}
return new DateTime(today.Year, today.Month, paidTo.Day).AddMonths(1);
}
private static DateTime GetEndDateForQuarterlyRentPeriod(DateTime paidTo, DateTime today)
{
var rentDue = paidTo.AddDays(1);
var endOfPeriodMonth = 0;
var endOfPeriodYear = today.Year;
if (rentDue.Day == 1)
{
endOfPeriodMonth = GetEndOfPeriodMonthWithRentDueDateAsFirst(today.Month);
}
else
{
endOfPeriodMonth = GetEndOfPeriodMonthWithRentDueDateGreaterThanFirst(today, rentDue);
}
return new DateTime(endOfPeriodYear, endOfPeriodMonth, paidTo.Day);
}
private static int GetEndOfPeriodMonthWithRentDueDateAsFirst(int currentMonth)
{
var currentMonthToNumOfMonthsInQuarterRatio = currentMonth / NumOfMonthsInQuarter;
if (currentMonth % NumOfMonthsInQuarter == 0)
{
currentMonthToNumOfMonthsInQuarterRatio--;
}
return (currentMonthToNumOfMonthsInQuarterRatio + 1) * NumOfMonthsInQuarter;
}
private static int GetEndOfPeriodMonthWithRentDueDateGreaterThanFirst(DateTime today, DateTime rentDue)
{
var endOfPeriodMonth = ((today.Month - rentDue.Month) / NumOfMonthsInQuarter + 1) * NumOfMonthsInQuarter + rentDue.Month;
if (today.Day < rentDue.Day)
{
endOfPeriodMonth -= NumOfMonthsInQuarter;
}
return endOfPeriodMonth;
}
private static DateTime GetEndDateForYearlyRentPeriod(DateTime paidTo, DateTime today)
{
var rentDue = paidTo.AddDays(1);
var endDateYear = today.Year;
var isTodayGreaterThanRentDueIgnoringYear = today.Month > rentDue.Month || (today.Month == rentDue.Month && today.Day > rentDue.Day);
if (isTodayGreaterThanRentDueIgnoringYear)
{
endDateYear++;
}
return new DateTime(endDateYear, rentDue.Month, paidTo.Day);
}
}
public enum DatePeriod
{
Day = 1,
Week = 2,
Fortnight = 3,
Month = 4,
Quarter = 5,
Year = 6
}
}
I use nager.date to know if a day is a holiday day or a weekend day Saturday and Sunday).
I need to extract the date (starting from today or any other date) after 5 working days.
DateTime date1 = new DateTime(2019, 12, 23);
int i = 0;
while ( i < 5)
{
if (DateSystem.IsPublicHoliday(date1, CountryCode.IT) || DateSystem.IsWeekend(date1, CountryCode.IT))
{
date1 = date1.AddDays(1);
}
else
{
date1= date1.AddDays(1);
i++;
}
}
The problem of this code is that if the last else occurs, it add me 1 day but without doing any other check.
For example:
If the start date is 13/07/2020, the result will be at the end 18/07/2020 and as you can see is on Saturday.
How could I modify this code to achieve what I need?
The order is important. The AddDays should be called first, and after it is called we check if the new day matches our criteria.
Note: I have renamed the i variable so it is more clear.
DateTime date1 = new DateTime(2019, 12, 23);
int daysAdded = 0;
while (daysAdded < 5)
{
date1 = date1.AddDays(1);
if (!DateSystem.IsPublicHoliday(date1, CountryCode.IT) && !DateSystem.IsWeekend(date1, CountryCode.IT)) {
// We only consider laboral days
// laboral days: They are not holidays and are not weekends
daysAdded ++;
}
}
I always try to generalize my solutions, so here's one enabling LINQ:
public bool IsWorkingDay(DateTime dt)
=> !DateSystem.IsPublicHoliday(dt) && !DateSystem.IsWeekend(dt);
public DateTime NextWorkingDay(DateTime dt)
{
dt = dt.AddDays(1);
while (!IsWorkingDay(dt))
dt = dt.AddDays(1);
return dt;
}
public IEnumerable<DateTime> WorkingDaysFrom(DateTime dt)
{
if (!IsWorkingDay(dt))
dt = NextWorkingDay(dt); // includes initial dt, remove if unwanted
while (true)
{
yield return dt;
dt = NextWorkingDay(dt);
}
}
This will pump out working days from a given date until end of time, and then use LINQ to grab the number you want:
var next5 = WorkingDaysFrom(DateTime.Today).Take(5).ToList();
here's how to get all the working days in 2020:
var working2020 = WorkingDaysFrom(new DateTime(2020, 1, 1))
.TakeWhile(dt => dt.Year == 2020)
.ToList();
DateTime date1 = new DateTime(2019, 12, 23);
int i = 0;
while ( i < 5)
{
date1 = date1.AddDays(1);
if (!DateSystem.IsPublicHoliday(date1, CountryCode.IT) && !DateSystem.IsWeekend(date1, CountryCode.IT))
{
i++;
}
}
but I think that you need a DateTime[] to store all the five days
This is a better and a faster way to do this without using third party libraries.
DateTime nowDate = DateTime.Now;
DateTime expectedDate;
if (nowDate.DayOfWeek == DayOfWeek.Saturday)
{
expectedDate = nowDate.AddDays(6);
}
else if (nowDate.DayOfWeek == DayOfWeek.Sunday)
{
expectedDate = nowDate.AddDays(5);
}
else
{
expectedDate = nowDate.AddDays(7);
}
I thought about the problem, and based on the LINQ suggestion Lasse-v-Karlsen made, developed this code, which gives you most flexibility:
void Main()
{
// a list of public holidays
var holidays = new List<DateTime>() {new DateTime(2020,1,1),
new DateTime(2020,12,24), new DateTime(2020,12,25), new DateTime(2020,12,26)};
// a function checking if the date is a public holiday
Func<DateTime, bool> isHoliday = (dt) => holidays.Any(a=>a==dt);
// the start date
var dt = new DateTime(2020, 07, 13);
// end date, 5 working days later
var endDate = GetWorkingDay(dt, 5, isHoliday);
// print it
Console.WriteLine(endDate?.ToString("yyyy-mm-dd"));
}
public DateTime? GetWorkingDay(DateTime dt, int skipWorkingDays = 0,
Func<DateTime, bool> holidays=null)
{
if (holidays == null) holidays = (dt) => false;
IEnumerable<DateTime> NextWorkingDay(DateTime dt)
{
while (true)
{
var day = dt.DayOfWeek;
if (day != DayOfWeek.Saturday && day != DayOfWeek.Sunday
&& !holidays.Invoke(dt)) yield return dt;
dt = dt.AddDays(1);
}
}
if (skipWorkingDays<0) return null;
if (skipWorkingDays==0) return NextWorkingDay(dt).First();
var nextXDays = NextWorkingDay(dt).Take(skipWorkingDays).ToList();
var endDate = nextXDays.OrderByDescending(d => d).First();
return endDate;
}
Whether you have a list of public holidays like in this example, or a function coming from a library telling you if a date is a public holiday or not, just feel free to modify the Lambda function isHoliday. In your case, it would be defined as:
Func<DateTime, bool> isHoliday = (dt) => DateSystem.IsPublicHoliday(dt, CountryCode.IT);
I want to get only weeks for the whole year where I want to get the start date (Monday) and end date (Friday) in C#.
For example: 1/52 = 02 Jan (Monday) - 09 Jan (Sunday) 2/52 = 10 Jan (Monday) - 17 Jan (Sunday)
and so on.
I can get current week dates but no idea how to get for the year.
// We Set the Monday as the first day of the week.
DayOfWeek day = datetime.DayOfWeek;
int days = day - DayOfWeek.Monday;
if (days == -1)
{
days = 6; // this is when we have sunday as a DayOfWeek day
}
DateTime start = datetime.AddDays(-days);
DateTime end = start.AddDays(6);
Without making it complicated you can simply use while like below.
while (datetime.DayOfWeek != DayOfWeek.Monday)
{
datetime= datetime.AddDays(1);
}
DateTime start = datetime;
DateTime end = start.AddDays(6);
Or you want to find week from the week index 1/52 for any year then write function like below. Use it like GetWeek(1, 2020) to get 06.01.2020 - 12.01.2020. Format it as per your requirement.
public DateTime GetNextMonday(DateTime datetime)
{
return datetime.AddDays((7 - (int)datetime.DayOfWeek + (int)DayOfWeek.Monday) % 7);
}
public string GetWeek(int week, int year)
{
var start = GetNextMonday(new DateTime(year, 1, 1).AddDays((week-1)*7));
var end = start.AddDays(6);
return start.ToShortDateString() + " - " + end.ToShortDateString();
}
As far as I have understood, probably this will help, I tried the below and it displayed for me the start and end dates for the specified years:
DateTime starting = new DateTime(2020, 1, 1);
DateTime ending = new DateTime(2020, 12, 1);
DateTime currentDay = starting;
DateTime start = currentDay;
DateTime end = currentDay;
while (ending.Year >= currentDay.Year)
{
if (currentDay.DayOfWeek == DayOfWeek.Monday)
{
start = currentDay;
end = start.AddDays(6);
currentDay = end;
Console.WriteLine(start + "(" + start.DayOfWeek + ")");
Console.WriteLine(end + "(" + end.DayOfWeek + ")");
}
else
{
currentDay = currentDay.AddDays(1);
}
}
You can use methods below to calculate start day of any week of any year
public static DateTime StartOfNthWeekOfYear(int year, int weekNumber, DayOfWeek firstDayOfWeek)
{
if(weekNumber < 1)
{
throw new ArgumentOutOfRangeException(nameof(weekNumber));
}
DateTime startOfWeek = StartOfFirstWeekOfYear(year, firstDayOfWeek).AddDays((weekNumber - 1) * 7);
DateTime endOfWeek = startOfWeek.AddDays(6);
if(endOfWeek.Year != year || startOfWeek.Year != year)
{
throw new ArgumentOutOfRangeException(nameof(weekNumber));
}
return startOfWeek;
}
public static DateTime StartOfFirstWeekOfYear(int year, DayOfWeek firstDayOfWeek)
{
DateTime startOfYear = new DateTime(year, 1, 1);
if (startOfYear.DayOfWeek != firstDayOfWeek)
{
return StartOfWeek(startOfYear, firstDayOfWeek).AddDays(7);
}
return startOfYear;
}
public static DateTime StartOfWeek(DateTime value, DayOfWeek firstDayOfWeek)
{
if (value.DayOfWeek != firstDayOfWeek)
{
return value.AddDays(-((7 + (int)value.DayOfWeek - (int)firstDayOfWeek) % 7));
}
return value;
}
I think this should work for Gregorian calendars and takes into account different cultures:
public static IList<DateTime> GetFirstDayOfWeekDates(CultureInfo cultureInfo, int year)
{
var lastDateOfYear = new DateTime(year, 12, 31);
var firstDate = new DateTime(year, 1, 1);
var dayOfWeek = cultureInfo.DateTimeFormat.FirstDayOfWeek;
while (firstDate.DayOfWeek != dayOfWeek)
{
firstDate = firstDate.AddDays(1);
}
var numberOfWeeksInYear = cultureInfo.Calendar.GetWeekOfYear(lastDateOfYear, cultureInfo.DateTimeFormat.CalendarWeekRule, dayOfWeek);
var firstDayOfWeekDates = new List<DateTime>();
firstDayOfWeekDates.Add(firstDate);
var currentDate = firstDate;
for (int i = 0; i < numberOfWeeksInYear; i++)
{
var weekLater = currentDate.AddDays(7);
if (weekLater.Year == year)
{
currentDate = weekLater;
firstDayOfWeekDates.Add(currentDate);
}
}
return firstDayOfWeekDates;
}
You can test this with a console app like this (make the method static):
static void Main(string[] args)
{
var ci = new CultureInfo("en-GB");
var dates = GetFirstDayOfWeekDates(ci, DateTime.Now.Year);
foreach (var dt in dates)
{
Console.WriteLine("Date: " + dt.ToShortDateString());
}
Console.ReadLine();
}
It brings back the following:
If you want to include the end date of the week as well then you can tweak this slightly by adding a new class called WeekDate:
public class WeekDate
{
public DateTime StartOfWeek { get; set; }
public DateTime EndOfWeek { get; set; }
}
GetFirstDayOfWeekDates then becomes:
public static IList<WeekDate> GetFirstDayOfWeekDates(CultureInfo cultureInfo, int year)
{
var lastDateOfYear = new DateTime(year, 12, 31);
var firstDate = new DateTime(year, 1, 1);
var dayOfWeek = cultureInfo.DateTimeFormat.FirstDayOfWeek;
while (firstDate.DayOfWeek != dayOfWeek)
{
firstDate = firstDate.AddDays(1);
}
var numberOfWeeksInYear = cultureInfo.Calendar.GetWeekOfYear(lastDateOfYear, cultureInfo.DateTimeFormat.CalendarWeekRule, dayOfWeek);
var firstDayOfWeekDates = new List<WeekDate>();
firstDayOfWeekDates.Add(new WeekDate { StartOfWeek = firstDate, EndOfWeek = firstDate.AddDays(6) });
var currentDate = firstDate;
for (int i = 0; i < numberOfWeeksInYear; i++)
{
var weekLater = currentDate.AddDays(7);
if (weekLater.Year == year)
{
currentDate = currentDate.AddDays(7);
firstDayOfWeekDates.Add(new WeekDate { StartOfWeek = currentDate, EndOfWeek = currentDate.AddDays(6) });
}
}
return firstDayOfWeekDates;
}
Which returns:
I have an initial and a final date range = 1/1/2015 - 1/30/2015
I have these date ranges that represent dates of unavailability.
1/5/2015 - 1/10/2015
1/15/2015 - 1/20/2015
1/22/2015 - 1/28/2015
I want this output, mainly the dates of availability from the main range:
A: 1/1/2015 - 1/4/2015
B: 1/11/2015 - 1/14/2015
C: 1/21/2015 - 1/21/2015
D: 1/29/2015 - 1/30/2015
I tried to generate a sequential date range like this in order to get the exception dates with Except() but I think I'm complicating the thing.
//dtStartDate = 1/1/2015
//dtEndDate = 1/30/2015
var days = (int)(dtEndDate - dtStartDate).TotalDays + 1;
var completeSeq = Enumerable.Range(0, days).Select(x => dtStartDate.AddDays(x)).ToArray();
How can I get the gap of date ranges from period of time.
I other words how can I get the A, B, C and D from this picture
http://www.tiikoni.com/tis/view/?id=ebe851c
If these dates overlap, they must not be considered only where is a gap.
----------UPDATE-----------
I think if I do this:
var range = Enumerable.Range(0, (int)(1/10/2015 - 1/5/2015).TotalDays + 1).Select(i => 1/5/2015.AddDays(i));
var missing = completeSeq.Except(range).ToArray();
for each date range I will have the exclusion of each date range given but still cannot get the gap!
I saw your question in my morning today and really liked it, but was busy the whole day. So, got a chance to play with your question and believe me I enjoyed it. Here is my code:-
DateTime startDate = new DateTime(2015, 1, 1);
DateTime endDate = new DateTime(2015, 1, 30);
int totalDays = (int)(endDate - startDate).TotalDays + 1;
availability.Add(new Availability { StartDate = endDate, EndDate = endDate });
var result = from x in Enumerable.Range(0, totalDays)
let d = startDate.AddDays(x)
from a in availability.Select((v, i) => new { Value = v, Index = i })
where (a.Index == availability.Count - 1 ?
d <= a.Value.StartDate : d < a.Value.StartDate)
&& (a.Index != 0 ? d > availability[a.Index - 1].EndDate : true)
group new { d, a } by a.Value.StartDate into g
select new
{
AvailableDates = String.Format("{0} - {1}",g.Min(x => x.d),
g.Max(x => x.d))
};
This, definitely need explanation so here it is:-
Step 1: Create a range of dates from Jan 01 till Jan 30 using Enumerable.Range
Step 2: Since after the second unavailable date range, we need to limit the dates selected from last endate till current object startdate, I have calculated index so that we can get access to the last enddate.
Step 3: Once we get the index, all we need to do is filter the dates except for first date range since we didn't have last object in this case.
Step 4: For the last item since we don't have the max range I am adding the endDate to our unavailable list (hope this makes sense).
Here is the Working Fiddle, if you get confused just remove group by and other filters and debug and see the resulting output it will look fairly easy :)
using System;
using System.Collections.Generic;
using System.Linq;
public static class Program {
public static void Main() {
Tuple<DateTime,DateTime> range=Tuple.Create(new DateTime(2015,1,1),new DateTime(2015,1,30));
Tuple<DateTime,DateTime>[] exclude=new[] {
Tuple.Create(new DateTime(2015,1,5),new DateTime(2015,1,10)),
Tuple.Create(new DateTime(2015,1,15),new DateTime(2015,1,20)),
Tuple.Create(new DateTime(2015,1,22),new DateTime(2015,1,28))
};
foreach(Tuple<DateTime,DateTime> r in ExcludeIntervals(range,exclude)) {
Console.WriteLine("{0} - {1}",r.Item1,r.Item2);
}
}
public static IEnumerable<Tuple<DateTime,DateTime>> ExcludeIntervals(Tuple<DateTime,DateTime> range,IEnumerable<Tuple<DateTime,DateTime>> exclude) {
IEnumerable<Tuple<DateTime,bool>> dates=
new[] { Tuple.Create(range.Item1.AddDays(-1),true),Tuple.Create(range.Item2.AddDays(1),false) }.
Concat(exclude.SelectMany(r => new[] { Tuple.Create(r.Item1,false),Tuple.Create(r.Item2,true) })).
OrderBy(d => d.Item1).ThenBy(d => d.Item2); //Get ordered list of time points where availability can change.
DateTime firstFreeDate=default(DateTime);
int count=1; //Count of unavailability intervals what is currently active. Start from 1 to threat as unavailable before range starts.
foreach(Tuple<DateTime,bool> date in dates) {
if(date.Item2) { //false - start of unavailability interval. true - end of unavailability interval.
if(--count==0) { //Become available.
firstFreeDate=date.Item1.AddDays(1);
}
} else {
if(++count==1) { //Become unavailable.
DateTime lastFreeDate=date.Item1.AddDays(-1);
if(lastFreeDate>=firstFreeDate) { //If next unavailability starts right after previous ended, then no gap.
yield return Tuple.Create(firstFreeDate,lastFreeDate);
}
}
}
}
}
}
ideone.com
Got a little oopy...
public class DateRange
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
public bool HasStart
{
get { return Start != DateTime.MinValue; }
}
public bool IsInRange(DateTime date)
{
return (date >= this.Start && date <= this.End);
}
public List<DateRange> GetAvailableDates(DateRange excludedRange)
{
return GetAvailableDates(new List<DateRange>(){excludedRange});
}
public List<DateRange> GetAvailableDates(List<DateRange> excludedRanges)
{
if (excludedRanges == null)
{
return new List<DateRange>() { this };
}
var list = new List<DateRange>();
var aRange = new DateRange();
var date = this.Start;
while (date <= this.End)
{
bool isInARange = excludedRanges.Any(er => er.HasStart && er.IsInRange(date));
if (!isInARange)
{
if (!aRange.HasStart)
{
aRange.Start = date;
}
aRange.End = date;
}
else
{
if (aRange.HasStart)
{
list.Add(aRange);
aRange = new DateRange();
}
}
date = date.AddDays(1);
}
if (aRange.HasStart)
{
list.Add(aRange);
}
return list;
}
}
I have a IEnumerable. I have a custom Interval class which just has two DateTimes inside it. I want to convert the IEnumerable to IEnumerable where n DateTimes would enumerate to n-1 Intervals.
So if I had 1st Jan, 1st Feb and 1st Mar as the DateTime then I want two intervals out, 1st Jan/1st Feb and 1st Feb/1st March.
Is there an existing C# Linq function that does this. Something like the below Correlate...
IEnumerable<Interval> intervals = dttms.Correlate<DateTime, Interval>((dttm1, dttm2) => new Interval(dttm1, dttm2));
If not I'll just roll my own.
public static IEnumerable<Timespan> Intervals(this IEnumerable<DateTime> source)
{
DateTime last;
bool firstFlag = true;
foreach( DateTime current in source)
{
if (firstFlag)
{
last = current;
firstFlag = false;
continue;
}
yield return current - last;
last = current;
}
}
or
public class Interval {DateTime Start; DateTime End;}
public static IEnumerable<Interval> Intervals(this IEnumerable<DateTime> source)
{
DateTime last;
bool firstFlag = true;
foreach( DateTime current in source)
{
if (firstFlag)
{
last = current;
firstFlag = false;
continue;
}
yield return new Interval {Start = last, End = current};
last = current;
}
}
or very generic:
public static IEnumerable<U> Correlate<T,U>(this IEnumerable<T> source, Func<T,T,U> correlate)
{
T last;
bool firstFlag = true;
foreach(T current in source)
{
if (firstFlag)
{
last = current;
firstFlag = false;
continue;
}
yield return correlate(last, current);
last = current;
}
}
var MyDateTimes = GetDateTimes();
var MyIntervals = MyDateTimes.Correlate((d1, d2) => new Interval {Start = d1, End = d2});
You could also just use Aggregate, Joel's answer would be better if you need in multiple scenarios:
var dates = new List<DateTime>
{
new DateTime(2010, 1, 1),
new DateTime(2010, 2, 1),
new DateTime(2010, 3, 1)
};
var intervals = dates.Aggregate(new List<Interval>(), (ivls, d) =>
{
if (ivls.Count != dates.Count-1)
{
ivls.Add(new Interval(d,dates[ivls.Count + 1]));
}
return ivls;
});
You can write your own extension method that will be able to do what you need.
Here's a slightly maddish solution based on LINQ:
var z = l.Aggregate(new Stack<KeyValuePair<DateTime, TimeSpan>>(),
(s, dt) =>
{
var ts = s.Count > 0 ? dt - s.Peek().Key : TimeSpan.Zero;
s.Push(new KeyValuePair<DateTime, TimeSpan>(dt, ts));
return s;
})
.Where(kv=>!kv.Value.Equals(TimeSpan.Zero))
.Select(kv => kv.Value)
.ToList();
l is an enumerable of DateTimes.
But now that I see that you actually don't have TimeSpans but start and end times, this would look like that:
var z = l.Aggregate(new Stack<Interval>(),
(s, dt) =>
{
s.Push(s.Count > 0 ?
new Interval { Start = s.Peek().End, End = dt } : new Interval { End = dt });
return s;
})
.Where(v=> v.Start != default(DateTime))
.Reverse()
.ToList();