• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

brick / geo / 13831530637

13 Mar 2025 08:44AM UTC coverage: 44.548% (-5.2%) from 49.787%
13831530637

push

github

BenMorel
Merge WKTParser & EWKTParser, add WKTTokenType

31 of 32 new or added lines in 3 files covered. (96.88%)

208 existing lines in 11 files now uncovered.

1573 of 3531 relevant lines covered (44.55%)

298.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.27
/src/IO/WKTParser.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Brick\Geo\IO;
6

7
use Brick\Geo\Exception\GeometryIOException;
8

9
/**
10
 * Well-Known Text parser, with support for EWKT.
11
 *
12
 * @internal
13
 */
14
final class WKTParser
15
{
16
    private const string REGEX_CAPTURE_WORD   = '([a-z]+)';
17
    private const string REGEX_CAPTURE_NUMBER = '(\-?[0-9]+(?:\.[0-9]+)?(?:e[\+\-]?[0-9]+)?)';
18
    private const string REGEX_WHITESPACE = '\s+';
19
    private const string REGEX_CAPTURE_OTHER = '(.+?)';
20
    private const string REGEX_CAPTURE_SRID = 'SRID\=([0-9]+)\s*;'; // EWKT
21

22
    /**
23
     * The list of tokens.
24
     *
25
     * The first element of each token is the token type, the second element is the token value.
26
     *
27
     * @var list<array{WKTTokenType, string}>
28
     */
29
    private array $tokens = [];
30

31
    /**
32
     * The current token pointer.
33
     */
34
    private int $current = 0;
35

36
    public function __construct(
37
        string $wkt,
38
        private bool $ewkt = false,
39
    ) {
40
        $this->scan($wkt);
4,350✔
41
    }
42

43
    private function scan(string $wkt) : void
44
    {
45
        $regexPatterns = [
4,350✔
46
            self::REGEX_CAPTURE_WORD,
4,350✔
47
            self::REGEX_CAPTURE_NUMBER,
4,350✔
48
            self::REGEX_WHITESPACE,
4,350✔
49
            self::REGEX_CAPTURE_OTHER,
4,350✔
50
        ];
4,350✔
51

52
        if ($this->ewkt) {
4,350✔
53
            array_unshift($regexPatterns, self::REGEX_CAPTURE_SRID);
1,392✔
54
        }
55

56
        $matchKeyToType = $this->ewkt ? [
4,350✔
57
            1 => WKTTokenType::SRID,
4,350✔
58
            2 => WKTTokenType::Word,
4,350✔
59
            3 => WKTTokenType::Number,
4,350✔
60
            4 => WKTTokenType::Other,
4,350✔
61
        ] : [
2,958✔
62
            1 => WKTTokenType::Word,
2,958✔
63
            2 => WKTTokenType::Number,
2,958✔
64
            3 => WKTTokenType::Other,
2,958✔
65
        ];
2,958✔
66

67
        $regex = '/' . implode('|', $regexPatterns) . '/i';
4,350✔
68

69
        preg_match_all($regex, $wkt, $matches, PREG_SET_ORDER);
4,350✔
70

71
        foreach ($matches as $match) {
4,350✔
72
            foreach ($match as $key => $value) {
4,350✔
73
                /** @var int $key */
74

75
                if ($key === 0) {
4,350✔
76
                    continue;
4,350✔
77
                }
78

79
                if ($value !== '') {
4,350✔
80
                    $this->tokens[] = [$matchKeyToType[$key], $value];
4,350✔
81
                }
82
            }
83
        }
84
    }
85

86
    /**
87
     * @return array{WKTTokenType, string}|null The next token, or null if there are no more tokens.
88
     */
89
    private function peekToken(): ?array
90
    {
91
        return $this->tokens[$this->current] ?? null;
4,350✔
92
    }
93

94
    /**
95
     * @return array{WKTTokenType, string}|null The next token, or null if there are no more tokens.
96
     */
97
    private function nextToken() : ?array
98
    {
99
        $token = $this->peekToken();
4,350✔
100

101
        if ($token === null) {
4,350✔
102
            return null;
4,336✔
103
        }
104

105
        $this->current++;
4,350✔
106

107
        return $token;
4,350✔
108
    }
109

110
    /**
111
     * @throws GeometryIOException
112
     */
113
    public function matchOpener() : void
114
    {
115
        $token = $this->nextToken();
2,834✔
116

117
        if ($token === null) {
2,834✔
UNCOV
118
            throw new GeometryIOException("Expected '(' but encountered end of stream");
×
119
        }
120
        if ($token[1] !== '(') {
2,834✔
UNCOV
121
            throw new GeometryIOException("Expected '(' but encountered '" . $token[1] . "'");
×
122
        }
123
    }
124

125
    /**
126
     * @throws GeometryIOException
127
     */
128
    public function matchCloser() : void
129
    {
130
        $token = $this->nextToken();
484✔
131

132
        if ($token === null) {
484✔
UNCOV
133
            throw new GeometryIOException("Expected ')' but encountered end of stream");
×
134
        }
135
        if ($token[1] !== ')') {
484✔
UNCOV
136
            throw new GeometryIOException("Expected ')' but encountered '" . $token[1] . "'");
×
137
        }
138
    }
139

140
    /**
141
     * @throws GeometryIOException
142
     */
143
    public function getNextWord() : string
144
    {
145
        $token = $this->nextToken();
4,350✔
146

147
        if ($token === null) {
4,350✔
UNCOV
148
            throw new GeometryIOException("Expected word but encountered end of stream");
×
149
        }
150
        if ($token[0] !== WKTTokenType::Word) {
4,350✔
UNCOV
151
            throw new GeometryIOException("Expected word but encountered '" . $token[1] . "'");
×
152
        }
153

154
        return $token[1];
4,350✔
155
    }
156

157
    /**
158
     * @return string|null The next word, or NULL if the next token is not a word, or there are no more tokens.
159
     */
160
    public function getOptionalNextWord() : ?string
161
    {
162
        $token = $this->tokens[$this->current] ?? null;
4,350✔
163

164
        if ($token === null) {
4,350✔
UNCOV
165
            return null;
×
166
        }
167

168
        if ($token[0] !== WKTTokenType::Word) {
4,350✔
169
            return null;
2,834✔
170
        }
171

172
        $this->current++;
3,186✔
173

174
        return $token[1];
3,186✔
175
    }
176

177
    public function matchOptionalOpener(): bool
178
    {
179
        $token = $this->peekToken();
2,528✔
180

181
        $isOpener = ($token !== null && $token[1] === '(');
2,528✔
182

183
        if ($isOpener) {
2,528✔
184
            $this->current++;
24✔
185
        }
186

187
        return $isOpener;
2,528✔
188
    }
189

190
    /**
191
     * Returns whether the next token is an opener or a word.
192
     *
193
     * @return bool True if the next token is an opener, false if it is a word.
194
     *
195
     * @throws GeometryIOException If the next token is not an opener or a word, or if there is no next token.
196
     */
197
    public function isNextOpenerOrWord() : bool
198
    {
199
        $token = $this->tokens[$this->current] ?? null;
402✔
200

201
        if ($token === null) {
402✔
UNCOV
202
            throw new GeometryIOException("Expected '(' or word but encountered end of stream");
×
203
        }
204

205
        if ($token[1] === '(') {
402✔
206
            return true;
394✔
207
        }
208

209
        if ($token[0] === WKTTokenType::Word) {
346✔
210
            return false;
346✔
211
        }
212

UNCOV
213
        throw new GeometryIOException("Expected '(' or word but encountered '" . $token[1] . "'");
×
214
    }
215

216
    /**
217
     * @throws GeometryIOException
218
     */
219
    public function getNextNumber() : float
220
    {
221
        $token = $this->nextToken();
2,812✔
222

223
        if ($token === null) {
2,812✔
224
            throw new GeometryIOException("Expected number but encountered end of stream");
×
225
        }
226

227
        if ($token[0] !== WKTTokenType::Number) {
2,812✔
UNCOV
228
            throw new GeometryIOException("Expected number but encountered '" . $token[1] . "'");
×
229
        }
230

231
        return (float) $token[1];
2,812✔
232
    }
233

234
    /**
235
     * @throws GeometryIOException
236
     */
237
    public function getNextCloserOrComma() : string
238
    {
239
        $token = $this->nextToken();
2,560✔
240

241
        if ($token === null) {
2,560✔
UNCOV
242
            throw new GeometryIOException("Expected ')' or ',' but encountered end of stream");
×
243
        }
244
        if ($token[1] !== ')' && $token[1] !== ',') {
2,560✔
UNCOV
245
            throw new GeometryIOException("Expected ')' or ',' but encountered '" . $token[1] . "'");
×
246
        }
247

248
        return $token[1];
2,560✔
249
    }
250

251
    public function getOptionalSRID() : int
252
    {
253
        $token = $this->tokens[$this->current] ?? null;
1,392✔
254

255
        if ($token === null) {
1,392✔
NEW
256
            return 0;
×
257
        }
258

259
        if ($token[0] !== WKTTokenType::SRID) {
1,392✔
260
            return 0;
696✔
261
        }
262

263
        $this->current++;
696✔
264

265
        return (int) $token[1];
696✔
266
    }
267

268
    public function isEndOfStream() : bool
269
    {
270
        return $this->nextToken() === null;
4,336✔
271
    }
272
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc