Recurring monthly events in Python

I was working on something in one of my little Django sites and wondered how you make a recurring monthly event in Python? What I mean by recurring event is "every fourth Saturday" or "every first and second Wednesday" and so on.

I did not want to make a dependency on some huge calender server module like Calcore or Twisted's caldav. All I wanted was a function that accepts "every fourth Saturday" and returns me an actual date that I can use for scheduling things.

A quick google didn't come up with anything, so I decided to do it myself. Here are my first and second attempts. The first attempt just works it out mathematically, the second attempt uses a module from the Python standard library.

"""Helper for recurring date."""

DAYS = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
]

from datetime import date

def eventdate(year, month, target_day, target_ordinal):
    """Convert a human event date to a real date.
    For example, 'the third Thursday of the month'
    the target_ordinal is 3 and the target_day is 'Thursday'.
    """

    day = DAYS.index(target_day.title())

    match = 0
    for i in range(1, 32):
        try:
            if date(year, month, i).weekday() == day:
                match += 1
                if match == target_ordinal:
                    return date(year, month, i)

        except ValueError:
            return None

def main():
    """Example when called directly."""
    today = date.today()
    if today.month == 12:
        year = today.year + 1
        month = 1
    else:
        year = today.year
        month = today.month + 1
    print "Next Month's Linux group is", eventdate(year, month, 'Thursday', 3)
    print "Next Month's Python group is", eventdate(year, month, 'Saturday', 4)

# start the ball rolling
if __name__ == "__main__":
    main()

That worked quite fine, but like all good Python programmers, I want to be as efficient (/lazy) as possible, surely the standard library can do this for me? Well I found that the calendar module will return a matrix of dates organised by week and day. This works as follows:

"""Event helpers."""

def eventdate(year, month, target_day, target_ordinal):
    """Convert a human event date to a real date.
    For example, 'the third Thursday of the month'
    the target_ordinal is 3 and the target_day is 'Thursday'.
    """

    import calendar
    day = getattr(calendar, target_day.upper())
    cal = calendar.Calendar()
    return cal.monthdatescalendar(year, month)[target_ordinal - 1][day]

def main():
    """Example when called directly."""
    from datetime import date
    today = date.today()
    if today.month == 12:
        year = today.year + 1
        month = 1
    else:
        year = today.year
        month = today.month + 1
    print "Next Month's Linux group is", eventdate(year, month, 'Thursday', 3)
    print "Next Month's Python group is", eventdate(year, month, 'Saturday', 4)

# start the ball rolling

if __name__ == "__main__":
    main()

This seems to work identically as the above but in less lines of code. I still get the feeling I am trying too hard and I am missing something obvious, but maybe I am just being too much of perfectionist (as always).

If anyone knows or can work out a more efficient method, please do let me know.

4 thoughts on “Recurring monthly events in Python

  1. <p>You should look at <a class="reference external" href="http://labix.org/python-dateutil">http://labix.org/python-dateutil</a>, it does this and much more.</p>

  2. <div class="highlight"><pre><span class="n">DAYS</span> <span class="o">=</span> <span class="p">[</span><span class="s">&#39;Sunday&#39;</span><span class="p">,</span> <span class="s">&#39;Monday&#39;</span><span class="p">,</span> <span class="s">&#39;Tuesday&#39;</span><span class="p">,</span> <span class="s">&#39;Wednesday&#39;</span><span class="p">,</span> <span class="s">&#39;Thursday&#39;</span><span class="p">,</span> <span class="s">&#39;Friday&#39;</span><span class="p">,</span>
    <span class="s">&#39;Saturday&#39;</span><span class="p">]</span>

    <span class="k">def</span> <span class="nf">eventdate</span><span class="p">(</span><span class="n">year</span><span class="p">,</span> <span class="n">month</span><span class="p">,</span> <span class="n">target_day</span><span class="p">,</span> <span class="n">target_ordinal</span><span class="p">):</span>
    <span class="k">import</span> <span class="nn">sqlite3</span>
    <span class="n">db</span> <span class="o">=</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="s">&#39;:memory:&#39;</span><span class="p">)</span>
    <span class="n">query</span> <span class="o">=</span> <span class="p">(</span><span class="s">&quot;select date(&#39;</span><span class="si">%d</span><span class="s">-01-01&#39;, &#39;</span><span class="si">%d</span><span class="s"> months&#39;, &#39;</span><span class="si">%d</span><span class="s"> days&#39;, &#39;weekday </span><span class="si">%d</span><span class="s">&#39;);&quot;</span>
    <span class="o">%</span> <span class="p">(</span><span class="n">year</span><span class="p">,</span> <span class="n">month</span><span class="o">-</span><span class="mf">1</span><span class="p">,</span> <span class="mf">7</span><span class="o">*</span><span class="p">(</span><span class="n">target_ordinal</span><span class="o">-</span><span class="mf">1</span><span class="p">),</span> <span class="n">DAYS</span><span class="o">.</span><span class="n">index</span><span class="p">(</span><span class="n">target_day</span><span class="p">)))</span>
    <span class="k">return</span> <span class="n">db</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">query</span><span class="p">)</span><span class="o">.</span><span class="n">fetchone</span><span class="p">()[</span><span class="mf">0</span><span class="p">]</span>
    </pre></div>

  3. <p>If you put the following code before returning the value you can handle things like &quot;the last Saturday&quot; by using a negative target_ordinal. Also, it checks to see if the date that is being returned is a zero indicating that that day doesn't come until the next (or came in the previous) week:</p>
    <blockquote>
    <dl class="docutils">
    <dt>if target_ordinal &gt; 0 and weeks[target_ordinal - 1][day] == 0:</dt>
    <dd>target_ordinal+=1</dd>
    <dt>elif target_ordinal &lt; 0 and weeks[target_ordinal][day] == 0:</dt>
    <dd>target_ordinal-=1</dd>
    <dt>if target_ordinal &lt; 0:</dt>
    <dd>target_ordinal += len(weeks) + 1</dd>
    </dl>
    </blockquote>

How about Global Thermonuclear War? Wouldn't you prefer a good game of chess? Powered by zpress