Completed
Push — master ( a2ba0d...8a4e5e )
by z38
02:44
created

Px::assertIndexMultipliers()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 15
ccs 11
cts 11
cp 1
rs 9.4285
cc 3
eloc 9
nc 3
nop 0
crap 3
1
<?php
2
3
namespace Z38\PcAxis;
4
5
use RuntimeException;
6
use stdClass;
7
8
/**
9
 * PC-Axis (PX) file reader
10
 */
11
class Px
12
{
13
    const DEFAULT_CHARSET = '437';
14
15
    /**
16
     * @var string
17
     */
18
    private $path;
19
20
    /**
21
     * @var resource
22
     */
23
    private $handle;
24
25
    /**
26
     * @var array
27
     */
28
    private $keywords;
29
30
    /**
31
     * @var array
32
     */
33
    private $data;
34
35
    /**
36
     * @var int
37
     */
38
    private $dataOffset;
39
40
    /**
41
     * @var string
42
     */
43
    private $charset;
44
45
    /**
46
     * @var string
47
     */
48
    private $codepage;
49
50
    /**
51
     * @var array
52
     */
53
    private $indexMultipliers;
54
55
    /**
56
     * Constructor
57
     *
58
     * @param string $path path to your PX file
59
     */
60 8
    public function __construct($path)
61
    {
62 8
        $this->path = $path;
63 8
        $this->charset = self::DEFAULT_CHARSET;
64 8
    }
65
66
    /**
67
     * Returns a list of all variables.
68
     *
69
     * @return array
70
     */
71 3
    public function variables()
72
    {
73 3
        return array_merge($this->keyword('STUB')->values, $this->keyword('HEADING')->values);
74
    }
75
76
    /**
77
     * Returns a list of all possible values of a variable.
78
     *
79
     * @param string $variable
80
     *
81
     * @return array
82
     */
83 3
    public function values($variable)
84
    {
85 3
        foreach ($this->keywordList('VALUES') as $keyword) {
86 3
            if ($keyword->subKeys[0] == $variable) {
87 3
                return $keyword->values;
88
            }
89 3
        }
90
        throw new RuntimeException(sprintf('Could not determine values of "%s".', $variable));
91
    }
92
93
    /**
94
     * Returns a list of all possible codes of a variable.
95
     *
96
     * @param string $variable
97
     *
98
     * @return array|null
99
     */
100 2
    public function codes($variable)
101
    {
102 2
        foreach ($this->keywordList('CODES') as $keyword) {
103 2
            if ($keyword->subKeys[0] == $variable) {
104 1
                return $keyword->values;
105
            }
106 2
        }
107
108 1
        return null;
109
    }
110
111
    /**
112
     * Computes the index within the data matrix.
113
     *
114
     * @param array $indices An array of all value indices
115
     *
116
     * @return int
117
     */
118 2
    public function index($indices)
119
    {
120 2
        $this->assertIndexMultipliers();
121
122 2
        $index = 0;
123 2
        for ($i = 0, $length = count($this->indexMultipliers); $i < $length; ++$i) {
124 2
            $index += $indices[$i] * $this->indexMultipliers[$i];
125 2
        }
126
127 2
        return $index;
128
    }
129
130
    /**
131
     * Gets a single data point.
132
     *
133
     * @param array $indices An array of all value indices
134
     *
135
     * @return string
136
     */
137 2
    public function datum($indices)
138
    {
139 2
        $this->assertData();
140
141 2
        $index = $this->index($indices);
142 2
        if (isset($this->data[$index])) {
143 2
            return $this->data[$index];
144
        } else {
145
            return null;
146
        }
147
    }
148
149
    /**
150
     * Returns a list of all keywords.
151
     *
152
     * @return array
153
     */
154
    public function keywords()
155
    {
156
        $this->assertKeywords();
157
158
        return $this->keywords;
159
    }
160
161
    /**
162
     * Returns all keywords with a given name.
163
     *
164
     * @param string $keyword
165
     *
166
     * @return array
167
     */
168 7
    public function keywordList($keyword)
169
    {
170 7
        $this->assertKeywords();
171
172 7
        if (isset($this->keywords[$keyword])) {
173 7
            return $this->keywords[$keyword];
174
        } else {
175
            return [];
176
        }
177
    }
178
179
    /**
180
     * Checks whether a keyword exists.
181
     *
182
     * @param string $keyword
183
     *
184
     * @return bool
185
     */
186 1
    public function hasKeyword($keyword)
187
    {
188 1
        $this->assertKeywords();
189
190 1
        return isset($this->keywords[$keyword]);
191
    }
192
193
    /**
194
     * Returns the first keyword with a given name.
195
     *
196
     * @param string $keyword
197
     *
198
     * @return object
199
     */
200 4
    public function keyword($keyword)
201
    {
202 4
        $list = $this->keywordList($keyword);
203 4
        if (empty($list)) {
204
            throw new RuntimeException(sprintf('Keyword "%s" does not exist.', $keyword));
205
        }
206
207 4
        return $list[0];
208
    }
209
210
    /**
211
     * Gets all data cells.
212
     *
213
     * @param array
214
     */
215
    public function data()
216
    {
217
        $this->assertData();
218
219
        return $this->data;
220
    }
221
222 8
    private function parseKeywordLine($line)
223
    {
224 8
        $data = new stdClass();
225
226 8
        $line = trim(str_replace('""', ' ', $line));
227 8
        $data->raw = $line;
228
229 8
        $equalPos = self::findQuoted($line, '=');
230 8
        if ($equalPos <= 0) {
231
            return;
232
        }
233
234 8
        $key = substr($line, 0, $equalPos);
235 8
        $data->subKeys = [];
236 8
        if (substr($key, -1) === ')' && ($start = self::findQuotedReverse($key, '(')) !== false) {
237 8
            $data->subKeys = self::split(substr($key, $start + 1, -1));
238 8
            $key = substr($key, 0, $start);
239 8
        }
240 8
        $data->lang = null;
241 8
        if (substr($key, -1) === ']' && ($start = self::findQuotedReverse($key, '[')) !== false) {
242 1
            $data->lang = trim(substr($key, $start + 1, -1), '"');
243 1
            $key = substr($key, 0, $start);
244 1
        }
245
246 8
        $data->values = self::split(substr($line, $equalPos + 1));
247
248 8
        if (!isset($this->keywords[$key])) {
249 8
            $this->keywords[$key] = [];
250 8
        }
251 8
        $this->keywords[$key][] = $data;
252
253 8
        if ($key === 'CHARSET') {
254 8
            $this->charset = $data->values[0];
255 8
            if ($this->charset === 'ANSI' && $this->codepage === null) {
256 8
                $this->codepage = 'ISO-8859-1';
257 8
            }
258 8
        } elseif ($key === 'CODEPAGE') {
259 1
            $this->codepage = $data->values[0];
260 1
        }
261 8
    }
262
263 8
    private function assertKeywords()
264
    {
265 8
        if ($this->keywords !== null) {
266 4
            return;
267
        }
268
269 8
        $this->handle = fopen($this->path, 'r');
270 8
        if ($this->handle === false) {
271
            throw new RuntimeException('Could not open file.');
272
        }
273
274 8
        $this->keywords = [];
275 8
        $remainder = '';
276 8
        while (($line = fgets($this->handle)) !== false) {
277 8
            $line = trim($this->decodeLine($line));
278 8
            if ($line == 'DATA=') {
279 8
                break;
280
            }
281 8
            $remainder .= $line;
282 8
            while (($i = self::findQuoted($remainder, ';')) !== false) {
283 8
                $this->parseKeywordLine(substr($remainder, 0, $i));
284 8
                $remainder = substr($remainder, $i + 1);
285 8
            }
286 8
        }
287
288 8
        $this->dataOffset = ftell($this->handle);
289 8
    }
290
291 2
    private function assertData()
292
    {
293 2
        if ($this->data !== null) {
294 1
            return;
295
        }
296
297 2
        $this->assertKeywords();
298
299 2
        fseek($this->handle, $this->dataOffset, SEEK_SET);
300
301 2
        $raw = '';
302 2
        while (($line = fgets($this->handle)) !== false) {
303 2
            $line = trim($this->decodeLine($line), "\r\n");
304 2
            $raw .= $line;
305 2
        }
306
307 2
        $cells = [];
308 2
        $len = strlen($raw);
309 2
        $value = '';
310 2
        for ($i = 0; $i < $len; $i++) {
311 2
            if ($value === '' && $raw[$i] === '"' && ($end = strpos($raw, '"', $i + 1)) !== false) {
312 2
                $cells[] = substr($raw, $i + 1, $end - $i - 1);
313 2
                $i = $end;
314 2
                $value = '';
315 2
            } elseif (in_array($raw[$i], [' ', ',', ';', "\t"])) {
316 2
                if ($value !== '') {
317 2
                    $cells[] = $value;
318 2
                    $value = '';
319 2
                }
320 2
            } else {
321 2
                $value .= $raw[$i];
322
            }
323 2
        }
324 2
        if ($value !== '') {
325
            $cells[] = $value;
326
        }
327
328 2
        $this->data = $cells;
329
330 2
        fclose($this->handle);
331 2
    }
332
333 2
    private function assertIndexMultipliers()
334
    {
335 2
        if ($this->indexMultipliers !== null) {
336 1
            return;
337
        }
338
339 2
        $variables = $this->variables();
340 2
        $count = count($variables);
341
342 2
        $this->indexMultipliers = [];
343 2
        $this->indexMultipliers[$count - 1] = 1;
344 2
        for ($i = $count - 2; $i >= 0; --$i) {
345 2
            $this->indexMultipliers[$i] = count($this->values($variables[$i + 1])) * $this->indexMultipliers[$i + 1];
346 2
        }
347 2
    }
348
349 8
    private function decodeLine($line)
350
    {
351 8
        if ($this->codepage !== null) {
352 8
            $line = iconv($this->codepage, 'UTF8', $line);
353 8
        } elseif ($this->charset !== 'ANSI') {
354 8
            $line = iconv($this->charset, 'UTF8', $line);
355 8
        }
356
357 8
        return $line;
358
    }
359
360 8
    private static function split($string)
361
    {
362 8
        $values = [];
363 8
        while (($pos = self::findQuoted($string, ',')) !== false) {
364 8
            $values[] = trim(trim(substr($string, 0, $pos)), '"');
365 8
            $string = substr($string, $pos + 1);
366 8
        }
367 8
        $values[] = trim(trim($string), '"');
368
369 8
        return $values;
370
    }
371
372 8
    private static function findQuoted($haystack, $needle)
373
    {
374 8
        $pos = 0;
375 8
        while (($pos = strpos($haystack, $needle, $pos)) !== false) {
376 8
            if (substr_count($haystack, '"', 0, $pos) % 2 == 0) {
377 8
                return $pos;
378
            }
379 8
            $pos++;
380 8
        }
381
382 8
        return $pos;
383
    }
384
385 8
    private static function findQuotedReverse($haystack, $needle)
386
    {
387 8
        $len = strlen($haystack);
388 8
        $pos = strlen($haystack);
389 8
        while (($pos = strrpos($haystack, $needle, $pos - $len)) !== false) {
390 8
            if (substr_count($haystack, '"', $pos) % 2 == 0) {
391 8
                return $pos;
392
            }
393
            $pos--;
394
        }
395
396
        return $pos;
397
    }
398
}
399