From 4b74897708cd7ae82bd1c7d4096450f671a45021 Mon Sep 17 00:00:00 2001 From: AlphaSheep Date: Fri, 27 Oct 2023 20:29:52 +0200 Subject: [PATCH 1/8] Update seed data for records to include a tie --- server/seed-data/cuberecords.json | 36 ++++++++++++++++--------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/server/seed-data/cuberecords.json b/server/seed-data/cuberecords.json index ced8cb10..f4447628 100644 --- a/server/seed-data/cuberecords.json +++ b/server/seed-data/cuberecords.json @@ -1,17 +1,19 @@ -[{"_id":"5887ae0d92633d12021d1353","singleId":"2015SUTH01","averageName":"Kenneth Sutherland","averageResult":"7.22","eventName":"3x3x3Cube","singleResult":"5.58","averageId":"2015SUTH01","singleName":"Kenneth Sutherland","eventId":"333","eventRank":10,"averageDate":"2022-08-28T00:00:00.000Z","averageResultRaw":722,"singleResultRaw":558,"singleDate":"2022-08-28T00:00:00.000Z"}, -{"_id":"5887ae0d92633d12021d1354","singleId":"2010BRUC05","averageName":"Liza Rowe","averageResult":"1.94","eventName":"2x2x2Cube","singleResult":"1.34","averageId":"2015ROWE03","singleName":"Rakesh Bruce","eventId":"222","eventRank":20,"averageDate":"2022-11-19T00:00:00.000Z","averageResultRaw":194,"singleResultRaw":134,"singleDate":"2022-03-06T00:00:00.000Z"}, -{"_id":"5887ae0d92633d12021d1355","singleId":"2015SUTH01","averageName":"Kenneth Sutherland","averageResult":"29.72","eventName":"4x4x4Cube","singleResult":"27.31","averageId":"2015SUTH01","singleName":"Kenneth Sutherland","eventId":"444","eventRank":30,"averageDate":"2022-10-07T00:00:00.000Z","averageResultRaw":2972,"singleResultRaw":2731,"singleDate":"2022-10-07T00:00:00.000Z"}, -{"_id":"5887ae0d92633d12021d1356","singleId":"2015SUTH01","averageName":"Kenneth Sutherland","averageResult":"1:02.63","eventName":"5x5x5Cube","singleResult":"54.17","averageId":"2015SUTH01","singleName":"Kenneth Sutherland","eventId":"555","eventRank":40,"averageDate":"2222-11-12T00:00:00.000Z","averageResultRaw":6263,"singleResultRaw":5417,"singleDate":"2022-06-03T00:00:00.000Z"}, -{"_id":"5887ae0d92633d12021d1357","singleId":"2015SUTH01","averageName":"Kenneth Sutherland","averageResult":"1:55.54","eventName":"6x6x6Cube","singleResult":"1:46.82","averageId":"2015SUTH01","singleName":"Kenneth Sutherland","eventId":"666","eventRank":50,"averageDate":"2022-11-12T00:00:00.000Z","averageResultRaw":11554,"singleResultRaw":10682,"singleDate":"2022-11-12T00:00:00.000Z"}, -{"_id":"5887ae0d92633d12021d1358","singleId":"2015ROWE03","averageName":"Denver Kriek","averageResult":"3:02.60","eventName":"7x7x7Cube","singleResult":"2:52.73","averageId":"2011KRIE02","singleName":"Liza Rowe","eventId":"777","eventRank":60,"averageDate":"2022-11-12T00:00:00.000Z","averageResultRaw":18260,"singleResultRaw":17273,"singleDate":"2022-10-07T00:00:00.000Z"}, -{"_id":"5887ae0d92633d12021d1359","singleId":"2012STOR01","averageName":"Rose Storm","averageResult":"45.46","eventName":"3x3x3Blindfolded","singleResult":"30.38","averageId":"2012STOR01","singleName":"Rose Storm","eventId":"333bf","eventRank":70,"averageDate":"2022-10-07T00:00:00.000Z","averageResultRaw":4546,"singleResultRaw":3038,"singleDate":"2022-03-06T00:00:00.000Z"}, -{"_id":"5887ae0d92633d12021d135a","singleId":"2014JOHN04","averageName":"Claudine Johnson","averageResult":"28.0","eventName":"3x3x3FewestMoves","singleResult":"23","averageId":"2014JOHN04","singleName":"Claudine Johnson","eventId":"333fm","eventRank":80,"averageDate":"2019-07-11T00:00:00.000Z","averageResultRaw":2800,"singleResultRaw":23,"singleDate":"2019-11-08T00:00:00.000Z"}, -{"_id":"5887ae0d92633d12021d135b","singleId":"2022DUMA05","averageName":"Larry Hoffmann","averageResult":"12.64","eventName":"3x3x3One-Handed","singleResult":"10.72","averageId":"2018HOFF01","singleName":"Luvuyo Duma","eventId":"333oh","eventRank":90,"averageDate":"2022-06-03T00:00:00.000Z","averageResultRaw":1264,"singleResultRaw":1072,"singleDate":"2222-10-07T00:00:00.000Z"}, -{"_id":"5887ae0d92633d12021d135d","singleId":"2021SHON01","averageName":"Mohamed Shongwe","averageResult":"44.60","eventName":"Megaminx","singleResult":"37.95","averageId":"2021SHON01","singleName":"Mohamed Shongwe","eventId":"minx","eventRank":120,"averageDate":"2022-11-19T00:00:00.000Z","averageResultRaw":4460,"singleResultRaw":3795,"singleDate":"2022-08-28T00:00:00.000Z"}, -{"_id":"5887ae0d92633d12021d135e","singleId":"2015ROWE03","averageName":"Liza Rowe","averageResult":"4.01","eventName":"Pyraminx","singleResult":"2.58","averageId":"2015ROWE03","singleName":"Liza Rowe","eventId":"pyram","eventRank":130,"averageDate":"2022-05-15T00:00:00.000Z","averageResultRaw":401,"singleResultRaw":258,"singleDate":"2016-11-27T00:00:00.000Z"}, -{"_id":"5887ae0d92633d12021d135f","singleId":"2017ZYLP03","averageName":"Priya van Zyl","averageResult":"8.15","eventName":"Clock","singleResult":"6.15","averageId":"2017ZYLP03","singleName":"Priya van Zyl","eventId":"clock","eventRank":110,"averageDate":"2016-07-15T00:00:00.000Z","averageResultRaw":815,"singleResultRaw":615,"singleDate":"2017-11-25T00:00:00.000Z"}, -{"_id":"5887ae0d92633d12021d1360","singleId":"2014CRAI02","averageName":"Liza Rowe","averageResult":"4.68","eventName":"Skewb","singleResult":"2.83","averageId":"2015ROWE03","singleName":"Pumla Craig","eventId":"skewb","eventRank":140,"averageDate":"2022-07-30T00:00:00.000Z","averageResultRaw":468,"singleResultRaw":283,"singleDate":"2016-04-02T00:00:00.000Z"}, -{"_id":"5887ae0d92633d12021d1361","singleId":"2011CROU04","averageName":"Lucy Crous","averageResult":"10.55","eventName":"Square-1","singleResult":"8.43","averageId":"2011CROU04","singleName":"Lucy Crous","eventId":"sq1","eventRank":150,"averageDate":"2022-10-07T00:00:00.000Z","averageResultRaw":1055,"singleResultRaw":843,"singleDate":"2022-10-07T00:00:00.000Z"}, -{"_id":"5887ae0d92633d12021d1362","singleId":"2012STOR01","averageName":"Claudine Johnson","averageResult":"7:41.35","eventName":"4x4x4Blindfolded","singleResult":"6:31.89","averageId":"2014JOHN04","singleName":"Rose Storm","eventId":"444bf","eventRank":160,"averageDate":"2022-10-07T00:00:00.000Z","averageResultRaw":46135,"singleResultRaw":39189,"singleDate":"2022-10-07T00:00:00.000Z"}, -{"_id":"5887ae0d92633d12021d1363","singleId":"2014JOHN04","averageName":null,"averageResult":"","eventName":"5x5x5Blindfolded","singleResult":"19:05.00","averageId":null,"singleName":"Claudine Johnson","eventId":"555bf","eventRank":170,"averageDate":null,"averageResultRaw":null,"singleResultRaw":114500,"singleDate":"2022-10-07T00:00:00.000Z"}, -{"_id":"5887ae0d92633d12021d1364","singleId":"2012STOR01","averageName":null,"averageResult":"","eventName":"3x3x3Multi-Blind","singleResult":"21/28 60:00","averageId":null,"singleName":"Rose Storm","eventId":"333mbf","eventRank":180,"averageDate":null,"averageResultRaw":null,"singleResultRaw":850360007,"singleDate":"2022-10-07T00:00:00.000Z"}] +[ +{"_id":"5887ae0d92633d12021d1353","singleId":["2015SUTH01"],"averageName":["Kenneth Sutherland"],"averageResult":"7.22","eventName":"3x3x3Cube","singleResult":"5.58","averageId":["2015SUTH01"],"singleName":["Kenneth Sutherland"],"eventId":"333","eventRank":10,"averageDate":["2022-08-28T00:00:00.000Z"],"averageResultRaw":722,"singleResultRaw":558,"singleDate":["2022-08-28T00:00:00.000Z"]}, +{"_id":"5887ae0d92633d12021d1354","singleId":["2010BRUC05"],"averageName":["Liza Rowe"],"averageResult":"1.94","eventName":"2x2x2Cube","singleResult":"1.34","averageId":["2015ROWE03"],"singleName":["Rakesh Bruce"],"eventId":"222","eventRank":20,"averageDate":["2022-11-19T00:00:00.000Z"],"averageResultRaw":194,"singleResultRaw":134,"singleDate":["2022-03-06T00:00:00.000Z"]}, +{"_id":"5887ae0d92633d12021d1355","singleId":["2015SUTH01"],"averageName":["Kenneth Sutherland"],"averageResult":"29.72","eventName":"4x4x4Cube","singleResult":"27.31","averageId":["2015SUTH01"],"singleName":["Kenneth Sutherland"],"eventId":"444","eventRank":30,"averageDate":["2022-10-07T00:00:00.000Z"],"averageResultRaw":2972,"singleResultRaw":2731,"singleDate":["2022-10-07T00:00:00.000Z"]}, +{"_id":"5887ae0d92633d12021d1356","singleId":["2015SUTH01"],"averageName":["Kenneth Sutherland"],"averageResult":"1:02.63","eventName":"5x5x5Cube","singleResult":"54.17","averageId":["2015SUTH01"],"singleName":["Kenneth Sutherland"],"eventId":"555","eventRank":40,"averageDate":["2222-11-12T00:00:00.000Z"],"averageResultRaw":6263,"singleResultRaw":5417,"singleDate":["2022-06-03T00:00:00.000Z"]}, +{"_id":"5887ae0d92633d12021d1357","singleId":["2015SUTH01"],"averageName":["Kenneth Sutherland"],"averageResult":"1:55.54","eventName":"6x6x6Cube","singleResult":"1:46.82","averageId":["2015SUTH01"],"singleName":["Kenneth Sutherland"],"eventId":"666","eventRank":50,"averageDate":["2022-11-12T00:00:00.000Z"],"averageResultRaw":11554,"singleResultRaw":10682,"singleDate":["2022-11-12T00:00:00.000Z"]}, +{"_id":"5887ae0d92633d12021d1358","singleId":["2015ROWE03"],"averageName":["Denver Kriek"],"averageResult":"3:02.60","eventName":"7x7x7Cube","singleResult":"2:52.73","averageId":["2011KRIE02"],"singleName":["Liza Rowe"],"eventId":"777","eventRank":60,"averageDate":["2022-11-12T00:00:00.000Z"],"averageResultRaw":18260,"singleResultRaw":17273,"singleDate":["2022-10-07T00:00:00.000Z"]}, +{"_id":"5887ae0d92633d12021d1359","singleId":["2012STOR01"],"averageName":["Rose Storm"],"averageResult":"45.46","eventName":"3x3x3Blindfolded","singleResult":"30.38","averageId":["2012STOR01"],"singleName":["Rose Storm"],"eventId":"333bf","eventRank":70,"averageDate":["2022-10-07T00:00:00.000Z"],"averageResultRaw":4546,"singleResultRaw":3038,"singleDate":["2022-03-06T00:00:00.000Z"]}, +{"_id":"5887ae0d92633d12021d135a","singleId":["2014JOHN04", "2015ROWE03"],"averageName":["Claudine Johnson"],"averageResult":"28.0","eventName":"3x3x3FewestMoves","singleResult":"23","averageId":["2014JOHN04"],"singleName":["Claudine Johnson", "Liza Rowe"],"eventId":"333fm","eventRank":80,"averageDate":["2019-07-11T00:00:00.000Z"],"averageResultRaw":2800,"singleResultRaw":23,"singleDate":["2019-11-08T00:00:00.000Z", "2020-12-09T00:00:00.000Z"]}, +{"_id":"5887ae0d92633d12021d135b","singleId":["2022DUMA05"],"averageName":["Larry Hoffmann"],"averageResult":"12.64","eventName":"3x3x3One-Handed","singleResult":"10.72","averageId":["2018HOFF01"],"singleName":["Luvuyo Duma"],"eventId":"333oh","eventRank":90,"averageDate":["2022-06-03T00:00:00.000Z"],"averageResultRaw":1264,"singleResultRaw":1072,"singleDate":["2222-10-07T00:00:00.000Z"]}, +{"_id":"5887ae0d92633d12021d135d","singleId":["2021SHON01"],"averageName":["Mohamed Shongwe"],"averageResult":"44.60","eventName":"Megaminx","singleResult":"37.95","averageId":["2021SHON01"],"singleName":["Mohamed Shongwe"],"eventId":"minx","eventRank":120,"averageDate":["2022-11-19T00:00:00.000Z"],"averageResultRaw":4460,"singleResultRaw":3795,"singleDate":["2022-08-28T00:00:00.000Z"]}, +{"_id":"5887ae0d92633d12021d135e","singleId":["2015ROWE03"],"averageName":["Liza Rowe"],"averageResult":"4.01","eventName":"Pyraminx","singleResult":"2.58","averageId":["2015ROWE03"],"singleName":["Liza Rowe"],"eventId":"pyram","eventRank":130,"averageDate":["2022-05-15T00:00:00.000Z"],"averageResultRaw":401,"singleResultRaw":258,"singleDate":["2016-11-27T00:00:00.000Z"]}, +{"_id":"5887ae0d92633d12021d135f","singleId":["2017ZYLP03"],"averageName":["Priya van Zyl"],"averageResult":"8.15","eventName":"Clock","singleResult":"6.15","averageId":["2017ZYLP03"],"singleName":["Priya van Zyl"],"eventId":"clock","eventRank":110,"averageDate":["2016-07-15T00:00:00.000Z"],"averageResultRaw":815,"singleResultRaw":615,"singleDate":["2017-11-25T00:00:00.000Z"]}, +{"_id":"5887ae0d92633d12021d1360","singleId":["2014CRAI02"],"averageName":["Liza Rowe"],"averageResult":"4.68","eventName":"Skewb","singleResult":"2.83","averageId":["2015ROWE03"],"singleName":["Pumla Craig"],"eventId":"skewb","eventRank":140,"averageDate":["2022-07-30T00:00:00.000Z"],"averageResultRaw":468,"singleResultRaw":283,"singleDate":["2016-04-02T00:00:00.000Z"]}, +{"_id":"5887ae0d92633d12021d1361","singleId":["2011CROU04"],"averageName":["Lucy Crous"],"averageResult":"10.55","eventName":"Square-1","singleResult":"8.43","averageId":["2011CROU04"],"singleName":["Lucy Crous"],"eventId":"sq1","eventRank":150,"averageDate":["2022-10-07T00:00:00.000Z"],"averageResultRaw":1055,"singleResultRaw":843,"singleDate":["2022-10-07T00:00:00.000Z"]}, +{"_id":"5887ae0d92633d12021d1362","singleId":["2012STOR01"],"averageName":["Claudine Johnson"],"averageResult":"7:41.35","eventName":"4x4x4Blindfolded","singleResult":"6:31.89","averageId":["2014JOHN04"],"singleName":["Rose Storm"],"eventId":"444bf","eventRank":160,"averageDate":["2022-10-07T00:00:00.000Z"],"averageResultRaw":46135,"singleResultRaw":39189,"singleDate":["2022-10-07T00:00:00.000Z"]}, +{"_id":"5887ae0d92633d12021d1363","singleId":["2014JOHN04"],"averageName":[],"averageResult":"","eventName":"5x5x5Blindfolded","singleResult":"19:05.00","averageId":[],"singleName":["Claudine Johnson"],"eventId":"555bf","eventRank":170,"averageDate":null,"averageResultRaw":null,"singleResultRaw":114500,"singleDate":["2022-10-07T00:00:00.000Z"]}, +{"_id":"5887ae0d92633d12021d1364","singleId":["2012STOR01"],"averageName":[],"averageResult":"","eventName":"3x3x3Multi-Blind","singleResult":"21/28 60:00","averageId":[],"singleName":["Rose Storm"],"eventId":"333mbf","eventRank":180,"averageDate":null,"averageResultRaw":null,"singleResultRaw":850360007,"singleDate":["2022-10-07T00:00:00.000Z"]} +] From d6d4e698e915a8985618c5d24d9d23c3bb49772a Mon Sep 17 00:00:00 2001 From: AlphaSheep Date: Fri, 27 Oct 2023 20:30:58 +0200 Subject: [PATCH 2/8] Refactor record update ETL script and add handling for tied records --- etl/record_updater.py | 284 ++++++++++++++++++++++++++---------------- 1 file changed, 180 insertions(+), 104 deletions(-) diff --git a/etl/record_updater.py b/etl/record_updater.py index 43c3d5ec..8d89307f 100755 --- a/etl/record_updater.py +++ b/etl/record_updater.py @@ -11,130 +11,206 @@ EXCLUDE_EVENTS = ["333mbo", "magic", "mmagic", "333ft"] +PREP_ZAPEOPLE_SQL = """ + CREATE TABLE ZAPeople AS + SELECT + id, + name + FROM + Persons + WHERE + countryId = "South Africa" + ; +""" + +PREP_ZASINGLERECORDS_SQL = """ + CREATE TABLE ZASingleRecords AS + SELECT + eventId, best, ZAPeople.id AS personId, ZAPeople.name + FROM + (SELECT eventId, best, personId FROM RanksSingle WHERE countryRank = 1) AS Records + INNER JOIN ZAPeople + ON Records.personId = ZAPeople.id + ; +""" + +PREP_ZAAVERAGERECORDS_SQL = """ + CREATE TABLE ZAAverageRecords AS + SELECT + eventId, best, ZAPeople.id AS personId, ZAPeople.name + FROM + (SELECT eventId, best, personId FROM RanksAverage WHERE countryRank=1) AS Records + INNER JOIN ZAPeople + ON Records.personId = ZAPeople.id + ; +""" + +PREP_ZARESULTS_SQL = """ + CREATE TABLE ZAResults AS + SELECT + competitionId, eventId, best, average, personId + FROM + Results + WHERE + personCountryId = "South Africa" + ; +""" + +GET_COMP_DATE_SQL = """ + SELECT + year, month, day + FROM + ZAResults LEFT JOIN Competitions ON ZAResults.competitionId = Competitions.id + WHERE + ZAResults.singleaverage=%s AND ZAResults.eventId=%s AND ZAResults.personId=%s + ; +""" + +FETCH_RECORD_TYPE_SQL = """ + SELECT + Events.id AS eventId, + Events.name AS eventName, + Events.rank AS eventRank, + ZARecordsTable.best AS result, + ZARecordsTable.personId AS id, + ZARecordsTable.name AS name + FROM + ZARecordsTable + LEFT JOIN + Events + ON + Events.id = ZARecordsTable.eventId + ORDER BY + eventRank + ; +""" + + +FETCH_RECORDS_SQL = """ + SELECT + Events.id AS eventId, + Events.name AS eventName, + Events.rank AS eventRank, + ZASingleRecords.personId AS singleId, + ZASingleRecords.name AS singleName, + ZASingleRecords.best AS singleResult, + ZAAverageRecords.personId AS averageId, + ZAAverageRecords.name AS averageName, + ZAAverageRecords.best AS averageResult + FROM + ZASingleRecords LEFT JOIN ZAAverageRecords + ON + ZASingleRecords.eventId = ZAAverageRecords.eventId + LEFT JOIN + Events + ON + Events.id = ZASingleRecords.eventId + ORDER BY + eventRank + ; +""" + def prepare_temp_tables(cursor): print('Preparing temporary tables') print('Finding South Africans') cursor.execute("DROP TABLE IF EXISTS ZAPeople;") - cursor.execute(""" - CREATE TABLE ZAPeople AS - SELECT - id, - name - FROM - Persons - WHERE - countryId = "South Africa" - ; - """) + cursor.execute(PREP_ZAPEOPLE_SQL) print('Extracting single records') cursor.execute("DROP TABLE IF EXISTS ZASingleRecords;") - cursor.execute(""" - CREATE TABLE ZASingleRecords AS - SELECT - eventId, best, ZAPeople.id AS personId, ZAPeople.name - FROM - (SELECT eventId, best, personId FROM RanksSingle WHERE countryRank = 1) AS Records - INNER JOIN ZAPeople - ON Records.personId = ZAPeople.id - ; - """) + cursor.execute(PREP_ZASINGLERECORDS_SQL) print('Extracting average records') cursor.execute("DROP TABLE IF EXISTS ZAAverageRecords;") - cursor.execute(""" - CREATE TABLE ZAAverageRecords AS - SELECT - eventId, best, ZAPeople.id AS personId, ZAPeople.name - FROM - (SELECT eventId, best, personId FROM RanksAverage WHERE countryRank=1) AS Records - INNER JOIN ZAPeople - ON Records.personId = ZAPeople.id - ; - """) + cursor.execute(PREP_ZAAVERAGERECORDS_SQL) print('Extracting results') cursor.execute("DROP TABLE IF EXISTS ZAResults;") - cursor.execute(""" - CREATE TABLE ZAResults AS - SELECT - competitionId, eventId, best, average, personId - FROM - Results - WHERE - personCountryId = "South Africa" - ; - """) - + cursor.execute(PREP_ZARESULTS_SQL) -def get_wca_records(cursor): +def get_records_of_type(cursor, record_table, result_field, single_or_average): print('Fetching Records...') - cursor.execute(""" - SELECT - Events.id AS eventId, - Events.name AS eventName, - ZASingleRecords.personId AS singleId, - ZASingleRecords.name AS singleName, - ZASingleRecords.best AS singleResult, - ZAAverageRecords.personId AS averageId, - ZAAverageRecords.name AS averageName, - ZAAverageRecords.best AS averageResult, - Events.rank AS eventRank - FROM - ZASingleRecords LEFT JOIN ZAAverageRecords - ON - ZASingleRecords.eventId = ZAAverageRecords.eventId - LEFT JOIN - Events - ON - Events.id = ZASingleRecords.eventId - ORDER BY - eventRank - ; - """) - - records = [{'eventId': row[0], - 'eventName': row[1], - 'singleId': row[2], - 'singleName': row[3], - 'singleResultRaw': row[4], - 'singleResult': format_result(row[4],row[0],'single'), - 'averageId': row[5], - 'averageName': row[6], - 'averageResultRaw': row[7], - 'averageResult': format_result(row[7],row[0],'average'), - 'eventRank': row[8]} - for row in cursor.fetchall()] - - # For each record, attach a date - for record in records: - if record['eventName'] in EXCLUDE_EVENTS: + cursor.execute(FETCH_RECORD_TYPE_SQL + .replace('ZARecordsTable', record_table)) + + raw_records = cursor.fetchall() + + record = {} + for row in raw_records: + event_id = row[0] + + if event_id in EXCLUDE_EVENTS: continue - print('Establishing dates of records for', record['eventName']) - - sql_query = ''' - SELECT - year, month, day - FROM - ZAResults LEFT JOIN Competitions ON ZAResults.competitionId = Competitions.id - WHERE - ZAResults.singleaverage=%s AND ZAResults.eventId=%s AND ZAResults.personId=%s; - ''' - cursor.execute(sql_query.replace('singleaverage','best'), (record['singleResultRaw'], record['eventId'], record['singleId'])) - - date = list(cursor.fetchall()[0]) - record['singleDate'] = datetime.date(*date).isoformat() - - cursor.execute(sql_query.replace('singleaverage','average'), (record['averageResultRaw'], record['eventId'], record['averageId'])) - date = cursor.fetchall() - if len(date) > 0: - date = date[0] - record['averageDate'] = datetime.date(*date).isoformat() + result = row[3] + person_id = row[4] + person_name = row[5] + + date = get_date_for_record(cursor, result, event_id, person_id, result_field, single_or_average) + + if event_id not in record.keys(): + record[event_id] = { + 'eventId': event_id, + 'eventName': row[1], + 'eventRank': row[2], + 'result': format_result(result, event_id, single_or_average), + 'resultRaw': result, + 'id': [person_id], + 'name': [row[5]], + 'date': [date], + } else: - record['averageDate'] = None + record[event_id]['id'].append(person_id) + record[event_id]['name'].append(person_name) + record[event_id]['date'].append(date) + + return record + + +def get_date_for_record(cursor, result, event_id, person_id, result_field, single_or_average): + + print(f'Establishing dates of {single_or_average} records for {event_id}') + + cursor.execute(GET_COMP_DATE_SQL.replace('singleaverage',result_field), (result, event_id, person_id)) + + date = cursor.fetchall() + + if len(date) > 0: + date = date[0] + return datetime.date(*date).isoformat() + else: + return None + + +def get_wca_records(cursor): + singles = get_records_of_type(cursor, 'ZASingleRecords', 'best', 'single') + averages = get_records_of_type(cursor, 'ZAAverageRecords', 'average', 'average') + + # Merge singles and averages + records = {} + for event_id in singles.keys(): + records[event_id] = { + 'eventId': event_id, + 'eventName': singles[event_id]['eventName'], + 'eventRank': singles[event_id]['eventRank'], + 'singleResult': singles[event_id]['result'], + 'singleId': singles[event_id]['id'], + 'singleName': singles[event_id]['name'], + 'singleDate': singles[event_id]['date'], + 'singleResultRaw': singles[event_id]['resultRaw'], + } + + for event_id in averages.keys(): + records[event_id]['averageResult'] = averages[event_id]['result'] + records[event_id]['averageId'] = averages[event_id]['id'] + records[event_id]['averageName'] = averages[event_id]['name'] + records[event_id]['averageDate'] = averages[event_id]['date'] + records[event_id]['averageResultRaw'] = averages[event_id]['resultRaw'] + + records = [records[event_id] for event_id in records.keys()] + records.sort(key=lambda x: x['eventRank']) return records From bdcb3ba29f719ebfe4fe9f39d2c24cb9fd2d8355 Mon Sep 17 00:00:00 2001 From: AlphaSheep Date: Fri, 27 Oct 2023 20:31:41 +0200 Subject: [PATCH 3/8] Add tie handling to API --- server/api/rankings/ranking.controller.js | 30 ++++--- .../api/rankings/ranking.controller.test.js | 89 +++++++++++++++---- server/api/records/record.controller.test.js | 20 ++--- server/api/records/record.model.js | 16 ++-- 4 files changed, 106 insertions(+), 49 deletions(-) diff --git a/server/api/rankings/ranking.controller.js b/server/api/rankings/ranking.controller.js index 898eaa14..d8024e9f 100644 --- a/server/api/rankings/ranking.controller.js +++ b/server/api/rankings/ranking.controller.js @@ -105,27 +105,29 @@ function combineRecords(singleRecords, averageRecords) { const combinedRecords = {}; for (let record in averageRecords) { - let eventId = averageRecords[record].eventId; - let province = averageRecords[record].province; - if (combinedRecords[eventId] === undefined) { - combinedRecords[eventId] = {}; - } - if (combinedRecords[eventId][province] === undefined) { - combinedRecords[eventId][province] = {}; - } - combinedRecords[eventId][province].average = averageRecords[record]; + addRecordToCombinedRecord(averageRecords[record], combinedRecords, 'average'); } for (let record in singleRecords) { - let eventId = singleRecords[record].eventId; - let province = singleRecords[record].province; + addRecordToCombinedRecord(singleRecords[record], combinedRecords, 'single'); + } + + return combinedRecords; +} + +function addRecordToCombinedRecord(record, combinedRecords, field) { + let eventId = record.eventId; + let province = record.province; if (combinedRecords[eventId] === undefined) { combinedRecords[eventId] = {}; } if (combinedRecords[eventId][province] === undefined) { combinedRecords[eventId][province] = {}; } - combinedRecords[eventId][province].single = singleRecords[record]; - } - return combinedRecords; + // handle ties + if (combinedRecords[eventId][province][field] !== undefined) { + combinedRecords[eventId][province][field].push(record); + } else { + combinedRecords[eventId][province][field] = [record]; + } } \ No newline at end of file diff --git a/server/api/rankings/ranking.controller.test.js b/server/api/rankings/ranking.controller.test.js index f0160a97..0eb5a562 100644 --- a/server/api/rankings/ranking.controller.test.js +++ b/server/api/rankings/ranking.controller.test.js @@ -28,6 +28,27 @@ const mockSingleRankingData = [ } ]; +const mockSingleRankingDataWithTie = [ + { + eventId: '333', + wcaID: '2000ABCD01', + personName: 'Test Person', + countryRank: 1, + province: 'GT', + provinceRank: 1, + best: '1.00' + }, + { + eventId: '333', + wcaID: '2000ABCD02', + personName: 'Someone Else', + countryRank: 1, + province: 'GT', + provinceRank: 1, + best: '1.00' + } +] + const mockAverageRankingData = [ { eventId: '333', @@ -288,30 +309,62 @@ describe ("Ranking controller:", function() { }); describe('Calling controller.getProvincialRecords', function () { - beforeEach(async function() { - mockingoose(Ranking.Single).toReturn([mockSingleRankingData[0]], 'find'); - mockingoose(Ranking.Average).toReturn([mockAverageRankingData[0]], 'find'); - }); - it('should respond with 200 OK', async function() { - await controller.getProvincialRecords(req, res); - expect(res.status).toHaveBeenCalledWith(200); - }); + describe('without a tie', function () { + beforeEach(async function() { + mockingoose(Ranking.Single).toReturn([mockSingleRankingData[0]], 'find'); + mockingoose(Ranking.Average).toReturn([mockAverageRankingData[0]], 'find'); + }); + + it('should respond with 200 OK', async function() { + await controller.getProvincialRecords(req, res); + expect(res.status).toHaveBeenCalledWith(200); + }); - it('should respond with provincial records', async function() { - await controller.getProvincialRecords(req, res); - expect(res.json).toHaveBeenCalledWith( - expect.objectContaining({ - '333': expect.objectContaining({ - 'GT': expect.objectContaining({ - single: expect.objectContaining(mockSingleRankingData[0]), - average: expect.objectContaining(mockAverageRankingData[0]) + it('should respond with provincial records', async function() { + await controller.getProvincialRecords(req, res); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + '333': expect.objectContaining({ + 'GT': expect.objectContaining({ + single: expect.arrayContaining([expect.objectContaining(mockSingleRankingData[0])]), + average: expect.arrayContaining([expect.objectContaining(mockAverageRankingData[0])]), + }) }) }) - }) - ); + ); + }); }); + describe('with a tie', function () { + beforeEach(async function() { + mockingoose(Ranking.Single).toReturn(mockSingleRankingDataWithTie, 'find'); + mockingoose(Ranking.Average).toReturn([mockAverageRankingData[0]], 'find'); + }); + + it('should respond with 200 OK', async function() { + await controller.getProvincialRecords(req, res); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it('should respond with provincial records', async function() { + await controller.getProvincialRecords(req, res); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + '333': expect.objectContaining({ + 'GT': expect.objectContaining({ + single: expect.arrayContaining([ + expect.objectContaining(mockSingleRankingDataWithTie[0]), + expect.objectContaining(mockSingleRankingDataWithTie[1]) + ]), + average: expect.arrayContaining([expect.objectContaining(mockAverageRankingData[0])]), + }) + }) + }) + ); + }); + }); }); + }); diff --git a/server/api/records/record.controller.test.js b/server/api/records/record.controller.test.js index aa643757..bea69bd9 100644 --- a/server/api/records/record.controller.test.js +++ b/server/api/records/record.controller.test.js @@ -11,24 +11,24 @@ const mockRecordData = [ { eventName: "3x3x3 Cube", eventId: "333", - singleName: "John Doe", + eventRank: 1, singleResult: "1:00:00", - singleId: "1", - averageName: "John Doe", + singleName: ["John Doe"], + singleId: ["1"], averageResult: "1:30:00", - averageId: "1", - eventRank: 1 + averageName: ["John Doe"], + averageId: ["1"], }, { eventName: "Skewb", eventId: "skewb", - singleName: "Bob Person", + eventRank: 2, singleResult: "0:30:00", - singleId: "2", - averageName: "Someone Else", + singleName: ["Bob Person"], + singleId: ["2"], averageResult: "0:40:00", - averageId: "3", - eventRank: 2 + averageName: ["Someone Else"], + averageId: ["3"], } ]; diff --git a/server/api/records/record.model.js b/server/api/records/record.model.js index faf21079..04fe2ffd 100644 --- a/server/api/records/record.model.js +++ b/server/api/records/record.model.js @@ -5,15 +5,17 @@ import mongoose from 'mongoose'; var RecordSchema = new mongoose.Schema({ eventName: String, eventId: String, - singleName: String, + eventRank: Number, + singleResult: String, - singleId: String, - singleDate: Date, - averageName: String, + singleName: [String], + singleId: [String], + singleDate: [Date], + averageResult: String, - averageId: String, - averageDate: Date, - eventRank: Number + averageName: [String], + averageId: [String], + averageDate: [Date], }); export default mongoose.model('Record', RecordSchema); From 7363430ca4401a21142acf22bb9b43b97f4fd302 Mon Sep 17 00:00:00 2001 From: AlphaSheep Date: Fri, 27 Oct 2023 20:32:05 +0200 Subject: [PATCH 4/8] Update record model to allow for ties --- client/src/app/interfaces/record/record.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/client/src/app/interfaces/record/record.ts b/client/src/app/interfaces/record/record.ts index bbd2f4e1..6288ef3e 100644 --- a/client/src/app/interfaces/record/record.ts +++ b/client/src/app/interfaces/record/record.ts @@ -1,16 +1,19 @@ export interface Record { eventName: string; eventId: string; - singleName: string; + eventRank?: number; + + province?: string; + singleResult: string; - singleId: string; - singleDate?: Date; + singleName: string[]; + singleId: string[]; + singleDate?: Date[]; singleNR?: boolean; - averageName: string; + averageResult: string; - averageId: string; - averageDate?: Date; + averageName: string[]; + averageId: string[]; + averageDate?: Date[]; averageNR?: boolean; - eventRank?: number; - province?: string; } From 4ead05b08cde4e50805df23c360edd4ab7486307 Mon Sep 17 00:00:00 2001 From: AlphaSheep Date: Fri, 27 Oct 2023 20:32:44 +0200 Subject: [PATCH 5/8] Add tie handling to record service --- .../services/record/record.service.spec.ts | 60 +++++++++---------- .../src/app/services/record/record.service.ts | 31 ++++++---- 2 files changed, 49 insertions(+), 42 deletions(-) diff --git a/client/src/app/services/record/record.service.spec.ts b/client/src/app/services/record/record.service.spec.ts index 82198c8e..397afe9e 100644 --- a/client/src/app/services/record/record.service.spec.ts +++ b/client/src/app/services/record/record.service.spec.ts @@ -26,28 +26,28 @@ describe('RecordService', () => { { eventName: "3x3x3 Cube", eventId: "333", - singleName: "John Doe", + eventRank: 1, singleResult: "1:00:00", - singleId: "1", - averageName: "John Doe", + singleName: ["John Doe"], + singleId: ["1"], + singleDate: [new Date()], averageResult: "1:30:00", - averageId: "1", - eventRank: 1, - singleDate: new Date(), - averageDate: new Date() + averageName: ["John Doe"], + averageId: ["1"], + averageDate: [new Date()] }, { eventName: "Skewb", eventId: "skewb", - singleName: "Bob Person", + eventRank: 2, singleResult: "0:30:00", - singleId: "2", - averageName: "Someone Else", + singleName: ["Bob Person"], + singleId: ["2"], + singleDate: [new Date()], averageResult: "0:40:00", - averageId: "3", - eventRank: 2, - singleDate: new Date(), - averageDate: new Date() + averageName: ["Someone Else"], + averageId: ["3"], + averageDate: [new Date()] } ]; @@ -72,7 +72,7 @@ describe('RecordService', () => { const mockRecordResponse: ProvincialRecordResponse = { "333": { "GT": { - "single": { + "single": [{ _id: "1", userId: "1", wcaID: "2345TEST01", @@ -82,8 +82,8 @@ describe('RecordService', () => { personName: "Test Person", province: "GT", provinceRank: 1 - }, - "average": { + }], + "average": [{ _id: "2", userId: "2", wcaID: "2345TEST02", @@ -93,10 +93,10 @@ describe('RecordService', () => { personName: "Test Person", province: "GT", provinceRank: 2 - } + }] }, "WC": { - "single": { + "single": [{ _id: "3", userId: "3", wcaID: "2345TEST03", @@ -106,7 +106,7 @@ describe('RecordService', () => { personName: "Test Person", province: "WC", provinceRank: 1 - } + }] } } }; @@ -116,28 +116,28 @@ describe('RecordService', () => { { eventName: "3x3x3 Cube", eventId: "333", - singleName: "Test Person", + province: "GT", singleResult: "1:00:00", - singleId: "2345TEST01", + singleName: ["Test Person"], + singleId: ["2345TEST01"], singleNR: true, - averageName: "Test Person", averageResult: "1:30:00", - averageId: "2345TEST02", + averageName: ["Test Person"], + averageId: ["2345TEST02"], averageNR: false, - province: "GT" }, { eventName: "3x3x3 Cube", eventId: "333", - singleName: "Test Person", + province: "WC", singleResult: "1:00:00", - singleId: "2345TEST03", + singleName: ["Test Person"], + singleId: ["2345TEST03"], singleNR: false, - averageName: "", averageResult: "", - averageId: "", + averageName: [], + averageId: [], averageNR: false, - province: "WC" } ] } diff --git a/client/src/app/services/record/record.service.ts b/client/src/app/services/record/record.service.ts index 62bf25f2..377ebd91 100644 --- a/client/src/app/services/record/record.service.ts +++ b/client/src/app/services/record/record.service.ts @@ -23,8 +23,8 @@ export class RecordService { .pipe( tap(records => { return records.map(record => { - record.singleDate = record.singleDate ? new Date(record.singleDate) : undefined; - record.averageDate = record.averageDate ? new Date(record.averageDate) : undefined; + record.singleDate = record.singleDate ? record.singleDate.map(date => new Date(date)) : undefined; + record.averageDate = record.averageDate ? record.averageDate.map(date => new Date(date)) : undefined; return record; }); }) @@ -44,17 +44,24 @@ export class RecordService { records[eventId] = []; Object.keys(response[eventId]).forEach((province) => { let ranking = response[eventId][province]; + + let firstSingle = ranking.single ? ranking.single[0] : undefined; + let firstAverage = ranking.average ? ranking.average[0] : undefined; + let record: Record = { eventName: this.eventsService.getEventName(eventId), eventId: eventId, - singleName: ranking.single?.personName ? ranking.single.personName : "", - singleResult: ranking.single?.best ? ranking.single.best : "", - singleId: ranking.single?.wcaID ? ranking.single.wcaID : "", - singleNR: ranking.single?.countryRank === 1, - averageName: ranking.average?.personName ? ranking.average.personName : "", - averageResult: ranking.average?.best ? ranking.average.best : "", - averageId: ranking.average?.wcaID ? ranking.average.wcaID : "", - averageNR: ranking.average?.countryRank === 1, + + singleResult: firstSingle?.best ? firstSingle.best : "", + singleName: ranking.single ? ranking.single.map(item => item.personName) : [], + singleId: ranking.single ? ranking.single.map(item => item.wcaID) : [], + singleNR: firstSingle?.countryRank === 1, + + averageResult: firstAverage?.best ? firstAverage.best : "", + averageName: ranking.average ? ranking.average.map(item => item.personName) : [], + averageId: ranking.average ? ranking.average.map(item => item.wcaID) : [], + averageNR: firstAverage?.countryRank === 1, + province: province }; records[eventId].push(record); @@ -67,8 +74,8 @@ export class RecordService { export type ProvincialRecordResponse = { [eventId: string]: { [province: string]: { - single?: Ranking; - average?: Ranking; + single?: Ranking[]; + average?: Ranking[]; } } } From 603142fa553f2a279a2634ec40e7747cde05525c Mon Sep 17 00:00:00 2001 From: AlphaSheep Date: Fri, 27 Oct 2023 20:33:06 +0200 Subject: [PATCH 6/8] Add tie handling on provincial records page --- .../provincial-records.component.html | 12 ++++++++---- .../provincial-records.component.less | 8 ++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/client/src/app/pages/provincial-records/provincial-records.component.html b/client/src/app/pages/provincial-records/provincial-records.component.html index 09e2b8fa..ae07ab86 100644 --- a/client/src/app/pages/provincial-records/provincial-records.component.html +++ b/client/src/app/pages/provincial-records/provincial-records.component.html @@ -24,8 +24,10 @@

