LogicalOperatorProcessor::shuntingYard()   C
last analyzed

Complexity

Conditions 8
Paths 9

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 8.006

Importance

Changes 0
Metric Value
dl 0
loc 35
ccs 21
cts 22
cp 0.9545
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 19
nc 9
nop 1
crap 8.006
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
use ptlis\SemanticVersion\VersionRange\LogicalAnd;
15
use ptlis\SemanticVersion\VersionRange\LogicalOr;
16
use ptlis\SemanticVersion\VersionRange\VersionRangeInterface;
17
18
/**
19
 * Build version ranges from stream of ComparatorVersions & LogicalOperators
20
 *
21
 * Basic (and limited) implementation of the shunting algorithm for logical AND / OR.
22
 */
23
final class LogicalOperatorProcessor
24
{
25
    /**
26
     * @var array Of operator precedences & associativity.
27
     */
28
    private $operatorPrecedence = [
29
        Token::LOGICAL_OR => [
30
            'precedence' => 0,
31
            'associativity' => 'left'
32
        ],
33
        Token::LOGICAL_AND => [
34
            'precedence' => 1,
35
            'associativity' => 'left'
36
        ]
37
    ];
38
39
    /**
40
     * Accepts an array of VersionRanges & logical operator tokens & returns a single version range implementing those
41
     *  constraints.
42
     *
43
     * @param array $tokenList
44
     *
45
     * @return VersionRangeInterface
46
     */
47 3
    public function run(array $tokenList)
48
    {
49 3
        return $this->buildRanges(
50 3
            $this->shuntingYard($tokenList)
51 3
        );
52
    }
53
54
    /**
55
     * Accepts an array of ComparatorVersions & logical operators in reverse polish notation & returns a single
56
     *  instance of a class implementing VersionRangeInterface.
57
     *
58
     * @param array $resultList
59
     *
60
     * @return VersionRangeInterface
61
     */
62 3
    private function buildRanges(array $resultList)
63
    {
64 3
        $stack = new \SplStack();
65
66 3
        foreach ($resultList as $result) {
67 3
            if ($result instanceof VersionRangeInterface) {
68 3
                $stack->push($result);
69 3
            } else {
70 3
                $operator1 = $stack->pop();
71 3
                $operator2 = $stack->pop();
72
73
                /** @var Token $result */
74 3
                if (Token::LOGICAL_AND === $result->getType()) {
75 2
                    $stack->push(new LogicalAnd(
76 2
                        $operator2,
77
                        $operator1
78 2
                    ));
79 2
                } else {
80 2
                    $stack->push(new LogicalOr(
81 2
                        $operator2,
82
                        $operator1
83 2
                    ));
84
                }
85
            }
86 3
        }
87
88 3
        return $stack->pop();
89
    }
90
91
    /**
92
     * Re-order token stream into reverse polish notation.
93
     *
94
     * @param array $tokenList
95
     *
96
     * @return array of ComparatorVersions and logical operator tokens
97
     */
98 3
    private function shuntingYard(array $tokenList)
99
    {
100 3
        $operatorStack = new \SplStack();
101 3
        $output = new \SplQueue();
102
103 3
        foreach ($tokenList as $token) {
104
            // Accumulate Versions & Comparators
105 3
            if ($token instanceof VersionRangeInterface) {
106 3
                $output->enqueue($token);
107
108
            // Handle operators
109 3
            } elseif ($token instanceof Token) {
110
                // Loop while the current token has higher precedence then the stack token
111 3
                $operator1 = $token;
112
                while (
113 3
                    $this->hasOperator($operatorStack)
114 3
                    && ($operator2 = $operatorStack->top())
115 3
                    && $this->hasLowerPrecedence($operator1, $operator2)
116 3
                ) {
117 1
                    $output->enqueue($operatorStack->pop());
118 1
                }
119
120 3
                $operatorStack->push($operator1);
121 3
            } else {
122
                throw new \RuntimeException('Invalid version number');
123
            }
124 3
        }
125
126
        // Merge remaining operators onto output list
127 3
        while ($this->hasOperator($operatorStack)) {
128 3
            $output->enqueue($operatorStack->pop());
129 3
        }
130
131 3
        return iterator_to_array($output);
132
    }
133
134
    /**
135
     * Returns true if we have an operator on the stack.
136
     *
137
     * @param \SplStack $stack
138
     * @return bool
139
     */
140 3
    private function hasOperator(\SplStack $stack)
141
    {
142 3
        return count($stack) > 0;
143
    }
144
145
    /**
146
     * Returns true if the first operator has lower precedence than the second.
147
     *
148
     * @param Token $operator1
149
     * @param Token $operator2
150
     *
151
     * @return bool
152
     */
153 1
    private function hasLowerPrecedence(Token $operator1, Token $operator2)
154
    {
155 1
        $associativity1 = $this->operatorPrecedence[$operator1->getType()]['associativity'];
156
157 1
        $precedence1 = $this->operatorPrecedence[$operator1->getType()]['precedence'];
158 1
        $precedence2 = $this->operatorPrecedence[$operator2->getType()]['precedence'];
159
160 1
        return ('left' === $associativity1 && $precedence1 === $precedence2)
161 1
            || $precedence1 < $precedence2;
162
    }
163
}
164