New to Telerik UI for ASP.NET AJAX? Download free 30-day trial

Convert Recurrence Rule to Descriptive text

Environment

Product Telerik WebForms Scheduler for ASP.NET AJAX

Description

This started as a question in forums, but I will post code sample here as it seems useful.

The goal is to take a RecurrenceRule string from the database for a given appointment and convert that into a plain English statement that could be used in an email for the appointment.

Solution

The shared sample code contains a static class (RecRuleParser) that does this. Usage: RecRuleParser.ParseRRule(<RecurrenceRule string from DB>, <boolean to include exceptions>)

Example 1:

RRULE: DTSTART:20100527T040000Z
DTEND: 20100528T040000Z
RRULE: FREQ=WEEKLY;INTERVAL=1;BYDAY=TH
EXDATE: 20100527T040000Z,20100813T040000Z
RESULT: Occurs weekly every Thursday starting on 5/27/2010 at 12:00 AM except on 5/27/2010 12:00:00 AM and 8/13/2010 12:00:00 AM


Example 2:

RRULE: DTSTART:20100914T180000Z
DTEND: 20100914T190000Z
RRULE: FREQ=MONTHLY;INTERVAL=1;BYSETPOS=4;BYDAY=SA,SU
RESULT: Occurs monthly on the 4th Saturday and Sunday starting on 9/14/2010 at 2:00 PM


Source code:

public static class RecRuleParser
{
    public static string ParseRRule(string rRule, bool showExceptions)
    {
        string parsed = string.Empty;
        StringBuilder englishStatement = new StringBuilder();

        // Break the input string into basic parts
        string[] elements = rRule.Split(' ');
        string startDate = elements[0];
        string endDate = elements[2];
        string recRule = elements[4];
        string recExcs = string.Empty;

        // Check for exceptions
        if (elements.Length > 5)
        {
            recExcs = elements[6];
        }

        // Attempt to parse the Zulu dates into DateTime objects
        DateTime dtStart = DateTime.ParseExact(GetElemValue(startDate), "yyyyMMddTHHmmssZ", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal);
        DateTime dtEnd = DateTime.ParseExact(GetElemValue(endDate), "yyyyMMddTHHmmssZ", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal);
        TimeSpan tsEnd = dtEnd.Subtract(dtStart);

        // Now work with the recurrence rule
        Dictionary<string, string> rruleElems = new Dictionary<string, string>();
        parsed = GetElemValue(recRule); // Get parsed recurrence rule
        elements = parsed.Split(';');

        // Convert the string to a dictionary for easy access
        for (int i = 0; i < elements.Length; i++)
        {
            string[] tmp = elements[i].Split('=');
            rruleElems.Add(tmp[0], tmp[1]);
        }

        // Construct English statement based on recurrence rule
        englishStatement.Append("Occurs " + rruleElems["FREQ"].ToLower());
        string calType = string.Empty;
        int timeToAdd = 0;

        // Attempt to convert COUNT to integer
        try
        {
            timeToAdd = Convert.ToInt32(rruleElems["COUNT"]);
        }
        catch
        {
            timeToAdd = 0;
        }

        switch (rruleElems["FREQ"].ToLower())
        {
            case "daily":
                // Handle daily recurrence
                string[] days = rruleElems["BYDAY"].Split(',');
                englishStatement.Append(ParseDayNames(days));
                dtEnd = dtEnd.AddDays(timeToAdd);
                calType = "days";
                break;
            case "weekly":
                // Handle weekly recurrence
                calType = "weeks";
                dtEnd = dtEnd.AddDays(timeToAdd * 7);
                try
                {
                    days = rruleElems["BYDAY"].Split(',');
                    englishStatement.Append(ParseDayNames(days));
                }
                catch
                {
                    throw new Exception("Error while processing Recurrence Rule");
                }
                break;
            case "monthly":
                // Handle monthly recurrence
                calType = "months";
                dtEnd = dtEnd.AddMonths(timeToAdd);
                try
                {
                    string bsp = GetDayEnding(rruleElems["BYSETPOS"]);
                    englishStatement.Append(" on the " + bsp + " " + ParseDayNames(rruleElems["BYDAY"].Split(',')).Replace(" every ", ""));
                }
                catch
                {
                    string bsp = GetDayEnding(rruleElems["BYMONTHDAY"]);
                    englishStatement.Append(" on the " + bsp + " day of each month");
                }
                break;
            case "yearly":
                // Handle yearly recurrence
                calType = "years";
                dtEnd = dtEnd.AddYears(timeToAdd);
                string mName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(Convert.ToInt32(rruleElems["BYMONTH"]));
                try
                {
                    string bsp = GetDayEnding(rruleElems["BYSETPOS"]);
                    englishStatement.Append(" on the " + bsp + " " + ParseDayNames(rruleElems["BYDAY"].Split(',')).Replace(" every ", "") + " of " + mName);
                }
                catch
                {
                    string bsp = GetDayEnding(rruleElems["BYMONTHDAY"]);
                    englishStatement.Append(" on the " + bsp + " day of " + mName);
                }
                break;
            case "hourly":
                // Handle hourly recurrence
                calType = "hours";
                dtEnd = dtEnd.AddHours(timeToAdd);
                break;
            default:
                break;
        }

        // Add start time to the English statement
        englishStatement.Append(" starting on " + dtStart.ToLocalTime().ToShortDateString() + " at " + dtStart.ToLocalTime().ToShortTimeString());

        // Add end time to the English statement if COUNT is specified
        if (timeToAdd > 0)
        {
            englishStatement.Append(" for the next " + rruleElems["COUNT"] + " " + calType);
            englishStatement.Append(" ending on " + dtEnd.ToLocalTime().ToShortDateString() + " at " + dtStart.AddHours(tsEnd.Hours).ToLocalTime().ToShortTimeString());
        }

        // Add exception dates to the English statement if present and requested
        if (recExcs.Length > 0 && showExceptions)
        {
            string[] excs = recExcs.Split(':')[1].Split(',');
            string retString = string.Empty;
            englishStatement.Append(" except on ");
            for (int r = 0; r < excs.Length; r++)
            {
                dtEnd = DateTime.ParseExact(excs[r], "yyyyMMddTHHmmssZ", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal).ToLocalTime();
                if (r < excs.Length && excs.Length > 2)
                {
                    retString += dtEnd + ",";
                }
                else
                {
                    if (r < excs.Length - 1 && excs.Length == 2)
                    {
                        retString += dtEnd + " and ";
                    }
                    else
                    {
                        retString += dtEnd;
                    }
                }
            }
            englishStatement.Append(retString);
        }

        return englishStatement.ToString();
    }

