适用于 .NET 的时间段库






4.96/5 (694投票s)
广泛的时间段计算和独立的日历时间段
新增:适用于 .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 已经提供了广泛的基类 DateTime
和 TimeSpan
用于基本的时间相关计算。库 Time Period 通过几个处理时间段的类扩展了 .NET Framework。这些时间段基本上由开始、持续时间和结束来表征。
根据定义,开始始终在结束之前发生。如果开始值为可能的最小值 (DateTime.MinValue
),则认为开始未定义。同样,如果结束值为可能的最大值 (DateTime.MaxValue
),则认为结束未定义。
这些时间段的实现基于接口 ITimePeriod
,并由专门化 ITimeRange
、ITimeBlock
和 ITimeInterval
扩展。
接口 ITimePeriod
提供时间段的信息和操作,而不定义计算关键属性的方式。
- 时间段的
Start
(开始)、End
(结束)和Duration
(持续时间) - 如果
Start
时间已定义,则HasStart
为true
。 - 如果
End
时间已定义,则HasEnd
为true
。 - 如果
Start
和End
时间都未定义,则IsAnytime
为true
。 - 如果
Start
和End
具有相同的值,则IsMoment
为true
。 - 对于不可变的时间段,
IsReadOnly
为true
(有关其用法,请参见下文)
两个时间段的关系由枚举 PeriodRelation
描述。
为了方便查询特殊、常用时间段关系,提供了 IsSamePeriod
、HasInside
、OverlapsWith
或 IntersectsWith
等方法。
时间范围
TimeRange
作为 ITimeRange
的实现,通过其 Start
和 End
定义时间段;持续时间由此计算得出。
一个 TimeRange
可以通过指定其 Start
/End
,Start
/Duration
或 Duration
/End
来创建。如果需要,给定的 Start
和 End
将按时间顺序排序。
为了修改这样一个时间段,有各种操作可用(橙色 = 新实例)
以下示例展示了 TimeRange
的用法
// ----------------------------------------------------------------------
public void TimeRangeSample()
{
// --- time range 1 ---
TimeRange timeRange1 = new TimeRange(
new DateTime( 2011, 2, 22, 14, 0, 0 ),
new DateTime( 2011, 2, 22, 18, 0, 0 ) );
Console.WriteLine( "TimeRange1: " + timeRange1 );
// > TimeRange1: 22.02.2011 14:00:00 - 18:00:00 | 04:00:00
// --- time range 2 ---
TimeRange timeRange2 = new TimeRange(
new DateTime( 2011, 2, 22, 15, 0, 0 ),
new TimeSpan( 2, 0, 0 ) );
Console.WriteLine( "TimeRange2: " + timeRange2 );
// > TimeRange2: 22.02.2011 15:00:00 - 17:00:00 | 02:00:00
// --- time range 3 ---
TimeRange timeRange3 = new TimeRange(
new DateTime( 2011, 2, 22, 16, 0, 0 ),
new DateTime( 2011, 2, 22, 21, 0, 0 ) );
Console.WriteLine( "TimeRange3: " + timeRange3 );
// > TimeRange3: 22.02.2011 16:00:00 - 21:00:00 | 05:00:00
// --- relation ---
Console.WriteLine( "TimeRange1.GetRelation( TimeRange2 ): " +
timeRange1.GetRelation( timeRange2 ) );
// > TimeRange1.GetRelation( TimeRange2 ): Enclosing
Console.WriteLine( "TimeRange1.GetRelation( TimeRange3 ): " +
timeRange1.GetRelation( timeRange3 ) );
// > TimeRange1.GetRelation( TimeRange3 ): EndInside
Console.WriteLine( "TimeRange3.GetRelation( TimeRange2 ): " +
timeRange3.GetRelation( timeRange2 ) );
// > TimeRange3.GetRelation( TimeRange2 ): StartInside
// --- intersection ---
Console.WriteLine( "TimeRange1.GetIntersection( TimeRange2 ): " +
timeRange1.GetIntersection( timeRange2 ) );
// > TimeRange1.GetIntersection( TimeRange2 ):
// 22.02.2011 15:00:00 - 17:00:00 | 02:00:00
Console.WriteLine( "TimeRange1.GetIntersection( TimeRange3 ): " +
timeRange1.GetIntersection( timeRange3 ) );
// > TimeRange1.GetIntersection( TimeRange3 ):
// 22.02.2011 16:00:00 - 18:00:00 | 02:00:00
Console.WriteLine( "TimeRange3.GetIntersection( TimeRange2 ): " +
timeRange3.GetIntersection( timeRange2 ) );
// > TimeRange3.GetIntersection( TimeRange2 ):
// 22.02.2011 16:00:00 - 17:00:00 | 01:00:00
} // TimeRangeSample
以下示例测试预订是否在一天的工作时间内
// ----------------------------------------------------------------------
public bool IsValidReservation( DateTime start, DateTime end )
{
if ( !TimeCompare.IsSameDay( start, end ) )
{
return false; // multiple day reservation
}
TimeRange workingHours =
new TimeRange( TimeTrim.Hour( start, 8 ), TimeTrim.Hour( start, 18 ) );
return workingHours.HasInside( new TimeRange( start, end ) );
} // IsValidReservation
时间块
TimeBlock
实现了 ITimeBlock
接口,并通过 Start
和 Duration
定义时间段;End
是计算出来的。
与 TimeRange
类似,可以使用 Start
/End
、Start
/Duration
或 Duration
/End
创建 TimeBlock
。如上所述,Start
和 End
在必要时将自动排序。
要修改时间块,可以使用以下操作(橙色 = 新实例)
以下示例展示了 TimeBlock
的用法
// ----------------------------------------------------------------------
public void TimeBlockSample()
{
// --- time block ---
TimeBlock timeBlock = new TimeBlock(
new DateTime( 2011, 2, 22, 11, 0, 0 ),
new TimeSpan( 2, 0, 0 ) );
Console.WriteLine( "TimeBlock: " + timeBlock );
// > TimeBlock: 22.02.2011 11:00:00 - 13:00:00 | 02:00:00
// --- modification ---
timeBlock.Start = new DateTime( 2011, 2, 22, 15, 0, 0 );
Console.WriteLine( "TimeBlock.Start: " + timeBlock );
// > TimeBlock.Start: 22.02.2011 15:00:00 - 17:00:00 | 02:00:00
timeBlock.Move( new TimeSpan( 1, 0, 0 ) );
Console.WriteLine( "TimeBlock.Move(1 hour): " + timeBlock );
// > TimeBlock.Move(1 hour): 22.02.2011 16:00:00 - 18:00:00 | 02:00:00
// --- previous/next ---
Console.WriteLine( "TimeBlock.GetPreviousPeriod(): " +
timeBlock.GetPreviousPeriod() );
// > TimeBlock.GetPreviousPeriod(): 22.02.2011 14:00:00 - 16:00:00 | 02:00:00
Console.WriteLine( "TimeBlock.GetNextPeriod(): " + timeBlock.GetNextPeriod() );
// > TimeBlock.GetNextPeriod(): 22.02.2011 18:00:00 - 20:00:00 | 02:00:00
Console.WriteLine( "TimeBlock.GetNextPeriod(+1 hour): " +
timeBlock.GetNextPeriod( new TimeSpan( 1, 0, 0 ) ) );
// > TimeBlock.GetNextPeriod(+1 hour): 22.02.2011 19:00:00 - 21:00:00 | 02:00:00
Console.WriteLine( "TimeBlock.GetNextPeriod(-1 hour): " +
timeBlock.GetNextPeriod( new TimeSpan( -1, 0, 0 ) ) );
// > TimeBlock.GetNextPeriod(-1 hour): 22.02.2011 17:00:00 - 19:00:00 | 02:00:00
} // TimeBlockSample
时间间隔
ITimeInterval
像 ITimeRange
一样通过 Start
和 End
确定其时间段。此外,还可以通过枚举 IntervalEdge
控制其 Start
和 End
的解释。
Closed
:边界时间点包含在计算中。这与ITimeRange
的行为一致。Open
:边界时间点表示一个边界值,该值在计算中被排除。
可能的区间变体如下
通常,区间边缘的值为 IntervalEdge.Closed
,这会导致与相邻时间段的交点。一旦其中一个相邻点的值设置为 IntervalEdge.Open
,则不存在交点。
// ----------------------------------------------------------------------
public void TimeIntervalSample()
{
// --- time interval 1 ---
TimeInterval timeInterval1 = new TimeInterval(
new DateTime( 2011, 5, 8 ),
new DateTime( 2011, 5, 9 ) );
Console.WriteLine( "TimeInterval1: " + timeInterval1 );
// > TimeInterval1: [08.05.2011 - 09.05.2011] | 1.00:00
// --- time interval 2 ---
TimeInterval timeInterval2 = new TimeInterval(
timeInterval1.End,
timeInterval1.End.AddDays( 1 ) );
Console.WriteLine( "TimeInterval2: " + timeInterval2 );
// > TimeInterval2: [09.05.2011 - 10.05.2011] | 1.00:00
// --- relation ---
Console.WriteLine( "Relation: " + timeInterval1.GetRelation( timeInterval2 ) );
// > Relation: EndTouching
Console.WriteLine( "Intersection: " +
timeInterval1.GetIntersection( timeInterval2 ) );
// > Intersection: [09.05.2011]
timeInterval1.EndEdge = IntervalEdge.Open;
Console.WriteLine( "TimeInterval1: " + timeInterval1 );
// > TimeInterval1: [08.05.2011 - 09.05.2011) | 1.00:00
timeInterval2.StartEdge = IntervalEdge.Open;
Console.WriteLine( "TimeInterval2: " + timeInterval2 );
// > TimeInterval2: (09.05.2011 - 10.05.2011] | 1.00:00
// --- relation ---
Console.WriteLine( "Relation: " + timeInterval1.GetRelation( timeInterval2 ) );
// > Relation: Before
Console.WriteLine( "Intersection: " +
timeInterval1.GetIntersection( timeInterval2 ) );
// > Intersection:
} // TimeIntervalSample
对于某些场景,例如搜索时间段中的间隙,排除时间段边缘可能会导致不期望的结果。在这种情况下,可以通过设置属性 IsIntervalEnabled
来关闭此排除。
可以使用 TimeSpec.MinPeriodDate
作为 Start
,TimeSpec.MaxPeriodDate
作为 End
来创建无边界的时间间隔。
时间段容器
在日常使用中,时间计算通常涉及多个时间段,这些时间段可以收集在一个容器中并作为一个整体进行操作。时间段 库为时间段提供了以下容器:
所有容器都基于接口 ITimePeriod
,因此容器本身就代表一个时间段。这样,它们可以像其他时间段一样用于计算,例如 ITimeRange
。
接口 ITimePeriodContainer
作为所有容器的基类,并通过从 IList<ITimePeriod>
派生提供列表功能。
时间段集合
ITimePeriodCollection
可以容纳任意 ITimePeriod
类型的元素,并将其所有元素的最早开始时间解释为集合时间段的开始。相应地,其所有元素的最晚结束时间作为集合时间段的结束。
时间段集合提供以下操作
以下示例展示了实现 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
类型的时间段连接成一个链,并确保连续的时间段之间没有间隙。
由于 ITimePeriodChain
可能会改变元素的位置,因此不能添加只读时间段。尝试这样做会导致 NotSupportedException
。ITimePeriodChain
提供以下功能:
以下示例展示了实现 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
接口中。
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
Year
和 Quarter
类的更详细描述见下文。
日历元素
对于最常用的日历元素,提供了专门的类。
时间段 | 单一时间段 | 多个时间段 | 指代年份的基准月份 |
年份 | 年份 | 年份 | 是 |
广播年份 | BroadcastYear | - | 否 |
半年 | 半年 | 半年 | 是 |
季度 | 季度 | 季度 | 是 |
月 | 月 | 月份 | 否 |
广播月 | BroadcastMonth | - | 否 |
周 | 周 | 周数 | 否 |
广播周 | BroadcastWeek | - | 否 |
日 | 日 | 天数 | 否 |
小时 | 小时 | 小时数 | 否 |
分钟 | 分钟 | 分钟数 | 否 |
具有多个时间段的元素的实例化可以通过指定的时间段数量来完成。
下图显示了季度和月份的日历元素,其他元素也类似。
所有日历元素都派生自基类 CalendarTimeRange
,而 CalendarTimeRange
又派生自 TimeRange
。CalendarTimeRange
包含时间日历 ITimeCalendar
,因此确保时间段的值在创建后不能更改(IsReadOnly=true
)。
由于通过基类 TimePeriod
继承,日历元素实现了 ITimePeriod
接口,因此它们都可以用于与其他时间段的计算。
以下示例展示了各种日历元素
// ----------------------------------------------------------------------
public void CalendarYearTimePeriodsSample()
{
DateTime moment = new DateTime( 2011, 8, 15 );
Console.WriteLine( "Calendar Periods of {0}:", moment.ToShortDateString() );
// > Calendar Periods of 15.08.2011:
Console.WriteLine( "Year : {0}", new Year( moment ) );
Console.WriteLine( "Halfyear : {0}", new Halfyear( moment ) );
Console.WriteLine( "Quarter : {0}", new Quarter( moment ) );
Console.WriteLine( "Month : {0}", new Month( moment ) );
Console.WriteLine( "Week : {0}", new Week( moment ) );
Console.WriteLine( "Day : {0}", new Day( moment ) );
Console.WriteLine( "Hour : {0}", new Hour( moment ) );
// > Year : 2011; 01.01.2011 - 31.12.2011 | 364.23:59
// > Halfyear : HY2 2011; 01.07.2011 - 31.12.2011 | 183.23:59
// > Quarter : Q3 2011; 01.07.2011 - 30.09.2011 | 91.23:59
// > Month : August 2011; 01.08.2011 - 31.08.2011 | 30.23:59
// > Week : w/c 33 2011; 15.08.2011 - 21.08.2011 | 6.23:59
// > Day : Montag; 15.08.2011 - 15.08.2011 | 0.23:59
// > Hour : 15.08.2011; 00:00 - 00:59 | 0.00:59
} // CalendarYearTimePeriodsSample
一些特定的日历元素提供方法来访问其子元素的时间段。以下示例显示了一个日历年的季度。
// ----------------------------------------------------------------------
public void YearQuartersSample()
{
Year year = new Year( 2012 );
ITimePeriodCollection quarters = year.GetQuarters();
Console.WriteLine( "Quarters of Year: {0}", year );
// > Quarters of Year: 2012; 01.01.2012 - 31.12.2012 | 365.23:59
foreach ( Quarter quarter in quarters )
{
Console.WriteLine( "Quarter: {0}", quarter );
}
// > Quarter: Q1 2012; 01.01.2012 - 31.03.2012 | 90.23:59
// > Quarter: Q2 2012; 01.04.2012 - 30.06.2012 | 90.23:59
// > Quarter: Q3 2012; 01.07.2012 - 30.09.2012 | 91.23:59
// > Quarter: Q4 2012; 01.10.2012 - 31.12.2012 | 91.23:59
} // YearQuartersSample
年份和年份期间
日历元素的一个特殊之处在于它们支持偏离(正常)日历年的日历时间段。
年份的开始可以通过属性 ITimeCalendar.YearBaseMonth
设置,并由日历元素 Year、Half Year 和 Quarter 考虑。年份开始的有效值可以是任意月份。因此,日历年只是一个特殊情况,其中 YearBaseMonth = YearMonth.January
。
以下属性控制年份边界的解释
- 如果一个时间段跨越多个日历年,则
MultipleCalendarYears
为true
。 - 如果一个时期与日历年中的某个时期相对应,则
IsCalendarYear
/Halfyear
/Quarter
为true
。
通常,七月或之后开始的财政年度使用下一个日历年的年份。日历属性 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 控制日历周数的计算。YearWeekType
由 ITimeCalendar
支持,因此定义了不同的计算方式。
// ----------------------------------------------------------------------
// 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
广播日历
BroadcastYear
、BroadcastMonth
和 BroadcastWeek
类支持 广播日历。
// ----------------------------------------------------------------------
public void BroadcastCalendar()
{
BroadcastYear year = new BroadcastYear( 2013 );
Console.WriteLine( "Broadcast year: " + year );
// > Broadcast year: 2013; 31.12.2012 - 29.12.2013 | 363.23:59
foreach ( BroadcastMonth month in year.GetMonths() )
{
Console.WriteLine( " Broadcast month: " + month );
foreach ( BroadcastWeek week in month.GetWeeks() )
{
Console.WriteLine( " Broadcast week: " + week );
}
}
} // BroadcastCalendar
时间段计算工具
时间轴
TimeLine
类是关于时间间隔和重叠计算的核心。它通过按照各自时刻的发生顺序对集合中的时间段进行分析。时间轴上的每个时刻都表示为一个 ITimeLineMoment
,并包含哪些时间段在特定时刻开始和结束的信息。这种表示允许在处理时间轴时通过加法和减法来跟踪运行余额。
时间轴上的时刻存储在 ITimeLineMomentCollection
中,该集合允许基于时间时刻进行高效的迭代和索引访问。
两个时间点之间的差值
.NET Framework 的 TimeSpan
结构仅提供天、小时、分钟、秒和毫秒的时间范围值。从用户角度来看,通常也希望表示时间范围的月份和年份。
- 上次访问是 1 年 4 个月 12 天前
- 当前年龄:28岁
时间段 库包含 DateDiff
类,该类计算两个日期值之间的时间差,并提供对已逝时间范围的访问。这正确地考虑了日历周期,以解释不同的月份持续时间。
// ----------------------------------------------------------------------
public void DateDiffSample()
{
DateTime date1 = new DateTime( 2009, 11, 8, 7, 13, 59 );
Console.WriteLine( "Date1: {0}", date1 );
// > Date1: 08.11.2009 07:13:59
DateTime date2 = new DateTime( 2011, 3, 20, 19, 55, 28 );
Console.WriteLine( "Date2: {0}", date2 );
// > Date2: 20.03.2011 19:55:28
DateDiff dateDiff = new DateDiff( date1, date2 );
// differences
Console.WriteLine( "DateDiff.Years: {0}", dateDiff.Years );
// > DateDiff.Years: 1
Console.WriteLine( "DateDiff.Quarters: {0}", dateDiff.Quarters );
// > DateDiff.Quarters: 5
Console.WriteLine( "DateDiff.Months: {0}", dateDiff.Months );
// > DateDiff.Months: 16
Console.WriteLine( "DateDiff.Weeks: {0}", dateDiff.Weeks );
// > DateDiff.Weeks: 70
Console.WriteLine( "DateDiff.Days: {0}", dateDiff.Days );
// > DateDiff.Days: 497
Console.WriteLine( "DateDiff.Weekdays: {0}", dateDiff.Weekdays );
// > DateDiff.Weekdays: 71
Console.WriteLine( "DateDiff.Hours: {0}", dateDiff.Hours );
// > DateDiff.Hours: 11940
Console.WriteLine( "DateDiff.Minutes: {0}", dateDiff.Minutes );
// > DateDiff.Minutes: 716441
Console.WriteLine( "DateDiff.Seconds: {0}", dateDiff.Seconds );
// > DateDiff.Seconds: 42986489
// elapsed
Console.WriteLine( "DateDiff.ElapsedYears: {0}", dateDiff.ElapsedYears );
// > DateDiff.ElapsedYears: 1
Console.WriteLine( "DateDiff.ElapsedMonths: {0}", dateDiff.ElapsedMonths );
// > DateDiff.ElapsedMonths: 4
Console.WriteLine( "DateDiff.ElapsedDays: {0}", dateDiff.ElapsedDays );
// > DateDiff.ElapsedDays: 12
Console.WriteLine( "DateDiff.ElapsedHours: {0}", dateDiff.ElapsedHours );
// > DateDiff.ElapsedHours: 12
Console.WriteLine( "DateDiff.ElapsedMinutes: {0}", dateDiff.ElapsedMinutes );
// > DateDiff.ElapsedMinutes: 41
Console.WriteLine( "DateDiff.ElapsedSeconds: {0}", dateDiff.ElapsedSeconds );
// > DateDiff.ElapsedSeconds: 29
// description
Console.WriteLine( "DateDiff.GetDescription(1): {0}", dateDiff.GetDescription( 1 ) );
// > DateDiff.GetDescription(1): 1 Year
Console.WriteLine( "DateDiff.GetDescription(2): {0}", dateDiff.GetDescription( 2 ) );
// > DateDiff.GetDescription(2): 1 Year 4 Months
Console.WriteLine( "DateDiff.GetDescription(3): {0}", dateDiff.GetDescription( 3 ) );
// > DateDiff.GetDescription(3): 1 Year 4 Months 12 Days
Console.WriteLine( "DateDiff.GetDescription(4): {0}", dateDiff.GetDescription( 4 ) );
// > DateDiff.GetDescription(4): 1 Year 4 Months 12 Days 12 Hours
Console.WriteLine( "DateDiff.GetDescription(5): {0}", dateDiff.GetDescription( 5 ) );
// > DateDiff.GetDescription(5): 1 Year 4 Months 12 Days 12 Hours 41 Mins
Console.WriteLine( "DateDiff.GetDescription(6): {0}", dateDiff.GetDescription( 6 ) );
// > DateDiff.GetDescription(6): 1 Year 4 Months 12 Days 12 Hours 41 Mins 29 Secs
} // DateDiffSample
DateDiff.GetDescription
方法可以格式化具有可变详细程度的时间持续时间。
时间间隔计算
TimeGapCalculator
计算集合中时间段之间的间隔。
时间点的解释可能受 ITimePeriodMapper
应用的影响。
以下示例展示了如何在考虑周末不可用的情况下,找到现有预订之间可能的最大间隔。
// ----------------------------------------------------------------------
public void TimeGapCalculatorSample()
{
// simulation of some reservations
TimePeriodCollection reservations = new TimePeriodCollection();
reservations.Add( new Days( 2011, 3, 7, 2 ) );
reservations.Add( new Days( 2011, 3, 16, 2 ) );
// the overall search range
CalendarTimeRange searchLimits = new CalendarTimeRange(
new DateTime( 2011, 3, 4 ), new DateTime( 2011, 3, 21 ) );
// search the largest free time block
ICalendarTimeRange largestFreeTimeBlock =
FindLargestFreeTimeBlock( reservations, searchLimits );
Console.WriteLine( "Largest free time: " + largestFreeTimeBlock );
// > Largest free time: 09.03.2011 00:00:00 - 11.03.2011 23:59:59 | 2.23:59
} // TimeGapCalculatorSample
// ----------------------------------------------------------------------
public ICalendarTimeRange FindLargestFreeTimeBlock(
IEnumerable<ITimePeriod> reservations,
ITimePeriod searchLimits = null, bool excludeWeekends = true )
{
TimePeriodCollection bookedPeriods = new TimePeriodCollection( reservations );
if ( searchLimits == null )
{
searchLimits = bookedPeriods; // use boundary of reservations
}
if ( excludeWeekends )
{
Week currentWeek = new Week( searchLimits.Start );
Week lastWeek = new Week( searchLimits.End );
do
{
ITimePeriodCollection days = currentWeek.GetDays();
foreach ( Day day in days )
{
if ( !searchLimits.HasInside( day ) )
{
continue; // outside of the search scope
}
if ( day.DayOfWeek == DayOfWeek.Saturday ||
day.DayOfWeek == DayOfWeek.Sunday )
{
bookedPeriods.Add( day ); // // exclude weekend day
}
}
currentWeek = currentWeek.GetNextWeek();
} while ( currentWeek.Start < lastWeek.Start );
}
// calculate the gaps using the time calendar as period mapper
TimeGapCalculator<TimeRange> gapCalculator =
new TimeGapCalculator<TimeRange>( new TimeCalendar() );
ITimePeriodCollection freeTimes =
gapCalculator.GetGaps( bookedPeriods, searchLimits );
if ( freeTimes.Count == 0 )
{
return null;
}
freeTimes.SortByDuration(); // move the largest gap to the start
return new CalendarTimeRange( freeTimes[ 0 ] );
} // FindLargestFreeTimeBlock
时间段的合并
在某些情况下,对重叠或相邻时间段进行合并视图是合理的——例如,与查找间隙相反。TimePeriodCombiner
类提供了合并此类时间段的可能性。
以下示例展示了根据图示组合时间段。
// ----------------------------------------------------------------------
public void TimePeriodCombinerSample()
{
TimePeriodCollection periods = new TimePeriodCollection();
periods.Add( new TimeRange( new DateTime( 2011, 3, 01 ), new DateTime( 2011, 3, 10 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 04 ), new DateTime( 2011, 3, 08 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 15 ), new DateTime( 2011, 3, 18 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 18 ), new DateTime( 2011, 3, 22 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 20 ), new DateTime( 2011, 3, 24 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 26 ), new DateTime( 2011, 3, 30 ) ) );
TimePeriodCombiner<TimeRange> periodCombiner = new TimePeriodCombiner<TimeRange>();
ITimePeriodCollection combinedPeriods = periodCombiner.CombinePeriods( periods );
foreach ( ITimePeriod combinedPeriod in combinedPeriods )
{
Console.WriteLine( "Combined Period: " + combinedPeriod );
}
// > Combined Period: 01.03.2011 - 10.03.2011 | 9.00:00
// > Combined Period: 15.03.2011 - 24.03.2011 | 9.00:00
// > Combined Period: 26.03.2011 - 30.03.2011 | 4.00:00
} // TimePeriodCombinerSample
时间段的交集
如果需要检查时间段是否存在交集(例如,重复预订),TimePeriodIntersector
类就能派上用场。
默认情况下,交集时间段会合并为一个。要保留所有交集时间段,可以将 IntersectPeriods
方法的参数 combinePeriods
设置为 false
。
以下示例展示了 TimePeriodIntersector
的用法
// ----------------------------------------------------------------------
public void TimePeriodIntersectorSample()
{
TimePeriodCollection periods = new TimePeriodCollection();
periods.Add( new TimeRange( new DateTime( 2011, 3, 01 ), new DateTime( 2011, 3, 10 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 05 ), new DateTime( 2011, 3, 15 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 12 ), new DateTime( 2011, 3, 18 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 20 ), new DateTime( 2011, 3, 24 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 22 ), new DateTime( 2011, 3, 28 ) ) );
periods.Add( new TimeRange( new DateTime( 2011, 3, 24 ), new DateTime( 2011, 3, 26 ) ) );
TimePeriodIntersector<TimeRange> periodIntersector =
new TimePeriodIntersector<TimeRange>();
ITimePeriodCollection intersectedPeriods = periodIntersector.IntersectPeriods( periods );
foreach ( ITimePeriod intersectedPeriod in intersectedPeriods )
{
Console.WriteLine( "Intersected Period: " + intersectedPeriod );
}
// > Intersected Period: 05.03.2011 - 10.03.2011 | 5.00:00
// > Intersected Period: 12.03.2011 - 15.03.2011 | 3.00:00
// > Intersected Period: 22.03.2011 - 26.03.2011 | 4.00:00
} // TimePeriodIntersectorSample
时间段的减法
使用 TimePeriodSubtractor
类,您可以从其他时间段(被减数)中减去时间段(减数)。
结果包含两个时间段集合之间的差异
// ----------------------------------------------------------------------
public void TimePeriodSubtractorSample()
{
DateTime moment = new DateTime( 2012, 1, 29 );
TimePeriodCollection sourcePeriods = new TimePeriodCollection
{
new TimeRange( moment.AddHours( 2 ), moment.AddDays( 1 ) )
};
TimePeriodCollection subtractingPeriods = new TimePeriodCollection
{
new TimeRange( moment.AddHours( 6 ), moment.AddHours( 10 ) ),
new TimeRange( moment.AddHours( 12 ), moment.AddHours( 16 ) )
};
TimePeriodSubtractor<timerange> subtractor = new TimePeriodSubtractor<timerange>();
ITimePeriodCollection subtractedPeriods =
subtractor.SubtractPeriods( sourcePeriods, subtractingPeriods );
foreach ( TimeRange subtractedPeriod in subtractedPeriods )
{
Console.WriteLine( "Subtracted Period: {0}", subtractedPeriod );
}
// > Subtracted Period : 29.01.2012 02:00:00 - 06:00:00 | 0.04:00
// > Subtracted Period : 29.01.2012 10:00:00 - 12:00:00 | 0.02:00
// > Subtracted Period : 29.01.2012 16:00:00 - 30.01.2012 00:00:00 | 0.08:00
} // TimePeriodSubtractorSample
日期的加减运算
通常,会遇到将某个时间段添加到给定日期,并从中推导出目标时间点的问题。乍听起来很简单,但往往会因多种因素而复杂化:
- 只应考虑营业时间
- 周末、节假日、服务和维护期应排除在外
一旦存在这样的要求,普通的日期算术必然会失败。在这种情况下,DateAdd
类可能会派上用场。
尽管类的名称可能暗示相反,但可以进行加法和减法运算。DateAdd
的一个特点是它能够使用 DateAdd.IncludePeriods
指定要包含的时间段,并使用 DateAdd.ExcludePeriods
排除某些时间段。也可以只指定其中之一。如果两者都未定义,则该工具的行为等同于 DateTime.Add
和 DateTime.Subtract
。
以下示例展示了 DateAdd
的用法
// ----------------------------------------------------------------------
public void DateAddSample()
{
DateAdd dateAdd = new DateAdd();
dateAdd.IncludePeriods.Add( new TimeRange( new DateTime( 2011, 3, 17 ),
new DateTime( 2011, 4, 20 ) ) );
// setup some periods to exclude
dateAdd.ExcludePeriods.Add( new TimeRange(
new DateTime( 2011, 3, 22 ), new DateTime( 2011, 3, 25 ) ) );
dateAdd.ExcludePeriods.Add( new TimeRange(
new DateTime( 2011, 4, 1 ), new DateTime( 2011, 4, 7 ) ) );
dateAdd.ExcludePeriods.Add( new TimeRange(
new DateTime( 2011, 4, 15 ), new DateTime( 2011, 4, 16 ) ) );
// positive
DateTime dateDiffPositive = new DateTime( 2011, 3, 19 );
DateTime? positive1 = dateAdd.Add( dateDiffPositive, Duration.Hours( 1 ) );
Console.WriteLine( "DateAdd Positive1: {0}", positive1 );
// > DateAdd Positive1: 19.03.2011 01:00:00
DateTime? positive2 = dateAdd.Add( dateDiffPositive, Duration.Days( 4 ) );
Console.WriteLine( "DateAdd Positive2: {0}", positive2 );
// > DateAdd Positive2: 26.03.2011 00:00:00
DateTime? positive3 = dateAdd.Add( dateDiffPositive, Duration.Days( 17 ) );
Console.WriteLine( "DateAdd Positive3: {0}", positive3 );
// > DateAdd Positive3: 14.04.2011 00:00:00
DateTime? positive4 = dateAdd.Add( dateDiffPositive, Duration.Days( 20 ) );
Console.WriteLine( "DateAdd Positive4: {0}", positive4 );
// > DateAdd Positive4: 18.04.2011 00:00:00
// negative
DateTime dateDiffNegative = new DateTime( 2011, 4, 18 );
DateTime? negative1 = dateAdd.Add( dateDiffNegative, Duration.Hours( -1 ) );
Console.WriteLine( "DateAdd Negative1: {0}", negative1 );
// > DateAdd Negative1: 17.04.2011 23:00:00
DateTime? negative2 = dateAdd.Add( dateDiffNegative, Duration.Days( -4 ) );
Console.WriteLine( "DateAdd Negative2: {0}", negative2 );
// > DateAdd Negative2: 13.04.2011 00:00:00
DateTime? negative3 = dateAdd.Add( dateDiffNegative, Duration.Days( -17 ) );
Console.WriteLine( "DateAdd Negative3: {0}", negative3 );
// > DateAdd Negative3: 22.03.2011 00:00:00
DateTime? negative4 = dateAdd.Add( dateDiffNegative, Duration.Days( -20 ) );
Console.WriteLine( "DateAdd Negative4: {0}", negative4 );
// > DateAdd Negative4: 19.03.2011 00:00:00
} // DateAddSample
专门化的 CalendarDateAdd
允许指定加减法使用的工作日和工作时间。
// ----------------------------------------------------------------------
public void CalendarDateAddSample()
{
CalendarDateAdd calendarDateAdd = new CalendarDateAdd();
// weekdays
calendarDateAdd.AddWorkingWeekDays();
// holidays
calendarDateAdd.ExcludePeriods.Add( new Day( 2011, 4, 5, calendarDateAdd.Calendar ) );
// working hours
calendarDateAdd.WorkingHours.Add( new HourRange( new Time( 08, 30 ), new Time( 12 ) ) );
calendarDateAdd.WorkingHours.Add( new HourRange( new Time( 13, 30 ), new Time( 18 ) ) );
DateTime start = new DateTime( 2011, 4, 1, 9, 0, 0 );
TimeSpan offset = new TimeSpan( 22, 0, 0 ); // 22 hours
DateTime? end = calendarDateAdd.Add( start, offset );
Console.WriteLine( "start: {0}", start );
// > start: 01.04.2011 09:00:00
Console.WriteLine( "offset: {0}", offset );
// > offset: 22:00:00
Console.WriteLine( "end: {0}", end );
// > end: 06.04.2011 16:30:00
} // CalendarDateAddSample
搜索日历时间段
CalendarPeriodCollector
提供了在给定时间限制内搜索特定日历时间段的可能性。通过使用 ICalendarPeriodCollectorFilter
,此类搜索可以根据以下标准进行限制:
- 按年份搜索
- 按月份搜索
- 按月份中的天数搜索
- 按工作日搜索
如果没有设置过滤器,则会将一个时间段的所有时间范围视为匹配。组合可以通过以下目标范围进行:
- 年份:
CalendarPeriodCollector.CollectYears
- 月份:
CalendarPeriodCollector.CollectMonths
- 天:
CalendarPeriodCollector.CollectDays
- 小时:
CalendarPeriodCollector.CollectHours
在正常模式下,找到的所有范围的时间范围都将合并。例如,这允许使用 CalendarPeriodCollector.CollectHours
查找一天中的所有小时。
为了进一步限制结果,可以按如下方式定义时间范围:
- 一年中的哪些月份:
ICalendarPeriodCollectorFilter.AddCollectingMonths
- 一个月的哪些天:
ICalendarPeriodCollectorFilter.AddCollectingDays
- 一天中的哪些小时:
ICalendarPeriodCollectorFilter.AddCollectingHours
例如,通过定义 08:00 到 10:00 的小时时间范围,结果将只包含一个覆盖这两个小时的时间段(而不是每个小时一个时间段)。这在组合大型时间范围时被证明是一个有价值(如果不是必要)的优化。
以下示例收集了多年一月份星期五的所有工作时间。
// ----------------------------------------------------------------------
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
环境元素
时间相关定义和基本计算位于各种实用程序类中。
时间规格 | 时间和时间段的常量 |
年份半年度 /年份季度 /年份月份 /年周类型 | 半年、季度、月份和周类型的枚举 |
时间工具 | 日期和时间值以及特定时间段的修改操作 |
时间比较 | 时间段比较函数 |
时间格式化器 | 时间段格式化 |
时间修剪 | 修剪时间段的函数 |
现在 | 计算各种时间段的当前时刻;例如,当前日历季度的开始时间。 |
持续时间 | 特定时间段的计算 |
日期 | 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% 或更多。
- 重构了
TimeLine
、TimeLineMoment
和TimeLineMommentCollection
。
新增对会计(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.Debug 和 Pub\Pcl.Release
- 新增对 .NET 可移植类库 (PCL) 的支持
- 2013年11月10日 - v1.7.1
Date
:修复了Equals()
Date
:将方法GetDateTime()
替换为属性DateTime
Time
:修复了CompareTo()
Time
:基于TimeSpan
持续时间的新构造函数Time
:新增属性IsZero
、IsFullDay
和IsFullDayOrZero
Time
:将ToString()
表示更改为 00:00:00.000,整天显示为 24:00:00.000Date
/Time
:添加了运算符-
、+
、<
、<=
、==
、!=
、>=
>
Date
/Time
:将GetDateTime()
重命名为ToDateTime()
并添加了新的静态变体。
- 2013年11月7日 - v1.7.0
- 库目标框架从 .NET 2.0 更改为 3.5
TimePeriodCollection
:允许小时、分钟、秒和毫秒的最大值。Date
/Time
:实现了接口IComparable
、IComparable<>
、IEquatable<>
ITimePeriodCollection
:新增属性TotalDuration
,包含所有项目持续时间的总和。ITimePeriodCollection
:用于计算特殊持续时间(如夏令时)的新接口。TimeZoneDurationProvider
:计算时区特定持续时间- 基于 CLR 类
TimeZoneInfo
- 通过自定义回调函数处理模棱两可和无效的 DTS 时刻。
- 在缺少回调的情况下抛出
Exceptions <code>AmbigiousMomentException
和InvalidMomentException
。
- 基于 CLR 类
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
- 新增对广播日历的支持:新增类
BroadcastYear
、BroadcastMonth
和BroadcastWeek
。
- 新增对广播日历的支持:新增类
- 2013年9月23日 - v1.4.12.0
TimeTool.GetWeekOfYear
:修复了在YearWeekType
设置为Iso8601
、CalendarWeekRule
设置为FirstFourDayWeek
且周不是从星期一开始时的计算问题。该计算影响Week
和Weeks
类 - 感谢 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
:修复了迭代接口从ITimeRange
到ITimePeriod
的问题。
- 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
- 首次公开发布