Test Failed
Branch master (46da36)
by Julien
03:07
created

TreeGen   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 443
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
wmc 50
c 0
b 0
f 0
lcom 1
cbo 5
dl 0
loc 443
rs 8.6206

32 Methods

Rating   Name   Duplication   Size   Complexity  
A getFullFighterList() 0 15 3
A shouldInsertBye() 0 4 3
createByeFighter() 0 1 ?
chunkAndShuffle() 0 1 ?
pushEmptyGroupsToTree() 0 1 ?
generateFights() 0 1 ?
addFighterToGroup() 0 1 ?
syncGroup() 0 1 ?
getByeGroup() 0 1 ?
getFighter() 0 1 ?
getFighters() 0 1 ?
getNumRounds() 0 1 ?
A __construct() 0 7 1
A run() 0 12 1
A getMaxFightersByEntity() 0 10 1
A getFightersByEntity() 0 13 2
B getTreeSize() 0 24 4
A repart() 0 14 4
A insertByes() 0 13 3
A generateGroupsForRound() 0 17 4
A saveGroup() 0 14 2
A createByeGroup() 0 9 2
A adjustFightersGroupWithByes() 0 15 1
A getPreviousRound() 0 5 1
A getParentGroup() 0 6 1
A getFightersByArea() 0 21 4
A addParentToChildren() 0 19 3
A destroyPreviousFights() 0 6 1
A generateNextRoundsFights() 0 9 2
A updateParentFight() 0 11 3
A chooseAndUpdateParentFight() 0 17 3
A getNumArea() 0 12 1

How to fix   Complexity   

Complex Class

