Lines
96.96%
32 / 33
Functions and Methods
91.66%
11 / 12
Classes and Traits
0.00%
0 / 1
| Name | Lines | Functions and Methods | CRAP | Classes and Traits | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| EntityStateHistory | 96.96% | 32 / 33 | 91.66% | 11 / 12 | 25 | 0.00% | 0 / 1 | |||
| __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 | |||||
| 1 | <?php | |
| 2 | ||
| 3 | declare(strict_types=1); | |
| 4 | ||
| 5 | namespace Stewart\Contracts\History; | |
| 6 | ||
| 7 | use Countable; | |
| 8 | use IteratorAggregate; | |
| 9 | use Stewart\Contracts\Entity\EntityId; | |
| 10 | use Stewart\Contracts\History\Collection\HistoricalStateCollection; | |
| 11 | use Stewart\Contracts\State\EntityState; | |
| 12 | use Stewart\Contracts\Time\Duration; | |
| 13 | use Stewart\Contracts\Time\Instant; | |
| 14 | use Traversable; | |
| 15 | ||
| 16 | /** @implements IteratorAggregate<int, EntityState> */ | |
| 17 | final 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 | } |