#!/usr/bin/env python
###
### Generate results, rankings, and team scores for the UCD Sprint Triathlon.
### Not the prettiest code in the world, but it works.  Feel free to take it
### and modify it for use with your own race if you like.
###
### Matt Roper <matt@mattrope.com>
###
### Updated 4-14-07 for 2007 race:
###  - now uses WCCTC ID's for school/gender (warns if provided school/gender
###    doesn't match ID)
###  - no longer a USAT race, so we aren't using their age groups; we're going
###    by 10-year increments instead
###

import string
import re
import math

# WCCTC school codes
SCHOOL = {
        "10" : "UCB",
        "11" : "UCSB",
        "12" : "Cal Poly",
        "13" : "UCD",
        "14" : "Stanford",
        "15" : "UCSD",
        "16" : "UCLA",
        "17" : "UNR",
        "18" : "CSUS",
        "19" : "USC",
        "20" : "CSULB",
        "21" : "UCSC",
        "22" : "UCI"
}

# Race data
data = []

# Counter for placing
display_i = 0

def main():
    global display_i

    fp = open("athletes.db", "r")
    line = fp.readline()

    # First line was headers; ignore it and proceed with data
    line = fp.readline()
    while line:
        line = line[:-1]
        [raceno, lname, fname, gender, age, division, team, wcctcid, wave, penalty, train, finish] = line.split(";")

        # Break finish time into components
        if string.strip(finish):
            (h, m, s) = finish.split(":")
        else:
            h = m = s = 0

        # Break penalty into components
        if string.strip(penalty):
            (pm, ps) = penalty.split(":")
        else:
            pm = ps = 0

        # Break train delay into components
        if string.strip(train):
            (tm, ts) = train.split(":")
        else:
            tm = ts = 0

        if string.strip(age) == "": age = "0"
        data.append({
            "raceno"   : string.strip(raceno),
            "lname"    : string.strip(lname),
            "fname"    : string.strip(fname),
            "gender"   : string.strip(gender),
            "age"      : int(age),
            "division" : string.strip(division),
            "school"   : string.strip(team),
            "wcctcid"  : string.strip(wcctcid),
            "wave"     : int(wave),
            "penalty"  : int(pm)*60 + int(ps),
            "train"    : int(tm)*60 + int(ts),
            "finish"   : int(h)*3600 + int(m)*60 + int(s)
            })
        line = fp.readline()
    fp.close()

    # Extract the finishers (i.e., those with a non-empty finish time)
    finishers = filter(lambda x: x['finish'] != 0, data)

    # Calculate final time based on wave and penalties
    for f in finishers:
        f['final'] = f['finish'] - (f['wave']-1)*20*60 + f['penalty'] - f['train']
        
    # Sort the finishers
    finishers.sort(lambda x, y: x['final'] - y['final'])

    # Separate collegiate (WCCTC) and non-collegiate (open or non-WCCTC)
    wcctc = filter(lambda x: x['wcctcid'] != "", finishers)
    noncollegiate = filter(lambda x: x['wcctcid'] == "", finishers)

    # Separate collegiate male, collegiate female
    cm = filter(lambda x: x['gender'] == "M", wcctc)
    cf = filter(lambda x: x['gender'] != "M", wcctc)

    # Score the collegiate men
    scores = {}
    counted = {}
    place = 1
    for athlete in cm:
        # Debugging; double check school specified matches WCCTC ID code...
        if SCHOOL[athlete["wcctcid"][0:2]] != athlete["school"]:
            print "(debug) %s != %s" % (SCHOOL[athlete["wcctcid"][0:2]], athlete["school"])

        # Scores go 50 deep
        if place > 50:
            break
        
        # How many points?
        athlete["points"] = placepoints(place)
        place += 1
        
        # Increase team score?
        team = SCHOOL[athlete["wcctcid"][0:2]]
        count = counted.get(team, 0)
        if count >= 7:
            continue
        else:
            score = scores.get(team, 0)
            score += athlete["points"]
            scores[team] = score
            counted[team] = count + 1

    # Sort the team scores
    items = scores.items()
    sortedscores = [ [v[1],v[0]] for v in items]
    sortedscores.sort()
    sortedscores.reverse()
    print "[ Team Scores (Men) ]"
    for [s, t] in sortedscores:
        print "%15s -- %4d" % (t, s)
    menscores = scores

    # Score the collegiate women
    scores = {}
    counted = {}
    place = 1
    for athlete in cf:
        # DEBUG
        if SCHOOL[athlete["wcctcid"][0:2]] != athlete["school"]:
            print "(debug) %s != %s" % (SCHOOL[athlete["wcctcid"][0:2]], athlete["school"])

        # Scores go 50 deep
        if place > 50:
            break
        
        # How many points?
        athlete["points"] = placepoints(place)
        place += 1
        
        # Increase team score?
        team = SCHOOL[athlete["wcctcid"][0:2]]
        count = counted.get(team, 0)
        if count >= 7:
            continue
        else:
            score = scores.get(team, 0)
            score += athlete["points"]
            scores[team] = score
            counted[team] = count + 1

    # Sort the team scores
    items = scores.items()
    sortedscores = [ [v[1],v[0]] for v in items]
    sortedscores.sort()
    sortedscores.reverse()
    print "\n[ Team Scores (Women) ]"
    for [s, t] in sortedscores:
        print "%15s -- %4d" % (t, s)
    womenscores = scores

    # Combined team scores; 'scores' still holds women's scores
    for t in menscores.keys():
        scores[t] = menscores[t] + womenscores.get(t, 0)

    # Re-sort
    items = scores.items()
    sortedscores = [ [v[1],v[0]] for v in items]
    sortedscores.sort()
    sortedscores.reverse()
    print "\n[ Team Scores (Combined) ]"
    for [s, t] in sortedscores:
        print "%15s -- %4d" % (t, s)


    ### WCCTC Collegiate Rankings
    print "\n[ Collegiate Men ]"
    for x in cm:
        display_collegiate(x)

    display_i = 0
    print "\n[ Collegiate Women ]"
    for x in cf:
        display_collegiate(x)

    #### Collegiate Exhibition
    #print "\n[ Collegiate Exhibition Men ]"
    #div = filter(lambda x: x['gender'] == "M", collegiateex)
    #for x in div: display_open(x)
    #print "\n[ Collegiate Exhibition Women ]"
    #div = filter(lambda x: x['gender'] == "F", collegiateex)
    #for x in div: display_open(x)

    ### Open division finishers
    for d in range(10,100,10):
        display_i = 0
        div = filter(lambda x: x['age'] >= d and x['age'] < d+10 and x['gender'] == "F", noncollegiate)
        if div:
            print "\n[ Open Division Rankings: Women %d-%d ]" % (d, d+9)
            for x in div: display_open(x)

        display_i = 0
        div = filter(lambda x: x['age'] >= d and x['age'] < d+10 and x['gender'] == "M", noncollegiate)
        if div:
            print "\n[ Open Division Rankings: Men %d-%d ]" % (d, d+9)
            for x in div: display_open(x)


    ## Debug: all finishers sorted on clock time, not finish time (to double
    ## check against bib tags that no one got missed)
    #print "\n[ All Finishers ]"
    #finishers.sort(lambda x, y: x['finish'] - y['finish'])
    #for x in finishers:
    #    print "%3s  %-25s %s  %s" % (x['raceno'], x['lname'] + ", " + x['fname'], displaytime(x['finish']), displaytime(x['final']))


