I like the fact the Olympics are coming to London and will be trying to see a few events.

But the ticketing process was and is frustrating. “For your convenience” they only accept one type of card (maybe it is convenient, maybe it drops the price of every ticket by £5, but using weasel words to gloss over an apparent inconvenience sticks in the craw). The lack of seating plans (how much do I need to pay to be in the home straight for athletics? Or for conspiracy theorists are they going to increase the number of expensive seats based on demand?) And the fact you are charged in mid-May and told what you paid for in mid-June (let’s ignore the massive one year interest free loan the organising committee gets from this).

Aha, but we’ve got some information to help with the last one. I know what tickets I asked for, what they cost, and I’ll be able to see from the credit card bill what I was charged. Is that enough to identify the tickets?

Cue some hastily written Python.

#!/usr/bin/env python
# Find prices of all possibilities of ticket allocations in the London
# 2012 Olympic lottery.
from itertools import chain, combinations
import re
import fileinput

def remove_pound(s):
    "Strip the pound sign from a cost."
    match = re.search('\d+\.\d{2}', s)
    return float(match.group(0))

class Session:
    def __init__(self):
        self.sport = ''
        self.code = ''
        self.datetime = ''
        self.venue = ''
        self.price_category = ''
        self.quantity = ''
        self.total_cost = ''
        self.maximum_cost = ''
    def cost(self):
        return remove_pound(self.total_cost)

    def max_cost(self):
        return remove_pound(self.maximum_cost)
    def __str__(self):
        return "%s (%s)" % (self.sport, self.code)
def powerset(iterable):
    "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)"
    s = list(iterable)
    return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))
Postage_Cost = 6
def calculate_prices(options, overhead = Postage_Cost):
    prices = {}
    for tickets in powerset(options):
        single_price = [t for t in tickets if t.max_cost == t.cost]
        dual_price = [t for t in tickets if t not in single_price]
        assert len(single_price) + len(dual_price) == len(tickets)
        for high_priced in powerset(dual_price):
            rest = [t for t in tickets if t not in high_priced]
            cost = sum(session.max_cost for session in high_priced)
            cost += sum(session.cost for session in rest)
            if cost > 0:
                cost += overhead
            seats = list(chain([str(s) + '*' for s in high_priced],
                               [str(s) for s in rest]))
            prices.setdefault(cost, []).append(seats)
    return prices

Fields = { 'Session code' : 'code',
           'Date/time' : 'datetime',
           'Venue' : 'venue',
           'Price Category' : 'price_category',
           'Quantity' : 'quantity',
           'Total cost' : 'total_cost',
           'Total maximum cost' : 'maximum_cost' }

def parse_email(line_iter):
    sessions = []
    current = None
    for line in line_iter:
        sport = re.match("\s+Sport: (.+)", line)
        if sport:
            if current:
            current = Session()
            current.sport = sport.group(1)
        elif current:
            for field, attribute in Fields.items():
                regexp = '\s+%s: (.+)' % field
                match = re.match(regexp, line)
                if match:
                    setattr(current, attribute, match.group(1))
    if current:
    return sessions
if __name__ == '__main__':
    sessions = parse_email(fileinput.input())
    possibilities = calculate_prices(sessions)
    for price in sorted(possibilities):
        for seats in possibilities[price]:
            print '%10.2f : %s' % (price, ', '.join(seats))

To use run python olympic.py paste in the ticket application confirmation. The result will be a list of prices and the tickets that lead to that cost. For example:

728 : Athletics (AT003), Hockey (HO039), Tennis (TE017)
730 : Hockey (HO011)*, Athletics (AT003), Rowing (RO001), Tennis (TE017)
740 : Cycling - Track (CT002), Hockey (HO011), Rowing (RO001), Tennis (TE017)

If you have sessions where you allowed the option of choosing a ticket that was more expensive than your preferred option and those tickets are assumed in a combination then they will be marked with an asterisk.

The code is a bit limited by assumptions that are true for my ticket application but won’t necessarily be true for all. I’ve assumed that the minimum you’ll pay for a block is your preferred option and that preferred and maximum price bands are adjacent. Neither is necessarily true but is the most that can be gleaned from the bare e-mail. To do more would require supplying the minimum band you were prepared to accept for every session and providing all the prices for all the bands for all the sports.