VersionTokenizer::addImplicitAnd()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 7
cts 7
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 6
nc 2
nop 1
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
    /** @var array Array mapping strings to comparator types */
20
    private $comparatorMap = [
21
        '>' => Token::GREATER_THAN,
22
        '>=' => Token::GREATER_THAN_EQUAL,
23
        '<' => Token::LESS_THAN,
24
        '<=' => Token::LESS_THAN_EQUAL,
25
        '=' => Token::EQUAL_TO
26
    ];
27
28
    /** @var array Array mapping strings to simple token types */
29
    private $simpleTokenMap = [
30
        '-' => Token::DASH_SEPARATOR,
31
        '.' => Token::DOT_SEPARATOR,
32
        '~' => Token::TILDE_RANGE,
33
        '^' => Token::CARET_RANGE,
34
        '*' => Token::WILDCARD_DIGITS,
35
        'x' => Token::WILDCARD_DIGITS
36
    ];
37
38
    /**
39
     * Accepts a version string & returns an array of tokenized values.
40
     *
41
     * @param string $versionString
42
     *
43
     * @return Token[]
44
     */
45 31
    public function tokenize($versionString)
46
    {
47
        /** @var Token[] $tokenList */
48 31
        $tokenList = [];
49 31
        $digitAccumulator = '';
50 31
        $stringAccumulator = '';
51
52
        // Iterate through string, character-by-character
53 31
        for ($i = 0; $i < strlen($versionString); $i++) {
54 31
            $chr = $this->getCharacter($i, $versionString);
55
56 31
            switch (true) {
57
                // Simple token types - separators and range indicators
58 31
                case !is_null($this->getSimpleToken($chr)):
59
                    // Handle preceding digits or labels
60 29
                    $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList);
61 29
                    $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList);
62
63 29
                    $tokenList[] = $this->getSimpleToken($chr);
64 29
                    break;
65
66
                // Comparator token types
67 31
                case !is_null($token = $this->getComparatorToken($i, $versionString)):
68
                    // Handle preceding digits or labels
69 11
                    $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList);
70 11
                    $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList);
71
72 11
                    $this->addImplicitAnd($tokenList);
73
74 11
                    $tokenList[] = $token;
75 11
                    break;
76
77
                // Skip the 'v' character if immediately preceding a digit
78 31
                case $this->isPrefixedV($i, $versionString):
79
                    // Do nothing
80 1
                    break;
81
82
                // Spaces, pipes, ampersands and commas may contextually be logical AND/OR
83 31
                case $this->isPossibleLogicalOperator($chr):
84
                    // Handle preceding digits or labels
85 6
                    $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList);
86 6
                    $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList);
87
88
                    // No previous tokens, or previous token was not a comparator
89
                    if (
90 6
                        !count($tokenList)
91 6
                        || !in_array($tokenList[count($tokenList)-1]->getType(), $this->comparatorMap)
92 6
                    ) {
93 5
                        $possibleOperator = trim($chr);
94
95 5
                        for ($j = $i + 1; $j < strlen($versionString); $j++) {
96 5
                            $operatorChr = $this->getCharacter($j, $versionString);
97
98 5
                            if ($this->isPossibleLogicalOperator($operatorChr)) {
99 3
                                $possibleOperator .= trim($operatorChr);
100 3
                            } else {
101 5
                                if (!strlen($possibleOperator) || ',' === $possibleOperator) {
102 4
                                    $tokenList[] = new Token(Token::LOGICAL_AND, $possibleOperator);
103 4
                                } else {
104 2
                                    $tokenList[] = new Token(Token::LOGICAL_OR, $possibleOperator);
105
                                }
106 5
                                $i = $j - 1;
107 5
                                break;
108
                            }
109 3
                        }
110 5
                    }
111
112 6
                    break;
113
114
                // Start accumulating on the first non-digit & continue until we reach a separator
115 31
                case !is_numeric($chr) || strlen($stringAccumulator):
116 5
                    $stringAccumulator .= $chr;
117 5
                    break;
118
119
                // Simple digits
120 31
                default:
121 31
                    $digitAccumulator .= $chr;
