sportsref/nfl/pbp.py
def parse_play_details(details):
    """Parses play details from play-by-play string and returns structured
    data.

    :details: detail string for play
    :returns: dictionary of play attributes
    """

    # if input isn't a string, return None
    if not isinstance(details, basestring):
        return None

    rushOptRE = r'(?P<rushDir>{})'.format(
        r'|'.join(RUSH_OPTS.keys())
    )
    passOptRE = r'(?P<passLoc>{})'.format(
        r'|'.join(PASS_OPTS.keys())
    )

    playerRE = r"\S{6,8}\d{2}"

    # initialize return dictionary - struct
    struct = {}

    # handle challenges
    # TODO: record the play both before & after an overturned challenge
    challengeRE = re.compile(
        r'.+\. (?P<challenger>.+?) challenged.*? the play was '
        '(?P<callUpheld>upheld|overturned)\.',
        re.IGNORECASE
    )
    match = challengeRE.search(details)
    if match:
        struct['isChallenge'] = True
        struct.update(match.groupdict())
        # if overturned, only record updated play
        if 'overturned' in details:
            overturnedIdx = details.index('overturned.')
            newStart = overturnedIdx + len('overturned.')
            details = details[newStart:].strip()
    else:
        struct['isChallenge'] = False

    # TODO: expand on laterals
    struct['isLateral'] = details.find('lateral') != -1

    # create rushing regex
    rusherRE = r"(?P<rusher>{0})".format(playerRE)
    rushOptRE = r"(?: {})?".format(rushOptRE)
    rushYardsRE = r"(?:(?:(?P<rushYds>\-?\d+) yards?)|(?:no gain))"
    # cases: tackle, fumble, td, penalty
    tackleRE = (r"(?: \(tackle by (?P<tackler1>{0})"
                r"(?: and (?P<tackler2>{0}))?\))?"
                .format(playerRE))
    # currently, plays with multiple fumbles record the original fumbler
    # and the final fumble recoverer
    fumbleRE = (
        r"(?:"
        r"\.? ?(?P<fumbler>{0}) fumbles"
        r"(?: \(forced by (?P<fumbForcer>{0})\))?"
        r"(?:.*, recovered by (?P<fumbRecoverer>{0}) at )?"
        r"(?:, ball out of bounds at )?"
        r"(?:(?P<fumbRecFieldSide>[a-z]+)?\-?(?P<fumbRecYdLine>\-?\d+))?"
        r"(?: and returned for (?P<fumbRetYds>\-?\d*) yards)?"
        r")?"
        .format(playerRE))
    tdSafetyRE = r"(?:(?P<isTD>, touchdown)|(?P<isSafety>, safety))?"
    # TODO: offsetting penalties
    penaltyRE = (r"(?:.*?"
                 r"\. Penalty on (?P<penOn>{0}|): "
                 r"(?P<penalty>[^\(,]+)"
                 r"(?: \((?P<penDeclined>Declined)\)|"
                 r", (?P<penYds>\d*) yards?)"
                 r"(?: \(no play\))?"
                 r")?"
                 .format(playerRE))

    rushREstr = (
        r"{}{}(?: for {}{}{}{}{})?"
    ).format(rusherRE, rushOptRE, rushYardsRE, tackleRE, fumbleRE, tdSafetyRE,
             penaltyRE)
    rushRE = re.compile(rushREstr, re.IGNORECASE)

    # create passing regex
    # TODO: capture "defended by X" for defensive stats
    passerRE = r"(?P<passer>{0})".format(playerRE)
    sackRE = (r"(?:sacked (?:by (?P<sacker1>{0})(?: and (?P<sacker2>{0}))? )?"
              r"for (?P<sackYds>\-?\d+) yards?)"
              .format(playerRE))
    # create throw RE
    completeRE = r"pass (?P<isComplete>(?:in)?complete)"
    passOptRE = r"(?: {})?".format(passOptRE)
    targetedRE = r"(?: (?:to |intended for )?(?P<target>{0}))?".format(
        playerRE)
    passYardsRE = r"(?: for (?:(?P<passYds>\-?\d+) yards?|no gain))"
    intRE = (r'(?: is intercepted by (?P<interceptor>{0}) at '.format(playerRE)
             + r'(?:(?P<intFieldSide>[a-z]*)?\-?(?P<intYdLine>\-?\d*))?'
             + r'(?: and returned for (?P<intRetYds>\-?\d+) yards?\.?)?)?')
    throwRE = r'(?:{}{}{}(?:(?:{}|{}){})?)'.format(
        completeRE, passOptRE, targetedRE, passYardsRE, intRE, tackleRE
    )
    passREstr = (
        r"{} (?:{}|{})(?:{}{}{})?"
    ).format(passerRE, sackRE, throwRE, fumbleRE, tdSafetyRE, penaltyRE)
    passRE = re.compile(passREstr, re.IGNORECASE)

    # create kickoff regex
    koKickerRE = r'(?P<koKicker>{0})'.format(playerRE)
    koYardsRE = (r' kicks (?:off|(?P<isOnside>onside))'
                 r' (?:(?P<koYds>\d+) yards?|no gain)')
    nextREs = []
    nextREs.append(
        (r', (?:returned|recovered) by (?P<koReturner>{0})(?: for '
         r'(?:(?P<koRetYds>\-?\d+) yards?|no gain))?').format(playerRE)
    )
    nextREs.append(
        (r'(?P<isMuffedCatch>, muffed catch by )(?P<muffedBy>{0}),'
         r'(?: recovered by (?P<muffRecoverer>{0}))?').format(playerRE) +
        r'(?: and returned for (?:(?P<muffRetYds>\-?\d+) yards|no gain))?'
    )
    nextREs.append(
        r', recovered by (?P<onsideRecoverer>{0})'.format(playerRE)
    )
    nextREs.append(r'(?P<oob>, out of bounds)')
    nextREs.append(r'(?P<isTouchback>, touchback)')
    # TODO: test the following line to fix a small subset of cases
    # (ex: muff -> oob)
    nextRE = ''.join(r'(?:{})?'.format(nre) for nre in nextREs)
    kickoffREstr = r'{}{}{}{}{}{}{}'.format(
        koKickerRE, koYardsRE, nextRE,
        tackleRE, fumbleRE, tdSafetyRE, penaltyRE
    )
    kickoffRE = re.compile(kickoffREstr, re.IGNORECASE)

    # create timeout regex
    timeoutREstr = r'Timeout #(?P<timeoutNum>\d) by (?P<timeoutTeam>.+)'
    timeoutRE = re.compile(timeoutREstr, re.IGNORECASE)

    # create FG regex
    fgKickerRE = r'(?P<fgKicker>{0})'.format(playerRE)
    fgBaseRE = (r' (?P<fgDist>\d+) yard field goal'
                r' (?P<fgGood>good|no good)')
    fgBlockRE = (
        r'(?:, (?P<isBlocked>blocked) by '
        r'(?P<fgBlocker>{0}))?'.format(playerRE) +
        r'(?:, recovered by (?P<fgBlockRecoverer>{0}))?'.format(playerRE) +
        r'(?: and returned for (?:(?P<fgBlockRetYds>\-?\d+) yards?|no gain))?'
    )
    fgREstr = r'{}{}{}{}{}'.format(fgKickerRE, fgBaseRE,
                                   fgBlockRE, tdSafetyRE, penaltyRE)
    fgRE = re.compile(fgREstr, re.IGNORECASE)

    # create punt regex
    punterRE = r'.*?(?P<punter>{0})'.format(playerRE)
    puntBlockRE = (
        (r' punts, (?P<isBlocked>blocked) by (?P<puntBlocker>{0})'
         r'(?:, recovered by (?P<puntBlockRecoverer>{0})').format(playerRE) +
        r'(?: and returned (?:(?P<puntBlockRetYds>\-?\d+) yards|no gain))?)?'
    )
    puntYdsRE = r' punts (?P<puntYds>\d+) yards?'
    nextREs = []
    nextREs.append(r', (?P<isFairCatch>fair catch) by (?P<fairCatcher>{0})'
                   .format(playerRE))
    nextREs.append(r', (?P<oob>out of bounds)')
    nextREs.append(
        (r'(?P<isMuffedCatch>, muffed catch by )(?P<muffedBy>{0}),'
         r' recovered by (?P<muffRecoverer>{0})').format(playerRE) +
        r' and returned for ' +
        r'(?:(?P<muffRetYds>\d+) yards|no gain)'
    )
    nextREs.append(
        r', returned by (?P<puntReturner>{0}) for '.format(playerRE) +
        r'(?:(?P<puntRetYds>\-?\d+) yards?|no gain)'
    )
    nextRE = r'(?:{})?'.format('|'.join(nextREs))
    puntREstr = r'{}(?:{}|{}){}{}{}{}{}'.format(
        punterRE, puntBlockRE, puntYdsRE, nextRE,
        tackleRE, fumbleRE, tdSafetyRE, penaltyRE
    )
    puntRE = re.compile(puntREstr, re.IGNORECASE)

    # create kneel regex
    kneelREstr = (r'(?P<kneelQB>{0}) kneels for '.format(playerRE) +
                  r'(?:(?P<kneelYds>\-?\d+) yards?|no gain)')
    kneelRE = re.compile(kneelREstr, re.IGNORECASE)

    # create spike regex
    spikeREstr = r'(?P<spikeQB>{0}) spiked the ball'.format(playerRE)
    spikeRE = re.compile(spikeREstr, re.IGNORECASE)

    # create XP regex
    extraPointREstr = (r'(?:(?P<xpKicker>{0}) kicks)? ?extra point '
                       r'(?P<xpGood>good|no good)').format(playerRE)
    extraPointRE = re.compile(extraPointREstr, re.IGNORECASE)

    # create 2pt conversion regex
    twoPointREstr = (
        r'Two Point Attempt: (?P<twoPoint>.*?),?\s+conversion\s+'
        r'(?P<twoPointSuccess>succeeds|fails)'
    )
    twoPointRE = re.compile(twoPointREstr, re.IGNORECASE)

    # create penalty regex
    psPenaltyREstr = (
        r'^Penalty on (?P<penOn>{0}|'.format(playerRE) + r'\w{3}): ' +
        r'(?P<penalty>[^\(,]+)(?: \((?P<penDeclined>Declined)\)|' +
        r', (?P<penYds>\d*) yards?|' +
        r'.*?(?: \(no play\)))')
    psPenaltyRE = re.compile(psPenaltyREstr, re.IGNORECASE)

    # try parsing as a kickoff
    match = kickoffRE.search(details)
    if match:
        # parse as a kickoff
        struct['isKickoff'] = True
        struct.update(match.groupdict())
        return struct

    # try parsing as a timeout
    match = timeoutRE.search(details)
    if match:
        # parse as timeout
        struct['isTimeout'] = True
        struct.update(match.groupdict())
        return struct

    # try parsing as a field goal
    match = fgRE.search(details)
    if match:
        # parse as a field goal
        struct['isFieldGoal'] = True
        struct.update(match.groupdict())
        return struct

    # try parsing as a punt
    match = puntRE.search(details)
    if match:
        # parse as a punt
        struct['isPunt'] = True
        struct.update(match.groupdict())
        return struct

    # try parsing as a kneel
    match = kneelRE.search(details)
    if match:
        # parse as a kneel
        struct['isKneel'] = True
        struct.update(match.groupdict())
        return struct

    # try parsing as a spike
    match = spikeRE.search(details)
    if match:
        # parse as a spike
        struct['isSpike'] = True
        struct.update(match.groupdict())
        return struct

    # try parsing as an XP
    match = extraPointRE.search(details)
    if match:
        # parse as an XP
        struct['isXP'] = True
        struct.update(match.groupdict())
        return struct

    # try parsing as a 2-point conversion
    match = twoPointRE.search(details)
    if match:
        # parse as a 2-point conversion
        struct['isTwoPoint'] = True
        struct['twoPointSuccess'] = match.group('twoPointSuccess')
        realPlay = sportsref.nfl.pbp.parse_play_details(
            match.group('twoPoint'))
        if realPlay:
            struct.update(realPlay)
        return struct

    # try parsing as a pass
    match = passRE.search(details)
    if match:
        # parse as a pass
        struct['isPass'] = True
        struct.update(match.groupdict())
        return struct

    # try parsing as a pre-snap penalty
    match = psPenaltyRE.search(details)
    if match:
        # parse as a pre-snap penalty
        struct['isPresnapPenalty'] = True
        struct.update(match.groupdict())
        return struct

    # try parsing as a run
    match = rushRE.search(details)
    if match:
        # parse as a run
        struct['isRun'] = True
        struct.update(match.groupdict())
        return struct

    return None
