sportsref/nba/pbp.py
def parse_play(boxscore_id, details, is_hm):
    """Parse play details from a play-by-play string describing a play.

    Assuming valid input, this function returns structured data in a dictionary
    describing the play. If the play detail string was invalid, this function
    returns None.

    :param boxscore_id: the boxscore ID of the play
    :param details: detail string for the play
    :param is_hm: bool indicating whether the offense is at home
    :param returns: dictionary of play attributes or None if invalid
    :rtype: dictionary or None
    """
    # if input isn't a string, return None
    if not details or not isinstance(details, basestring):
        return None

    bs = sportsref.nba.BoxScore(boxscore_id)
    aw, hm = bs.away(), bs.home()
    season = sportsref.nba.Season(bs.season())
    hm_roster = set(bs.basic_stats().query('is_home == True').player_id.values)

    p = {}
    p['detail'] = details
    p['home'] = hm
    p['away'] = aw
    p['is_home_play'] = is_hm

    # parsing field goal attempts
    shotRE = (r'(?P<shooter>{0}) (?P<is_fgm>makes|misses) '
              '(?P<is_three>2|3)\-pt shot').format(PLAYER_RE)
    distRE = r' (?:from (?P<shot_dist>\d+) ft|at rim)'
    assistRE = r' \(assist by (?P<assister>{0})\)'.format(PLAYER_RE)
    blockRE = r' \(block by (?P<blocker>{0})\)'.format(PLAYER_RE)
    shotRE = r'{0}{1}(?:{2}|{3})?'.format(shotRE, distRE, assistRE, blockRE)
    m = re.match(shotRE, details, re.IGNORECASE)
    if m:
        p['is_fga'] = True
        p.update(m.groupdict())
        p['shot_dist'] = p['shot_dist'] if p['shot_dist'] is not None else 0
        p['shot_dist'] = int(p['shot_dist'])
        p['is_fgm'] = p['is_fgm'] == 'makes'
        p['is_three'] = p['is_three'] == '3'
        p['is_assist'] = pd.notnull(p.get('assister'))
        p['is_block'] = pd.notnull(p.get('blocker'))
        shooter_home = p['shooter'] in hm_roster
        p['off_team'] = hm if shooter_home else aw
        p['def_team'] = aw if shooter_home else hm
        return p

    # parsing jump balls
    jumpRE = ((r'Jump ball: (?P<away_jumper>{0}) vs\. (?P<home_jumper>{0})'
               r'(?: \((?P<gains_poss>{0}) gains possession\))?')
              .format(PLAYER_RE))
    m = re.match(jumpRE, details, re.IGNORECASE)
    if m:
        p['is_jump_ball'] = True
        p.update(m.groupdict())
        return p

    # parsing rebounds
    rebRE = (r'(?P<is_oreb>Offensive|Defensive) rebound'
             r' by (?P<rebounder>{0}|Team)').format(PLAYER_RE)
    m = re.match(rebRE, details, re.I)
    if m:
        p['is_reb'] = True
        p.update(m.groupdict())
        p['is_oreb'] = p['is_oreb'].lower() == 'offensive'
        p['is_dreb'] = not p['is_oreb']
        if p['rebounder'] == 'Team':
            p['reb_team'], other = (hm, aw) if is_hm else (aw, hm)
        else:
            reb_home = p['rebounder'] in hm_roster
            p['reb_team'], other = (hm, aw) if reb_home else (aw, hm)
        p['off_team'] = p['reb_team'] if p['is_oreb'] else other
        p['def_team'] = p['reb_team'] if p['is_dreb'] else other
        return p

    # parsing free throws
    ftRE = (r'(?P<ft_shooter>{}) (?P<is_ftm>makes|misses) '
            r'(?P<is_tech_fta>technical )?(?P<is_flag_fta>flagrant )?'
            r'(?P<is_clearpath_fta>clear path )?free throw'
            r'(?: (?P<fta_num>\d+) of (?P<tot_fta>\d+))?').format(PLAYER_RE)
    m = re.match(ftRE, details, re.I)
    if m:
        p['is_fta'] = True
        p.update(m.groupdict())
        p['is_ftm'] = p['is_ftm'] == 'makes'
        p['is_tech_fta'] = bool(p['is_tech_fta'])
        p['is_flag_fta'] = bool(p['is_flag_fta'])
        p['is_clearpath_fta'] = bool(p['is_clearpath_fta'])
        p['is_pf_fta'] = not p['is_tech_fta']
        if p['tot_fta']:
            p['tot_fta'] = int(p['tot_fta'])
        if p['fta_num']:
            p['fta_num'] = int(p['fta_num'])
        ft_home = p['ft_shooter'] in hm_roster
        p['fta_team'] = hm if ft_home else aw
        if not p['is_tech_fta']:
            p['off_team'] = hm if ft_home else aw
            p['def_team'] = aw if ft_home else hm
        return p

    # parsing substitutions
    subRE = (r'(?P<sub_in>{0}) enters the game for '
             r'(?P<sub_out>{0})').format(PLAYER_RE)
    m = re.match(subRE, details, re.I)
    if m:
        p['is_sub'] = True
        p.update(m.groupdict())
        sub_home = p['sub_in'] in hm_roster or p['sub_out'] in hm_roster
        p['sub_team'] = hm if sub_home else aw
        return p

    # parsing turnovers
    toReasons = (r'(?P<to_type>[^;]+)(?:; steal by '
                 r'(?P<stealer>{0}))?').format(PLAYER_RE)
    toRE = (r'Turnover by (?P<to_by>{}|Team) '
            r'\((?:{})\)').format(PLAYER_RE, toReasons)
    m = re.match(toRE, details, re.I)
    if m:
        p['is_to'] = True
        p.update(m.groupdict())
        p['to_type'] = p['to_type'].lower()
        if p['to_type'] == 'offensive foul':
            return None
        p['is_steal'] = pd.notnull(p['stealer'])
        p['is_travel'] = p['to_type'] == 'traveling'
        p['is_shot_clock_viol'] = p['to_type'] == 'shot clock'
        p['is_oob'] = p['to_type'] == 'step out of bounds'
        p['is_three_sec_viol'] = p['to_type'] == '3 sec'
        p['is_backcourt_viol'] = p['to_type'] == 'back court'
        p['is_off_goaltend'] = p['to_type'] == 'offensive goaltending'
        p['is_double_dribble'] = p['to_type'] == 'dbl dribble'
        p['is_discont_dribble'] = p['to_type'] == 'discontinued dribble'
        p['is_carry'] = p['to_type'] == 'palming'
        if p['to_by'] == 'Team':
            p['off_team'] = hm if is_hm else aw
            p['def_team'] = aw if is_hm else hm
        else:
            to_home = p['to_by'] in hm_roster
            p['off_team'] = hm if to_home else aw
            p['def_team'] = aw if to_home else hm
        return p

    # parsing shooting fouls
    shotFoulRE = (r'Shooting(?P<is_block_foul> block)? foul by (?P<fouler>{0})'
                  r'(?: \(drawn by (?P<drew_foul>{0})\))?').format(PLAYER_RE)
    m = re.match(shotFoulRE, details, re.I)
    if m:
        p['is_pf'] = True
        p['is_shot_foul'] = True
        p.update(m.groupdict())
        p['is_block_foul'] = bool(p['is_block_foul'])
        foul_on_home = p['fouler'] in hm_roster
        p['off_team'] = aw if foul_on_home else hm
        p['def_team'] = hm if foul_on_home else aw
        p['foul_team'] = p['def_team']
        return p

    # parsing offensive fouls
    offFoulRE = (r'Offensive(?P<is_charge> charge)? foul '
                 r'by (?P<to_by>{0})'
                 r'(?: \(drawn by (?P<drew_foul>{0})\))?').format(PLAYER_RE)
    m = re.match(offFoulRE, details, re.I)
    if m:
        p['is_pf'] = True
        p['is_off_foul'] = True
        p['is_to'] = True
        p['to_type'] = 'offensive foul'
        p.update(m.groupdict())
        p['is_charge'] = bool(p['is_charge'])
        p['fouler'] = p['to_by']
        foul_on_home = p['fouler'] in hm_roster
        p['off_team'] = hm if foul_on_home else aw
        p['def_team'] = aw if foul_on_home else hm
        p['foul_team'] = p['off_team']
        return p

    # parsing personal fouls
    foulRE = (r'Personal (?P<is_take_foul>take )?(?P<is_block_foul>block )?'
              r'foul by (?P<fouler>{0})(?: \(drawn by '
              r'(?P<drew_foul>{0})\))?').format(PLAYER_RE)
    m = re.match(foulRE, details, re.I)
    if m:
        p['is_pf'] = True
        p.update(m.groupdict())
        p['is_take_foul'] = bool(p['is_take_foul'])
        p['is_block_foul'] = bool(p['is_block_foul'])
        foul_on_home = p['fouler'] in hm_roster
        p['off_team'] = aw if foul_on_home else hm
        p['def_team'] = hm if foul_on_home else aw
        p['foul_team'] = p['def_team']
        return p

    # TODO: parsing double personal fouls
    # double_foul_re = (r'Double personal foul by (?P<fouler1>{0}) and '
    #                   r'(?P<fouler2>{0})').format(PLAYER_RE)
    # m = re.match(double_Foul_re, details, re.I)
    # if m:
    #     p['is_pf'] = True
    #     p.update(m.groupdict())
    #     p['off_team'] =

    # parsing loose ball fouls
    looseBallRE = (r'Loose ball foul by (?P<fouler>{0})'
                   r'(?: \(drawn by (?P<drew_foul>{0})\))?').format(PLAYER_RE)
    m = re.match(looseBallRE, details, re.I)
    if m:
        p['is_pf'] = True
        p['is_loose_ball_foul'] = True
        p.update(m.groupdict())
        foul_home = p['fouler'] in hm_roster
        p['foul_team'] = hm if foul_home else aw
        return p

    # parsing punching fouls
    # TODO

    # parsing away from play fouls
    awayFromBallRE = ((r'Away from play foul by (?P<fouler>{0})'
                       r'(?: \(drawn by (?P<drew_foul>{0})\))?')
                      .format(PLAYER_RE))
    m = re.match(awayFromBallRE, details, re.I)
    if m:
        p['is_pf'] = True
        p['is_away_from_play_foul'] = True
        p.update(m.groupdict())
        foul_on_home = p['fouler'] in hm_roster
        # TODO: figure out who had the ball based on previous play
        p['foul_team'] = hm if foul_on_home else aw
        return p

    # parsing inbound fouls
    inboundRE = (r'Inbound foul by (?P<fouler>{0})'
                 r'(?: \(drawn by (?P<drew_foul>{0})\))?').format(PLAYER_RE)
    m = re.match(inboundRE, details, re.I)
    if m:
        p['is_pf'] = True
        p['is_inbound_foul'] = True
        p.update(m.groupdict())
        foul_on_home = p['fouler'] in hm_roster
        p['off_team'] = aw if foul_on_home else hm
        p['def_team'] = hm if foul_on_home else aw
        p['foul_team'] = p['def_team']
        return p

    # parsing flagrant fouls
    flagrantRE = (r'Flagrant foul type (?P<flag_type>1|2) by (?P<fouler>{0})'
                  r'(?: \(drawn by (?P<drew_foul>{0})\))?').format(PLAYER_RE)
    m = re.match(flagrantRE, details, re.I)
    if m:
        p['is_pf'] = True
        p['is_flagrant'] = True
        p.update(m.groupdict())
        foul_on_home = p['fouler'] in hm_roster
        p['foul_team'] = hm if foul_on_home else aw
        return p

    # parsing clear path fouls
    clearPathRE = (r'Clear path foul by (?P<fouler>{0})'
                   r'(?: \(drawn by (?P<drew_foul>{0})\))?').format(PLAYER_RE)
    m = re.match(clearPathRE, details, re.I)
    if m:
        p['is_pf'] = True
        p['is_clear_path_foul'] = True
        p.update(m.groupdict())
        foul_on_home = p['fouler'] in hm_roster
        p['off_team'] = aw if foul_on_home else hm
        p['def_team'] = hm if foul_on_home else aw
        p['foul_team'] = p['def_team']
        return p

    # parsing timeouts
    timeoutRE = r'(?P<timeout_team>.*?) (?:full )?timeout'
    m = re.match(timeoutRE, details, re.I)
    if m:
        p['is_timeout'] = True
        p.update(m.groupdict())
        isOfficialTO = p['timeout_team'].lower() == 'official'
        name_to_id = season.team_names_to_ids()
        p['timeout_team'] = (
            'Official' if isOfficialTO else
            name_to_id.get(hm, name_to_id.get(aw, p['timeout_team']))
        )
        return p

    # parsing technical fouls
    techRE = (r'(?P<is_hanging>Hanging )?'
              r'(?P<is_taunting>Taunting )?'
              r'(?P<is_ill_def>Ill def )?'
              r'(?P<is_delay>Delay )?'
              r'(?P<is_unsport>Non unsport )?'
              r'tech(?:nical)? foul by '
              r'(?P<tech_fouler>{0}|Team)').format(PLAYER_RE)
    m = re.match(techRE, details, re.I)
    if m:
        p['is_tech_foul'] = True
        p.update(m.groupdict())
        p['is_hanging'] = bool(p['is_hanging'])
        p['is_taunting'] = bool(p['is_taunting'])
        p['is_ill_def'] = bool(p['is_ill_def'])
        p['is_delay'] = bool(p['is_delay'])
        p['is_unsport'] = bool(p['is_unsport'])
        foul_on_home = p['tech_fouler'] in hm_roster
        p['foul_team'] = hm if foul_on_home else aw
        return p

    # parsing ejections
    ejectRE = r'(?P<ejectee>{0}|Team) ejected from game'.format(PLAYER_RE)
    m = re.match(ejectRE, details, re.I)
    if m:
        p['is_ejection'] = True
        p.update(m.groupdict())
        if p['ejectee'] == 'Team':
            p['ejectee_team'] = hm if is_hm else aw
        else:
            eject_home = p['ejectee'] in hm_roster
            p['ejectee_team'] = hm if eject_home else aw
        return p

    # parsing defensive 3 seconds techs
    def3TechRE = (r'(?:Def 3 sec tech foul|Defensive three seconds)'
                  r' by (?P<tech_fouler>{})').format(PLAYER_RE)
    m = re.match(def3TechRE, details, re.I)
    if m:
        p['is_tech_foul'] = True
        p['is_def_three_secs'] = True
        p.update(m.groupdict())
        foul_on_home = p['tech_fouler'] in hm_roster
        p['off_team'] = aw if foul_on_home else hm
        p['def_team'] = hm if foul_on_home else aw
        p['foul_team'] = p['def_team']
        return p

    # parsing violations
    violRE = (r'Violation by (?P<violator>{0}|Team) '
              r'\((?P<viol_type>.*)\)').format(PLAYER_RE)
    m = re.match(violRE, details, re.I)
    if m:
        p['is_viol'] = True
        p.update(m.groupdict())
        if p['viol_type'] == 'kicked_ball':
            p['is_to'] = True
            p['to_by'] = p['violator']
        if p['violator'] == 'Team':
            p['viol_team'] = hm if is_hm else aw
        else:
            viol_home = p['violator'] in hm_roster
            p['viol_team'] = hm if viol_home else aw
        return p

    p['is_error'] = True
    return p
