Completed
Push — master ( eb64f7...67e2e1 )
by brian
02:02
created

VersionTokenizer::getComparatorToken()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 20
ccs 10
cts 10
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 10
nc 3
nop 2
crap 3
1
<?php
2
3
/**
4
 * @copyright   (c) 2014-2017 brian ridley
5
 * @author      brian ridley <[email protected]>
6
 * @license     http://opensource.org/licenses/MIT MIT
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace ptlis\SemanticVersion\Parse;
13
14
/**
15
 * Tokenizer for version numbers.
16
 */
17
final class VersionTokenizer
18
{
19
    private $comparatorMap = [
20
        '>' => Token::GREATER_THAN,
21
        '>=' => Token::GREATER_THAN_EQUAL,
22
        '<' => Token::LESS_THAN,
23
        '<=' => Token::LESS_THAN_EQUAL,
24
        '=' => Token::EQUAL_TO
25
    ];
26
27
    /**
28
     * Accepts a version string & returns an array of tokenized values.
29
     *
30
     * @param string $versionString
31
     *
32
     * @return Token[]
33
     */
34 31
    public function tokenize($versionString)
35
    {
36
        /** @var Token[] $tokenList */
37 31
        $tokenList = [];
38 31
        $digitAccumulator = '';
39 31
        $stringAccumulator = '';
40
41
        // Iterate through string, character-by-character
42 31
        for ($i = 0; $i < strlen($versionString); $i++) {
43 31
            $chr = $this->getCharacter($i, $versionString);
44
45
            switch (true) {
46
47
                // Simple token types - separators and range indicators
48 31
                case !is_null($this->getSimpleToken($chr)):
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
49
50
                    // Handle preceding digits or labels
51 29
                    $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList);
52 29
                    $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList);
53
54 29
                    $tokenList[] = $this->getSimpleToken($chr);
55 29
                    break;
56
57
                // Comparator token types
58 31
                case !is_null($token = $this->getComparatorToken($i, $versionString)):
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
59
60
                    // Handle preceding digits or labels
61 11
                    $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList);
62 11
                    $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList);
63
64 11
                    $this->addImplicitAnd($tokenList);
65
66 11
                    $tokenList[] = $token;
67 11
                    break;
68
69
                // Skip the 'v' character if immediately preceding a digit
70 31
                case $this->isPrefixedV($i, $versionString):
71
                    // Do nothing
72 1
                    break;
73
74
                // Spaces, pipes, ampersands and commas may contextually be logical AND/OR
75 31
                case $this->isPossibleLogicalOperator($chr):
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
76
77
                    // Handle preceding digits or labels
78 6
                    $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList);
79 6
                    $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList);
80
81
                    $operatorTokenList = [
82 6
                        Token::GREATER_THAN,
83
                        Token::GREATER_THAN_EQUAL,
84
                        Token::LESS_THAN,
85
                        Token::LESS_THAN_EQUAL,
86
                        Token::EQUAL_TO
87
                    ];
88
89
                    // No previous tokens, or previous token was not a comparator
90
                    if (
91 6
                        !count($tokenList)
92 6
                        || !in_array($tokenList[count($tokenList)-1]->getType(), $operatorTokenList)
93
                    ) {
94 5
                        $possibleOperator = trim($chr);
95
96 5
                        for ($j = $i + 1; $j < strlen($versionString); $j++) {
97 5
                            $operatorChr = $this->getCharacter($j, $versionString);
98
99 5
                            if ($this->isPossibleLogicalOperator($operatorChr)) {
100 3
                                $possibleOperator .= trim($operatorChr);
101
                            } else {
102 5
                                if (!strlen($possibleOperator) || ',' === $possibleOperator) {
103 4
                                    $tokenList[] = new Token(Token::LOGICAL_AND, $possibleOperator);
104
                                } else {
105 2
                                    $tokenList[] = new Token(Token::LOGICAL_OR, $possibleOperator);
106
                                }
107 5
                                $i = $j - 1;
108 5
                                break;
109
                            }
110
                        }
111
                    }
112
113 6
                    break;
114
115
                // Start accumulating on the first non-digit & continue until we reach a separator
116 31
                case !is_numeric($chr) || strlen($stringAccumulator):
117 5
                    $stringAccumulator .= $chr;
118 5
                    break;
119
120
                // Simple digits
121
                default:
122 31
                    $digitAccumulator .= $chr;