def parse_play_details(details):
    """Parses play details from play-by-play string and returns structured
    data.

    :details: detail string for play
    :returns: dictionary of play attributes
    """

    # if input isn't a string, return None
    if not isinstance(details, basestring):
        return None

    rushOptRE = r'(?P<rushDir>{})'.format(
        r'|'.join(RUSH_OPTS.keys())
    )
    passOptRE = r'(?P<passLoc>{})'.format(
        r'|'.join(PASS_OPTS.keys())
    )

    playerRE = r"\S{6,8}\d{2}"

    # initialize return dictionary - struct
    struct = {}

    # handle challenges
    # TODO: record the play both before & after an overturned challenge
    challengeRE = re.compile(
        r'.+\. (?P<challenger>.+?) challenged.*? the play was '
        '(?P<callUpheld>upheld|overturned)\.',
        re.IGNORECASE
    )
    match = challengeRE.search(details)
    if match:
        struct['isChallenge'] = True
        struct.update(match.groupdict())
        # if overturned, only record updated play
        if 'overturned' in details:
            overturnedIdx = details.index('overturned.')
            newStart = overturnedIdx + len('overturned.')
            details = details[newStart:].strip()
    else:
        struct['isChallenge'] = False

    # TODO: expand on laterals
    struct['isLateral'] = details.find('lateral') != -1

    # create rushing regex
    rusherRE = r"(?P<rusher>{0})".format(playerRE)
    rushOptRE = r"(?: {})?".format(rushOptRE)
    rushYardsRE = r"(?:(?:(?P<rushYds>\-?\d+) yards?)|(?:no gain))"
    # cases: tackle, fumble, td, penalty
    tackleRE = (r"(?: \(tackle by (?P<tackler1>{0})"
                r"(?: and (?P<tackler2>{0}))?\))?"
                .format(playerRE))
    # currently, plays with multiple fumbles record the original fumbler
    # and the final fumble recoverer
    fumbleRE = (
        r"(?:"
        r"\.? ?(?P<fumbler>{0}) fumbles"
        r"(?: \(forced by (?P<fumbForcer>{0})\))?"
        r"(?:.*, recovered by (?P<fumbRecoverer>{0}) at )?"
        r"(?:, ball out of bounds at )?"
        r"(?:(?P<fumbRecFieldSide>[a-z]+)?\-?(?P<fumbRecYdLine>\-?\d+))?"
        r"(?: and returned for (?P<fumbRetYds>\-?\d*) yards)?"
        r")?"
        .format(playerRE))
    tdSafetyRE = r"(?:(?P<isTD>, touchdown)|(?P<isSafety>, safety))?"
    # TODO: offsetting penalties
    penaltyRE = (r"(?:.*?"
                 r"\. Penalty on (?P<penOn>{0}|): "
                 r"(?P<penalty>[^\(,]+)"
                 r"(?: \((?P<penDeclined>Declined)\)|"
                 r", (?P<penYds>\d*) yards?)"
                 r"(?: \(no play\))?"
                 r")?"
                 .format(playerRE))

    rushREstr = (
        r"{}{}(?: for {}{}{}{}{})?"
    ).format(rusherRE, rushOptRE, rushYardsRE, tackleRE, fumbleRE, tdSafetyRE,
             penaltyRE)
    rushRE = re.compile(rushREstr, re.IGNORECASE)

    # create passing regex
    # TODO: capture "defended by X" for defensive stats
    passerRE = r"(?P<passer>{0})".format(playerRE)
    sackRE = (r"(?:sacked (?:by (?P<sacker1>{0})(?: and (?P<sacker2>{0}))? )?"
              r"for (?P<sackYds>\-?\d+) yards?)"
              .format(playerRE))
    # create throw RE
    completeRE = r"pass (?P<isComplete>(?:in)?complete)"
    passOptRE = r"(?: {})?".format(passOptRE)
    targetedRE = r"(?: (?:to |intended for )?(?P<target>{0}))?".format(
        playerRE)
    passYardsRE = r"(?: for (?:(?P<passYds>\-?\d+) yards?|no gain))"
    intRE = (r'(?: is intercepted by (?P<interceptor>{0}) at '.format(playerRE)
             + r'(?:(?P<intFieldSide>[a-z]*)?\-?(?P<intYdLine>\-?\d*))?'
             + r'(?: and returned for (?P<intRetYds>\-?\d+) yards?\.?)?)?')
    throwRE = r'(?:{}{}{}(?:(?:{}|{}){})?)'.format(
        completeRE, passOptRE, targetedRE, passYardsRE, intRE, tackleRE
    )
    passREstr = (
        r"{} (?:{}|{})(?:{}{}{})?"
    ).format(passerRE, sackRE, throwRE, fumbleRE, tdSafetyRE, penaltyRE)
    passRE = re.compile(passREstr, re.IGNORECASE)

    # create kickoff regex
    koKickerRE = r'(?P<koKicker>{0})'.format(playerRE)
    koYardsRE = (r' kicks (?:off|(?P<isOnside>onside))'
                 r' (?:(?P<koYds>\d+) yards?|no gain)')
    nextREs = []
    nextREs.append(
        (r', (?:returned|recovered) by (?P<koReturner>{0})(?: for '
         r'(?:(?P<koRetYds>\-?\d+) yards?|no gain))?').format(playerRE)
    )
    nextREs.append(
        (r'(?P<isMuffedCatch>, muffed catch by )(?P<muffedBy>{0}),'
         r'(?: recovered by (?P<muffRecoverer>{0}))?').format(playerRE) +
        r'(?: and returned for (?:(?P<muffRetYds>\-?\d+) yards|no gain))?'
    )
    nextREs.append(
        r', recovered by (?P<onsideRecoverer>{0})'.format(playerRE)
    )
    nextREs.append(r'(?P<oob>, out of bounds)')
    nextREs.append(r'(?P<isTouchback>, touchback)')
    # TODO: test the following line to fix a small subset of cases
    # (ex: muff -> oob)
    nextRE = ''.join(r'(?:{})?'.format(nre) for nre in nextREs)
    kickoffREstr = r'{}{}{}{}{}{}{}'.format(
        koKickerRE, koYardsRE, nextRE,
        tackleRE, fumbleRE, tdSafetyRE, penaltyRE
    )
    kickoffRE = re.compile(kickoffREstr, re.IGNORECASE)

    # create timeout regex
    timeoutREstr = r'Timeout #(?P<timeoutNum>\d) by (?P<timeoutTeam>.+)'
    timeoutRE = re.compile(timeoutREstr, re.IGNORECASE)

    # create FG regex
    fgKickerRE = r'(?P<fgKicker>{0})'.format(playerRE)
    fgBaseRE = (r' (?P<fgDist>\d+) yard field goal'
                r' (?P<fgGood>good|no good)')
    fgBlockRE = (
        r'(?:, (?P<isBlocked>blocked) by '
        r'(?P<fgBlocker>{0}))?'.format(playerRE) +
        r'(?:, recovered by (?P<fgBlockRecoverer>{0}))?'.format(playerRE) +
        r'(?: and returned for (?:(?P<fgBlockRetYds>\-?\d+) yards?|no gain))?'
    )
    fgREstr = r'{}{}{}{}{}'.format(fgKickerRE, fgBaseRE,
                                   fgBlockRE, tdSafetyRE, penaltyRE)
    fgRE = re.compile(fgREstr, re.IGNORECASE)

    # create punt regex
    punterRE = r'.*?(?P<punter>{0})'.format(playerRE)
    puntBlockRE = (
        (r' punts, (?P<isBlocked>blocked) by (?P<puntBlocker>{0})'
         r'(?:, recovered by (?P<puntBlockRecoverer>{0})').format(playerRE) +
        r'(?: and returned (?:(?P<puntBlockRetYds>\-?\d+) yards|no gain))?)?'
    )
    puntYdsRE = r' punts (?P<puntYds>\d+) yards?'
    nextREs = []
    nextREs.append(r', (?P<isFairCatch>fair catch) by (?P<fairCatcher>{0})'
                   .format(playerRE))
    nextREs.append(r', (?P<oob>out of bounds)')
    nextREs.append(
        (r'(?P<isMuffedCatch>, muffed catch by )(?P<muffedBy>{0}),'
         r' recovered by (?P<muffRecoverer>{0})').format(playerRE) +
        r' and returned for ' +
        r'(?:(?P<muffRetYds>\d+) yards|no gain)'
    )
    nextREs.append(
        r', returned by (?P<puntReturner>{0}) for '.format(playerRE) +
        r'(?:(?P<puntRetYds>\-?\d+) yards?|no gain)'
    )
    nextRE = r'(?:{})?'.format('|'.join(nextREs))
    puntREstr = r'{}(?:{}|{}){}{}{}{}{}'.format(
        punterRE, puntBlockRE, puntYdsRE, nextRE,
        tackleRE, fumbleRE, tdSafetyRE, penaltyRE
    )
    puntRE = re.compile(puntREstr, re.IGNORECASE)

    # create kneel regex
    kneelREstr = (r'(?P<kneelQB>{0}) kneels for '.format(playerRE) +
                  r'(?:(?P<kneelYds>\-?\d+) yards?|no gain)')
    kneelRE = re.compile(kneelREstr, re.IGNORECASE)

    # create spike regex
    spikeREstr = r'(?P<spikeQB>{0}) spiked the ball'.format(playerRE)
    spikeRE = re.compile(spikeREstr, re.IGNORECASE)

    # create XP regex
    extraPointREstr = (r'(?:(?P<xpKicker>{0}) kicks)? ?extra point '
                       r'(?P<xpGood>good|no good)').format(playerRE)
    extraPointRE = re.compile(extraPointREstr, re.IGNORECASE)

    # create 2pt conversion regex
    twoPointREstr = (
        r'Two Point Attempt: (?P<twoPoint>.*?),?\s+conversion\s+'
        r'(?P<twoPointSuccess>succeeds|fails)'
    )
    twoPointRE = re.compile(twoPointREstr, re.IGNORECASE)

    # create penalty regex
    psPenaltyREstr = (
        r'^Penalty on (?P<penOn>{0}|'.format(playerRE) + r'\w{3}): ' +
        r'(?P<penalty>[^\(,]+)(?: \((?P<penDeclined>Declined)\)|' +
        r', (?P<penYds>\d*) yards?|' +
        r'.*?(?: \(no play\)))')
    psPenaltyRE = re.compile(psPenaltyREstr, re.IGNORECASE)

    # try parsing as a kickoff
    match = kickoffRE.search(details)
    if match:
        # parse as a kickoff
        struct['isKickoff'] = True
        struct.update(match.groupdict())
        return struct

    # try parsing as a timeout
    match = timeoutRE.search(details)
    if match:
        # parse as timeout
        struct['isTimeout'] = True
        struct.update(match.groupdict())
        return struct

    # try parsing as a field goal
    match = fgRE.search(details)
    if match:
        # parse as a field goal
        struct['isFieldGoal'] = True
        struct.update(match.groupdict())
        return struct

    # try parsing as a punt
    match = puntRE.search(details)
    if match:
        # parse as a punt
        struct['isPunt'] = True
        struct.update(match.groupdict())
        return struct

    # try parsing as a kneel
    match = kneelRE.search(details)
    if match:
        # parse as a kneel
        struct['isKneel'] = True
        struct.update(match.groupdict())
        return struct

    # try parsing as a spike
    match = spikeRE.search(details)
    if match:
        # parse as a spike
        struct['isSpike'] = True
        struct.update(match.groupdict())
        return struct

    # try parsing as an XP
    match = extraPointRE.search(details)
    if match:
        # parse as an XP
        struct['isXP'] = True
        struct.update(match.groupdict())
        return struct

    # try parsing as a 2-point conversion
    match = twoPointRE.search(details)
    if match:
        # parse as a 2-point conversion
        struct['isTwoPoint'] = True
        struct['twoPointSuccess'] = match.group('twoPointSuccess')
        realPlay = sportsref.nfl.pbp.parse_play_details(
            match.group('twoPoint'))
        if realPlay:
            struct.update(realPlay)
        return struct

    # try parsing as a pass
    match = passRE.search(details)
    if match:
        # parse as a pass
        struct['isPass'] = True
        struct.update(match.groupdict())
        return struct

    # try parsing as a pre-snap penalty
    match = psPenaltyRE.search(details)
    if match:
        # parse as a pre-snap penalty
        struct['isPresnapPenalty'] = True
        struct.update(match.groupdict())
        return struct

    # try parsing as a run
    match = rushRE.search(details)
    if match:
        # parse as a run
        struct['isRun'] = True
        struct.update(match.groupdict())
        return struct

    return None