def parse_play(boxscore_id, details, is_hm):
    """Parse play details from a play-by-play string describing a play.

    Assuming valid input, this function returns structured data in a dictionary
    describing the play. If the play detail string was invalid, this function
    returns None.

    :param boxscore_id: the boxscore ID of the play
    :param details: detail string for the play
    :param is_hm: bool indicating whether the offense is at home
    :param returns: dictionary of play attributes or None if invalid
    :rtype: dictionary or None
    """
    # if input isn't a string, return None
    if not details or not isinstance(details, basestring):
        return None

    bs = sportsref.nba.BoxScore(boxscore_id)
    aw, hm = bs.away(), bs.home()
    season = sportsref.nba.Season(bs.season())
    hm_roster = set(bs.basic_stats().query('is_home == True').player_id.values)

    p = {}
    p['detail'] = details
    p['home'] = hm
    p['away'] = aw
    p['is_home_play'] = is_hm

    # parsing field goal attempts
    shotRE = (r'(?P<shooter>{0}) (?P<is_fgm>makes|misses) '
              '(?P<is_three>2|3)\-pt shot').format(PLAYER_RE)
    distRE = r' (?:from (?P<shot_dist>\d+) ft|at rim)'
    assistRE = r' \(assist by (?P<assister>{0})\)'.format(PLAYER_RE)
    blockRE = r' \(block by (?P<blocker>{0})\)'.format(PLAYER_RE)
    shotRE = r'{0}{1}(?:{2}|{3})?'.format(shotRE, distRE, assistRE, blockRE)
    m = re.match(shotRE, details, re.IGNORECASE)
    if m:
        p['is_fga'] = True
        p.update(m.groupdict())
        p['shot_dist'] = p['shot_dist'] if p['shot_dist'] is not None else 0
        p['shot_dist'] = int(p['shot_dist'])
        p['is_fgm'] = p['is_fgm'] == 'makes'
        p['is_three'] = p['is_three'] == '3'
        p['is_assist'] = pd.notnull(p.get('assister'))
        p['is_block'] = pd.notnull(p.get('blocker'))
        shooter_home = p['shooter'] in hm_roster
        p['off_team'] = hm if shooter_home else aw
        p['def_team'] = aw if shooter_home else hm
        return p

    # parsing jump balls
    jumpRE = ((r'Jump ball: (?P<away_jumper>{0}) vs\. (?P<home_jumper>{0})'
               r'(?: \((?P<gains_poss>{0}) gains possession\))?')
              .format(PLAYER_RE))
    m = re.match(jumpRE, details, re.IGNORECASE)
    if m:
        p['is_jump_ball'] = True
        p.update(m.groupdict())
        return p

    # parsing rebounds
    rebRE = (r'(?P<is_oreb>Offensive|Defensive) rebound'
             r' by (?P<rebounder>{0}|Team)').format(PLAYER_RE)
    m = re.match(rebRE, details, re.I)
    if m:
        p['is_reb'] = True
        p.update(m.groupdict())
        p['is_oreb'] = p['is_oreb'].lower() == 'offensive'
        p['is_dreb'] = not p['is_oreb']
        if p['rebounder'] == 'Team':
            p['reb_team'], other = (hm, aw) if is_hm else (aw, hm)
        else:
            reb_home = p['rebounder'] in hm_roster
            p['reb_team'], other = (hm, aw) if reb_home else (aw, hm)
        p['off_team'] = p['reb_team'] if p['is_oreb'] else other
        p['def_team'] = p['reb_team'] if p['is_dreb'] else other
        return p

    # parsing free throws
    ftRE = (r'(?P<ft_shooter>{}) (?P<is_ftm>makes|misses) '
            r'(?P<is_tech_fta>technical )?(?P<is_flag_fta>flagrant )?'
            r'(?P<is_clearpath_fta>clear path )?free throw'
            r'(?: (?P<fta_num>\d+) of (?P<tot_fta>\d+))?').format(PLAYER_RE)
    m = re.match(ftRE, details, re.I)
    if m:
        p['is_fta'] = True
        p.update(m.groupdict())
        p['is_ftm'] = p['is_ftm'] == 'makes'
        p['is_tech_fta'] = bool(p['is_tech_fta'])
        p['is_flag_fta'] = bool(p['is_flag_fta'])
        p['is_clearpath_fta'] = bool(p['is_clearpath_fta'])
        p['is_pf_fta'] = not p['is_tech_fta']
        if p['tot_fta']:
            p['tot_fta'] = int(p['tot_fta'])
        if p['fta_num']:
            p['fta_num'] = int(p['fta_num'])
        ft_home = p['ft_shooter'] in hm_roster
        p['fta_team'] = hm if ft_home else aw
        if not p['is_tech_fta']:
            p['off_team'] = hm if ft_home else aw
            p['def_team'] = aw if ft_home else hm
        return p

    # parsing substitutions
    subRE = (r'(?P<sub_in>{0}) enters the game for '
             r'(?P<sub_out>{0})').format(PLAYER_RE)
    m = re.match(subRE, details, re.I)
    if m:
        p['is_sub'] = True
        p.update(m.groupdict())
        sub_home = p['sub_in'] in hm_roster or p['sub_out'] in hm_roster
        p['sub_team'] = hm if sub_home else aw
        return p

    # parsing turnovers
    toReasons = (r'(?P<to_type>[^;]+)(?:; steal by '
                 r'(?P<stealer>{0}))?').format(PLAYER_RE)
    toRE = (r'Turnover by (?P<to_by>{}|Team) '
            r'\((?:{})\)').format(PLAYER_RE, toReasons)
    m = re.match(toRE, details, re.I)
    if m:
        p['is_to'] = True
        p.update(m.groupdict())
        p['to_type'] = p['to_type'].lower()
        if p['to_type'] == 'offensive foul':
            return None
        p['is_steal'] = pd.notnull(p['stealer'])
        p['is_travel'] = p['to_type'] == 'traveling'
        p['is_shot_clock_viol'] = p['to_type'] == 'shot clock'
        p['is_oob'] = p['to_type'] == 'step out of bounds'
        p['is_three_sec_viol'] = p['to_type'] == '3 sec'
        p['is_backcourt_viol'] = p['to_type'] == 'back court'
        p['is_off_goaltend'] = p['to_type'] == 'offensive goaltending'
        p['is_double_dribble'] = p['to_type'] == 'dbl dribble'
        p['is_discont_dribble'] = p['to_type'] == 'discontinued dribble'
        p['is_carry'] = p['to_type'] == 'palming'
        if p['to_by'] == 'Team':
            p['off_team'] = hm if is_hm else aw
            p['def_team'] = aw if is_hm else hm
        else:
            to_home = p['to_by'] in hm_roster
            p['off_team'] = hm if to_home else aw
            p['def_team'] = aw if to_home else hm
        return p

    # parsing shooting fouls
    shotFoulRE = (r'Shooting(?P<is_block_foul> block)? foul by (?P<fouler>{0})'
                  r'(?: \(drawn by (?P<drew_foul>{0})\))?').format(PLAYER_RE)
    m = re.match(shotFoulRE, details, re.I)
    if m:
        p['is_pf'] = True
        p['is_shot_foul'] = True
        p.update(m.groupdict())
        p['is_block_foul'] = bool(p['is_block_foul'])
        foul_on_home = p['fouler'] in hm_roster
        p['off_team'] = aw if foul_on_home else hm
        p['def_team'] = hm if foul_on_home else aw
        p['foul_team'] = p['def_team']
        return p

    # parsing offensive fouls
    offFoulRE = (r'Offensive(?P<is_charge> charge)? foul '
                 r'by (?P<to_by>{0})'
                 r'(?: \(drawn by (?P<drew_foul>{0})\))?').format(PLAYER_RE)
    m = re.match(offFoulRE, details, re.I)
    if m:
        p['is_pf'] = True
        p['is_off_foul'] = True
        p['is_to'] = True
        p['to_type'] = 'offensive foul'
        p.update(m.groupdict())
        p['is_charge'] = bool(p['is_charge'])
        p['fouler'] = p['to_by']
        foul_on_home = p['fouler'] in hm_roster
        p['off_team'] = hm if foul_on_home else aw
        p['def_team'] = aw if foul_on_home else hm
        p['foul_team'] = p['off_team']
        return p

    # parsing personal fouls
    foulRE = (r'Personal (?P<is_take_foul>take )?(?P<is_block_foul>block )?'
              r'foul by (?P<fouler>{0})(?: \(drawn by '
              r'(?P<drew_foul>{0})\))?').format(PLAYER_RE)
    m = re.match(foulRE, details, re.I)
    if m:
        p['is_pf'] = True
        p.update(m.groupdict())
        p['is_take_foul'] = bool(p['is_take_foul'])
        p['is_block_foul'] = bool(p['is_block_foul'])
        foul_on_home = p['fouler'] in hm_roster
        p['off_team'] = aw if foul_on_home else hm
        p['def_team'] = hm if foul_on_home else aw
        p['foul_team'] = p['def_team']
        return p

    # TODO: parsing double personal fouls
    # double_foul_re = (r'Double personal foul by (?P<fouler1>{0}) and '
    #                   r'(?P<fouler2>{0})').format(PLAYER_RE)
    # m = re.match(double_Foul_re, details, re.I)
    # if m:
    #     p['is_pf'] = True
    #     p.update(m.groupdict())
    #     p['off_team'] =

    # parsing loose ball fouls
    looseBallRE = (r'Loose ball foul by (?P<fouler>{0})'
                   r'(?: \(drawn by (?P<drew_foul>{0})\))?').format(PLAYER_RE)
    m = re.match(looseBallRE, details, re.I)
    if m:
        p['is_pf'] = True
        p['is_loose_ball_foul'] = True
        p.update(m.groupdict())
        foul_home = p['fouler'] in hm_roster
        p['foul_team'] = hm if foul_home else aw
        return p

    # parsing punching fouls
    # TODO

    # parsing away from play fouls
    awayFromBallRE = ((r'Away from play foul by (?P<fouler>{0})'
                       r'(?: \(drawn by (?P<drew_foul>{0})\))?')
                      .format(PLAYER_RE))
    m = re.match(awayFromBallRE, details, re.I)
    if m:
        p['is_pf'] = True
        p['is_away_from_play_foul'] = True
        p.update(m.groupdict())
        foul_on_home = p['fouler'] in hm_roster
        # TODO: figure out who had the ball based on previous play
        p['foul_team'] = hm if foul_on_home else aw
        return p

    # parsing inbound fouls
    inboundRE = (r'Inbound foul by (?P<fouler>{0})'
                 r'(?: \(drawn by (?P<drew_foul>{0})\))?').format(PLAYER_RE)
    m = re.match(inboundRE, details, re.I)
    if m:
        p['is_pf'] = True
        p['is_inbound_foul'] = True
        p.update(m.groupdict())
        foul_on_home = p['fouler'] in hm_roster
        p['off_team'] = aw if foul_on_home else hm
        p['def_team'] = hm if foul_on_home else aw
        p['foul_team'] = p['def_team']
        return p

    # parsing flagrant fouls
    flagrantRE = (r'Flagrant foul type (?P<flag_type>1|2) by (?P<fouler>{0})'
                  r'(?: \(drawn by (?P<drew_foul>{0})\))?').format(PLAYER_RE)
    m = re.match(flagrantRE, details, re.I)
    if m:
        p['is_pf'] = True
        p['is_flagrant'] = True
        p.update(m.groupdict())
        foul_on_home = p['fouler'] in hm_roster
        p['foul_team'] = hm if foul_on_home else aw
        return p

    # parsing clear path fouls
    clearPathRE = (r'Clear path foul by (?P<fouler>{0})'
                   r'(?: \(drawn by (?P<drew_foul>{0})\))?').format(PLAYER_RE)
    m = re.match(clearPathRE, details, re.I)
    if m:
        p['is_pf'] = True
        p['is_clear_path_foul'] = True
        p.update(m.groupdict())
        foul_on_home = p['fouler'] in hm_roster
        p['off_team'] = aw if foul_on_home else hm
        p['def_team'] = hm if foul_on_home else aw
        p['foul_team'] = p['def_team']
        return p

    # parsing timeouts
    timeoutRE = r'(?P<timeout_team>.*?) (?:full )?timeout'
    m = re.match(timeoutRE, details, re.I)
    if m:
        p['is_timeout'] = True
        p.update(m.groupdict())
        isOfficialTO = p['timeout_team'].lower() == 'official'
        name_to_id = season.team_names_to_ids()
        p['timeout_team'] = (
            'Official' if isOfficialTO else
            name_to_id.get(hm, name_to_id.get(aw, p['timeout_team']))
        )
        return p

    # parsing technical fouls
    techRE = (r'(?P<is_hanging>Hanging )?'
              r'(?P<is_taunting>Taunting )?'
              r'(?P<is_ill_def>Ill def )?'
              r'(?P<is_delay>Delay )?'
              r'(?P<is_unsport>Non unsport )?'
              r'tech(?:nical)? foul by '
              r'(?P<tech_fouler>{0}|Team)').format(PLAYER_RE)
    m = re.match(techRE, details, re.I)
    if m:
        p['is_tech_foul'] = True
        p.update(m.groupdict())
        p['is_hanging'] = bool(p['is_hanging'])
        p['is_taunting'] = bool(p['is_taunting'])
        p['is_ill_def'] = bool(p['is_ill_def'])
        p['is_delay'] = bool(p['is_delay'])
        p['is_unsport'] = bool(p['is_unsport'])
        foul_on_home = p['tech_fouler'] in hm_roster
        p['foul_team'] = hm if foul_on_home else aw
        return p

    # parsing ejections
    ejectRE = r'(?P<ejectee>{0}|Team) ejected from game'.format(PLAYER_RE)
    m = re.match(ejectRE, details, re.I)
    if m:
        p['is_ejection'] = True
        p.update(m.groupdict())
        if p['ejectee'] == 'Team':
            p['ejectee_team'] = hm if is_hm else aw
        else:
            eject_home = p['ejectee'] in hm_roster
            p['ejectee_team'] = hm if eject_home else aw
        return p

    # parsing defensive 3 seconds techs
    def3TechRE = (r'(?:Def 3 sec tech foul|Defensive three seconds)'
                  r' by (?P<tech_fouler>{})').format(PLAYER_RE)
    m = re.match(def3TechRE, details, re.I)
    if m:
        p['is_tech_foul'] = True
        p['is_def_three_secs'] = True
        p.update(m.groupdict())
        foul_on_home = p['tech_fouler'] in hm_roster
        p['off_team'] = aw if foul_on_home else hm
        p['def_team'] = hm if foul_on_home else aw
        p['foul_team'] = p['def_team']
        return p

    # parsing violations
    violRE = (r'Violation by (?P<violator>{0}|Team) '
              r'\((?P<viol_type>.*)\)').format(PLAYER_RE)
    m = re.match(violRE, details, re.I)
    if m:
        p['is_viol'] = True
        p.update(m.groupdict())
        if p['viol_type'] == 'kicked_ball':
            p['is_to'] = True
            p['to_by'] = p['violator']
        if p['violator'] == 'Team':
            p['viol_team'] = hm if is_hm else aw
        else:
            viol_home = p['violator'] in hm_roster
            p['viol_team'] = hm if viol_home else aw
        return p

    p['is_error'] = True
    return p
