Search code examples
c#datetimezonedst

Find all timezone offset periods between two timezones


I have code that will give me the offset in seconds between a destination timezone and source timezone, for all periods between two dates.

The following code will do this:

// example start and end date
DateTime startDate = DateTime.SpecifyKind(DateTime.Now.Date, DateTimeKind.Unspecified);
DateTime endDate = startDate.AddYears(10);

// the timezones to use
var sourceZone = TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time");
var destinationZone = TimeZoneInfo.FindSystemTimeZoneById("Mountain Standard Time");

// the periods and timezone offsets
var results = new List<Tuple<DateTime, DateTime, double>>();

// loop variables
DateTime currentDate = startDate;
DateTime periodStartDate = currentDate;

// the current period offset (in seconds)
double? currentOffset = null;

while (currentDate < endDate)
{
    DateTime destTime;

    // if the current time is invalid for the source timezone
    // then advance time until it is not invalid
    while (sourceZone.IsInvalidTime(currentDate))
        currentDate = currentDate.AddMinutes(30d);

    destTime = TimeZoneInfo.ConvertTime(currentDate, sourceZone, destinationZone);

    // calculate the offset for this iteration
    double iterationOffset = destTime.Subtract(currentDate).TotalSeconds;

    if (currentOffset == null)
        // the current offset is null so use the iteration offset
        currentOffset = iterationOffset;
    else if (iterationOffset != currentOffset.Value)
    {
        // the current offset doesn't equal the iteration offset
        // that means that a period has been identified

        // add the period to the list
        results.Add(Tuple.Create(periodStartDate, currentDate, currentOffset.Value));

        // the start of the period is the new current date
        periodStartDate = currentDate;

        // the current offset becomes the iteration offset
        currentOffset = iterationOffset;
    }

    // increment the day by 30 minutes
    currentDate = currentDate.AddMinutes(30d);
}

foreach (var item in results)
    Console.WriteLine("{0}\t{1}\t{2}", item.Item1, item.Item2, item.Item3);

Results:

╔═══════════════════════╦═══════════════════════╦════════╗
║      PeriodStart      ║       PeriodEnd       ║ Offset ║
╠═══════════════════════╬═══════════════════════╬════════╣
║ 7/13/2015 12:00:00 AM ║ 10/25/2015 1:00:00 AM ║ -25200 ║
║ 10/25/2015 1:00:00 AM ║ 11/1/2015 8:00:00 AM  ║ -21600 ║
║ 11/1/2015 8:00:00 AM  ║ 3/13/2016 9:00:00 AM  ║ -25200 ║
║ 3/13/2016 9:00:00 AM  ║ 3/27/2016 2:00:00 AM  ║ -21600 ║
║ 3/27/2016 2:00:00 AM  ║ 10/30/2016 1:00:00 AM ║ -25200 ║
║ 10/30/2016 1:00:00 AM ║ 11/6/2016 8:00:00 AM  ║ -21600 ║
║ 11/6/2016 8:00:00 AM  ║ 3/12/2017 9:00:00 AM  ║ -25200 ║
║ 3/12/2017 9:00:00 AM  ║ 3/26/2017 2:00:00 AM  ║ -21600 ║
║ 3/26/2017 2:00:00 AM  ║ 10/29/2017 1:00:00 AM ║ -25200 ║
║ 10/29/2017 1:00:00 AM ║ 11/5/2017 8:00:00 AM  ║ -21600 ║
║ 11/5/2017 8:00:00 AM  ║ 3/11/2018 9:00:00 AM  ║ -25200 ║
║          ...          ║          ...          ║   ...  ║
╚═══════════════════════╩═══════════════════════╩════════╝

Now while this seems to work, I know that it is not the most efficient way to do it. I'm basically stuck on converting this to a method that doesn't have to use hard-coded time increments to find all these periods where a change in offset occurs.

Any help would be appreciated.


Solution

  • Here is a solution that uses Noda Time:

    using System;
    using System.Linq;
    using NodaTime;
    
    ...
    
    // Get some time zones.  You can use Tzdb or Bcl zones here.
    DateTimeZone sourceZone = DateTimeZoneProviders.Bcl["GMT Standard Time"]; // London
    DateTimeZone destinationZone = DateTimeZoneProviders.Bcl["Mountain Standard Time"]; // Denver
    
    // Determine the period of time we're interested in evaluating.
    // I'm taking today in the source time zone, up to 10 years in the future.
    Instant now = SystemClock.Instance.Now;
    Instant start = sourceZone.AtStartOfDay(now.InZone(sourceZone).Date).ToInstant();
    Instant end = start.InZone(sourceZone).LocalDateTime.PlusYears(10).InZoneLeniently(sourceZone).ToInstant();
    
    // Get the intervals for our each of the zones over these periods
    var sourceIntervals = sourceZone.GetZoneIntervals(start, end);
    var destinationIntervals = destinationZone.GetZoneIntervals(start, end);
    
    // Find all of the instants we care about, including the start and end points,
    // and all transitions from either zone in between
    var instants = sourceIntervals.Union(destinationIntervals)
        .SelectMany(x => new[] {x.Start, x.End})
        .Union(new[] {start, end})
        .OrderBy(x => x).Distinct()
        .Where(x => x >= start && x < end)
        .ToArray();
    
    // Loop through the instants
    for (int i = 0; i < instants.Length -1; i++)
    {
        // Get this instant and the next one
        Instant instant1 = instants[i];
        Instant instant2 = instants[i + 1];
    
        // convert each instant to the source zone
        ZonedDateTime zdt1 = instant1.InZone(sourceZone);
        ZonedDateTime zdt2 = instant2.InZone(sourceZone);
    
        // Get the offsets for instant1 in each zone 
        Offset sourceOffset = zdt1.Offset;
        Offset destOffset = destinationZone.GetUtcOffset(instant1);
    
        // Calc the difference between the offsets
        int deltaSeconds = (destOffset.Milliseconds - sourceOffset.Milliseconds)/1000;
    
        // Convert to the same types you had in your example (optional)
        DateTime dt1 = zdt1.ToDateTimeUnspecified();
        DateTime dt2 = zdt2.ToDateTimeUnspecified();
    
        // emit output
        Console.WriteLine("{0}\t{1}\t{2}", dt1, dt2, deltaSeconds);
    }
    

    Note that the output is identical to your own, except for the first date. Yours gave 7/13/2015 when I ran it, and mine gave 7/14/2015. This is because your code has a bug, in that your starting date is not based on today in the source time zone, but instead it starts with today in the local time zone.

    Also, I assumed you wanted all output to be in terms of the source time zone, since that's what your example gave.

    Additionally, you may want to consider that the output isn't particularly clear with regard to the transitions in the source zone. During a fall-back transition you can't tell which of the two times the output represents, and during a spring-forward transition it's not clear where the gap actually is. A DateTimeOffset output would be much clearer in this regard.