Converting UTC date time to a time in the future, considering daylight savings time of a time zone

We have a system that represents weeks with UTC times representing begin & end date times from the America/Chicago time zone. Weeks start at midnight on Saturday morning central time and end at 23:59:59 on Friday evening central time, so their UTC entries in the database are:

Week 1 - begin: 2015-10-24 05:00:00, end 2015-10-31 04:59:59
Week 2 - begin: 2015-10-31 05:00:00, end 2015-11-07 05:59:59
Week 3 - begin: 2015-11-07 06:00:00, end 2015-11-14 05:59:59
Week 4 - begin: 2015-11-14 06:00:00, end 2015-11-21 05:59:59
Week 5 - begin: 2015-11-21 06:00:00, end 2015-11-28 05:59:59

So from the above examples of the weeks, you can see the time change from daylight to standard time reflect between 10/31 & 11/7.

I am needing to return N weeks from a given week. Our systems are C# Azure worker & web roles, and run in the Azure cloud (all compute nodes are UTC). My logic is to, take the starting week, and add N weeks work of days to the start date/time of the week and ask for weeks that have a start date greater than the original start date, and less than or equal to the calculated future date.

var weeks = repository.Fetch(x => x.BeginDate <= nWeeksAheadUtc && x.BeginDate > week.BeginDate)l=;

This works except when a change for daylight savings occurs within the resulting answer. Because of the time change, asking for the next 3 weeks from Week 1 based on adding 21 days to Week 1's start date only results in Week 2 & 3 being returned, because the calculated future value is 2015-11-14 05:00:00, which excludes Week 4.

I have solved the problem using Nodatime in the following manner:

LocalDateTime localDateTime = LocalDateTime.FromDateTime(week.BeginDate);
ZonedDateTime zonedDateTime = localDateTime.InZoneStrictly(DateTimeZoneProviders.Tzdb["UTC"]);
zonedDateTime = zonedDateTime.WithZone(DateTimeZoneProviders.Tzdb["America/Chicago"]);
DateTime centralDateTime = zonedDateTime.ToDateTimeUnspecified();
DateTime futureDateTime = centralDateTime.Add(TimeSpan.FromDays(weekCount*7));
localDateTime = LocalDateTime.FromDateTime(futureDateTime);
zonedDateTime = localDateTime.InZoneStrictly(DateTimeZoneProviders.Tzdb["America/Chicago"]);
DateTime nWeeksAheadUtc = zonedDateTime.ToDateTimeUtc();

var weeks = repository.Fetch(x => x.BeginDate <= nWeeksAheadUtc && x.BeginDate > week.BeginDate).OrderBy(x => x.RetailerWeekNumber).ToList();

