Lines 100.00% 14 / 14
Functions and Methods 100.00% 7 / 7
Classes and Traits 100.00% 1 / 1
Name Lines Functions and Methods CRAP Classes and Traits
TimelineEntry 100.00% 14 / 14 100.00% 7 / 7 15 100.00% 1 / 1
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 kind 100.00% 4 / 4 100.00% 1 / 1 5
 counts 100.00% 4 / 4 100.00% 1 / 1 1
 days 100.00% 2 / 2 100.00% 1 / 1 1
 withInTravel 100.00% 1 / 1 100.00% 1 / 1 1
 isMilestone 100.00% 1 / 1 100.00% 1 / 1 2
 isListed 100.00% 1 / 1 100.00% 1 / 1 4
1<?php
2
3declare(strict_types=1);
4
5namespace App\Timeline;
6
7use App\Entity\AgentStat;
8use App\Entity\DoneBanner;
9use App\Entity\TimelineEvent;
10use App\Medal\MedalLevel;
11
12/**
13 * One entry of the timeline: a stats upload with what changed since the one before it, a
14 * custom event or a Bannergress banner done. A travel (an event with an end date) is two entries,
15 * its end and its start (travelEdge); the entries between are in it (inTravel), with a line beside
16 * the timeline. Travels cannot overlap.
17 */
18final readonly class TimelineEntry
19{
20    public const string UPLOAD = 'upload';
21
22    public const string EVENT = 'event';
23
24    public const string TRAVEL = 'travel';
25
26    public const string BANNER = 'banner';
27
28    public const string TRAVEL_START = 'start';
29
30    public const string TRAVEL_END = 'end';
31
32    /**
33     * @param list<MedalLevel> $medalUps medals with a higher level than in the previous upload, at their new level
34     */
35    public function __construct(
36        public \DateTimeImmutable $date,
37        public ?AgentStat $stat = null,
38        public ?TimelineEvent $event = null,
39        /** A Bannergress banner done that day. */
40        public ?DoneBanner $banner = null,
41        /** The first upload: nothing to compare with. */
42        public bool $first = false,
43        public int $apGain = 0,
44        /** The level, when it changed. */
45        public ?int $newLevel = null,
46        public array $medalUps = [],
47        /** The recursions, when there is a new one. */
48        public ?int $recursions = null,
49        /** For a travel: TRAVEL_START or TRAVEL_END, the entry of that day. */
50        public ?string $travelEdge = null,
51        /** Between the end and the start of a travel. */
52        public bool $inTravel = false,
53    ) {
54    }
55
56    /** UPLOAD, EVENT, TRAVEL (an event with an end date) or BANNER: the timeline's filter. */
57    public function kind(): string
58    {
59        return match (true) {
60            $this->banner instanceof DoneBanner => self::BANNER,
61            !$this->event instanceof TimelineEvent => self::UPLOAD,
62            $this->event->endDate instanceof \DateTimeImmutable => self::TRAVEL,
63            default => self::EVENT,
64        };
65    }
66
67    /**
68     * The number of entries of each kind, for the timeline's summary: a travel once, not its end
69     * and its start (the app's `TimelineSummary`).
70     *
71     * @param list<self> $entries
72     *
73     * @return array<string, int> by kind
74     */
75    public static function counts(array $entries): array
76    {
77        return \array_count_values(\array_map(
78            static fn (self $entry): string => $entry->kind(),
79            \array_filter($entries, static fn (self $entry): bool => self::TRAVEL_END !== $entry->travelEdge),
80        ));
81    }
82
83    /**
84     * The first and the last day it covers (Y-m-d), for the filter: an end or a start of a
85     * travel its day.
86     *
87     * @return array{string, string}
88     */
89    public function days(): array
90    {
91        $day = $this->date->format('Y-m-d');
92
93        return [$day, $day];
94    }
95
96    /** The other entries in a travel (inTravel) with a new one: the timeline's travel line. */
97    public function withInTravel(): self
98    {
99        return new self($this->date, $this->stat, $this->event, $this->banner, $this->first, $this->apGain, $this->newLevel, $this->medalUps, $this->recursions, $this->travelEdge, true);
100    }
101
102    /** The first upload or a recursion: an upload the list always has (the app's `isMilestone`). */
103    public function isMilestone(): bool
104    {
105        return $this->first || null !== $this->recursions;
106    }
107
108    /**
109     * Whether the list has it: an upload only for what changed besides the AP (a milestone, a new
110     * level or medals at a higher tier), as the app's list.
111     */
112    public function isListed(): bool
113    {
114        return self::UPLOAD !== $this->kind() || $this->isMilestone() || null !== $this->newLevel || [] !== $this->medalUps;
115    }
116}