Time Period Library for .NET
Introduction
When implementing some software for another project, I came across several requirements involving calculations with time periods. These calculations were an important part of the solution and had high demands in respect to correctness and accuracy of the results.
The required functionality covered the following areas:
- Support for individual time periods
- Working with calendar periods within calendar years
- Working with calendar periods deviating from the calendar year (fiscal or school periods)
The time calculations should be made available to both server components (Web Services and tasks) as well as for a rich client (Silverlight).
Analyzing the situation brought me to the conclusion that neither the components of the .NET Framework (which I didn't expect) nor any other available tools would cover all the requirements. Because I already encountered similar needs in earlier projects, I decided to develop a generic library for this purpose.
From several development cycles resulted the following library Time Period, which is now available for the following .NET runtime environments:
- .NET Framework from Version 2
- .NET Framework for Silverlight from Version 4
- .NET Framework for Windows Phone from Version 7
To visualize some of the library functionality, I have put online the Silverlight application Calendar Period Collector under http://www.cpc.itenso.com/. It demonstrates the search for calendar periods.
Time Periods
The .NET Framework already offers the extensive base classes DateTime
and TimeSpan
for basic time related calculations. The library Time Period extends the .NET Framework by several classes for handling periods of time. Such periods are basically characterized by a start, a duration, and an end:
Per definition, the start always occurs before the end. The start is considered undefined if it holds the minimal possible value (DateTime.MinValue
). Likewise the end is undefined if it holds the maximal possible value (DateTime.MaxValue
).
The implementation of these time periods is based on the interface ITimePeriod
and extended by the specializations ITimeRange
and ITimeBlock
:
The interface ITimePeriod
offers information and operations for time periods without defining the ways in which the crucial properties are being calculated:
Start
,End
, andDuration
of the time periodHasStart
istrue
if theStart
time is definedHasEnd
istrue
if theEnd
time is definedIsAnytime
istrue
if neither theStart
nor theEnd
times are definedIsMoment
istrue
ifStart
andEnd
hold identical valuesIsReadOnly
istrue
for immutable time periods (for its usage, see below)
The relation of two time periods is described by the enumeration PeriodRelation
:
Methods like IsSamePeriod
, HasInside
, OverlapsWith
, or IntersectsWith
are available for convenience to query for special, often used variants of such period relations.
Time Range
TimeRange
as an implementation of ITimeRange
defines the time period by its Start
and End
; the duration is calculated from these:
A TimeRange
can be created by specifying its Start
/End
, Start
/Duration
, or Duration
/End
. If required, the given Start
and End
will be sorted chronologically.
For the modification of such a time period, various operations are available (Orange = new instance):
The following example shows the usage of TimeRange
:
// ----------------------------------------------------------------------
public static void TimeRangeSample()
{
// --- time range 1 ---
TimeRange timeRange1 = new TimeRange(
new DateTime( 2011, 2, 22, 14, 0, 0 ),
new DateTime( 2011, 2, 22, 18, 0, 0 ) );
Console.WriteLine( "TimeRange1: " + timeRange1 );
// > TimeRange1: 22.02.2011 14:00:00 - 18:00:00 | 04:00:00
// --- time range 2 ---
TimeRange timeRange2 = new TimeRange(
new DateTime( 2011, 2, 22, 15, 0, 0 ),
new TimeSpan( 2, 0, 0 ) );
Console.WriteLine( "TimeRange2: " + timeRange2 );
// > TimeRange2: 22.02.2011 15:00:00 - 17:00:00 | 02:00:00
// --- time range 3 ---
TimeRange timeRange3 = new TimeRange(
new DateTime( 2011, 2, 22, 16, 0, 0 ),
new DateTime( 2011, 2, 22, 21, 0, 0 ) );
Console.WriteLine( "TimeRange3: " + timeRange3 );
// > TimeRange3: 22.02.2011 16:00:00 - 21:00:00 | 05:00:00
// --- relation ---
Console.WriteLine( "TimeRange1.GetRelation( TimeRange2 ): " +
timeRange1.GetRelation( timeRange2 ) );
// > TimeRange1.GetRelation( TimeRange2 ): Enclosing
Console.WriteLine( "TimeRange1.GetRelation( TimeRange3 ): " +
timeRange1.GetRelation( timeRange3 ) );
// > TimeRange1.GetRelation( TimeRange3 ): EndInside
Console.WriteLine( "TimeRange3.GetRelation( TimeRange2 ): " +
timeRange3.GetRelation( timeRange2 ) );
// > TimeRange3.GetRelation( TimeRange2 ): StartInside
// --- intersection ---
Console.WriteLine( "TimeRange1.GetIntersection( TimeRange2 ): " +
timeRange1.GetIntersection( timeRange2 ) );
// > TimeRange1.GetIntersection( TimeRange2 ): 22.02.2011 15:00:00 - 17:00:00 | 02:00:00
Console.WriteLine( "TimeRange1.GetIntersection( TimeRange3 ): " +
timeRange1.GetIntersection( timeRange3 ) );
// > TimeRange1.GetIntersection( TimeRange3 ): 22.02.2011 16:00:00 - 18:00:00 | 02:00:00
Console.WriteLine( "TimeRange3.GetIntersection( TimeRange2 ): " +
timeRange3.GetIntersection( timeRange2 ) );
// > TimeRange3.GetIntersection( TimeRange2 ): 22.02.2011 16:00:00 - 17:00:00 | 01:00:00
} // TimeRangeSample
The following example tests whether a reservation is within the working hours of a day:
// ----------------------------------------------------------------------
public static bool IsValidReservation( DateTime start, DateTime end )
{
if ( !TimeCompare.IsSameDay( start, end ) )
{
return false; // multiple day reservation
}
TimeRange workingHours =
new TimeRange( TimeTrim.Hour( start, 8 ), TimeTrim.Hour( start, 18 ) );
return workingHours.HasInside( new TimeRange( start, end ) );
} // IsValidReservation
Time Block
TimeBlock
implements the interface ITimeBlock
and defines the time period by Start
and Duration
; the End is being calculated:
As with TimeRange
, a TimeBlock
can be created with Start
/End
, Start
/Duration
, or Duration
/End
. As above, Start
and End
will be automatically sorted if necessary.
For the modification of a time block, these operations are available (Orange = new instance):
The following example shows the usage of TimeBlock
:
// ----------------------------------------------------------------------
public static void TimeBlockSample()
{
// --- time block ---
TimeBlock timeBlock = new TimeBlock(
new DateTime( 2011, 2, 22, 11, 0, 0 ),
new TimeSpan( 2, 0, 0 ) );
Console.WriteLine( "TimeBlock: " + timeBlock );
// > TimeBlock: 22.02.2011 11:00:00 - 13:00:00 | 02:00:00
// --- modification ---
timeBlock.Start = new DateTime( 2011, 2, 22, 15, 0, 0 );
Console.WriteLine( "TimeBlock.Start: " + timeBlock );
// > TimeBlock.Start: 22.02.2011 15:00:00 - 17:00:00 | 02:00:00
timeBlock.Move( new TimeSpan( 1, 0, 0 ) );
Console.WriteLine( "TimeBlock.Move(1 hour): " + timeBlock );
// > TimeBlock.Move(1 hour): 22.02.2011 16:00:00 - 18:00:00 | 02:00:00
// --- previous/next ---
Console.WriteLine( "TimeBlock.GetPreviousPeriod(): " +
timeBlock.GetPreviousPeriod() );
// > TimeBlock.GetPreviousPeriod(): 22.02.2011 14:00:00 - 16:00:00 | 02:00:00
Console.WriteLine( "TimeBlock.GetNextPeriod(): " + timeBlock.GetNextPeriod() );
// > TimeBlock.GetNextPeriod(): 22.02.2011 18:00:00 - 20:00:00 | 02:00:00
Console.WriteLine( "TimeBlock.GetNextPeriod(+1 hour): " +
timeBlock.GetNextPeriod( new TimeSpan( 1, 0, 0 ) ) );
// > TimeBlock.GetNextPeriod(+1 hour): 22.02.2011 19:00:00 - 21:00:00 | 02:00:00
Console.WriteLine( "TimeBlock.GetNextPeriod(-1 hour): " +
timeBlock.GetNextPeriod( new TimeSpan( -1, 0, 0 ) ) );
// > TimeBlock.GetNextPeriod(-1 hour): 22.02.2011 17:00:00 - 19:00:00 | 02:00:00
} // TimeBlockSample
Time Period Container
In everyday usage, time calculations often involve several periods which can be collected in a container and operated upon as a whole. The Time Period library offers the following containers for time periods:
All containers are based on the interface ITimePeriod
, so containers themselves represent a time period. Like this, they can be used in calculations like other periods, for example, ITimeRange
.
The interface ITimePeriodContainer
serves as the base for all containers, and offers list functionality by deriving from IList<ITimePeriod>
.
Time Period Collection
A ITimePeriodCollection
can hold arbitrary elements of type ITimePeriod
and interprets the earliest start of all its elements as the start of the collection time period. Correspondingly, the latest end of all its elements serves as the end of the collection period:
The time period collection offers the following operations:
The following example shows the usage of the class TimePeriodCollection
, which implements the interface ITimePeriodCollection
:
// ----------------------------------------------------------------------
public static void TimePeriodCollectionSample()
{
TimePeriodCollection timePeriods = new TimePeriodCollection();
DateTime testDay = new DateTime( 2010, 7, 23 );
// --- items ---
timePeriods.Add( new TimeRange( TimeTrim.Hour( testDay, 8 ),
TimeTrim.Hour( testDay, 11 ) ) );
timePeriods.Add( new TimeBlock( TimeTrim.Hour( testDay, 10 ), Duration.Hours( 3 ) ) );
timePeriods.Add( new TimeRange( TimeTrim.Hour( testDay, 16, 15 ),
TimeTrim.Hour( testDay, 18, 45 ) ) );
timePeriods.Add( new TimeRange( TimeTrim.Hour( testDay, 14 ),
TimeTrim.Hour( testDay, 15, 30 ) ) );
Console.WriteLine( "TimePeriodCollection: " + timePeriods );
// > TimePeriodCollection: Count = 4; 23.07.2010 08:00:00 - 18:45:00 | 10:45:00
Console.WriteLine( "TimePeriodCollection.Items" );
foreach ( ITimePeriod timePeriod in timePeriods )
{
Console.WriteLine( "Item: " + timePeriod );
}
// > Item: 23.07.2010 08:00:00 - 11:00:00 | 03:00:00
// > Item: 23.07.2010 10:00:00 - 13:00:00 | 03:00:00
// > Item: 23.07.2010 16:15:00 - 18:45:00 | 02:30:00
// > Item: 23.07.2010 14:00:00 - 15:30:00 | 01:30:00
// --- intersection by moment ---
DateTime intersectionMoment = new DateTime( 2010, 7, 23, 10, 30, 0 );
ITimePeriodCollection momentIntersections =
timePeriods.IntersectionPeriods( intersectionMoment );
Console.WriteLine( "TimePeriodCollection.IntesectionPeriods of " +
intersectionMoment );
// > TimePeriodCollection.IntesectionPeriods of 23.07.2010 10:30:00
foreach ( ITimePeriod momentIntersection in momentIntersections )
{
Console.WriteLine( "Intersection: " + momentIntersection );
}
// > Intersection: 23.07.2010 08:00:00 - 11:00:00 | 03:00:00
// > Intersection: 23.07.2010 10:00:00 - 13:00:00 | 03:00:00
// --- intersection by period ---
TimeRange intersectionPeriod =
new TimeRange( TimeTrim.Hour( testDay, 9 ),
TimeTrim.Hour( testDay, 14, 30 ) );
ITimePeriodCollection periodIntersections =
timePeriods.IntersectionPeriods( intersectionPeriod );
Console.WriteLine( "TimePeriodCollection.IntesectionPeriods of " +
intersectionPeriod );
// > TimePeriodCollection.IntesectionPeriods
// of 23.07.2010 09:00:00 - 14:30:00 | 05:30:00
foreach ( ITimePeriod periodIntersection in periodIntersections )
{
Console.WriteLine( "Intersection: " + periodIntersection );
}
// > Intersection: 23.07.2010 08:00:00 - 11:00:00 | 03:00:00
// > Intersection: 23.07.2010 10:00:00 - 13:00:00 | 03:00:00
// > Intersection: 23.07.2010 14:00:00 - 15:30:00 | 01:30:00
} // TimePeriodCollectionSample
Time Period Chain
ITimePeriodChain
connects several time periods of type ITimePeriod
in a chain and ensures that no gaps exist between successive periods:
Because ITimePeriodChain
might change the position of elements, no read-only time periods can be added. Attempting this leads to a NotSupportedException
. ITimePeriodChain
offers the following functionality:
The following example shows the usage of class TimePeriodChain
, which implements the interface ITimePeriodChain
:
// ----------------------------------------------------------------------
public static void TimePeriodChainSample()
{
TimePeriodChain timePeriods = new TimePeriodChain();
DateTime now = ClockProxy.Clock.Now;
DateTime testDay = new DateTime( 2010, 7, 23 );
// --- add ---
timePeriods.Add( new TimeBlock( TimeTrim.Hour( testDay, 8 ), Duration.Hours( 2 ) ) );
timePeriods.Add( new TimeBlock( now, Duration.Hours( 1, 30 ) ) );
timePeriods.Add( new TimeBlock( now, Duration.Hour ) );
Console.WriteLine( "TimePeriodChain.Add(): " + timePeriods );
// > TimePeriodChain.Add(): 23.07.2010 08:00:00 - 12:30:00 | 04:30:00
foreach ( ITimePeriod timePeriod in timePeriods )
{
Console.WriteLine( "Item: " + timePeriod );
}
// > Item: 23.07.2010 08:00:00 - 10:00:00 | 02:00:00
// > Item: 23.07.2010 10:00:00 - 11:30:00 | 01:30:00
// > Item: 23.07.2010 11:30:00 - 12:30:00 | 01:00:00
// --- insert ---
timePeriods.Insert( 2, new TimeBlock( now, Duration.Minutes( 45 ) ) );
Console.WriteLine( "TimePeriodChain.Insert(): " + timePeriods );
// > TimePeriodChain.Insert(): 23.07.2010 08:00:00 - 13:15:00 | 05:15:00
foreach ( ITimePeriod timePeriod in timePeriods )
{
Console.WriteLine( "Item: " + timePeriod );
}
// > Item: 23.07.2010 08:00:00 - 10:00:00 | 02:00:00
// > Item: 23.07.2010 10:00:00 - 11:30:00 | 01:30:00
// > Item: 23.07.2010 11:30:00 - 12:15:00 | 00:45:00
// > Item: 23.07.2010 12:15:00 - 13:15:00 | 01:00:00
} // TimePeriodChainSample
Calendar Time Periods
Calculations with calendar periods must consider the peculiarity that the end of a time period doesn't equal the start of the following period. The following example shows the corresponding values for the hours of day between 13h and 15h:
- 13:00:00.0000000 – 13:59:59.9999999
- 14:00:00.0000000 – 14:59:59.9999999
The end lies a moment before the next start, the difference between the two is at least 1 Tick = 100 nanoseconds. This is an important aspect and may not be neglected in calculations involving time periods.
The Time Period library offers the interface ITimePeriodMapper
, which can convert moments of a time period in both directions. Applied to the scenario above, this would be handled as follows:
// ----------------------------------------------------------------------
public static void TimePeriodMapperSample()
{
TimeCalendar timeCalendar = new TimeCalendar();
CultureInfo ci = CultureInfo.InvariantCulture;
DateTime start = new DateTime( 2011, 3, 1, 13, 0, 0 );
DateTime end = new DateTime( 2011, 3, 1, 14, 0, 0 );
Console.WriteLine( "Original start: {0}", start.ToString( "HH:mm:ss.fffffff", ci ) );
// > Original start: 13:00:00.0000000
Console.WriteLine( "Original end: {0}", end.ToString( "HH:mm:ss.fffffff", ci ) );
// > Original end: 14:00:00.0000000
Console.WriteLine( "Mapping offset start: {0}", timeCalendar.StartOffset );
// > Mapping offset start: 00:00:00
Console.WriteLine( "Mapping offset end: {0}", timeCalendar.EndOffset );
// > Mapping offset end: -00:00:00.0000001
Console.WriteLine( "Mapped start: {0}",
timeCalendar.MapStart( start ).ToString( "HH:mm:ss.fffffff", ci ) );
// > Mapped start: 13:00:00.0000000
Console.WriteLine( "Mapped end: {0}",
timeCalendar.MapEnd( end ).ToString( "HH:mm:ss.fffffff", ci ) );
// > Mapped end: 13:59:59.9999999
} // TimePeriodMapperSample
Time Calendar
The task of interpretation of time periods of calendar elements is combined in the interface ITimeCalendar
:
ITimeCalendar
covers the following areas:
- Assignment to a
CultureInfo
(default =CultureInfo
of the current thread) - Mapping of period boundaries (
ITimePeriodMapper
) - Base month of the year (default = January)
- Definition of how to interpret calendar weeks
- Naming of periods like, for example, the name of the year (fiscal year, school year, ...)
- Various calendar related calculations
Deriving from ITimePeriodMapper
, the mapping of time period boundaries happens with the properties StartOffset
(default = 0) and EndOffset
(default = -1 Tick).
The following example shows a specialization of a time calendar for a fiscal year:
// ------------------------------------------------------------------------
public class FiscalTimeCalendar : TimeCalendar
{
// ----------------------------------------------------------------------
public FiscalTimeCalendar()
: base(
new TimeCalendarConfig
{
YearBaseMonth = YearMonth.October, // october year base month
YearWeekType = YearWeekType.Iso8601, // ISO 8601 week numbering
YearType = YearType.FiscalYear// treat years as fiscal years
} )
{
} // FiscalTimeCalendar
} // class FiscalTimeCalendar
This time calendar can now be used as follows:
// ----------------------------------------------------------------------
public static void FiscalYearSample()
{
FiscalTimeCalendar calendar = new FiscalTimeCalendar(); // use fiscal periods
DateTime moment1 = new DateTime( 2006, 9, 30 );
Console.WriteLine( "Fiscal Year of {0}: {1}", moment1.ToShortDateString(),
new Year( moment1, calendar ).YearName );
// > Fiscal Year of 30.09.2006: FY2005
Console.WriteLine( "Fiscal Quarter of {0}: {1}", moment1.ToShortDateString(),
new Quarter( moment1, calendar ).QuarterOfYearName );
// > Fiscal Quarter of 30.09.2006: FQ4 2005
DateTime moment2 = new DateTime( 2006, 10, 1 );
Console.WriteLine( "Fiscal Year of {0}: {1}", moment2.ToShortDateString(),
new Year( moment2, calendar ).YearName );
// > Fiscal Year of 01.10.2006: FY2006
Console.WriteLine( "Fiscal Quarter of {0}: {1}", moment1.ToShortDateString(),
new Quarter( moment2, calendar ).QuarterOfYearName );
// > Fiscal Quarter of 30.09.2006: FQ1 2006
} // FiscalYearSample
A more thorough description of the classes Year
and Quarter
follows below.
Calendar Elements
For the most commonly used calendar elements, specialized classes are available:
Time period | Single period | Multiple periods | Refers to year's base month |
Year | Year |
Years |
Yes |
Half year | Halfyear |
Halfyears |
Yes |
Quarter | Quarter |
Quarters |
Yes |
Month | Month |
Months |
No |
Week of year | Week |
Weeks |
No |
Day | Day |
Days |
No |
Hour | Hour |
Hours |
No |
Minute | Minute |
Minutes |
No |
Instantiating elements with multiple periods can happen with a specified number of periods.
The following diagram shows the calendar elements for quarters and months, other elements are analogous:
All calendar elements derive from the base class CalendarTimeRange
which itself derives from TimeRange
. CalendarTimeRange
contains the time calendar ITimeCalendar
and thus ensures that the values of the time period cannot be changed after creation (IsReadOnly=true
).
Because by inheritance through the base class TimePeriod
, the calendar elements implement the interface ITimePeriod
, they can all be used for calculations with other time periods.
The following example shows various calendar elements:
// ----------------------------------------------------------------------
public static void CalendarYearTimePeriodsSample()
{
DateTime moment = new DateTime( 2011, 8, 15 );
Console.WriteLine( "Calendar Periods of {0}:", moment.ToShortDateString() );
// > Calendar Periods of 15.08.2011:
Console.WriteLine( "Year : {0}", new Year( moment ) );
Console.WriteLine( "Halfyear : {0}", new Halfyear( moment ) );
Console.WriteLine( "Quarter : {0}", new Quarter( moment ) );
Console.WriteLine( "Month : {0}", new Month( moment ) );
Console.WriteLine( "Week : {0}", new Week( moment ) );
Console.WriteLine( "Day : {0}", new Day( moment ) );
Console.WriteLine( "Hour : {0}", new Hour( moment ) );
// > Year : 2011; 01.01.2011 - 31.12.2011 | 364.23:59
// > Halfyear : HY2 2011; 01.07.2011 - 31.12.2011 | 183.23:59
// > Quarter : Q3 2011; 01.07.2011 - 30.09.2011 | 91.23:59
// > Month : August 2011; 01.08.2011 - 31.08.2011 | 30.23:59
// > Week : w/c 33 2011; 15.08.2011 - 21.08.2011 | 6.23:59
// > Day : Montag; 15.08.2011 - 15.08.2011 | 0.23:59
// > Hour : 15.08.2011; 00:00 - 00:59 | 0.00:59
} // CalendarYearTimePeriodsSample
Some specific calendar elements offer methods to access the time periods of their sub-elements. The following example shows the quarters of a calendar year:
// ----------------------------------------------------------------------
public static void YearQuartersSample()
{
Year year = new Year( 2012 );
ITimePeriodCollection quarters = year.GetQuarters();
Console.WriteLine( "Quarters of Year: {0}", year );
// > Quarters of Year: 2012; 01.01.2012 - 31.12.2012 | 365.23:59
foreach ( Quarter quarter in quarters )
{
Console.WriteLine( "Quarter: {0}", quarter );
}
// > Quarter: Q1 2012; 01.01.2012 - 31.03.2012 | 90.23:59
// > Quarter: Q2 2012; 01.04.2012 - 30.06.2012 | 90.23:59
// > Quarter: Q3 2012; 01.07.2012 - 30.09.2012 | 91.23:59
// > Quarter: Q4 2012; 01.10.2012 - 31.12.2012 | 91.23:59
} // YearQuartersSample
Year and Year Periods
A peculiarity of the calendar elements is their support for calendar periods which deviate from (normal) calendar years:
The beginning of the year can be set through the property ITimeCalendar.YearBaseMonth
and will be considered by the calendar elements Year, Half Year, and Quarter. Valid values for the start of a year can be an arbitrary month. The calendar year thus simply represents the special case where YearBaseMonth = YearMonth.January
.
The following properties govern the interpretation of the boundaries between years:
MultipleCalendarYears
holds true if a period spans over multiple calendar yearsIsCalendarYear
/Halfyear
/Quarter
holds true if a period corresponds the one of the calendar year
The following example shows the calendar elements of a fiscal year:
// ----------------------------------------------------------------------
public static void FiscalYearTimePeriodsSample()
{
DateTime moment = new DateTime( 2011, 8, 15 );
FiscalTimeCalendar fiscalCalendar = new FiscalTimeCalendar();
Console.WriteLine( "Fiscal Year Periods of {0}:", moment.ToShortDateString() );
// > Fiscal Year Periods of 15.08.2011:
Console.WriteLine( "Year : {0}", new Year( moment, fiscalCalendar ) );
Console.WriteLine( "Halfyear : {0}", new Halfyear( moment, fiscalCalendar ) );
Console.WriteLine( "Quarter : {0}", new Quarter( moment, fiscalCalendar ) );
// > Year : FY2010; 01.10.2010 - 30.09.2011 | 364.23:59
// > Halfyear : FHY2 2010; 01.04.2011 - 30.09.2011 | 182.23:59
// > Quarter : FQ4 2010; 01.07.2011 - 30.09.2011 | 91.23:59
} // FiscalYearTimePeriodsSample
Moving the beginning of the year influences the outcome of all contained elements and their operations:
// ----------------------------------------------------------------------
public static void YearStartSample()
{
TimeCalendar calendar = new TimeCalendar(
new TimeCalendarConfig { YearBaseMonth = YearMonth.February } );
Years years = new Years( 2012, 2, calendar ); // 2012-2013
Console.WriteLine( "Quarters of Years (February): {0}", years );
// > Quarters of Years (February): 2012 - 2014; 01.02.2012 - 31.01.2014 | 730.23:59
foreach ( Year year in years.GetYears() )
{
foreach ( Quarter quarter in year.GetQuarters() )
{
Console.WriteLine( "Quarter: {0}", quarter );
}
}
// > Quarter: Q1 2012; 01.02.2012 - 30.04.2012 | 89.23:59
// > Quarter: Q2 2012; 01.05.2012 - 31.07.2012 | 91.23:59
// > Quarter: Q3 2012; 01.08.2012 - 31.10.2012 | 91.23:59
// > Quarter: Q4 2012; 01.11.2012 - 31.01.2013 | 91.23:59
// > Quarter: Q1 2013; 01.02.2013 - 30.04.2013 | 88.23:59
// > Quarter: Q2 2013; 01.05.2013 - 31.07.2013 | 91.23:59
// > Quarter: Q3 2013; 01.08.2013 - 31.10.2013 | 91.23:59
// > Quarter: Q4 2013; 01.11.2013 - 31.01.2014 | 91.23:59
} // YearStartSample
Following are some illustrative usages of often useful utility functions:
// ----------------------------------------------------------------------
public static bool IntersectsYear( DateTime start, DateTime end, int year )
{
return new Year( year ).IntersectsWith( new TimeRange( start, end ) );
} // IntersectsYear
// ----------------------------------------------------------------------
public static DateTime GetLastDayOfPastQuarter( DateTime moment )
{
return new Quarter( new TimeCalendar(
YearMonth.October ) ).GetPreviousQuarter().LastDayStart;
} // GetLastDayOfPastQuarter
// ----------------------------------------------------------------------
public static DateTime GetFirstDayOfWeek( DateTime moment )
{
return new Week( moment ).FirstDayStart;
} // GetFirstDayOfWeek
// ----------------------------------------------------------------------
public static bool IsInCurrentWeek( DateTime test )
{
return new Week().HasInside( test );
} // IsInCurrentWeek
Weeks
Common practice numbers the weeks of a year from 1 to 52/53. The .NET Framework offers in Calendar.GetWeekOfYear
a method to get at this number of the week for a given moment in time. Unfortunately, this deviates from the definition given in ISO 8601, which can lead to wrong interpretations and other misbehavior.
The Time Period library contains the enumeration YearWeekType
, which controls the calculation of calendar week numbers according to ISO 8601. YearWeekType
is supported by ITimeCalendar
and thus defines the different ways of calculation:
// ----------------------------------------------------------------------
// see also http://blogs.msdn.com/b/shawnste/archive/2006/01/24/517178.aspx
public static void CalendarWeekSample()
{
DateTime testDate = new DateTime( 2007, 12, 31 );
// .NET calendar week
TimeCalendar calendar = new TimeCalendar();
Console.WriteLine( "Calendar Week of {0}: {1}", testDate.ToShortDateString(),
new Week( testDate, cCalendar ).WeekOfYear );
// > Calendar Week of 31.12.2007: 53
// ISO 8601 calendar week
TimeCalendar calendarIso8601 = new TimeCalendar(
new TimeCalendarConfig { YearWeekType = YearWeekType.Iso8601 } );
Console.WriteLine( "ISO 8601 Week of {0}: {1}", testDate.ToShortDateString(),
new Week( testDate, calendarIso8601 ).WeekOfYear );
// > ISO 8601 Week of 31.12.2007: 1
} // CalendarWeekSample
Time Period Calculation Tools
DateTimeSet
A DateTimeSet
is a list containing date values of type DateTime
and ensures:
- the date values are sorted chronologically
- a date value is unique
The class can be used to find gaps in time periods, as for example in the implementation of the method TimeGapCalculator.GetGaps
.
The following example shows the usage of a DateTimeSet
:
// ----------------------------------------------------------------------
public static void DateTimeSetSample()
{
DateTimeSet moments = new DateTimeSet();
// --- add ---
moments.Add( new DateTime( 2012, 8, 10, 18, 15, 0 ) );
moments.Add( new DateTime( 2012, 8, 10, 15, 0, 0 ) );
moments.Add( new DateTime( 2012, 8, 10, 13, 30, 0 ) );
moments.Add( new DateTime( 2012, 8, 10, 15, 0, 0 ) ); // twice -> ignored
Console.WriteLine( "DateTimeSet.Add(): " + moments );
// > DateTimeSet.Add(): Count = 3; 10.08.2012 13:30:00 - 18:15:00 | 04:45:00
for ( int i = 0; i < moments.Count; i++ )
{
Console.WriteLine( "Moment[{0:0}]: {1}", i, moments[ i ] );
}
// > Moment[0]: 10.08.2012 13:30:00
// > Moment[1]: 10.08.2012 15:00:00
// > Moment[2]: 10.08.2012 18:15:00
// --- durations ---
IList<TimeSpan> durations = moments.GetDurations( 0, moments.Count );
Console.WriteLine( "DateTimeSet.GetDurations() " );
for ( int i = 0; i < durations.Count; i++ )
{
Console.WriteLine( "Duration[{0:0}]: {1}", i, durations[ i ] );
}
// > Duration[0]: 01:30:00
// > Duration[1]: 03:15:00
} // DateTimeSetSample
Its method DateTimeSet.GetDurations
calculates the time spans between the contained dates.
TimeGapCalculator
A TimeGapCalculator
calculates the gaps between time periods in a collection:
Interpretation of the moments of time can be subject to the application of a ITimePeriodMapper
.
The following example shows how to find the largest possible gap between existing bookings while considering weekends as unavailable:
// ----------------------------------------------------------------------
public static void TimeGapCalculatorSample()
{
// simulation of some reservations
TimePeriodCollection reservations = new TimePeriodCollection();
reservations.Add( new Days( 2011, 3, 7, 2 ) );
reservations.Add( new Days( 2011, 3, 16, 2 ) );
// the overall search range
CalendarTimeRange searchLimits = new CalendarTimeRange(
new DateTime( 2011, 3, 4 ), new DateTime( 2011, 3, 21 ) );
// search the largest free time block
ICalendarTimeRange largestFreeTimeBlock =
FindLargestFreeTimeBlock( reservations, searchLimits );
Console.WriteLine( "Largest free time: " + largestFreeTimeBlock );
// > Largest free time: 18.03.2011 00:00:00 - 20.03.2011 23:59:59 | 2.23:59
} // TimeGapCalculatorSample
// ----------------------------------------------------------------------
public static ICalendarTimeRange FindLargestFreeTimeBlock(
IEnumerable<ITimeperiod> reservations,
ITimePeriod searchLimits = null, bool excludeWeekends = true )
{
TimePeriodCollection bookedPeriods = new TimePeriodCollection( reservations );
if ( searchLimits == null )
{
searchLimits = bookedPeriods; // use boundary of reservations
}
if ( excludeWeekends )
{
Week currentWeek = new Week( searchLimits.Start );
Week lastWeek = new Week( searchLimits.End );
do
{
ITimePeriodCollection days = currentWeek.GetDays();
foreach ( Day day in days )
{
if ( !searchLimits.HasInside( day ) )
{
continue; // outside of the search scope
}
if ( day.DayOfWeek == DayOfWeek.Saturday ||
day.DayOfWeek == DayOfWeek.Sunday )
{
bookedPeriods.Add( day ); // // exclude weekend day
}
}
currentWeek = currentWeek.GetNextWeek();
} while ( currentWeek.Start < lastWeek.Start );
}
// calculate the gaps using the time calendar as period mapper
TimeGapCalculator<TimeRange> gapCalculator =
new TimeGapCalculator<TimeRange>( new TimeCalendar() );
ITimePeriodCollection freeTimes =
gapCalculator.GetGaps( bookedPeriods, searchLimits );
if ( freeTimes.Count == 0 )
{
return null;
}
freeTimes.SortByDuration(); // move the largest gap to the start
return new CalendarTimeRange( freeTimes[ 0 ] );
} // FindLargestFreeTimeBlock
CalendarPeriodCollector
A CalendarPeriodCollector
offers the possibility to search for certain calendar periods within given time limits. By using a ICalendarPeriodCollectorFilter
, such a search can be restricted by the following criteria:
- Search by years
- Search by months
- Search by days of months
- Search by weekdays
Without a filter set, all time ranges of a period will be considered matching. Combining can be done by the following target scopes:
- Years:
CalendarPeriodCollector.CollectYears
- Months:
CalendarPeriodCollector.CollectMonths
- Days:
CalendarPeriodCollector.CollectDays
- Hours:
CalendarPeriodCollector.CollectHours
In normal mode, all time ranges of the found ranges will be combined. For example, this allows to find all hours of a day by using CalendarPeriodCollector.CollectHours
.
To further constrain the result, time ranges can be defined as follows:
- Which months of a year:
ICalendarPeriodCollectorFilter.AddCollectingMonths
- Which days of a month:
ICalendarPeriodCollectorFilter.AddCollectingDays
- Which hours of a day:
ICalendarPeriodCollectorFilter.AddCollectingHours
The following example collects all working hours of Fridays in the month of January of several years:
// ----------------------------------------------------------------------
public static void CalendarPeriodCollectorSample()
{
CalendarPeriodCollectorFilter filter = new CalendarPeriodCollectorFilter();
filter.AddFilterMonth( YearMonth.January ); // only januaries
filter.AddFilterWeekDay( DayOfWeek.Friday ); // only fridays
filter.AddCollectingHours( new HourRange( 8, 18 ) ); // working hours
CalendarTimeRange testPeriod =
new CalendarTimeRange( new DateTime( 2010, 1, 1 ), new DateTime( 2011, 12, 31 ) );
Console.WriteLine( "Calendar period collector of period: " + testPeriod );
// > Calendar period collector of period:
// 01.01.2010 00:00:00 - 30.12.2011 23:59:59 | 728.23:59
CalendarPeriodCollector collector = new CalendarPeriodCollector();
collector.CollectHours( testPeriod, filter );
foreach ( ITimePeriod period in collector.Periods )
{
Console.WriteLine( "Period: " + period );
}
// > Period: 01.01.2010 - 01.01.2010; 08:00 - 17:59 | 0.09:59
// > Period: 08.01.2010 - 08.01.2010; 08:00 - 17:59 | 0.09:59
// > Period: 15.01.2010 - 15.01.2010; 08:00 - 17:59 | 0.09:59
// > Period: 22.01.2010 - 22.01.2010; 08:00 - 17:59 | 0.09:59
// > Period: 29.01.2010 - 29.01.2010; 08:00 - 17:59 | 0.09:59
// > Period: 07.01.2011 - 07.01.2011; 08:00 - 17:59 | 0.09:59
// > Period: 14.01.2011 - 14.01.2011; 08:00 - 17:59 | 0.09:59
// > Period: 21.01.2011 - 21.01.2011; 08:00 - 17:59 | 0.09:59
// > Period: 28.01.2011 - 28.01.2011; 08:00 - 17:59 | 0.09:59
} // CalendarPeriodCollectorSample
By deriving from the class CalendarPeriodCollectorFilter
, individual time periods can be combined:
// ------------------------------------------------------------------------
public class LastDaysOfMonthPeriodCollectorFilter : CalendarPeriodCollectorFilter
{
// ----------------------------------------------------------------------
public override ITimePeriodCollection GetHourPeriods( Day day,
ITimePeriod periodLimits )
{
// allow only the last day of the month
if ( day.Month == day.AddDays( TimeSpec.DaysPerWeek ).Month )
{
return null;
}
return base.GetHourPeriods( day, periodLimits );
} // GetHourPeriods
} // class LastDaysOfMonthPeriodCollectorFilter
Environmental Elements
Time related definitions and basic calculations are located in various utility classes:
TimeSpec |
Constants for times and periods |
YearHalfyear /YearQuarter /YearMonth /YearWeekType |
Enumerations for half years, quarters, months, and week types |
TimeTool |
Operations for modifications of date and time values as well as for specific time periods |
TimeCompare |
Functions for comparison of time periods |
TimeFormatter |
Formatting of time periods |
TimeTrim |
Functions to trim time periods |
Now |
Calculation of the current moment of time for the various time periods; e.g., the start time of the current calendar quarter |
Duration |
Calculation for specific time periods |
Library and Unit Tests
The library Time Period is available in three versions:
- Library for .NET 2.0 including Unit Tests
- Library for .NET for Silverlight 4
- Library for .NET for Windows Phone 7
Most of the classes are covered by NUnit tests. The source code is the same for all three variants (see below: Composite Library Development), but the Unit Tests are only available with the complete .NET Framework.
Creating stable working tests for time based functionality is not an easy task, because various factors influence the state of the test objects:
- Differing Cultures make use of different calendars
- Functionality which is based on
DateTime.Now
can (and most often will) result in differing behavior and test results when executed at different times - Time calculations – especially involving time periods – lead to a multitude of special cases
Considering this, it is of little surprise to find almost three times as much code in the Unit Tests as in the actual library implementation.
Applications
To visualize the calendar objects, the library contains the application Time Period Demo for the command line console, for Silverlight, and Windows Phone.
For calculating calendar periods, the Silverlight application Calendar Period Collector has been made available. The tool is basically a configuration frontend for the most important parameters of the class CalendarPeriodCollectorFilter
, and can calculate the time periods with the CalendarPeriodCollector
. The results can be copied to the Clipboard and pasted into Microsoft Excel:
The application can be executed live on http://www.cpc.itenso.com/.
Composite Library Development
The following naming conventions are being used in the Time Period library to separate files for the different target platforms where necessary:
- <FileName>.Desktop.<Extension>
- <FileName>.Silverlight.<Extension>
- <FileName>.WindowsPhone.<Extension>
The name of the DLL as well as the namespace is identical for all target platforms. These project settings can be changed under Properties > Application > Assembly Name and Default namespace.
The output for the Debug und Release targets will be placed in different directories for each target platform (Properties > Build > Output Path):
- ..\Pub\Desktop.<Debug|Release>\
- ..\Pub\Silverlight.<Debug|Release>\
- ..\Pub\WindowsPhone<Debug|Release>\
To prevent problems with Visual Studio and some of its extension tools, it is necessary (!) to place the temporary compiler output in separate directories per target platform. To do this, it is necessary to Unload Project and insert the following configuration elements into each target:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
...
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
...
<BaseIntermediateOutputPath>obj\Desktop.Debug\</BaseIntermediateOutputPath>
<UseHostCompilerIfAvailable>false</UseHostCompilerIfAvailable>
...
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
...
<BaseIntermediateOutputPath>obj\Desktop.Release\</BaseIntermediateOutputPath>
<UseHostCompilerIfAvailable>false</UseHostCompilerIfAvailable>
...
</PropertyGroup>
...
</Project>
History
- 14th March, 2011
- Initial public release.
发表评论
XZ82Wi Some truly nice stuff on this site, I love it.