From 476cd0bd61bc8dfb9467fcaddd81b14c1903d478 Mon Sep 17 00:00:00 2001 From: Dorian Zedler Date: Thu, 28 Jul 2022 17:43:34 +0200 Subject: [PATCH] Refactor: reimplement flowchart calculation --- src/data/DigitalrockApi.tsx | 1 + src/data/SpeedFlowchart.tsx | 429 ++++++++++++++++--------------- src/models/Competition.tsx | 30 ++- src/models/Participant.tsx | 64 ++++- src/pages/SpeedFlowchartPage.tsx | 38 ++- src/utils/Routing.tsx | 8 +- 6 files changed, 329 insertions(+), 241 deletions(-) diff --git a/src/data/DigitalrockApi.tsx b/src/data/DigitalrockApi.tsx index 5a38934..2738e2d 100644 --- a/src/data/DigitalrockApi.tsx +++ b/src/data/DigitalrockApi.tsx @@ -15,6 +15,7 @@ interface CompetitionList { */ export class DigitalrockAPi { private BASE_URL = 'https://www.digitalrock.de/egroupware/ranking/json.php?'; + // private BASE_URL = '/test.json?'; /** * function to get competitions diff --git a/src/data/SpeedFlowchart.tsx b/src/data/SpeedFlowchart.tsx index 134d1f9..f2ff5da 100644 --- a/src/data/SpeedFlowchart.tsx +++ b/src/data/SpeedFlowchart.tsx @@ -1,9 +1,22 @@ -import { SpeedCompetitionCategoryResult } from '../models/Competition'; -import { Participant } from '../models/Participant'; +import { + RouteNames, + SpeedCompetitionCategoryResult, +} from '../models/Competition'; +import { + Participant, + participantFromApiParticipant, +} from '../models/Participant'; + +export interface SpeedRoundParticipant { + id: string; + firstName: string; + hasWon: boolean; +} export interface SpeedRoundPair { laneA?: Participant; laneB?: Participant; + winner?: 'A' | 'B'; } export interface SpeedRound { @@ -12,217 +25,217 @@ export interface SpeedRound { roundName?: string; } +export interface SpeedFlowchartResult { + rounds: SpeedRound[]; +} + /** - * Class for use with speed flowcharts + * + * @param {number} roundNumber + * @param {RouteNames} routeNames + * @return {string | undefined} */ -export class SpeedFlowchart { - private _result: SpeedCompetitionCategoryResult; +function getRoundName( + roundNumber: number, + routeNames: RouteNames, +): string | undefined { + if (roundNumber < 2 || roundNumber > 6) return undefined; - /** - * - * @param {SpeedCompetitionCategoryResult} result - */ - constructor(result: SpeedCompetitionCategoryResult) { - this._result = result; + return routeNames[roundNumber]; +} + +/** + * + * @param {string} name + * @return {number} + */ +function getRoundRank(name: string): number { + const match = name.match(/1\/([842])/); + console.log(match); + if (match === undefined || match === null || match.length !== 2) { + return 2; } + return parseInt(match[1]); +} - /** - * - * @return {SpeedRound[]} list of speed rounds - */ - public _convert() { - const allData: SpeedRound[] = []; +/** + * + * @param {SpeedRoundPair} pair + * @param {number} roundIndex + */ +function computeWinnerOfPair(pair: SpeedRoundPair, roundIndex: number) { + if ( + pair.laneA?.results[roundIndex]?.rank === undefined || + pair.laneB?.results[roundIndex]?.rank === undefined + ) + return; - const rounds = - Object.keys(this._result.route_names).length > 2 - ? this._result.route_names['2']?.includes('8') - ? 2 - : 1 - : 0; - - const setAorB = (pair: SpeedRoundPair, participant: Participant) => { - if (pair.laneA === undefined) pair.laneA = participant; - else pair.laneB = participant; - }; - // console.log(this._result.route_names); - - for (const round in this._result.route_names) { - if ( - Object.keys(this._result.route_names).indexOf(round) >= 0 && - parseInt(round) >= 0 - ) { - // console.log(round); - - if (parseInt(round) === 0) { - // this is the first round - - // find pairs (always worse vs. best (1-16; 1-15; ...)) (they are sorted by the rank of the better athlete (1-2-3-4-5-6-7-8) - - const qualificationResults: Participant[] = []; - - for (let x = 0; x < this._result.participants.length; x++) { - qualificationResults.push(this._result.participants[x]); - } - - qualificationResults.sort(function (a, b) { - return ( - parseInt(a.result_rank0 ?? '') - parseInt(b.result_rank0 ?? '') - ); - }); - - const nextRoundPairs: SpeedRoundPair[] = []; - const totalMatches = - (Object.keys(this._result.route_names).length > 2 - ? this._result.route_names['2']?.includes('8') - ? 2 - : 1 - : 0) + 2; - const nextRoundMatches = Math.pow(2, totalMatches - 1); - - for (let i = 0; i < nextRoundMatches; i++) { - nextRoundPairs.push({ - laneA: qualificationResults[i], - laneB: qualificationResults[nextRoundMatches * 2 - i - 1], - }); - } - - // build second round pairs (sorted by the rank of the better athlete and worst vs. best (1-8; 2-7; ... )) - - const sortedFirstRoundPairs: [SpeedRoundPair?, SpeedRoundPair?][] = - []; - - for (let i = 0; i < nextRoundMatches; i += 1) { - sortedFirstRoundPairs.push([ - nextRoundPairs.shift(), - nextRoundPairs.pop(), - ]); - } - - // sort these pairs (containing two pairs of athletes) by the rank of the better athlete (1-4;2-3) - - const finalSortedFirstRoundPairs = []; - - for (let i = 0; i < nextRoundMatches / 4; i++) { - finalSortedFirstRoundPairs.push(sortedFirstRoundPairs[i]); - finalSortedFirstRoundPairs.push( - sortedFirstRoundPairs[nextRoundMatches / 2 - i - 1], - ); - } - - // convert the list of pairs of pairs of athletes back to a single list of pairs of athletes - - const finalFirstRoundPairs: SpeedRoundPair[] = []; - - for (let i = 0; i < finalSortedFirstRoundPairs.length; i++) { - const [laneA, laneB] = finalSortedFirstRoundPairs[i]; - if (laneA !== undefined) finalFirstRoundPairs.push(laneA); - if (laneB !== undefined) finalFirstRoundPairs.push(laneB); - } - - // push the first round to all data - allData.push({ - pairs: finalFirstRoundPairs, - roundIndex: 2, - roundName: this._result.route_names[2] ?? '', - }); - - // console.log(allData); - } else if (parseInt(round) > 0) { - // this is not the first round - const nextRound: SpeedRound = { - pairs: [], - roundIndex: -1, - roundName: '', - }; - - // only used when the current round is the 1/2 final - const smallFinal: SpeedRoundPair = {}; - const Final: SpeedRoundPair = {}; - - for ( - let i = 0; - i < allData[allData.length - 1].pairs.length; - i += 1 - ) { - const thisPair: SpeedRoundPair = - allData[allData.length - 1].pairs[i]; - let thisWinner; - let thisLooser; - const thisWinnerIsFirstOfNewPair = i % 2 === 0; - - if (thisPair.laneA === undefined || thisPair.laneB === undefined) { - continue; - } - - if (thisWinnerIsFirstOfNewPair) { - nextRound.pairs.push({}); - } - - if ( - Object.keys(thisPair.laneA).indexOf('result_rank' + round) < 0 || - Object.keys(thisPair.laneB).indexOf('result_rank' + round) < 0 - ) { - continue; - } - - if ( - parseInt(thisPair.laneA['result_rank' + round] as string) < - parseInt(thisPair.laneB['result_rank' + round] as string) - ) { - thisWinner = thisPair.laneA; - thisLooser = thisPair.laneB; - } else if ( - parseInt(thisPair.laneA['result_rank' + round] as string) > - parseInt(thisPair.laneB['result_rank' + round] as string) - ) { - thisWinner = thisPair.laneB; - thisLooser = thisPair.laneA; - } else { - // no result yet!! - /* console.log( - 'got no winner yet, rank 0: ' + - thisPair.laneA['result_rank' + round] + - ' rank 1: ' + - thisPair.laneB['result_rank' + round], - );*/ - continue; - } - - // console.log(thisWinner['firstname'] + ' has won in round ' + round); - - if (parseInt(round) - rounds === 2) { - // if we are in the 1/2 final - setAorB(Final, thisWinner); - setAorB(smallFinal, thisLooser); - } else { - setAorB(nextRound.pairs[nextRound.pairs.length - 1], thisWinner); - } - } - - if (smallFinal.laneA !== undefined && Final.laneA !== undefined) { - // Final - allData.push({ - pairs: [Final], - roundIndex: parseInt(round) + 2, - roundName: - this._result.route_names[String(parseInt(round) + 2)] + - ' / ' + - this._result.route_names[String(parseInt(round) + 1)], - }); - // small Final - allData.push({ - pairs: [smallFinal], - roundIndex: parseInt(round) + 1, - }); - } else { - nextRound.roundIndex = parseInt(round) + 1; - nextRound.roundName = this._result.route_names[parseInt(round) + 1]; - allData.push(nextRound); - } - } - } - } - - return allData; + if (pair.winner === undefined) { + pair.winner = + pair.laneA.results[roundIndex].rank > pair.laneB.results[roundIndex].rank + ? 'B' + : 'A'; } } + +/** + * + * @param {SpeedRoundPair} pair + * @param {number} roundNumber + * @return {Participant | undefined} + */ +function getWinnerOfPair( + pair: SpeedRoundPair, + roundNumber: number, +): Participant | undefined { + computeWinnerOfPair(pair, roundNumber); + return pair.winner === 'A' ? pair.laneA : pair.laneB; +} + +/** + * + * @param {SpeedRoundPair} pair + * @param {number} roundNumber + * @return {Participant | undefined} + */ +function getLooserOfPair( + pair: SpeedRoundPair, + roundNumber: number, +): Participant | undefined { + computeWinnerOfPair(pair, roundNumber); + return pair.winner === 'A' ? pair.laneB : pair.laneA; +} + +/** + * + * @param {number} roundIndex index of the new round + * @param {string} roundName name of the new round + * @param {SpeedRound} previousRound + * @param {boolean} takeLooser + * @return {SpeedRound} + */ +function computeRoundFromPreviousRound( + roundIndex: number, + roundName: string, + previousRound: SpeedRound, + takeLooser = false, +): SpeedRound { + const getAdvancingParticipant = takeLooser + ? getLooserOfPair + : getWinnerOfPair; + const nextRoundPairs = new Array(previousRound.pairs.length / 2) + .fill(0) + .map((_, i) => { + return { + laneA: getAdvancingParticipant( + previousRound.pairs[i * 2], + previousRound.roundIndex, + ), + laneB: getAdvancingParticipant( + previousRound.pairs[i * 2 + 1], + previousRound.roundIndex, + ), + }; + }); + + return { + pairs: nextRoundPairs, + roundIndex: roundIndex, + roundName: roundName, + }; +} + +/** + * + * @param {SpeedCompetitionCategoryResult} result The result to process + * @return {SpeedFlowchartResult} + */ +export function convertResultsToSpeedFlowchartResult( + result: SpeedCompetitionCategoryResult, +): SpeedFlowchartResult { + const rounds: SpeedRound[] = []; + const convertedParticipants = result.participants + .map(fromApi => participantFromApiParticipant(fromApi)) + // sort by qualification result + .sort((a, b) => a.results[0].rank - b.results[0].rank); + + const roundIndices = Object.keys(result.route_names) + .map(number => parseInt(number)) + .filter(number => number > 0); + + console.log(`Have final rounds:`, roundIndices); + + // process first round + const firstRoundName = getRoundName(roundIndices[0], result.route_names); + const firstRoundNumber = getRoundRank( + getRoundName(roundIndices[0], result.route_names) ?? '', + ); + + const getOpponent = (ofRank: number): Participant => { + return convertedParticipants[firstRoundNumber * 2 - 1 - ofRank]; + }; + + // Should be: + // 0, 1, 2, 3, 4, 5, 6, 7 + // - 1,16, 8, 9, 4,13, 5,12, 2,15, 7,10, 3,14, 6,11 + // - 1, 8, 4, 5, 2, 7, 3, 6 for firstRoundNumber=8 + // - 1, 4, 2, 3 for firstRoundNumber=4 + // - 1, 2 for firstRoundNumber=2 + // TODO: come up with a proper alogorithm maybe + const ranksOfLaneAInOrder = [ + [1, 2], + [1, 4, 2, 3], + [1, 8, 4, 5, 2, 7, 3, 6], + ][firstRoundNumber / 4]; + console.log(ranksOfLaneAInOrder); + + const firstRoundPairs = ranksOfLaneAInOrder.map(rank => { + return { + laneA: convertedParticipants[rank - 1], + laneB: getOpponent(rank - 1), + }; + }); + + const firstRound: SpeedRound = { + pairs: firstRoundPairs, + roundIndex: roundIndices[0], + roundName: firstRoundName, + }; + + rounds.push(firstRound); + + // compute following rounds + for (let i = 1; i < roundIndices.length - 2; i++) { + rounds.push( + computeRoundFromPreviousRound( + roundIndices[i], + result.route_names[roundIndices[i]] ?? '', + rounds[i - 1], + ), + ); + } + + // compute final and semi final + const semifinalRoundIndex = roundIndices[roundIndices.length - 2]; + const semifinal = computeRoundFromPreviousRound( + semifinalRoundIndex, + result.route_names[semifinalRoundIndex] ?? '', + rounds[rounds.length - 1], + true, + ); + computeWinnerOfPair(semifinal.pairs[0], semifinalRoundIndex); + rounds.push(semifinal); + + const finalRoundIndex = roundIndices[roundIndices.length - 1]; + const final = computeRoundFromPreviousRound( + finalRoundIndex, + result.route_names[finalRoundIndex] ?? '', + rounds[rounds.length - 2], + ); + computeWinnerOfPair(final.pairs[0], finalRoundIndex); + rounds.push(final); + + return { + rounds: rounds, + }; +} diff --git a/src/models/Competition.tsx b/src/models/Competition.tsx index 8ab07c4..e89f098 100644 --- a/src/models/Competition.tsx +++ b/src/models/Competition.tsx @@ -1,5 +1,5 @@ import { Category } from './Category'; -import { Participant } from './Participant'; +import { ParticipantFromApi } from './Participant'; export interface Competiton { WetId: string; @@ -10,17 +10,19 @@ export interface Competiton { cats: Category[]; } -export interface SpeedCompetitionCategoryResult extends Competiton { - route_names: { - '0'?: string; // Qualification - '1'?: string; // 1/16 - Final - '2'?: string; // 1/8 - Final - '3'?: string; // 1/4 - Final - '4'?: string; // 1/2 - Final - '5'?: string; // Small final - '6'?: string; // Final - '-1'?: string; // General result - [key: string]: string | undefined; - }; - participants: Participant[]; +export interface RouteNames { + '0'?: string; // Qualification + '1'?: string; // 1/16 - Final + '2'?: string; // 1/8 - Final + '3'?: string; // 1/4 - Final + '4'?: string; // 1/2 - Final + '5'?: string; // Small final + '6'?: string; // Final + '-1'?: string; // General result + [key: string]: string | undefined; +} + +export interface SpeedCompetitionCategoryResult extends Competiton { + route_names: RouteNames; + participants: ParticipantFromApi[]; } diff --git a/src/models/Participant.tsx b/src/models/Participant.tsx index 899e4f9..9207f91 100644 --- a/src/models/Participant.tsx +++ b/src/models/Participant.tsx @@ -1,7 +1,7 @@ /** * A pariticpant of a competition */ -export interface Participant { +export interface ParticipantFromApi { PerId: string; firstname: string; lastname: string; @@ -13,5 +13,65 @@ export interface Participant { ['result_rank4']?: string; ['result_rank5']?: string; ['result_rank6']?: string; - [key: string]: string | number | undefined; + [key: string]: string | number | boolean | undefined; +} +export interface Participant { + id: string; + firstName: string; + lastName: string; + results: Result[]; + overallRank?: number; +} + +export interface Result { + rank: number; + result: string; +} + +/** + * Function to extract results from PartiipantFromApi + * @param {ParticipantFromApi} fromApi + * @return {Result[]} + */ +function _extractResults(fromApi: ParticipantFromApi): Result[] { + const results: Result[] = Array(7); + + const existingResults = Object.keys(fromApi).filter(key => + key.match(/result[0-9]/), + ); + + for (const result of existingResults) { + let roundNumber = 0; + const match = result.match(/result([0-9])/); + if (match !== undefined && match !== null) { + roundNumber = parseInt(match[1]); + } + if (roundNumber < 0) { + continue; + } + results[roundNumber] = { + rank: parseInt(fromApi[`result_rank${roundNumber}`] as string), + result: fromApi[`result${roundNumber}`] as string, + }; + } + return results; +} + +/** + * Function to clean up participants from the api + * @param {ParticipantFromApi} fromApi + * @return {Participant} + */ +export function participantFromApiParticipant( + fromApi: ParticipantFromApi, +): Participant { + const results = _extractResults(fromApi); + + return { + id: fromApi.PerId, + firstName: fromApi.firstname, + lastName: fromApi.lastname, + results: results, + overallRank: fromApi.result_rank, + }; } diff --git a/src/pages/SpeedFlowchartPage.tsx b/src/pages/SpeedFlowchartPage.tsx index ef542c7..8708e09 100644 --- a/src/pages/SpeedFlowchartPage.tsx +++ b/src/pages/SpeedFlowchartPage.tsx @@ -1,8 +1,11 @@ import { Context } from '../data/Context'; import { useContext, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; -import { SpeedFlowchart, SpeedRound } from '../data/SpeedFlowchart'; -import { Card, CardContent, Grid } from '@mui/material'; +import { + convertResultsToSpeedFlowchartResult, + SpeedFlowchartResult, +} from '../data/SpeedFlowchart'; +import { Card, CardContent, Grid, Typography } from '@mui/material'; /** * @@ -12,7 +15,8 @@ export default function SpeedFlowchartPage() { const { competitionId, categoryId } = useParams(); const { api } = useContext(Context); - const [rounds, setRounds] = useState([]); + const [flowchartResult, setFlowchartResult] = + useState(); useEffect(() => { if (competitionId === undefined || categoryId === undefined) { @@ -20,12 +24,9 @@ export default function SpeedFlowchartPage() { } api.getCompetitionResults(competitionId, categoryId).then(r => { - const flowchart = new SpeedFlowchart(r); - const rounds = flowchart - ._convert() - .filter(round => round.roundName !== undefined && round.pairs.length); - console.log(rounds); - setRounds(rounds); + const flowchartResult = convertResultsToSpeedFlowchartResult(r); + console.log(flowchartResult); + setFlowchartResult(flowchartResult); }); }, []); @@ -33,7 +34,7 @@ export default function SpeedFlowchartPage() { <>

RESULT:

- {rounds.map((round, roundKey) => ( + {flowchartResult?.rounds.map((round, roundKey) => (

{round.roundName}

@@ -45,9 +46,20 @@ export default function SpeedFlowchartPage() { > - A: {pair.laneA?.firstname} {pair.laneA?.lastname} -
- B: {pair.laneB?.firstname} {pair.laneB?.lastname} + + A: {pair.laneA?.firstName} {pair.laneA?.lastName} + + + B: {pair.laneB?.firstName} {pair.laneB?.lastName} +
diff --git a/src/utils/Routing.tsx b/src/utils/Routing.tsx index feb181f..b60a764 100644 --- a/src/utils/Routing.tsx +++ b/src/utils/Routing.tsx @@ -1,4 +1,4 @@ -import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { BrowserRouter, Routes, Route, HashRouter } from 'react-router-dom'; import CalendarPage from '../pages/CalendarPage'; import SpeedFlowchartPage from '../pages/SpeedFlowchartPage'; import PageTemplate from './PageTemplate'; @@ -9,16 +9,16 @@ import PageTemplate from './PageTemplate'; */ export default function Routing() { return ( - + } /> } > - + ); }