Lines
100.00%
40 / 40
Methods
100.00%
5 / 5
Classes
100.00%
1 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| build | 100.00% | 11 / 11 | 100.00% | 1 / 1 | 6 | ||
| eventEntries | 100.00% | 7 / 7 | 100.00% | 1 / 1 | 2 | ||
| inTravel | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 3 | ||
| statEntry | 100.00% | 17 / 17 | 100.00% | 1 / 1 | 6 | ||
| 21 | final readonly class TimelineBuilder | |
| 22 | { | |
| 23 | public function __construct(private MedalChecker $medalChecker) | |
| 24 | { | |
| 25 | } | |
| 26 | ||
| 27 | /** | |
| 28 | * @param list<AgentStat> $stats newest first | |
| 29 | * @param list<TimelineEvent> $events | |
| 30 | * @param list<DoneBanner> $banners those with the day they were done | |
| 31 | * | |
| 32 | * @return list<TimelineEntry> | |
| 33 | */ | |
| 34 | public function build(array $stats, array $events, array $banners = []): array | |
| 35 | { | |
| 36 | $entries = []; | |
| 37 | foreach ($stats as $index => $stat) { | |
| 38 | $entries[] = $this->statEntry($stat, $stats[$index + 1] ?? null); | |
| 39 | } | |
| 40 | ||
| 41 | foreach ($events as $event) { | |
| 42 | \array_push($entries, ...$this->eventEntries($event)); | |
| 43 | } | |
| 44 | ||
| 45 | foreach ($banners as $banner) { | |
| 46 | if ($banner->doneAt instanceof \DateTimeImmutable) { | |
| 47 | // As events: before that day's uploads | |
| 48 | $entries[] = new TimelineEntry($banner->doneAt->setTime(23, 59, 59), banner: $banner); | |
| 49 | } | |
| 50 | } | |
| 51 | ||
| 52 | // usort is stable: uploads and events keep their order on the same date | |
| 53 | \usort($entries, static fn (TimelineEntry $a, TimelineEntry $b): int => $b->date <=> $a->date); | |
| 54 | ||
| 55 | $travels = \array_values(\array_filter($events, static fn (TimelineEvent $event): bool => $event->endDate instanceof \DateTimeImmutable)); | |
| 56 | ||
| 57 | return \array_map(static fn (TimelineEntry $entry): TimelineEntry => self::inTravel($entry, $travels) ? $entry->withInTravel() : $entry, $entries); | |
| 58 | } | |
| 59 | ||
| 60 | /** | |
| 61 | * A single-day event at the end of its day: before that day's uploads, newest first. A travel | |
| 62 | * as its end, at the end of its last day, and its start, at the start of its first: after | |
| 63 | * that day's uploads, which are in the travel. | |
| 64 | * | |
| 65 | * @return list<TimelineEntry> | |
| 66 | */ | |
| 67 | private function eventEntries(TimelineEvent $event): array | |
| 68 | { | |
| 69 | $date = $event->date ?? $event->createdAt; | |
| 70 | if (!$event->endDate instanceof \DateTimeImmutable) { | |
| 71 | return [new TimelineEntry($date->setTime(23, 59, 59), event: $event)]; | |
| 72 | } | |
| 73 | ||
| 74 | return [ | |
| 75 | new TimelineEntry($event->endDate->setTime(23, 59, 59), event: $event, travelEdge: TimelineEntry::TRAVEL_END), | |
| 76 | new TimelineEntry($date->setTime(0, 0), event: $event, travelEdge: TimelineEntry::TRAVEL_START), | |
| 77 | ]; | |
| 78 | } | |
| 79 | ||
| 80 | /** | |
| 81 | * Whether an entry other than a travel's end or start is on a day of a travel. | |
| 82 | * | |
| 83 | * @param list<TimelineEvent> $travels events with an end date | |
| 84 | */ | |
| 85 | private static function inTravel(TimelineEntry $entry, array $travels): bool | |
| 86 | { | |
| 87 | if (null !== $entry->travelEdge) { | |
| 88 | return false; | |
| 89 | } | |
| 90 | ||
| 91 | // Y-m-d compares as text | |
| 92 | $day = $entry->date->format('Y-m-d'); | |
| 93 | ||
| 94 | return \array_any($travels, fn ($travel) => $day >= ($travel->date ?? $travel->createdAt)->format('Y-m-d') && $day <= $travel->endDate?->format('Y-m-d')); | |
| 95 | } | |
| 96 | ||
| 97 | private function statEntry(AgentStat $stat, ?AgentStat $previous): TimelineEntry | |
| 98 | { | |
| 99 | if (!$previous instanceof AgentStat) { | |
| 100 | return new TimelineEntry($stat->datetime, $stat, first: true); | |
| 101 | } | |
| 102 | ||
| 103 | $before = []; | |
| 104 | foreach ($this->medalChecker->medalLevels($previous) as $level) { | |
| 105 | $before[$level->medal->column] = $level->level; | |
| 106 | } | |
| 107 | ||
| 108 | $medalUps = \array_values(\array_filter( | |
| 109 | $this->medalChecker->medalLevels($stat), | |
| 110 | static fn (MedalLevel $level): bool => $level->level > ($before[$level->medal->column] ?? 0), | |
| 111 | )); | |
| 112 | ||
| 113 | return new TimelineEntry( | |
| 114 | $stat->datetime, | |
| 115 | $stat, | |
| 116 | apGain: \max(0, $stat->ap - $previous->ap), | |
| 117 | newLevel: null !== $previous->level && $stat->level !== $previous->level ? $stat->level : null, | |
| 118 | medalUps: $medalUps, | |
| 119 | recursions: ($stat->recursions ?? 0) > \max(0, $previous->recursions ?? 0) ? $stat->recursions : null, | |
| 120 | ); | |
| 121 | } | |
| 122 | } |