RangeHelper   A
last analyzed

Complexity

Total Complexity 22

Size/Duplication

Total Lines 149
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 67
c 4
b 0
f 0
dl 0
loc 149
rs 10
wmc 22

4 Methods

Rating   Name   Duplication   Size   Complexity  
A isInRange() 0 11 4
A getRangeLimits() 0 27 4
B findOverlappingRanges() 0 21 7
B validate() 0 54 7
1
<?php
2
/**
3
 * Billing Boss
4
 *
5
 * @link      https://github.com/ranskills/billing-boss-php
6
 * @copyright Copyright (c) 2018 Ransford Ako Okpoti
7
 * @license   Refer to the LICENSE distributed with this library
8
 * @since     1.0
9
 */
10
11
namespace BillingBoss;
12
13
use BillingBoss\Exception\RangeException;
14
use BillingBoss\Exception\RangeOverlapException;
15
use BillingBoss\Exception\RangeConflictException;
16
17
/**
18
 * A utility class for range notation related functions
19
 *
20
 * @package   BillingBoss
21
 * @link      https://github.com/ranskills/billing-boss-php
22
 * @copyright Copyright (c) 2018 Ransford Ako Okpoti
23
 * @license   Refer to the LICENSE distributed with this library
24
 * @since     1.0.0
25
 */
26
final class RangeHelper
27
{
28
29
    /**
30
     * @param $str
31
     * @return array
32
     * @throws RangeException
33
     */
34
    public static function validate($str): array
35
    {
36
        $segments = preg_split(sprintf('/%s/', Expr::PIPE), $str);
37
38
        $matches = [];
39
        foreach($segments as $segment) {
40
            $structure = trim($segment);
41
            $partOfAnotherStructure = strpos($segment, ',') !== false;
42
            if ($partOfAnotherStructure) {
43
                $structure = trim(explode(',', $segment)[1]);
44
            }
45
46
            if(preg_match(sprintf('/^%s$/', Expr::RANGE), $structure, $matches) === 0) {
47
                throw new RangeException(
48
                    sprintf('Invalid range specified "%s"', $structure),
49
                    -1
50
                );
51
            }
52
        }
53
54
        $matches = [];
55
        $numMatches = preg_match_all(sprintf('/%s/', Expr::RANGE), $str, $matches);
56
        if ($numMatches === 0) {
57
            throw new RangeConflictException('No range values provided', RangeException::NO_RANGE_FOUND);
58
        }
59
60
        $numAstericks = substr_count($str, '*');
61
        if ($numAstericks > 1) {
62
            throw new RangeConflictException(
63
                'More than one * provided in the ranges. There can be only one within a set of ranges to be examined.',
64
                RangeException::MULTIPLE_OPEN_ENDED_UPPER_LIMIT
65
            );
66
        }
67
68
        $ranges = self::getRangeLimits($str);
69
        $overlappingRanges = self::findOverlappingRanges($ranges);
70
71
        if (count($overlappingRanges) !== 0) {
72
            $message = sprintf(
73
                'The ranges %s - %s and %s - %s are overlapping and will lead to unexpected results.',
74
                $overlappingRanges[0][0],
75
                $overlappingRanges[0][1],
76
                $overlappingRanges[1][0],
77
                $overlappingRanges[1][1]
78
            );
79
80
            throw new RangeOverlapException(
81
                $message,
82
                RangeException::NO_RANGE_FOUND,
83
                $overlappingRanges
84
            );
85
        }
86
87
        return $ranges;
88
    }
89
90
    /**
91
     * Returns a list of overlapping ranges
92
     *
93
     * @param array $ranges
94
     * @return array A non-empty array if there are overlapping ranges, an empty array otherwise
95
     */
96
    private static function findOverlappingRanges(array $ranges): array
97
    {
98
        $numRanges = count($ranges);
99
100
        for ($i = 0; $i < $numRanges; $i++) {
101
            for ($j = $i + 1; $j < $numRanges; $j++) {
102
                $inRange = self::isInRange($ranges[$i], $ranges[$j][0]) ||
103
                           self::isInRange($ranges[$i], $ranges[$j][1]) ||
104
                           self::isInRange($ranges[$j], $ranges[$i][0]) ||
105
                           self::isInRange($ranges[$j], $ranges[$i][1]);
106
107
                if ($inRange) {
108
                    return [
109
                        $ranges[$i],
110
                        $ranges[$j]
111
                    ];
112
                }
113
            }
114
        }
115
116
        return [];
117
    }
118
119
    /**
120
     * Returns the ranges expressed in a string.
121
     *
122
     * @param string $str
123
     * @return array
124
     * @throws RangeConflictException
125
     */
126
    private static function getRangeLimits($str): array
127
    {
128
        $ranges = [];
129
130
        preg_match_all(sprintf('/%s/', Expr::RANGE), $str, $matches);
131
132
        $lowerLimits = $matches[1];
133
        $upperLimits = $matches[2];
134
        $len = count($lowerLimits);
135
136
        for ($i = 0; $i < $len; $i++) {
137
            if (is_numeric($upperLimits[$i]) && floatval($lowerLimits[$i]) > floatval($upperLimits[$i])) {
138
                $message = sprintf(
139
                    'Invalid limits provided. The lower limit (%s) is greater than the upper limit (%s)',
140
                    $lowerLimits[$i],
141
                    $upperLimits[$i]
142
                );
143
                throw new RangeConflictException(
144
                    $message,
145
                    RangeException::LOWER_LIMIT_GREATER_THAN_UPPER_LIMIT
146
                );
147
            }
148
149
            $ranges[] = [$lowerLimits[$i], $upperLimits[$i]];
150
        }
151
152
        return $ranges;
153
    }
154
155
    /**
156
     * Checks if a given value is within a specified range
157
     *
158
     * @param array $range An array of two elements with the first and last elements being the lower and upper limits
159
     *                     for the boundary
160
     * @param              string|integer $value
161
     *
162
     * @return bool        true if value is within the range, false otherwise
163
     */
164
    public static function isInRange(array $range, $value): bool
165
    {
166
        if ($value === '*') {
167
            return false;
168
        }
169
170
        if (is_numeric($range[1])) {
171
            return $value >= $range[0] && $value <= $range[1];
172
        }
173
174
        return $value >= $range[0];
175
    }
176
}
177