Completed
Push — master ( 3afcbd...1b2464 )
by Chris
01:41
created

RangeSet::createFromHeader()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 2
nop 2
dl 0
loc 18
ccs 10
cts 10
cp 1
crap 4
rs 9.2
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
     * Create a new instance from a Range header string
68
     *
69
     * @param string|null $header
70
     * @param int $maxRanges
71
     * @return self|null
72
     */
73 11
    public static function createFromHeader(?string $header, int $maxRanges = self::DEFAULT_MAX_RANGES): ?self
74
    {
75 11
        if ($header === null) {
76 1
            return null;
77
        }
78
79 10
        if (!\preg_match(self::HEADER_PARSE_EXPR, $header, $match)) {
80 1
            throw new InvalidRangeHeaderException('Invalid header: Parse failure');
81
        }
82
83 9
        $unit = $match['unit'];
84 9
        $ranges = \explode(',', $match['ranges']);
85
86 9
        if (\count($ranges) > $maxRanges) {
87 1
            throw new InvalidRangeHeaderException("Invalid header: Too many ranges");
88
        }
89
90 9
        return new self($unit, self::parseRanges($ranges));
91
    }
92
93
    /**
94
     * @param string $unit
95
     * @param Range[] $ranges
96
     */
97 7
    public function __construct(string $unit, array $ranges)
98
    {
99 7
        $this->unit = $unit;
100 7
        $this->ranges = $ranges;
101
    }
102
103
    /**
104
     * Get the unit for ranges in the set
105
     *
106
     * @return string
107
     */
108 5
    public function getUnit(): string
109
    {
110 5
        return $this->unit;
111
    }
112
113
    /**
114
     * Get a set of normalized ranges applied to a resource size
115
     *
116
     * @param int $size
117
     * @return Range[]
118
     */
119 6
    public function getRangesForSize(int $size): array
120
    {
121
        /** @var Range[] $ranges */
122 6
        $ranges = [];
123
124 6
        foreach ($this->ranges as $range) {
125
            try {
126 6
                $range = $range->normalize($size);
127
128 5
                if ($range->getStart() < $size) {
129 5
                    $ranges[] = $range;
130
                }
131 6
            } catch (UnsatisfiableRangeException $e) {
132
                // ignore, other ranges in the set may be satisfiable
133
            }
134
        }
135
136 6
        if (empty($ranges)) {
137 1
            throw new UnsatisfiableRangeException('No specified ranges are satisfiable by a resource of the specified size');
138
        }
139
140 5
        $previousCount = null;
141 5
        $count = \count($ranges);
142
143 5
        while ($count > 1 && $count !== $previousCount) {
144 2
            \usort($ranges, static function(Range $a, Range $b) {
145 2
                return $a->getStart() <=> $b->getStart();
146 2
            });
147
148
            $previousCount = $count;
149
150
            for ($i = 0; $i < $count - 1; $i++) {
151
                if ($ranges[$i]->overlaps($ranges[$i + 1])) {
152
                    $ranges[$i] = $ranges[$i]->combine($ranges[$i + 1]);
153
                    unset($ranges[$i + 1]);
154
                    break;
155
                }
156
            }
157
158
            $count = \count($ranges);
159
        }
160
161
        return $ranges;
162
    }
163
}
164