Lines 96.96% 32 / 33
Methods 91.66% 11 / 12
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 isEmpty 100.00% 1 / 1 100.00% 1 / 1 1
 count 100.00% 1 / 1 100.00% 1 / 1 1
 getStateAtStart 100.00% 1 / 1 100.00% 1 / 1 1
 getLastState 100.00% 1 / 1 100.00% 1 / 1 1
 hasBeenIn 100.00% 1 / 1 100.00% 1 / 1 1
 getLastChangeTo 100.00% 7 / 7 100.00% 1 / 1 5
 countChanges 100.00% 7 / 7 100.00% 1 / 1 4
 getDurationIn 100.00% 8 / 8 100.00% 1 / 1 4
 getIterator 0.00% 0 / 1 0.00% 0 / 1 2
 findEnteredAt 100.00% 1 / 1 100.00% 1 / 1 1
 clampToWindow 100.00% 3 / 3 100.00% 1 / 1 4
17final readonly class EntityStateHistory implements IteratorAggregate, Countable
18{
19    public function __construct(
20        public EntityId $entityId,
21        public HistoryWindow $window,
22        public HistoricalStateCollection $states,
23    ) {}
24
25    public function isEmpty(): bool
26    {
27        return $this->states->isEmpty();
28    }
29
30    public function count(): int
31    {
32        return $this->states->count();
33    }
34
35    public function getStateAtStart(): ?EntityState
36    {
37        return $this->states->getFirst();
38    }
39
40    public function getLastState(): ?EntityState
41    {
42        return $this->states->getLast();
43    }
44
45    public function hasBeenIn(string ...$states): bool
46    {
47        return $this->states->containsWhere(static fn(EntityState $entry): bool => \in_array($entry->state, $states, true));
48    }
49
50    public function getLastChangeTo(string $state): ?EntityState
51    {
52        $lastChange = null;
53        $previous = null;
54
55        foreach ($this->states as $entry) {
56            if ($previous !== null && $entry->state === $state && $previous->state !== $state) {
57                $lastChange = $entry;
58            }
59
60            $previous = $entry;
61        }
62
63        return $lastChange;
64    }
65
66    public function countChanges(): int
67    {
68        $changes = 0;
69        $previous = null;
70
71        foreach ($this->states as $entry) {
72            if ($previous !== null && $entry->state !== $previous->state) {
73                ++$changes;
74            }
75
76            $previous = $entry;
77        }
78
79        return $changes;
80    }
81
82    public function getDurationIn(string ...$states): Duration
83    {
84        $total = Duration::zero();
85        $entries = $this->states->listValues();
86
87        foreach ($entries as $index => $entry) {
88            if (!\in_array($entry->state, $states, true)) {
89                continue;
90            }
91
92            $next = $entries[$index + 1] ?? null;
93            $segmentEnd = $next === null ? $this->window->endsAt : $this->clampToWindow($this->findEnteredAt($next));
94            $total = $total->plus($segmentEnd->elapsedSince($this->clampToWindow($this->findEnteredAt($entry))));
95        }
96
97        return $total;
98    }
99
100    public function getIterator(): Traversable
101    {
102        return $this->states->getIterator();
103    }
104
105    private function findEnteredAt(EntityState $entry): Instant
106    {
107        return $entry->lastChangedAt ?? $entry->lastUpdatedAt ?? $this->window->startsAt;
108    }
109
110    private function clampToWindow(Instant $moment): Instant
111    {
112        return match (true) {
113            $moment->isBefore($this->window->startsAt) => $this->window->startsAt,
114            $moment->isAfter($this->window->endsAt) => $this->window->endsAt,
115            default => $moment,
116        };
117    }
118}