122 31
                    break;
123 31
            }
124 31
        }
125
126
        // Handle any remaining digits or labels
127 31
        $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList);
128 31
        $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList);
129
130 31
        return $tokenList;
131
    }
132
133
    /**
134
     * Add an implicit AND between a version & the comparator of a subsequent version.
135
     *
136
     * @param Token[] $tokenList
137
     */
138 11
    private function addImplicitAnd(array &$tokenList)
139
    {
140
        $digitTokenList = [
141 11
            Token::DIGITS,
142
            Token::WILDCARD_DIGITS
143 11
        ];
144
145 11
        if (count($tokenList) && in_array($tokenList[count($tokenList) -1]->getType(), $digitTokenList)) {
146 1
            $tokenList[] = new Token(Token::LOGICAL_AND, '');
147 1
        }
148 11
    }
149
150
    /**
151
     * Returns true if the character is an AND comparator
152
     *
153
     * @param string $chr
154
     *
155
     * @return bool
156
     */
157 31
    private function isPossibleLogicalOperator($chr)
158
    {
159 31
        return in_array($chr, array(',', '|')) || ctype_space($chr);
160
    }
161
162
    /**
163
     * Tries to find a comparator token beginning at the specified index.
164
     *
165
     * @param int $index
166
     * @param string $versionString
167
     *
168
     * @return Token|null
169
     */
170 31
    private function getComparatorToken(&$index, $versionString)
171
    {
172 31
        $token = null;
173
174
        // See if the first character matches a token
175 31
        $comparator = substr($versionString, $index, 1);
176 31
        if (array_key_exists($comparator, $this->comparatorMap)) {
177
            // Check for second character in comparator ('<=' or '=>')
178 11
            $nextChr = $this->getCharacter($index + 1, $versionString);
179 11
            if (array_key_exists($comparator . $nextChr, $this->comparatorMap)) {
180 6
                $comparator .= $nextChr;
181 6
                $index++;
182 6
            }
183
184 11
            $token = new Token($this->comparatorMap[$comparator], $comparator);
185 11
        }
186
187 31
        return $token;
188
    }
189
190
    /**
191
     * Get a token from a single-character.
192
     *
193
     * @param string $chr
194
     *
195
     * @return Token|null
196
     */
197 31
    private function getSimpleToken($chr)
198
    {
199 31
        $token = null;
200 31
        if (array_key_exists($chr, $this->simpleTokenMap)) {
201 29
            $token = new Token($this->simpleTokenMap[$chr], $chr);
202 29
        }
203
204 31
        return $token;
205
    }
206
207
    /**
208
     * Add a token to the list if the string length is greater than 0, empties string.
209
     *
210
     * @param string $type One of Token class constants
211
     * @param string $value
212
     * @param Token[] $tokenList
213
     */
214 31
    public function conditionallyAddToken($type, &$value, &$tokenList)
215
    {
216 31
        if (strlen($value)) {
217 31
            $tokenList[] = new Token(
218 31
                $type,
219
                $value
220 31
            );
221
222 31
            $value = '';
223 31
        }
224 31
    }
225
226
    /**
227
     * Returns true if the character is a 'v' prefix to a version number (e.g. v1.0.7).
228
     *
229
     * @param int $index
230
     * @param string $versionString
231
     *
232
     * @return bool
233
     */
234 31
    private function isPrefixedV($index, $versionString)
235
    {
236 31
        $chr = $this->getCharacter($index, $versionString);
237
238
        return 'v' === $chr
239 31
            && $index + 1 < strlen($versionString)
240 31
            && is_numeric($this->getCharacter($index + 1, $versionString));
241
    }
242
243
    /**
244
     * Get the next character after the specified index or an empty string if not present.
245
     *
246
     * @param int $index
247
     * @param string $versionString
248
     * @return string
249
     */
250 31
    private function getCharacter($index, $versionString)
251
    {
252 31
        $chr = '';
253 31
        if ($index < strlen($versionString)) {
254 31
            $chr = substr($versionString, $index, 1);
255 31
        }
256
257 31
        return $chr;
258
    }
259
}
260