import csv, sys, json, glob, re from os import path from bs4 import BeautifulSoup as bs4 class JFRBidding: # alignment of the bidding table __directions = ['W', 'N', 'E', 'S'] # converts csv file to list of row lists def __csv_to_list(self, file_path): file_data = [] with file(file_path) as csv_file: csv_data = csv.reader(csv_file) for line in csv_data: file_data.append(line) return file_data # converts CSV lineup data to {round}.{sector}_{table}.{pair numbers} structure def __parse_lineup_data(self, sitting_data): round_lineups = {} for sitting in sitting_data[1:]: round_no = int(sitting[2]) table_no = sitting[0] + '_' + sitting[1] if not round_lineups.has_key(round_no): round_lineups[round_no] = {} round_lineups[round_no][table_no] = sorted([int(sitting[3]), int(sitting[4])]) return round_lineups # converts CSV bidding to {board}.{sector}_{table}.{round}.{bidding}[] structure, # including erased calls def __parse_bidding_data(self, bidding_data): bids = {} for bid in bidding_data[1:]: board_no = int(bid[4]) round_no = int(bid[3]) table_no = bid[1] + '_' + bid[2] bid_counter = int(bid[5]) bid_erased = int(bid[10]) if not bids.has_key(board_no): bids[board_no] = {} if not bids[board_no].has_key(table_no): bids[board_no][table_no] = {} if not bids[board_no][table_no].has_key(round_no): bids[board_no][table_no][round_no] = {} if bid_erased == 1: if bids[board_no][table_no][round_no].has_key(bid_counter): if bids[board_no][table_no][round_no][bid_counter]['direction'] == bid[6]: bids[board_no][table_no][round_no].pop(bid_counter, None) else: bids[board_no][table_no][round_no][bid_counter] = {'direction': bid[6], 'bid': bid[7] } return bids # converts bidding data into HTML table def __format_bidding(self, bidding): html_output = '' html_output = html_output + '' for dir in self.__directions: html_output = html_output + '' html_output = html_output + '' for bid_round in bidding: html_output = html_output + '' for bid in bid_round: bid_match = re.match(r'(\d)([SHDCN])', bid) if bid_match: bid = bid_match.group(1) + '' html_output = html_output + '' html_output = html_output + '' html_output = html_output + '
' + dir + '
' + bid + '
' return html_output # returns file path for bidding HTML output # {prefix}_bidding_{jfr_board_number}_{pair_numbers}.txt def __get_bidding_file_output_path(self, board_no, round_no=None, table_no=None, pair_numbers=None): return '{0}_bidding_{1:03}_{2}.txt'.format( self.__tournament_prefix, board_no, '_'.join( map(str, self.__round_lineups[round_no][table_no] if pair_numbers is None # read pair numbers from lineup else pair_numbers) # or use numbers provided, e.g. from JFR HTML ) ) def __map_board_numbers(self, custom_mapping=None): self.__tournament_files = [f for f in glob.glob(self.__tournament_prefix + '*.html') if re.search(self.__tournament_files_match, f)] if custom_mapping is not None: custom_files = [] for jfr_number in range(custom_mapping[0], custom_mapping[1]+1): # only include these board numbers from custom mapping which actually exist in JFP output board_files = [f for f in self.__tournament_files if f.endswith('{0:03}.html'.format(jfr_number))] if len(board_files): self.__board_number_mapping[jfr_number - custom_mapping[0] + custom_mapping[2]] = jfr_number custom_files = custom_files + board_files self.__tournament_files = custom_files else: for tournament_file in self.__tournament_files: # scan for all JFR board HTML files and read actual board numbers from HTML headers file_number = re.match(self.__tournament_files_match, tournament_file).group(1) with file(tournament_file, 'r+') as board_html: board_content = bs4(board_html, from_encoding='utf-8') # first found

