RelativeTimeFormatter::dateAgoInWords()   F
last analyzed

Complexity

Conditions 17
Paths 264

Size

Total Lines 70

Duplication

Lines 15
Ratio 21.43 %

Importance

Changes 0
Metric Value
cc 17
nc 264
nop 2
dl 15
loc 70
rs 3.5833
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5
 *
6
 * Licensed under The MIT License
7
 * For full copyright and license information, please see the LICENSE.txt
8
 * Redistributions of files must retain the above copyright notice.
9
 *
10
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11
 * @link          https://cakephp.org CakePHP(tm) Project
12
 * @since         3.2.0
13
 * @license       https://opensource.org/licenses/mit-license.php MIT License
14
 */
15
namespace Cake\I18n;
16
17
use Cake\Chronos\ChronosInterface;
18
use DateTimeInterface;
19
20
/**
21
 * Helper class for formatting relative dates & times.
22
 *
23
 * @internal
24
 */
25
class RelativeTimeFormatter
26
{
27
    /**
28
     * Get the difference in a human readable format.
29
     *
30
     * @param \Cake\Chronos\ChronosInterface $date The datetime to start with.
31
     * @param \Cake\Chronos\ChronosInterface|null $other The datetime to compare against.
32
     * @param bool $absolute removes time difference modifiers ago, after, etc
33
     * @return string The difference between the two days in a human readable format
34
     * @see \Cake\Chronos\ChronosInterface::diffForHumans
35
     */
36
    public function diffForHumans(ChronosInterface $date, ChronosInterface $other = null, $absolute = false)
37
    {
38
        $isNow = $other === null;
39
        if ($isNow) {
40
            $other = $date->now($date->getTimezone());
41
        }
42
        $diffInterval = $date->diff($other);
43
44
        switch (true) {
45
            case ($diffInterval->y > 0):
46
                $count = $diffInterval->y;
47
                $message = __dn('cake', '{0} year', '{0} years', $count, $count);
48
                break;
49
            case ($diffInterval->m > 0):
50
                $count = $diffInterval->m;
51
                $message = __dn('cake', '{0} month', '{0} months', $count, $count);
52
                break;
53
            case ($diffInterval->d > 0):
54
                $count = $diffInterval->d;
55
                if ($count >= ChronosInterface::DAYS_PER_WEEK) {
56
                    $count = (int)($count / ChronosInterface::DAYS_PER_WEEK);
57
                    $message = __dn('cake', '{0} week', '{0} weeks', $count, $count);
58
                } else {
59
                    $message = __dn('cake', '{0} day', '{0} days', $count, $count);
60
                }
61
                break;
62
            case ($diffInterval->h > 0):
63
                $count = $diffInterval->h;
64
                $message = __dn('cake', '{0} hour', '{0} hours', $count, $count);
65
                break;
66
            case ($diffInterval->i > 0):
67
                $count = $diffInterval->i;
68
                $message = __dn('cake', '{0} minute', '{0} minutes', $count, $count);
69
                break;
70
            default:
71
                $count = $diffInterval->s;
72
                $message = __dn('cake', '{0} second', '{0} seconds', $count, $count);
73
                break;
74
        }
75
        if ($absolute) {
76
            return $message;
77
        }
78
        $isFuture = $diffInterval->invert === 1;
79
        if ($isNow) {
80
            return $isFuture ? __d('cake', '{0} from now', $message) : __d('cake', '{0} ago', $message);
81
        }
82
83
        return $isFuture ? __d('cake', '{0} after', $message) : __d('cake', '{0} before', $message);
84
    }
85
86
    /**
87
     * Format a into a relative timestring.
88
     *
89
     * @param \DateTimeInterface $time The time instance to format.
90
     * @param array $options Array of options.
91
     * @return string Relative time string.
92
     * @see \Cake\I18n\Time::timeAgoInWords()
93
     */
94
    public function timeAgoInWords(DateTimeInterface $time, array $options = [])
95
    {
96
        $options = $this->_options($options, FrozenTime::class);
97
        if ($options['timezone'] && $time instanceof ChronosInterface) {
98
            $time = $time->timezone($options['timezone']);
0 ignored issues
show
Bug introduced by
It seems like $options['timezone'] can also be of type array; however, Cake\Chronos\ChronosInterface::timezone() does only seem to accept object<DateTimeZone>|string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
99
        }
100
101
        $now = $options['from']->format('U');
102
        $inSeconds = $time->format('U');
103
        $backwards = ($inSeconds > $now);
104
105
        $futureTime = $now;
106
        $pastTime = $inSeconds;
107
        if ($backwards) {
108
            $futureTime = $inSeconds;
109
            $pastTime = $now;
110
        }
111
        $diff = $futureTime - $pastTime;
112
113
        if (!$diff) {
114
            return __d('cake', 'just now', 'just now');
115
        }
116
117 View Code Duplication
        if ($diff > abs($now - (new FrozenTime($options['end']))->format('U'))) {
118
            return sprintf($options['absoluteString'], $time->i18nFormat($options['format']));
0 ignored issues
show
Bug introduced by
The method i18nFormat() does not exist on DateTimeInterface. Did you maybe mean format()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
119
        }
120
121
        $diffData = $this->_diffData($futureTime, $pastTime, $backwards, $options);
122
        list($fNum, $fWord, $years, $months, $weeks, $days, $hours, $minutes, $seconds) = array_values($diffData);
123
124
        $relativeDate = [];
125 View Code Duplication
        if ($fNum >= 1 && $years > 0) {
126
            $relativeDate[] = __dn('cake', '{0} year', '{0} years', $years, $years);
127
        }
128 View Code Duplication
        if ($fNum >= 2 && $months > 0) {
129
            $relativeDate[] = __dn('cake', '{0} month', '{0} months', $months, $months);
130
        }
131 View Code Duplication
        if ($fNum >= 3 && $weeks > 0) {
132
            $relativeDate[] = __dn('cake', '{0} week', '{0} weeks', $weeks, $weeks);
133
        }
134 View Code Duplication
        if ($fNum >= 4 && $days > 0) {
135
            $relativeDate[] = __dn('cake', '{0} day', '{0} days', $days, $days);
136
        }
137
        if ($fNum >= 5 && $hours > 0) {
138
            $relativeDate[] = __dn('cake', '{0} hour', '{0} hours', $hours, $hours);
139
        }
140
        if ($fNum >= 6 && $minutes > 0) {
141
            $relativeDate[] = __dn('cake', '{0} minute', '{0} minutes', $minutes, $minutes);
142
        }
143
        if ($fNum >= 7 && $seconds > 0) {
144
            $relativeDate[] = __dn('cake', '{0} second', '{0} seconds', $seconds, $seconds);
145
        }
146
        $relativeDate = implode(', ', $relativeDate);
147
148
        // When time has passed
149
        if (!$backwards) {
150
            $aboutAgo = [
151
                'second' => __d('cake', 'about a second ago'),
152
                'minute' => __d('cake', 'about a minute ago'),
153
                'hour' => __d('cake', 'about an hour ago'),
154
                'day' => __d('cake', 'about a day ago'),
155
                'week' => __d('cake', 'about a week ago'),
156
                'month' => __d('cake', 'about a month ago'),
157
                'year' => __d('cake', 'about a year ago'),
158
            ];
159
160
            return $relativeDate ? sprintf($options['relativeString'], $relativeDate) : $aboutAgo[$fWord];
161
        }
162
163
        // When time is to come
164
        if ($relativeDate) {
165
            return $relativeDate;
166
        }
167
        $aboutIn = [
168
            'second' => __d('cake', 'in about a second'),
169
            'minute' => __d('cake', 'in about a minute'),
170
            'hour' => __d('cake', 'in about an hour'),
171
            'day' => __d('cake', 'in about a day'),
172
            'week' => __d('cake', 'in about a week'),
173
            'month' => __d('cake', 'in about a month'),
174
            'year' => __d('cake', 'in about a year'),
175
        ];
176
177
        return $aboutIn[$fWord];
178
    }
179
180
    /**
181
     * Calculate the data needed to format a relative difference string.
182
     *
183
     * @param int|string $futureTime The timestamp from the future.
184
     * @param int|string $pastTime The timestamp from the past.
185
     * @param bool $backwards Whether or not the difference was backwards.
186
     * @param array $options An array of options.
187
     * @return array An array of values.
188
     */
189
    protected function _diffData($futureTime, $pastTime, $backwards, $options)
190
    {
191
        $diff = (int)$futureTime - (int)$pastTime;
192
193
        // If more than a week, then take into account the length of months
194
        if ($diff >= 604800) {
195
            list($future['H'], $future['i'], $future['s'], $future['d'], $future['m'], $future['Y']) = explode('/', date('H/i/s/d/m/Y', $futureTime));
196
197
            list($past['H'], $past['i'], $past['s'], $past['d'], $past['m'], $past['Y']) = explode('/', date('H/i/s/d/m/Y', $pastTime));
198
            $weeks = $days = $hours = $minutes = $seconds = 0;
199
200
            $years = (int)$future['Y'] - (int)$past['Y'];
0 ignored issues
show
Bug introduced by
The variable $future does not exist. Did you mean $futureTime?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
201
            $months = (int)$future['m'] + ((12 * $years) - (int)$past['m']);
0 ignored issues
show
Bug introduced by
The variable $future does not exist. Did you mean $futureTime?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
202
203
            if ($months >= 12) {
204
                $years = floor($months / 12);
205
                $months -= ($years * 12);
206
            }
207
            if ((int)$future['m'] < (int)$past['m'] && (int)$future['Y'] - (int)$past['Y'] === 1) {
0 ignored issues
show
Bug introduced by
The variable $future does not exist. Did you mean $futureTime?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
208
                $years--;
209
            }
210
211
            if ((int)$future['d'] >= (int)$past['d']) {
0 ignored issues
show
Bug introduced by
The variable $future does not exist. Did you mean $futureTime?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
212
                $days = (int)$future['d'] - (int)$past['d'];
0 ignored issues
show
Bug introduced by
The variable $future does not exist. Did you mean $futureTime?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
213
            } else {
214
                $daysInPastMonth = (int)date('t', $pastTime);
215
                $daysInFutureMonth = (int)date('t', mktime(0, 0, 0, (int)$future['m'] - 1, 1, (int)$future['Y']));
0 ignored issues
show
Bug introduced by
The variable $future does not exist. Did you mean $futureTime?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
216
217
                if (!$backwards) {
218
                    $days = ($daysInPastMonth - (int)$past['d']) + (int)$future['d'];
0 ignored issues
show
Bug introduced by
The variable $future does not exist. Did you mean $futureTime?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
219
                } else {
220
                    $days = ($daysInFutureMonth - (int)$past['d']) + (int)$future['d'];
0 ignored issues
show
Bug introduced by
The variable $future does not exist. Did you mean $futureTime?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
221
                }
222
223
                if ($future['m'] != $past['m']) {
0 ignored issues
show
Bug introduced by
The variable $future does not exist. Did you mean $futureTime?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
224
                    $months--;
225
                }
226
            }
227
228
            if (!$months && $years >= 1 && $diff < ($years * 31536000)) {
229
                $months = 11;
230
                $years--;
231
            }
232
233
            if ($months >= 12) {
234
                $years++;
235
                $months -= 12;
236
            }
237
238
            if ($days >= 7) {
239
                $weeks = floor($days / 7);
240
                $days -= ($weeks * 7);
241
            }
242
        } else {
243
            $years = $months = $weeks = 0;
244
            $days = floor($diff / 86400);
245
246
            $diff -= ($days * 86400);
247
248
            $hours = floor($diff / 3600);
249
            $diff -= ($hours * 3600);
250
251
            $minutes = floor($diff / 60);
252
            $diff -= ($minutes * 60);
253
            $seconds = $diff;
254
        }
255
256
        $fWord = $options['accuracy']['second'];
257
        if ($years > 0) {
258
            $fWord = $options['accuracy']['year'];
259
        } elseif (abs($months) > 0) {
260
            $fWord = $options['accuracy']['month'];
261
        } elseif (abs($weeks) > 0) {
262
            $fWord = $options['accuracy']['week'];
263
        } elseif (abs($days) > 0) {
264
            $fWord = $options['accuracy']['day'];
265
        } elseif (abs($hours) > 0) {
266
            $fWord = $options['accuracy']['hour'];
267
        } elseif (abs($minutes) > 0) {
268
            $fWord = $options['accuracy']['minute'];
269
        }
270
271
        $fNum = str_replace(['year', 'month', 'week', 'day', 'hour', 'minute', 'second'], [1, 2, 3, 4, 5, 6, 7], $fWord);
272
273
        return [$fNum, $fWord, $years, $months, $weeks, $days, $hours, $minutes, $seconds];
274
    }
275
276
    /**
277
     * Format a into a relative date string.
278
     *
279
     * @param \DateTimeInterface $date The date to format.
280
     * @param array $options Array of options.
281
     * @return string Relative date string.
282
     * @see \Cake\I18n\Date::timeAgoInWords()
283
     */
284
    public function dateAgoInWords(DateTimeInterface $date, array $options = [])
285
    {
286
        $options = $this->_options($options, FrozenDate::class);
287
        if ($options['timezone'] && $date instanceof ChronosInterface) {
288
            $date = $date->timezone($options['timezone']);
0 ignored issues
show
Bug introduced by
It seems like $options['timezone'] can also be of type array; however, Cake\Chronos\ChronosInterface::timezone() does only seem to accept object<DateTimeZone>|string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
289
        }
290
291
        $now = $options['from']->format('U');
292
        $inSeconds = $date->format('U');
293
        $backwards = ($inSeconds > $now);
294
295
        $futureTime = $now;
296
        $pastTime = $inSeconds;
297
        if ($backwards) {
298
            $futureTime = $inSeconds;
299
            $pastTime = $now;
300
        }
301
        $diff = $futureTime - $pastTime;
302
303
        if (!$diff) {
304
            return __d('cake', 'today');
305
        }
306
307 View Code Duplication
        if ($diff > abs($now - (new FrozenDate($options['end']))->format('U'))) {
308
            return sprintf($options['absoluteString'], $date->i18nFormat($options['format']));
0 ignored issues
show
Bug introduced by
The method i18nFormat() does not exist on DateTimeInterface. Did you maybe mean format()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
309
        }
310
311
        $diffData = $this->_diffData($futureTime, $pastTime, $backwards, $options);
312
        list($fNum, $fWord, $years, $months, $weeks, $days) = array_values($diffData);
313
314
        $relativeDate = [];
315 View Code Duplication
        if ($fNum >= 1 && $years > 0) {
316
            $relativeDate[] = __dn('cake', '{0} year', '{0} years', $years, $years);
317
        }
318 View Code Duplication
        if ($fNum >= 2 && $months > 0) {
319
            $relativeDate[] = __dn('cake', '{0} month', '{0} months', $months, $months);
320
        }
321 View Code Duplication
        if ($fNum >= 3 && $weeks > 0) {
322
            $relativeDate[] = __dn('cake', '{0} week', '{0} weeks', $weeks, $weeks);
323
        }
324 View Code Duplication
        if ($fNum >= 4 && $days > 0) {
325
            $relativeDate[] = __dn('cake', '{0} day', '{0} days', $days, $days);
326
        }
327
        $relativeDate = implode(', ', $relativeDate);
328
329
        // When time has passed
330
        if (!$backwards) {
331
            $aboutAgo = [
332
                'day' => __d('cake', 'about a day ago'),
333
                'week' => __d('cake', 'about a week ago'),
334
                'month' => __d('cake', 'about a month ago'),
335
                'year' => __d('cake', 'about a year ago'),
336
            ];
337
338
            return $relativeDate ? sprintf($options['relativeString'], $relativeDate) : $aboutAgo[$fWord];
339
        }
340
341
        // When time is to come
342
        if ($relativeDate) {
343
            return $relativeDate;
344
        }
345
        $aboutIn = [
346
            'day' => __d('cake', 'in about a day'),
347
            'week' => __d('cake', 'in about a week'),
348
            'month' => __d('cake', 'in about a month'),
349
            'year' => __d('cake', 'in about a year'),
350
        ];
351
352
        return $aboutIn[$fWord];
353
    }
354
355
    /**
356
     * Build the options for relative date formatting.
357
     *
358
     * @param array $options The options provided by the user.
359
     * @param string $class The class name to use for defaults.
360
     * @return array Options with defaults applied.
361
     */
362
    protected function _options($options, $class)
363
    {
364
        $options += [
365
            'from' => $class::now(),
366
            'timezone' => null,
367
            'format' => $class::$wordFormat,
368
            'accuracy' => $class::$wordAccuracy,
369
            'end' => $class::$wordEnd,
370
            'relativeString' => __d('cake', '%s ago'),
371
            'absoluteString' => __d('cake', 'on %s'),
372
        ];
373
        if (is_string($options['accuracy'])) {
374
            $accuracy = $options['accuracy'];
375
            $options['accuracy'] = [];
376
            foreach ($class::$wordAccuracy as $key => $level) {
377
                $options['accuracy'][$key] = $accuracy;
378
            }
379
        } else {
380
            $options['accuracy'] += $class::$wordAccuracy;
381
        }
382
383
        return $options;
384
    }
385
}
386