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

brick / geo / 13642551372

03 Mar 2025 11:25PM UTC coverage: 84.406% (+0.8%) from 83.61%
13642551372

push

github

BenMorel
Merge WKTParser & EWKTParser

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

69 existing lines in 8 files now uncovered.

1548 of 1834 relevant lines covered (84.41%)

1988.83 hits per line

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

83.13
/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
final class WKTParser
13
{
14
    private const string REGEX_CAPTURE_WORD   = '([a-z]+)';
15
    private const string REGEX_CAPTURE_NUMBER = '(\-?[0-9]+(?:\.[0-9]+)?(?:e[\+\-]?[0-9]+)?)';
16
    private const string REGEX_WHITESPACE = '\s+';
17
    private const string REGEX_CAPTURE_OTHER = '(.+?)';
18
    private const string REGEX_CAPTURE_SRID = 'SRID\=([0-9]+)\s*;'; // EWKT
19

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

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

34
    public function __construct(
35
        string $wkt,
36
        private bool $ewkt = false,
37
    ) {
38
        $this->scan($wkt);
14,513✔
39
    }
40

41
    private function scan(string $wkt) : void
42
    {
43
        $regexPatterns = [
14,513✔
44
            self::REGEX_CAPTURE_WORD,
14,513✔
45
            self::REGEX_CAPTURE_NUMBER,
14,513✔
46
            self::REGEX_WHITESPACE,
14,513✔
47
            self::REGEX_CAPTURE_OTHER,
14,513✔
48
        ];
14,513✔
49

50
        if ($this->ewkt) {
14,513✔
51
            array_unshift($regexPatterns, self::REGEX_CAPTURE_SRID);
4,760✔
52
        }
53

54
        $matchKeyToType = $this->ewkt ? [
14,513✔
55
            1 => WKTTokenType::SRID,
14,513✔
56
            2 => WKTTokenType::Word,
14,513✔
57
            3 => WKTTokenType::Number,
14,513✔
58
            4 => WKTTokenType::Other,
14,513✔
59
        ] : [
9,753✔
60
            1 => WKTTokenType::Word,
9,753✔
61
            2 => WKTTokenType::Number,
9,753✔
62
            3 => WKTTokenType::Other,
9,753✔
63
        ];
9,753✔
64

65
        $regex = '/' . implode('|', $regexPatterns) . '/i';
14,513✔
66

67
        preg_match_all($regex, $wkt, $matches, PREG_SET_ORDER);
14,513✔
68

69
        foreach ($matches as $match) {
14,513✔
70
            foreach ($match as $key => $value) {
14,513✔
71
                /** @var int $key */
72

73
                if ($key === 0) {
14,513✔
74
                    continue;
14,513✔
75
                }
76

77
                if ($value !== '') {
14,513✔
78
                    $this->tokens[] = [$matchKeyToType[$key], $value];
14,513✔
79
                }
80
            }
81
        }
82
    }
83

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

93
        if ($token === null) {
14,513✔
94
            return null;
14,464✔
95
        }
96

97
        $this->current++;
14,513✔
98

99
        return $token;
14,513✔
100
    }
101

102
    /**
103
     * @throws GeometryIOException
104
     */
105
    public function matchOpener() : void
106
    {
107
        $token = $this->nextToken();
9,249✔
108

109
        if ($token === null) {
9,249✔
110
            throw new GeometryIOException("Expected '(' but encountered end of stream");
×
111
        }
112
        if ($token[1] !== '(') {
9,249✔
113
            throw new GeometryIOException("Expected '(' but encountered '" . $token[1] . "'");
×
114
        }
115
    }
116

117
    /**
118
     * @throws GeometryIOException
119
     */
120
    public function matchCloser() : void
121
    {
122
        $token = $this->nextToken();
1,508✔
123

124
        if ($token === null) {
1,508✔
125
            throw new GeometryIOException("Expected ')' but encountered end of stream");
×
126
        }
127
        if ($token[1] !== ')') {
1,508✔
128
            throw new GeometryIOException("Expected ')' but encountered '" . $token[1] . "'");
×
129
        }
130
    }
