Completed
Pull Request — master (#20)
by Arnold
01:57
created

DateExtension::duration()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

Changes 4
Bugs 1 Features 1
Metric Value
cc 5
eloc 8
c 4
b 1
f 1
nc 4
nop 3
dl 0
loc 17
ccs 9
cts 9
cp 1
crap 5
rs 9.6111
1
<?php
2
3
namespace Jasny\Twig;
4
5
use Twig\Extension\AbstractExtension;
6
use Twig\TwigFilter;
7
8
/**
9
 * Format a date based on the current locale in Twig
10
 */
11
class DateExtension extends AbstractExtension
12
{
13
    /**
14
     * Class constructor
15
     */
16 40
    public function __construct()
17
    {
18 40
        if (!extension_loaded('intl')) {
19
            throw new \Exception("The Date Twig extension requires the 'intl' PHP extension."); // @codeCoverageIgnore
20
        }
21 40
    }
22
23
24
    /**
25
     * Return extension name
26
     *
27
     * @return string
28
     */
29
    public function getName()
30
    {
31
        return 'jasny/date';
32
    }
33
34
    /**
35
     * Callback for Twig to get all the filters.
36
     *
37
     * @return \Twig\TwigFilter[]
38
     */
39 30
    public function getFilters()
40
    {
41
        return [
42 30
            new TwigFilter('localdate', [$this, 'localDate']),
43 30
            new TwigFilter('localtime', [$this, 'localTime']),
44 30
            new TwigFilter('localdatetime', [$this, 'localDateTime']),
45 30
            new TwigFilter('duration', [$this, 'duration']),
46 30
            new TwigFilter('age', [$this, 'age']),
47
        ];
48
    }
49
50
    /**
51
     * Turn a value into a DateTime object
52
     *
53
     * @param string|int|\DateTime $date
54
     * @return \DateTime
55
     */
56 22
    protected function valueToDateTime($date)
57
    {
58 22
        if (!$date instanceof \DateTime) {
59 22
            $date = is_int($date) ? \DateTime::createFromFormat('U', $date) : new \DateTime((string)$date);
60
        }
61
62 22
        if ($date === false) {
63
            throw new RuntimeError("Invalid date '$date'");
0 ignored issues
show
introduced by
Instantiated class Jasny\Twig\RuntimeError not found.
Loading history...
Bug introduced by
The type Jasny\Twig\RuntimeError was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
64
        }
65
66 22
        return $date;
67
    }
68
69
    /**
70
     * Get configured intl date formatter.
71
     *
72
     * @param string|null $dateFormat
73
     * @param string|null $timeFormat
74
     * @param string      $calendar
75
     * @return \IntlDateFormatter
76
     */
77 20
    protected function getDateFormatter($dateFormat, $timeFormat, $calendar)
78
    {
79 20
        $datetype = isset($dateFormat) ? $this->getFormat($dateFormat) : null;
80 20
        $timetype = isset($timeFormat) ? $this->getFormat($timeFormat) : null;
81
82 20
        $calendarConst = $calendar === 'traditional' ? \IntlDateFormatter::TRADITIONAL : \IntlDateFormatter::GREGORIAN;
83
84 20
        $pattern = $this->getDateTimePattern(
85 20
            isset($datetype) ? $datetype : $dateFormat,
86 20
            isset($timetype) ? $timetype : $timeFormat,
87
            $calendarConst
88
        );
89
90 20
        return new \IntlDateFormatter(\Locale::getDefault(), $datetype, $timetype, null, $calendarConst, $pattern);
91
    }
92
93
    /**
94
     * Format the date/time value as a string based on the current locale
95
     *
96
     * @param string|false $format  'short', 'medium', 'long', 'full', 'none' or false
97
     * @return int|null
98
     */
99 20
    protected function getFormat($format)
100
    {
101 20
        if ($format === false) {
102 18
            $format = 'none';
103
        }
104
105
        $types = [
106 20
            'none' => \IntlDateFormatter::NONE,
107
            'short' => \IntlDateFormatter::SHORT,
108
            'medium' => \IntlDateFormatter::MEDIUM,
109
            'long' => \IntlDateFormatter::LONG,
110
            'full' => \IntlDateFormatter::FULL
111
        ];
112
113 20
        return isset($types[$format]) ? $types[$format] : null;
114
    }
115
116
    /**
117
     * Get the date/time pattern.
118
     *
119
     * @param int|string $datetype
120
     * @param int|string $timetype
121
     * @param int        $calendar
122
     * @return string
123
     */
124 20
    protected function getDateTimePattern($datetype, $timetype, $calendar = \IntlDateFormatter::GREGORIAN)
125
    {
126 20
        if (is_int($datetype) && is_int($timetype)) {
127 10
            return null;
128
        }
129
130 10
        return $this->getDatePattern(
131 10
            isset($datetype) ? $datetype : \IntlDateFormatter::SHORT,
132 10
            isset($timetype) ? $timetype : \IntlDateFormatter::SHORT,
133
            $calendar
134
        );
135
    }
136
137
    /**
138
     * Get the formatter to create a date and/or time pattern
139
     *
140
     * @param int|string $datetype
141
     * @param int|string $timetype
142
     * @param int        $calendar
143
     * @return \IntlDateFormatter
144
     */
145 4
    protected function getDatePatternFormatter($datetype, $timetype, $calendar = \IntlDateFormatter::GREGORIAN)
146
    {
147 4
        return \IntlDateFormatter::create(
148 4
            \Locale::getDefault(),
149 4
            is_int($datetype) ? $datetype : \IntlDateFormatter::NONE,
150 4
            is_int($timetype) ? $timetype : \IntlDateFormatter::NONE,
151 4
            \IntlTimeZone::getGMT(),
0 ignored issues
show
Bug introduced by
IntlTimeZone::getGMT() of type IntlTimeZone is incompatible with the type string expected by parameter $timezone of IntlDateFormatter::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

151
            /** @scrutinizer ignore-type */ \IntlTimeZone::getGMT(),
Loading history...
152 4
            $calendar
153
        );
154
    }
155
156
    /**
157
     * Get the date and/or time pattern
158
     * Default date pattern is short date pattern with 4 digit year.
159
     *
160
     * @param int|string $datetype
161
     * @param int|string $timetype
162
     * @param int        $calendar
163
     * @return string
164
     */
165 10
    protected function getDatePattern($datetype, $timetype, $calendar = \IntlDateFormatter::GREGORIAN)
166
    {
167
        $createPattern =
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: $createPattern = (is_int...ntlDateFormatter::NONE), Probably Intended Meaning: $createPattern = is_int(...ntlDateFormatter::NONE)
Loading history...
168 10
            (is_int($datetype) && $datetype !== \IntlDateFormatter::NONE) ||
169 10
            (is_int($timetype) && $timetype !== \IntlDateFormatter::NONE);
170
171 10
        $pattern = $createPattern ? $this->getDatePatternFormatter($datetype, $timetype, $calendar)->getPattern() : '';
172
173 10
        return trim(
174 10
            (is_string($datetype) ? $datetype . ' ' : '') .
175 10
            preg_replace('/\byy?\b/', 'yyyy', $pattern) .
176 10
            (is_string($timetype) ? ' ' . $timetype : '')
177
        );
178
    }
179
180
    /**
181
     * Format the date and/or time value as a string based on the current locale
182
     *
183
     * @param \DateTime|int|string $value
184
     * @param string               $dateFormat  null, 'short', 'medium', 'long', 'full' or pattern
185
     * @param string               $timeFormat  null, 'short', 'medium', 'long', 'full' or pattern
186
     * @param string               $calendar    'gregorian' or 'traditional'
187
     * @return string
188
     */
189 23
    protected function formatLocal($value, $dateFormat, $timeFormat, $calendar = 'gregorian')
190
    {
191 23
        if (!isset($value)) {
192 3
            return null;
193
        }
194
195 20
        $date = $this->valueToDateTime($value);
196 20
        $formatter = $this->getDateFormatter($dateFormat, $timeFormat, $calendar);
197
198 20
        return $formatter->format($date->getTimestamp());
199
    }
200
201
    /**
202
     * Format the date value as a string based on the current locale
203
     *
204
     * @param DateTime|int|string $date
0 ignored issues
show
Bug introduced by
The type Jasny\Twig\DateTime was not found. Did you mean DateTime? If so, make sure to prefix the type with \.
Loading history...
205
     * @param string              $format    null, 'short', 'medium', 'long', 'full' or pattern
206
     * @param string              $calendar  'gregorian' or 'traditional'
207
     * @return string
208
     */
209 11
    public function localDate($date, $format = null, $calendar = 'gregorian')
210
    {
211 11
        return $this->formatLocal($date, $format, false, $calendar);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type string expected by parameter $timeFormat of Jasny\Twig\DateExtension::formatLocal(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

211
        return $this->formatLocal($date, $format, /** @scrutinizer ignore-type */ false, $calendar);
Loading history...
212
    }
213
214
    /**
215
     * Format the time value as a string based on the current locale
216
     *
217
     * @param DateTime|int|string $date
218
     * @param string              $format    'short', 'medium', 'long', 'full' or pattern
219
     * @param string              $calendar  'gregorian' or 'traditional'
220
     * @return string
221
     */
222 7
    public function localTime($date, $format = 'short', $calendar = 'gregorian')
223
    {
224 7
        return $this->formatLocal($date, false, $format, $calendar);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type string expected by parameter $dateFormat of Jasny\Twig\DateExtension::formatLocal(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

224
        return $this->formatLocal($date, /** @scrutinizer ignore-type */ false, $format, $calendar);
Loading history...
225
    }
226
227
    /**
228
     * Format the date/time value as a string based on the current locale
229
     *
230
     * @param DateTime|int|string $date
231
     * @param string              $format    date format, pattern or ['date'=>format, 'time'=>format)
232
     * @param string              $calendar  'gregorian' or 'traditional'
233
     * @return string
234
     */
235 5
    public function localDateTime($date, $format = null, $calendar = 'gregorian')
236
    {
237 5
        if (is_array($format) || $format instanceof \stdClass || !isset($format)) {
238 2
            $formatDate = isset($format['date']) ? $format['date'] : null;
239 2
            $formatTime = isset($format['time']) ? $format['time'] : 'short';
240
        } else {
241 3
            $formatDate = $format;
242 3
            $formatTime = false;
243
        }
244
245 5
        return $this->formatLocal($date, $formatDate, $formatTime, $calendar);
246
    }
247
248
249
    /**
250
     * Split duration into seconds, minutes, hours, days, weeks and years.
251
     *
252
     * @param int $seconds
253
     * @return array
254
     */
255 13
    protected function splitDuration($seconds, $max)
256
    {
257 13
        if ($max < 1 || $seconds < 60) {
258 1
            return [$seconds];
259
        }
260
261 12
        $minutes = floor($seconds / 60);
262 12
        $seconds = $seconds % 60;
263 12
        if ($max < 2 || $minutes < 60) {
264 2
            return [$seconds, $minutes];
265
        }
266
267 10
        $hours = floor($minutes / 60);
268 10
        $minutes = $minutes % 60;
269 10
        if ($max < 3 || $hours < 24) {
270 4
            return [$seconds, $minutes, $hours];
271
        }
272
273 6
        $days = floor($hours / 24);
274 6
        $hours = $hours % 24;
275 6
        if ($max < 4 || $days < 7) {
276 2
            return [$seconds, $minutes, $hours, $days];
277
        }
278
279 4
        $weeks = floor($days / 7);
280 4
        $days = $days % 7;
281 4
        if ($max < 5 || $weeks < 52) {
282 2
            return [$seconds, $minutes, $hours, $days, $weeks];
283
        }
284
285 2
        $years = floor($weeks / 52);
286 2
        $weeks = $weeks % 52;
287 2
        return [$seconds, $minutes, $hours, $days, $weeks, $years];
288
    }
289
290
    /**
291
     * Calculate duration from seconds.
292
     * One year is seen as exactly 52 weeks.
293
     *
294
     * Use null to skip a unit.
295
     *
296
     * @param int    $value     Time in seconds
297
     * @param array  $units     Time units (seconds, minutes, hours, days, weeks, years)
298
     * @param string $separator
299
     * @return string
300
     */
301 14
    public function duration($value, $units = ['s', 'm', 'h', 'd', 'w', 'y'], $separator = ' ')
302
    {
303 14
        if (!isset($value)) {
304 1
            return null;
305
        }
306
307 13
        $parts = $this->splitDuration($value, count($units) - 1) + array_fill(0, 6, null);
308
309 13
        $duration = '';
310
311 13
        for ($i = 5; $i >= 0; $i--) {
312 13
            if (isset($parts[$i]) && isset($units[$i])) {
313 13
                $duration .= $separator . $parts[$i] . $units[$i];
314
            }
315
        }
316
317 13
        return trim($duration, $separator);
318
    }
319
320
    /**
321
     * Get the age (in years) based on a date.
322
     *
323
     * @param DateTime|string $value
324
     * @return int
325
     */
326 3
    public function age($value)
327
    {
328 3
        if (!isset($value)) {
329 1
            return null;
330
        }
331
332 2
        $date = $this->valueToDateTime($value);
333
334 2
        return $date->diff(new \DateTime())->format('%y');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $date->diff(new DateTime())->format('%y') returns the type string which is incompatible with the documented return type integer.
Loading history...
335
    }
336
}
337