DateBase::toUnixTimestamp()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
namespace Ivory\Value;
4
5
use Ivory\Value\Alg\ComparableWithPhpOperators;
6
use Ivory\Value\Alg\IComparable;
7
8
/**
9
 * Common base for date and date/time representations.
10
 *
11
 * @internal Only for the purpose of Ivory itself.
12
 */
13
abstract class DateBase implements IComparable
14
{
15
    use ComparableWithPhpOperators;
16
17
    // NOTE: the order of the fields is important for the `<` and `>` operators to work correctly
18
    /** @var int -1, 0, or 1 if this date is <tt>-infinity</tt>, finite, or <tt>infinity</tt> */
19
    protected $inf;
20
    /** @var \DateTimeImmutable; the UTC timezone is always used */
21
    protected $dt;
22
23
24
    /**
25
     * @return static the special `infinity` date, taking part after any other date
26
     */
27
    public static function infinity(): DateBase
28
    {
29
        static $inst = null;
30
        if ($inst === null) {
31
            $inst = new static(1, null);
32
        }
33
        return $inst;
34
    }
35
36
    /**
37
     * @return static the special `-infinity` date, taking part before any other date
38
     */
39
    public static function minusInfinity(): DateBase
40
    {
41
        static $inst = null;
42
        if ($inst === null) {
43
            $inst = new static(-1, null);
44
        }
45
        return $inst;
46
    }
47
48
    protected static function getUTCTimeZone(): \DateTimeZone
49
    {
50
        static $utc = null;
51
        if ($utc === null) {
52
            $utc = new \DateTimeZone('UTC');
53
        }
54
        return $utc;
55
    }
56
57
58
    /**
59
     * @internal Only for the purpose of Ivory itself.
60
     * @param int $inf
61
     * @param \DateTimeImmutable|null $dt
62
     */
63
    final protected function __construct(int $inf, ?\DateTimeImmutable $dt = null)
64
    {
65
        $this->inf = $inf;
66
        $this->dt = $dt;
67
    }
68
69
    /**
70
     * @return bool <tt>true</tt> if this is a finite date/time,
71
     *              <tt>false</tt> if <tt>infinity</tt> or <tt>-infinity</tt>
72
     */
73
    final public function isFinite(): bool
74
    {
75
        return !$this->inf;
76
    }
77
78
    /**
79
     * @return int|null the year part of the date/time;
80
     *                  years before Christ are negative, starting from -1 for year 1 BC, -2 for year 2 BC, etc.;
81
     *                  <tt>null</tt> iff the date/time is not finite
82
     */
83
    final public function getYear(): ?int
84
    {
85
        $z = $this->getZeroBasedYear();
86
        if ($z > 0 || $z === null) {
87
            return $z;
88
        } else {
89
            return $z - 1;
90
        }
91
    }
92
93
    /**
94
     * Returns the year from this date/time, interpreting years before Christ as non-positive numbers: 0 for year 1 BC,
95
     * -1 for year 2 BC, etc. This is the number appearing as year in the ISO 8601 date string format.
96
     *
97
     * _Ivory design note: not named <tt>getISOYear()</tt> to avoid confusion with <tt>EXTRACT(ISOYEAR FROM ...)</tt>._
98
     *
99
     * @return int|null the year of the date/time, basing year 1 BC as zero;
100
     *                  <tt>null</tt> iff the date/time is not finite
101
     */
102
    final public function getZeroBasedYear(): ?int
103
    {
104
        return ($this->inf ? null : (int)$this->dt->format('Y'));
105
    }
106
107
    /**
108
     * @return int|null the month part of the date/time;
109
     *                  <tt>null</tt> iff the date/time is not finite
110
     */
111
    final public function getMonth(): ?int
112
    {
113
        return ($this->inf ? null : (int)$this->dt->format('n'));
114
    }
115
116
    /**
117
     * @return int|null the day part of the date/time;
118
     *                  <tt>null</tt> iff the date/time is not finite
119
     */
120
    final public function getDay(): ?int
121
    {
122
        return ($this->inf ? null : (int)$this->dt->format('j'));
123
    }
124
125
    /**
126
     * @param string $dateFmt the format string as accepted by {@link date()}
127
     * @return string|null the date/time formatted according to <tt>$dateFmt</tt>;
128
     *                     <tt>null</tt> iff the date/time is not finite
129
     */
130
    final public function format(string $dateFmt): ?string
131
    {
132
        if ($this->inf) {
133
            return null;
134
        } else {
135
            return $this->dt->format($dateFmt);
136
        }
137
    }
138
139
140
    /**
141
     * @return string|null the date/time represented as an ISO 8601 string;
142
     *                     years before Christ represented are using the minus prefix, year 1 BC as <tt>0000</tt>;
143
     *                     <tt>null</tt> iff the date/time is not finite
144
     */
145
    public function toISOString(): ?string
146
    {
147
        if ($this->inf) {
148
            return null;
149
        } else {
150
            return $this->dt->format($this->getISOFormat());
151
        }
152
    }
153
154
    /**
155
     * @return string date format as defined by ISO 8601
156
     */
157
    abstract protected function getISOFormat(): string;
158
159
    /**
160
     * @return int|null the date/time represented as the UNIX timestamp;
161
     *                  <tt>null</tt> iff the date is not finite;
162
     *                  note that a UNIX timestamp represents the number of seconds since 1970-01-01 UTC, i.e., it
163
     *                    corresponds to usage of PHP functions {@link gmmktime()} and {@link gmdate()} rather than
164
     *                    {@link mktime()} or {@link date()}
165
     */
166
    public function toUnixTimestamp(): ?int
167
    {
168
        if ($this->inf) {
169
            return null;
170
        } else {
171
            return $this->dt->getTimestamp();
172
        }
173
    }
174
175
    /**
176
     * @param \DateTimeZone|null $timezone timezone to create the {@link \DateTime} object with;
177
     *                                     if omitted, the current timezone is used
178
     * @return \DateTime|null the date/time represented as a {@link \DateTime} object;
179
     *                        <tt>null</tt> iff the date/time is not finite
180
     */
181
    public function toDateTime(?\DateTimeZone $timezone = null): ?\DateTime
182
    {
183
        if ($this->inf) {
184
            return null;
185
        }
186
        // OPT: \DateTime::createFromFormat() is supposed to be twice as fast as new \DateTime()
187
        $isoStr = $this->toISOString();
188
        try {
189
            return new \DateTime($isoStr, $timezone);
190
        } catch (\Exception $e) {
191
            throw new \LogicException('Date/time error', 0, $e);
192
        }
193
    }
194
195
    /**
196
     * @param \DateTimeZone|null $timezone timezone to create the {@link \DateTime} object with;
197
     *                                     if omitted, the current timezone is used
198
     * @return \DateTimeImmutable|null the date/time represented as a {@link \DateTimeImmutable} object;
199
     *                                 <tt>null</tt> iff the date/time is not finite
200
     */
201
    public function toDateTimeImmutable(?\DateTimeZone $timezone = null): ?\DateTimeImmutable
202
    {
203
        if ($this->inf) {
204
            return null;
205
        }
206
        if ($timezone === $this->dt->getTimezone()) {
207
            return $this->dt;
208
        }
209
210
        $isoStr = $this->toISOString();
211
        try {
212
            return new \DateTimeImmutable($isoStr, $timezone);
213
        } catch (\Exception $e) {
214
            throw new \LogicException('Date/time error', 0, $e);
215
        }
216
    }
217
218
    /**
219
     * Adds a given number of days (1 by default) to this date and returns the result. Only affects finite dates.
220
     *
221
     * @param int $days
222
     * @return static the date/time <tt>$days</tt> days after (or before, if negative) this date/time
223
     */
224
    public function addDay(int $days = 1): DateBase
225
    {
226
        return $this->addPartsImpl(0, 0, $days, 0, 0, 0);
227
    }
228
229
    /**
230
     * Adds a given number of months (1 by default) to this date and returns the result. Only affects finite dates.
231
     *
232
     * Note that addition of months respects the month days, and might actually change the day part. Example:
233
     * - adding 1 month to `2015-05-31` results in `2015-07-01` (June only has 30 days).
234
     *
235
     * @param int $months
236
     * @return static the date/time <tt>$months</tt> months after (or before, if negative) this date/time
237
     */
238
    public function addMonth(int $months = 1): DateBase
239
    {
240
        return $this->addPartsImpl(0, $months, 0, 0, 0, 0);
241
    }
242
243
    /**
244
     * Adds a given number of years (1 by default) to this date/time and returns the result. Only affects finite dates.
245
     *
246
     * @param int $years
247
     * @return static the date/time <tt>$years</tt> years after (or before, if negative) this date/time
248
     */
249
    public function addYear(int $years = 1): DateBase
250
    {
251
        return $this->addPartsImpl($years, 0, 0, 0, 0, 0);
252
    }
253
254
255
    final protected function addPartsImpl(
256
        int $years, int $months, int $days, int $hours, int $minutes, $seconds
257
    ): DateBase {
258
        if ($this->inf) {
259
            return $this;
260
        }
261
262
        $yp = ($years >= 0 ? '+' : '');
263
        $mp = ($months >= 0 ? '+' : '');
264
        $dp = ($days >= 0 ? '+' : '');
265
        $hp = ($hours >= 0 ? '+' : '');
266
        $ip = ($minutes >= 0 ? '+' : '');
267
268
        $wholeSec = (int)$seconds;
269
        $fracSec = $seconds - $wholeSec;
270
        if ($fracSec != 0) {
271
            // in current PHP, there is no method for modifying the microseconds of a date/time - we must do it by hand
272
            $resFracSec = $fracSec + (double)$this->dt->format('.u');
273
            if ($resFracSec < 0) {
274
                $resFracSec++;
275
                $wholeSec--;
276
            } elseif ($resFracSec >= 1) {
277
                $resFracSec--;
278
                $wholeSec++;
279
            }
280
        }
281
        $sp = ($wholeSec >= 0 ? '+' : '');
282
283
        $dt = $this->dt->modify(
284
            "$yp$years years $mp$months months $dp$days days $hp$hours hours $ip$minutes minutes $sp$wholeSec seconds"
285
        );
286
287
        if ($fracSec != 0) {
288
            $resFracSecStr = substr((string)$resFracSec, 2); // cut off the leading '0.'
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $resFracSec does not seem to be defined for all execution paths leading up to this point.
Loading history...
289
            $isoStr = $dt->format('Y-m-d H:i:s.') . $resFracSecStr;
290
            $tz = self::getUTCTimeZone();
291
            try {
292
                $dt = new \DateTimeImmutable($isoStr, $tz);
293
            } catch (\Exception $e) {
294
                throw new \LogicException('Date/time error', 0, $e);
295
            }
296
        }
297
298
        return new static(0, $dt);
299
    }
300
}
301