{{getEventName(eventId)}} - - {{record.singleName}} + + @@ -45,8 +47,10 @@

{{getEventName(eventId)}} {{record.averageResult}} - - {{record.averageName}} + + diff --git a/client/src/app/pages/provincial-records/provincial-records.component.less b/client/src/app/pages/provincial-records/provincial-records.component.less index c78e2a8b..3163aa41 100644 --- a/client/src/app/pages/provincial-records/provincial-records.component.less +++ b/client/src/app/pages/provincial-records/provincial-records.component.less @@ -80,5 +80,13 @@ .table-middle { text-align: center; } + + .table-name { + padding-top: 0.4em; + padding-bottom: 0.4em; + div:not(:first-child) { + margin-top: 0.4em; + } + } } } \ No newline at end of file From b816166cd4bfb802ef5d1bd085a659415d49fc70 Mon Sep 17 00:00:00 2001 From: AlphaSheep Date: Fri, 27 Oct 2023 20:33:24 +0200 Subject: [PATCH 7/8] Add tie handling on national records list --- .../national-records-list.component.html | 12 ++++++++---- .../national-records-list.component.less | 8 ++++++++ .../national-records-list.component.ts | 11 ++++++++--- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/client/src/app/components/national-records-list/national-records-list.component.html b/client/src/app/components/national-records-list/national-records-list.component.html index 70e56b64..61eeaaba 100644 --- a/client/src/app/components/national-records-list/national-records-list.component.html +++ b/client/src/app/components/national-records-list/national-records-list.component.html @@ -18,8 +18,10 @@