Complex classes like TreeGen often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TreeGen, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Xoco70\KendoTournaments\TreeGen;
4
5
use Illuminate\Support\Collection;
6
use Illuminate\Support\Facades\App;
7
use Xoco70\KendoTournaments\Contracts\TreeGenerable;
8
use Xoco70\KendoTournaments\Exceptions\TreeGenerationException;
9
use Xoco70\KendoTournaments\Models\Championship;
10
use Xoco70\KendoTournaments\Models\ChampionshipSettings;
11
use Xoco70\KendoTournaments\Models\Fight;
12
use Xoco70\KendoTournaments\Models\FightersGroup;
13
14
abstract class TreeGen implements TreeGenerable
15
{
16
    protected $groupBy;
17
    protected $tree;
18
    public $championship;
19
    public $settings;
20
    protected $numFighters;
21
22
    abstract protected function pushEmptyGroupsToTree($numFighters);
23
24
    abstract protected function generateFights();
25
26
    abstract protected function createByeFighter();
27
28
    abstract protected function chunkAndShuffle(Collection $fightersByEntity);
29
30
    abstract protected function addFighterToGroup(FightersGroup $group, $fighter);
31
32
    abstract protected function syncGroup(FightersGroup $group, $fighters);
33
34
    abstract protected function getByeGroup($fighters);
35
36
    abstract protected function getFighter($fighterId);
37
38
    abstract protected function getFighters();
39
40
    abstract protected function getNumRounds($fightersCount);
41
42
    /**
43
     * @param Championship $championship
44
     * @param $groupBy
45
     */
46
    public function __construct(Championship $championship, $groupBy)
47
    {
48
        $this->championship = $championship;
49
        $this->groupBy = $groupBy;
50
        $this->settings = $championship->getSettings();
51
        $this->tree = new Collection();
52
    }
53
54
55
    /**
56
     * Generate tree groups for a championship.
57
     *
58
     * @throws TreeGenerationException
59
     */
60
    public function run()
61
    {
62
        $usersByArea = $this->getFightersByArea();
63
        $numFighters = sizeof($usersByArea->collapse());
64
        $this->generateGroupsForRound($usersByArea, 1);
65
        $this->pushEmptyGroupsToTree($numFighters); // Abstract
66
        $this->addParentToChildren($numFighters);
67
        $this->generateFights(); // Abstract
68
        //TODO In direct elimination without Prelim, short_id are not generating well
69
        $this->generateNextRoundsFights();
70
        Fight::generateFightsId($this->championship);
71
    }
72
73
    /**
74
     * Get the biggest entity group
75
     * @param $userGroups
76
     *
77
     * @return int
78
     */
79
    private function getMaxFightersByEntity($userGroups): int
80
    {
81
        return $userGroups
82
            ->sortByDesc(function ($group) {
83
                return $group->count();
84
            })
85
            ->first()
86
            ->count();
87
88
    }
89
90
    /**
91
     * Get Competitor's list ordered by entities
92
     * Countries for Internation Tournament, State for a National Tournament, etc.
93
     *
94
     * @param $fighters
95
     * @return Collection
96
     */
97
    private function getFightersByEntity($fighters): Collection
98
    {
99
        // Right now, we are treating users and teams as equals.
100
        // It doesn't matter right now, because we only need name attribute which is common to both models
101
102
        // $this->groupBy contains federation_id, association_id, club_id, etc.
103
        if (($this->groupBy) != null) {
104
            $fighterGroups = $fighters->groupBy($this->groupBy); // Collection of Collection
105
        } else {
106
            $fighterGroups = $fighters->chunk(1); // Collection of Collection
107
        }
108
        return $fighterGroups;
109
    }
110
111
    /**
112
     * Get the size the first round will have
113
     * @param $fighterCount
114
     * @param $groupSize
115
     * @return int
116
     */
117
    protected function getTreeSize($fighterCount, $groupSize)
118
    {
119
        $squareMultiplied = collect([1, 2, 4, 8, 16, 32, 64])
120
            ->map(function ($item) use ($groupSize) {
121
                return $item * $groupSize;
122
            }); // [4, 8, 16, 32, 64,...]
0 ignored issues
show
Unused Code Comprehensibility introduced by
73% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
123
124
        foreach ($squareMultiplied as $limit) {
125
            if ($fighterCount <= $limit) {
126
                $treeSize = $limit;
127
                $numAreas = $this->championship->getSettings()->fightingAreas;
128
                $fighterCountPerArea = $treeSize / $numAreas;
129
                if ($fighterCountPerArea < $groupSize) {
130
                    $treeSize = $treeSize * $numAreas;
131
                }
132
                return $treeSize;
133
            }
134
135
        }
136
137
138
        return 64 * $groupSize;
139
140
    }
141
142
    /**
143
     * Repart BYE in the tree,
144
     * @param $fighterGroups
145
     * @param int $max
146
     *
147
     * @return Collection
148
     */
149
    private function repart($fighterGroups, $max)
150
    {
151
        $fighters = new Collection();
152
        for ($i = 0; $i < $max; $i++) {
153
            foreach ($fighterGroups as $fighterGroup) {
154
                $fighter = $fighterGroup->values()->get($i);
155
                if ($fighter != null) {
156
                    $fighters->push($fighter);
157
                }
158
            }
159
        }
160
161
        return $fighters;
162
    }
163
164
    /**
165
     * Insert byes in an homogen way.
166
     *
167
     * @param Collection $fighters
168
     * @param Collection $byeGroup
169
     *
170
     * @return Collection
171
     */
172
    private function insertByes(Collection $fighters, Collection $byeGroup)
173
    {
174
        $bye = count($byeGroup) > 0 ? $byeGroup[0] : [];
175
        $sizeFighters = count($fighters);
176
        $sizeGroupBy = count($byeGroup);
177
178
        $frequency = $sizeGroupBy != 0
179
            ? (int)floor($sizeFighters / $sizeGroupBy)
180
            : -1;
181
182
        // Create Copy of $competitors
183
        return $this->getFullFighterList($fighters, $frequency, $sizeGroupBy, $bye);
184
    }
185
186
    /**
187
     * @param Collection $usersByArea
188
     */
189
    public function generateGroupsForRound(Collection $usersByArea, $round)
190
    {
191
        $order = 1;
192
        foreach ($usersByArea as $fightersByEntity) {
193
            // Chunking to make small round robin groups
194
            $chunkedFighters = $this->chunkAndShuffle($fightersByEntity);
195
            foreach ($chunkedFighters as $fighters) {
196
                $fighters = $fighters->pluck('id');
197
                if (!app()->runningUnitTests()) {
198
                    $fighters = $fighters->shuffle();
199
                }
200
                $group = $this->saveGroup($order, $round, null);
201
                $this->syncGroup($group, $fighters);
202
                $order++;
203
            }
204
        }
205
    }
206
207
    /**
208
     * @param $order
209
     * @param $round
210
     * @param $parent
211
     * @return FightersGroup
212
     */
213
    protected function saveGroup($order, $round, $parent): FightersGroup
214
    {
215
        $group = new FightersGroup();
216
        $group->area = $this->getNumArea($round, $order);
217
218
        $group->order = $order;
219
        $group->round = $round;
220
        $group->championship_id = $this->championship->id;
221
        if ($parent != null) {
222
            $group->parent_id = $parent->id;
223
        }
224
        $group->save();
225
        return $group;
226
    }
227
228
229
    /**
230
     * @param integer $groupSize
231
     * @return Collection
232
     */
233
    public function createByeGroup($groupSize): Collection
234
    {
235
        $byeFighter = $this->createByeFighter();
236
        $group = new Collection();
237
        for ($i = 0; $i < $groupSize; $i++) {
238
            $group->push($byeFighter);
239
        }
240
        return $group;
241
    }
242
243
    /**
244
     * @param $fighters
245
     * @param Collection $fighterGroups
246
     * @return Collection
247
     */
248
    public function adjustFightersGroupWithByes($fighters, $fighterGroups): Collection
249
    {
250
        $tmpFighterGroups = clone $fighterGroups;
251
        $byeGroup = $this->getByeGroup($fighters);
252
253
254
        // Get biggest competitor's group
255
        $max = $this->getMaxFightersByEntity($tmpFighterGroups);
256
257
        // We reacommodate them so that we can mix them up and they don't fight with another competitor of his entity.
258
259
        $fighters = $this->repart($fighterGroups, $max);
260
        $fighters = $this->insertByes($fighters, $byeGroup);
261
        return $fighters;
262
    }
263
264
    /**
265
     * Get All Groups on previous round
266
     * @param $currentRound
267
     * @return Collection
268
     */
269
    private function getPreviousRound($currentRound)
270
    {
271
        $previousRound = $this->championship->groupsByRound($currentRound + 1)->get();
272
        return $previousRound;
273
    }
274
275
    /**
276
     * Get the next group on the right ( parent ), final round being the ancestor
277
     * @param $matchNumber
278
     * @param Collection $previousRound
279
     * @return mixed
280
     */
281
    private function getParentGroup($matchNumber, $previousRound)
282
    {
283
        $parentIndex = intval(($matchNumber + 1) / 2);
284
        $parent = $previousRound->get($parentIndex - 1);
285
        return $parent;
286
    }
287
288
289
    /**
290
     * Group Fighters by area
291
     * @return Collection
292
     * @throws TreeGenerationException
293
     */
294
    private function getFightersByArea()
295
    {
296
        // If previous trees already exists, delete all
297
        $this->championship->fightersGroups()->delete();
298
        $areas = $this->settings->fightingAreas;
299
        $fighters = $this->getFighters();
300
        // If there is less than 2 competitors average by area
301
        if ($fighters->count() / $areas < ChampionshipSettings::MIN_COMPETITORS_BY_AREA) {
302
            throw new TreeGenerationException(trans('msg.min_competitor_required', ['number' => config('kendo-tournaments.MIN_COMPETITORS_X_AREA')]));
303
        }
304
305
        if ($this->settings->hasPreliminary && $fighters->count() / ($this->settings->preliminaryGroupSize * $areas) < 1 ) {
306
            throw new TreeGenerationException(trans('msg.min_competitor_required', ['number' => config('kendo-tournaments.MIN_COMPETITORS_X_AREA')]));
307
        }
308
309
        // Get Competitor's / Team list ordered by entities ( Federation, Assoc, Club, etc...)
310
        $fighterByEntity = $this->getFightersByEntity($fighters); // Chunk(1)
311
        $fightersWithBye = $this->adjustFightersGroupWithByes($fighters, $fighterByEntity);
312
        // Chunk user by areas
313
        return $fightersWithBye->chunk(count($fightersWithBye) / $areas);
314
    }
315
316
    /**
317
     * Attach a parent to every child for nestedSet Navigation
318
     * @param $numFightersElim
319
     */
320
    private function addParentToChildren($numFightersElim)
321
    {
322
        $numRounds = $this->getNumRounds($numFightersElim);
323
        $groupsDesc = $this->championship
324
            ->fightersGroups()
325
            ->where('round', '<', $numRounds)
326
            ->orderByDesc('id')->get();
327
328
        $groupsDescByRound = $groupsDesc->groupBy('round');
329
330
        foreach ($groupsDescByRound as $round => $groups) {
331
            $previousRound = $this->getPreviousRound($round);
332
            foreach ($groups->reverse()->values() as $matchNumber => $group) {
333
                $parent = $this->getParentGroup($matchNumber + 1, $previousRound);
334
                $group->parent_id = $parent->id;
335
                $group->save();
336
            }
337
        }
338
    }
339
340
    /**
341
     * @param Collection $fighters
342
     * @param $frequency
343
     * @param $sizeGroupBy
344
     * @param $bye
345
     * @return Collection
346
     */
347
    private function getFullFighterList(Collection $fighters, $frequency, $sizeGroupBy, $bye): Collection
348
    {
349
        $newFighters = new Collection();
350
        $count = 0;
351
        $byeCount = 0;
352
        foreach ($fighters as $fighter) {
353
            if ($this->shouldInsertBye($frequency, $sizeGroupBy, $count, $byeCount)) {
354
                $newFighters->push($bye);
355
                $byeCount++;
356
            }
357
            $newFighters->push($fighter);
358
            $count++;
359
        }
360
        return $newFighters;
361
    }
362
363
    /**
364
     * @param $frequency
365
     * @param $sizeGroupBy
366
     * @param $count
367
     * @param $byeCount
368
     * @return bool
369
     */
370
    private function shouldInsertBye($frequency, $sizeGroupBy, $count, $byeCount): bool
371
    {
372
        return $frequency != -1 && $count % $frequency == 0 && $byeCount < $sizeGroupBy;
373
    }
374
375
376
    /**
377
     * Destroy Previous Fights for demo
378
     */
379
    protected function destroyPreviousFights()
380
    {
381
        // Delete previous fight for this championship
382
        $arrGroupsId = $this->championship->fightersGroups()->get()->pluck('id');
383
        Fight::destroy($arrGroupsId);
384
    }
385
386
387
    /**
388
     * Generate Fights for next rounds
389
     */
390
    public function generateNextRoundsFights()
391
    {
392
        $fightersCount = $this->championship->competitors->count() + $this->championship->teams->count();
393
        $maxRounds = $this->getNumRounds($fightersCount);
394
        for ($numRound = 1; $numRound < $maxRounds; $numRound++) {
395
            $groupsByRound = $this->championship->fightersGroups()->where('round', $numRound)->with('parent', 'children')->get();
396
            $this->updateParentFight($groupsByRound); // should be groupsByRound
397
        }
398
    }
399
400
    /**
401
     * @param $groupsByRound
402
     */
403
    protected function updateParentFight($groupsByRound)
404
    {
405
        foreach ($groupsByRound as $keyGroup => $group) {
406
            $parentGroup = $group->parent;
407
            if ($parentGroup == null) break;
408
            $parentFight = $parentGroup->fights->get(0);
409
410
            // determine whether c1 or c2 must be updated
411
            $this->chooseAndUpdateParentFight($keyGroup, $group, $parentFight);
412
        }
413
    }
414
415
    /**
416
     * @param $group
417
     * @param $parentFight
418
     */
419
    protected function chooseAndUpdateParentFight($keyGroup, FightersGroup $group, Fight $parentFight)
420
    {
421
        // we need to know if the child has empty fighters, is this BYE or undetermined
422
        if ($group->hasDeterminedParent()) {
423
            $valueToUpdate = $group->getValueToUpdate(); // This should be OK
424
            if ($valueToUpdate != null) {
425
                $fighterToUpdate = $group->getParentFighterToUpdate($keyGroup);
426
                $parentFight->$fighterToUpdate = $valueToUpdate;
427
                $parentFight->save();
428
                // Add fighter to pivot table
429
                $parentGroup = $parentFight->group;
430
431
                $fighter = $this->getFighter($valueToUpdate);
432
                $this->addFighterToGroup($parentGroup, $fighter);
433
            }
434
        }
435
    }
436
437
438
    /**
439
     * Calculate the area of the group ( group is still not created )
440
     * @param $round
441
     * @param $order
442
     * @return int
443
     */
444
    protected function getNumArea($round, $order)
445
    {
446
        $totalAreas = $this->settings->fightingAreas;
447
        $numFighters = $this->championship->fighters->count(); // 4
448
        $numGroups = $this->getTreeSize($numFighters, $this->championship->getGroupSize()) / $this->championship->getGroupSize(); // 1 -> 1
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
449
450
        $areaSize = $numGroups / ($totalAreas * pow(2, $round - 1));
451
452
        $numArea = intval(ceil($order / $areaSize)); // if round == 4, and second match 2/2 = 1 BAD
453
//        dump($numArea);
454
        return $numArea;
455
    }
456
}
457