.NET的时间段库

目录

介绍

时间段

时间范围

时间块

时间间隔

时间段容器

时间段集合

时间段链

日历时间段

时间日历

日历元素

年和年期

星期

会计日历

广播日历

时间段计算工具

时间线

两个时间点之间的差异

时间间隔计算

合并时间段

时间段的交集

时间段的减法

日期的加法和减法

搜索日历时间段

搜索天数

环境要素

库和单元测试

应用

复合库开发


新增内容:.NET 7的时间段库

介绍

在为另一个项目实施一些软件时,我遇到了几个涉及时间段计算的要求。这些计算是解决方案的重要组成部分,对结果的正确性和准确性有很高的要求。

所需的功能包括以下方面:

  • 支持各个时间段
  • 在日历年的日历期间工作
  • 使用与日历年不同的日历期间(财政或学校期间)
  • 处理会计和广播日历

时间计算应同时用于服务器组件(Web服务和任务)以及富客户端(WPFSilverlight)和移动设备。

通过分析情况,我得出的结论是,.NET Framework的组件(我没有预料到)或任何其他可用的工具都无法满足所有需求。因为我在早期的项目中已经遇到过类似的需求,所以我决定为此开发一个通用库。

从多个开发周期中产生了以下库时间段,该库现在可用于以下.NET运行时环境:

  • .NET Framework 3.5或更高版本
  • .NET Core框架
  • .NET Mono Framework
  • Xamarin
  • 通用Windows平台——UWP

时间段

.NET Framework已经提供了广泛的基类DateTimeTimeSpan与时间相关的基本计算。库 Time Period .NET Framework扩展了几个类,用于处理时间段。这些时期基本上以开始、持续时间和结束为特征:

根据定义,开始总是发生在结束之前。如果start包含最小可能值(DateTime.MinValue),则将其视为未定义。同样,如果末端保持最大可能值(DateTime.MaxValue),则端点是未定义的。

这些时间段的实现基于ITimePeriod接口,并由专业化ITimeRangeITimeBlockITimeInterval扩展:

ITimePeriod接口提供时间段的信息和操作,但未定义关键属性的计算方式:

  • StartEndDuration时间段
  • HasStarttrue如果Start时间是定义了的
  • HasEndtrue如果End时间是否定义了的
  • IsAnytimetrue如果既没有定义Start时间,也没有定义End时间
  • IsMomenttrue如果StartEnd保持相同的值
  • IsReadOnlytrue适用于不可变的时间段(有关其用法,请参见下文)

两个时间段的关系由以下PeriodRelation列举描述:

为了方便起见,可以使用IsSamePeriodHasInsideOverlapsWithIntersectsWith等方法来查询此类周期关系的特殊、常用的变体。

时间范围

TimeRange作为ITimeRange的实现,通过其StartEnd定义时间段;持续时间的计算公式如下:

TimeRange可以通过指定其Start/EndStart/DurationDuration/End来创建。如果需要,给定的StartEnd将按时间顺序排序。

对于此类时间段的修改,可以使用各种操作(橙色=新实例):

以下示例显示了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接口并通过StartDuration定义时间段; End正在计算:

TimeRange一样,TimeBlock可以用Start/EndStart/DurationDuration/End创建。如上所述,必要时StartEnd会自动排序。

对于时间块的修改,可以使用以下操作(橙色=新实例):

以下示例显示了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确定其时间段,如有StartEndITimeRange。此外,还可以通过枚举IntervalEdge来控制其StartEnd的解释:

  • 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可能会更改元素的位置,因此无法添加只读时间段。尝试这样做会导致NotSupportedExceptionITimePeriodChain提供以下功能:

以下示例显示了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,时间段边界的映射发生在属性StartOffsetdefault = 0)和EndOffsetdefault = -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

下面将对这些类YearQuarter进行更全面的描述。

日历元素

对于最常用的日历元素,可以使用专门的类:

时间段

单周期

多个时期

指以年为基准的月份

Year

Years

是的

广播年

BroadcastYear

-

不是

半年

Halfyear

Halfyears

是的

季度

Quarter

Quarters

是的

Month

Months

不是

广播月份

BroadcastMonth

-

不是

Week

Weeks

不是

广播出周

BroadcastWeek

-

不是

Day

Days

不是

小时

Hour

Hours

不是

分钟

Minute

Minutes

不是

实例化具有多个周期的元素可以在指定数量的周期内发生。

下图显示了季度和月份的日历元素,其他元素类似:

所有日历元素都派生自基类CalendarTimeRange,而基类本身派生自TimeRangeCalendarTimeRange包含时间日历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的特殊情况。

以下属性控制对年份之间边界的解释:

  • 如果时间段跨越多个日历年,则MultipleCalendarYearstrue
  • 如果某个时间段与日历年中的一个时间段相对应,则IsCalendarYear/Halfyear/Quartertrue

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

星期

通常的做法是将一年中的周数从152/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

广播日历

广播日历由以下类BroadcastYearBroadcastMonthBroadcastWeek支持:

// ----------------------------------------------------------------------
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 FrameworkTimeSpan结构仅提供天、小时、分钟、秒和毫秒的时间范围值。从用户的角度来看,通常还需要表示时间范围的月份和年份:

  • 最后一次访问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.AddDateTime.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:0010: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/
YearQuarter/
YearMonth/
YearWeekType

半年、季度、月和周类型的枚举

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的功能在不同时间执行时可以有(并且通常会导致)不同的行为和测试结果
  • 时间计算——特别是涉及时间段——会导致许多特殊情况

考虑到这一点,在单元测试中发现几乎是实际库实现中代码的三倍也就不足为奇了。

应用

为了可视化日历对象,该库包含用于命令行控制台、SilverlightWindows 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值