Skip to content

Commit 5de24b5

Browse files
nicoSWDclaude
andauthored
Improve start lists (#53)
* Improve start lists * fix: update CalendarSpeedRelayFormattingTest to use IFSCAthleteGender IFSCRoundCategory was renamed to IFSCAthleteGender in the rebased Improve start lists commit. Update the test file that still referenced the old class name. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add gender * Move --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 007dea1 commit 5de24b5

13 files changed

Lines changed: 527 additions & 74 deletions

File tree

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
* @link https://github.com/nicoSWD
66
* @author Nicolas Oelgart <nico@ifsc.stream>
77
*/
8-
namespace SportClimbing\IfscCalendar\Domain\Round;
8+
namespace SportClimbing\IfscCalendar\Domain\Athlete;
99

10-
enum IFSCRoundCategory: string
10+
enum IFSCAthleteGender: string
1111
{
1212
case MEN = 'men';
1313
case WOMEN = 'women';

src/Domain/Round/IFSCRound.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
namespace SportClimbing\IfscCalendar\Domain\Round;
99

1010
use DateTimeImmutable;
11+
use SportClimbing\IfscCalendar\Domain\Athlete\IFSCAthleteGender;
1112
use SportClimbing\IfscCalendar\Domain\Discipline\IFSCDisciplines;
1213
use SportClimbing\IfscCalendar\Domain\Stream\LiveStream;
1314

1415
final readonly class IFSCRound
1516
{
16-
/** @param IFSCRoundCategory[] $categories */
17+
/** @param IFSCAthleteGender[] $categories */
1718
public function __construct(
1819
public string $name,
1920
public array $categories,

src/Domain/Round/IFSCSameStreamRoundsMerger.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
namespace SportClimbing\IfscCalendar\Domain\Round;
99

1010
use DateTimeImmutable;
11+
use SportClimbing\IfscCalendar\Domain\Athlete\IFSCAthleteGender;
1112
use SportClimbing\IfscCalendar\Domain\Tags\IFSCTagsParser;
1213

1314
final readonly class IFSCSameStreamRoundsMerger
@@ -107,7 +108,7 @@ private function buildMergedName(array $rounds): string
107108

108109
/**
109110
* @param IFSCRound[] $rounds
110-
* @return IFSCRoundCategory[]
111+
* @return IFSCAthleteGender[]
111112
*/
112113
private function mergedCategories(array $rounds): array
113114
{
@@ -121,7 +122,7 @@ private function mergedCategories(array $rounds): array
121122
}
122123
}
123124

124-
usort($categories, static fn (IFSCRoundCategory $a, IFSCRoundCategory $b) => $a->value <=> $b->value);
125+
usort($categories, static fn (IFSCAthleteGender $a, IFSCAthleteGender $b) => $a->value <=> $b->value);
125126

126127
return $categories;
127128
}

src/Domain/StartList/IFSCStartListGenerator.php

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@
88
namespace SportClimbing\IfscCalendar\Domain\StartList;
99

1010
use Closure;
11+
use SportClimbing\IfscCalendar\Domain\Athlete\IFSCAthlete;
1112
use SportClimbing\IfscCalendar\Domain\Athlete\IFSCAthleteException;
1213
use SportClimbing\IfscCalendar\Domain\Athlete\IFSCAthleteService;
1314
use SportClimbing\IfscCalendar\Domain\Ranking\IFSCAthleteRankingCalculator;
1415

15-
use SportClimbing\IfscCalendar\Domain\Round\IFSCRoundCategory;
16+
use SportClimbing\IfscCalendar\Domain\Athlete\IFSCAthleteGender;
1617

1718
final readonly class IFSCStartListGenerator
1819
{
19-
private const int LIST_MAX_SIZE = 40;
20+
private const int PER_GENDER_MAX = 20;
2021

2122
public function __construct(
2223
private IFSCStartListProviderInterface $startListProvider,
@@ -37,25 +38,55 @@ public function buildStartList(int $eventId): IFSCStartListResult
3738
$athlete = $this->athleteService->fetchAthlete($starter->athleteId);
3839
$starter->score = $this->rankingCalculator->calculateScore($athlete);
3940
$starter->photoUrl = $athlete->photoUrl;
40-
$starter->instagram = $athlete->instagram;
41-
42-
$starter->category = match ($athlete->gender) {
43-
'male' => IFSCRoundCategory::MEN,
44-
'female' => IFSCRoundCategory::WOMEN,
45-
default => null,
46-
};
41+
$starter->instagram = $this->normalizeInstagram($athlete->instagram);
42+
$starter->gender = $this->getGender($athlete);
4743

4844
$startList[] = $starter;
4945
}
5046

5147
usort($startList, $this->sortByScore());
5248

5349
return new IFSCStartListResult(
54-
starters: array_slice($startList, 0, self::LIST_MAX_SIZE),
50+
starters: $this->selectTopByGender($startList),
5551
total: count($startList),
5652
);
5753
}
5854

55+
/**
56+
* @param IFSCStarter[] $startList
57+
* @return IFSCStarter[]
58+
*/
59+
private function selectTopByGender(array $startList): array
60+
{
61+
$men = $this->filterByGender($startList, IFSCAthleteGender::MEN);
62+
$women = $this->filterByGender($startList, IFSCAthleteGender::WOMEN);
63+
64+
$selectedMen = $this->selectTopFromPool($men, array_slice($women, self::PER_GENDER_MAX));
65+
$selectedWomen = $this->selectTopFromPool($women, array_slice($men, self::PER_GENDER_MAX));
66+
67+
$result = array_merge($selectedMen, $selectedWomen);
68+
usort($result, $this->sortByScore());
69+
70+
return $result;
71+
}
72+
73+
/**
74+
* @param IFSCStarter[] $pool
75+
* @param IFSCStarter[] $fillPool
76+
* @return IFSCStarter[]
77+
*/
78+
private function selectTopFromPool(array $pool, array $fillPool): array
79+
{
80+
$selected = array_slice($pool, 0, self::PER_GENDER_MAX);
81+
$shortfall = self::PER_GENDER_MAX - count($selected);
82+
83+
if ($shortfall > 0) {
84+
$selected = array_merge($selected, array_slice($fillPool, 0, $shortfall));
85+
}
86+
87+
return $selected;
88+
}
89+
5990
private function sortByScore(): Closure
6091
{
6192
return static function (IFSCStarter $athlete1, IFSCStarter $athlete2): int {
@@ -69,6 +100,39 @@ private function sortByScore(): Closure
69100
};
70101
}
71102

103+
/** @return IFSCStarter[] */
104+
public function filterByGender(array $startList, IFSCAthleteGender $gender): array
105+
{
106+
return array_values(array_filter($startList, fn (IFSCStarter $starter): bool => $starter->gender === $gender));
107+
}
108+
109+
private function normalizeInstagram(?string $instagram): ?string
110+
{
111+
if ($instagram === null || $instagram === '') {
112+
return null;
113+
}
114+
115+
if (str_contains($instagram, 'instagram.com/')) {
116+
preg_match('~instagram\.com/([^/?#]+)~', $instagram, $matches);
117+
return $matches[1] ?? null;
118+
}
119+
120+
return ltrim($instagram, '@');
121+
}
122+
123+
/**
124+
* @param IFSCAthlete $athlete
125+
* @return IFSCAthleteGender|null
126+
*/
127+
private function getGender(IFSCAthlete $athlete): ?IFSCAthleteGender
128+
{
129+
return match ($athlete->gender) {
130+
'male' => IFSCAthleteGender::MEN,
131+
'female' => IFSCAthleteGender::WOMEN,
132+
default => null,
133+
};
134+
}
135+
72136
/**
73137
* @return IFSCStarter[]
74138
* @throws IFSCStartListException

src/Domain/StartList/IFSCStarter.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@
77
*/
88
namespace SportClimbing\IfscCalendar\Domain\StartList;
99

10-
use SportClimbing\IfscCalendar\Domain\Round\IFSCRoundCategory;
10+
use SportClimbing\IfscCalendar\Domain\Discipline\IFSCDiscipline;
11+
use SportClimbing\IfscCalendar\Domain\Athlete\IFSCAthleteGender;
1112

1213
final class IFSCStarter
1314
{
14-
public ?IFSCRoundCategory $category = null;
15-
15+
/** @param IFSCDiscipline[] $disciplines */
1616
public function __construct(
1717
public readonly int $athleteId,
1818
public readonly string $firstName,
1919
public readonly string $lastName,
2020
public readonly string $country,
21+
public ?IFSCAthleteGender $gender = null,
22+
public readonly array $disciplines = [],
2123
public float $score = 0,
2224
public ?string $photoUrl = null,
2325
public ?string $instagram = null,

src/Domain/Tags/IFSCParsedTags.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
use SportClimbing\IfscCalendar\Domain\Discipline\IFSCDiscipline;
1111
use SportClimbing\IfscCalendar\Domain\Event\IFSCEventTagsRegex as Tag;
12-
use SportClimbing\IfscCalendar\Domain\Round\IFSCRoundCategory;
12+
use SportClimbing\IfscCalendar\Domain\Athlete\IFSCAthleteGender;
1313
use SportClimbing\IfscCalendar\Domain\Round\IFSCRoundKind;
1414

1515
final readonly class IFSCParsedTags
@@ -29,8 +29,8 @@
2929
];
3030

3131
private const array CATEGORIES = [
32-
IFSCRoundCategory::WOMEN->value => Tag::WOMEN,
33-
IFSCRoundCategory::MEN->value => Tag::MEN,
32+
IFSCAthleteGender::WOMEN->value => Tag::WOMEN,
33+
IFSCAthleteGender::MEN->value => Tag::MEN,
3434
];
3535

3636
/** @param Tag[] $tags */
@@ -75,20 +75,20 @@ public function getRoundKind(): ?IFSCRoundKind
7575
return null;
7676
}
7777

78-
/** @return IFSCRoundCategory[] */
78+
/** @return IFSCAthleteGender[] */
7979
public function getCategories(): array
8080
{
8181
$categories = [];
8282

8383
foreach (self::CATEGORIES as $name => $tag) {
8484
if ($this->hasTag($tag)) {
85-
$categories[] = IFSCRoundCategory::from($name);
85+
$categories[] = IFSCAthleteGender::from($name);
8686
}
8787
}
8888

8989
if (empty($categories)) {
90-
$categories[] = IFSCRoundCategory::WOMEN;
91-
$categories[] = IFSCRoundCategory::MEN;
90+
$categories[] = IFSCAthleteGender::WOMEN;
91+
$categories[] = IFSCAthleteGender::MEN;
9292
}
9393

9494
return $categories;

src/Infrastructure/Calendar/ICalCalendar.php

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Eluceo\iCal\Domain\ValueObject\Uri;
2424
use Exception;
2525
use SportClimbing\IfscCalendar\Domain\Calendar\IFSCCalendarGeneratorInterface;
26+
use SportClimbing\IfscCalendar\Domain\Discipline\IFSCDiscipline;
2627
use SportClimbing\IfscCalendar\Domain\Event\IFSCEvent;
2728
use SportClimbing\IfscCalendar\Domain\Round\IFSCRound;
2829
use SportClimbing\IfscCalendar\Domain\StartList\IFSCStarter;
@@ -223,13 +224,42 @@ private function buildDescription(IFSCEvent $event, ?IFSCRound $round = null): s
223224
/** @return IFSCStarter[] */
224225
private function getFilteredStartList(IFSCEvent $event, ?IFSCRound $round): array
225226
{
226-
if (!$round || empty($round->categories)) {
227+
if (!$round) {
228+
return $event->startList;
229+
}
230+
231+
if (empty($round->categories) && empty($round->disciplines->all())) {
227232
return $event->startList;
228233
}
229234

230235
return array_filter(
231236
$event->startList,
232-
fn (IFSCStarter $athlete): bool => $athlete->category === null || in_array($athlete->category, $round->categories, strict: true)
237+
fn (IFSCStarter $athlete): bool =>
238+
$this->matchesRoundCategory($athlete, $round) &&
239+
$this->matchesRoundDiscipline($athlete, $round)
240+
);
241+
}
242+
243+
private function matchesRoundCategory(IFSCStarter $athlete, IFSCRound $round): bool
244+
{
245+
if (empty($round->categories)) {
246+
return true;
247+
}
248+
249+
return $athlete->gender === null || in_array($athlete->gender, $round->categories, strict: true);
250+
}
251+
252+
private function matchesRoundDiscipline(IFSCStarter $athlete, IFSCRound $round): bool
253+
{
254+
$roundDisciplines = $round->disciplines->all();
255+
256+
if (empty($roundDisciplines)) {
257+
return true;
258+
}
259+
260+
return array_any(
261+
$athlete->disciplines,
262+
static fn (IFSCDiscipline $discipline): bool => in_array($discipline, $roundDisciplines, strict: true),
233263
);
234264
}
235265

src/Infrastructure/Calendar/JsonCalendar.php

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
use SportClimbing\IfscCalendar\Domain\Discipline\IFSCDiscipline;
1515
use SportClimbing\IfscCalendar\Domain\Event\IFSCEvent;
1616
use SportClimbing\IfscCalendar\Domain\Round\IFSCRound;
17-
use SportClimbing\IfscCalendar\Domain\Round\IFSCRoundCategory;
17+
use SportClimbing\IfscCalendar\Domain\Athlete\IFSCAthleteGender;
1818
use SportClimbing\IfscCalendar\Domain\StartList\IFSCStarter;
1919
use Override;
2020

@@ -99,9 +99,11 @@ private function formatStarters(array $starters): array
9999
'athlete_id' => $starter->athleteId,
100100
'first_name' => $starter->firstName,
101101
'last_name' => $starter->lastName,
102+
'gender' => $starter->gender,
102103
'country' => $starter->country,
103104
'photo_url' => $starter->photoUrl,
104-
'instagram' => $this->normalizeInstagram($starter->instagram),
105+
'instagram' => $starter->instagram,
106+
'disciplines' => $this->buildStarterDisciplines($starter),
105107
];
106108

107109
return array_map($format, $starters);
@@ -137,7 +139,7 @@ private function buildDisciplines(IFSCRound $round): array
137139
/** @return string[] */
138140
private function buildCategories(IFSCRound $round): array
139141
{
140-
return array_map(static fn (IFSCRoundCategory $category): string => $category->value, $round->categories);
142+
return array_map(static fn (IFSCAthleteGender $category): string => $category->value, $round->categories);
141143
}
142144

143145
private function countryName(string $countryCode): string
@@ -157,22 +159,14 @@ private function countryName(string $countryCode): string
157159
return \Locale::getDisplayRegion("und-{$isoCode}", 'en');
158160
}
159161

160-
private function normalizeInstagram(?string $instagram): ?string
162+
private function formatDate(DateTimeInterface $dateTime): string
161163
{
162-
if ($instagram === null || $instagram === '') {
163-
return null;
164-
}
165-
166-
if (str_contains($instagram, 'instagram.com/')) {
167-
preg_match('~instagram\.com/([^/?#]+)~', $instagram, $matches);
168-
return $matches[1] ?? null;
169-
}
170-
171-
return ltrim($instagram, '@');
164+
return $dateTime->format(DateTimeInterface::RFC3339);
172165
}
173166

174-
private function formatDate(DateTimeInterface $dateTime): string
167+
/** @return string[] */
168+
private function buildStarterDisciplines(IFSCStarter $starter): array
175169
{
176-
return $dateTime->format(DateTimeInterface::RFC3339);
170+
return array_map(static fn (IFSCDiscipline $discipline): string => $discipline->value, $starter->disciplines);
177171
}
178172
}

0 commit comments

Comments
 (0)