目录
新增内容:.NET 7的时间段库
介绍
在为另一个项目实施一些软件时,我遇到了几个涉及时间段计算的要求。这些计算是解决方案的重要组成部分,对结果的正确性和准确性有很高的要求。
所需的功能包括以下方面:
- 支持各个时间段
- 在日历年的日历期间工作
- 使用与日历年不同的日历期间(财政或学校期间)
- 处理会计和广播日历
时间计算应同时用于服务器组件(Web服务和任务)以及富客户端(WPF和Silverlight)和移动设备。
通过分析情况,我得出的结论是,.NET Framework的组件(我没有预料到)或任何其他可用的工具都无法满足所有需求。因为我在早期的项目中已经遇到过类似的需求,所以我决定为此开发一个通用库。
从多个开发周期中产生了以下库时间段,该库现在可用于以下.NET运行时环境:
- .NET Framework 3.5或更高版本
- .NET Core框架
- .NET Mono Framework
- Xamarin
- 通用Windows平台——UWP
时间段
.NET Framework已经提供了广泛的基类DateTime和TimeSpan与时间相关的基本计算。库 Time Period 将.NET Framework扩展了几个类,用于处理时间段。这些时期基本上以开始、持续时间和结束为特征:
根据定义,开始总是发生在结束之前。如果start包含最小可能值(DateTime.MinValue),则将其视为未定义。同样,如果末端保持最大可能值(DateTime.MaxValue),则端点是未定义的。
这些时间段的实现基于ITimePeriod接口,并由专业化ITimeRange、ITimeBlock和ITimeInterval扩展:
该ITimePeriod接口提供时间段的信息和操作,但未定义关键属性的计算方式:
- Start、End和Duration时间段
- HasStart是true如果Start时间是定义了的
- HasEnd是true如果End时间是否定义了的
- IsAnytime是true如果既没有定义Start时间,也没有定义End时间
- IsMoment是true如果Start和End保持相同的值
- IsReadOnly是true适用于不可变的时间段(有关其用法,请参见下文)
两个时间段的关系由以下PeriodRelation列举描述:
为了方便起见,可以使用IsSamePeriod、HasInside、OverlapsWith或IntersectsWith等方法来查询此类周期关系的特殊、常用的变体。
时间范围
TimeRange作为ITimeRange的实现,通过其Start和End定义时间段;持续时间的计算公式如下:
TimeRange可以通过指定其Start/End、Start/Duration或Duration/End来创建。如果需要,给定的Start和End将按时间顺序排序。
对于此类时间段的修改,可以使用各种操作(橙色=新实例):
以下示例显示了TimeRange的使用:
// ----------------------------------------------------------------------
public 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
以下示例测试预留是否在一天的工作时间内:
// ----------------------------------------------------------------------
public 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
时间块
TimeBlock实现ITimeBlock接口并通过Start和Duration定义时间段; End正在计算:
与TimeRange一样,TimeBlock可以用Start/End、Start/Duration或Duration/End创建。如上所述,必要时Start和End会自动排序。
对于时间块的修改,可以使用以下操作(橙色=新实例):
以下示例显示了TimeBlock的使用:
// ----------------------------------------------------------------------
public 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
时间间隔
ITimeInterval确定其时间段,如有Start和End的ITimeRange。此外,还可以通过枚举IntervalEdge来控制其Start和End的解释:
- Closed:时间的边界矩包含在计算中。这对应于ITimeRange的行为。
- Open:时间的边界矩表示在计算中被排除的边界值。
可能的间隔变体如下所示:
通常,间隔周期中的边具有值IntervalEdge.Closed,这会导致与相邻时间段的交点。一旦其中一个相邻点的值设置为IntervalEdge.Open,就不存在交点:
// ----------------------------------------------------------------------
public void TimeIntervalSample()
{
// --- time interval 1 ---
TimeInterval timeInterval1 = new TimeInterval(
new DateTime( 2011, 5, 8 ),
new DateTime( 2011, 5, 9 ) );
Console.WriteLine( "TimeInterval1: " + timeInterval1 );
// > TimeInterval1: [08.05.2011 - 09.05.2011] | 1.00:00
// --- time interval 2 ---
TimeInterval timeInterval2 = new TimeInterval(
timeInterval1.End,
timeInterval1.End.AddDays( 1 ) );
Console.WriteLine( "TimeInterval2: " + timeInterval2 );
// > TimeInterval2: [09.05.2011 - 10.05.2011] | 1.00:00
// --- relation ---
Console.WriteLine( "Relation: " + timeInterval1.GetRelation( timeInterval2 ) );
// > Relation: EndTouching
Console.WriteLine( "Intersection: " +
timeInterval1.GetIntersection( timeInterval2 ) );
// > Intersection: [09.05.2011]
timeInterval1.EndEdge = IntervalEdge.Open;
Console.WriteLine( "TimeInterval1: " + timeInterval1 );
// > TimeInterval1: [08.05.2011 - 09.05.2011) | 1.00:00
timeInterval2.StartEdge = IntervalEdge.Open;
Console.WriteLine( "TimeInterval2: " + timeInterval2 );
// > TimeInterval2: (09.05.2011 - 10.05.2011] | 1.00:00
// --- relation ---
Console.WriteLine( "Relation: " + timeInterval1.GetRelation( timeInterval2 ) );
// > Relation: Before
Console.WriteLine( "Intersection: " +
timeInterval1.GetIntersection( timeInterval2 ) );
// > Intersection:
} // TimeIntervalSample
对于某些方案,例如搜索时间段中的间隙,排除期间边可能会导致不希望的结果。在这种情况下,可以通过设置IsIntervalEnabled属性来关闭此排除项。
可以为Start使用值TimeSpec.MinPeriodDate和为End使用值TimeSpec.MaxPeriodDate创建没有边界的时间间隔。
时间段容器
在日常使用中,时间计算通常涉及多个时间段,这些时间段可以收集在一个容器中并作为一个整体进行操作。时间段库为时间段提供以下容器:
所有容器都基于ITimePeriod接口,因此容器本身代表一个时间段。像这样,它们可以像其他期间一样用于计算,例如ITimeRange。
该ITimePeriodContainer接口充当所有容器的基础,并通过派生自IList<ITimePeriod>来提供列表功能。
时间段集合
ITimePeriodCollection可以保存任意ITimePeriod类型的元素,并将其所有元素的最早开始解释为集合时间段的开始。相应地,其所有元素的最新结束作为收集期的结束:
时间段集合提供以下操作:
下面的示例显示了TimePeriodCollection类的用法,该类实现了ITimePeriodCollection接口:
// ----------------------------------------------------------------------
public 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 | 0.10:45
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 | 0.05:30
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
时间段链
ITimePeriodChain连接链中类型ITimePeriod的多个时间段,并确保连续时间段之间不存在间隙:
由于ITimePeriodChain可能会更改元素的位置,因此无法添加只读时间段。尝试这样做会导致NotSupportedException。ITimePeriodChain提供以下功能:
以下示例显示了TimePeriodChain类的用法,该类实现了ITimePeriodChain接口:
// ----------------------------------------------------------------------
public 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(): Count = 3; 23.07.2010 08:00:00 - 12:30:00 | 0.04:30
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(): Count = 4; 23.07.2010 08:00:00 - 13:15:00 | 0.05:15
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
日历时间段
使用日历时间进行计算时,必须考虑一个时间段的结束时间不等于下一个时间段的开始的特殊性。以下示例显示了13小时到15小时之间的小时数的相应值:
- 13:00:00.0000000 - 13:59:59.9999999
- 14:00:00.0000000 - 14:59:59.9999999
结束位于下一次开始前的一刻,两者之间的差异至少为1Tick = 100纳秒。这是一个重要的方面,在涉及时间段的计算中不能忽视。
时间段库提供了ITimePeriodMapper接口,可以在两个方向上转换时间段的时刻。应用于上述方案,将按如下方式处理:
// ----------------------------------------------------------------------
public 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
时间日历
解释日历元素时间段的任务组合在ITimeCalendar接口中:
ITimeCalendar涵盖以下领域:
- 分配给CultureInfo(默认值 = 当前线程的CultureInfo)
- 时间段边界的映射 (ITimePeriodMapper)
- 一年中的基准月(默认值= 1月)
- 如何解释日历周的定义
- 会计日历的定义
- 期间的命名,例如年份名称(财政年度、学年等)
- 各种与日历相关的计算
派生自ITimePeriodMapper,时间段边界的映射发生在属性StartOffset(default = 0)和EndOffset(default = -1 Tick)上。
以下示例显示了会计年度的时间日历的专用化:
// ------------------------------------------------------------------------
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
此时间日历现在可以按如下方式使用:
// ----------------------------------------------------------------------
public 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
下面将对这些类Year和Quarter进行更全面的描述。
日历元素
对于最常用的日历元素,可以使用专门的类:
时间段 | 单周期 | 多个时期 | 指以年为基准的月份 |
年 | Year | Years | 是的 |
广播年 | BroadcastYear | - | 不是 |
半年 | Halfyear | Halfyears | 是的 |
季度 | Quarter | Quarters | 是的 |
月 | Month | Months | 不是 |
广播月份 | BroadcastMonth | - | 不是 |
周 | Week | Weeks | 不是 |
广播出周 | BroadcastWeek | - | 不是 |
日 | Day | Days | 不是 |
小时 | Hour | Hours | 不是 |
分钟 | Minute | Minutes | 不是 |
实例化具有多个周期的元素可以在指定数量的周期内发生。
下图显示了季度和月份的日历元素,其他元素类似:
所有日历元素都派生自基类CalendarTimeRange,而基类本身派生自TimeRange。CalendarTimeRange包含时间日历ITimeCalendar,从而确保时间段的值在创建后无法更改(IsReadOnly=true)。
因为通过TimePeriod基类的继承,日历元素实现了ITimePeriod接口,它们都可以用于其他时间段的计算。
以下示例显示了各种日历元素:
// ----------------------------------------------------------------------
public 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
某些特定的日历元素提供了访问其子元素的时间段的方法。以下示例显示了日历年的季度:
// ----------------------------------------------------------------------
public 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
年和年期
日历元素的一个特点是它们支持偏离(正常)日历年的日历周期:
年初可以通过ITimeCalendar.YearBaseMonth属性设置,并将由日历元素“年”、“半年”和“季度”考虑。年初的有效值可以是任意月份。因此,日历年仅表示其中YearBaseMonth = YearMonth.January的特殊情况。
以下属性控制对年份之间边界的解释:
- 如果时间段跨越多个日历年,则MultipleCalendarYears是true
- 如果某个时间段与日历年中的一个时间段相对应,则IsCalendarYear/Halfyear/Quarter是true
从7月或更晚开始的会计年度通常使用下一个日历年的年号。日历属性FiscalYearBaseMonth提供了定义月份的可能性,之后会计年度将在下一个日历年分配。
以下示例显示了会计年度的日历元素:
// ----------------------------------------------------------------------
public 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
将年初移出会影响所有包含元素及其操作的结果:
// ----------------------------------------------------------------------
public 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
以下是通常有用的效用函数的一些说明性用法:
// ----------------------------------------------------------------------
public bool IntersectsYear( DateTime start, DateTime end, int year )
{
return new Year( year ).IntersectsWith( new TimeRange( start, end ) );
} // IntersectsYear
// ----------------------------------------------------------------------
public void GetDaysOfPastQuarter( DateTime moment,
out DateTime firstDay, out DateTime lastDay )
{
TimeCalendar calendar = new TimeCalendar(
new TimeCalendarConfig { YearBaseMonth = YearMonth.October } );
Quarter quarter = new Quarter( moment, calendar );
Quarter pastQuarter = quarter.GetPreviousQuarter();
firstDay = pastQuarter.FirstDayStart;
lastDay = pastQuarter.LastDayStart;
} // GetDaysOfPastQuarter
// ----------------------------------------------------------------------
public DateTime GetFirstDayOfWeek( DateTime moment )
{
return new Week( moment ).FirstDayStart;
} // GetFirstDayOfWeek
// ----------------------------------------------------------------------
public bool IsInCurrentWeek( DateTime test )
{
return new Week().HasInside( test );
} // IsInCurrentWeek
星期
通常的做法是将一年中的周数从1到52/53。.NET Framework提供了一种Calendar.GetWeekOfYear方法,用于在给定时间点获取一周中的此数字。不幸的是,这偏离了ISO 8601中给出的定义,这可能导致错误的解释和其他不当行为。
时间段库包含枚举YearWeekType,它根据ISO 8601控制日历周数的计算。ITimeCalendar支持YearWeekType,因此定义了不同的计算方式:
// ----------------------------------------------------------------------
// see also http://blogs.msdn.com/b/shawnste/archive/2006/01/24/517178.aspx
public 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, calendar ).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
会计日历
为了简化计划,会计相关行业通常将年份分为季度,季度由四周或五周(4-4-5 日历)的月份组成。这样的年份通常与:
- 一个月的最后一个工作日(FiscalYearAlignment.LastDay)
- 接近月底的工作日(FiscalYearAlignment.NearestDay)
周的安排根据以下分组标准进行:
- 4-4-5周(FiscalQuarterGrouping.FourFourFiveWeeks)
- 4-5-4周(FiscalQuarterGrouping.FourFiveFourWeeks)
- 5-4-4周(FiscalQuarterGrouping.FiveFourFourWeeks)
此行为的控制位于ITimeCalendar中且仅适用于会计年度(YearType.FiscalYear)。日历属性FiscalFirstDayOfYear确定一年开始的星期日。
以下示例显示了在8月的最后一个星期六结束的会计年度:
// ----------------------------------------------------------------------
public void FiscalYearLastDay()
{
ITimeCalendar calendar = new TimeCalendar( new TimeCalendarConfig
{
YearType = YearType.FiscalYear,
YearBaseMonth = YearMonth.September,
FiscalFirstDayOfYear = DayOfWeek.Sunday,
FiscalYearAlignment = FiscalYearAlignment.LastDay,
FiscalQuarterGrouping = FiscalQuarterGrouping.FourFourFiveWeeks
} );
Years years = new Years( 2005, 14, calendar );
foreach ( Year year in years.GetYears() )
{
Console.WriteLine( "Fiscal year {0}: {1} - {2}", year.YearValue,
year.Start.ToString( "yyyy-MM-dd" ), year.End.ToString( "yyyy-MM-dd" ) );
}
} // FiscalYearLastDay
下一个财政年度在接近8月底的那个星期六结束:
public void FiscalYearNearestDay()
{
ITimeCalendar calendar = new TimeCalendar( new TimeCalendarConfig
{
YearType = YearType.FiscalYear,
YearBaseMonth = YearMonth.September,
FiscalFirstDayOfYear = DayOfWeek.Sunday,
FiscalYearAlignment = FiscalYearAlignment.NearestDay,
FiscalQuarterGrouping = FiscalQuarterGrouping.FourFourFiveWeeks
} );
Years years = new Years( 2005, 14, calendar );
foreach ( Year year in years.GetYears() )
{
Console.WriteLine( "Fiscal year {0}: {1} - {2}", year.YearValue,
year.Start.ToString( "yyyy-MM-dd" ), year.End.ToString( "yyyy-MM-dd" ) );
}
} // FiscalYearNearestDay
广播日历
广播日历由以下类BroadcastYear、BroadcastMonth和BroadcastWeek支持:
// ----------------------------------------------------------------------
public void BroadcastCalendar()
{
BroadcastYear year = new BroadcastYear( 2013 );
Console.WriteLine( "Broadcast year: " + year );
// > Broadcast year: 2013; 31.12.2012 - 29.12.2013 | 363.23:59
foreach ( BroadcastMonth month in year.GetMonths() )
{
Console.WriteLine( " Broadcast month: " + month );
foreach ( BroadcastWeek week in month.GetWeeks() )
{
Console.WriteLine( " Broadcast week: " + week );
}
}
} // BroadcastCalendar
时间段计算工具
时间线
该TimeLine类是关于时间差距和重叠的计算的核心。它通过根据相应时刻的出现对集合进行排序来分析集合的时间段。时间轴上的每个时刻都表示为一个ITimeLineMoment并包含有关哪些时间段在特定时刻开始和结束的信息。此表示允许在处理时间线时通过加法和减法来跟踪运行余额。
时间轴的时刻存储在ITimeLineMomentCollection中,允许有效地进行迭代和基于时间时刻的索引访问。
两个时间点之间的差异
.NET Framework的TimeSpan结构仅提供天、小时、分钟、秒和毫秒的时间范围值。从用户的角度来看,通常还需要表示时间范围的月份和年份:
- 最后一次访问1年,4个月和12天前
- 当前年龄:28岁
时间段库包括类DateDiff,该类计算两个日期值之间的时间差,并提供对经过的时间范围的访问。这适当地考虑了日历期间,以考虑不同的月份持续时间:
// ----------------------------------------------------------------------
public void DateDiffSample()
{
DateTime date1 = new DateTime( 2009, 11, 8, 7, 13, 59 );
Console.WriteLine( "Date1: {0}", date1 );
// > Date1: 08.11.2009 07:13:59
DateTime date2 = new DateTime( 2011, 3, 20, 19, 55, 28 );
Console.WriteLine( "Date2: {0}", date2 );
// > Date2: 20.03.2011 19:55:28
DateDiff dateDiff = new DateDiff( date1, date2 );
// differences
Console.WriteLine( "DateDiff.Years: {0}", dateDiff.Years );
// > DateDiff.Years: 1
Console.WriteLine( "DateDiff.Quarters: {0}", dateDiff.Quarters );
// > DateDiff.Quarters: 5
Console.WriteLine( "DateDiff.Months: {0}", dateDiff.Months );
// > DateDiff.Months: 16
Console.WriteLine( "DateDiff.Weeks: {0}", dateDiff.Weeks );
// > DateDiff.Weeks: 70
Console.WriteLine( "DateDiff.Days: {0}", dateDiff.Days );
// > DateDiff.Days: 497
Console.WriteLine( "DateDiff.Weekdays: {0}", dateDiff.Weekdays );
// > DateDiff.Weekdays: 71
Console.WriteLine( "DateDiff.Hours: {0}", dateDiff.Hours );
// > DateDiff.Hours: 11940
Console.WriteLine( "DateDiff.Minutes: {0}", dateDiff.Minutes );
// > DateDiff.Minutes: 716441
Console.WriteLine( "DateDiff.Seconds: {0}", dateDiff.Seconds );
// > DateDiff.Seconds: 42986489
// elapsed
Console.WriteLine( "DateDiff.ElapsedYears: {0}", dateDiff.ElapsedYears );
// > DateDiff.ElapsedYears: 1
Console.WriteLine( "DateDiff.ElapsedMonths: {0}", dateDiff.ElapsedMonths );
// > DateDiff.ElapsedMonths: 4
Console.WriteLine( "DateDiff.ElapsedDays: {0}", dateDiff.ElapsedDays );
// > DateDiff.ElapsedDays: 12
Console.WriteLine( "DateDiff.ElapsedHours: {0}", dateDiff.ElapsedHours );
// > DateDiff.ElapsedHours: 12
Console.WriteLine( "DateDiff.ElapsedMinutes: {0}", dateDiff.ElapsedMinutes );
// > DateDiff.ElapsedMinutes: 41
Console.WriteLine( "DateDiff.ElapsedSeconds: {0}", dateDiff.ElapsedSeconds );
// > DateDiff.ElapsedSeconds: 29
// description
Console.WriteLine( "DateDiff.GetDescription(1): {0}", dateDiff.GetDescription( 1 ) );
// > DateDiff.GetDescription(1): 1 Year
Console.WriteLine( "DateDiff.GetDescription(2): {0}", dateDiff.GetDescription( 2 ) );
// > DateDiff.GetDescription(2): 1 Year 4 Months
Console.WriteLine( "DateDiff.GetDescription(3): {0}", dateDiff.GetDescription( 3 ) );
// > DateDiff.GetDescription(3): 1 Year 4 Months 12 Days
Console.WriteLine( "DateDiff.GetDescription(4): {0}", dateDiff.GetDescription( 4 ) );
// > DateDiff.GetDescription(4): 1 Year 4 Months 12 Days 12 Hours
Console.WriteLine( "DateDiff.GetDescription(5): {0}", dateDiff.GetDescription( 5 ) );
// > DateDiff.GetDescription(5): 1 Year 4 Months 12 Days 12 Hours 41 Mins
Console.WriteLine( "DateDiff.GetDescription(6): {0}", dateDiff.GetDescription( 6 ) );
// > DateDiff.GetDescription(6): 1 Year 4 Months 12 Days 12 Hours 41 Mins 29 Secs
} // DateDiffSample
该DateDiff.GetDescription方法可以设置具有可变详细级别的持续时间的格式。
时间间隔计算
TimeGapCalculator计算集合中时间段之间的差距:
对时间时刻的解释可以受制于ITimePeriodMapper应用。
以下示例说明如何在将周末视为不可用时查找现有预订之间的最大可能差距:
// ----------------------------------------------------------------------
public 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: 09.03.2011 00:00:00 - 11.03.2011 23:59:59 | 2.23:59
} // TimeGapCalculatorSample
// ----------------------------------------------------------------------
public 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
合并时间段
在某些情况下,对重叠或相邻的时间段有一个统一的看法是合理的,例如,与寻找差距相反。该TimePeriodCombiner类提供了合并以下时间段的可能性:
以下示例显示了根据图示的时间段的组合:
// ----------------------------------------------------------------------
public void TimePeriodCombinerSample()
{
TimePeriodCollection periods = new TimePeriodCollection();
periods.Add( new TimeRange( new DateTime( 2011, 3, 01 ), new DateTime( 2011, 3, 10 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 04 ), new DateTime( 2011, 3, 08 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 15 ), new DateTime( 2011, 3, 18 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 18 ), new DateTime( 2011, 3, 22 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 20 ), new DateTime( 2011, 3, 24 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 26 ), new DateTime( 2011, 3, 30 ) ) );
TimePeriodCombiner<TimeRange> periodCombiner = new TimePeriodCombiner<TimeRange>();
ITimePeriodCollection combinedPeriods = periodCombiner.CombinePeriods( periods );
foreach ( ITimePeriod combinedPeriod in combinedPeriods )
{
Console.WriteLine( "Combined Period: " + combinedPeriod );
}
// > Combined Period: 01.03.2011 - 10.03.2011 | 9.00:00
// > Combined Period: 15.03.2011 - 24.03.2011 | 9.00:00
// > Combined Period: 26.03.2011 - 30.03.2011 | 4.00:00
} // TimePeriodCombinerSample
时间段的交集
如果应该检查时间段是否有交叉点(例如,重复预订),则TimePeriodIntersector类将提供帮助:
默认情况下,交集周期将合并为一个。要保持所有交集周期,可以将IntersectPeriods方法的combinePeriods参数设置为false。
以下示例显示了TimePeriodIntersector的使用:
// ----------------------------------------------------------------------
public void TimePeriodIntersectorSample()
{
TimePeriodCollection periods = new TimePeriodCollection();
periods.Add( new TimeRange( new DateTime( 2011, 3, 01 ), new DateTime( 2011, 3, 10 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 05 ), new DateTime( 2011, 3, 15 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 12 ), new DateTime( 2011, 3, 18 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 20 ), new DateTime( 2011, 3, 24 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 22 ), new DateTime( 2011, 3, 28 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 24 ), new DateTime( 2011, 3, 26 ) ) );
TimePeriodIntersector<TimeRange> periodIntersector =
new TimePeriodIntersector<TimeRange>();
ITimePeriodCollection intersectedPeriods = periodIntersector.IntersectPeriods( periods );
foreach ( ITimePeriod intersectedPeriod in intersectedPeriods )
{
Console.WriteLine( "Intersected Period: " + intersectedPeriod );
}
// > Intersected Period: 05.03.2011 - 10.03.2011 | 5.00:00
// > Intersected Period: 12.03.2011 - 15.03.2011 | 3.00:00
// > Intersected Period: 22.03.2011 - 26.03.2011 | 4.00:00
} // TimePeriodIntersectorSample
时间段的减法
使用该TimePeriodSubtractor类,您可以从其他时间段(minuend)中减去时间段(subtrahend):
结果包含两个时间段集合之间的差异:
// ----------------------------------------------------------------------
public void TimePeriodSubtractorSample()
{
DateTime moment = new DateTime( 2012, 1, 29 );
TimePeriodCollection sourcePeriods = new TimePeriodCollection
{
new TimeRange( moment.AddHours( 2 ), moment.AddDays( 1 ) )
};
TimePeriodCollection subtractingPeriods = new TimePeriodCollection
{
new TimeRange( moment.AddHours( 6 ), moment.AddHours( 10 ) ),
new TimeRange( moment.AddHours( 12 ), moment.AddHours( 16 ) )
};
TimePeriodSubtractor<timerange> subtractor = new TimePeriodSubtractor<timerange>();
ITimePeriodCollection subtractedPeriods =
subtractor.SubtractPeriods( sourcePeriods, subtractingPeriods );
foreach ( TimeRange subtractedPeriod in subtractedPeriods )
{
Console.WriteLine( "Subtracted Period: {0}", subtractedPeriod );
}
// > Subtracted Period : 29.01.2012 02:00:00 - 06:00:00 | 0.04:00
// > Subtracted Period : 29.01.2012 10:00:00 - 12:00:00 | 0.02:00
// > Subtracted Period : 29.01.2012 16:00:00 - 30.01.2012 00:00:00 | 0.08:00
} // TimePeriodSubtractorSample
日期的加法和减法
通常,会出现将特定时间段添加到给定日期并从中得出目标时间点的问题。乍一听很容易,但往往因以下几个因素而变得复杂:
- 只应考虑营业时间
- 周末、节假日、服务和维护期应不包括在内
一旦存在这样的要求,通用的日期算术就必然会失败。在这种情况下,DateAdd类可能会来救援:
尽管类的名称可能另有暗示,但可以进行加法和减法。DateAdd的一个特性是它能够指定要用DateAdd.IncludePeriods包含的周期,以及用DateAdd.ExcludePeriods排除某些周期。也可以只指定两者之一。如果两者都未定义,则该工具的行为等效于DateTime.Add和DateTime.Subtract。
以下示例显示了DateAdd的使用:
// ----------------------------------------------------------------------
public void DateAddSample()
{
DateAdd dateAdd = new DateAdd();
dateAdd.IncludePeriods.Add( new TimeRange( new DateTime( 2011, 3, 17 ),
new DateTime( 2011, 4, 20 ) ) );
// setup some periods to exclude
dateAdd.ExcludePeriods.Add( new TimeRange(
new DateTime( 2011, 3, 22 ), new DateTime( 2011, 3, 25 ) ) );
dateAdd.ExcludePeriods.Add( new TimeRange(
new DateTime( 2011, 4, 1 ), new DateTime( 2011, 4, 7 ) ) );
dateAdd.ExcludePeriods.Add( new TimeRange(
new DateTime( 2011, 4, 15 ), new DateTime( 2011, 4, 16 ) ) );
// positive
DateTime dateDiffPositive = new DateTime( 2011, 3, 19 );
DateTime? positive1 = dateAdd.Add( dateDiffPositive, Duration.Hours( 1 ) );
Console.WriteLine( "DateAdd Positive1: {0}", positive1 );
// > DateAdd Positive1: 19.03.2011 01:00:00
DateTime? positive2 = dateAdd.Add( dateDiffPositive, Duration.Days( 4 ) );
Console.WriteLine( "DateAdd Positive2: {0}", positive2 );
// > DateAdd Positive2: 26.03.2011 00:00:00
DateTime? positive3 = dateAdd.Add( dateDiffPositive, Duration.Days( 17 ) );
Console.WriteLine( "DateAdd Positive3: {0}", positive3 );
// > DateAdd Positive3: 14.04.2011 00:00:00
DateTime? positive4 = dateAdd.Add( dateDiffPositive, Duration.Days( 20 ) );
Console.WriteLine( "DateAdd Positive4: {0}", positive4 );
// > DateAdd Positive4: 18.04.2011 00:00:00
// negative
DateTime dateDiffNegative = new DateTime( 2011, 4, 18 );
DateTime? negative1 = dateAdd.Add( dateDiffNegative, Duration.Hours( -1 ) );
Console.WriteLine( "DateAdd Negative1: {0}", negative1 );
// > DateAdd Negative1: 17.04.2011 23:00:00
DateTime? negative2 = dateAdd.Add( dateDiffNegative, Duration.Days( -4 ) );
Console.WriteLine( "DateAdd Negative2: {0}", negative2 );
// > DateAdd Negative2: 13.04.2011 00:00:00
DateTime? negative3 = dateAdd.Add( dateDiffNegative, Duration.Days( -17 ) );
Console.WriteLine( "DateAdd Negative3: {0}", negative3 );
// > DateAdd Negative3: 22.03.2011 00:00:00
DateTime? negative4 = dateAdd.Add( dateDiffNegative, Duration.Days( -20 ) );
Console.WriteLine( "DateAdd Negative4: {0}", negative4 );
// > DateAdd Negative4: 19.03.2011 00:00:00
} // DateAddSample
专业化CalendarDateAdd允许指定加法或减法使用的工作日和工作时间:
// ----------------------------------------------------------------------
public void CalendarDateAddSample()
{
CalendarDateAdd calendarDateAdd = new CalendarDateAdd();
// weekdays
calendarDateAdd.AddWorkingWeekDays();
// holidays
calendarDateAdd.ExcludePeriods.Add( new Day( 2011, 4, 5, calendarDateAdd.Calendar ) );
// working hours
calendarDateAdd.WorkingHours.Add( new HourRange( new Time( 08, 30 ), new Time( 12 ) ) );
calendarDateAdd.WorkingHours.Add( new HourRange( new Time( 13, 30 ), new Time( 18 ) ) );
DateTime start = new DateTime( 2011, 4, 1, 9, 0, 0 );
TimeSpan offset = new TimeSpan( 22, 0, 0 ); // 22 hours
DateTime? end = calendarDateAdd.Add( start, offset );
Console.WriteLine( "start: {0}", start );
// > start: 01.04.2011 09:00:00
Console.WriteLine( "offset: {0}", offset );
// > offset: 22:00:00
Console.WriteLine( "end: {0}", end );
// > end: 06.04.2011 16:30:00
} // CalendarDateAddSample
搜索日历时间段
CalendarPeriodCollector提供了在给定时间限制内搜索某些日历期间的可能性。通过使用ICalendarPeriodCollectorFilter,此类搜索可以受到以下条件的限制:
- 按年份搜索
- 按月份搜索
- 按月数搜索
- 按工作日搜索
如果没有筛选器集,则将期间的所有时间范围视为匹配。可以通过以下目标范围进行合并:
- 年:CalendarPeriodCollector.CollectYears
- 月:CalendarPeriodCollector.CollectMonths
- 日:CalendarPeriodCollector.CollectDays
- 时:CalendarPeriodCollector.CollectHours
在正常模式下,将合并找到的范围的所有时间范围。例如,这允许通过使用CalendarPeriodCollector.CollectHours来查找一天中的所有时间。
为了进一步约束结果,可以按如下方式定义时间范围:
- 一年中的哪个月份:ICalendarPeriodCollectorFilter.AddCollectingMonths
- 一个月中的哪几天:ICalendarPeriodCollectorFilter.AddCollectingDays
- 一天中的哪些时间:ICalendarPeriodCollectorFilter.AddCollectingHours
例如,通过定义从08:00到10:00的小时的时间范围,结果将只包含一个涵盖两个小时的时间段(而不是每个小时都有一个时间段)。在组合大时间范围时,这被证明是一种有价值的(如果不是必要的)优化。
以下示例收集了几年1月份星期五的所有工作时间:
// ----------------------------------------------------------------------
public void CalendarPeriodCollectorSample()
{
CalendarPeriodCollectorFilter filter = new CalendarPeriodCollectorFilter();
filter.Months.Add( YearMonth.January ); // only Januaries
filter.WeekDays.Add( DayOfWeek.Friday ); // only Fridays
filter.CollectingHours.Add( 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( filter, testPeriod );
collector.CollectHours();
foreach ( ITimePeriod period in collector.Periods )
{
Console.WriteLine( "Period: " + period );
}
// > Period: 01.01.2010; 08:00 - 17:59 | 0.09:59
// > Period: 08.01.2010; 08:00 - 17:59 | 0.09:59
// > Period: 15.01.2010; 08:00 - 17:59 | 0.09:59
// > Period: 22.01.2010; 08:00 - 17:59 | 0.09:59
// > Period: 29.01.2010; 08:00 - 17:59 | 0.09:59
// > Period: 07.01.2011; 08:00 - 17:59 | 0.09:59
// > Period: 14.01.2011; 08:00 - 17:59 | 0.09:59
// > Period: 21.01.2011; 08:00 - 17:59 | 0.09:59
// > Period: 28.01.2011; 08:00 - 17:59 | 0.09:59
} // CalendarPeriodCollectorSample
搜索天数
在许多情况下,需要确定下一个可用的工作日,给定多个工作日。从给定时间点开始计算天数时,应排除周末、节假日、服务和维护期。
为了帮助完成此任务,可以使用该DaySeeker类。与CalendarPeriodCollector类似,该类可以使用预定义的过滤器进行控制。以下示例显示了在跳过所有周末和节假日时搜索工作日:
此示例的实现如下所示:
// ----------------------------------------------------------------------
public void DaySeekerSample()
{
Day start = new Day( new DateTime( 2011, 2, 15 ) );
Console.WriteLine( "DaySeeker Start: " + start );
// > DaySeeker Start: Dienstag; 15.02.2011 | 0.23:59
CalendarVisitorFilter filter = new CalendarVisitorFilter();
filter.AddWorkingWeekDays(); // only working days
filter.ExcludePeriods.Add( new Week( 2011, 9 ) ); // week #9
Console.WriteLine( "DaySeeker Holidays: " + filter.ExcludePeriods[ 0 ] );
// > DaySeeker Holidays: w/c 9 2011; 28.02.2011 - 06.03.2011 | 6.23:59
DaySeeker daySeeker = new DaySeeker( filter );
Day day1 = daySeeker.FindDay( start, 3 ); // same working week
Console.WriteLine( "DaySeeker(3): " + day1 );
// > DaySeeker(3): Freitag; 18.02.2011 | 0.23:59
Day day2 = daySeeker.FindDay( start, 4 ); // Saturday -> next Monday
Console.WriteLine( "DaySeeker(4): " + day2 );
// > DaySeeker(4): Montag; 21.02.2011 | 0.23:59
Day day3 = daySeeker.FindDay( start, 9 ); // holidays -> next Monday
Console.WriteLine( "DaySeeker(9): " + day3 );
// > DaySeeker(9): Montag; 07.03.2011 | 0.23:59
} // DaySeekerSample
环境要素
与时间相关的定义和基本计算位于各种实用程序类中:
TimeSpec | 时间和期间的常量 |
YearHalfyear/ | 半年、季度、月和周类型的枚举 |
TimeTool | 用于修改日期和时间值以及特定时间段的操作 |
TimeCompare | 用于比较时间段的函数 |
TimeFormatter | 时间段的格式 |
TimeTrim | 修剪时间段的函数 |
Now | 计算各个时间段的当前时间点;例如,当前日历季度的开始时间 |
Duration | 特定时间段的计算 |
Date | DateTime日期部分 |
Time | DateTime时间部分 |
CalendarVisitor | 用于循环访问日历周期的抽象基类 |
DateTimeSet | 时间中独特时刻的排序列表 |
CalendarVisitor | 用于循环访问日历周期的抽象基类 |
BroadcastCalendarTool | 广播日历的计算工具 |
FiscalCalendarTool | 会计日历的计算工具 |
库和单元测试
库时间段有四个版本:
- .NET 2.0库,包括单元测试
- 用于Silverlight 4的.NET库
- 用于Windows Phone 7的.NET库
- 适用于Windows应用商店、.NET 4、Silverlight 4、Windows Phone 7的可移植类库
NUnit测试涵盖了大多数类。所有三个变体的源代码都是相同的(请参阅下文:复合库开发),但单元测试仅适用于可移植类库和完整的.NET Framework。
为基于时间的功能创建稳定的工作测试并非易事,因为各种因素都会影响测试对象的状态:
- 不同的文化使用不同的日历
- 基于DateTime.Now的功能在不同时间执行时可以有(并且通常会导致)不同的行为和测试结果
- 时间计算——特别是涉及时间段——会导致许多特殊情况
考虑到这一点,在单元测试中发现几乎是实际库实现中代码的三倍也就不足为奇了。
应用
为了可视化日历对象,该库包含用于命令行控制台、Silverlight和Windows Phone的应用程序 Time Period Demo。
为了计算日历期间,Silverlight应用程序日历期间收集器已经可用。该工具基本上是CalendarPeriodCollectorFilter类中最重要的参数的配置前端,并且可以用CalendarPeriodCollector计算时间段。结果可以复制到剪贴板并粘贴到Microsoft Excel中:
复合库开发
时间段库中使用以下命名约定,以便在必要时分隔不同目标平台的文件:
- <FileName>.Desktop.<Extension>
- <FileName>.Silverlight.<Extension>
- <FileName>.WindowsPhone.<Extension>
- <FileName>.Pcl.<Extension>
对于所有目标平台,DLL的名称和命名空间都是相同的。可以在“属性”>“应用程序”>“程序集名称”和“默认命名空间”下更改这些项目设置。
Debug und Release目标的输出将放置在每个目标平台的不同目录中(“属性”>“生成”>输出路径):
- ..\Pub\Desktop.<Debug|Release>\
- ..\Pub\Silverlight.<Debug|Release>\
- ..\Pub\WindowsPhone<Debug|Release>\
- ..\Pub\Pcl<Debug|Release>\
为了防止Visual Studio及其某些扩展工具出现问题,必须(!)将临时编译器输出放在每个目标平台的单独目录中。为此,必须卸载项目并将以下配置元素插入到每个目标中:
<?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>
https://www.codeproject.com/Articles/168662/Time-Period-Library-for-NET