123 31
                    break;
124
            }
125
        }
126
127
        // Handle any remaining digits or labels
128 31
        $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList);
129 31
        $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList);
130
131 31
        return $tokenList;
132
    }
133
134
    /**
135
     * Add an implicit AND between a version & the comparator of a subsequent version.
136
     *
137
     * @param Token[] $tokenList
138
     */
139 11
    private function addImplicitAnd(array &$tokenList)
140
    {
141
        $digitTokenList = [
142 11
            Token::DIGITS,
143
            Token::WILDCARD_DIGITS
144
        ];
145
146 11
        if (count($tokenList) && in_array($tokenList[count($tokenList) -1]->getType(), $digitTokenList)) {
147 1
            $tokenList[] = new Token(Token::LOGICAL_AND, '');
148
        }
149 11
    }
150
151
    /**
152
     * Returns true if the character is an AND comparator
153
     *
154
     * @param string $chr
155
     *
156
     * @return bool
157
     */
158 31
    private function isPossibleLogicalOperator($chr)
159
    {
160 31
        return in_array($chr, array(',', '|')) || ctype_space($chr);
161
    }
162
163
    /**
164
     * Tries to find a comparator token beginning at the specified index.
165
     *
166
     * @param int $index
167
     * @param string $versionString
168
     *
169
     * @return Token|null
170
     */
171 31
    private function getComparatorToken(&$index, $versionString)
172
    {
173 31
        $token = null;
174
175
        // See if the first character matches a token
176 31
        $comparator = substr($versionString, $index, 1);
177 31
        if (array_key_exists($comparator, $this->comparatorMap)) {
178
179
            // Check for second character in comparator ('<=' or '=>')
180 11
            $nextChr = $this->getCharacter($index + 1, $versionString);
181 11
            if (array_key_exists($comparator . $nextChr, $this->comparatorMap)) {
182 6
                $comparator .= $nextChr;
183 6
                $index++;
184
            }
185
186 11
            $token = new Token($this->comparatorMap[$comparator], $comparator);
187
        }
188
189 31
        return $token;
190
    }
191
192
    /**
193
     * Get a token from a single-character.
194
     *
195
     * @param string $chr
196
     *
197
     * @return Token|null
198
     */
199 31
    private function getSimpleToken($chr)
200
    {
201
        $tokenMap = [
202 31
            '-' => Token::DASH_SEPARATOR,
203
            '.' => Token::DOT_SEPARATOR,
204
            '~' => Token::TILDE_RANGE,
205
            '^' => Token::CARET_RANGE,
206
            '*' => Token::WILDCARD_DIGITS,
207
            'x' => Token::WILDCARD_DIGITS
208
        ];
209
210 31
        $token = null;
211 31
        if (array_key_exists($chr, $tokenMap)) {
212 29
            $token = new Token($tokenMap[$chr], $chr);
213
        }
214
215 31
        return $token;
216
    }
217
218
    /**
219
     * Add a token to the list if the string length is greater than 0, empties string.
220
     *
221
     * @param string $type One of Token class constants
222
     * @param string $value
223
     * @param Token[] $tokenList
224
     */
225 31
    public function conditionallyAddToken($type, &$value, &$tokenList)
226
    {
227 31
        if (strlen($value)) {
228 31
            $tokenList[] = new Token(
229
                $type,
230
                $value
231
            );
232
233 31
            $value = '';
234
        }
235 31
    }
236
237
    /**
238
     * Returns true if the character is a 'v' prefix to a version number (e.g. v1.0.7).
239
     *
240
     * @param int $index
241
     * @param string $versionString
242
     *
243
     * @return bool
244
     */
245 31
    private function isPrefixedV($index, $versionString)
246
    {
247 31
        $chr = $this->getCharacter($index, $versionString);
248
249 31
        return 'v' === $chr
250 31
            && $index + 1 < strlen($versionString)
251 31
            && is_numeric($this->getCharacter($index + 1, $versionString));
252
    }
253
254
    /**
255
     * Get the next character after the specified index or an empty string if not present.
256
     *
257
     * @param int $index
258
     * @param string $versionString
259
     * @return string
260
     */
261 31
    private function getCharacter($index, $versionString)
262
    {
263 31
        $chr = '';
264 31
        if ($index < strlen($versionString)) {
265 31
            $chr = substr($versionString, $index, 1);
266
        }
267
268 31
        return $chr;
269
    }
270
}
271