65.9K
CodeProject 正在变化。 阅读更多。
Home

适用于 .NET 的时间段库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (694投票s)

2011年3月14日

MIT

21分钟阅读

viewsIcon

3562050

downloadIcon

31146

广泛的时间段计算和独立的日历时间段

新增:适用于 .NET 7 的时间段库

引言

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

所需功能涵盖以下领域

  • 支持独立时间段
  • 在日历年内处理日历时间段
  • 处理偏离日历年的日历时间段(财政或学期)
  • 处理会计和广播日历

时间计算应同时提供给服务器组件(Web 服务和任务)以及富客户端(WPF 和 Silverlight)和移动设备。

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

经过几个开发周期,形成了以下库 Time Period,现在可用于以下 .NET 运行时环境:

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

时间段

.NET Framework 已经提供了广泛的基类 DateTimeTimeSpan 用于基本的时间相关计算。库 Time Period 通过几个处理时间段的类扩展了 .NET Framework。这些时间段基本上由开始、持续时间和结束来表征。

Time Period

根据定义,开始始终在结束之前发生。如果开始值为可能的最小值 (DateTime.MinValue),则认为开始未定义。同样,如果结束值为可能的最大值 (DateTime.MaxValue),则认为结束未定义。

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

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

  • 时间段的 Start(开始)、End(结束)和 Duration(持续时间)
  • 如果 Start 时间已定义,则 HasStarttrue
  • 如果 End 时间已定义,则 HasEndtrue
  • 如果 StartEnd 时间都未定义,则 IsAnytimetrue
  • 如果 StartEnd 具有相同的值,则 IsMomenttrue
  • 对于不可变的时间段,IsReadOnlytrue(有关其用法,请参见下文)

两个时间段的关系由枚举 PeriodRelation 描述。

为了方便查询特殊、常用时间段关系,提供了 IsSamePeriodHasInsideOverlapsWithIntersectsWith 等方法。

时间范围

TimeRange 作为 ITimeRange 的实现,通过其 StartEnd 定义时间段;持续时间由此计算得出。

Time Range

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

为了修改这样一个时间段,有各种操作可用(橙色 = 新实例)

Time Range Operations

以下示例展示了 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 是计算出来的。

Time Block

TimeRange 类似,可以使用 Start/EndStart/DurationDuration/End 创建 TimeBlock。如上所述,StartEnd 在必要时将自动排序。

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

Time Block Operations

以下示例展示了 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

时间间隔

ITimeIntervalITimeRange 一样通过 StartEnd 确定其时间段。此外,还可以通过枚举 IntervalEdge 控制其 StartEnd 的解释。

  • Closed:边界时间点包含在计算中。这与 ITimeRange 的行为一致。
  • Open:边界时间点表示一个边界值,该值在计算中被排除。

可能的区间变体如下

Time Interval

通常,区间边缘的值为 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 来关闭此排除。

可以使用 TimeSpec.MinPeriodDate 作为 StartTimeSpec.MaxPeriodDate 作为 End 来创建无边界的时间间隔。

时间段容器

在日常使用中,时间计算通常涉及多个时间段,这些时间段可以收集在一个容器中并作为一个整体进行操作。时间段 库为时间段提供了以下容器:

Time Period Container Interfaces

所有容器都基于接口 ITimePeriod,因此容器本身就代表一个时间段。这样,它们可以像其他时间段一样用于计算,例如 ITimeRange

接口 ITimePeriodContainer 作为所有容器的基类,并通过从 IList<ITimePeriod> 派生提供列表功能。

时间段集合

ITimePeriodCollection 可以容纳任意 ITimePeriod 类型的元素,并将其所有元素的最早开始时间解释为集合时间段的开始。相应地,其所有元素的最晚结束时间作为集合时间段的结束。

Time Period Collection

时间段集合提供以下操作

Time Period Collection Operations

以下示例展示了实现 ITimePeriodCollection 接口的 TimePeriodCollection 类的用法。

// ----------------------------------------------------------------------
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 类型的时间段连接成一个链,并确保连续的时间段之间没有间隙。

Time Period Chain

由于 ITimePeriodChain 可能会改变元素的位置,因此不能添加只读时间段。尝试这样做会导致 NotSupportedExceptionITimePeriodChain 提供以下功能:

Time Period Chain Operations

以下示例展示了实现 ITimePeriodChain 接口的 TimePeriodChain 类的用法。

// ----------------------------------------------------------------------
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

日历时间段

日历时间段的计算必须考虑特殊性:一个时间段的结束不等于下一个时间段的开始。以下示例显示了 13h 到 15h 之间小时的相应值:

  • 13:00:00.0000000 - 13:59:59.9999999
  • 14:00:00.0000000 - 14:59:59.9999999

