Issues (113)

src/action/UsageInterval.php (1 issue)

Severity
1
<?php
2
declare(strict_types=1);
3
4
namespace hiqdev\php\billing\action;
5
use DateInterval;
6
use DateTimeImmutable;
7
use InvalidArgumentException;
8
use JsonSerializable;
9
10
/** @readonly */
11
final class UsageInterval implements JsonSerializable
12
{
13
    /** @readonly */
14
    private DateTimeImmutable $start;
15
    /** @readonly */
16
    private DateTimeImmutable $end;
17
    /** @readonly */
18
    private DateTimeImmutable $month;
19
20
    private function __construct(
21
        DateTimeImmutable $start,
22
        DateTimeImmutable $end
23
    ) {
24
        $this->start = $start;
25
        $this->end = $end;
26
        $this->month = self::toMonth($start);
27
    }
28
29
    private static function toMonth(DateTimeImmutable $date): DateTimeImmutable
30
    {
31
        return $date->modify('first day of this month midnight');
32
    }
33
34
    public static function wholeMonth(DateTimeImmutable $time): self
35
    {
36
        $start = self::toMonth($time);
37
38
        return new self(
39
            $start,
40
            $start->modify('+1 month'),
41
        );
42
    }
43
44
    /**
45
     * Calculates the usage interval for the given month for the given start and end sale dates.
46
     *
47
     * @param DateTimeImmutable $month the month to calculate the usage interval for
48
     * @param DateTimeImmutable $start the start date of the sale
49
     * @param DateTimeImmutable|null $end the end date of the sale or null if the sale is active
50
     * @throws InvalidArgumentException if the start date is greater than the end date
51
     * @return static
52
     */
53
    public static function withinMonth(
54
        DateTimeImmutable $month,
55
        DateTimeImmutable $start,
56
        ?DateTimeImmutable $end
57
    ): self {
58
        $month = self::toMonth($month);
59
        $nextMonth = $month->modify('+1 month');
60
61
        if ($end !== null && $start > $end) {
62
            throw new InvalidArgumentException('Start date must be less than end date');
63
        }
64
65
        if ($start >= $nextMonth) {
66
            $start = $month;
67
            $end = $month;
68
        }
69
70
        if ($end !== null && $end < $month) {
71
            $start = $month;
72
            $end = $month;
73
        }
74
75
        $effectiveSince = max($start, $month);
76
        $effectiveTill = min(
77
            $end ?? new DateTimeImmutable('2999-01-01'),
78
            $month->modify('+1 month')
79
        );
80
81
        return new self(
82
            $effectiveSince,
83
            $effectiveTill,
84
        );
85
    }
86
87
    /**
88
     * Calculates the usage interval for the given month for the given start date and fraction of month value.
89
     *
90
     * @param DateTimeImmutable $month the month to calculate the usage interval for
91
     * @param DateTimeImmutable $start the start date of the sale
92
     * @param float $fractionOfMonth the fraction of manth
93
     * @return static
94
     */
95
    public static function withMonthAndFraction(
96
        DateTimeImmutable $month,
97
        DateTimeImmutable $start,
98
        float $fractionOfMonth
99
    ): self {
100
        if ($fractionOfMonth < 0 || $fractionOfMonth > 1) {
101
            throw new InvalidArgumentException('Fraction of month must be between 0 and 1');
102
        }
103
        $month = self::toMonth($month);
104
        $nextMonth = $month->modify('+1 month');
105
106
        if ($start >= $nextMonth) {
107
            $start = $month;
108
        }
109
110
        $effectiveSince = max($start, $month);
111
112
        if ($fractionOfMonth === 1.0) {
0 ignored issues
show
The condition $fractionOfMonth === 1.0 is always false.
Loading history...
113
            $effectiveTill = $month->modify('+1 month');
114
        } else {
115
            $interval = new self($month, $nextMonth);
116
            $seconds = $interval->secondsInMonth() * $fractionOfMonth;
117
            $effectiveTill = $effectiveSince->modify(sprintf('+%d seconds', $seconds));
118
        }
119
120
        return new self(
121
            $effectiveSince,
122
            $effectiveTill,
123
        );
124
    }
125
126
    public function start(): DateTimeImmutable
127
    {
128
        return $this->start;
129
    }
130
131
    public function end(): DateTimeImmutable
132
    {
133
        return $this->end;
134
    }
135
136
    public function dateTimeInterval(): DateInterval
137
    {
138
        return $this->start->diff($this->end);
139
    }
140
141
    public function seconds(): int
142
    {
143
        $interval = $this->dateTimeInterval();
144
145
        return $interval->s
146
                + $interval->i * 60
147
                + $interval->h * 3600
148
                + $interval->days * 86400;
149
    }
150
151
    public function minutes(): float
152
    {
153
        return $this->seconds() / 60;
154
    }
155
156
    public function hours(): float
157
    {
158
        return $this->seconds() / 60 / 60;
159
    }
160
161
    public function secondsInMonth(): int
162
    {
163
        return $this->month->format('t') * 86400;
164
    }
165
166
    public function ratioOfMonth(): float
167
    {
168
        $usageSeconds = $this->seconds();
169
        $secondsInCurrentMonth = $this->secondsInMonth();
170
171
        return $usageSeconds / $secondsInCurrentMonth;
172
    }
173
174
    /**
175
     * Extends the usage interval to include both current and other intervals.
176
     *
177
     * @param UsageInterval $other
178
     * @return self
179
     */
180
    public function extend(self $other): self
181
    {
182
        $newStart = min($this->start, $other->start);
183
        $newEnd = max($this->end, $other->end);
184
185
        if ($newStart > $newEnd) {
186
            throw new InvalidArgumentException('Cannot extend intervals: resulting interval would be invalid');
187
        }
188
        return new self(
189
            $newStart,
190
            $newEnd,
191
        );
192
    }
193
194
    public function jsonSerialize(): array
195
    {
196
        return array_filter(get_object_vars($this));
197
    }
198
}
199