Search code examples
c#countdownnodatime

Noda Time Countdown


I have written an application to show the remaining time until the next big industry trade-show.
(it's about two years in the future at the time of writing)

I started off using the standard DateTime class but quickly ran into issues dealing with the varying number of days in each month, 2016 is a Leap Year and contains a Leap Day, Daylight Savings Time, etc.

Thankfully I discovered NodaTime. (Thanks @JonSkeet)

Not so thankfully, the way I am used to working with DateTime doesn't apply and I'm having a really hard time figuring out how to get the time remaining. (There aren't many examples floating around)

For example, the following code doesn't work because you can't subtract an instant from a LocalDateTime:

void example()
{
    DateTime DT = Convert.ToDateTime("09/12/2016 10:00AM");

    LocalDateTime NodaLocalDateTime = new LocalDateTime(
        DT.Year, DT.Month, DT.Day, DT.Hour, DT.Minute, 0);

        Period P = NodaLocalDateTime - SystemClock.Instance.Now;
}

So the question becomes:

How do you get the remaining time from now until some date?


Solution

  • To determine a "calendrical" amount of time between two events, you want Period as you've already discovered. However, that only deals with local dates and times.

    To determine a "calendar-neutral" amount of time between two events, you can use Instant and Duration - but then you can't display the number of months left.

    Both of these approaches have drawbacks, but basically they're fundamental to the way that time works. If you use the local time approach, then you'll find that the amount of time will jump back or forward an hour as you go over a DST transition. If you use the instant approach, you're restricted to days/months/hours/minutes etc - not months.

    One option for between the two would be to use LocalDateTime and Period, but anchor both the event and the current time in UTC. That way there'll never be a discontinuity, as UTC is an unchanging base line, effectively. This also means that you'll always display the same amount of "time left" regardless of where in the world you look at the counter (or host the code, depending on exactly what you were planning to do).

    If you want more details about why you can't get a Period between two ZonedDateTime values, I could think of examples which are fundamentally problematic. The bottom line is that calendrical arithmetic and time zones don't play nicely together though...

    Just to give some actual code, I would have something like:

    public sealed class EventCountdown
    {
         private readonly LocalDateTime eventTimeUtc;
         private readonly IClock clock;
    
         // It's probably most convenient to express the event time with the time zone
         // in which it occurs. You could easily change this though.
         public EventCountdown(ZonedDateTime zonedEventTime, IClock clock)
         {
             this.eventTimeUtc = zonedEventTime.WithZone(DateTimeZone.Utc).LocalDateTime;
             this.clock = clock;
         }
    
         public Period GetPeriodRemaining()
         {
             return Period.Between(clock.Now.InUtc().LocalDateTime, eventTimeUtc);
         }
    }
    

    Note that in Noda Time 2.0 the IClock.Now property is being changed to a GetCurrentInstant method... but in that case you'd probably use a ZonedClock in UTC and call GetCurrentLocalDateTime on it.