13

I would like to write (or use if it already exits) a function in C# that returns the date/time of the next DST transition given a System.TimeZoneInfo object and a particular "as of" time in that time zone. The time returned should be in the provided time zone. The function I want has this signature:

public DateTime GetNextTransition(DateTime asOfTime, TimeZoneInfo timeZone)
{
    // Implement me!
}

For example, if I pass in the "Eastern Standard Time" TimeZoneInfo object, and 1/21/2011@17:00 as the "asOfTime", I expect this function to return 3/13/2011@2:00.

The System.TimeZoneInfo.TransitionTime structure appears to have all the information I need, but ideally there would be some sort of built-in mechanism to convert the rule into an actual date. Anyone have any suggestions?

Stuart Lange
  • 4,049
  • 6
  • 24
  • 30
  • DST is a quite the animal...keep in mind places such as the majority of Arizona (some Indian reservations honor DST) do not honor DST. Our timezone remains MST and thus we do not adjust our time...whereas say Denver would transition to MDT and adjust their time. – Aaron McIver Jan 21 '11 at 23:09
  • 1
    Use Noda Time and see [this thread](http://stackoverflow.com/questions/24373618/getting-daylight-savings-time-start-and-end-in-nodatime) on how to get transition times using it. – Azimuth Nov 08 '16 at 07:40

3 Answers3

8

Hello_ there. It might be too late but I will post here the code that I used for this purpose. This could possibly safe someone's time to implement it. I did it actually with the help of the link is @Jamiegs answer.

    public static DateTime? GetNextTransition(DateTime asOfTime, TimeZoneInfo timeZone)
    {
        TimeZoneInfo.AdjustmentRule[] adjustments = timeZone.GetAdjustmentRules();
        if (adjustments.Length == 0)
        {
            // if no adjustment then no transition date exists
            return null;
        }

        int year = asOfTime.Year;
        TimeZoneInfo.AdjustmentRule adjustment = null;
        foreach (TimeZoneInfo.AdjustmentRule adj in adjustments)
        {
            // Determine if this adjustment rule covers year desired
            if (adj.DateStart.Year <= year && adj.DateEnd.Year >= year)
            {
                adjustment = adj;
                break;
            }
        }

        if (adjustment == null)
        {
            // no adjustment found so no transition date exists in the range
            return null;
        }


        DateTime dtAdjustmentStart = GetAdjustmentDate(adjustment.DaylightTransitionStart, year);
        DateTime dtAdjustmentEnd = GetAdjustmentDate(adjustment.DaylightTransitionEnd, year);


        if (dtAdjustmentStart >= asOfTime)
        {
            // if adjusment start date is greater than asOfTime date then this should be the next transition date
            return dtAdjustmentStart;
        }
        else if (dtAdjustmentEnd >= asOfTime)
        {
            // otherwise adjustment end date should be the next transition date
            return dtAdjustmentEnd;
        }
        else
        {
            // then it should be the next year's DaylightTransitionStart

            year++;
            foreach (TimeZoneInfo.AdjustmentRule adj in adjustments)
            {
                // Determine if this adjustment rule covers year desired
                if (adj.DateStart.Year <= year && adj.DateEnd.Year >= year)
                {
                    adjustment = adj;
                    break;
                }
            }

            dtAdjustmentStart = GetAdjustmentDate(adjustment.DaylightTransitionStart, year);
            return dtAdjustmentStart;
        }
    }


    public static DateTime GetAdjustmentDate(TimeZoneInfo.TransitionTime transitionTime, int year)
    {
        if (transitionTime.IsFixedDateRule)
        {
            return new DateTime(year, transitionTime.Month, transitionTime.Day);
        }
        else
        {
            // For non-fixed date rules, get local calendar
            Calendar cal = CultureInfo.CurrentCulture.Calendar;
            // Get first day of week for transition
            // For example, the 3rd week starts no earlier than the 15th of the month
            int startOfWeek = transitionTime.Week * 7 - 6;
            // What day of the week does the month start on?
            int firstDayOfWeek = (int)cal.GetDayOfWeek(new DateTime(year, transitionTime.Month, 1));
            // Determine how much start date has to be adjusted
            int transitionDay;
            int changeDayOfWeek = (int)transitionTime.DayOfWeek;

            if (firstDayOfWeek <= changeDayOfWeek)
                transitionDay = startOfWeek + (changeDayOfWeek - firstDayOfWeek);
            else
                transitionDay = startOfWeek + (7 - firstDayOfWeek + changeDayOfWeek);

            // Adjust for months with no fifth week
            if (transitionDay > cal.GetDaysInMonth(year, transitionTime.Month))
                transitionDay -= 7;

            return new DateTime(year, transitionTime.Month, transitionDay, transitionTime.TimeOfDay.Hour, transitionTime.TimeOfDay.Minute, transitionTime.TimeOfDay.Second);
        }
    }

Sample usage will look like this:

// This should give you DateTime object for date 26 March 2017 
// because this date is first transition date after 1 January 2017 for Central Europe Standard Time zone
DateTime nextTransitionDate = GetNextTransition(new DateTime(2017, 1, 1), TimeZoneInfo.FindSystemTimeZoneById("Central Europe Standard Time"))

You can find the code that I played with here.

codtex
  • 6,128
  • 2
  • 17
  • 34
0

System.TimeZoneInfo.TransitionTime looks like a structure that can hold such time transition data, not a function that figures out the actual values. To create such a function, I would find the data online somewhere, then create values using the static CreateFloatingDateRule or CreateFixedDateRule methods.

Patrick Szalapski
  • 8,738
  • 11
  • 67
  • 129
0

Thanks to @codtex for your answer. Unfortunately it only works in the northern hemisphere. In the southern hemisphere daylight savings usually starts somewhere around October. That means dtAdjustmentEnd would need to be checked first rather than dtAdjustmentStart.

So instead of creating another if statement to handle this, and to simplify the code that handles the next year's transition, I updated the code to get the transitions for the given year and the next, sort them and take the first one that is after the asOfTime.

public static DateTime? GetNextTransition(DateTime asOfTime, TimeZoneInfo timeZone)
{
    TimeZoneInfo.AdjustmentRule[] adjustments = timeZone.GetAdjustmentRules();
    if (adjustments.Length == 0)
    {
        // if no adjustment then no transition date exists
        return null;
    }

    int year = asOfTime.Year;
    TimeZoneInfo.AdjustmentRule adjustment = null;
    foreach (TimeZoneInfo.AdjustmentRule adj in adjustments)
    {
        // Determine if this adjustment rule covers year desired
        if (adj.DateStart.Year <= year && adj.DateEnd.Year >= year)
        {
            adjustment = adj;
            break;
        }
    }

    if (adjustment == null)
    {
        // no adjustment found so no transition date exists in the range
        return null;
    }


    // Calculate the adjustments for the asOfTime year and the next.
    // Sort them because DST start for any given year can be after DST end.
    // Take the first one on or after the asOfTime.
    var adjustmentTimes = new List<DateTime>()
    {
        GetAdjustmentDate(adjustment.DaylightTransitionStart, year),
        GetAdjustmentDate(adjustment.DaylightTransitionEnd, year),
        GetAdjustmentDate(adjustment.DaylightTransitionStart, year + 1),
        GetAdjustmentDate(adjustment.DaylightTransitionEnd, year + 1),
    };
    adjustmentTimes.Sort();

    return adjustmentTimes.First(at => at > asOfTime);
}

public static DateTime GetAdjustmentDate(TimeZoneInfo.TransitionTime transitionTime, int year)
{
    if (transitionTime.IsFixedDateRule)
    {
        return new DateTime(year, transitionTime.Month, transitionTime.Day);
    }
    else
    {
        // For non-fixed date rules, get local calendar
        Calendar cal = CultureInfo.CurrentCulture.Calendar;
        // Get first day of week for transition
        // For example, the 3rd week starts no earlier than the 15th of the month
        int startOfWeek = transitionTime.Week * 7 - 6;
        // What day of the week does the month start on?
        int firstDayOfWeek = (int)cal.GetDayOfWeek(new DateTime(year, transitionTime.Month, 1));
        // Determine how much start date has to be adjusted
        int transitionDay;
        int changeDayOfWeek = (int)transitionTime.DayOfWeek;

        if (firstDayOfWeek <= changeDayOfWeek)
            transitionDay = startOfWeek + (changeDayOfWeek - firstDayOfWeek);
        else
            transitionDay = startOfWeek + (7 - firstDayOfWeek + changeDayOfWeek);

        // Adjust for months with no fifth week
        if (transitionDay > cal.GetDaysInMonth(year, transitionTime.Month))
            transitionDay -= 7;

        return new DateTime(year, transitionTime.Month, transitionDay, transitionTime.TimeOfDay.Hour, transitionTime.TimeOfDay.Minute, transitionTime.TimeOfDay.Second);
    }
}

This would be less efficient, but unless it's going to be called thousands of times a second, nobody is going to notice.

stritch000
  • 355
  • 4
  • 10
  • I haven't checked whether this code will actually work or not - but in my time implementing BCL-based time zones in Noda Time, I found there are *lots* of weird corner cases in the way that `TimeZoneInfo` works. I'd personally suggest (and I'm biased, of course) that using Noda Time would be the simplest approach here, if at all possible. – Jon Skeet Jun 27 '23 at 06:26
  • @JonSkeet Fair call, bias aside. – stritch000 Jun 27 '23 at 07:48