While it functions, it seems cumbersome and not very intuitive to developers that would follow me in maintaining this code. Is there a cleaner way to do this via the Nodatime API or (base C# date/time) that I am missing?

Adding in the requested example - I just created a UnitTest project for this and these three classes:

Week.cs

using System;

namespace NodaTimeTest
{
    public class Week
    {
        public int Id { get; set; }
        public DateTime BeginDate { get; set; }
        public DateTime EndDate { get; set; }
    }
}

WeekService.cs

using NodaTime;
using System;
using System.Collections.Generic;
using System.Linq;

namespace NodaTimeTest
{
    public class WeekService
    {
        private readonly List<Week> repository;

        public WeekService()
        {
            this.repository = this.InitWeeks();
        }

        public List<Week> GetNextWeeks(int weekId, int weekCount)
        {
            Week week = this.repository.First(x => x.Id == weekId);

            // the meat - how to do this the right way?
            LocalDateTime localDateTime = LocalDateTime.FromDateTime(week.BeginDate);
            ZonedDateTime zonedDateTime = localDateTime.InZoneStrictly(DateTimeZoneProviders.Tzdb["UTC"]);
            zonedDateTime = zonedDateTime.WithZone(DateTimeZoneProviders.Tzdb["America/Chicago"]);
            DateTime centralDateTime = zonedDateTime.ToDateTimeUnspecified();
            DateTime futureDateTime = centralDateTime.Add(TimeSpan.FromDays(weekCount * 7));
            localDateTime = LocalDateTime.FromDateTime(futureDateTime);
            zonedDateTime = localDateTime.InZoneStrictly(DateTimeZoneProviders.Tzdb["America/Chicago"]);
            DateTime nWeeksAheadUtc = zonedDateTime.ToDateTimeUtc();

            var weeks = repository.Where(x => x.BeginDate <= nWeeksAheadUtc && x.BeginDate > week.BeginDate).OrderBy(x => x.Id).ToList();

            return weeks;
        }

        private List<Week> InitWeeks()
        {
            // sets up our list of 10 example dates in UTC encompassing America/Chicago daylight savings time change on 11/1
            // this means that all weeks are 168 hours long, except week "4", which is 169 hours long.
            var weeks = new List<Week>();
            DateTime beginDate = new DateTime(2015, 10, 10, 5, 0, 0, DateTimeKind.Utc);

            for (int i = 1; i <= 10; i++)
            {
                DateTime endDate = beginDate.AddDays(7).AddSeconds(-1);

                if (endDate.Date == new DateTime(2015, 11, 7, 0, 0, 0, DateTimeKind.Utc))
                {
                    endDate = endDate.AddHours(1);
                }

                weeks.Add(new Week { Id = i, BeginDate = beginDate, EndDate = endDate });

                beginDate = endDate.AddSeconds(1);
            }

            return weeks;
        }
    }
}

WeekServiceTest:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;

namespace NodaTimeTest
{
    [TestClass]
    public class WeekServiceTest
    {
        private readonly WeekService weekService = new WeekService();

        [TestMethod]
        public void TestGetNextThreeWeeksOverDaylightTimeChange()
        {
            var result = this.weekService.GetNextWeeks(2, 3);

            Assert.AreEqual(3, result.ElementAt(0).Id);
            Assert.AreEqual(4, result.ElementAt(1).Id);
            Assert.AreEqual(5, result.ElementAt(2).Id);
        }

        [TestMethod]
        public void TestGetNextThreeWeeksWithNoDaylightTimeChange()
        {
            var result = this.weekService.GetNextWeeks(5, 3);

            Assert.AreEqual(6, result.ElementAt(0).Id);
            Assert.AreEqual(7, result.ElementAt(1).Id);
            Assert.AreEqual(8, result.ElementAt(2).Id);
        }
    }
}
Jon Skeet
people
quotationmark

Okay, if I've understood you correctly, I think you probably want something like:

var zone = DateTimeZoneProviders.Tzdb["America/Chicago"];
var instantStart = Instant.FromDateTimeUtc(week.BeginDate);
var chicagoStart = instantStart.InZone(zone);
var localEnd = chicagoStart.LocalDateTime.PlusWeeks(weekCount);
var chicagoEnd = localEnd.InZoneLeniently(zone);
var bclEnd = chicagoEnd.ToDateTimeUtc();

var result = repository
    .Fetch(x => x.BeginDate >= week.BeginDate && x.BeginDate < bclEnd)
    .OrderBy(x => x.RetailerWeekNumber)
    .ToList();

Note that I've made the lower bound inclusive and the upper bound exclusive - that's typically the simplest way to do things.

You could of course chain a lot of this together if you really wanted to:

var zone = DateTimeZoneProviders.Tzdb["America/Chicago"];
var bclEnd = Instant.FromDateTimeUtc(week.BeginDate)
    .InZone(zone)
    .LocalDateTime
    .PlusWeeks(weekCount)
    .InZoneLeniently(zone)
    .ToDateTimeUtc();

EDIT: The above is if your BeginDate is really the instant from which you want to start getting data. It sounds like in reality, you want to add a week to that to start with. At that point it would be:

var zone = DateTimeZoneProviders.Tzdb["America/Chicago"];
var instantNow = Instant.FromDateTimeUtc(week.BeginDate);
var chicagoNow = instantStart.InZone(zone);
var localStart = chicagoNow.LocalDateTime.PlusWeeks(1);
var localEnd = localEnd(weekCount);
var bclStart = localStart.InZoneLeniently(zone).ToDateTimeUtc();
var bclEnd = localEnd.InZoneLeniently(zone).ToDateTimeUtc();

var result = repository
    .Fetch(x => x.BeginDate >= bclStart && x.BeginDate < bclEnd)
    .OrderBy(x => x.RetailerWeekNumber)
    .ToList();

people

See more on this question at Stackoverflow