Failed Conditions
Pull Request — master (#4398)
by Owen
15:22
created

CalculationLocale   F

Complexity

Total Complexity 74

Size/Duplication

Total Lines 405
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 74
eloc 188
dl 0
loc 405
rs 2.48
c 1
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
B translateFormulaToEnglish() 0 23 7
D setLocale() 0 81 21
A loadLocales() 0 8 4
A getFALSE() 0 3 1
A localeFunc() 0 14 4
A getLocaleBoolean() 0 3 1
A translateFormulaBlock() 0 22 1
A getTRUE() 0 3 1
C getFalseTrueArray() 0 41 12
B translateFormulaToLocale() 0 30 7
A getLocale() 0 3 1
A translateFormula() 0 30 5
A translateSeparator() 0 28 6
A getLocaleFile() 0 13 3

How to fix   Complexity   

Complex Class

Complex classes like CalculationLocale often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CalculationLocale, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Calculation;
4
5
class CalculationLocale extends CalculationBase
6
{
7
    public const FORMULA_OPEN_FUNCTION_BRACE = '(';
8
    public const FORMULA_CLOSE_FUNCTION_BRACE = ')';
9
    public const FORMULA_OPEN_MATRIX_BRACE = '{';
10
    public const FORMULA_CLOSE_MATRIX_BRACE = '}';
11
    public const FORMULA_STRING_QUOTE = '"';
12
13
    //    Strip xlfn and xlws prefixes from function name
14
    public const CALCULATION_REGEXP_STRIP_XLFN_XLWS = '/(_xlfn[.])?(_xlws[.])?(?=[\p{L}][\p{L}\p{N}\.]*[\s]*[(])/';
15
16
    /**
17
     * The current locale setting.
18
     */
19
    protected static string $localeLanguage = 'en_us'; //    US English    (default locale)
20
21
    /**
22
     * List of available locale settings
23
     * Note that this is read for the locale subdirectory only when requested.
24
     *
25
     * @var string[]
26
     */
27
    protected static array $validLocaleLanguages = [
28
        'en', //    English        (default language)
29
    ];
30
31
    /**
32
     * Locale-specific argument separator for function arguments.
33
     */
34
    protected static string $localeArgumentSeparator = ',';
35
36
    protected static array $localeFunctions = [];
37
38
    /**
39
     * Locale-specific translations for Excel constants (True, False and Null).
40
     *
41
     * @var array<string, string>
42
     */
43
    protected static array $localeBoolean = [
44
        'TRUE' => 'TRUE',
45
        'FALSE' => 'FALSE',
46
        'NULL' => 'NULL',
47
    ];
48
49
    /** @var array<int, array<int, string>> */
50
    protected static array $falseTrueArray = [];
51
52
    public static function getLocaleBoolean(string $index): string
53
    {
54
        return self::$localeBoolean[$index];
55
    }
56
57
    protected static function loadLocales(): void
58
    {
59
        $localeFileDirectory = __DIR__ . '/locale/';
60
        $localeFileNames = glob($localeFileDirectory . '*', GLOB_ONLYDIR) ?: [];
61
        foreach ($localeFileNames as $filename) {
62
            $filename = substr($filename, strlen($localeFileDirectory));
63
            if ($filename != 'en') {
64
                self::$validLocaleLanguages[] = $filename;
65
            }
66
        }
67
    }
68
69
    /**
70
     * Return the locale-specific translation of TRUE.
71
     *
72
     * @return string locale-specific translation of TRUE
73
     */
74
    public static function getTRUE(): string
75
    {
76
        return self::$localeBoolean['TRUE'];
77
    }
78
79
    /**
80
     * Return the locale-specific translation of FALSE.
81
     *
82
     * @return string locale-specific translation of FALSE
83
     */
84
    public static function getFALSE(): string
85
    {
86
        return self::$localeBoolean['FALSE'];
87
    }
88
89
    /**
90
     * Get the currently defined locale code.
91
     */
92
    public function getLocale(): string
93
    {
94
        return self::$localeLanguage;
95
    }
96
97
    protected function getLocaleFile(string $localeDir, string $locale, string $language, string $file): string
98
    {
99
        $localeFileName = $localeDir . str_replace('_', DIRECTORY_SEPARATOR, $locale)
100
            . DIRECTORY_SEPARATOR . $file;
101
        if (!file_exists($localeFileName)) {
102
            //    If there isn't a locale specific file, look for a language specific file
103
            $localeFileName = $localeDir . $language . DIRECTORY_SEPARATOR . $file;
104
            if (!file_exists($localeFileName)) {
105
                throw new Exception('Locale file not found');
106
            }
107
        }
108
109
        return $localeFileName;
110
    }
111
112
    /** @return array<int, array<int, string>> */
113
    public function getFalseTrueArray(): array
114
    {
115
        if (!empty(self::$falseTrueArray)) {
116
            return self::$falseTrueArray;
117
        }
118
        if (count(self::$validLocaleLanguages) == 1) {
119
            self::loadLocales();
120
        }
121
        $falseTrueArray = [['FALSE'], ['TRUE']];
122
        foreach (self::$validLocaleLanguages as $language) {
123
            if (str_starts_with($language, 'en')) {
124
                continue;
125
            }
126
            $locale = $language;
127
            if (str_contains($locale, '_')) {
128
                [$language] = explode('_', $locale);
129
            }
130
            $localeDir = implode(DIRECTORY_SEPARATOR, [__DIR__, 'locale', null]);
131
132
            try {
133
                $functionNamesFile = $this->getLocaleFile($localeDir, $locale, $language, 'functions');
134
            } catch (Exception $e) {
135
                continue;
136
            }
137
            //    Retrieve the list of locale or language specific function names
138
            $localeFunctions = file($functionNamesFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
139
            foreach ($localeFunctions as $localeFunction) {
140
                [$localeFunction] = explode('##', $localeFunction); //    Strip out comments
141
                if (str_contains($localeFunction, '=')) {
142
                    [$fName, $lfName] = array_map('trim', explode('=', $localeFunction));
143
                    if ($fName === 'FALSE') {
144
                        $falseTrueArray[0][] = $lfName;
145
                    } elseif ($fName === 'TRUE') {
146
                        $falseTrueArray[1][] = $lfName;
147
                    }
148
                }
149
            }
150
        }
151
        self::$falseTrueArray = $falseTrueArray;
152
153
        return $falseTrueArray;
154
    }
155
156
    /**
157
     * Set the locale code.
158
     *
159
     * @param string $locale The locale to use for formula translation, eg: 'en_us'
160
     */
161
    public function setLocale(string $locale): bool
162
    {
163
        //    Identify our locale and language
164
        $language = $locale = strtolower($locale);
165
        if (str_contains($locale, '_')) {
166
            [$language] = explode('_', $locale);
167
        }
168
        if (count(self::$validLocaleLanguages) == 1) {
169
            self::loadLocales();
170
        }
171
172
        //    Test whether we have any language data for this language (any locale)
173
        if (in_array($language, self::$validLocaleLanguages, true)) {
174
            //    initialise language/locale settings
175
            self::$localeFunctions = [];
176
            self::$localeArgumentSeparator = ',';
177
            self::$localeBoolean = ['TRUE' => 'TRUE', 'FALSE' => 'FALSE', 'NULL' => 'NULL'];
178
179
            //    Default is US English, if user isn't requesting US english, then read the necessary data from the locale files
180
            if ($locale !== 'en_us') {
181
                $localeDir = implode(DIRECTORY_SEPARATOR, [__DIR__, 'locale', null]);
182
183
                //    Search for a file with a list of function names for locale
184
                try {
185
                    $functionNamesFile = $this->getLocaleFile($localeDir, $locale, $language, 'functions');
186
                } catch (Exception $e) {
187
                    return false;
188
                }
189
190
                //    Retrieve the list of locale or language specific function names
191
                $localeFunctions = file($functionNamesFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
192
                $phpSpreadsheetFunctions = &self::getFunctionsAddress();
193
                foreach ($localeFunctions as $localeFunction) {
194
                    [$localeFunction] = explode('##', $localeFunction); //    Strip out comments
195
                    if (str_contains($localeFunction, '=')) {
196
                        [$fName, $lfName] = array_map('trim', explode('=', $localeFunction));
197
                        if ((str_starts_with($fName, '*') || isset($phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) {
198
                            self::$localeFunctions[$fName] = $lfName;
199
                        }
200
                    }
201
                }
202
                //    Default the TRUE and FALSE constants to the locale names of the TRUE() and FALSE() functions
203
                if (isset(self::$localeFunctions['TRUE'])) {
204
                    self::$localeBoolean['TRUE'] = self::$localeFunctions['TRUE'];
205
                }
206
                if (isset(self::$localeFunctions['FALSE'])) {
207
                    self::$localeBoolean['FALSE'] = self::$localeFunctions['FALSE'];
208
                }
209
210
                try {
211
                    $configFile = $this->getLocaleFile($localeDir, $locale, $language, 'config');
212
                } catch (Exception) {
213
                    return false;
214
                }
215
216
                $localeSettings = file($configFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
217
                foreach ($localeSettings as $localeSetting) {
218
                    [$localeSetting] = explode('##', $localeSetting); //    Strip out comments
219
                    if (str_contains($localeSetting, '=')) {
220
                        [$settingName, $settingValue] = array_map('trim', explode('=', $localeSetting));
221
                        $settingName = strtoupper($settingName);
222
                        if ($settingValue !== '') {
223
                            switch ($settingName) {
224
                                case 'ARGUMENTSEPARATOR':
225
                                    self::$localeArgumentSeparator = $settingValue;
226
227
                                    break;
228
                            }
229
                        }
230
                    }
231
                }
232
            }
233
234
            self::$functionReplaceFromExcel = self::$functionReplaceToExcel
235
            = self::$functionReplaceFromLocale = self::$functionReplaceToLocale = null;
236
            self::$localeLanguage = $locale;
237
238
            return true;
239
        }
240
241
        return false;
242
    }
243
244
    public static function translateSeparator(
245
        string $fromSeparator,
246
        string $toSeparator,
247
        string $formula,
248
        int &$inBracesLevel,
249
        string $openBrace = self::FORMULA_OPEN_FUNCTION_BRACE,
250
        string $closeBrace = self::FORMULA_CLOSE_FUNCTION_BRACE
251
    ): string {
252
        $strlen = mb_strlen($formula);
253
        for ($i = 0; $i < $strlen; ++$i) {
254
            $chr = mb_substr($formula, $i, 1);
255
            switch ($chr) {
256
                case $openBrace:
257
                    ++$inBracesLevel;
258
259
                    break;
260
                case $closeBrace:
261
                    --$inBracesLevel;
262
263
                    break;
264
                case $fromSeparator:
265
                    if ($inBracesLevel > 0) {
266
                        $formula = mb_substr($formula, 0, $i) . $toSeparator . mb_substr($formula, $i + 1);
267
                    }
268
            }
269
        }
270
271
        return $formula;
272
    }
273
274
    protected static function translateFormulaBlock(
275
        array $from,
276
        array $to,
277
        string $formula,
278
        int &$inFunctionBracesLevel,
279
        int &$inMatrixBracesLevel,
280
        string $fromSeparator,
281
        string $toSeparator
282
    ): string {
283
        // Function Names
284
        $formula = (string) preg_replace($from, $to, $formula);
285
286
        // Temporarily adjust matrix separators so that they won't be confused with function arguments
287
        $formula = self::translateSeparator(';', '|', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE);
288
        $formula = self::translateSeparator(',', '!', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE);
289
        // Function Argument Separators
290
        $formula = self::translateSeparator($fromSeparator, $toSeparator, $formula, $inFunctionBracesLevel);
291
        // Restore matrix separators
292
        $formula = self::translateSeparator('|', ';', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE);
293
        $formula = self::translateSeparator('!', ',', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE);
294
295
        return $formula;
296
    }
297
298
    protected static function translateFormula(array $from, array $to, string $formula, string $fromSeparator, string $toSeparator): string
299
    {
300
        // Convert any Excel function names and constant names to the required language;
301
        //     and adjust function argument separators
302
        if (self::$localeLanguage !== 'en_us') {
303
            $inFunctionBracesLevel = 0;
304
            $inMatrixBracesLevel = 0;
305
            //    If there is the possibility of separators within a quoted string, then we treat them as literals
306
            if (str_contains($formula, self::FORMULA_STRING_QUOTE)) {
307
                //    So instead we skip replacing in any quoted strings by only replacing in every other array element
308
                //       after we've exploded the formula
309
                $temp = explode(self::FORMULA_STRING_QUOTE, $formula);
310
                $notWithinQuotes = false;
311
                foreach ($temp as &$value) {
312
                    //    Only adjust in alternating array entries
313
                    $notWithinQuotes = $notWithinQuotes === false;
314
                    if ($notWithinQuotes === true) {
315
                        $value = self::translateFormulaBlock($from, $to, $value, $inFunctionBracesLevel, $inMatrixBracesLevel, $fromSeparator, $toSeparator);
316
                    }
317
                }
318
                unset($value);
319
                //    Then rebuild the formula string
320
                $formula = implode(self::FORMULA_STRING_QUOTE, $temp);
321
            } else {
322
                //    If there's no quoted strings, then we do a simple count/replace
323
                $formula = self::translateFormulaBlock($from, $to, $formula, $inFunctionBracesLevel, $inMatrixBracesLevel, $fromSeparator, $toSeparator);
324
            }
325
        }
326
327
        return $formula;
328
    }
329
330
    private static ?array $functionReplaceFromExcel;
331
332
    private static ?array $functionReplaceToLocale;
333
334
    public function translateFormulaToLocale(string $formula): string
335
    {
336
        $formula = preg_replace(self::CALCULATION_REGEXP_STRIP_XLFN_XLWS, '', $formula) ?? '';
337
        // Build list of function names and constants for translation
338
        if (self::$functionReplaceFromExcel === null) {
339
            self::$functionReplaceFromExcel = [];
340
            foreach (array_keys(self::$localeFunctions) as $excelFunctionName) {
341
                self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelFunctionName, '/') . '([\s]*\()/ui';
342
            }
343
            foreach (array_keys(self::$localeBoolean) as $excelBoolean) {
344
                self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/ui';
345
            }
346
        }
347
348
        if (self::$functionReplaceToLocale === null) {
349
            self::$functionReplaceToLocale = [];
350
            foreach (self::$localeFunctions as $localeFunctionName) {
351
                self::$functionReplaceToLocale[] = '$1' . trim($localeFunctionName) . '$2';
352
            }
353
            foreach (self::$localeBoolean as $localeBoolean) {
354
                self::$functionReplaceToLocale[] = '$1' . trim($localeBoolean) . '$2';
355
            }
356
        }
357
358
        return self::translateFormula(
359
            self::$functionReplaceFromExcel,
360
            self::$functionReplaceToLocale,
361
            $formula,
362
            ',',
363
            self::$localeArgumentSeparator
364
        );
365
    }
366
367
    protected static ?array $functionReplaceFromLocale;
368
369
    protected static ?array $functionReplaceToExcel;
370
371
    public function translateFormulaToEnglish(string $formula): string
372
    {
373
        if (self::$functionReplaceFromLocale === null) {
374
            self::$functionReplaceFromLocale = [];
375
            foreach (self::$localeFunctions as $localeFunctionName) {
376
                self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($localeFunctionName, '/') . '([\s]*\()/ui';
377
            }
378
            foreach (self::$localeBoolean as $excelBoolean) {
379
                self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/ui';
380
            }
381
        }
382
383
        if (self::$functionReplaceToExcel === null) {
384
            self::$functionReplaceToExcel = [];
385
            foreach (array_keys(self::$localeFunctions) as $excelFunctionName) {
386
                self::$functionReplaceToExcel[] = '$1' . trim($excelFunctionName) . '$2';
387
            }
388
            foreach (array_keys(self::$localeBoolean) as $excelBoolean) {
389
                self::$functionReplaceToExcel[] = '$1' . trim($excelBoolean) . '$2';
390
            }
391
        }
392
393
        return self::translateFormula(self::$functionReplaceFromLocale, self::$functionReplaceToExcel, $formula, self::$localeArgumentSeparator, ',');
394
    }
395
396
    public static function localeFunc(string $function): string
397
    {
398
        if (self::$localeLanguage !== 'en_us') {
399
            $functionName = trim($function, '(');
400
            if (isset(self::$localeFunctions[$functionName])) {
401
                $brace = ($functionName != $function);
402
                $function = self::$localeFunctions[$functionName];
403
                if ($brace) {
404
                    $function .= '(';
405
                }
406
            }
407
        }
408
409
        return $function;
410
    }
411
}
412