Completed
Push — master ( 1b2464...2d08f8 )
by Chris
01:38
created

RangeSet::combineOverlappingRanges()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 1
dl 0
loc 18
ccs 10
cts 10
cp 1
crap 3
rs 9.4285
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace DaveRandom\Resume;
4
5
final class RangeSet
6
{
7
    public const DEFAULT_MAX_RANGES = 10;
8
9
    private const HEADER_PARSE_EXPR = /** @lang regex */ '/
10
      ^
11
      \s*                 # tolerate lead white-space
12
      (?<unit> [^\s=]+ )  # unit is everything up to first = or white-space
13
      (?: \s*=\s* | \s+ ) # separator is = or white-space
14
      (?<ranges> .+ )     # remainder is range spec
15
    /x';
16
17
    private const RANGE_PARSE_EXPR = /** @lang regex */ '/
18
      ^
19
      (?<start> [0-9]* ) # start is a decimal number
20
      \s*-\s*            # separator is a dash
21
      (?<end> [0-9]* )   # end is a decimal number
22
      $
23
    /x';
24
25
    /**
26
     * The unit for ranges in the set
27
     *
28
     * @var string
29
     */
30
    private $unit;
31
32
    /**
33
     * The ranges in the set
34
     *
35
     * @var Range[]
36
     */
37
    private $ranges = [];
38
39
    /**
40
     * Parse an array of range specifiers into an array of Range objects
41
     *
42
     * @param string[] $ranges
43
     * @return Range[]
44
     */
45 9
    private static function parseRanges(array $ranges): array
46
    {
47 9
        $result = [];
48
49 9
        foreach ($ranges as $i => $range) {
50 9
            if (!\preg_match(self::RANGE_PARSE_EXPR, \trim($range), $match)) {
51 1
                throw new InvalidRangeHeaderException("Invalid range format at position {$i}: Parse failure");
52
            }
53
54 8
            if ($match['start'] === '' && $match['end'] === '') {
55 1
                throw new InvalidRangeHeaderException("Invalid range format at position {$i}: Start and end empty");
56
            }
57
58 7
            $result[] = $match['start'] === ''
59 1
                ? new Range(((int)$match['end']) * -1)
60 7
                : new Range((int)$match['start'], $match['end'] !== '' ? (int)$match['end'] : null);
61
        }
62
63 7
        return $result;
64
    }
65
66
    /**
67
     * Get a set of normalized ranges applied to a resource size
68
     *
69
     * @param int $size
70
     * @return Range[]
71
     */
72 6
    private function normalizeRangesForSize(int $size): array
73
    {
74 6
        $result = [];
75
76 6
        foreach ($this->ranges as $range) {
77
            try {
78 6
                $range = $range->normalize($size);
79
80 5
                if ($range->getStart() < $size) {
81 5
                    $result[] = $range;
82
                }
83 6
            } catch (UnsatisfiableRangeException $e) {
84
                // ignore, other ranges in the set may be satisfiable
85
            }
86
        }
87
88 6
        if (empty($result)) {
89 1
            throw new UnsatisfiableRangeException('No specified ranges are satisfiable by a resource of the specified size');
90
        }
91
92 5
        return $result;
93
    }
94
95
    /**
96
     * Combine overlapping ranges in the supplied array and return the result
97
     *
98
     * @param Range[] $ranges
99
     * @return Range[]
100
     */
101
    private function combineOverlappingRanges(array $ranges)
102
    {
103 2
        \usort($ranges, static function(Range $a, Range $b) {
104 2
            return $a->getStart() <=> $b->getStart();
105 2
        });
106
107 2
        for ($i = 0, $l = \count($ranges) - 1; $i < $l; $i++) {
108 2
            if (!$ranges[$i]->overlaps($ranges[$i + 1])) {
109 1
                continue;
110
            }
111
112 1
            $ranges[$i] = $ranges[$i]->combine($ranges[$i + 1]);
113 1
            unset($ranges[$i + 1]);
114
115 1
            $i++;
116
        }
117
118 2
        return $ranges;
119
    }
120
121
    /**
122
     * Create a new instance from a Range header string
123
     *
124
     * @param string|null $header
125
     * @param int $maxRanges
126
     * @return self|null
127
     */
128 11
    public static function createFromHeader(?string $header, int $maxRanges = self::DEFAULT_MAX_RANGES): ?self
129
    {
130 11
        if ($header === null) {
131 1
            return null;
132
        }
133
134 10
        if (!\preg_match(self::HEADER_PARSE_EXPR, $header, $match)) {
135 1
            throw new InvalidRangeHeaderException('Invalid header: Parse failure');
136
        }
137
138 9
        $unit = $match['unit'];
139 9
        $ranges = \explode(',', $match['ranges']);
140
141 9
        if (\count($ranges) > $maxRanges) {
142 1
            throw new InvalidRangeHeaderException("Invalid header: Too many ranges");
143
        }
144
145 9
        return new self($unit, self::parseRanges($ranges));
146
    }
147
148
    /**
149
     * @param string $unit
150
     * @param Range[] $ranges
151
     */
152 7
    public function __construct(string $unit, array $ranges)
153
    {
154 7
        $this->unit = $unit;
155 7
        $this->ranges = $ranges;
156
    }
157
158
    /**
159
     * Get the unit for ranges in the set
160
     *
161
     * @return string
162
     */
163 5
    public function getUnit(): string
164
    {
165 5
        return $this->unit;
166
    }
167
168
    /**
169
     * Get a set of normalized ranges applied to a resource size, reduced to the minimum set of ranges
170
     *
171
     * @param int $size
172
     * @return Range[]
173
     */
174 6
    public function getRangesForSize(int $size): array
175
    {
176 6
        $ranges = $this->normalizeRangesForSize($size);
177
178 5
        $previousCount = null;
179 5
        $count = \count($ranges);
180
181 5
        while ($count > 1 && $count !== $previousCount) {
182 2
            $previousCount = $count;
183
184 2
            $ranges = $this->combineOverlappingRanges($ranges);
185
186 2
            $count = \count($ranges);
187
        }
188
189 5
        return $ranges;
190
    }
191
}
192