Passed
Push — master ( 5ce7f7...dbb49b )
by Victor
09:47
created

Flattener::flatten()   B

Complexity

Conditions 6
Paths 14

Size

Total Lines 58
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 25
c 2
b 0
f 0
nc 14
nop 1
dl 0
loc 58
rs 8.8977

How to fix   Long Method   

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
namespace Vctls\IntervalGraph;
4
5
use Closure;
6
7
use function count;
8
9
/**
10
 * Transforms an array of overlapping intervals into another array of adjacent intervals.
11
 *
12
 * Each new interval holds the keys of the corresponding original intervals.
13
 *
14
 * @package Vctls\IntervalGraph
15
 */
16
class Flattener implements FlattenerInterface
17
{
18
    /** @var Closure Substract one step from a bound value. */
19
    protected $substractStep;
20
21
    /** @var Closure Add one step to the bound value. */
22
    protected $addStep;
23
24
    /**
25
     * Set the Closure for decrementing bound values when dealing with
26
     * discontinuous sets.
27
     *
28
     * @param Closure $substractStep
29
     * @return Flattener
30
     */
31
    public function setSubstractStep(Closure $substractStep): FlattenerInterface
32
    {
33
        $this->substractStep = $substractStep;
34
        return $this;
35
    }
36
37
    /**
38
     * Set the Closure for incrementing bound values when dealing with
39
     * discontinuous sets.
40
     *
41
     * @param Closure $addStep
42
     * @return Flattener
43
     */
44
    public function setAddStep(Closure $addStep): FlattenerInterface
45
    {
46
        $this->addStep = $addStep;
47
        return $this;
48
    }
49
50
    /**
51
     * Make an array of bounds from an array of intervals.
52
     *
53
     * Assign the value of the interval to each bound.
54
     *
55
     * Assign and a '+' sign if it is a low bound, and a '-' if it is an high bound.
56
     * ```
57
     * bound = [
58
     *   bound value,
59
     *   bound type,
60
     *   TODO included,
61
     *   interval key,
62
     *   interval value
63
     * ]
64
     * ```
65
     *
66
     * @param array[] $intervals
67
     * @return array[]
68
     */
69
    public static function intervalsToSignedBounds(array $intervals): array
70
    {
71
        $bounds = [];
72
        foreach ($intervals as $key => $interval) {
73
            // TODO Get included boolean from interval bound.
74
            $bounds[] = [$interval[1], '-', true, $key, $interval[2] ?? null];
75
            $bounds[] = [$interval[0], '+', true, $key, $interval[2] ?? null];
76
        }
77
        // Order the bounds.
78
        usort(
79
            $bounds,
80
            static function (array $d1, array $d2) {
81
                return ($d1[0] < $d2[0]) ? -1 : 1;
82
            }
83
        );
84
        return $bounds;
85
    }
86
87
    /**
88
     * Extract discrete values from an array of intervals.
89
     *
90
     * Intervals with the exact same lower and higher bound will be considered as discrete values.
91
     *
92
     * They will be removed from the initial array, and returned in a separate array.
93
     *
94
     * @param array $intervals The initial array.
95
     * @return array An array containing only discrete values.
96
     */
97
    public static function extractDiscreteValues(array &$intervals): array
98
    {
99
        $discreteValues = array_filter(
100
            $intervals,
101
            static function ($interval) {
102
                return $interval[0] === $interval[1];
103
            }
104
        );
105
106
        $intervals = array_diff_key($intervals, $discreteValues);
107
108
        return $discreteValues;
109
    }
110
111
    /**
112
     * Create each new interval and calculate its value based on the active intervals on each bound.
113
     *
114
     * @param array[] $intervals
115
     * @return array[]
116
     */
117
    public function flatten(array $intervals): array
118
    {
119
        $discreteValues = self::extractDiscreteValues($intervals);
120
121
122
        $bounds = self::intervalsToSignedBounds($intervals);
123
        $newIntervals = [];
124
        $activeIntervals = [];
125
126
        $boundsCount = count($bounds);
127
128
        // Create new intervals for each set of two consecutive bounds,
129
        // and calculate its total value.
130
        for ($i = 1; $i < $boundsCount; $i++) {
131
            // Set the current bound.
132
            [$curBoundValue, $curBoundType, $curBoundIncluded, $curBoundIntervalKey] = $bounds[$i - 1];
133
            [$nextBoundValue, $nextBoundType, $nextBoundIncluded] = $bounds[$i];
134
135
            if ($curBoundType === '+') {
136
                // If this is a low bound,
137
                // add the key of the interval to the array of active intervals.
138
                $activeIntervals[$curBoundIntervalKey] = true;
139
            } else {
140
                // If this is an high bound, remove the key.
141
                unset($activeIntervals[$curBoundIntervalKey]);
142
            }
143
144
            // If the current bound is the same as the next, which happens when multiple intervals
145
            // begin or end at the same time, skip interval creation.
146
            // Use weak type comparison by default, in order to correctly compare object types like DateTime.
147
            if ($curBoundValue == $nextBoundValue) {
148
                continue;
149
            }
150
151
            $newHighBound = $this->makeHighBound($nextBoundType, $nextBoundIncluded, $nextBoundValue);
152
            $newLowBound = $this->makeLowBound($curBoundType, $curBoundIncluded, $curBoundValue);
153
154
            // If the new high bound is lower or equal to the new low bound,
155
            // which can happen when using steps,
156
            // skip interval creation.
157
            // TODO Check validity and add tests
158
            if ($newHighBound <= $newLowBound) {
159
                continue;
160
            }
161
162
            $newIntervals[] = [
163
                $newLowBound,
164
                $newHighBound,
165
                $activeIntervals
166
            ];
167
        }
168
169
        // Push discrete values back into the array.
170
        if (!empty($discreteValues)) {
171
            array_push($newIntervals, ...$discreteValues);
172
        }
173
174
        return $newIntervals;
175
    }
176
177
    /**
178
     * Define the high bound of the new interval.
179
     *
180
     * @param string $nextBoundType Type of the next bound: low (-) or high (+)
181
     * @param bool $nextBoundIncluded Is the next bound included?
182
     * @param mixed $nextBoundValue
183
     * @return mixed
184
     */
185
    private function makeHighBound(string $nextBoundType, bool $nextBoundIncluded, $nextBoundValue)
186
    {
187
        if (
188
            isset($this->substractStep) && (
189
                ($nextBoundIncluded && $nextBoundType === '+')
190
                || (!$nextBoundIncluded && $nextBoundType === '+')
191
            )
192
        ) {
193
            return ($this->substractStep)($nextBoundValue);
194
        }
195
        return $nextBoundValue;
196
    }
197
198
    /**
199
     * Define the low bound of the new interval.
200
     *
201
     * @param string $curBoundType Type of the current bound: low (-) or high (+)
202
     * @param bool $curBoundIncluded Is the current bound included?
203
     * @param mixed $curBoundValue
204
     * @return mixed
205
     */
206
    private function makeLowBound(string $curBoundType, bool $curBoundIncluded, $curBoundValue)
207
    {
208
        if (
209
            isset($this->addStep) && $curBoundType === '-' && $curBoundIncluded
210
        ) {
211
            return ($this->addStep)($curBoundValue);
212
        }
213
        return $curBoundValue;
214
    }
215
}
216