def displaytime(t):
    s = t % 60
    t /= 60
    m = t % 60
    t /= 60
    h = t
    return "%d:%02d:%02d" % (h, m, s)

def displayshorttime(t):
    s = t % 60
    t /= 60
    m = t % 60
    t /= 60
    h = t
    return "%d:%02d" % (m, s)

def placepoints(place):
    # Old 2006 rules; no longer valid in 2007
    ##### Since there were two races on our race weekend, only half of the usual
    ##### points are awarded:  1st gets 1st place points, 2nd gets 3rd place points,
    ##### 3rd gets 5th place points, etc.
    ####place = place * 2 - 1

    if place > 50: return 0
    
    # Return the number of points awarded for this place.  Note the extra 0.5
    # added on before we take the floor (int) function -- even though the
    # constitution (and formula) says nothing about rounding, the example
    # points shown are rounded to the nearest integer rather than being
    # truncated with the floor function.  It also appears that the other
    # collegiate races have scored with rounding, so I'll follow their lead,
    # even though I think this is technically wrong.
    return int(200*(1-math.log10(place)/math.log10(51)) + 0.5)

def display_collegiate(x):
    global display_i
    display_i += 1

    # Display a collegiate ranking, with adjustments for train or drafting
    adjust = ""
    if x['train']:
        adjust = "-%s (train) " % displayshorttime(x['train'])
    if x['penalty']:
        adjust += "+%s (drafting)" % displayshorttime(x['penalty'])
    print "%3d) %3s  %-25s %-12s %-20s %s %4d points" % (display_i, x['raceno'], x['lname'] + ", " + x['fname'], x['school'], adjust, displaytime(x['final']), x.get('points', 0))

def display_open(x):
    global display_i
    display_i += 1

    # Display an open ranking, with adjustments for train or drafting
    adjust = ""
    if x['train']:
        adjust = "-%s (train) " % displayshorttime(x['train'])
    if x['penalty']:
        adjust += "+%s (drafting)" % displayshorttime(x['penalty'])
    print "%3d) %3s  %-25s %-20s %s" % (display_i, x['raceno'], x['lname'] + ", " + x['fname'], adjust, displaytime(x['final']))

main()
