Completed
Push — reducePrefixMapperFileCount ( 09d7d0...7b6d76 )
by Joshua
20:39 queued 08:53
created

GeneratePhonePrefixData::createOutputFileNames()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 43
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 43
rs 8.439
c 0
b 0
f 0
cc 6
eloc 20
nc 3
nop 4
1
<?php
2
3
namespace libphonenumber\buildtools;
4
5
use Symfony\Component\Console\Helper\ProgressBar;
6
use Symfony\Component\Console\Output\OutputInterface;
7
8
/**
9
 * Class GeneratePhonePrefixData
10
 * @package libphonenumber\buildtools
11
 * @internal
12
 */
13
class GeneratePhonePrefixData
14
{
15
    const NANPA_COUNTRY_CODE = 1;
16
    const DATA_FILE_EXTENSION = '.txt';
17
    const GENERATION_COMMENT = <<<'EOT'
18
/**
19
 * This file is automatically @generated by {@link GeneratePhonePrefixData}.
20
 * Please don't modify it directly.
21
 */
22
23
24
EOT;
25
26
    public $inputDir;
27
    private $filesToIgnore = array('.', '..', '.svn', '.git');
28
    private $outputDir;
29
    private $englishMaps = array();
30
    private $prefixesToExpand = array(
31
        861 => 5,
32
        12 => 2,
33
        13 => 2,
34
        14 => 2,
35
        15 => 2,
36
        16 => 2,
37
        17 => 2,
38
        18 => 2,
39
        19 => 2,
40
    );
41
42
43
    public function start($inputDir, $outputDir, OutputInterface $consoleOutput, $expandCountries)
44
    {
45
        $this->inputDir = $inputDir;
46
        $this->outputDir = $outputDir;
47
48
        $inputOutputMappings = $this->createInputOutputMappings($expandCountries);
49
        $availableDataFiles = array();
50
51
        $progress = new ProgressBar($consoleOutput, count($inputOutputMappings));
52
53
        $progress->start();
54
        foreach ($inputOutputMappings as $textFile => $outputFiles) {
55
            $mappings = $this->readMappingsFromFile($textFile);
56
57
            $language = $this->getLanguageFromTextFile($textFile);
58
59
            $this->removeEmptyEnglishMappings($mappings, $language);
60
            $this->makeDataFallbackToEnglish($textFile, $mappings);
61
            $mappingForFiles = $this->splitMap($mappings, $outputFiles);
62
63
            foreach ($mappingForFiles as $outputFile => $value) {
64
                $this->writeMappingFile($language, $outputFile, $value);
65
                $this->addConfigurationMapping($availableDataFiles, $language, $outputFile);
66
            }
67
            $progress->advance();
68
        }
69
70
        $this->writeConfigMap($availableDataFiles);
71
        $progress->finish();
72
    }
73
74
    private function createInputOutputMappings($expandCountries)
75
    {
76
        $topLevel = scandir($this->inputDir);
77
78
        $mappings = array();
79
80
        foreach ($topLevel as $languageDirectory) {
81
            if (in_array($languageDirectory, $this->filesToIgnore)) {
82
                continue;
83
            }
84
85
            $fileLocation = $this->inputDir . DIRECTORY_SEPARATOR . $languageDirectory;
86
87
            if (is_dir($fileLocation)) {
88
                // Will contain files
89
90
                $countryCodeFiles = scandir($fileLocation);
91
92
                foreach ($countryCodeFiles as $countryCodeFileName) {
93
                    if (in_array($countryCodeFileName, $this->filesToIgnore)) {
94
                        continue;
95
                    }
96
97
98
                    $outputFiles = $this->createOutputFileNames(
99
                        $countryCodeFileName,
100
                        $this->getCountryCodeFromTextFileName($countryCodeFileName),
101
                        $languageDirectory,
102
                        $expandCountries
103
                    );
104
105
                    $mappings[$languageDirectory . DIRECTORY_SEPARATOR . $countryCodeFileName] = $outputFiles;
106
                }
107
            }
108
        }
109
110
        return $mappings;
111
    }
112
113
    /**
114
     * Method used by {@code #createInputOutputMappings()} to generate the list of output binary files
115
     * from the provided input text file. For the data files expected to be large (currently only
116
     * NANPA is supported), this method generates a list containing one output file for each area
117
     * code. Otherwise, a single file is added to the list.
118
     */
119
120
    private function createOutputFileNames($file, $countryCode, $language, $expandCountries)
0 ignored issues
show
Unused Code introduced by
The parameter $file is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
121
    {
122
        $outputFiles = array();
123
124
        if ($expandCountries === false) {
125
            $outputFiles[] = $this->generateFilename($countryCode, $language);
126
            return $outputFiles;
127
        }
128
129
        /*
130
         * Reduce memory usage for China numbers
131
         * @see https://github.com/giggsey/libphonenumber-for-php/issues/44
132
         *
133
         * Analytics of the data suggests that the following prefixes need expanding:
134
         *  - 861 (to 5 chars)
135
         */
136
        $phonePrefixes = array();
137
        $prefixesToExpand = $this->prefixesToExpand;
138
139
        $this->parseTextFile(
140
            $this->getFilePathFromLanguageAndCountryCode($language, $countryCode),
141
            function ($prefix, $location) use (&$phonePrefixes, $prefixesToExpand, $countryCode) {
0 ignored issues
show
Unused Code introduced by
The parameter $location is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
142
                $length = strlen($countryCode);
143
                foreach ($prefixesToExpand as $p => $l) {
144
                    if (GeneratePhonePrefixData::startsWith($prefix, $p)) {
145
                        // Allow later entries to overwrite initial ones
146
                        $length = $l;
147
                    }
148
                }
149
150
                $shortPrefix = substr($prefix, 0, $length);
151
                if (!in_array($shortPrefix, $phonePrefixes)) {
152
                    $phonePrefixes[] = $shortPrefix;
153
                }
154
            }
155
        );
156
157
        foreach ($phonePrefixes as $prefix) {
158
            $outputFiles[] = $this->generateFilename($prefix, $language);
159
        }
160
161
        return $outputFiles;
162
    }
163
164
    /**
165
     * Reads phone prefix data from the provides file path and invokes the given handler for each
166
     * mapping read.
167
     *
168
     * @param $filePath
169
     * @param $handler
170
     * @return array
171
     * @throws \InvalidArgumentException
172
     */
173
    private function parseTextFile($filePath, \Closure $handler)
174
    {
175
        if (!file_exists($filePath) || !is_readable($filePath)) {
176
            throw new \InvalidArgumentException("File '{$filePath}' does not exist");
177
        }
178
179
        $data = file($filePath);
180
181
        $countryData = array();
182
183
        foreach ($data as $line) {
184
            // Remove \n
185
            $line = str_replace("\n", "", $line);
186
            $line = str_replace("\r", "", $line);
187
            $line = trim($line);
188
189
            if (strlen($line) == 0 || substr($line, 0, 1) == '#') {
190
                continue;
191
            }
192
            if (strpos($line, '|')) {
193
                // Valid line
194
                $parts = explode('|', $line);
195
196
197
                $prefix = $parts[0];
198
                $location = $parts[1];
199
200
                $handler($prefix, $location);
201
            }
202
        }
203
204
        return $countryData;
205
    }
206
207
    private function getFilePathFromLanguageAndCountryCode($language, $code)
208
    {
209
        return $this->getFilePath($language . DIRECTORY_SEPARATOR . $code . self::DATA_FILE_EXTENSION);
210
    }
211
212
    private function getFilePath($fileName)
213
    {
214
        $path = $this->inputDir . $fileName;
215
216
        return $path;
217
    }
218
219
    private function generateFilename($prefix, $language)
220
    {
221
        return $language . DIRECTORY_SEPARATOR . $prefix . self::DATA_FILE_EXTENSION;
222
    }
223
224
    private function getCountryCodeFromTextFileName($countryCodeFileName)
225
    {
226
        return str_replace(self::DATA_FILE_EXTENSION, '', $countryCodeFileName);
227
    }
228
229
    private function readMappingsFromFile($inputFile)
230
    {
231
        $areaCodeMap = array();
232
233
        $this->parseTextFile(
234
            $this->inputDir . $inputFile,
235
            function ($prefix, $location) use (&$areaCodeMap) {
236
                $areaCodeMap[$prefix] = $location;
237
            }
238
        );
239
240
        return $areaCodeMap;
241
    }
242
243
    private function getLanguageFromTextFile($textFile)
244
    {
245
        $parts = explode(DIRECTORY_SEPARATOR, $textFile);
246
247
        return $parts[0];
248
    }
249
250
    private function removeEmptyEnglishMappings(&$mappings, $language)
251
    {
252
        if ($language != "en") {
253
            return;
254
        }
255
256
        foreach ($mappings as $k => $v) {
257
            if ($v == "") {
258
                unset($mappings[$k]);
259
            }
260
        }
261
    }
262
263
    /**
264
     * Compress the provided mappings according to the English data file if any.
265
     * @param string $textFile
266
     * @param array $mappings
267
     */
268
    private function makeDataFallbackToEnglish($textFile, &$mappings)
269
    {
270
        $englishPath = $this->getEnglishDataPath($textFile);
271
272
        if ($textFile == $englishPath || !file_exists($this->getFilePath($englishPath))) {
273
            return;
274
        }
275
276
        $countryCode = substr($textFile, 3, 2);
277
278
        if (!array_key_exists($countryCode, $this->englishMaps)) {
279
            $englishMap = $this->readMappingsFromFile($englishPath);
280
281
            $this->englishMaps[$countryCode] = $englishMap;
282
        }
283
284
        $this->compressAccordingToEnglishData($this->englishMaps[$countryCode], $mappings);
285
    }
286
287
    private function getEnglishDataPath($textFile)
288
    {
289
        return "en" . DIRECTORY_SEPARATOR . substr($textFile, 3);
290
    }
291
292
    private function compressAccordingToEnglishData($englishMap, &$nonEnglishMap)
293
    {
294
        foreach ($nonEnglishMap as $prefix => $value) {
295
            if (array_key_exists($prefix, $englishMap)) {
296
                $englishDescription = $englishMap[$prefix];
297
                if ($englishDescription == $value) {
298
                    if (!$this->hasOverlappingPrefix($prefix, $nonEnglishMap)) {
299
                        unset($nonEnglishMap[$prefix]);
300
                    } else {
301
                        $nonEnglishMap[$prefix] = "";
302
                    }
303
                }
304
            }
305
        }
306
    }
307
308
    private function hasOverlappingPrefix($number, $mappings)
309
    {
310
        while (strlen($number) > 0) {
311
            $number = substr($number, 0, -1);
312
313
            if (array_key_exists($number, $mappings)) {
314
                return true;
315
            }
316
        }
317
318
        return false;
319
    }
320
321
    private function splitMap($mappings, $outputFiles)
322
    {
323
        $mappingForFiles = array();
324
325
        foreach ($mappings as $prefix => $location) {
326
            $targetFile = null;
327
328
            foreach ($outputFiles as $k => $outputFile) {
329
                $outputFilePrefix = $this->getPhonePrefixLanguagePairFromFilename($outputFile)->prefix;
330
                if (self::startsWith($prefix, $outputFilePrefix)) {
331
                    $targetFile = $outputFilePrefix;
332
                    break;
333
                }
334
            }
335
336
            if (!array_key_exists($targetFile, $mappingForFiles)) {
337
                $mappingForFiles[$targetFile] = array();
338
            }
339
            $mappingForFiles[$targetFile][$prefix] = $location;
340
        }
341
342
        return $mappingForFiles;
343
    }
344
345
    /**
346
     * Extracts the phone prefix and the language code contained in the provided file name.
347
     */
348
    private function getPhonePrefixLanguagePairFromFilename($outputFile)
349
    {
350
        $parts = explode(DIRECTORY_SEPARATOR, $outputFile);
351
352
        $returnObj = new \stdClass();
353
        $returnObj->language = $parts[0];
354
355
        $returnObj->prefix = $this->getCountryCodeFromTextFileName($parts[1]);
356
357
        return $returnObj;
358
    }
359
360
    /**
361
     *
362
     * @link http://stackoverflow.com/a/834355/403165
363
     * @param $haystack
364
     * @param $needle
365
     * @return bool
366
     */
367
    private static function startsWith($haystack, $needle)
368
    {
369
        return !strncmp($haystack, $needle, strlen($needle));
370
    }
371
372
    private function writeMappingFile($language, $outputFile, $data)
373
    {
374
        if (!file_exists($this->outputDir . $language)) {
375
            mkdir($this->outputDir . $language);
376
        }
377
378
        $phpSource = '<?php' . PHP_EOL
379
            . self::GENERATION_COMMENT
380
            . 'return ' . var_export($data, true) . ';'
381
            . PHP_EOL;
382
383
        $outputPath = $this->outputDir . $language . DIRECTORY_SEPARATOR . $outputFile . '.php';
384
385
        file_put_contents($outputPath, $phpSource);
386
    }
387
388
    public function addConfigurationMapping(&$availableDataFiles, $language, $prefix)
389
    {
390
        if (!array_key_exists($language, $availableDataFiles)) {
391
            $availableDataFiles[$language] = array();
392
        }
393
394
        $availableDataFiles[$language][] = $prefix;
395
    }
396
397
    private function writeConfigMap($availableDataFiles)
398
    {
399
        $phpSource = '<?php' . PHP_EOL
400
            . self::GENERATION_COMMENT
401
            . 'return ' . var_export($availableDataFiles, true) . ';'
402
            . PHP_EOL;
403
404
        $outputPath = $this->outputDir . 'Map.php';
405
406
        file_put_contents($outputPath, $phpSource);
407
    }
408
}
409