    private static string GetElemValue(string elem)
    {
        // Helper function to extract value from element
        string[] elems = elem.Split(':');
        return elems[1].Trim();
    }

    private static string GetDayName(string day)
    {
        // Helper function to get day name
        switch (day)
        {
            case "MO":
                return "Monday";
            case "TU":
                return "Tuesday";
            case "WE":
                return "Wednesday";
            case "TH":
                return "Thursday";
            case "FR":
                return "Friday";
            case "SA":
                return "Saturday";
            case "SU":
                return "Sunday";
            default:
                return "";
        }
    }

    private static string ParseDayNames(string[] days)
    {
        // Helper function to parse day names
        string retString = string.Empty;
        if (days.Length < 7)
        {
            retString += " every";
            for (int d = 0; d < days.Length; d++)
            {
                days[d] = GetDayName(days[d]);
                if (d == days.Length - 1 && days.Length > 1)
                {
                    days[d] = " and " + days[d];
                }
                else
                {
                    if (days.Length > 2)
                    {
                        days[d] += ",";
                    }
                }
                retString += " " + days[d];
            }
        }
        return retString;
    }

    private static string GetDayEnding(string d)
    {
        // Helper function to handle the ordinal suffix for day numbers
        if (d.EndsWith("1") && d != "11")
        {
            d += "st";
        }
        if (d.EndsWith("2") && d != "12")
        {
            d += "nd";
        }
        if (d.EndsWith("3") && d != "13")
        {
            d += "rd";
        }
        if (d.Length < 3)
        {
            d += "th";
        }
        return d;
    }
}
In this article