chengkun
2025-09-15 0cc7f61de2b106c9664033fc27d6426d072ea019
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
<?php
 
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
 
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
 
class FormattedNumber
{
    /**    Constants                */
    /**    Regular Expressions        */
    private const STRING_REGEXP_FRACTION = '~^\s*(-?)((\d*)\s+)?(\d+\/\d+)\s*$~';
 
    private const STRING_REGEXP_PERCENT = '~^(?:(?: *(?<PrefixedSign>[-+])? *\% *(?<PrefixedSign2>[-+])? *(?<PrefixedValue>[0-9]+\.?[0-9*]*(?:E[-+]?[0-9]*)?) *)|(?: *(?<PostfixedSign>[-+])? *(?<PostfixedValue>[0-9]+\.?[0-9]*(?:E[-+]?[0-9]*)?) *\% *))$~i';
 
    // preg_quoted string for major currency symbols, with a %s for locale currency
    private const CURRENCY_CONVERSION_LIST = '\$€£¥%s';
 
    private const STRING_CONVERSION_LIST = [
        [self::class, 'convertToNumberIfNumeric'],
        [self::class, 'convertToNumberIfFraction'],
        [self::class, 'convertToNumberIfPercent'],
        [self::class, 'convertToNumberIfCurrency'],
    ];
 
    /**
     * Identify whether a string contains a formatted numeric value,
     * and convert it to a numeric if it is.
     *
     * @param string $operand string value to test
     */
    public static function convertToNumberIfFormatted(string &$operand): bool
    {
        foreach (self::STRING_CONVERSION_LIST as $conversionMethod) {
            if ($conversionMethod($operand) === true) {
                return true;
            }
        }
 
        return false;
    }
 
    /**
     * Identify whether a string contains a numeric value,
     * and convert it to a numeric if it is.
     *
     * @param string $operand string value to test
     */
    public static function convertToNumberIfNumeric(string &$operand): bool
    {
        $thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/');
        $value = preg_replace(['/(\d)' . $thousandsSeparator . '(\d)/u', '/([+-])\s+(\d)/u'], ['$1$2', '$1$2'], trim($operand));
        $decimalSeparator = preg_quote(StringHelper::getDecimalSeparator(), '/');
        $value = preg_replace(['/(\d)' . $decimalSeparator . '(\d)/u', '/([+-])\s+(\d)/u'], ['$1.$2', '$1$2'], $value ?? '');
 
        if (is_numeric($value)) {
            $operand = (float) $value;
 
            return true;
        }
 
        return false;
    }
 
    /**
     * Identify whether a string contains a fractional numeric value,
     * and convert it to a numeric if it is.
     *
     * @param string $operand string value to test
     */
    public static function convertToNumberIfFraction(string &$operand): bool
    {
        if (preg_match(self::STRING_REGEXP_FRACTION, $operand, $match)) {
            $sign = ($match[1] === '-') ? '-' : '+';
            $wholePart = ($match[3] === '') ? '' : ($sign . $match[3]);
            $fractionFormula = '=' . $wholePart . $sign . $match[4];
            $operand = Calculation::getInstance()->_calculateFormulaValue($fractionFormula);
 
            return true;
        }
 
        return false;
    }
 
    /**
     * Identify whether a string contains a percentage, and if so,
     * convert it to a numeric.
     *
     * @param string $operand string value to test
     */
    public static function convertToNumberIfPercent(string &$operand): bool
    {
        $thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/');
        $value = preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', trim($operand));
        $decimalSeparator = preg_quote(StringHelper::getDecimalSeparator(), '/');
        $value = preg_replace(['/(\d)' . $decimalSeparator . '(\d)/u', '/([+-])\s+(\d)/u'], ['$1.$2', '$1$2'], $value ?? '');
 
        $match = [];
        if ($value !== null && preg_match(self::STRING_REGEXP_PERCENT, $value, $match, PREG_UNMATCHED_AS_NULL)) {
            //Calculate the percentage
            $sign = ($match['PrefixedSign'] ?? $match['PrefixedSign2'] ?? $match['PostfixedSign']) ?? '';
            $operand = (float) ($sign . ($match['PostfixedValue'] ?? $match['PrefixedValue'])) / 100;
 
            return true;
        }
 
        return false;
    }
 
    /**
     * Identify whether a string contains a currency value, and if so,
     * convert it to a numeric.
     *
     * @param string $operand string value to test
     */
    public static function convertToNumberIfCurrency(string &$operand): bool
    {
        $currencyRegexp = self::currencyMatcherRegexp();
        $thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/');
        $value = preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $operand);
 
        $match = [];
        if ($value !== null && preg_match($currencyRegexp, $value, $match, PREG_UNMATCHED_AS_NULL)) {
            //Determine the sign
            $sign = ($match['PrefixedSign'] ?? $match['PrefixedSign2'] ?? $match['PostfixedSign']) ?? '';
            $decimalSeparator = StringHelper::getDecimalSeparator();
            //Cast to a float
            $intermediate = (string) ($match['PostfixedValue'] ?? $match['PrefixedValue']);
            $intermediate = str_replace($decimalSeparator, '.', $intermediate);
            if (is_numeric($intermediate)) {
                $operand = (float) ($sign . str_replace($decimalSeparator, '.', $intermediate));
 
                return true;
            }
        }
 
        return false;
    }
 
    public static function currencyMatcherRegexp(): string
    {
        $currencyCodes = sprintf(self::CURRENCY_CONVERSION_LIST, preg_quote(StringHelper::getCurrencyCode(), '/'));
        $decimalSeparator = preg_quote(StringHelper::getDecimalSeparator(), '/');
 
        return '~^(?:(?: *(?<PrefixedSign>[-+])? *(?<PrefixedCurrency>[' . $currencyCodes . ']) *(?<PrefixedSign2>[-+])? *(?<PrefixedValue>[0-9]+[' . $decimalSeparator . ']?[0-9*]*(?:E[-+]?[0-9]*)?) *)|(?: *(?<PostfixedSign>[-+])? *(?<PostfixedValue>[0-9]+' . $decimalSeparator . '?[0-9]*(?:E[-+]?[0-9]*)?) *(?<PostfixedCurrency>[' . $currencyCodes . ']) *))$~ui';
    }
}