Completed
Push — master ( 12bd23...4aeb2b )
by Julien
02:03
created

TreeGen   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 430
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
wmc 47
c 0
b 0
f 0
lcom 1
cbo 5
dl 0
loc 430
rs 8.439

32 Methods

Rating   Name   Duplication   Size   Complexity  
pushEmptyGroupsToTree() 0 1 ?
generateFights() 0 1 ?
createByeFighter() 0 1 ?
chunkAndShuffle() 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 13 1
A getMaxFightersByEntity() 0 10 1
A getFightersByEntity() 0 13 2
A getTreeSize() 0 14 3
A repart() 0 14 4
A insertByes() 0 13 3
A generateGroupsForRound() 0 17 4
A saveGroup() 0 13 2
A createByeGroup() 0 9 2
A adjustFightersGroupWithByes() 0 15 1
A getPreviousRound() 0 5 1
A getParentGroup() 0 6 1
A getFightersByArea() 0 18 2
A addParentToChildren() 0 19 3
A getFullFighterList() 0 15 3
A shouldInsertBye() 0 4 3
A destroyPreviousFights() 0 6 1
A generateNextRoundsFights() 0 9 2
A updateParentFight() 0 11 3
A chooseAndUpdateParentFight() 0 17 3
A getNumArea() 0 10 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\Competitor;
12
use Xoco70\KendoTournaments\Models\DirectEliminationFight;
13
use Xoco70\KendoTournaments\Models\Fight;
14
use Xoco70\KendoTournaments\Models\FightersGroup;
15
use Xoco70\KendoTournaments\Models\PreliminaryFight;
16
17
abstract class TreeGen implements TreeGenerable
18
{
19
    protected $groupBy;
20
    protected $tree;
21
    public $championship;
22
    public $settings;
23
    protected $numFighters;
24
25
    abstract protected function pushEmptyGroupsToTree($numFighters);
26
27
    abstract protected function generateFights();
28
29
    abstract protected function createByeFighter();
30
31
    abstract protected function chunkAndShuffle($round, Collection $fightersByEntity);
32
33
    abstract protected function addFighterToGroup(FightersGroup $group, $fighter);
34
35
    abstract protected function syncGroup(FightersGroup $group, $fighters);
36
37
    abstract protected function getByeGroup($fighters);
38
39
    abstract protected function getFighter($fighterId);
40
41
    abstract protected function getFighters();
42
43
    abstract protected function getNumRounds($fightersCount);
44
45
    /**
46
     * @param Championship $championship
47
     * @param $groupBy
48
     */
49
    public function __construct(Championship $championship, $groupBy)
50
    {
51
        $this->championship = $championship;
52
        $this->groupBy = $groupBy;
53
        $this->settings = $championship->getSettings();
54
        $this->tree = new Collection();
55
    }
56
57
58
    /**
59
     * Generate tree groups for a championship.
60
     *
61
     * @throws TreeGenerationException
62
     */
63
    public function run()
64
    {
65
        $usersByArea = $this->getFightersByArea();
66
        $numFighters = sizeof($usersByArea->collapse());
67
68
        $this->generateGroupsForRound($usersByArea, 1);
69
        $this->pushEmptyGroupsToTree($numFighters);
70
        $this->addParentToChildren($numFighters);
71
        $this->generateFights();
72
        $this->generateNextRoundsFights();
73
        Fight::generateFightsId($this->championship);
74
75
    }
76
77
    /**
78
     * Get the biggest entity group
79
     * @param $userGroups
80
     *
81
     * @return int
82
     */
83
    private function getMaxFightersByEntity($userGroups): int
84
    {
85
        return $userGroups
86
            ->sortByDesc(function ($group) {
87
                return $group->count();
88
            })
89
            ->first()
90
            ->count();
91
92
    }
93
94
    /**
95
     * Get Competitor's list ordered by entities
96
     * Countries for Internation Tournament, State for a National Tournament, etc.
97
     *
98
     * @param $fighters
99
     * @return Collection
100
     */
101
    private function getFightersByEntity($fighters): Collection
102
    {
103
        // Right now, we are treating users and teams as equals.
104
        // It doesn't matter right now, because we only need name attribute which is common to both models
105
106
        // $this->groupBy contains federation_id, association_id, club_id, etc.
107
        if (($this->groupBy) != null) {
108
            $fighterGroups = $fighters->groupBy($this->groupBy); // Collection of Collection
109
        } else {
110
            $fighterGroups = $fighters->chunk(1); // Collection of Collection
111
        }
112
        return $fighterGroups;
113
    }
114
115
    /**
116
     * Get the size the first round will have
117
     * @param $fighterCount
118
     * @param $groupSize
119
     * @return int
120
     */
121
    protected function getTreeSize($fighterCount, $groupSize)
122
    {
123
        $square = collect([1, 2, 4, 8, 16, 32, 64]);
124
        $squareMultiplied = $square->map(function ($item) use ($groupSize) {
125
            return $item * $groupSize;
126
        });
127
128
        foreach ($squareMultiplied as $limit) {
129
            if ($fighterCount <= $limit) {
130
                return $limit;
131
            }
132
        }
133
        return 64 * $groupSize;
134
    }
135
136
    /**
137
     * Repart BYE in the tree,
138
     * @param $fighterGroups
139
     * @param int $max
140
     *
141
     * @return Collection
142
     */
143
    private function repart($fighterGroups, $max)
144
    {
145
        $fighters = new Collection();
146
        for ($i = 0; $i < $max; $i++) {
147
            foreach ($fighterGroups as $fighterGroup) {
148
                $fighter = $fighterGroup->values()->get($i);
149
                if ($fighter != null) {
150
                    $fighters->push($fighter);
151
                }
152
            }
153
        }
154
155
        return $fighters;
156
    }
157
158
    /**
159
     * Insert byes in an homogen way.
160
     *
161
     * @param Collection $fighters
162
     * @param Collection $byeGroup
163
     *
164
     * @return Collection
165
     */
166
    private function insertByes(Collection $fighters, Collection $byeGroup)
167
    {
168
        $bye = count($byeGroup) > 0 ? $byeGroup[0] : [];
169
        $sizeFighters = count($fighters);
170
        $sizeGroupBy = count($byeGroup);
171
172
        $frequency = $sizeGroupBy != 0
173
            ? (int)floor($sizeFighters / $sizeGroupBy)
174
            : -1;
175
176
        // Create Copy of $competitors
177
        return $this->getFullFighterList($fighters, $frequency, $sizeGroupBy, $bye);
178
    }
179
180
    /**
181
     * @param Collection $usersByArea
182
     * @param integer $round
183
     *
184
     */
185
    public function generateGroupsForRound($usersByArea, $round)
186
    {
187
        $order = 1;
188
        foreach ($usersByArea as $fightersByEntity) {
189
            // Chunking to make small round robin groups
190
            $chunkedFighters = $this->chunkAndShuffle($round, $fightersByEntity);
191
            foreach ($chunkedFighters as $fighters) {
192
                $fighters = $fighters->pluck('id');
193
                if (!App::runningUnitTests()) {
194
                    $fighters = $fighters->shuffle();
195
                }
196
                $group = $this->saveGroup($order, $round, null);
197
                $this->syncGroup($group, $fighters);
198
                $order++;
199
            }
200
        }
201
    }
202
203
    /**
204
     * @param $order
205
     * @param $round
206
     * @param $parent
207
     * @return FightersGroup
208
     */
209
    protected function saveGroup($order, $round, $parent): FightersGroup
210
    {
211
        $group = new FightersGroup();
212
        $group->area = $this->getNumArea($round, $order);
213
        $group->order = $order;
214
        $group->round = $round;
215
        $group->championship_id = $this->championship->id;
216
        if ($parent != null) {
217
            $group->parent_id = $parent->id;
218
        }
219
        $group->save();
220
        return $group;
221
    }
222
223
224
    /**
225
     * @param integer $groupSize
226
     * @return Collection
227
     */
228
    public function createByeGroup($groupSize): Collection
229
    {
230
        $byeFighter = $this->createByeFighter();
231
        $group = new Collection();
232
        for ($i = 0; $i < $groupSize; $i++) {
233
            $group->push($byeFighter);
234
        }
235
        return $group;
236
    }
237
238
    /**
239
     * @param $fighters
240
     * @param Collection $fighterGroups
241
     * @return Collection
242
     */
243
    public function adjustFightersGroupWithByes($fighters, $fighterGroups): Collection
244
    {
245
        $tmpFighterGroups = clone $fighterGroups;
246
        $byeGroup = $this->getByeGroup($fighters);
247
248
        // Get biggest competitor's group
249
        $max = $this->getMaxFightersByEntity($tmpFighterGroups);
250
251
        // We reacommodate them so that we can mix them up and they don't fight with another competitor of his entity.
252
253
        $fighters = $this->repart($fighterGroups, $max);
254
        $fighters = $this->insertByes($fighters, $byeGroup);
255
256
        return $fighters;
257
    }
258
259
    /**
260
     * Get All Groups on previous round
261
     * @param $currentRound
262
     * @return Collection
263
     */
264
    private function getPreviousRound($currentRound)
265
    {
266
        $previousRound = $this->championship->groupsByRound($currentRound + 1)->get();
267
        return $previousRound;
268
    }
269
270
    /**
271
     * Get the next group on the right ( parent ), final round being the ancestor
272
     * @param $matchNumber
273
     * @param Collection $previousRound
274
     * @return mixed
275
     */
276
    private function getParentGroup($matchNumber, $previousRound)
277
    {
278
        $parentIndex = intval(($matchNumber + 1) / 2);
279
        $parent = $previousRound->get($parentIndex - 1);
280
        return $parent;
281
    }
282
283
284
    /**
285
     * Group Fighters by area
286
     * @return Collection
287
     * @throws TreeGenerationException
288
     */
289
    private function getFightersByArea()
290
    {
291
        // If previous trees already exists, delete all
292
        $this->championship->fightersGroups()->delete();
293
        $areas = $this->settings->fightingAreas;
294
        $fighters = $this->getFighters();
295
296
297
        if ($fighters->count() / $areas < ChampionshipSettings::MIN_COMPETITORS_BY_AREA) {
298
            throw new TreeGenerationException();
299
        }
300
        // Get Competitor's / Team list ordered by entities ( Federation, Assoc, Club, etc...)
301
        $fighterByEntity = $this->getFightersByEntity($fighters); // Chunk(1)
302
        $fightersWithBye = $this->adjustFightersGroupWithByes($fighters, $fighterByEntity);
303
304
        // Chunk user by areas
305
        return $fightersWithBye->chunk(count($fightersWithBye) / $areas);
306
    }
307
308
    /**
309
     * Attach a parent to every child for nestedSet Navigation
310
     * @param $numFightersElim
311
     */
312
    private function addParentToChildren($numFightersElim)
313
    {
314
        $numRounds = $this->getNumRounds($numFightersElim);
315
        $groupsDesc = $this->championship
316
            ->fightersGroups()
317
            ->where('round', '<', $numRounds)
318
            ->orderByDesc('id')->get();
319
320
        $groupsDescByRound = $groupsDesc->groupBy('round');
321
322
        foreach ($groupsDescByRound as $round => $groups) {
323
            $previousRound = $this->getPreviousRound($round);
324
            foreach ($groups->reverse()->values() as $matchNumber => $group) {
325
                $parent = $this->getParentGroup($matchNumber + 1, $previousRound);
326
                $group->parent_id = $parent->id;
327
                $group->save();
328
            }
329
        }
330
    }
331
332
    /**
333
     * @param Collection $fighters
334
     * @param $frequency
335
     * @param $sizeGroupBy
336
     * @param $bye
337
     * @return Collection
338
     */
339
    private function getFullFighterList(Collection $fighters, $frequency, $sizeGroupBy, $bye): Collection
340
    {
341
        $newFighters = new Collection();
342
        $count = 0;
343
        $byeCount = 0;
344
        foreach ($fighters as $fighter) {
345
            if ($this->shouldInsertBye($frequency, $sizeGroupBy, $count, $byeCount)) {
346
                $newFighters->push($bye);
347
                $byeCount++;
348
            }
349
            $newFighters->push($fighter);
350
            $count++;
351
        }
352
        return $newFighters;
353
    }
354
355
    /**
356
     * @param $frequency
357
     * @param $sizeGroupBy
358
     * @param $count
359
     * @param $byeCount
360
     * @return bool
361
     */
362
    private function shouldInsertBye($frequency, $sizeGroupBy, $count, $byeCount): bool
363
    {
364
        return $frequency != -1 && $count % $frequency == 0 && $byeCount < $sizeGroupBy;
365
    }
366
367
368
    /**
369
     * Destroy Previous Fights for demo
370
     */
371
    protected function destroyPreviousFights()
372
    {
373
        // Delete previous fight for this championship
374
        $arrGroupsId = $this->championship->fightersGroups()->get()->pluck('id');
375
        Fight::destroy($arrGroupsId);
376
    }
377
378
379
    /**
380
     * Generate Fights for next rounds
381
     */
382
    public function generateNextRoundsFights()
383
    {
384
        $fightersCount = $this->championship->competitors->count() + $this->championship->teams->count();
385
        $maxRounds = $this->getNumRounds($fightersCount);
386
        for ($numRound = 1; $numRound < $maxRounds; $numRound++) {
387
            $groupsByRound = $this->championship->fightersGroups()->where('round', $numRound)->with('parent', 'children')->get();
388
            $this->updateParentFight($groupsByRound); // should be groupsByRound
389
        }
390
    }
391
392
    /**
393
     * @param $groupsByRound
394
     */
395
    protected function updateParentFight($groupsByRound)
396
    {
397
        foreach ($groupsByRound as $keyGroup => $group) {
398
            $parentGroup = $group->parent;
399
            if ($parentGroup == null) break;
400
            $parentFight = $parentGroup->fights->get(0);
401
402
            // determine whether c1 or c2 must be updated
403
            $this->chooseAndUpdateParentFight($keyGroup, $group, $parentFight);
404
        }
405
    }
406
407
    /**
408
     * @param $group
409
     * @param $parentFight
410
     */
411
    protected function chooseAndUpdateParentFight($keyGroup, FightersGroup $group, Fight $parentFight)
412
    {
413
        // we need to know if the child has empty fighters, is this BYE or undetermined
414
        if ($group->hasDeterminedParent()) {
415
            $valueToUpdate = $group->getValueToUpdate(); // This should be OK
416
            if ($valueToUpdate != null) {
417
                $fighterToUpdate = $group->getParentFighterToUpdate($keyGroup);
418
                $parentFight->$fighterToUpdate = $valueToUpdate;
419
                $parentFight->save();
420
                // Add fighter to pivot table
421
                $parentGroup = $parentFight->group;
422
423
                $fighter = $this->getFighter($valueToUpdate);
424
                $this->addFighterToGroup($parentGroup, $fighter);
425
            }
426
        }
427
    }
428
429
430
    /**
431
     * Calculate the area of the group ( group is still not created )
432
     * @param $round
433
     * @param $order
434
     * @return int
435
     */
436
    protected function getNumArea($round, $order)
437
    {
438
        //TODO Could go into FightersGroup Class
439
        $totalAreas = $this->settings->fightingAreas;
440
        $numFighters = $this->championship->fighters->count();
441
        $numGroups = $this->getTreeSize($numFighters, $this->championship->getGroupSize()) / 2; // 5 -> 8
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...
442
        $areaSize = $numGroups / $totalAreas; // 8 / 2 = 4
443
        $numArea = intval(ceil($order / $areaSize * $round)); // 1era ronda ok, segunda ronda,
444
        return $numArea;
445
    }
446
}
447