def parse_play ( boxscore_id , details , is_hm ) : # if input isn't a string, return None if not details or not isinstance ( details , basestring ) : return None bs = sportsref . nba . BoxScore ( boxscore_id ) aw , hm = bs . away ( ) , bs . home ( ) season = sportsref . nba . Season ( bs . season ( ) ) hm_roster = set ( bs . basic_stats ( ) . query ( 'is_home == True' ) . player_id . values ) p = { } p [ 'detail' ] = details p [ 'home' ] = hm p [ 'away' ] = aw p [ 'is_home_play' ] = is_hm # parsing field goal attempts shotRE = ( r'(?P<shooter>{0}) (?P<is_fgm>makes|misses) ' '(?P<is_three>2|3)\-pt shot' ) . format ( PLAYER_RE ) distRE = r' (?:from (?P<shot_dist>\d+) ft|at rim)' assistRE = r' \(assist by (?P<assister>{0})\)' . format ( PLAYER_RE ) blockRE = r' \(block by (?P<blocker>{0})\)' . format ( PLAYER_RE ) shotRE = r'{0}{1}(?:{2}|{3})?' . format ( shotRE , distRE , assistRE , blockRE ) m = re . match ( shotRE , details , re . IGNORECASE ) if m : p [ 'is_fga' ] = True p . update ( m . groupdict ( ) ) p [ 'shot_dist' ] = p [ 'shot_dist' ] if p [ 'shot_dist' ] is not None else 0 p [ 'shot_dist' ] = int ( p [ 'shot_dist' ] ) p [ 'is_fgm' ] = p [ 'is_fgm' ] == 'makes' p [ 'is_three' ] = p [ 'is_three' ] == '3' p [ 'is_assist' ] = pd . notnull ( p . get ( 'assister' ) ) p [ 'is_block' ] = pd . notnull ( p . get ( 'blocker' ) ) shooter_home = p [ 'shooter' ] in hm_roster p [ 'off_team' ] = hm if shooter_home else aw p [ 'def_team' ] = aw if shooter_home else hm return p # parsing jump balls jumpRE = ( ( r'Jump ball: (?P<away_jumper>{0}) vs\. (?P<home_jumper>{0})' r'(?: \((?P<gains_poss>{0}) gains possession\))?' ) . format ( PLAYER_RE ) ) m = re . match ( jumpRE , details , re . IGNORECASE ) if m : p [ 'is_jump_ball' ] = True p . update ( m . groupdict ( ) ) return p # parsing rebounds rebRE = ( r'(?P<is_oreb>Offensive|Defensive) rebound' r' by (?P<rebounder>{0}|Team)' ) . format ( PLAYER_RE ) m = re . match ( rebRE , details , re . I ) if m : p [ 'is_reb' ] = True p . update ( m . groupdict ( ) ) p [ 'is_oreb' ] = p [ 'is_oreb' ] . lower ( ) == 'offensive' p [ 'is_dreb' ] = not p [ 'is_oreb' ] if p [ 'rebounder' ] == 'Team' : p [ 'reb_team' ] , other = ( hm , aw ) if is_hm else ( aw , hm ) else : reb_home = p [ 'rebounder' ] in hm_roster p [ 'reb_team' ] , other = ( hm , aw ) if reb_home else ( aw , hm ) p [ 'off_team' ] = p [ 'reb_team' ] if p [ 'is_oreb' ] else other p [ 'def_team' ] = p [ 'reb_team' ] if p [ 'is_dreb' ] else other return p # parsing free throws ftRE = ( r'(?P<ft_shooter>{}) (?P<is_ftm>makes|misses) ' r'(?P<is_tech_fta>technical )?(?P<is_flag_fta>flagrant )?' r'(?P<is_clearpath_fta>clear path )?free throw' r'(?: (?P<fta_num>\d+) of (?P<tot_fta>\d+))?' ) . format ( PLAYER_RE ) m = re . match ( ftRE , details , re . I ) if m : p [ 'is_fta' ] = True p . update ( m . groupdict ( ) ) p [ 'is_ftm' ] = p [ 'is_ftm' ] == 'makes' p [ 'is_tech_fta' ] = bool ( p [ 'is_tech_fta' ] ) p [ 'is_flag_fta' ] = bool ( p [ 'is_flag_fta' ] ) p [ 'is_clearpath_fta' ] = bool ( p [ 'is_clearpath_fta' ] ) p [ 'is_pf_fta' ] = not p [ 'is_tech_fta' ] if p [ 'tot_fta' ] : p [ 'tot_fta' ] = int ( p [ 'tot_fta' ] ) if p [ 'fta_num' ] : p [ 'fta_num' ] = int ( p [ 'fta_num' ] ) ft_home = p [ 'ft_shooter' ] in hm_roster p [ 'fta_team' ] = hm if ft_home else aw if not p [ 'is_tech_fta' ] : p [ 'off_team' ] = hm if ft_home else aw p [ 'def_team' ] = aw if ft_home else hm return p # parsing substitutions subRE = ( r'(?P<sub_in>{0}) enters the game for ' r'(?P<sub_out>{0})' ) . format ( PLAYER_RE ) m = re . match ( subRE , details , re . I ) if m : p [ 'is_sub' ] = True p . update ( m . groupdict ( ) ) sub_home = p [ 'sub_in' ] in hm_roster or p [ 'sub_out' ] in hm_roster p [ 'sub_team' ] = hm if sub_home else aw return p # parsing turnovers toReasons = ( r'(?P<to_type>[^;]+)(?:; steal by ' r'(?P<stealer>{0}))?' ) . format ( PLAYER_RE ) toRE = ( r'Turnover by (?P<to_by>{}|Team) ' r'\((?:{})\)' ) . format ( PLAYER_RE , toReasons ) m = re . match ( toRE , details , re . I ) if m : p [ 'is_to' ] = True p . update ( m . groupdict ( ) ) p [ 'to_type' ] = p [ 'to_type' ] . lower ( ) if p [ 'to_type' ] == 'offensive foul' : return None p [ 'is_steal' ] = pd . notnull ( p [ 'stealer' ] ) p [ 'is_travel' ] = p [ 'to_type' ] == 'traveling' p [ 'is_shot_clock_viol' ] = p [ 'to_type' ] == 'shot clock' p [ 'is_oob' ] = p [ 'to_type' ] == 'step out of bounds' p [ 'is_three_sec_viol' ] = p [ 'to_type' ] == '3 sec' p [ 'is_backcourt_viol' ] = p [ 'to_type' ] == 'back court' p [ 'is_off_goaltend' ] = p [ 'to_type' ] == 'offensive goaltending' p [ 'is_double_dribble' ] = p [ 'to_type' ] == 'dbl dribble' p [ 'is_discont_dribble' ] = p [ 'to_type' ] == 'discontinued dribble' p [ 'is_carry' ] = p [ 'to_type' ] == 'palming' if p [ 'to_by' ] == 'Team' : p [ 'off_team' ] = hm if is_hm else aw p [ 'def_team' ] = aw if is_hm else hm else : to_home = p [ 'to_by' ] in hm_roster p [ 'off_team' ] = hm if to_home else aw p [ 'def_team' ] = aw if to_home else hm return p # parsing shooting fouls shotFoulRE = ( r'Shooting(?P<is_block_foul> block)? foul by (?P<fouler>{0})' r'(?: \(drawn by (?P<drew_foul>{0})\))?' ) . format ( PLAYER_RE ) m = re . match ( shotFoulRE , details , re . I ) if m : p [ 'is_pf' ] = True p [ 'is_shot_foul' ] = True p . update ( m . groupdict ( ) ) p [ 'is_block_foul' ] = bool ( p [ 'is_block_foul' ] ) foul_on_home = p [ 'fouler' ] in hm_roster p [ 'off_team' ] = aw if foul_on_home else hm p [ 'def_team' ] = hm if foul_on_home else aw p [ 'foul_team' ] = p [ 'def_team' ] return p # parsing offensive fouls offFoulRE = ( r'Offensive(?P<is_charge> charge)? foul ' r'by (?P<to_by>{0})' r'(?: \(drawn by (?P<drew_foul>{0})\))?' ) . format ( PLAYER_RE ) m = re . match ( offFoulRE , details , re . I ) if m : p [ 'is_pf' ] = True p [ 'is_off_foul' ] = True p [ 'is_to' ] = True p [ 'to_type' ] = 'offensive foul' p . update ( m . groupdict ( ) ) p [ 'is_charge' ] = bool ( p [ 'is_charge' ] ) p [ 'fouler' ] = p [ 'to_by' ] foul_on_home = p [ 'fouler' ] in hm_roster p [ 'off_team' ] = hm if foul_on_home else aw p [ 'def_team' ] = aw if foul_on_home else hm p [ 'foul_team' ] = p [ 'off_team' ] return p # parsing personal fouls foulRE = ( r'Personal (?P<is_take_foul>take )?(?P<is_block_foul>block )?' r'foul by (?P<fouler>{0})(?: \(drawn by ' r'(?P<drew_foul>{0})\))?' ) . format ( PLAYER_RE ) m = re . match ( foulRE , details , re . I ) if m : p [ 'is_pf' ] = True p . update ( m . groupdict ( ) ) p [ 'is_take_foul' ] = bool ( p [ 'is_take_foul' ] ) p [ 'is_block_foul' ] = bool ( p [ 'is_block_foul' ] ) foul_on_home = p [ 'fouler' ] in hm_roster p [ 'off_team' ] = aw if foul_on_home else hm p [ 'def_team' ] = hm if foul_on_home else aw p [ 'foul_team' ] = p [ 'def_team' ] return p # TODO: parsing double personal fouls # double_foul_re = (r'Double personal foul by (?P<fouler1>{0}) and ' #                   r'(?P<fouler2>{0})').format(PLAYER_RE) # m = re.match(double_Foul_re, details, re.I) # if m: #     p['is_pf'] = True #     p.update(m.groupdict()) #     p['off_team'] = # parsing loose ball fouls looseBallRE = ( r'Loose ball foul by (?P<fouler>{0})' r'(?: \(drawn by (?P<drew_foul>{0})\))?' ) . format ( PLAYER_RE ) m = re . match ( looseBallRE , details , re . I ) if m : p [ 'is_pf' ] = True p [ 'is_loose_ball_foul' ] = True p . update ( m . groupdict ( ) ) foul_home = p [ 'fouler' ] in hm_roster p [ 'foul_team' ] = hm if foul_home else aw return p # parsing punching fouls # TODO # parsing away from play fouls awayFromBallRE = ( ( r'Away from play foul by (?P<fouler>{0})' r'(?: \(drawn by (?P<drew_foul>{0})\))?' ) . format ( PLAYER_RE ) ) m = re . match ( awayFromBallRE , details , re . I ) if m : p [ 'is_pf' ] = True p [ 'is_away_from_play_foul' ] = True p . update ( m . groupdict ( ) ) foul_on_home = p [ 'fouler' ] in hm_roster # TODO: figure out who had the ball based on previous play p [ 'foul_team' ] = hm if foul_on_home else aw return p # parsing inbound fouls inboundRE = ( r'Inbound foul by (?P<fouler>{0})' r'(?: \(drawn by (?P<drew_foul>{0})\))?' ) . format ( PLAYER_RE ) m = re . match ( inboundRE , details , re . I ) if m : p [ 'is_pf' ] = True p [ 'is_inbound_foul' ] = True p . update ( m . groupdict ( ) ) foul_on_home = p [ 'fouler' ] in hm_roster p [ 'off_team' ] = aw if foul_on_home else hm p [ 'def_team' ] = hm if foul_on_home else aw p [ 'foul_team' ] = p [ 'def_team' ] return p # parsing flagrant fouls flagrantRE = ( r'Flagrant foul type (?P<flag_type>1|2) by (?P<fouler>{0})' r'(?: \(drawn by (?P<drew_foul>{0})\))?' ) . format ( PLAYER_RE ) m = re . match ( flagrantRE , details , re . I ) if m : p [ 'is_pf' ] = True p [ 'is_flagrant' ] = True p . update ( m . groupdict ( ) ) foul_on_home = p [ 'fouler' ] in hm_roster p [ 'foul_team' ] = hm if foul_on_home else aw return p # parsing clear path fouls clearPathRE = ( r'Clear path foul by (?P<fouler>{0})' r'(?: \(drawn by (?P<drew_foul>{0})\))?' ) . format ( PLAYER_RE ) m = re . match ( clearPathRE , details , re . I ) if m : p [ 'is_pf' ] = True p [ 'is_clear_path_foul' ] = True p . update ( m . groupdict ( ) ) foul_on_home = p [ 'fouler' ] in hm_roster p [ 'off_team' ] = aw if foul_on_home else hm p [ 'def_team' ] = hm if foul_on_home else aw p [ 'foul_team' ] = p [ 'def_team' ] return p # parsing timeouts timeoutRE = r'(?P<timeout_team>.*?) (?:full )?timeout' m = re . match ( timeoutRE , details , re . I ) if m : p [ 'is_timeout' ] = True p . update ( m . groupdict ( ) ) isOfficialTO = p [ 'timeout_team' ] . lower ( ) == 'official' name_to_id = season . team_names_to_ids ( ) p [ 'timeout_team' ] = ( 'Official' if isOfficialTO else name_to_id . get ( hm , name_to_id . get ( aw , p [ 'timeout_team' ] ) ) ) return p # parsing technical fouls techRE = ( r'(?P<is_hanging>Hanging )?' r'(?P<is_taunting>Taunting )?' r'(?P<is_ill_def>Ill def )?' r'(?P<is_delay>Delay )?' r'(?P<is_unsport>Non unsport )?' r'tech(?:nical)? foul by ' r'(?P<tech_fouler>{0}|Team)' ) . format ( PLAYER_RE ) m = re . match ( techRE , details , re . I ) if m : p [ 'is_tech_foul' ] = True p . update ( m . groupdict ( ) ) p [ 'is_hanging' ] = bool ( p [ 'is_hanging' ] ) p [ 'is_taunting' ] = bool ( p [ 'is_taunting' ] ) p [ 'is_ill_def' ] = bool ( p [ 'is_ill_def' ] ) p [ 'is_delay' ] = bool ( p [ 'is_delay' ] ) p [ 'is_unsport' ] = bool ( p [ 'is_unsport' ] ) foul_on_home = p [ 'tech_fouler' ] in hm_roster p [ 'foul_team' ] = hm if foul_on_home else aw return p # parsing ejections ejectRE = r'(?P<ejectee>{0}|Team) ejected from game' . format ( PLAYER_RE ) m = re . match ( ejectRE , details , re . I ) if m : p [ 'is_ejection' ] = True p . update ( m . groupdict ( ) ) if p [ 'ejectee' ] == 'Team' : p [ 'ejectee_team' ] = hm if is_hm else aw else : eject_home = p [ 'ejectee' ] in hm_roster p [ 'ejectee_team' ] = hm if eject_home else aw return p # parsing defensive 3 seconds techs def3TechRE = ( r'(?:Def 3 sec tech foul|Defensive three seconds)' r' by (?P<tech_fouler>{})' ) . format ( PLAYER_RE ) m = re . match ( def3TechRE , details , re . I ) if m : p [ 'is_tech_foul' ] = True p [ 'is_def_three_secs' ] = True p . update ( m . groupdict ( ) ) foul_on_home = p [ 'tech_fouler' ] in hm_roster p [ 'off_team' ] = aw if foul_on_home else hm p [ 'def_team' ] = hm if foul_on_home else aw p [ 'foul_team' ] = p [ 'def_team' ] return p # parsing violations violRE = ( r'Violation by (?P<violator>{0}|Team) ' r'\((?P<viol_type>.*)\)' ) . format ( PLAYER_RE ) m = re . match ( violRE , details , re . I ) if m : p [ 'is_viol' ] = True p . update ( m . groupdict ( ) ) if p [ 'viol_type' ] == 'kicked_ball' : p [ 'is_to' ] = True p [ 'to_by' ] = p [ 'violator' ] if p [ 'violator' ] == 'Team' : p [ 'viol_team' ] = hm if is_hm else aw else : viol_home = p [ 'violator' ] in hm_roster p [ 'viol_team' ] = hm if viol_home else aw return p p [ 'is_error' ] = True return p
Parse play details from a play-by-play string describing a play.

    Assuming valid input, this function returns structured data in a dictionary
    describing the play. If the play detail string was invalid, this function
    returns None.

    :param boxscore_id: the boxscore ID of the play
    :param details: detail string for the play
    :param is_hm: bool indicating whether the offense is at home
    :param returns: dictionary of play attributes or None if invalid
    :rtype: dictionary or None
Parse play details from a play - by - play string describing a play .