Just wondering if there is a more optimized and/or neater way (using LINQ for example) of writing what I have below to get a list of business week date ranges between two dates?
This is what I have currently ..
// Some storage
public class Bucket
{
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}
// Other code removed for brevity ...
DateTime start = new DateTime(2015, 7, 1);
DateTime end = new DateTime(2015, 9, 1);
DayOfWeek firstDayOfWeek = DayOfWeek.Monday;
DayOfWeek lastDayOfWeek = DayOfWeek.Friday;
var buckets = new List<Bucket>();
var currentDate = start;
DateTime startOfBucket = currentDate;
DateTime endOfBucket = currentDate;
while (currentDate <= end)
{
var currentDayOfWeek = currentDate.DayOfWeek;
// Skip days outside the business week
if (currentDayOfWeek >= firstDayOfWeek && currentDayOfWeek <= lastDayOfWeek)
{
if (currentDayOfWeek == firstDayOfWeek)
{
// Start a new bucket
startOfBucket = currentDate;
}
if ((currentDayOfWeek == lastDayOfWeek) || (currentDate == end))
{
// End of bucket
endOfBucket = currentDate;
// Create bucket
buckets.Add(new Bucket()
{
StartDate = startOfBucket,
EndDate = endOfBucket
});
}
}
currentDate = currentDate.AddDays(1);
}
And this will give me the following date ranges ...
Start: 01/Jul/2015 End: 03/Jul/2015
Start: 06/Jul/2015 End: 10/Jul/2015
Start: 13/Jul/2015 End: 17/Jul/2015
Start: 20/Jul/2015 End: 24/Jul/2015
Start: 27/Jul/2015 End: 31/Jul/2015
Start: 03/Aug/2015 End: 07/Aug/2015
Start: 10/Aug/2015 End: 14/Aug/2015
Start: 17/Aug/2015 End: 21/Aug/2015
Start: 24/Aug/2015 End: 28/Aug/2015
Start: 31/Aug/2015 End: 01/Sep/2015
N.B. The first and last weeks are purposefully not full weeks (they abide to the date range given).
Edit
The solution provided here gives the number of days between the two dates but I am interested in getting the collection of date ranges.
Also, I don't need to account for any holidays.
Thanks,
It's quite handy using linq
var startDate = new DateTime(2015, 7, 1);
var endDate = new DateTime(2015, 9, 1);
var workDates = Enumerable.Range(0, (int)(endDate - startDate).TotalDays + 1)
.Select(i => startDate.AddDays(i))
.Where(date => (date.DayOfWeek != DayOfWeek.Saturday && date.DayOfWeek != DayOfWeek.Sunday))
.Select(i => i);
var display = workDates
.GroupAdjacentBy((x, y) => x.AddDays(1) == y)
.Select(g => string.Format("Start: {0:dd/MMM/yyyy} End: {1:dd/MMM/yyyy}", g.First(), g.Last()));
With the extension method GroupAdjacentBy<T>
public static class IEnumerableExtension
{
public static IEnumerable<IEnumerable<T>> GroupAdjacentBy<T>(
this IEnumerable<T> source, Func<T, T, bool> predicate)
{
using (var e = source.GetEnumerator())
{
if (e.MoveNext())
{
var list = new List<T> { e.Current };
var pred = e.Current;
while (e.MoveNext())
{
if (predicate(pred, e.Current))
{
list.Add(e.Current);
}
else
{
yield return list;
list = new List<T> { e.Current };
}
pred = e.Current;
}
yield return list;
}
}
}
}
Fiddle
This is based on Eric's accepted answer so please give him any upvote. I've just modified his solution to handle business weeks that could be 7 days long and also for one that could wrap a weekend.
var startDate = new DateTime(2015, 7, 1);
var endDate = new DateTime(2015, 9, 1);
DayOfWeek firstDayOfWeek = DayOfWeek.Monday;
DayOfWeek lastDayOfWeek = DayOfWeek.Friday;
var workDates = Enumerable.Range(0, (int)(endDate - startDate).TotalDays + 1)
.Select(i => startDate.AddDays(i))
.Where(date =>
// Normal work weeks where first day of week is before last (numerically) e.g. Monday -> Friday or Sunday -> Saturday
(firstDayOfWeek < lastDayOfWeek && date.DayOfWeek >= firstDayOfWeek && date.DayOfWeek <= lastDayOfWeek) ||
// Cater for business weeks whose start and end dates wrap over the weekend e.g. Thursday -> Tuesday
(lastDayOfWeek < firstDayOfWeek && (date.DayOfWeek >= firstDayOfWeek || date.DayOfWeek <= lastDayOfWeek)))
.Select(i => i);
var display = workDates
.GroupAdjacentBy((x, y) => x.AddDays(1) == y && !(x.DayOfWeek == lastDayOfWeek && y.DayOfWeek == firstDayOfWeek))
.Select(g => string.Format("Start: {0:dd/MMM/yyyy} End: {1:dd/MMM/yyyy}", g.First(), g.Last()));
Related
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);
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!
Say I have a list of LockedDate.
A LockedDate has a DateTime and an IsYearly bool. If IsYearly is true then the year should not be considered because it could be any year. Time should never be considered.
Ex: X-Mas, Dec 25 is yearly.
Now I have a List of LockedDate.
There are no duplicates.
Now I need this function:
This function will do:
If a LockedDate is NOT yearly and the day, month, and year are within the range from source, add to return list.
If a LockedDate IS yearly, and its month / day fall in the range, then add a new date for each year in the range.
Say I have Dec 25 with IsYearly as true. My range is Jan 22 2013 to Feb 23 2015 inclusive. would need to add Dec 25 2013 as a new date and Dec 25 2014 as a new Date to the list.
List<Date> GetDateRange(List<LockedDate> source, DateTime start, DateTime end)
{
}
Thanks
Dec 25 Yearly -> Dec 25 2013, Dec 25 2014
Dec 2, 2011 NOT Yearly -> Nothing
March 25, 2013 => March 25 2013
This might give you at least an idea, it's not tested at all yet:
List<DateTime> GetDateRange(List<LockedDate> source, DateTime start, DateTime end)
{
if (start > end)
throw new ArgumentException("Start must be before end");
var ts = end - start;
var dates = Enumerable.Range(0, ts.Days)
.Select(i => start.AddDays(i))
.Where(d => source.Any(ld => ld.Date == d
|| (ld.IsYearly && ld.Date.Month == d.Month && ld.Date.Day == d.Day)));
return dates.ToList();
}
Update Here's the demo with your sample data, it seems to work: http://ideone.com/3KFi97
This code does what you want without using Linq:
List<DateTime> GetDateRange(List<LockedDate> source, DateTime start, DateTime end)
{
List<DateTime> result = new List<DateTime>();
foreach (LockedDate lockedDate in source)
{
if (!lockedDate.IsYearly && (lockedDate.Date >= start && lockedDate.Date <= end))
{
result.Add(lockedDate.Date);
}
else if (lockedDate.IsYearly && (lockedDate.Date >= start && lockedDate.Date <= end))
{
DateTime date = new DateTime(start.Year, lockedDate.Date.Month, lockedDate.Date.Day);
do
{
result.Add(date);
date = date.AddYears(1);
} while (date <= end);
}
}
return result;
}
Make sure to ask about parts you don't understand, so I can explain in detail, but I think it's pretty straightforward.
This code assumes your LockedDate class has a property Date for the DateTime object.
var notYearly = lockDates.Where(d => !d.IsYearly && (d.Date.Date >= start && d.Date.Date <= end)).Select(d => d.Date);
var years = ((end - start).Days / 365) + 2;
var startYear = start.Year;
var yearly = lockDates.Where(d => d.IsYearly)
.SelectMany(d => Enumerable.Range(startYear, years)
.Select(e => new DateTime(e, d.Date.Month, d.Date.Day))
.Where(i => i.Date >= start && i.Date <= end));
var allDates = notYearly.Union(yearly)
Should be more efficient than just iterating through all days between start and end and checking, if that date ok.
If you have a Class LockedDate like this:
public class LockedDate
{
public DateTime Date { get; set; }
public bool IsYearly { get; set; }
...
}
Than you could use this code to get your needed dates:
List<DateTime> GetDateRange(List<LockedDate> source, DateTime start, DateTime end)
{
List<DateTime> dt = new List<DateTime>();
foreach(LockedDate d in source)
{
if(!d.IsYearly)
{
if(start<=d.Date && d.Date<=end)
dt.Add(d.Date);
}
else
{
for(DateTime i = new DateTime(start.Year,d.Date.Month,d.Date.Day);i<=new DateTime(end.Year,d.Date.Month,d.Date.Day);i=i.AddYears(1))
{
dt.Add(i);
}
}
}
return dt;
}
I have DateStart, DateEnd Periodicity, TypePeriodicity fields.
We have a query:
var result = Events.Where(e => e.DateStart <=today && e.DateEnd >= today).ToList();
I want that this query to check Periodicity.
For example:
name - record1
DateStart = 2012-02-02
DateEnd = 2012-03-31
Periodicity = 2
TypePeriodicity = 1 ( it's mean a week, may be also day = 0, month=2):
I want the following, if current date equals:
2,3,4,5 February - return `record1`
6,7,8..12 - not return, because TypePeriodicity = 1 and Periodicity = 2, which means every 2 weeks
13..19 - return `record1`
20..26 - not return
and so on until `DateEnd`
Thanks.
PS. Maybe not LINQ, but simple method that recieve result as parameter.
Here is something to get you started:
You could define a DateEvaluator delegate like so:
delegate bool DateEvaluator(DateTime startDate, DateTime endDate, DateTime dateToCheck, int periodicity);
The purpose of the delegate would be to evaluate for a given periodicity type if a date should be considered as within range. We would have hence 3 date evaluators.
One for each period type: Lets call them dayPeriodicityChecker, weekPeriodicityChecker and monthPeriodicityChecker
Our dayPeriodicityChecker is straightforward:
DateEvaluator dayPeriodicityChecker = (startDate, endDate, dateToCheck, periodicity) =>
{
if ((dateToCheck < startDate) || (dateToCheck > endDate))
return false;
TimeSpan dateDiff = dateToCheck - startDate;
return dateDiff.Days % periodicity == 0;
};
Our weekPeriodicityChecker needs to account for the start day of week, so the start date would need to be adjusted to the date in which the startDate week actually starts:
DateEvaluator weekPeriodicityChecker = (startDate, endDate, dateToCheck, periodicity) =>
{
if ((dateToCheck < startDate) || (dateToCheck > endDate))
return false;
DateTime adjustedStartDate = startDate.AddDays(-(int)startDate.DayOfWeek + 1);
TimeSpan dateDiff = dateToCheck - adjustedStartDate;
return (dateDiff.Days / 7) % periodicity == 0;
};
Our monthPeriodicityChecker needs to cater for months with a variable number of days:
DateEvaluator monthPeriodicityChecker dateToCheck, periodicity) =>
{
if ((dateToCheck < startDate) || (dateToCheck > endDate))
return false;
int monthDiff = 0;
while (startDate.AddMonths(1) < dateToCheck)
{
monthDiff++
// i'm sure there is a speedier way to calculate the month difference, but this should do for the purpose of this example
}
return (monthDiff - 1) % periodicity == 0;
};
Once you have all your date evaluators defined you could put them in an array like so:
DateEvaluator[] dateEvaluators = new DateEvaluator[]
{
dayPeriodicityChecker,
weekPeriodicityChecker,
monthPeriodicityChecker
};
This will allow you to do :
int periodicityType = 0; // or 1=week or 2=months
bool isDateIn = dateEvaluators[periodicityType ](startDate, endDate, dateTocheck, Periodicity)
So lets test this:
PeriodicityEvent pEvent = new PeriodicityEvent
{
Name = "record1",
DateStart = new DateTime(2012, 02, 02),
DateEnd = new DateTime(2012, 03, 31),
PeriodicityType = 1,
Periodicity = 2
};
DateTime baseDate = new DateTime(2012, 02, 01);
for (int i = 0; i < 30; i++)
{
DateTime testDate = baseDate.AddDays(i);
if (dateEvaluators[pEvent.PeriodicityType](pEvent.DateStart, pEvent.DateEnd, testDate, pEvent.Periodicity))
{
Console.WriteLine("{0} is in", testDate.ToString("dd MMM"));
}
else
{
Console.WriteLine("{0} is out", testDate.ToString("dd MMM"));
}
}
This will produce the desired output as below:
To use you would simply do:
Events.Where(e => dateEvaluators[e.PeriodType](e.DateStart, e.DateEnd, today, e.Periodicity).ToList();
Good luck!
Given a Date. How can I add a number of days to it while skipping weekends and other holidays coming between the range?
List <DateTime> holidays = new List<DateTime>()
{
new DateTime(2012, 01, 03),
new DateTime(2012, 01, 26)
};
dateTimeReview.Value = CalculateFutureDate(dateTimeStart.Value, 7,holidays);
static DateTime CalculateFutureDate(DateTime fromDate, int numberofWorkDays, ICollection<DateTime> holidays)
{
var futureDate = fromDate;
for (var i = 0; i < numberofWorkDays; i++ )
{
if (futureDate.DayOfWeek == DayOfWeek.Saturday
|| futureDate.DayOfWeek == DayOfWeek.Sunday
|| (holidays != null && holidays.Contains(futureDate)))
{
futureDate = futureDate.AddDays(1); // Increase FutureDate by one because of condition
futureDate = futureDate.AddDays(1); // Add a working day
}
}
return futureDate;
}
To skip holidays you will first need to create your own list of holidays. Holidays are different in every country and also subject to other factors.
Then you should add days one by one in a loop with a check if the added day is not a weekend day and does not occur in the list of holidays, until the given number of days has been added.
Unfortunately, there is no easier way to do this.
I tried the code above and didn't work. The returned date will somehow includes the holidays and weekends as well. I also want to check that the returned date to be on Workdays only.
So, below are my modified codes.
Basically it will calculate the number of workdays to be added and if the end date falls on holidays/weekends, shift the date to the next day.
Do take note that this is on an assumption that the start date is not on weekends/holidays.
static DateTime CalculateFutureDate(DateTime fromDate, int numberofWorkDays,
ICollection<DateTime> holidays)
{
var futureDate = fromDate;
for (var i = 0; i < numberofWorkDays; i++ )
{
if (futureDate.DayOfWeek == DayOfWeek.Saturday
|| futureDate.DayOfWeek == DayOfWeek.Sunday
|| (holidays != null && holidays.Contains(futureDate)))
{
futureDate = futureDate.AddDays(1);
numberofWorkDays++;
}
else
{
futureDate = futureDate.AddDays(1);
}
}
while(futureDate.DayOfWeek == DayOfWeek.Saturday
|| futureDate.DayOfWeek == DayOfWeek.Sunday
|| (holidays != null && holidays.Contains(futureDate)))
{
futureDate = futureDate.AddDays(1);
}
return futureDate;
}
I've built something similar to check for Office Hours:
public static DateTime AddBusinessHours(DateTime date, long hours)
{
int i = 0;
DateTime tmpDate = date;
do
{
tmpDate = tmpDate.AddHours(1);
if (!IsWeekend(tmpDate) && !IsHoliday(tmpDate) && IsOfficeHours(tmpDate))
i++;
}
while (i < hours);
return tmpDate;
}
public static bool IsWeekend(DateTime date)
{
return (date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday);
}
public static bool IsHoliday(DateTime date)
{
//All dates in the holiday calendar are without hours and minutes.
//With the normal date object, the Contains does not work.
DateTime tmp = new DateTime(date.Year, date.Month, date.Day);
HolidayCalendar calendar = HolidayCalendar.Instance;
return (calendar.Dates.Contains(tmp));
}
public static bool IsOfficeHours(DateTime date)
{
return (date.Hour >= 8 && date.Hour < 20); //Office Hours are between 8AM and 8PM
}
But as mentioned above, you need to run your own holiday calendar.
public static DateTime AddBusinessDays(DateTime pActualDate, int pNumberofWorkDays)
{
ICollection<DateTime> holidays = GetAllHolidays();
int i = default(int);
while (i < pNumberofWorkDays)
{
pActualDate = pActualDate.AddDays(1);
if (pActualDate.DayOfWeek == DayOfWeek.Saturday || pActualDate.DayOfWeek == DayOfWeek.Sunday
|| (holidays != null && holidays.Contains(pActualDate))) { }
else
{ i++; }
}
return pActualDate;
}
private static ICollection<DateTime> GetAllHolidays()
{
ICollection<DateTime> holidays = GetPublicHolidays().Select(s => s.Holidays).ToList();
HashSet<DateTime> finalHolidays = new HashSet<DateTime>();
//if sunday holiday then the following monday will be holiday
bool isMonday = GetCalendar().Any(s => s.Type == "KR" && s.IsMonday);
foreach (var hol in holidays)
{
if (hol.DayOfWeek == DayOfWeek.Sunday && isMonday)
{
//adding monday following day holiday to the list
finalHolidays.Add(hol.AddDays(1));
}
}
//exclude weekends from the holiday list
var excludeWeekends = holidays.Where(s => s.DayOfWeek == DayOfWeek.Sunday || s.DayOfWeek == DayOfWeek.Saturday);
//adding monday to the existing holiday collection
finalHolidays.UnionWith(holidays.Except(excludeWeekends));
return finalHolidays;
}