Search code examples
c#datetimetimezonedst

Convert DST rule to DateTime in C#


I have DST rules like this one:

"2,-1,1,3600000"

  • the 2 is the zero based month
  • The -1 is the week number containing the day, -1 means the last week in the month which contains the day
  • The 1 is the day of the week, 1 - Sunday through to 7 - Saturday
  • The 3600000 is the mS from midnight on the appointed day that the change to/from DST will take place and is expressed in local time including DST, so the end of DST switch time is in DST.

what's the proper way to transform it in a C# DateTime?

So far I've done this:

public static DateTime ConvertDstRule(int year, string rule, bool isEndRule)
{
    const int DaysInWeek = 7;
    var ruleName = isEndRule ? "endRule" : "startRule";

    var startStrings = rule.Split(',');
    var month = Convert.ToInt32(startStrings[0]);
    if ((month < 0) || (month > 11))
    {
        throw new ArgumentOutOfRangeException(ruleName, "The month value must be between 0 and 11");
    }

    var week = Convert.ToInt32(startStrings[1]);
    if ((week < -1) || (week > 5))
    {
        throw new ArgumentOutOfRangeException(ruleName, "The week value must be between -1 and 5");
    }

    if ((Convert.ToInt32(startStrings[2]) < 1) || (Convert.ToInt32(startStrings[2]) > 7))
    {
        throw new ArgumentOutOfRangeException(ruleName, "The day value must be between 1 and 7");
    }

    var day = (DayOfWeek)(Convert.ToInt32(startStrings[2]) - 1); // DayOfWeek is zero based so shift by one.
    var timeOffset = Convert.ToInt64(startStrings[3]);
    if ((timeOffset / 1000 / 60) > 86400)
    {
        throw new ArgumentOutOfRangeException(ruleName, "The time offset is limited to one day");
    }

    // Find the start of the relevant year.
    var startTime = new DateTime(year, 1, 1);

    // Add on the month to get to the start of the selected month.
    startTime = startTime.AddMonths(month);

    // If the week is negative then go to the first occurance of the day in
    // the next month, adding a negative week number will jump back into
    // the previous month.
    if (week < 0)
    {
        startTime = startTime.AddMonths(1);
    }
    else
    {
        week = week - 1;
    }

    // Jump to the first occurence of the day to switch in that month.
    var monthStartsOn = startTime.DayOfWeek;
    var daysToSwitchDay = (int)day - (int)monthStartsOn;

    // This is likely to be negative as most zones switch on a Sunday
    if (daysToSwitchDay < 0)
    {
        daysToSwitchDay = DaysInWeek + daysToSwitchDay; // daysToSwitchDay is negative so add it.
    }

    startTime = startTime.AddDays(daysToSwitchDay); // Now on the correct day.

    startTime = startTime.AddDays(week * 7); // Week counts from 1.

    startTime = startTime.AddMilliseconds(timeOffset);
    if (isEndRule)
    {
        startTime = startTime.AddHours(-1); // Take off the DST hour to convert it to UTC.
    }

    return startTime;
}

Does it takes into account half-hour DST changes like in India? Can you spot any bug in this code?


Solution

  • A few things:

    • The type of input you are describing is called a "Transition Rule". Or outside the context of time zones, it is a particular type of "Recurrence Rule". It simply describes a pattern for determining when a particular point in time occurs for a given year, based on month, week, and weekday.

    • A transition rule does not tell you everything you need to know to calculate values for time zones. In particular, it doesn't tell you what the offset from UTC is before or after the transition, or the direction that the offset is being adjusted. Also, you would need a set of these, as there is usually two of these per year, but there could also be any number of them. For example, in 2014, Russia had only one transition, and Egypt had four. Also consider that these rules have changed over time, so a single rule, or even a single pair of rules, will not tell you how to convert values for all points in time. For a given time zone, you need a set-of-sets-of-rules, which is what we mean when we say "a time zone".

    • In the .NET Framework, the TimeZoneInfo class is used for working with time zones. It contains subclasses, TimeZoneInfo.AdjustmentRule and TimeZoneInfo.TransitionTime. In particular, there's an internal function in TimeZoneInfo called TransitionTimeToDateTime which does exactly what you are asking. It takes TransitionTime and a year, and gives you the DateTime that the transition occurs within the year. You can find this in the .NET Reference Source here.

    However, if you really think you want to try to implement time zone rules from your own data sources, you should be thinking about several things:

    1. Do you really think you can do better than the hundreds of people that have come before you? Maybe so, but don't go into this with a "this is simple" attitude. I suggest you watch this short video, and do a LOT of research before attempting this.

    2. Are you thinking about how you will keep things maintained? Time zone rules change often. Annually, there can be about a dozen changes worldwide. Are you planning to monitor news feeds, discussion forums, government press releases, and other sources? Are you prepared to act when a government doesn't give that much warning that it's DST rules are going to change?

    3. How much impact will it have on your system when things aren't accurate? It could range from not much (ex: a blog or forum) to critical (ex: airline schedules, communications, medical, financial, etc.).

    Additionally:

    • The suggestion of using Noda Time from the question's comments is a good one, but unfortunately Noda Time doesn't have any specific API for interpreting just a single transition rule. Instead, it's prefered that you use existing TZDB zones, such as "America/New_York". Alternatively, you can use the TimeZoneInfo class in the .NET Framework, with IDs like "Eastern Standard Time".

    • Internally, Noda Time handles transition rules through the ZoneYearOffset class. You can see how a TransitionRule maps to a ZoneYearOffset in this code.

    • In your code sample, the comment "Take off the DST hour to convert it to UTC." is highly misleading. The output of your function is in terms of local time. UTC has nothing to do with it. Nowhere else in the code you showed here are you tracking offsets from UTC, either with or without DST. I think you meant "... convert it to standard time", but I'm not sure why you would want to do that. This particular function shouldn't try to account for that.

    • You also asked: "Does it takes into account half-hour DST changes like in India?" The question is invalid, because India doesn't have DST. Its standard time zone offset has a half-hour in it (UTC+05:30), but there's no DST. The only place in the world that currently uses a half-hour DST bias is Lord Howe Island, which is represented by the "Australia/Lord_Howe" time zone in the tzdb, and currently has no Windows equivalent. Everywhere else in the world (except a few research stations in Antarctica), if DST is used the transition bias is one hour.