element should be actual board number board_number = re.sub('[^0-9]', '', board_content.select('h4')[0].contents[0].strip()) self.__board_number_mapping[int(board_number, 10)] = int(file_number, 10) # sitting read from BWS __round_lineups = {} # bidding read from BWS __bids = {} # full path + JFR prefix __tournament_prefix = '' # RegEx matching board HTML files __tournament_files_match = None # matched files, including board number mapping boundaries __tournament_files = [] # BWS number -> JFR number mapping __board_number_mapping = {} def __init__(self, bidding_file, lineup_file, file_prefix, board_mapping): self.__round_lineups = self.__parse_lineup_data(self.__csv_to_list(lineup_file)) self.__bids = self.__parse_bidding_data(self.__csv_to_list(bidding_file)) self.__tournament_prefix = path.splitext(path.realpath(file_prefix + '.html'))[0] self.__tournament_files_match = re.compile(re.escape(self.__tournament_prefix) + '([0-9]{3})\.html') self.__map_board_numbers(board_mapping) def write_bidding_tables(self): for board_no, board_data in self.__bids.items(): if board_no in self.__board_number_mapping: for table_no, table_data in board_data.items(): for round_no, round_data in table_data.items(): bidding = sorted(round_data) dealer = round_data[bidding[0]]['direction'] bidding_table = [[], [], [], []] # compile bidding player-by-player for bid_index in bidding: bid = round_data[bid_index] bidding_table[self.__directions.index(bid['direction'])].append(bid['bid']) last_bidder = bid['direction'] # fill skipped calls for players before dealer in the first round for pos in range(0, self.__directions.index(dealer)): bidding_table[pos].insert(0, '') # fill skipped calls for players after pass out (so that bidding table is a proper matrix) for pos in range(self.__directions.index(last_bidder), len(self.__directions)): bidding_table[pos].append('') # transpose the bidding table, align it row-by-row (bidding round-by-round) bidding_table = map(list, zip(*bidding_table)) bidding_file_path = self.__get_bidding_file_output_path(self.__board_number_mapping[board_no], round_no, table_no) with file(bidding_file_path, 'w') as bidding_file: bidding_file.write(self.__format_bidding(bidding_table)) def write_bidding_scripts(self): for tournament_file in self.__tournament_files: with file(tournament_file, 'r+') as board_html: board_content = bs4(board_html, from_encoding='utf-8') header_scripts = board_content.select('head script') # check for jQuery, append if necessary jquery_scripts = [script for script in header_scripts if script['src'] == 'javas/jquery.js'] if not len(jquery_scripts): jquery_scripts.append(bs4('').script) board_content.head.append(jquery_scripts[0]) # check for bidding.js bidding_scripts = [script for script in header_scripts if script['src'] == 'javas/bidding.js'] # and make sure bidding.js is appended after jQuery for script in bidding_scripts: script.extract() jquery_scripts[0].insert_after(bs4('').script) board_html.seek(0) board_html.write(board_content.prettify('utf-8', formatter='html')) board_html.truncate() def write_bidding_links(self): for tournament_file in self.__tournament_files: file_number = re.match(self.__tournament_files_match, tournament_file).group(1) board_text_path = path.splitext(tournament_file)[0] + '.txt' with file(board_text_path, 'r+') as board_text: board_text_content = bs4(board_text, from_encoding='iso-8859-2') for row in board_text_content.select('tr'): cells = row.select('td') # traveller table rows for specific score entries should have 11 cells if len(cells) == 11: pair_numbers = sorted([int(cells[1].contents[0]), int(cells[2].contents[0])]) bidding_link = bs4('[lic]') bidding_link.a['data-bidding-link'] = path.basename(self.__get_bidding_file_output_path( int(file_number, 10), pair_numbers=pair_numbers )) # fourth cell is the contract for link in cells[3].select('a.biddingLink'): link.extract() cells[3].append(bidding_link) board_text.seek(0) board_text.write(board_text_content.table.prettify('iso-8859-2', formatter='html')) board_text.truncate() bidding_parser = JFRBidding(bidding_file=sys.argv[1], lineup_file=sys.argv[2], file_prefix=sys.argv[3], board_mapping=map(int, sys.argv[4:]) if len(sys.argv) > 4 else None) bidding_parser.write_bidding_tables() bidding_parser.write_bidding_scripts() bidding_parser.write_bidding_links()