Skip to content

Instantly share code, notes, and snippets.

@esterkaufman
Last active October 14, 2021 08:03
Show Gist options
  • Save esterkaufman/2b234565f374b6104c203d5a806d329e to your computer and use it in GitHub Desktop.
Save esterkaufman/2b234565f374b6104c203d5a806d329e to your computer and use it in GitHub Desktop.
Calculate Working Days in C# With Holidays in Israel
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.Caching;
namespace EsterCommonScripts
{
public static class BusinessDaysExtentionsHelper
{
/// <summary>
/// Calculates number of business days, taking into account:
/// - weekends (Fridays and Saturdays)
/// - bank holidays in the middle of the week
/// </summary>
/// <param name="startDate">First day in the time interval</param>
/// <param name="endDate">Last day in the time interval</param>
/// <returns>Number of business days during the 'span'</returns>
public static int GetBusinessDays(this DateTime startDate, DateTime endDate)
{
var dir = 1;
if (startDate.Date == endDate.Date)
{
return 1;// Return 1 if the dates are the same
}
if (startDate.DayOfWeek == DayOfWeek.Friday && startDate.Date.AddDays(1) == endDate.Date)
{
return 0;// Return 0 if the dates are consecutive Friday - Saturday
}
if (startDate > endDate)
{
dir = -1;
var tempEnd = endDate;//Swap dates if start > end
endDate = startDate;
startDate = tempEnd;
}
if (startDate.DayOfWeek > DayOfWeek.Thursday)
{
startDate = startDate.AddDays(startDate.DayOfWeek == DayOfWeek.Friday ? 2 : 1);
}
if (endDate.DayOfWeek > DayOfWeek.Thursday)
{
endDate = endDate.AddDays(endDate.DayOfWeek == DayOfWeek.Friday ? -1 : -2);
}
int diff = (int)endDate.Date.AddDays(1).Subtract(startDate.Date).TotalDays;
// Sum of the totalDays between dates + weekends during the time
int result = diff / 7 * 5 + diff % 7;
// Need to handle case the end.DayOfWeek is smaller than startDate.DayOfWeek (and smaller in more than 1)
// EX: end = Sunday and start = Tuesday (not when end = Sunday and start = Monday)
if (endDate.DayOfWeek < (startDate.DayOfWeek - 1))
{
result -= 2;
}
// Subtract the number of holidays during the time interval
int holidaysCount = GetHolidays(startDate, endDate, 1).Count();
result -= holidaysCount;
return result *= dir;
}
/// <summary>
/// Calculates the dueDate after adding the business days to start date, taking into account:
/// - weekends (Fridays and Saturdays)
/// - bank holidays in the middle of the week
/// </summary>
/// <param name="startDate">First day in the time interval</param>
/// <param name="businessDays">business days count to add to startDate</param>
/// <returns>The dueDate after adding the calculated business days</returns>
public static DateTime AddBusinessDays(this DateTime startDate, int businessDays)
{
if (businessDays < 0)
{
throw new ArgumentException("parameter 'businessDays' cannot be negative", "businessDays");
}
if (businessDays == 0) return startDate;
businessDays -= 1;//Exclue the firstDay
if (startDate.DayOfWeek == DayOfWeek.Friday)
{
startDate = startDate.AddDays(2);
businessDays -= 1;
}
else if (startDate.DayOfWeek == DayOfWeek.Saturday)
{
startDate = startDate.AddDays(1);
businessDays -= 1;
}
businessDays = businessDays < 0 ? 0 : businessDays;//If after calculations, businessDays is negative - set to 0
var tempStart = startDate;
int extraDays = businessDays % 5;
// Add full wekks: businessDays parameter + weekends during the time
startDate = startDate.AddDays(businessDays / 5 * 7);
if ((int)startDate.DayOfWeek + extraDays > 5)
{
extraDays += 2;// Add weekend if occurred in extra days.
}
startDate = startDate.AddDays(extraDays);
startDate = AddHolidaysAndWeekends(tempStart, startDate);
return startDate;
}
// Add days if find Holidays in time interval or dueDate fall into weekends
private static DateTime AddHolidaysAndWeekends(DateTime startDate, DateTime endDate)
{
int holidaysFound = -1;
var dir = startDate > endDate ? -1 : 1;
while (holidaysFound != 0)
{
holidaysFound = GetHolidays(startDate, endDate, dir).Count();
startDate = endDate.AddDays(1);//For next iteration, need to check the next range
//If found holidays, add days and check weekends again
for (int i = 0; i < holidaysFound; i++)
{
endDate = endDate.AddDays(1);
if (endDate.DayOfWeek > DayOfWeek.Thursday) // Push the date if occurs in weekends
{
endDate = endDate.AddDays(endDate.DayOfWeek == DayOfWeek.Friday ? 2 : 1);
}
}
//If NOT found holidays, push date if occurs in weekend and check again holidays
if (holidaysFound == 0 && endDate.DayOfWeek > DayOfWeek.Thursday)
{
holidaysFound = endDate.DayOfWeek == DayOfWeek.Friday ? 2 : 1;
endDate = endDate.AddDays(holidaysFound);
}
}
return endDate;
}
/// <summary>
/// Returns all the holidays that occur during this period, taking into account:
/// - weekends (Fridays and Saturdays)
/// </summary>
/// <param name="startDate">First day in the time interval</param>
/// <param name="endDate">Last day in the time interval</param>
/// <returns>The holidays dates</returns>
private static IEnumerable<DateTime> GetHolidays(DateTime startDate, DateTime endDate, int dir)
{
var dates = new List<DateTime>();
var holidaysDates = GetHolidaysInYearRange(startDate.Year, endDate.Year);
// Returns all the dates that between startDate to endDate and not in Friday or Saturday
dates.AddRange(holidaysDates
.Where(h => (dir > 0 ?
h >= startDate.Date && h <= endDate.Date :
h >= endDate.Date && h <= startDate.Date)
&& h.DayOfWeek < DayOfWeek.Friday));
return dates;
}
private static readonly MemoryCache _cache = MemoryCache.Default;
private static CacheItemPolicy _policy = new CacheItemPolicy() { SlidingExpiration = TimeSpan.FromMinutes(30) };
public static void StoreItemsInCache(string year, IEnumerable<DateTime> itemsToAdd)
{
if (!_cache.Contains(year) && itemsToAdd != null)
_cache.Add(year, itemsToAdd, _policy);
}
public static IEnumerable<DateTime> GetItemsFromCache(string year)
{
if (_cache.Contains(year))
return _cache.Get(year) as IEnumerable<DateTime>;
return null;
}
private static IEnumerable<DateTime> GetHolidaysInYearRange(int startYear, int endYear)
{
var holidaysDates = new List<DateTime>();
IDictionary<string, int> Holidays = new Dictionary<string, int>
{
// value = if happend before Jenuary, should be 1. otherwise 0
{"א' תשרי", 1},
{"ב' תשרי", 1},
{"י' תשרי", 1},
{"ט\"ו תשרי", 1},
{"ט\"ז תשרי", 1},
{"י\"ז תשרי", 1},
{"י\"ח תשרי", 1},
{"י\"ט תשרי", 1},
{"כ' תשרי", 1},
{"כ\"א תשרי", 1},
{"כ\"ב תשרי", 1},
{"ט\"ו ניסן", 0},
{"ט\"ז ניסן", 0},
{"י\"ז ניסן", 0},
{"י\"ח ניסן", 0},
{"י\"ט ניסן", 0},
{"כ' ניסן", 0},
{"כ\"א ניסן", 0},
{"ו' סיון", 0}
};
CultureInfo jewishCulture = CultureInfo.CreateSpecificCulture("he-IL");
jewishCulture.DateTimeFormat.Calendar = new HebrewCalendar();
for (int year = startYear; year <= endYear; year++)
{
var cachedDatesFromYear = GetItemsFromCache($"{year}");
if (cachedDatesFromYear != null)
{
holidaysDates.AddRange(cachedDatesFromYear);
}
else
{
string heb_year = GetHebrewJewishYear(jewishCulture, year);
string next_heb_year = GetHebrewJewishYear(jewishCulture, year + 1);
var yearHolidaysDates = Holidays.Select(h => GetHebrewJewishDate(jewishCulture, h.Key, h.Value == 0 ? heb_year : next_heb_year)).ToList();
var hDay = GetYomHaAtzmaut(jewishCulture, heb_year);
if (hDay.HasValue)
{
yearHolidaysDates.Add(hDay.Value);
}
StoreItemsInCache($"{year}", yearHolidaysDates);
holidaysDates.AddRange(yearHolidaysDates);
}
}
return holidaysDates;
}
private static string GetHebrewJewishYear(CultureInfo jewishCulture, int year)
{
return new DateTime(year, 01, 01).ToString("yyyy", jewishCulture);
}
private static DateTime GetHebrewJewishDate(CultureInfo jewishCulture, string heb_day_month, string heb_year)
{
return DateTime.Parse($"{heb_day_month} {heb_year}", jewishCulture);
}
private static DateTime? GetYomHaAtzmaut(CultureInfo jewishCulture, string heb_year)
{
var date = GetHebrewJewishDate(jewishCulture, "ג' אייר", heb_year);
if (date.DayOfWeek == DayOfWeek.Thursday)
{
return date;
}
date = GetHebrewJewishDate(jewishCulture, "ד' אייר", heb_year);
if (date.DayOfWeek == DayOfWeek.Thursday)
{
return date;
}
date = GetHebrewJewishDate(jewishCulture, "ו' אייר", heb_year);
if (date.DayOfWeek == DayOfWeek.Tuesday)
{
return date;
}
date = GetHebrewJewishDate(jewishCulture, "ה' אייר", heb_year);
if (date.DayOfWeek != DayOfWeek.Monday)
{
return date;
}
return null;
}
}
}
@esterkaufman
Copy link
Author

A set of useful DateTime-based helper classes to skip holidays and add-subtract-calculate business days.

All of the methods taking into account: weekends (Fridays and Saturdays) and holidays in Israel.

The following main methods can be either used as static helper methods or extension methods:
GetBusinessDays – Calculates the business days between given dates
AddBusinessDays – Calculates the due date after adding the required business days to a start date.

You can use also in the other methods, that meant for private calculations, but you’ll need to make them public before using outside of the class

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment