Completed
Push — master ( 7da6eb...f08cf3 )
by Victor
02:31 queued 12s
created

Flattener::flatten()   A

Complexity

Conditions 5
Paths 10

Size

Total Lines 49
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 23
nc 10
nop 1
dl 0
loc 49
rs 9.2408
c 1
b 0
f 0
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
            if ($curBoundValue === $nextBoundValue) {
147
                continue;
148
            }
149
150
            $newHighBound = $this->makeHighBound($nextBoundType, $nextBoundIncluded, $nextBoundValue);
151
            $newLowBound = $this->makeLowBound($curBoundType, $curBoundIncluded, $curBoundValue);
152
153
            $newIntervals[] = [
154
                $newLowBound,
155
                $newHighBound,
156
                $activeIntervals
157
            ];
158
        }
159
160
        // Push discrete values back into the array.
161
        if (!empty($discreteValues)) {
162
            array_push($newIntervals, ...$discreteValues);
163
        }
164
165
        return $newIntervals;
166
    }
167
168
    /**
169
     * Define the high bound of the new interval.
170
     *
171
     * @param string $nextBoundType Type of the next bound: low (-) or high (+)
172
     * @param bool $nextBoundIncluded Is the next bound included?
173
     * @param mixed $nextBoundValue
174
     * @return mixed
175
     */
176
    private function makeHighBound(string $nextBoundType, bool $nextBoundIncluded, $nextBoundValue)
177
    {
178
        if (
179
            isset($this->addStep, $this->substractStep) && (
180
                ($nextBoundIncluded && $nextBoundType === '+')
181
                || (!$nextBoundIncluded && $nextBoundType === '+')
182
            )
183
        ) {
184
            return ($this->substractStep)($nextBoundValue);
185
        }
186
        return $nextBoundValue;
187
    }
188
189
    /**
190
     * Define the low bound of the new interval.
191
     *
192
     * @param string $curBoundType Type of the current bound: low (-) or high (+)
193
     * @param bool $curBoundIncluded Is the current bound included?
194
     * @param mixed $curBoundValue
195
     * @return mixed
196
     */
197
    private function makeLowBound(string $curBoundType, bool $curBoundIncluded, $curBoundValue)
198
    {
199
        if (
200
            isset($this->addStep, $this->substractStep)
201
            && $curBoundType === '-' && $curBoundIncluded
202
        ) {
203
            return ($this->addStep)($curBoundValue);
204
        }
205
        return $curBoundValue;
206
    }
207
}
208