Click here to Skip to main content
14,333,745 members

Date and Time Source Interface and its Implementations

Rate this:
5.00 (4 votes)
Please Sign up or sign in to vote.
5.00 (4 votes)
10 Dec 2016CPOL
Date and time source interface allows to use different actual date and time sources (e.g. current system time or accelerated system time) in your C# code to simplify debugging, simulation, etc.

Introduction

Our code deals with date and time quite often. For example, we might need to do some work at specified dates and times (say at 10:00 AM every Monday); or some operation must be repeated every 5 minutes. .NET platform provides useful types for this including System.DateTime, System.TimeSpan, System.Diagnostics.Stopwatch, as well as different timer classes. But there are cases where we need functionality which is not available in .NET "out of the box".

Consider our application does some operation by schedule every day (or every hour - it doesn't matter). If we need to debug the application, then we definitely would not like to wait until the operation time comes "naturally". Several options exist which may help in this situation. If the application logic is controlled by some editable settings (e.g., stored in a configuration file), then special debug settings could be created; or we may change current system time to some moment "before event". A different option is described here: enable date and time "virtualization" by using a date and time source (or say provider) interface. The article presents my vision of such interface. Two implementations of the interface are suggested as well. One implementation provides system date and time; it's an alternative to the direct use of .NET types (DateTime, Stopwatch). Another implementation allows to specify a time acceleration (deceleration) factor so the time effectively goes faster or slower compared to the system time for the code which uses it. There is also an option to specify an arbitrary date and time value as a starting point. This feature can be used to reproduce time-dependent results when debugging or developing some simulation code.

Using the Code

The Downloadable Archive Contents

The archive contains Visual Studio 2013 solution DateTimeSource. The date and time source interface IDateTimeSource and two of its implementations SystemDateTimeSource and SimulatedDateTimeSource are defined in one source file DateTimeSource.cs. The solution contains two test projects as well: UnitTests console test program and TestSimDateTimeGui WinForms application.

IDateTimeSource Interface

Getting the Current Date and Time

The interface provides a method to get the current (in terms of the class which implements the interface) UTC date and time as a DateTime value. This value may be equal or not equal to the "real" date and time (i.e., current system time) depending on the semantics of the interface implementation. Use DateTime.ToLocalTime method to convert the value returned to local time.

public interface IDateTimeSource
{
    DateTime GetUtcNow();

This GetUtcNow method shall be used instead of DateTime.Now or DateTime.UtcNow in all code that depends on the current date and time if you want to get benefits from using a customizable date and time source, for example:

void DoSomethingAt10AM(IDateTimeSource dtSrc)
{
    TimeSpan localTime = dtSrc.GetUtcNow().ToLocalTime().TimeOfDay;
    if (localTime.Hours == 10)
    {
        // do some operation
    }
}

Using ticks-related Methods

The interface provides several methods to work with "ticks" which can be used to measure time intervals. Ticks represent date and time in arbitrary integer units. The exact number of ticks per second (i.e., the frequency in Hertz) depends on the interface implementation. It can be obtained with the help of GetFrequency method.

public interface IDateTimeSource
{
    long GetTicks();

    long GetFrequency();

The tick-related methods can be used to repeat some actions with a specified time interval. Consider we need to implement a function which returns a current measurement from some sensor (e.g., a thermometer). Let's assume that getting these measurements is a relatively time-consuming operation; at the same time, we have a priori knowledge that the measured parameter cannot change too fast. In this case, we may improve performance by caching the last received value and returning it to a caller until the value is obsolete:

readonly TimeSpan minCheckInterval = TimeSpan.FromMilliseconds(500);

long lastCheckTicks = -1;

double lastValue;

double GetMeasurement(IDateTimeSource dtSrc, MySensor sensor)
{
    if ((lastCheckTicks < 0) || (dtSrc.TimeElapsedFrom(lastCheckTicks) >= minCheckInterval))
    {
       // we may need to save current ticks before or after calling to GetValue()
       // depending on the cache logic we want to implement
       lastCheckTicks = dtSrc.GetTicks();

       // a time-consuming operation
       lastValue = sensor.GetValue();
    }
    return lastValue;
}

The function can be made simpler with the help of IsTimeElapsedFrom method:

double GetMeasurement(IDateTimeSource dtSrc, MySensor sensor)
{
    if (dtSrc.IsTimeElapsedFrom(ref lastCheckTicks, minCheckInterval))
    {
        lastValue = sensor.GetValue();
    }
    return lastValue;
}

There is a method which converts ticks to DateTime:

public interface IDateTimeSource
{
    DateTime GetDateTimeUtc(long ticks);

After running this code fragment...

DateTime dt1 = dtSrc.GetUtcNow(); // [1]
long ticks = dtSrc.GetTicks(); // [2]
DateTime dt2 = dtSrc.GetDateTimeUtc(ticks); // [3]

...we may expect that dt2 is almost equal to dt1 normally, i.e., in case the code execution was not interrupted for a considerable amount of time (which is always possible in multitasking environment) and the system time hasn't been corrected after executing line [1] but before [2] or [3].

SystemDateTimeSource

The class is IDateTimeSource implementation based on the current system time.

Its GetUtcNow method simply returns DateTime.UtcNow.

Its GetTicks and other ticks-related methods are based on Stopwatch:

public interface IDateTimeSource
{
    public long GetTicks()
    {
        return Stopwatch.GetTimestamp();
    }

There is no need to create multiple instances of SystemDateTimeSource. Static class SystemDateTime implements the singleton pattern for it:

DateTime utcNow = SystemDateTime.Instance.GetUtcNow();

SimulatedDateTimeSource

This IDateTimeSource implementation demonstrates the real power of date and time virtualization. The class allows to accelerate/decelerate its virtual "clock" comparing to the system time and also to specify an arbitrary start (initial) date and time.

Using Time Acceleration

The class constructors allow to specify the time acceleration factor and optionally the start date and time:

public class SimulatedDateTimeSource : StopwatchDateTimeSource, IDateTimeSource
{
    public SimulatedDateTimeSource(double timeAcceleration, DateTime? start = null)

Consider the following code fragment:

var dtSrc = new SimulatedDateTimeSource(2.0); // time acceleration factor is 2
DateTime dt1 = dtSrc.GetUtcNow();
DateTime dt2 = SystemDateTime.Instance.GetUtcNow();
TimeSpan diff = dt1 - dt2; // [1]
Console.WriteLine(diff);
Thread.Sleep(1000);
dt1 = dtSrc.GetUtcNow();
dt2 = SystemDateTime.Instance.GetUtcNow();
diff = dt1 - dt2; // [2]
Console.WriteLine(diff);

After line [1], the value of diff will be close to TimeSpan.Zero normally (i.e., dt1 and dt2 almost equal) with the same reservations as previously (code execution not interrupted, system time not corrected).

After line [2], the value of diff is expected to be about +1 second because the "clock" inside this simulated date/time source instance runs twice as fast as the real system time.

Setting Start Date and Time

We may specify an arbitrary start date and time for our date/time source as well:

var startDt = new DateTime(1900, 1, 1, 12, 0, 0, DateTimeKind.Utc);
var dtSrc = new SimulatedDateTimeSource(1.0, startDt);
Console.WriteLine(dtSrc.GetUtcNow().ToString(CultureInfo.InvariantCulture));
Thread.Sleep(1000);
Console.WriteLine(dtSrc.GetUtcNow().ToString(CultureInfo.InvariantCulture));

The expected output:

01/01/1900 12:00:00
01/01/1900 12:00:01

There is a second constructor which allows to specify another IDateTimeSource as the base for this SimulatedDateTimeSource. It makes it possible to create "chained" date/time sources.

public class SimulatedDateTimeSource : StopwatchDateTimeSource, IDateTimeSource
{
    public SimulatedDateTimeSource
      (IDateTimeSource source, double timeAcceleration, DateTime? start = null)

Limitations

Date/time Resolution Decreases If Using Acceleration

Current SimulatedDateTimeSource implementation uses SystemDateTimeSource as the base date/time source by default; in its turn SystemDateTimeSource returns DateTime.UtcNow in its GetUtcNow. The resolution of DateTime.UtcNow (DateTime.Now) is not great. I executed this code on my system to find it:

DateTime dt2;
int count = 0;
var dt1 = DateTime.UtcNow;
do
{
    dt2 = DateTime.UtcNow;
    ++count;
}
while (dt1 == dt2);
Console.WriteLine("Count: {0}. Diff: {1}", count, (dt2 - dt1));

The typical results observed:

Count: 128419. Diff: 00:00:00.0050000
Count: 136151. Diff: 00:00:00.0050000
Count: 137519. Diff: 00:00:00.0050001

The date/time resolution found is about 5 milliseconds.

Here is the GetUtcNow implementation from SimulatedDateTimeSource:

public DateTime GetUtcNow()
{
    TimeSpan diff = source.GetUtcNow() - startDateTime;
    double ticks = (double)startDateTime.Ticks + correction.Ticks + (diff.Ticks * acceleration);
    if (ticks >= DateTime.MaxValue.Ticks)
    {
        return DateTime.MaxValue;
    }
    if (ticks <= DateTime.MinValue.Ticks)
    {
        return DateTime.MinValue;
    }
    return new DateTime((long)ticks, DateTimeKind.Utc);
}

You see the time difference ticks are multiplied by acceleration which deteriorates the resolution. I ran the modified test code:

var dtSrc = new SimulatedDateTimeSource(3.0);
DateTime dt2;
int count = 0;
var dt1 = dtSrc.GetUtcNow();
do
{
    dt2 = dtSrc.GetUtcNow();
    ++count;
}
while (dt1 == dt2);
Console.WriteLine("Count: {0}. Diff: {1}", count, (dt2 - dt1));

The typical results:

Count: 52274. Diff: 00:00:00.0150016
Count: 46914. Diff: 00:00:00.0149888
Count: 46679. Diff: 00:00:00.0150016

Now the date/time resolution is about 15 milliseconds (3 times worse, proportionally to acceleration).

Time Acceleration Factor Limitations

The current SimulatedDateTimeSource implementation does not accept time acceleration factors if the resulting frequency will not fit to the range [1..long.MaxValue]. Below is a part of SimulatedDateTimeSource.Reset method which is used by both SimulatedDateTimeSource constructors and also may be called anytime after constructing an instance of the class to change the simulation parameters:

public void Reset(IDateTimeSource source, double timeAcceleration, DateTime? start = null)
{
    double f = source.GetFrequency() * timeAcceleration;
    if ((f > long.MaxValue) || (f < 1))
    {
        throw new ArgumentOutOfRangeException("timeAcceleration");
    }

Another important fact is every time a SystemDateTimeSource instance converts ticks to TimeSpan (this happens in GetTimeSpan, TimeElapsedFrom, IsTimeElapsedFrom, GetDateTimeUtc methods), it clips the result to the range [TimeSpan.MinValue..TimeSpan.MaxValue]. It's not of much importance when using SystemDateTimeSource by itself but shall be taken into account if using SimulatedDateTimeSource (based on SystemDateTimeSource by default) with big time acceleration factors: in this case TimeSpan.MaxValue value can be practically reached.

Points of Interest

Working with date and time and measuring time intervals is not as easy as it seems. Many factors complicate it: hardware limitations (e.g., system real-time clock drift), CPU power saving modes, multitasking effects, multiprocessor/multicore effects, non-trivial system time synchronization algorithms. There are interesting articles at CodeProject that cover these topics, for example, A Tale of Two Timers.

History

  • December 12, 2016: First revision

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Alexey Chernobaev
Software Developer
Russian Federation Russian Federation
C# / C++ developer. Create applications mostly for Windows, sometimes for Linux. Used Object Pascal / Delphi in the good old times.

Comments and Discussions

 
-- There are no messages in this forum --
Article
Posted 10 Dec 2016

Stats

6.5K views
3 downloads
5 bookmarked