South African Records

- - {{record.singleName}} + + @@ -42,8 +44,10 @@

South African Records

- - {{record.averageName}} + + diff --git a/client/src/app/components/national-records-list/national-records-list.component.less b/client/src/app/components/national-records-list/national-records-list.component.less index 6f2d00f9..fb69de2e 100644 --- a/client/src/app/components/national-records-list/national-records-list.component.less +++ b/client/src/app/components/national-records-list/national-records-list.component.less @@ -76,6 +76,14 @@ text-align: center; } + .table-name { + padding-top: 0.4em; + padding-bottom: 0.4em; + div:not(:first-child) { + margin-top: 0.4em; + } + } + .label { font-size: 0.8em; padding: 0.15em 0.4em 0.3em 0.4em; diff --git a/client/src/app/components/national-records-list/national-records-list.component.ts b/client/src/app/components/national-records-list/national-records-list.component.ts index 6b88c28d..8bec39fc 100644 --- a/client/src/app/components/national-records-list/national-records-list.component.ts +++ b/client/src/app/components/national-records-list/national-records-list.component.ts @@ -45,10 +45,15 @@ export class NationalRecordsListComponent { }); } - isNew(date?: Date) { - if (!date) return false; + isNew(dates?: Date[]) { + if (!dates) return false; + const today = new Date(); const lastMonth = new Date(today.getFullYear(), today.getMonth() - 1, today.getDate()); - return date > lastMonth; + + for (let i in dates) { + if (dates[i] > lastMonth) return true; + } + return false } } From a1d2ba1bc5d38b4014feb1619d1384f6aeb2ef98 Mon Sep 17 00:00:00 2001 From: AlphaSheep Date: Fri, 27 Oct 2023 20:44:48 +0200 Subject: [PATCH 8/8] Fix failing UI test for tied records --- .../national-records-list.component.spec.ts | 46 +++++++++++++------ .../provincial-records.component.spec.ts | 32 ++++++------- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/client/src/app/components/national-records-list/national-records-list.component.spec.ts b/client/src/app/components/national-records-list/national-records-list.component.spec.ts index a1ea943c..204cadb2 100644 --- a/client/src/app/components/national-records-list/national-records-list.component.spec.ts +++ b/client/src/app/components/national-records-list/national-records-list.component.spec.ts @@ -14,27 +14,27 @@ const mockRecordData: Record[] = [ { eventName: "3x3x3 Cube", eventId: "333", - singleName: "John Doe", singleResult: "1:00:00", - singleId: "1", - singleDate: mockPastDate, - averageName: "John Doe", + singleName: ["John Doe"], + singleId: ["1"], + singleDate: [mockPastDate], averageResult: "1:30:00", - averageId: "1", - averageDate: mockPastDate, + averageName: ["John Doe"], + averageId: ["1"], + averageDate: [mockPastDate], eventRank: 1 }, { eventName: "Skewb", eventId: "skewb", - singleName: "Bob Person", singleResult: "0:30:00", - singleId: "2", - singleDate: mockNewDate, - averageName: "Someone Else", + singleName: ["Bob Person"], + singleId: ["2"], + singleDate: [mockNewDate], averageResult: "0:40:00", - averageId: "3", - averageDate: mockNewDate, + averageName: ["Someone Else"], + averageId: ["3"], + averageDate: [mockNewDate], eventRank: 2 } ]; @@ -103,12 +103,28 @@ describe('NationalRecordsListComponent', () => { describe('isNew', () => { it('should return true for dates within the last month', () => { - expect(component.isNew(mockToday)).toBeTrue(); - expect(component.isNew(mockNewDate)).toBeTrue(); + expect(component.isNew([mockToday])).toBeTrue(); + expect(component.isNew([mockNewDate])).toBeTrue(); }); it('should return false for dates older than a month', () => { - expect(component.isNew(mockPastDate)).toBeFalse(); + expect(component.isNew([mockPastDate])).toBeFalse(); + }); + + it('should return false for empty dates', () => { + expect(component.isNew([])).toBeFalse(); + }); + + it('should return false for multiple past dates', () => { + expect(component.isNew([mockPastDate, mockPastDate])).toBeFalse(); + }); + + it('should return true for multiple new dates', () => { + expect(component.isNew([mockToday, mockNewDate])).toBeTrue(); + }); + + it('should return true for a mix of new and old dates', () => { + expect(component.isNew([mockToday, mockPastDate])).toBeTrue(); }); it('should show a NEW tag for new records', () => { diff --git a/client/src/app/pages/provincial-records/provincial-records.component.spec.ts b/client/src/app/pages/provincial-records/provincial-records.component.spec.ts index dd5ac36d..89f81c94 100644 --- a/client/src/app/pages/provincial-records/provincial-records.component.spec.ts +++ b/client/src/app/pages/provincial-records/provincial-records.component.spec.ts @@ -16,23 +16,23 @@ const mockProvincialRecords: ProvincialRecordTable = { { eventName: "3x3x3 Cube", eventId: "333", - singleName: "Test Person", singleResult: "1:00:00", - singleId: "2345TEST01", - averageName: "Test Person", + singleName: ["Test Person"], + singleId: ["2345TEST01"], averageResult: "1:30:00", - averageId: "2345TEST02", + averageName: ["Test Person"], + averageId: ["2345TEST02"], province: "GT" }, { eventName: "3x3x3 Cube", eventId: "333", - singleName: "Test Person", singleResult: "1:00:00", - singleId: "2345TEST03", - averageName: "Another Person", + singleName: ["Test Person"], + singleId: ["2345TEST03"], averageResult: "1:30:00", - averageId: "2345TEST04", + averageName: ["Another Person"], + averageId: ["2345TEST04"], province: "WC" } ], @@ -40,23 +40,23 @@ const mockProvincialRecords: ProvincialRecordTable = { { eventName: "2x2x2 Cube", eventId: "222", - singleName: "Someone Else", singleResult: "1:00:00", - singleId: "2345TEST01", - averageName: "", + singleName: ["Someone Else"], + singleId: ["2345TEST01"], averageResult: "", - averageId: "", + averageName: [], + averageId: [], province: "GT" }, { eventName: "2x2x2 Cube", eventId: "222", - singleName: "Test Person", singleResult: "1:00:00", - singleId: "2345TEST03", - averageName: "", + singleName: ["Test Person"], + singleId: ["2345TEST03"], averageResult: "", - averageId: "", + averageName: [], + averageId: [], province: "WC" } ]