CalculationLocale   F
last analyzed

Complexity

Total Complexity 74

Size/Duplication

Total Lines 418
Duplicated Lines 0 %

Test Coverage

Coverage 96.02%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 74
eloc 188
c 1
b 0
f 0
dl 0
loc 418
ccs 169
cts 176
cp 0.9602
rs 2.48

14 Methods

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