Passed
Push — master ( 599c2e...8427a2 )
by Alexander
01:13
created

Lexer::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 6
rs 10
1
<?php
2
3
4
namespace App;
5
6
7
use App\Exception\LexerException;
8
use App\Exception\UnknownIdentifier;
9
10
class Lexer
11
{
12
    /**
13
     * @var string
14
     */
15
    private $input;
16
    /**
17
     * @var string|null
18
     */
19
    private $currentChar;
20
    /**
21
     * @var int
22
     */
23
    private $pos = 0;
24
25
    /**
26
     * Lexer constructor.
27
     * @param string $input
28
     */
29
    public function __construct(
30
        string $input
31
    )
32
    {
33
        $this->input       = $input;
34
        $this->currentChar = $this->input[$this->pos];
35
    }
36
37
    private function isAlpha(string $char): bool
38
    {
39
        return (bool)preg_match('/[a-zA-Z_]/', $char);
40
    }
41
42
    /**
43
     * @return Token
44
     * @throws UnknownIdentifier
45
     */
46
    private function identifier(): Token
47
    {
48
        $identifier = '';
49
        while ($this->currentChar !== null && $this->isAlpha($this->currentChar)) {
50
            $identifier .= $this->currentChar;
51
            $this->advance();
52
        }
53
54
        $startPos = $this->pos;
55
        while ($this->input[$startPos] === ' ') {
56
            $startPos++;
57
        }
58
        $nextChar = $this->input[$startPos] ?? null;
59
60
        if ($nextChar === '(') {
61
            return new Token(Token::FUNCTION_CALL, $identifier);
62
        }
63
64
        throw new UnknownIdentifier('Unknown identifier ' . $identifier);
65
    }
66
67
    /**
68
     * @return Token
69
     * @throws LexerException
70
     * @throws UnknownIdentifier
71
     */
72
    public function getNextToken(): Token
73
    {
74
        while ($this->currentChar !== null) {
75
            $this->skipWhitespace();
76
            if ($this->isAlpha($this->currentChar)) {
77
                return $this->identifier();
78
            }
79
            if ($this->currentChar === ',') {
80
                $this->advance();
81
                return new Token(Token::COMMA, ',');
82
            }
83
            if ($this->currentChar === '+') {
84
                $this->advance();
85
                return new Token(Token::PLUS, '+');
86
            }
87
            if ($this->currentChar === '-') {
88
                $this->advance();
89
                return new Token(Token::MINUS, '-');
90
            }
91
            if ($this->currentChar === '^') {
92
                $this->advance();
93
                return new Token(Token::POWER, '^');
94
            }
95
            if ($this->currentChar === '*') {
96
                return $this->analyzeAsterisk();
97
            }
98
            if ($this->currentChar === '/') {
99
                $this->advance();
100
                return new Token(Token::REALDIV, '/');
101
            }
102
            if ($this->currentChar === '(') {
103
                $this->advance();
104
                return new Token(Token::LPAREN, '(');
105
            }
106
            if ($this->currentChar === ')') {
107
                $this->advance();
108
                return new Token(Token::RPAREN, ')');
109
            }
110
111
            if (is_numeric($this->currentChar)) {
112
                return $this->parseNumber();
113
            }
114
        }
115
116
        return new Token(Token::EOF, Token::EOF);
117
    }
118
119
    private function skipWhitespace(): void
120
    {
121
        if ($this->currentChar === ' ') {
122
            $this->advance();
123
        }
124
    }
125
126
    private function advance(): void
127
    {
128
        $this->pos++;
129
        $this->currentChar = $this->input[$this->pos] ?? null;
130
    }
131
132
    private function peek(): ?string
133
    {
134
        $pos = $this->pos + 1;
135
        return $this->input[$pos] ?? null;
136
    }
137
138
    /**
139
     * @return Token
140
     * @throws LexerException
141
     */
142
    private function parseNumber(): Token
143
    {
144
        if ($this->currentChar === null) {
145
            throw new LexerException('Cannot found current char');
146
        }
147
        $number = '';
148
        while (is_numeric($this->currentChar) || ($number !== '' && $this->currentChar === '.')) {
149
            $number .= $this->currentChar;
150
            $this->advance();
151
        }
152
153
        if ($this->validateNumber($number) === false) {
154
            throw new LexerException('Parsed number not valid');
155
        }
156
157
        if (strpos($number, '.')) {
158
            return new Token(Token::REAL, (float)$number);
159
        }
160
161
        return new Token(Token::INT, (int)$number);
162
    }
163
164
    private function validateNumber(string $number): bool
165
    {
166
        return (bool)preg_match('/^[\d]+\.?[\d]*/', $number);
167
    }
168
169
    /**
170
     * @return Token
171
     */
172
    private function analyzeAsterisk(): Token
173
    {
174
        if ($this->peek() === '*') {
175
            $this->advance();
176
            $this->advance();
177
            return new Token(Token::POWER, '**');
178
        }
179
180
        $this->advance();
181
        return new Token(Token::MUL, '*');
182
    }
183
}