displaying calendar items closest to today using datetime
Question: I have a dictionary of my calendar items for a month (_date_ as "key", _items_
in the form of a list as "value") that I want to print out a certain way (That
dictionary in included in the code, assigned to `dct`). I only want to display
items that are **on** or **after** the current date (i.e. today). The display
format is:

    
    
    day: item1, item2
    

I also want those items to span only 5 lines of stdout with each line 49
characters wide (spaces included). This is necessary because the output will
be displayed in conky (app for linux).

Since a day can have multiple agenda items, the output will have to be wrapped
and printed out on more than one line. I want the code to account for that by
selecting only those days whose items can fit in 5 or less lines instead of
printing 5 days with associated items on >5 lines. For e.g.

    
    
    day1: item1, item2
          item3
    day2: item1
    day3: item1,
          item2
    

Thats 3 days on/after current day printing on 5 lines with each line 49 char
wide. Strings exceeding 49 char are wrapped on newline.

Here is the code i've written to do this:

    
    
    #!/usr/bin/env python
    
    from datetime import date, timedelta, datetime
    import heapq
    import re
    import textwrap
    
    pattern_string = '(1[012]|[1-9]):[0-5][0-9](\\s)?(?i)(am|pm)'
    pattern = re.compile(pattern_string)
    
    # Explanation of pattern_string:
    # ------------------------------
    #(              #start of group #1
     #1[012]                #  start with 10, 11, 12
     #|             #  or
     #[1-9]             #  start with 1,2,...9
    #)              #end of group #1
     #:             #   follow by a semi colon (:)
      #[0-5][0-9]           #     follw by 0..5 and 0..9, which means 00 to 59
                #(\\s)?     #       follow by a white space (optional)
                      #(?i)     #         next checking is case insensitive
                          #(am|pm)  #           follow by am or pm
    # The 12-hour clock format is start from 0-12, then a semi colon (:) and follow by 00-59 , and end with am or pm.
    # Time format that match:
    # 1. "1:00am", "1:00 am","1:00 AM" ,
    # 2. "1:00pm", "1:00 pm", "1:00 PM",
    # 3. "12:50 pm"
    
    d = date.today() # datetime.date(2013, 8, 11)
    e = datetime.today() # datetime.datetime(2013, 8, 11, 5, 56, 28, 702926)
    today = d.strftime('%a %b %d') # 'Sun Aug 11'
    
    dct = {
    'Thu Aug 01' : [' Weigh In'], 
    'Thu Aug 08' : [' 8:00am', 'Serum uric acid test', '12:00pm', 'Make Cheesecake'], 
    'Sun Aug 11' : [" Awais chotu's birthday", ' Car wash'], 
    'Mon Aug 12' : ['10:00am', 'Start car for 10 minutes'], 
    'Thu Aug 15' : [" Hooray! You're Facebook Free!", '10:00am', 'Start car for 10 minutes'], 
    'Mon Aug 19' : ['10:00am', 'Start car for 10 minutes'], 
    'Thu Aug 22' : ['10:00am', 'Start car for 10 minutes'], 
    'Mon Aug 26' : ['10:00am', 'Start car for 10 minutes'], 
    'Thu Aug 29' : ['10:00am', 'Start car for 10 minutes']
    }
    
    def join_time(lst):
        '''Searches for a time format string in supplied list and concatenates it + the event next to it as an single item
           to a list and returns that list'''
        mod_lst = []
        for number, item in enumerate(lst):
            if re.search(pattern, item):
                mod_lst.append(item + ' ' + lst[number+1]) # append the item (i.e time e.g '1:00am') and the item next to it (i.e. event)
                del lst[number+1]
            else:
                mod_lst.append(item)
        return mod_lst
    
    def parse_date(datestring):
        return datetime.strptime(datestring + ' ' + str(date.today().year), "%a %b %d %Y") # returns a datetime obj for the time string; "Sun Aug 11" = datetime.datetime(1900, 8, 11, 0, 0)
    
    deltas = [] # holds datetime.timedelta() objs; timedelta(days, seconds, microseconds)
    val_len = []
    key_len = {}
    
    for key in dct:
        num = len(''.join(item for item in dct[key]))
        val_len.append(num) # calculate the combined len of all items in the 
                            # list which are the val of a key and add them to val_len
        if num > 37:
            key_len[key] = 2
        else:
            key_len[key] = 1
    
    # val_len = [31, 9, 61, 31, 31, 49, 31, 32, 31]
    # key_len = {'Sun Aug 11': 1, 'Mon Aug 12': 1, 'Thu Aug 01': 1, 'Thu Aug 15': 2, 'Thu Aug 22': 1, 'Mon Aug 19': 1, 'Thu Aug 08': 2, 'Mon Aug 26': 1, 'Thu Aug 29': 1}
    
    counter = 0
    for eachLen in val_len:
        if eachLen > 37:
            counter = counter + 2
        else:
            counter = counter + 1
    
    # counter = 11
    
    if counter > 5: # because we want only those 5 events in our conky output which are closest to today
        n = counter - 5 # n = 6, these no of event lines should be skipped
    
        for key in dct:
            deltas.append(e - parse_date(key)) # today - key date (e.g. 'Sun Aug 11') ---> datetime.datetime(2013, 8, 11, 5, 56, 28, 702926) - datetime.datetime(1900, 8, 11, 0, 0)
    
        # TODO: 'n' no of event lines should be skipped, NOT n no of days!  
        for key in sorted(dct, key=parse_date): # sorted() returns ['Thu Aug 01', 'Thu Aug 08', 'Sun Aug 11', 'Mon Aug 12', 'Thu Aug 15', 'Mon Aug 19', 'Thu Aug 22', 'Mon Aug 26', 'Thu Aug 29']
            tdelta = e - parse_date(key)
            if tdelta in heapq.nlargest(n, deltas): # heapq.nlargest(x, iterable[, key]); returns list of 'x' no. of largest items in iterable
                pass                                # In this case it should return a list of top 6 largest timedeltas; if the tdelta is in 
                                                    # that list, it means its not amongst the 5 events we want to print
            else:
                if key == today:
                    value = dct[key]
                    val1 = '${color green}' + key + '$color: ' 
                    mod_val = join_time(value) 
                    val2 = textwrap.wrap(', '.join(item for item in mod_val), 37)
                    print val1 + '${color 40E0D0}' + '$color\n          ${color 40E0D0}'.join(item for item in val2) + '$color'
                else:
                    value = dct[key]
                    mod_val = join_time(value)
                    output = key + ': ' + ', '.join(item for item in mod_val)
                    print '\n           '.join(textwrap.wrap(output, 49))
    
    else:   
        for key in sorted(dct, key=parse_date):
            if key == today:
                value = dct[key]
                val1 = '${color green}' + key + '$color: ' 
                mod_val = join_time(value) 
                val2 = textwrap.wrap(', '.join(item for item in mod_val), 37)
                print val1 + '${color 40E0D0}' + '$color\n          ${color 40E0D0}'.join(item for item in val2) + '$color'
            else:
                value = dct[key]
                mod_val = join_time(value)
                output = key + ': ' + ', '.join(item for item in mod_val)
                print '\n           '.join(textwrap.wrap(output, 49))
    

