Refactor: reimplement flowchart calculation
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Dorian Zedler 2022-07-28 17:43:34 +02:00
parent cb63919d40
commit 476cd0bd61
Signed by: dorian
GPG key ID: 989DE36109AFA354
6 changed files with 329 additions and 241 deletions

View file

@ -15,6 +15,7 @@ interface CompetitionList {
*/ */
export class DigitalrockAPi { export class DigitalrockAPi {
private BASE_URL = 'https://www.digitalrock.de/egroupware/ranking/json.php?'; private BASE_URL = 'https://www.digitalrock.de/egroupware/ranking/json.php?';
// private BASE_URL = '/test.json?';
/** /**
* function to get competitions * function to get competitions

View file

@ -1,9 +1,22 @@
import { SpeedCompetitionCategoryResult } from '../models/Competition'; import {
import { Participant } from '../models/Participant'; RouteNames,
SpeedCompetitionCategoryResult,
} from '../models/Competition';
import {
Participant,
participantFromApiParticipant,
} from '../models/Participant';
export interface SpeedRoundParticipant {
id: string;
firstName: string;
hasWon: boolean;
}
export interface SpeedRoundPair { export interface SpeedRoundPair {
laneA?: Participant; laneA?: Participant;
laneB?: Participant; laneB?: Participant;
winner?: 'A' | 'B';
} }
export interface SpeedRound { export interface SpeedRound {
@ -12,217 +25,217 @@ export interface SpeedRound {
roundName?: string; roundName?: string;
} }
export interface SpeedFlowchartResult {
rounds: SpeedRound[];
}
/** /**
* Class for use with speed flowcharts
*/
export class SpeedFlowchart {
private _result: SpeedCompetitionCategoryResult;
/**
* *
* @param {SpeedCompetitionCategoryResult} result * @param {number} roundNumber
* @param {RouteNames} routeNames
* @return {string | undefined}
*/ */
constructor(result: SpeedCompetitionCategoryResult) { function getRoundName(
this._result = result; roundNumber: number,
} routeNames: RouteNames,
): string | undefined {
if (roundNumber < 2 || roundNumber > 6) return undefined;
/** return routeNames[roundNumber];
}
/**
* *
* @return {SpeedRound[]} list of speed rounds * @param {string} name
* @return {number}
*/ */
public _convert() { function getRoundRank(name: string): number {
const allData: SpeedRound[] = []; const match = name.match(/1\/([842])/);
console.log(match);
if (match === undefined || match === null || match.length !== 2) {
return 2;
}
return parseInt(match[1]);
}
const rounds = /**
Object.keys(this._result.route_names).length > 2 *
? this._result.route_names['2']?.includes('8') * @param {SpeedRoundPair} pair
? 2 * @param {number} roundIndex
: 1 */
: 0; function computeWinnerOfPair(pair: SpeedRoundPair, roundIndex: number) {
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 ( if (
Object.keys(this._result.route_names).indexOf(round) >= 0 && pair.laneA?.results[roundIndex]?.rank === undefined ||
parseInt(round) >= 0 pair.laneB?.results[roundIndex]?.rank === undefined
) { )
// console.log(round); return;
if (parseInt(round) === 0) { if (pair.winner === undefined) {
// this is the first round pair.winner =
pair.laneA.results[roundIndex].rank > pair.laneB.results[roundIndex].rank
// 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) ? 'B'
: 'A';
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;
} }
} }
/**
*
* @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,
};
}

View file

@ -1,5 +1,5 @@
import { Category } from './Category'; import { Category } from './Category';
import { Participant } from './Participant'; import { ParticipantFromApi } from './Participant';
export interface Competiton { export interface Competiton {
WetId: string; WetId: string;
@ -10,8 +10,7 @@ export interface Competiton {
cats: Category[]; cats: Category[];
} }
export interface SpeedCompetitionCategoryResult extends Competiton { export interface RouteNames {
route_names: {
'0'?: string; // Qualification '0'?: string; // Qualification
'1'?: string; // 1/16 - Final '1'?: string; // 1/16 - Final
'2'?: string; // 1/8 - Final '2'?: string; // 1/8 - Final
@ -21,6 +20,9 @@ export interface SpeedCompetitionCategoryResult extends Competiton {
'6'?: string; // Final '6'?: string; // Final
'-1'?: string; // General result '-1'?: string; // General result
[key: string]: string | undefined; [key: string]: string | undefined;
}; }
participants: Participant[];
export interface SpeedCompetitionCategoryResult extends Competiton {
route_names: RouteNames;
participants: ParticipantFromApi[];
} }

View file

@ -1,7 +1,7 @@
/** /**
* A pariticpant of a competition * A pariticpant of a competition
*/ */
export interface Participant { export interface ParticipantFromApi {
PerId: string; PerId: string;
firstname: string; firstname: string;
lastname: string; lastname: string;
@ -13,5 +13,65 @@ export interface Participant {
['result_rank4']?: string; ['result_rank4']?: string;
['result_rank5']?: string; ['result_rank5']?: string;
['result_rank6']?: 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,
};
} }

View file

@ -1,8 +1,11 @@
import { Context } from '../data/Context'; import { Context } from '../data/Context';
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { SpeedFlowchart, SpeedRound } from '../data/SpeedFlowchart'; import {
import { Card, CardContent, Grid } from '@mui/material'; 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 { competitionId, categoryId } = useParams();
const { api } = useContext(Context); const { api } = useContext(Context);
const [rounds, setRounds] = useState<SpeedRound[]>([]); const [flowchartResult, setFlowchartResult] =
useState<SpeedFlowchartResult>();
useEffect(() => { useEffect(() => {
if (competitionId === undefined || categoryId === undefined) { if (competitionId === undefined || categoryId === undefined) {
@ -20,12 +24,9 @@ export default function SpeedFlowchartPage() {
} }
api.getCompetitionResults(competitionId, categoryId).then(r => { api.getCompetitionResults(competitionId, categoryId).then(r => {
const flowchart = new SpeedFlowchart(r); const flowchartResult = convertResultsToSpeedFlowchartResult(r);
const rounds = flowchart console.log(flowchartResult);
._convert() setFlowchartResult(flowchartResult);
.filter(round => round.roundName !== undefined && round.pairs.length);
console.log(rounds);
setRounds(rounds);
}); });
}, []); }, []);
@ -33,7 +34,7 @@ export default function SpeedFlowchartPage() {
<> <>
<h1>RESULT:</h1> <h1>RESULT:</h1>
<Grid container spacing={2}> <Grid container spacing={2}>
{rounds.map((round, roundKey) => ( {flowchartResult?.rounds.map((round, roundKey) => (
<Grid key={`flowchart-column-${roundKey}`} item xs={12 / 5}> <Grid key={`flowchart-column-${roundKey}`} item xs={12 / 5}>
<h3>{round.roundName}</h3> <h3>{round.roundName}</h3>
<Grid container spacing={2}> <Grid container spacing={2}>
@ -45,9 +46,20 @@ export default function SpeedFlowchartPage() {
> >
<Card> <Card>
<CardContent> <CardContent>
A: {pair.laneA?.firstname} {pair.laneA?.lastname} <Typography
<br /> sx={{
B: {pair.laneB?.firstname} {pair.laneB?.lastname} fontWeight: pair.winner === 'A' ? 'bold' : 'plain',
}}
>
A: {pair.laneA?.firstName} {pair.laneA?.lastName}
</Typography>
<Typography
sx={{
fontWeight: pair.winner === 'B' ? 'bold' : 'plain',
}}
>
B: {pair.laneB?.firstName} {pair.laneB?.lastName}
</Typography>
</CardContent> </CardContent>
</Card> </Card>
</Grid> </Grid>

View file

@ -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 CalendarPage from '../pages/CalendarPage';
import SpeedFlowchartPage from '../pages/SpeedFlowchartPage'; import SpeedFlowchartPage from '../pages/SpeedFlowchartPage';
import PageTemplate from './PageTemplate'; import PageTemplate from './PageTemplate';
@ -9,16 +9,16 @@ import PageTemplate from './PageTemplate';
*/ */
export default function Routing() { export default function Routing() {
return ( return (
<BrowserRouter> <HashRouter>
<PageTemplate> <PageTemplate>
<Routes> <Routes>
<Route path='/' element={<CalendarPage />} /> <Route path='/' element={<CalendarPage />} />
<Route <Route
path='/#/speed-flowchart/:competitionId/:categoryId' path='/speed-flowchart/:competitionId/:categoryId'
element={<SpeedFlowchartPage />} element={<SpeedFlowchartPage />}
></Route> ></Route>
</Routes> </Routes>
</PageTemplate> </PageTemplate>
</BrowserRouter> </HashRouter>
); );
} }