An app has a feature that's much like any calendar application (like the Outlook calendar for example). Consequently, I need to do a lot of date logic. I wrote a class with date calculation methods that are useful in any scenario.
You can choose in what resolution of time you want to view the calendar, like a whole month, a week, a workweek, or a day.
Each calendar view corresponds to a date range. If you view the calendar by weeks, everytime you browse to a given week or navigate to the previous/next week, a class generates a date range containing the start and end DateTime of the visible period in time.
For workweek resolution, this means the saturday and sunday are skipped and so the date ranges are not adjacent and don't "touch" each other.
The same principle applies to the month view. The start of the visible date range is not just the first day of the month. If the month's first day is not a monday, a few days of the previous month are shown too, and are part of the visible date range. The same kind of logic applies for the end of the month.

My plan was to create classes that function as date range generators. For each type of calendar view a specific date range generator class knows how to create the asked period of time.
The interface:
/// <summary>
/// Represents a class that generates date ranges for loading appointmens
/// of an agenda.
/// </summary>
public interface IAgendaDateRangeGenerator
{
/// <summary>
/// Generates the date range that contains the given date.
///
/// Example case: when the class should generate date ranges of each week in the year,
/// this method would generate a date range starting at monday and ending at sunday with
/// the given date within the date range. In other words, the generator creates the week of
/// the year that contains the given date.
/// </summary>
/// <param name="date"></param>
/// <returns></returns>
DateRange DateRangeForDay(DateTime date);
/// <summary>
/// Generates the previous period of time.
/// </summary>
/// <param name="dateRange"></param>
/// <returns></returns>
DateRange PreviousRange(DateRange dateRange);
/// <summary>
/// Generates the next period of time.
/// </summary>
/// <param name="dateRange"></param>
/// <returns></returns>
DateRange NextRange(DateRange dateRange);
}
The date helper class containing date calculation logic (mainly for context/part of review):
public class DateHelper
{
public static DateTime EndOfDay(DateTime date)
{
return new DateTime(date.Year, date.Month, date.Day, 23, 59, 59, 999);
}
public static DateTime BeginOfDay(DateTime date)
{
return date.Date; // By returning date component, the time component will be zero values.
}
public static DateTime BeginOfWeekContaining(DateTime date)
{
int diff = (7 + (date.DayOfWeek - DayOfWeek.Monday)) % 7;
return date.AddDays(-1 * diff);
}
public static DateTime EndOfWeekContaining(DateTime date)
{
DateTime monday = BeginOfWeekContaining(date);
return monday.AddDays(6);
}
/// <summary>
/// Gives the first date in future that's on the given DayOfWeek.
/// Example, if the given date is a tuesday and the next following saturday is wanted
/// this method returns the date of that saturday.
/// Does not change time component.
/// </summary>
/// <param name="date"></param>
/// <param name="dayOfWeek"></param>
/// <returns></returns>
public static DateTime FindFirstNextDay(DateTime date, DayOfWeek wantedDayOfWeek)
{
do
{
date = date.AddDays(1);
}
while (date.DayOfWeek != wantedDayOfWeek);
return date;
}
/// <summary>
/// Opposite of method FindFirstNextDay
/// Looks in history until it finds the date occurence with the given DayOfWeek.
/// Does not change time component.
/// </summary>
/// <param name="date"></param>
/// <param name="wantedDayOfWeek"></param>
/// <returns></returns>
public static DateTime FindFirstPreviousDay(DateTime date, DayOfWeek wantedDayOfWeek)
{
do
{
date = date.AddDays(-1);
}
while (date.DayOfWeek != wantedDayOfWeek);
return date;
}
/// <summary>
/// Returns the date of the first day of the month that contains the given date.
/// </summary>
/// <param name="date"></param>
/// <returns></returns>
public static DateTime BeginOfMonthContaining(DateTime date)
{
int month = date.Month;
while (date.AddDays(-1).Month == month)
{
date = date.AddDays(-1);
}
return date;
}
/// <summary>
/// Returns date of last day of the month that contains the given date.
/// </summary>
/// <param name="date"></param>
/// <returns></returns>
public static DateTime EndOfMonthContaining(DateTime date)
{
int month = date.Month;
while(date.AddDays(1).Month == month)
{
date = date.AddDays(1);
}
return date;
}
}
Example of date range generator for workweeks (monday-friday):
public class WorkWeekDateRangeGenerator : IAgendaDateRangeGenerator
{
public DateRange DateRangeForDay(DateTime date)
{
DateTime monday = DateHelper.BeginOfWeekContaining(date);
DateTime friday = DateHelper.FindFirstNextDay(monday, DayOfWeek.Friday);
return new DateRange(monday, friday);
}
public DateRange PreviousRange(DateRange fromRange)
{
DateTime start = DateHelper.FindFirstPreviousDay(fromRange.StartDate, DayOfWeek.Monday);
DateTime end = DateHelper.FindFirstPreviousDay(fromRange.StartDate, DayOfWeek.Friday);
return new DateRange(start, end);
}
public DateRange NextRange(DateRange fromRange)
{
DateTime start = DateHelper.FindFirstNextDay(fromRange.EndDate, DayOfWeek.Monday);
DateTime end = DateHelper.FindFirstNextDay(fromRange.EndDate, DayOfWeek.Friday);
return new DateRange(start, end);
}
}
As you can see, the interface is designed to be stateless. Which means the generator classes's PreviousRange
and NextRange
methods get a reference date range for determining the previous/next period. The class doesn't have to keep track of the current period.
The outside world that uses the class stores the current date range and passes it to the PreviousRange
or NextRange
method of the generator class instance when navigating.
My biggest question is wether it is wise to have a stateless design. Mainly because the periods of time are not necessarily adjacent (like in my workweek generator example). If the design wouldn't be stateless, I'd probably store a week number in the generator class (in the case of the workweek example). Then I use the week number to create the date ranges and increase/decrease the week number each time a next/previous period is requested. It would be more like a state-machine then. I'm looking forward to your review and possible improvements or maybe even a total different approach.