The result is:

    
    
    Thu Aug 22: 10:00am Start car for 10 minutes
    Mon Aug 26: 10:00am Start car for 10 minutes
    Thu Aug 29: 10:00am Start car for 10 minutes
    

I've commented the code heavily so it shouldn't be difficult to figure out how
it works. I'm basically calculating the days farthest away from current day
using datetime and skipping those days and their items. The code usually works
well but once in a while it doesn't. In this case the output should have been:

    
    
    Mon Aug 19: 10:00am Start car for 10 minutes
    Thu Aug 22: 10:00am Start car for 10 minutes
    Mon Aug 26: 10:00am Start car for 10 minutes
    Thu Aug 29: 10:00am Start car for 10 minutes
    

since these are the days after the current day (Fri 16 Aug) whose items fit in
5 lines. _How do I fix it to skip`n` no of lines rather than no of days
farthest away from today?_

I was thinking of using `key_len` dict to somehow filter the output further,
by printing the items of only **those** days whose items length sum up to < or
= 5...

I'm stuck.


Answer: It's very hard to tell what you're asking here, and your code is a huge
muddle.

However, the reason you're getting the wrong output in the given example is
very obvious, and matches the `TODO` comment in your code, so I'm going to
assume that's the only part you're asking about:

    
    
     # TODO: 'n' no of event lines should be skipped, NOT n no of days!
    

I don't understand why you want to skip to the _last_ 5 lines after today
instead of the first 5, but I'll assume you have some good reason for that.

The easiest way to solve this is to just do them in reverse, prepend the lines
to a string instead of `print`ing them directly, stop when you've reached 5
lines, and then print the string. (This would also save the wasteful re-
building of the heap over and over, etc.)

For example, something like this:

    
    
    outlines = []
    for key in sorted(dct, key=parse_date, reverse=True): # sorted() returns ['Thu Aug 01', 'Thu Aug 08', 'Sun Aug 11', 'Mon Aug 12', 'Thu Aug 15', 'Mon Aug 19', 'Thu Aug 22', 'Mon Aug 26', 'Thu Aug 29']
        if parse_date(key) < parse_date(today):
            break
        tdelta = e - parse_date(key)
        if key == today:
            value = dct[key]
            val1 = '${color green}' + key + '$color: ' 
            mod_val = join_time(value) 
            val2 = textwrap.wrap(', '.join(item for item in mod_val), 37)
            outstr = val1 + '${color 40E0D0}' + '$color\n          ${color 40E0D0}'.join(item for item in val2) + '$color'
            outlines[:0] = outstr.splitlines()
        else:
            value = dct[key]
            mod_val = join_time(value)
            output = key + ': ' + ', '.join(item for item in mod_val)
            outstr = '\n           '.join(textwrap.wrap(output, 49))
            outlines[:0] = outstr.splitlines()
        if len(outlines) >= 5:
            break
    print '\n'.join(outlines)
    

There are a lot of ways you could simplify this. For example, instead of
passing around string representations of dates and using `parse_date` all over
the place, just pass around dates, and format them once at the end. Use string
formatting instead of 120-character multiple-concatenation expressions. Build
your data structures once and use them, instead of rebuilding them over and
over where you need them. And so on. But this should be all you need to get it
to work.