结束时间比下一个开始时间早一刻,两者之间的差值至少为 1 Tick = 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 接口中。

Time Calendar Interfaces

ITimeCalendar 涵盖以下领域:

  • 分配给 CultureInfo(默认 = 当前线程的 CultureInfo
  • 时间段边界映射(ITimePeriodMapper
  • 年份的基准月份(默认为一月)
  • 如何解释日历周的定义
  • 会计日历定义
  • 时间段的命名,例如年份的名称(财政年度、学年等)
  • 各种日历相关计算

ITimePeriodMapper 派生,时间段边界的映射通过属性 StartOffset(默认 = 0)和 EndOffset(默认 = -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 类的更详细描述见下文。

日历元素

对于最常用的日历元素,提供了专门的类。

时间段 单一时间段 多个时间段 指代年份的基准月份
年份 年份 年份
广播年份 BroadcastYear -
半年 半年 半年
季度 季度 季度
月份
广播月 BroadcastMonth -
周数
广播周 BroadcastWeek -
天数
小时 小时 小时数
分钟 分钟 分钟数

具有多个时间段的元素的实例化可以通过指定的时间段数量来完成。

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

所有日历元素都派生自基类 CalendarTimeRange,而 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 设置,并由日历元素 Year、Half Year 和 Quarter 考虑。年份开始的有效值可以是任意月份。因此,日历年只是一个特殊情况,其中 YearBaseMonth = YearMonth.January

以下属性控制年份边界的解释

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

通常,七月或之后开始的财政年度使用下一个日历年的年份。日历属性 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 控制日历周数的计算。YearWeekTypeITimeCalendar 支持,因此定义了不同的计算方式。

// ----------------------------------------------------------------------
// 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 确定一年开始的星期几。

以下示例显示了以八月最后一个星期六结束的财政年度。

// ----------------------------------------------------------------------
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

接下来的财政年度将在距离八月底更近的那个星期六结束。

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,并包含哪些时间段在特定时刻开始和结束的信息。这种表示允许在处理时间轴时通过加法和减法来跟踪运行余额。

Time Line

时间轴上的时刻存储在 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 计算集合中时间段之间的间隔。

Time Gap Calculator

时间点的解释可能受 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 类提供了合并此类时间段的可能性。

Time Periods Consolidation

以下示例展示了根据图示组合时间段。

// ----------------------------------------------------------------------
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 类就能派上用场。

Time Periods Intersections

默认情况下,交集时间段会合并为一个。要保留所有交集时间段,可以将 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 类,您可以从其他时间段(被减数)中减去时间段(减数)。

Time Period Subtraction

结果包含两个时间段集合之间的差异

// ----------------------------------------------------------------------
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 类可能会派上用场。

Date Addition and Subtraction

尽管类的名称可能暗示相反,但可以进行加法和减法运算。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:00 到 10:00 的小时时间范围,结果将只包含一个覆盖这两个小时的时间段(而不是每个小时一个时间段)。这在组合大型时间范围时被证明是一个有价值(如果不是必要)的优化。

以下示例收集了多年一月份星期五的所有工作时间。

// ----------------------------------------------------------------------
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 类似,此类别可以通过预定义过滤器进行控制。以下示例显示了在跳过所有周末和节假日的情况下搜索工作日。

Day Seeker

此示例的实现如下所示

// ----------------------------------------------------------------------
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

环境元素

时间相关定义和基本计算位于各种实用程序类中。

时间规格 时间和时间段的常量
年份半年度/
年份季度/
年份月份/
年周类型
半年、季度、月份和周类型的枚举
时间工具 日期和时间值以及特定时间段的修改操作
时间比较 时间段比较函数
时间格式化器 时间段格式化
时间修剪 修剪时间段的函数
现在 计算各种时间段的当前时刻;例如,当前日历季度的开始时间。
持续时间 特定时间段的计算
日期 DateTime 的日期部分
时间 DateTime 的时间部分
日历访问者 迭代日历时间段的抽象基类
日期时间集合 唯一时间点的有序列表
日历访问者 迭代日历时间段的抽象基类
广播日历工具 广播日历的计算工具
财政日历工具 财政日历的计算工具

库和单元测试

时间段库有四个版本

  • 适用于 .NET 2.0 的库,包括单元测试
  • 适用于 Silverlight 4 的 .NET 库
  • 适用于 Windows Phone 7 的 .NET 库
  • o 适用于 Windows Store、.NET 4、Silverlight 4、Windows Phone 7 的可移植类库

大多数类都由 NUnit 测试覆盖。所有三种变体的源代码都相同(见下文:复合库开发),但单元测试仅适用于可移植类库和完整的 .NET Framework。

为基于时间的功能创建稳定的工作测试并非易事,因为各种因素会影响测试对象的状态。

  • 不同的文化使用不同的日历
  • 基于 DateTime.Now 的功能在不同时间执行时可能(而且通常会)导致不同的行为和测试结果。
  • 时间计算——尤其是涉及时间段的计算——会导致大量特殊情况。

考虑到这一点,单元测试中的代码量几乎是实际库实现的三倍,这并不奇怪。

应用

为了可视化日历对象,该库包含用于命令行控制台、Silverlight 和 Windows Phone 的应用程序 Time Period Demo

为了计算日历时间段,我们提供了 Silverlight 应用程序 Calendar Period Collector。该工具基本上是 CalendarPeriodCollectorFilter 类最重要参数的配置前端,并且可以使用 CalendarPeriodCollector 计算时间段。结果可以复制到剪贴板并粘贴到 Microsoft Excel 中。

复合库开发

在 Time Period 库中,为了在必要时区分不同目标平台的文件,使用了以下命名约定:

  • <文件名>.Desktop.<扩展名>
  • <文件名>.Silverlight.<扩展名>
  • <文件名>.WindowsPhone.<扩展名>
  • <文件名>.Pcl.<扩展名>

DLL 的名称和命名空间对于所有目标平台都是相同的。这些项目设置可以在 属性 > 应用程序 > 程序集名称和默认命名空间 下更改。

Debug 和 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>

历史

  • 2023年8月30日 - v2.1.4
    • CalendarPeriodCollector 修复了空指针访问(感谢 VenkiDevelopers)
  • 2023年8月29日 - v2.1.3
    • CalendarPeriodCollector 按日小时收集时间段时不遵守限制时间段(感谢 12868655 和 jntsouza)
  • 2023年8月18日 - v2.1.2
    • 增加了目标框架 .NET 7
    • 修复了空范围的关系获取(感谢 jnyrup)
    • 修复了时间段减法器忽略合并时间段参数的问题(感谢 Mar-Pfa)
  • 2017年5月27日 - v2.1.1
    • 修复了缺少程序集签名的问题
    • 移除了“应用程序”和“复合库开发”章节。
  • 2017年5月24日 - v2.1.0
    • 更改目标框架以支持 .NET、.NET Core、Mono、Xamarin、UWP
    • 移除了对 Silverlight、PCL 和 WindowsPhone 的支持
    • 测试框架从 NUnit 更改为 xUnit
    • CalendarDateAdd.GetAvailableWeekPeriods 现在受保护
    • TimeFormatter.GetPeriod:修复了复制/粘贴错误
    • TimePeriodSubtractor.SubtractPeriods:修复了缺少参数 combinePeriods 的问题
    • TimeLine.CombinePeriods/IntersectPeriods:修复了多时刻处理
    • HalfyearTimeRange.GetMonths:修复了下半年的计算
  • 2014年4月3日 - v2.0.0
    • 优化了间隙、交集和重叠计算算法
      • 性能提升了 30% 或更多。
      • 重构了 TimeLineTimeLineMomentTimeLineMommentCollection
    • 新增对会计(4-4-5)日历的支持
      • ITimeCalendar:用 month 参数扩展了 GetYear 方法。
      • ITimeCalendar/TimeCalendarConfig:新增属性 FiscalFirstDayOfYear 用于控制年份的第一天。
      • ITimeCalendar/TimeCalendarConfig:新增属性 FiscalYearAlignment 用于控制年份对齐方式。
      • ITimeCalendar/TimeCalendarConfig:新增属性 FiscalQuarterGrouping 用于控制季度内的周分组。
      • Year/Halfyear/Quarter/Month:新增对财政会计日历的支持。
    • 新的实用程序类 FiscalCalendarTool.
    • DateDiff:新增带有 TimeSpan 的构造函数。
    • Merged YearCalendarTimeRange 合并到 CalendarTimeRange 中。
    • ITimeFormatter:新增属性以自定义持续时间分隔符和间隔标记。
    • 新文档章节《会计日历》和《时间线》。
  • 2013年11月23日 - v1.7.2
    • 新增对 .NET 可移植类库 (PCL) 的支持
      • 平台:.NET 4.0, Silverlight 4, Windows Phone 7, Windows 8
      • 二进制目标文件夹:Pub\Pcl.DebugPub\Pcl.Release
  • 2013年11月10日 - v1.7.1
    • Date:修复了 Equals()
    • Date:将方法 GetDateTime() 替换为属性 DateTime
    • Time:修复了 CompareTo()
    • Time:基于 TimeSpan 持续时间的新构造函数
    • Time:新增属性 IsZeroIsFullDayIsFullDayOrZero
    • Time:将 ToString() 表示更改为 00:00:00.000,整天显示为 24:00:00.000
    • Date/Time:添加了运算符 -+<<===!=>= >
    • Date/Time:将 GetDateTime() 重命名为 ToDateTime() 并添加了新的静态变体。
  • 2013年11月7日 - v1.7.0
    • 库目标框架从 .NET 2.0 更改为 3.5
    • TimePeriodCollection:允许小时、分钟、秒和毫秒的最大值。
    • Date/Time:实现了接口 IComparableIComparable<>IEquatable<>
    • ITimePeriodCollection:新增属性 TotalDuration,包含所有项目持续时间的总和。
    • ITimePeriodCollection:用于计算特殊持续时间(如夏令时)的新接口。
    • TimeZoneDurationProvider:计算时区特定持续时间
      • 基于 CLR 类 TimeZoneInfo
      • 通过自定义回调函数处理模棱两可和无效的 DTS 时刻。
      • 在缺少回调的情况下抛出 Exceptions <code>AmbigiousMomentExceptionInvalidMomentException
    • ITimePeriod/ITimePeriodCollection:通过 GetTotalDuration( IDurationProvider ) 支持持续时间提供者
    • ITimePeriodCollection:新增方法 GetTotalDuration 以汇总所有持续时间,同时遵守 IDurationProvider
    • CalendardateDiff/DurationCalculator:新增对 IDurationProvider 的支持
    • DurationCalculator:将方法 ExcludePeriods 替换为属性
    • DurationCalculator:新增属性 IncludePeriods 用于指定包含的时间段。
    • 财政年度:新增对在上一个日历年内开始的财政年度的支持。
      • ITimeCalendar:新增属性 FiscalYearBaseMonth,指定财政年度切换的月份(默认=七月)
      • ITimeCalendar:新增方法 GetYear(),用于计算目标年份,同时遵守 FiscalYearBaseMonth
      • Year/Halfyear/Quarter:新增对 FiscalYearBaseMonth 的支持。
  • 2013年10月27日 - v1.6.0
    • TimePeriodCollection:修复了 SortByEnd() 中的排序问题。
    • TimeFormatter.GetInterval:修复了格式描述
    • ITimePeriodComparer:支持基于 IComparer 的时间段比较。
      • ResverseTimePeriodComparer:以相反的顺序比较两个时间段。
      • TimePeriodStartComparer/TimePeriodEndComparer/TimePeriodDurationComparer:预定义的时间段比较器。
      • ITimePeriod:新增方法 CompareTo()
      • ITimePeriodCollection:新增方法 SortBy()SortReverseBy()
    • ITimeLineMomemtCollection/ITimeLine/ITimePeriodCollection:新增方法 HasOverlaps() 以识别是否存在任何时间段重叠。
    • ITimeLineMomemtCollection/ITimeLine/ITimePeriodCollection:新增方法 HasGaps() 以识别是否存在任何间隙。
    • DurationCalculator:新类,用于计算持续时间,同时考虑工作日和每日小时数。
  • 2013年10月1日 - v1.5.0
    • 新增对广播日历的支持:新增类 BroadcastYearBroadcastMonthBroadcastWeek
  • 2013年9月23日 - v1.4.12.0
    • TimeTool.GetWeekOfYear:修复了在 YearWeekType 设置为 Iso8601CalendarWeekRule 设置为 FirstFourDayWeek 且周不是从星期一开始时的计算问题。该计算影响 WeekWeeks 类 - 感谢 Paul。
    • Date.GetDateTime:参数 hour 现在是强制性的。
    • TimeFormatter.GetDateTime:使用类成员文化进行字符串转换。
    • 移除了到 http://www.cpc.itenso.com 的链接。
  • 2012年9月4日 - v1.4.11.0
    • 新类 TimeLinePeriodEvaluator:通过遍历时间轴评估时间段
  • 2012年8月23日 - v1.4.10.0
    • TimeLine.GetTimeLineMoments:修复了迭代接口从 ITimeRangeITimePeriod 的问题。
  • 2012年5月17日 - v1.4.9.0
    • 新增 NuGet 包
    • HashTool:实用程序现在是公共的。
    • TimePeriodSubtractor:添加了 Silverlight 和 Windows Phone 项目中缺失的文件引用。
  • 2012年3月2日 - v1.4.6.0
    • DateDiff:修复了在特定场景下不可表示的 DateTime 异常。
  • 2012年1月30日 - v1.4.5.0
    • 新类 TimePeriodSubtractor:从另一个时间段集合中减去一个时间段集合。
  • ... 完整的变更历史记录在 docu/ChangeHistory.txt 中
  • 2011年3月14日 - v1.0.0.0
    • 首次公开发布
© . All rights reserved.