Convert Recurrence Rule to Descriptive text

Thread is closed for posting
4 posts, 0 answers
  1. 63752B49-810B-4063-B9EA-8BEF6654660F
    63752B49-810B-4063-B9EA-8BEF6654660F avatar
    49 posts
    Member since:
    Sep 2012

    Posted 15 Sep 2010 Link to this post

    Requirements

    RadControls version

    Version independent
    .NET version

    3.5
    Visual Studio version

    2008
    programming language

    C#
    browser support

    all browsers supported by RadControls


    PROJECT DESCRIPTION
    This started as a question or two posted in forums, but will post code sample here as well as it seems useful. The goal was 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.
    The attached zip file contains a static class (RecRuleParser) that does this.
    Usage: RecRuleParser.ParseRRule(<RecurrenceRule string from DB>, <boolean to include exceptions>);
    Examples:
    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

    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

    It may not be perfect, but it is working quite well for us.

    Thanks,
    Kevin
  2. CBA6844D-D8DA-4579-9783-9FDBD71E261B
    CBA6844D-D8DA-4579-9783-9FDBD71E261B avatar
    103 posts
    Member since:
    May 2011

    Posted 07 Jul 2011 Link to this post

    Thanks very much. I'm using this now. I converted to VB and had to make a couple changes involving the .Split() method to get it to work.

    I found a bug with recurrence when the rule is set to the Last day of week or month, it renders as the -1 day. I am working on a fix. For now, here is the vb:

    Imports System.Collections.Generic
    Imports System.Linq
    Imports System.Text
    Imports System.Globalization
     
    Public Class RecRuleParser
        Private Sub New()
        End Sub
        Public Shared Function ParseRRule(rRule As String, showExceptions As Boolean) As String
            If rRule = "" Then
                Return ""
            End If
            Dim parsed As String = String.Empty
            'set up the return string
            Dim englishStatement As New StringBuilder()
            'Break it into basic parts
            Dim elements As String() = rRule.Split(vbCrLf)
            'it's double spaced in the db, so deal with it accordingly
            Dim startDate As String = elements(0).Trim
            Dim endDate As String = elements(1).Trim
            Dim recRule As String = elements(2).Trim
            Dim recExcs As String = String.Empty
            'check for exceptions
            If elements.Length > 3 Then
                recExcs = elements(3).Trim
            End If
            'Attempt to parse the zulu dates into something else
            Dim dtStart As DateTime = DateTime.ParseExact(getElemValue(startDate), "yyyyMMddTHHmmssZ", System.Globalization.CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal)
            Dim dtEnd As DateTime = DateTime.ParseExact(getElemValue(endDate), "yyyyMMddTHHmmssZ", System.Globalization.CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal)
            Dim tsEnd As TimeSpan = dtEnd.Subtract(dtStart)
            'Now work with the recurrence rule
            Dim rruleElems As New Dictionary(Of String, String)()
            'Convert the string to a dictionary so we can find things easy
            parsed = getElemValue(recRule)
            'no need having unnecessarily declared strings
            elements = parsed.Split(";"c)
            For i As Integer = 0 To elements.Length - 1
                Dim tmp As String() = elements(i).Split("="c)
                rruleElems.Add(tmp(0), tmp(1))
            Next
            englishStatement.Append("Occurs " & rruleElems("FREQ").ToLower())
            Dim calType As String = String.Empty
            'need a scratchpad
            'start translating into English
            Dim timeToAdd As Integer = 0
            Try
                timeToAdd = Convert.ToInt32(rruleElems("COUNT"))
            Catch
                timeToAdd = 0
            End Try
            Dim days As String()
            Select Case rruleElems("FREQ").ToLower()
                Case "daily"
                    days = rruleElems("BYDAY").Split(","c)
                    englishStatement.Append(parseDayNames(days))
                    dtEnd = dtEnd.AddDays(timeToAdd)
                    calType = "days"
                    Exit Select
                Case "weekly"
                    calType = "weeks"
                    dtEnd = dtEnd.AddDays(timeToAdd * 7)
                    Try
                        days = rruleElems("BYDAY").Split(","c)
                        englishStatement.Append(parseDayNames(days))
                    Catch
                        'just in case we missed something on this one
                        Throw New Exception("Error while processing Recurrence Rule")
                    End Try
                    Exit Select
                Case "monthly"
                    calType = "months"
                    dtEnd = dtEnd.AddMonths(timeToAdd)
                    'see if it's positional
                    Try
                        Dim bsp As String = getDayEnding(rruleElems("BYSETPOS"))
                        englishStatement.Append(" on the " & bsp & " " & parseDayNames(rruleElems("BYDAY").Split(","c)).Replace(" every ", ""))
                    Catch
                        'Ok, no BYSETPOS, let's go for BYMONTHDAY
                        Dim bsp As String = getDayEnding(rruleElems("BYMONTHDAY"))
                        englishStatement.Append(" on the " & bsp & " day of each month")
                    End Try
                    Exit Select
                Case "yearly"
                    calType = "years"
                    dtEnd = dtEnd.AddYears(timeToAdd)
                    'looks a lot like monthly....
                    Dim mName As String = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(Convert.ToInt32(rruleElems("BYMONTH")))
                    'see if it's positional
                    Try
                        Dim bsp As String = getDayEnding(rruleElems("BYSETPOS"))
                        englishStatement.Append(" on the " & bsp & " " & parseDayNames(rruleElems("BYDAY").Split(","c)).Replace(" every ", "") & " of " & mName)
                    Catch
                        'Ok, no BYSETPOS, let's go for BYMONTHDAY
                        Dim bsp As String = getDayEnding(rruleElems("BYMONTHDAY"))
                        englishStatement.Append(" on the " & bsp & " day of " & mName)
                    End Try
                    Exit Select
                Case "hourly"
                    calType = "hours"
                    dtEnd = dtEnd.AddHours(timeToAdd)
                    Exit Select
                Case Else
                    Exit Select
     
            End Select
            englishStatement.Append(" starting on " & dtStart.ToLocalTime().ToShortDateString() & " at " & dtStart.ToLocalTime().ToShortTimeString())
     
            If timeToAdd > 0 Then
                englishStatement.Append(" for the next " & rruleElems("COUNT") & " " & calType)
                englishStatement.Append(" ending on " & dtEnd.ToLocalTime().ToShortDateString() & " at " & dtStart.AddHours(tsEnd.Hours).ToLocalTime().ToShortTimeString())
            End If
            If recExcs.Length > 0 AndAlso showExceptions Then
                Dim excs As String() = recExcs.Split(":"c)(1).Split(","c)
                Dim retString As String = String.Empty
                englishStatement.Append(" except on ")
                For r As Integer = 0 To excs.Length - 1
                    'we'll use dtEnd, it's not doing anything now
                    dtEnd = DateTime.ParseExact(excs(r), "yyyyMMddTHHmmssZ", System.Globalization.CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal).ToLocalTime()
                    If r < excs.Length AndAlso excs.Length > 2 Then
                        retString += dtEnd & ","
                    Else
                        If r < excs.Length - 1 AndAlso excs.Length = 2 Then
                            retString += dtEnd & " and "
                        Else
                            retString += dtEnd
                        End If
                    End If
                Next
                englishStatement.Append(retString)
            End If
            Return englishStatement.ToString()
        End Function
        Private Shared Function getElemValue(elem As String) As String
            'just easier than writing split all over the place
            Dim elems As String() = elem.Split(":"c)
            Return elems(1).Trim()
        End Function
        Private Shared Function getDayName(day As String) As String
            'pretty self explanatory
            Select Case 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"
                Case Else
                    Return ""
            End Select
        End Function
        Private Shared Function parseDayNames(days As String()) As String
            Dim retString As String = String.Empty
            If True Then
                If days.Length < 7 Then
                    retString += " every"
                    For d As Integer = 0 To days.Length - 1
                        days(d) = getDayName(days(d))
     
                        If d = days.Length - 1 AndAlso days.Length > 1 Then
                            days(d) = " and " & days(d)
                        Else
                            If days.Length > 2 Then
                                days(d) += ","
                            End If
                        End If
                        retString += " " & days(d)
                    Next
                End If
            End If
            Return retString
        End Function
        Private Shared Function getDayEnding(d As String) As String
            'tried to avoid a big ugly if statement
            'handle the events on the "n"th day of the month
            If d.EndsWith("1") AndAlso d <> "11" Then
                d += "st"
            End If
            If d.EndsWith("2") AndAlso d <> "12" Then
                d += "nd"
            End If
            If d.EndsWith("3") AndAlso d <> "13" Then
                d += "rd"
            End If
            If d.Length < 3 Then
                'hasn't been appended yet
                d += "th"
            End If
            Return d
        End Function
    End Class
    Dan
  3. CC2C2AB8-D774-4B8A-A68C-25F08247665D
    CC2C2AB8-D774-4B8A-A68C-25F08247665D avatar
    4 posts
    Member since:
    Sep 2009

    Posted 16 Apr 2015 in reply to 63752B49-810B-4063-B9EA-8BEF6654660F Link to this post

    Thanks!
  4. AA320E63-07CF-4BB8-8AA8-CF981A309374
    AA320E63-07CF-4BB8-8AA8-CF981A309374 avatar
    100 posts
    Member since:
    Jun 2014

    Posted 22 Nov 2015 Link to this post

    Hi Kevin , thanks for your post.

    I've implemented it in my solution however, it throws some exception which doesn't allow the class to generate the correct statement. For instance; if i don't pick "End After xx occurrence" this rule doesn't send out a "Count" parameter which impacts on the end date and the class doesn't add the info of the end date in the statement. 
    There is another issue which seems a bit strange. The recurrence control creates the year of the end date as 9999. Do you know how to handle this issue?
    Thank you

Back to Top

This Code Library is part of the product documentation and subject to the respective product license agreement.