Completed
Push — issue/v4/1096-manage-applicati... ( c1709d...2578ad )
by
unknown
07:36 queued 04:09
created

DateRange::normalize()   D

Complexity

Conditions 10
Paths 5

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 39
rs 4.8196
cc 10
eloc 22
nc 5
nop 1

How to fix   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
 * BEdita, API-first content management framework
4
 * Copyright 2017 ChannelWeb Srl, Chialab Srl
5
 *
6
 * This file is part of BEdita: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as published
8
 * by the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
12
 */
13
14
namespace BEdita\Core\Model\Entity;
15
16
use Cake\I18n\Time;
17
use Cake\ORM\Entity;
18
19
/**
20
 * DateRange Entity
21
 *
22
 * @property int $id
23
 * @property int $object_id
24
 * @property \Cake\I18n\Time $start_date
25
 * @property \Cake\I18n\Time|null $end_date
26
 * @property array $params
27
 *
28
 * @property \BEdita\Core\Model\Entity\ObjectEntity $object
29
 */
30
class DateRange extends Entity
31
{
32
33
    /**
34
     * {@inheritDoc}
35
     */
36
    protected $_accessible = [
37
        '*' => true,
38
        'id' => false
39
    ];
40
41
    /**
42
     * {@inheritDoc}
43
     */
44
    protected $_hidden = [
45
        'id',
46
        'object_id',
47
    ];
48
49
    /**
50
     * Check if this Date Range is before the passed Date Range.
51
     *
52
     * A Date Range is "before" another Date Range if its `end_date` is lower than,
53
     * or equal, to the other Date Range's `start_date`. If `end_date` is `null`,
54
     * for this purpose it assumes the same value as `start_date`.
55
     *
56
     * **Warning**: this method **does not** take `params` into account.
57
     *
58
     * @param \BEdita\Core\Model\Entity\DateRange $dateRange Date Range being compared.
59
     * @return bool
60
     */
61
    public function isBefore(DateRange $dateRange)
62
    {
63
        static::checkWellFormed($this, $dateRange);
64
65
        return $this->start_date < $dateRange->start_date && ($this->end_date === null || $this->end_date <= $dateRange->start_date);
66
    }
67
68
    /**
69
     * Check if this Date Range is after the passed Date Range.
70
     *
71
     * A Date Range is "after" another Date Range if its `start_date` is greater than,
72
     * or equal, to the other Date Range's `end_date`. If `end_date` is `null`,
73
     * for this purpose it assumes the same value as `start_date`.
74
     *
75
     * **Warning**: this method **does not** take `params` into account.
76
     *
77
     * @param \BEdita\Core\Model\Entity\DateRange $dateRange Date Range being compared.
78
     * @return bool
79
     */
80
    public function isAfter(DateRange $dateRange)
81
    {
82
        static::checkWellFormed($this, $dateRange);
83
84
        return $this->start_date > $dateRange->start_date && ($dateRange->end_date === null || $this->start_date >= $dateRange->end_date);
85
    }
86
87
    /**
88
     * Normalize an array of Date Ranges by sorting and joining overlapping Date Ranges.
89
     *
90
     * Normalization sorts Date Ranges in a set by `start_date` in ascending order.
91
     * Also, if two or more Date Ranges do overlap, or are adjacent
92
     * (i.e. `$d1->end_date === $d2->start_date`), they are merged in one Date Range.
93
     * Duplicate Date Ranges are removed.
94
     *
95
     * **Warning**: this method **does not** take `params` into account.
96
     *
97
     * @param \BEdita\Core\Model\Entity\DateRange[] $dateRanges Set of Date Ranges.
98
     * @return \BEdita\Core\Model\Entity\DateRange[]
99
     */
100
    public static function normalize(array $dateRanges)
101
    {
102
        if (empty($dateRanges)) {
103
            return [];
104
        }
105
        static::checkWellFormed(...$dateRanges);
106
107
        // Sort items.
108
        usort($dateRanges, function (DateRange $dateRange1, DateRange $dateRange2) {
109
            if ($dateRange1->isBefore($dateRange2)) {
110
                return -1;
111
            }
112
            if ($dateRange1->isAfter($dateRange2)) {
113
                return 1;
114
            }
115
116
            return 0;
117
        });
118
119
        // Merge items.
120
        $result = [];
121
        $last = array_shift($dateRanges);
122
        while (($current = array_shift($dateRanges)) !== null) {
123
            if ($last->isBefore($current) && $last->end_date < $current->start_date) {
124
                $result[] = $last;
125
                $last = $current;
126
127
                continue;
128
            }
129
130
            $last->start_date = $last->start_date->min($current->start_date);
131
            if ($last->end_date === null || ($current->end_date !== null && $last->end_date < $current->end_date)) {
132
                $last->end_date = $current->end_date;
133
            }
134
        }
135
        $result[] = $last;
136
137
        return $result;
138
    }
139
140
    /**
141
     * Compute union of multiple sets of Date Ranges.
142
     *
143
     * This method computes union of multiple sets of Date Ranges.
144
     * The result is returned in normalized form.
145
     *
146
     * **Warning**: this method **does not** take `params` into account.
147
     *
148
     * @param \BEdita\Core\Model\Entity\DateRange[][] ...$dateRanges Set of Date Ranges.
149
     * @return \BEdita\Core\Model\Entity\DateRange[]
150
     */
151
    public static function union(...$dateRanges)
0 ignored issues
show
introduced by
Expected 1 space between type hint and argument "$dateRanges"; 0 found
Loading history...
152
    {
153
        $dateRanges = call_user_func_array('array_merge', $dateRanges);
154
155
        return static::normalize($dateRanges);
156
    }
157
158
    /**
159
     * Compute difference between two sets of Date Ranges.
160
     *
161
     * When computing complement of `$array1` with respect to `$array2`:
162
     *  - Date Ranges with `end_date = null` are treated as unit sets, all
163
     *    other Date Ranges are considered intervals.
164
     *  - complement of an interval with respect to another interval results
165
     *    in the difference of the two sets.
166
     *  - complement of an interval with respect to a unit set results in
167
     *    the interval unmodified.
168
     *  - complement of a unit sets with respect to an interval results in
169
     *    either the unit set unmodified if they are not overlapping, or in
170
     *    the empty set otherwise.
171
     *  - complement of a unit sets with respect to another unit set results
172
     *    in either the unit set unmodified if they are not the same, or in
173
     *    the empty set otherwise.
174
     *
175
     * **Warning**: this method does **not** take `params` into account.
176
     *
177
     * @param \BEdita\Core\Model\Entity\DateRange[] $array1 First set of Date Ranges.
178
     * @param \BEdita\Core\Model\Entity\DateRange[] $array2 Second set of Date Ranges.
179
     * @return \BEdita\Core\Model\Entity\DateRange[]
180
     */
181
    public static function diff(array $array1, array $array2)
182
    {
183
        // Ensure arrays are normalized.
184
        $array1 = static::normalize($array1);
185
        $array2 = static::normalize($array2);
186
187
        $result = [];
188
        $dateRange = null;
189
        foreach ($array1 as $dateRange1) {
190
            if ($dateRange !== null) {
191
                $result[] = $dateRange;
192
            }
193
            $dateRange = clone $dateRange1;
194
195
            while (($dateRange2 = current($array2)) !== false) {
196
                if ($dateRange->end_date === null && $dateRange2->end_date === null && $dateRange->start_date == $dateRange2->start_date) {
197
                    // Unit sets match. Discard range.
198
                    $dateRange = null;
199
                    next($array2);
200
201
                    break;
202
                }
203
                if ($dateRange2->end_date === null || $dateRange2->isBefore($dateRange)) {
204
                    // Does not affect intersection.
205
                    next($array2);
206
207
                    continue;
208
                }
209
                if ($dateRange2->isAfter($dateRange)) {
210
                    // A step too far.
211
                    break;
212
                }
213
                if ($dateRange->start_date < $dateRange2->start_date) {
214
                    // Split the range.
215
                    $temp = clone $dateRange;
216
                    $temp->end_date = $dateRange2->start_date;
217
                    $result[] = $temp;
218
219
                    $dateRange->start_date = $dateRange2->start_date;
220
                }
221
                if ($dateRange->end_date < $dateRange2->end_date) {
222
                    // Discard range.
223
                    $dateRange = null;
224
225
                    break;
226
                }
227
228
                $dateRange->start_date = $dateRange2->end_date;
229
230
                next($array2);
231
            }
232
        }
233
234
        if ($dateRange !== null) {
235
            $result[] = $dateRange;
236
        }
237
238
        return $result;
239
    }
240
241
    /**
242
     * Check that all the Date Ranges passed as arguments are actually well formed.
243
     *
244
     * A "well formed" Date Range is an instance of class {@see \BEdita\Core\Model\Entity\DateRange}
245
     * whose field `start_date` is an instance of {@see Cake\I18n\Time} and field `end_date` is
246
     * either `null` or an instance of {@see Cake\I18n\Time}.
247
     *
248
     * @param array ...$dateRanges Date Ranges to check.
249
     * @return void
250
     * @throws \LogicException Throws an exception if a malformed Date Range is encountered.
251
     */
252
    public static function checkWellFormed(... $dateRanges)
253
    {
254
        $getType = function ($var) {
255
            if (!is_object($var)) {
256
                return gettype($var);
257
            }
258
259
            return get_class($var);
260
        };
261
262
        foreach ($dateRanges as $dateRange) {
263
            if (!($dateRange instanceof static)) {
264
                throw new \LogicException(
265
                    __d('bedita', 'Invalid Date Range entity class: expected "{0}", got "{1}"', static::class, $getType($dateRange))
266
                );
267
            }
268
269 View Code Duplication
            if (!($dateRange->start_date instanceof Time)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
270
                throw new \LogicException(
271
                    __d('bedita', 'Invalid "{0}": expected "{1}", got "{2}"', 'start_date', Time::class, $getType($dateRange->start_date))
272
                );
273
            }
274
275 View Code Duplication
            if (!($dateRange->end_date instanceof Time) && $dateRange->end_date !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
276
                throw new \LogicException(
277
                    __d('bedita', 'Invalid "{0}": expected "{1}", got "{2}"', 'end_date', Time::class, $getType($dateRange->end_date))
278
                );
279
            }
280
        }
281
    }
282
}
283