Lines 96.87% 31 / 32
Methods 88.88% 8 / 9
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 100.00% 5 / 5 100.00% 1 / 1 1
 stubEventTime 100.00% 2 / 2 100.00% 1 / 1 1
 getLocation 0.00% 0 / 1 0.00% 0 / 1 2
 findNextEvent 100.00% 8 / 8 100.00% 1 / 1 4
 getDayOn 100.00% 6 / 6 100.00% 1 / 1 3
 getPositionAt 100.00% 5 / 5 100.00% 1 / 1 5
 getCurrentPosition 100.00% 1 / 1 100.00% 1 / 1 1
 isSunUp 100.00% 1 / 1 100.00% 1 / 1 1
 findEventTimeOn 100.00% 3 / 3 100.00% 1 / 1 2
19final class FixedSunCalendar implements SunCalendar
20{
21    private const float DAYTIME_ELEVATION_DEGREES = 45.0;
22
23    private const float SOUTH_AZIMUTH_DEGREES = 180.0;
24
25    /** @var array<string, TimeOfDay> */
26    private array $timesByEvent;
27
28    public function __construct(
29        private readonly Clock $clock,
30        private readonly GeoLocation $location = new GeoLocation(47.5, 19.0),
31    ) {
32        $this->timesByEvent = [
33            SunEvent::Sunrise->value => TimeOfDay::parse('06:00'),
34            SunEvent::SolarNoon->value => TimeOfDay::parse('12:00'),
35            SunEvent::Sunset->value => TimeOfDay::parse('18:00'),
36        ];
37    }
38
39    public function stubEventTime(SunEvent $event, TimeOfDay|string $time): self
40    {
41        $this->timesByEvent[$event->value] = TimeOfDay::fromTimeOrString($time);
42
43        return $this;
44    }
45
46    public function getLocation(): GeoLocation
47    {
48        return $this->location;
49    }
50
51    public function findNextEvent(SunEvent $event, ?SunOffset $offset = null, ?Instant $after = null): ?Instant
52    {
53        $after ??= $this->clock->getNow();
54        $offset ??= SunOffset::none();
55        $today = $after->toDateTime($this->clock->getTimeZone())->setTime(0, 0);
56
57        // Yesterday counts too: an offset can push its event past midnight.
58        foreach (['-1 day', 'today', '+1 day', '+2 days'] as $shift) {
59            $eventTime = $this->findEventTimeOn($event, $today->modify($shift));
60
61            if ($eventTime !== null && $offset->applyTo($eventTime)->isAfter($after)) {
62                return $offset->applyTo($eventTime);
63            }
64        }
65
66        return null;
67    }
68
69    public function getDayOn(DateTimeImmutable $day): SunDay
70    {
71        $sunDay = SunDay::forDate($day);
72
73        foreach (SunEvent::cases() as $event) {
74            $eventTime = $this->findEventTimeOn($event, $day);
75
76            if ($eventTime !== null) {
77                $sunDay = $sunDay->withEventTime($event, $eventTime);
78            }
79        }
80
81        return $sunDay;
82    }
83
84    public function getPositionAt(Instant $moment): SunPosition
85    {
86        $day = $this->getDayOn($moment->toDateTime($this->clock->getTimeZone()));
87        $sunrise = $day->findEventTime(SunEvent::Sunrise);
88        $sunset = $day->findEventTime(SunEvent::Sunset);
89        $isUp = $sunrise !== null && $sunset !== null && !$moment->isBefore($sunrise) && $moment->isBefore($sunset);
90
91        return new SunPosition(self::SOUTH_AZIMUTH_DEGREES, $isUp ? self::DAYTIME_ELEVATION_DEGREES : -self::DAYTIME_ELEVATION_DEGREES);
92    }
93
94    public function getCurrentPosition(): SunPosition
95    {
96        return $this->getPositionAt($this->clock->getNow());
97    }
98
99    public function isSunUp(): bool
100    {
101        return $this->getCurrentPosition()->isHigherThan(0.0);
102    }
103
104    private function findEventTimeOn(SunEvent $event, DateTimeImmutable $day): ?Instant
105    {
106        $time = $this->timesByEvent[$event->value] ?? null;
107        $moment = $time?->resolveOnDay($day, $this->clock->getTimeZone());
108
109        return $moment === null ? null : Instant::fromDateTime($moment);
110    }
111}