def parse_play_details ( details ) : # if input isn't a string, return None if not isinstance ( details , basestring ) : return None rushOptRE = r'(?P<rushDir>{})' . format ( r'|' . join ( RUSH_OPTS . keys ( ) ) ) passOptRE = r'(?P<passLoc>{})' . format ( r'|' . join ( PASS_OPTS . keys ( ) ) ) playerRE = r"\S{6,8}\d{2}" # initialize return dictionary - struct struct = { } # handle challenges # TODO: record the play both before & after an overturned challenge challengeRE = re . compile ( r'.+\. (?P<challenger>.+?) challenged.*? the play was ' '(?P<callUpheld>upheld|overturned)\.' , re . IGNORECASE ) match = challengeRE . search ( details ) if match : struct [ 'isChallenge' ] = True struct . update ( match . groupdict ( ) ) # if overturned, only record updated play if 'overturned' in details : overturnedIdx = details . index ( 'overturned.' ) newStart = overturnedIdx + len ( 'overturned.' ) details = details [ newStart : ] . strip ( ) else : struct [ 'isChallenge' ] = False # TODO: expand on laterals struct [ 'isLateral' ] = details . find ( 'lateral' ) != - 1 # create rushing regex rusherRE = r"(?P<rusher>{0})" . format ( playerRE ) rushOptRE = r"(?: {})?" . format ( rushOptRE ) rushYardsRE = r"(?:(?:(?P<rushYds>\-?\d+) yards?)|(?:no gain))" # cases: tackle, fumble, td, penalty tackleRE = ( r"(?: \(tackle by (?P<tackler1>{0})" r"(?: and (?P<tackler2>{0}))?\))?" . format ( playerRE ) ) # currently, plays with multiple fumbles record the original fumbler # and the final fumble recoverer fumbleRE = ( r"(?:" r"\.? ?(?P<fumbler>{0}) fumbles" r"(?: \(forced by (?P<fumbForcer>{0})\))?" r"(?:.*, recovered by (?P<fumbRecoverer>{0}) at )?" r"(?:, ball out of bounds at )?" r"(?:(?P<fumbRecFieldSide>[a-z]+)?\-?(?P<fumbRecYdLine>\-?\d+))?" r"(?: and returned for (?P<fumbRetYds>\-?\d*) yards)?" r")?" . format ( playerRE ) ) tdSafetyRE = r"(?:(?P<isTD>, touchdown)|(?P<isSafety>, safety))?" # TODO: offsetting penalties penaltyRE = ( r"(?:.*?" r"\. Penalty on (?P<penOn>{0}|): " r"(?P<penalty>[^\(,]+)" r"(?: \((?P<penDeclined>Declined)\)|" r", (?P<penYds>\d*) yards?)" r"(?: \(no play\))?" r")?" . format ( playerRE ) ) rushREstr = ( r"{}{}(?: for {}{}{}{}{})?" ) . format ( rusherRE , rushOptRE , rushYardsRE , tackleRE , fumbleRE , tdSafetyRE , penaltyRE ) rushRE = re . compile ( rushREstr , re . IGNORECASE ) # create passing regex # TODO: capture "defended by X" for defensive stats passerRE = r"(?P<passer>{0})" . format ( playerRE ) sackRE = ( r"(?:sacked (?:by (?P<sacker1>{0})(?: and (?P<sacker2>{0}))? )?" r"for (?P<sackYds>\-?\d+) yards?)" . format ( playerRE ) ) # create throw RE completeRE = r"pass (?P<isComplete>(?:in)?complete)" passOptRE = r"(?: {})?" . format ( passOptRE ) targetedRE = r"(?: (?:to |intended for )?(?P<target>{0}))?" . format ( playerRE ) passYardsRE = r"(?: for (?:(?P<passYds>\-?\d+) yards?|no gain))" intRE = ( r'(?: is intercepted by (?P<interceptor>{0}) at ' . format ( playerRE ) + r'(?:(?P<intFieldSide>[a-z]*)?\-?(?P<intYdLine>\-?\d*))?' + r'(?: and returned for (?P<intRetYds>\-?\d+) yards?\.?)?)?' ) throwRE = r'(?:{}{}{}(?:(?:{}|{}){})?)' . format ( completeRE , passOptRE , targetedRE , passYardsRE , intRE , tackleRE ) passREstr = ( r"{} (?:{}|{})(?:{}{}{})?" ) . format ( passerRE , sackRE , throwRE , fumbleRE , tdSafetyRE , penaltyRE ) passRE = re . compile ( passREstr , re . IGNORECASE ) # create kickoff regex koKickerRE = r'(?P<koKicker>{0})' . format ( playerRE ) koYardsRE = ( r' kicks (?:off|(?P<isOnside>onside))' r' (?:(?P<koYds>\d+) yards?|no gain)' ) nextREs = [ ] nextREs . append ( ( r', (?:returned|recovered) by (?P<koReturner>{0})(?: for ' r'(?:(?P<koRetYds>\-?\d+) yards?|no gain))?' ) . format ( playerRE ) ) nextREs . append ( ( r'(?P<isMuffedCatch>, muffed catch by )(?P<muffedBy>{0}),' r'(?: recovered by (?P<muffRecoverer>{0}))?' ) . format ( playerRE ) + r'(?: and returned for (?:(?P<muffRetYds>\-?\d+) yards|no gain))?' ) nextREs . append ( r', recovered by (?P<onsideRecoverer>{0})' . format ( playerRE ) ) nextREs . append ( r'(?P<oob>, out of bounds)' ) nextREs . append ( r'(?P<isTouchback>, touchback)' ) # TODO: test the following line to fix a small subset of cases # (ex: muff -> oob) nextRE = '' . join ( r'(?:{})?' . format ( nre ) for nre in nextREs ) kickoffREstr = r'{}{}{}{}{}{}{}' . format ( koKickerRE , koYardsRE , nextRE , tackleRE , fumbleRE , tdSafetyRE , penaltyRE ) kickoffRE = re . compile ( kickoffREstr , re . IGNORECASE ) # create timeout regex timeoutREstr = r'Timeout #(?P<timeoutNum>\d) by (?P<timeoutTeam>.+)' timeoutRE = re . compile ( timeoutREstr , re . IGNORECASE ) # create FG regex fgKickerRE = r'(?P<fgKicker>{0})' . format ( playerRE ) fgBaseRE = ( r' (?P<fgDist>\d+) yard field goal' r' (?P<fgGood>good|no good)' ) fgBlockRE = ( r'(?:, (?P<isBlocked>blocked) by ' r'(?P<fgBlocker>{0}))?' . format ( playerRE ) + r'(?:, recovered by (?P<fgBlockRecoverer>{0}))?' . format ( playerRE ) + r'(?: and returned for (?:(?P<fgBlockRetYds>\-?\d+) yards?|no gain))?' ) fgREstr = r'{}{}{}{}{}' . format ( fgKickerRE , fgBaseRE , fgBlockRE , tdSafetyRE , penaltyRE ) fgRE = re . compile ( fgREstr , re . IGNORECASE ) # create punt regex punterRE = r'.*?(?P<punter>{0})' . format ( playerRE ) puntBlockRE = ( ( r' punts, (?P<isBlocked>blocked) by (?P<puntBlocker>{0})' r'(?:, recovered by (?P<puntBlockRecoverer>{0})' ) . format ( playerRE ) + r'(?: and returned (?:(?P<puntBlockRetYds>\-?\d+) yards|no gain))?)?' ) puntYdsRE = r' punts (?P<puntYds>\d+) yards?' nextREs = [ ] nextREs . append ( r', (?P<isFairCatch>fair catch) by (?P<fairCatcher>{0})' . format ( playerRE ) ) nextREs . append ( r', (?P<oob>out of bounds)' ) nextREs . append ( ( r'(?P<isMuffedCatch>, muffed catch by )(?P<muffedBy>{0}),' r' recovered by (?P<muffRecoverer>{0})' ) . format ( playerRE ) + r' and returned for ' + r'(?:(?P<muffRetYds>\d+) yards|no gain)' ) nextREs . append ( r', returned by (?P<puntReturner>{0}) for ' . format ( playerRE ) + r'(?:(?P<puntRetYds>\-?\d+) yards?|no gain)' ) nextRE = r'(?:{})?' . format ( '|' . join ( nextREs ) ) puntREstr = r'{}(?:{}|{}){}{}{}{}{}' . format ( punterRE , puntBlockRE , puntYdsRE , nextRE , tackleRE , fumbleRE , tdSafetyRE , penaltyRE ) puntRE = re . compile ( puntREstr , re . IGNORECASE ) # create kneel regex kneelREstr = ( r'(?P<kneelQB>{0}) kneels for ' . format ( playerRE ) + r'(?:(?P<kneelYds>\-?\d+) yards?|no gain)' ) kneelRE = re . compile ( kneelREstr , re . IGNORECASE ) # create spike regex spikeREstr = r'(?P<spikeQB>{0}) spiked the ball' . format ( playerRE ) spikeRE = re . compile ( spikeREstr , re . IGNORECASE ) # create XP regex extraPointREstr = ( r'(?:(?P<xpKicker>{0}) kicks)? ?extra point ' r'(?P<xpGood>good|no good)' ) . format ( playerRE ) extraPointRE = re . compile ( extraPointREstr , re . IGNORECASE ) # create 2pt conversion regex twoPointREstr = ( r'Two Point Attempt: (?P<twoPoint>.*?),?\s+conversion\s+' r'(?P<twoPointSuccess>succeeds|fails)' ) twoPointRE = re . compile ( twoPointREstr , re . IGNORECASE ) # create penalty regex psPenaltyREstr = ( r'^Penalty on (?P<penOn>{0}|' . format ( playerRE ) + r'\w{3}): ' + r'(?P<penalty>[^\(,]+)(?: \((?P<penDeclined>Declined)\)|' + r', (?P<penYds>\d*) yards?|' + r'.*?(?: \(no play\)))' ) psPenaltyRE = re . compile ( psPenaltyREstr , re . IGNORECASE ) # try parsing as a kickoff match = kickoffRE . search ( details ) if match : # parse as a kickoff struct [ 'isKickoff' ] = True struct . update ( match . groupdict ( ) ) return struct # try parsing as a timeout match = timeoutRE . search ( details ) if match : # parse as timeout struct [ 'isTimeout' ] = True struct . update ( match . groupdict ( ) ) return struct # try parsing as a field goal match = fgRE . search ( details ) if match : # parse as a field goal struct [ 'isFieldGoal' ] = True struct . update ( match . groupdict ( ) ) return struct # try parsing as a punt match = puntRE . search ( details ) if match : # parse as a punt struct [ 'isPunt' ] = True struct . update ( match . groupdict ( ) ) return struct # try parsing as a kneel match = kneelRE . search ( details ) if match : # parse as a kneel struct [ 'isKneel' ] = True struct . update ( match . groupdict ( ) ) return struct # try parsing as a spike match = spikeRE . search ( details ) if match : # parse as a spike struct [ 'isSpike' ] = True struct . update ( match . groupdict ( ) ) return struct # try parsing as an XP match = extraPointRE . search ( details ) if match : # parse as an XP struct [ 'isXP' ] = True struct . update ( match . groupdict ( ) ) return struct # try parsing as a 2-point conversion match = twoPointRE . search ( details ) if match : # parse as a 2-point conversion struct [ 'isTwoPoint' ] = True struct [ 'twoPointSuccess' ] = match . group ( 'twoPointSuccess' ) realPlay = sportsref . nfl . pbp . parse_play_details ( match . group ( 'twoPoint' ) ) if realPlay : struct . update ( realPlay ) return struct # try parsing as a pass match = passRE . search ( details ) if match : # parse as a pass struct [ 'isPass' ] = True struct . update ( match . groupdict ( ) ) return struct # try parsing as a pre-snap penalty match = psPenaltyRE . search ( details ) if match : # parse as a pre-snap penalty struct [ 'isPresnapPenalty' ] = True struct . update ( match . groupdict ( ) ) return struct # try parsing as a run match = rushRE . search ( details ) if match : # parse as a run struct [ 'isRun' ] = True struct . update ( match . groupdict ( ) ) return struct return None
Parses play details from play-by-play string and returns structured
    data.

    :details: detail string for play
    :returns: dictionary of play attributes
Parses play details from play - by - play string and returns structured data .