131

132
    /**
133
     * @throws GeometryIOException
134
     */
135
    public function getNextWord() : string
136
    {
137
        $token = $this->nextToken();
14,513✔
138

139
        if ($token === null) {
14,513✔
140
            throw new GeometryIOException("Expected word but encountered end of stream");
×
141
        }
142
        if ($token[0] !== WKTTokenType::Word) {
14,513✔
143
            throw new GeometryIOException("Expected word but encountered '" . $token[1] . "'");
×
144
        }
145

146
        return $token[1];
14,513✔
147
    }
148

149
    /**
150
     * @return string|null The next word, or NULL if the next token is not a word, or there are no more tokens.
151
     */
152
    public function getOptionalNextWord() : ?string
153
    {
154
        $token = $this->tokens[$this->current] ?? null;
14,513✔
155

156
        if ($token === null) {
14,513✔
157
            return null;
×
158
        }
159

160
        if ($token[0] !== WKTTokenType::Word) {
14,513✔
161
            return null;
9,249✔
162
        }
163

164
        $this->current++;
10,971✔
165

166
        return $token[1];
10,971✔
167
    }
168

169
    /**
170
     * Returns whether the next token is an opener or a word.
171
     *
172
     * @return bool True if the next token is an opener, false if it is a word.
173
     *
174
     * @throws GeometryIOException If the next token is not an opener or a word, or if there is no next token.
175
     */
176
    public function isNextOpenerOrWord() : bool
177
    {
178
        $token = $this->tokens[$this->current] ?? null;
1,323✔
179

180
        if ($token === null) {
1,323✔
181
            throw new GeometryIOException("Expected '(' or word but encountered end of stream");
×
182
        }
183

184
        if ($token[1] === '(') {
1,323✔
185
            return true;
1,295✔
186
        }
187

188
        if ($token[0] === WKTTokenType::Word) {
1,127✔
189
            return false;
1,127✔
190
        }
191

192
        throw new GeometryIOException("Expected '(' or word but encountered '" . $token[1] . "'");
×
193
    }
194

195
    /**
196
     * @throws GeometryIOException
197
     */
198
    public function getNextNumber() : float
199
    {
200
        $token = $this->nextToken();
9,172✔
201

202
        if ($token === null) {
9,172✔
203
            throw new GeometryIOException("Expected number but encountered end of stream");
×
204
        }
205

206
        if ($token[0] !== WKTTokenType::Number) {
9,172✔
207
            throw new GeometryIOException("Expected number but encountered '" . $token[1] . "'");
×
208
        }
209

210
        return (float) $token[1];
9,172✔
211
    }
212

213
    /**
214
     * @throws GeometryIOException
215
     */
216
    public function getNextCloserOrComma() : string
217
    {
218
        $token = $this->nextToken();
8,387✔
219

220
        if ($token === null) {
8,387✔
221
            throw new GeometryIOException("Expected ')' or ',' but encountered end of stream");
×
222
        }
223
        if ($token[1] !== ')' && $token[1] !== ',') {
8,387✔
224
            throw new GeometryIOException("Expected ')' or ',' but encountered '" . $token[1] . "'");
×
225
        }
226

227
        return $token[1];
8,387✔
228
    }
229

230
    public function getOptionalSRID() : int
231
    {
232
        $token = $this->tokens[$this->current] ?? null;
4,760✔
233

234
        if ($token === null) {
4,760✔
NEW
235
            return 0;
×
236
        }
237

238
        if ($token[0] !== WKTTokenType::SRID) {
4,760✔
239
            return 0;
2,380✔
240
        }
241

242
        $this->current++;
2,380✔
243

244
        return (int) $token[1];
2,380✔
245
    }
246

247
    public function isEndOfStream() : bool
248
    {
249
        return $this->nextToken() === null;
14,464✔
250
    }
251
}
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