Completed
Push — master ( 8b947d...3afcbd )
by Chris
01:32
created

RangeSet::getRangesForSize()   D

Complexity

Conditions 9
Paths 24

Size

Total Lines 43
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 9

Importance

Changes 0
Metric Value
cc 9
eloc 22
nc 24
nop 1
dl 0
loc 43
ccs 15
cts 15
cp 1
crap 9
rs 4.909
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
    /**
10
     * The unit for ranges in the set
11
     *
12
     * @var string
13
     */
14
    private $unit;
15
16
    /**
17
     * The ranges in the set
18
     *
19
     * @var Range[]
20
     */
21
    private $ranges = [];
22
23
    /**
24
     * Create a new instance from a Range header string
25
     *
26
     * @param string|null $header
27
     * @param int $maxRanges
28
     * @return self|null
29
     */
30 11
    public static function createFromHeader(?string $header, int $maxRanges = self::DEFAULT_MAX_RANGES): ?self
31
    {
32 11
        static $headerParseExpr = /** @lang regex */ '/
33
          ^
34
          \s*                 # tolerate lead white-space
35
          (?<unit> [^\s=]+ )  # unit is everything up to first = or white-space
36
          (?: \s*=\s* | \s+ ) # separator is = or white-space
37
          (?<ranges> .+ )     # remainder is range spec
38
        /x';
39
40 11
        static $rangeParseExpr = /** @lang regex */ '/
41
          ^
42
          (?<start> [0-9]* ) # start is a decimal number
43
          \s*-\s*            # separator is a dash
44
          (?<end> [0-9]* )   # end is a decimal number
45
          $
46
        /x';
47
48 11
        if ($header === null) {
49 1
            return null;
50
        }
51
52 10
        if (!\preg_match($headerParseExpr, $header, $match)) {
53 1
            throw new InvalidRangeHeaderException('Invalid header: Parse failure');
54
        }
55
56 9
        $unit = $match['unit'];
57 9
        $rangeSpec = \explode(',', $match['ranges']);
58
59 9
        if (\count($rangeSpec) > $maxRanges) {
60 1
            throw new InvalidRangeHeaderException("Invalid header: Too many ranges");
61
        }
62
63 9
        $ranges = [];
64
65 9
        foreach (\explode(',', $match['ranges']) as $i => $range) {
66 9
            if (!\preg_match($rangeParseExpr, \trim($range), $match)) {
67 1
                throw new InvalidRangeHeaderException("Invalid range format at position {$i}: Parse failure");
68
            }
69
70 8
            if ($match['start'] === '' && $match['end'] === '') {
71 1
                throw new InvalidRangeHeaderException("Invalid range format at position {$i}: Start and end empty");
72
            }
73
74 7
            $ranges[] = $match['start'] === ''
75 1
                ? new Range(((int)$match['end']) * -1)
76 7
                : new Range((int)$match['start'], $match['end'] !== '' ? (int)$match['end'] : null);
77
        }
78
79 7
        return new self($unit, $ranges);
80
    }
81
82
    /**
83
     * @param string $unit
84
     * @param Range[] $ranges
85
     */
86 7
    public function __construct(string $unit, array $ranges)
87
    {
88 7
        $this->unit = $unit;
89 7
        $this->ranges = $ranges;
90
    }
91
92
    /**
93
     * Get the unit for ranges in the set
94
     *
95
     * @return string
96
     */
97 5
    public function getUnit(): string
98
    {
99 5
        return $this->unit;
100
    }
101
102
    /**
103
     * Get a set of normalized ranges applied to a resource size
104
     *
105
     * @param int $size
106
     * @return Range[]
107
     */
108 6
    public function getRangesForSize(int $size): array
109
    {
110
        /** @var Range[] $ranges */
111 6
        $ranges = [];
112
113 6
        foreach ($this->ranges as $range) {
114
            try {
115 6
                $range = $range->normalize($size);
116
117 5
                if ($range->getStart() < $size) {
118 5
                    $ranges[] = $range;
119
                }
120 6
            } catch (UnsatisfiableRangeException $e) {
121
                // ignore, other ranges in the set may be satisfiable
122
            }
123
        }
124
125 6
        if (empty($ranges)) {
126 1
            throw new UnsatisfiableRangeException('No specified ranges are satisfiable by a resource of the specified size');
127
        }
128
129 5
        $previousCount = null;
130 5
        $count = \count($ranges);
131
132 5
        while ($count > 1 && $count !== $previousCount) {
133 2
            \usort($ranges, static function(Range $a, Range $b) {
134 2
                return $a->getStart() <=> $b->getStart();
135 2
            });
136
137
            $previousCount = $count;
138
139
            for ($i = 0; $i < $count - 1; $i++) {
140
                if ($ranges[$i]->overlaps($ranges[$i + 1])) {
141
                    $ranges[$i] = $ranges[$i]->combine($ranges[$i + 1]);
142
                    unset($ranges[$i + 1]);
143
                    break;
144
                }
145
            }
146
147
            $count = \count($ranges);
148
        }
149
150
        return $ranges;
151
    }
152
}
153