Skip to content

Commit

Permalink
Add average round duration (#17)
Browse files Browse the repository at this point in the history
* Guess end time base on YouTube's start time if times mismatch
* Guess end time based on the average of past events
  • Loading branch information
nicoSWD committed Apr 13, 2024
1 parent dcbb52d commit b9115a7
Show file tree
Hide file tree
Showing 12 changed files with 379 additions and 48 deletions.
54 changes: 54 additions & 0 deletions bin/avg-round-duration
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env php
<?php declare(strict_types=1);

use nicoSWD\IfscCalendar\Domain\Event\IFSCEventTagsRegex as Tag;
use nicoSWD\IfscCalendar\Domain\Tags\IFSCTagsParser;

require __DIR__ . '/../vendor/autoload.php';

$videos = json_decode(
file_get_contents(__DIR__ . '/../vendor/sportclimbing/ifsc-youtube-videos/data/videos.json')
);

$tagsParser = new IFSCTagsParser();

function cache_key(array $tags): string
{
$key = [];

foreach ($tags as $tag) {
$key[] = $tag->name;
}

sort($key);

return strtolower(implode('_', $key));
}

$videoDurations = [];

foreach ($videos as $video) {
$tags = $tagsParser->fromString($video->title);

if (!$tags->hasTag(Tag::WOMEN) && !$tags->hasTag(Tag::MEN)) {
$videoTags = [Tag::WOMEN, Tag::MEN, ...$tags->allTags()];
} else {
$videoTags = $tags->allTags();
}

$videoDurations[cache_key($videoTags)][] = $video->duration;
}

$results = [];

foreach ($videoDurations as $key => $minutes) {
$averageDuration = (int) floor(array_sum($minutes) / count($minutes));
$averageDuration -= ($averageDuration % 30);

if ($averageDuration > 30) {
$results[$key] = $averageDuration;
}
}

ksort($results);
var_export($results);
4 changes: 4 additions & 0 deletions config/services/domain.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,15 @@ services:
- '@nicoSWD\IfscCalendar\Domain\Round\IFSCRoundFactory'
- '@nicoSWD\IfscCalendar\Infrastructure\Round\InfoSheetRoundProvider'

nicoSWD\IfscCalendar\Domain\Round\IFSCAverageRoundDuration:
class: nicoSWD\IfscCalendar\Domain\Round\IFSCAverageRoundDuration

nicoSWD\IfscCalendar\Domain\Round\IFSCRoundFactory:
class: nicoSWD\IfscCalendar\Domain\Round\IFSCRoundFactory
arguments:
- '@nicoSWD\IfscCalendar\Domain\Tags\IFSCTagsParser'
- '@nicoSWD\IfscCalendar\Domain\YouTube\YouTubeLiveStreamFinder'
- '@nicoSWD\IfscCalendar\Domain\Round\IFSCAverageRoundDuration'

nicoSWD\IfscCalendar\Domain\Round\IFSCRoundNameNormalizer:
class: nicoSWD\IfscCalendar\Domain\Round\IFSCRoundNameNormalizer
Expand Down
77 changes: 77 additions & 0 deletions src/Domain/Round/IFSCAverageRoundDuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php declare(strict_types=1);

/**
* @license http://opensource.org/licenses/mit-license.php MIT
* @link https://github.com/nicoSWD
* @author Nicolas Oelgart <nico@oelgart.com>
*/
namespace nicoSWD\IfscCalendar\Domain\Round;

use nicoSWD\IfscCalendar\Domain\Event\IFSCEventTagsRegex as Tag;

final readonly class IFSCAverageRoundDuration
{
private const int DEFAULT_ROUND_DURATION = 90;

// Run `bin/avg-round-duration` to generate an updated list
private const array AVERAGE_DURATIONS = [
'boulder_combined_men_qualification' => 120,
'boulder_combined_men_qualification_women' => 120,
'boulder_combined_qualification_women' => 120,
'boulder_final_lead_men' => 180,
'boulder_final_lead_men_semi_final_women' => 360,
'boulder_final_lead_men_women' => 240,
'boulder_final_lead_women' => 180,
'boulder_final_men' => 120,
'boulder_final_men_paraclimbing' => 150,
'boulder_final_men_women' => 180,
'boulder_final_women' => 120,
'boulder_lead_men_semi_final' => 180,
'boulder_lead_men_semi_final_women' => 120,
'boulder_lead_semi_final_women' => 90,
'boulder_men_semi_final' => 120,
'boulder_men_semi_final_women' => 120,
'boulder_semi_final_women' => 120,
'combined_final_men' => 240,
'combined_final_men_women' => 420,
'combined_final_women' => 240,
'combined_lead_men_qualification' => 90,
'combined_lead_men_qualification_women' => 90,
'combined_lead_qualification_women' => 120,
'combined_men_qualification_speed_women' => 60,
'final_lead_men' => 60,
'final_lead_men_women' => 120,
'final_lead_women' => 120,
'final_men_paraclimbing_women' => 210,
'final_men_speed_women' => 90,
'final_speed_women' => 60,
'lead_men_semi_final' => 120,
'lead_men_semi_final_women' => 150,
'lead_semi_final_women' => 210,
'men_paraclimbing_women' => 120,
'men_qualification_speed' => 60,
'men_qualification_speed_women' => 90,
];

/** @param Tag[] $tags */
public function fromTags(array $tags): int
{
return self::AVERAGE_DURATIONS[$this->lookupKey($tags)]
?? self::DEFAULT_ROUND_DURATION;
}

/** @param Tag[] $tags */
private function lookupKey(array $tags): string
{
$tags = array_map(
static fn (Tag $tag): string => $tag->name,
$tags,
);

sort($tags);

return strtolower(
implode('_', $tags),
);
}
}
52 changes: 38 additions & 14 deletions src/Domain/Round/IFSCRoundFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,50 @@
use nicoSWD\IfscCalendar\Domain\Stream\LiveStream;
use nicoSWD\IfscCalendar\Domain\Tags\IFSCParsedTags;
use nicoSWD\IfscCalendar\Domain\Tags\IFSCTagsParser;
use nicoSWD\IfscCalendar\Domain\YouTube\YouTubeLiveStreamFinder;
use nicoSWD\IfscCalendar\Domain\YouTube\YouTubeLiveStreamFinderInterface;

final readonly class IFSCRoundFactory
{
private const int DEFAULT_ROUND_DURATION = 90;

public function __construct(
private IFSCTagsParser $tagsParser,
private YouTubeLiveStreamFinder $liveStreamFinder,
private YouTubeLiveStreamFinderInterface $liveStreamFinder,
private IFSCAverageRoundDuration $averageRoundDuration,
) {
}

public function create(
IFSCEventInfo $event,
string $roundName,
DateTimeImmutable $startTime,
DateTimeImmutable $endTime,
?DateTimeImmutable $endTime,
IFSCRoundStatus $status,
): IFSCRound {
$tags = $this->getTags($roundName);
$liveStream = $this->findLiveStream($event, $roundName);

if ($liveStream->scheduledStartTime) {
$startTime = $this->buildStartTime($liveStream, $event);
$endTime = $this->buildEndTime($startTime, $liveStream);
$youTubeStartTime = $this->buildStartTime($liveStream, $event);

if ($endTime && !$this->startTimeMatchesYouTubes($startTime, $liveStream)) {
$diff = $startTime->diff($endTime);
$endTime = $youTubeStartTime->add($diff);
}

$startTime = $youTubeStartTime;

if ($liveStream->duration > 0) {
$endTime = $startTime->modify(
sprintf('+%d minutes', $liveStream->duration),
);
}

$status = IFSCRoundStatus::CONFIRMED;
}

if (!$endTime) {
$endTime = $this->calcEndTime($startTime, $tags);
}

return new IFSCRound(
name: $roundName,
categories: $tags->getCategories(),
Expand Down Expand Up @@ -80,16 +96,24 @@ private function findLiveStream(IFSCEventInfo $event, string $roundName): LiveSt
return $this->liveStreamFinder->findLiveStream($event, $roundName);
}

private function buildEndTime(DateTimeImmutable $startTime, LiveStream $liveStream): DateTimeImmutable
private function averageRoundDuration(IFSCParsedTags $tags): int
{
if ($liveStream->duration > 0) {
$duration = $liveStream->duration;
} else {
$duration = self::DEFAULT_ROUND_DURATION;
}
return $this->averageRoundDuration->fromTags($tags->allTags());
}

private function startTimeMatchesYouTubes(DateTimeImmutable $startTime, LiveStream $liveStream): bool
{
$diff = $startTime->diff($liveStream->scheduledStartTime);

return
$diff->i <= 5 &&
($diff->y + $diff->m + $diff->d + $diff->h) === 0;
}

private function calcEndTime(DateTimeImmutable $startTime, IFSCParsedTags $tags): DateTimeImmutable
{
return $startTime->modify(
sprintf('+%d minutes', $duration)
sprintf('+%d minutes', $this->averageRoundDuration($tags)),
);
}
}
2 changes: 1 addition & 1 deletion src/Domain/Schedule/IFSCSchedule.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
public function __construct(
public string $name,
public DateTimeImmutable $startsAt,
public DateTimeImmutable $endsAt,
public ?DateTimeImmutable $endsAt,
public bool $isPreRound,
) {
}
Expand Down
2 changes: 1 addition & 1 deletion src/Domain/Schedule/IFSCScheduleFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public function __construct(
public function create(
string $name,
DateTimeImmutable $startsAt,
DateTimeImmutable $endsAt,
?DateTimeImmutable $endsAt,
): IFSCSchedule {
$tags = $this->tagsParser->fromString($name);

Expand Down
2 changes: 1 addition & 1 deletion src/Domain/Tags/IFSCParsedTags.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public function isPreRound(): bool
return $this->hasTag(Tag::PRE_ROUND);
}

private function hasTag(Tag $tag): bool
public function hasTag(Tag $tag): bool
{
return in_array($tag, $this->tags, strict: true);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Domain/YouTube/YouTubeLiveStreamFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use nicoSWD\IfscCalendar\Domain\Event\Info\IFSCEventInfo;
use nicoSWD\IfscCalendar\Domain\Stream\LiveStream;

final readonly class YouTubeLiveStreamFinder
final readonly class YouTubeLiveStreamFinder implements YouTubeLiveStreamFinderInterface
{
public function __construct(
private YouTubeLinkMatcher $linkMatcher,
Expand Down
16 changes: 16 additions & 0 deletions src/Domain/YouTube/YouTubeLiveStreamFinderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

/**
* @license http://opensource.org/licenses/mit-license.php MIT
* @link https://github.com/nicoSWD
* @author Nicolas Oelgart <nico@oelgart.com>
*/
namespace nicoSWD\IfscCalendar\Domain\YouTube;

use nicoSWD\IfscCalendar\Domain\Event\Info\IFSCEventInfo;
use nicoSWD\IfscCalendar\Domain\Stream\LiveStream;

interface YouTubeLiveStreamFinderInterface
{
public function findLiveStream(IFSCEventInfo $event, string $roundName): LiveStream;
}
6 changes: 3 additions & 3 deletions src/Infrastructure/Schedule/InfoSheetScheduleProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ private function parseDaySchedule(string $schedule, DateTimeZone $timeZone): Ite
$startsAt = $this->createStartDate($dayName, $match['start_time'][$key], $timeZone);
}

$endsAt = $this->createEndDate($dayName, $match['end_time'][$key] ?? null, $startsAt, $timeZone);
$endsAt = $this->createEndDate($dayName, $match['end_time'][$key] ?? null, $timeZone);
$lastStart = $startsAt;

$schedule = $this->scheduleFactory->create(
Expand Down Expand Up @@ -113,13 +113,13 @@ private function createStartDate(string $day, string $time, DateTimeZone $timeZo
);
}

private function createEndDate(string $dayName, ?string $time, DateTimeImmutable $startsAt, DateTimeZone $timeZone): DateTimeImmutable
private function createEndDate(string $dayName, ?string $time, DateTimeZone $timeZone): ?DateTimeImmutable
{
if ($time !== null && trim($time) !== '') {
return $this->createStartDate($dayName, $time, $timeZone);
}

return $startsAt->modify('+2 hours');
return null;
}

/** @return string[] */
Expand Down
Loading

0 comments on commit b9115a7

Please sign in to comment.