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
introduced
by
![]() |
|||
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 |