DecimalFormatter::prepareNumber()   F
last analyzed

Complexity

Conditions 34
Paths > 20000

Size

Total Lines 167
Code Lines 110

Duplication

Lines 30
Ratio 17.96 %

Importance

Changes 0
Metric Value
cc 34
eloc 110
nc 39680
nop 2
dl 30
loc 167
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
// +---------------------------------------------------------------------------+
4
// | This file is part of the Agavi package.                                   |
5
// | Copyright (c) 2005-2011 the Agavi Project.                                |
6
// |                                                                           |
7
// | For the full copyright and license information, please view the LICENSE   |
8
// | file that was distributed with this source code. You can also view the    |
9
// | LICENSE file online at http://www.agavi.org/LICENSE.txt                   |
10
// |   vi: set noexpandtab:                                                    |
11
// |   Local Variables:                                                        |
12
// |   indent-tabs-mode: t                                                     |
13
// |   End:                                                                    |
14
// +---------------------------------------------------------------------------+
15
16
namespace Agavi\Util;
17
18
use Agavi\Translation\Locale;
19
20
/**
21
 * The decimal formatter will format numbers according to a given format.
22
 *
23
 * The format is close to the one used by
24
 * {@link http://icu.sourceforge.net/apiref/icu4c/classDecimalFormat.html ICU}.
25
 * It consists of the following elements
26
 *
27
 * @package    agavi
28
 * @subpackage util
29
 *
30
 * @author     Dominik del Bondio <[email protected]>
31
 * @copyright  Authors
32
 * @copyright  The Agavi Project
33
 *
34
 * @since      0.11.0
35
 *
36
 * @version    $Id$
37
 */
38
class DecimalFormatter
39
{
40
    /**
41
     * @var        string The format string given by the user
42
     */
43
    protected $originalFormatString = null;
44
45
    /**
46
     * @var        string The format string which will be given to sprintf
47
     */
48
    protected $formatString = '';
49
50
    /**
51
     * @var        string The format string which will be given to sprintf if the
52
     *                    number is negative
53
     */
54
    protected $negativeFormatString = null;
55
56
    /**
57
     * @var        int The minimum number of integrals displayed (will be padded
58
     *                 with 0 on the left)
59
     */
60
    protected $minShowedIntegrals = 0;
61
62
    /**
63
     * @var        int The minimum number of fractionals displayed (will be
64
     *                 padded with 0 on the right)
65
     */
66
    protected $minShowedFractionals = 0;
67
68
    /**
69
     * @var        int The maximum number of fractionals displayed
70
     *                 (-1 means all get displayed)
71
     */
72
    protected $maxShowedFractionals = 0;
73
74
    /**
75
     * @var        bool Whether the format string has the location of the minus
76
     *                  defined
77
     */
78
    protected $hasMinus = false;
79
80
    /**
81
     * @var        bool Whether the format string has the location of the
82
     *                  currency sign defined
83
     */
84
    protected $hasCurrency = false;
85
86
    /**
87
     * @var        int The type of the currency symbol.
88
     */
89
    protected $currencyType = null;
90
91
    /**
92
     * @var        array An array containing the distances for the grouping
93
     *                   operators which will be applied to the number
94
     */
95
    protected $groupingDistances = array();
96
97
    /**
98
     * @var        string The grouping(thousands) separator
99
     */
100
    protected $groupingSeparator = ',';
101
102
    /**
103
     * @var        string The decimal separator
104
     */
105
    protected $decimalSeparator = '.';
106
107
    /**
108
     * @var        int The rounding mode
109
     */
110
    protected $roundingMode = DecimalFormatter::ROUND_SCIENTIFIC;
111
112
    const CURRENCY_SYMBOL = 1;
113
    const CURRENCY_CODE = 2;
114
    const CURRENCY_NAME = 3;
115
116
    const ROUND_NONE = 0;
117
    const ROUND_SCIENTIFIC = 1;
118
    const ROUND_FINANCIAL = 2;
119
    const ROUND_FLOOR = 3;
120
    const ROUND_CEIL = 4;
121
122
    const IN_PREFIX = 1;
123
    const IN_NUMBER = 2;
124
    const IN_POSTFIX = 3;
125
126
    /**
127
     * Constructs a new Decimalformatter with the optional format.
128
     *
129
     * @param      string $format The format (if any).
130
     *
131
     * @author     Dominik del Bondio <[email protected]>
132
     * @since      0.11.0
133
     */
134
    public function __construct($format = null)
135
    {
136
        if ($format !== null) {
137
            $this->setFormat($format);
138
        }
139
    }
140
141
    /**
142
     * Returns the format which is currently used to format numbers.
143
     *
144
     * @return     string The current format.
145
     *
146
     * @author     Dominik del Bondio <[email protected]>
147
     * @since      0.11.0
148
     */
149
    public function getFormat()
150
    {
151
        return $this->originalFormatString;
152
    }
153
154
    /**
155
     * Sets the format to be used for formatting numbers.
156
     *
157
     * @return     string The current format.
158
     *
159
     * @author     Dominik del Bondio <[email protected]>
160
     * @since      0.11.0
161
     */
162
    public function setFormat($format)
163
    {
164
        if ($this->originalFormatString == $format) {
165
            // the given and the currently set format string are equal so we have nothing to do
166
            return;
167
        }
168
169
        $this->originalFormatString = $format;
170
171
        if (($pos = strpos($format, ';')) !== false) {
172
            $fullFormat = $format;
173
            $format = substr($fullFormat, 0, $pos);
174
            $negativeFormat = substr($fullFormat, $pos + 1);
175
        } else {
176
            $fullFormat = $format;
0 ignored issues
show
Unused Code introduced by
$fullFormat is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
177
            $negativeFormat = '-#';
178
        }
179
180
        $numberChars = array('0', '#', '.', ',');
181
182
        $formatStr = '';
183
184
        // an array containing the distances between the grouping operators (and up to the decimals) from left to right
185
        $groupingDistances = array();
186
        $currentGroupingDistance = 0;
187
188
        $hasMinus = false;
0 ignored issues
show
Unused Code introduced by
$hasMinus is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
189
        $hasCurrency = false;
190
        $currencyType = 0;
191
        $minShowedIntegrals = 0;
192
        $minShowedFractionals = 0;
193
        $maxShowedFractionals = 0;
194
        $skippedFractionals = 0;
195
        $numberState = 'inInteger';
196
197
        $inQuote = false;
198
        $quoteStr = '';
199
        $state = self::IN_PREFIX;
200
        $len = strlen($format);
201
202
        for ($i = 0; $i < $len; ++$i) {
0 ignored issues
show
Comprehensibility Bug introduced by
Loop incrementor ($i) jumbling with inner loop
Loading history...
203
            $c = $format[$i];
204
            $cNext = (($i + 1) < $len) ? $format[$i + 1] : 0;
205
206
            switch ($state) {
207
                case self::IN_POSTFIX:
208
                case self::IN_PREFIX: {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
209
                    if ($state == self::IN_PREFIX && in_array($c, $numberChars)) {
210
                        --$i;
211
                        $state = self::IN_NUMBER;
212
                    } elseif ($inQuote) {
213
                        // quote closed
214
                        if ($c == '\'') {
215
                            // when the quoted string was empty we need to output a '
216
                            if (strlen($quoteStr) == 0) {
217
                                $quoteStr = '\'';
218
                            }
219
                            $formatStr .= $quoteStr;
220
                            $inQuote = false;
221
                        } else {
222
                            // quote % for sprintf usage
223
                            if ($c == '%') {
224
                                $c = '%' . $c;
225
                            }
226
                            $quoteStr .= $c;
227
                        }
228
                    } else {
229
                        if ($c == '\'') {
230
                            $quoteStr = '';
231
                            $inQuote = true;
232
//					} elseif($c == '-') {
0 ignored issues
show
Unused Code Comprehensibility introduced by
44% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
233
//						$hasMinus = true;
234
//						$formatStr .= '%2$s';
235
                        } elseif (/*$c == '¤'*/ !$hasCurrency && ord($c) == 194 && ord($cNext) == 164) {
236
                            ++$i;
237
                            $hasCurrency = true;
238
                            $currencyType = self::CURRENCY_SYMBOL;
239
                            $formatStr .= '%3$s';
240
241
                            for (; $i + 2 < $len && ord($format[$i + 1]) == 194 && ord($format[$i + 2]) == 164 && $currencyType < self::CURRENCY_NAME; $i += 2) {
242
                                ++$currencyType;
243
                            }
244
                        } else {
245
                            // quote % for sprintf usage
246
                            if ($c == '%') {
247
                                $c = '%' . $c;
248
                            }
249
                            $formatStr .= $c;
250
                        }
251
                    }
252
                    break;
253
                }
254
                case self::IN_NUMBER: {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
255
                    if (!in_array($c, $numberChars)) {
256
                        if ($numberState == 'inInteger') {
257
                            $groupingDistances[] = $currentGroupingDistance;
258
                        }
259
                        $formatStr .= '%1$s';
260
                        --$i;
261
                        $state = self::IN_POSTFIX;
262
                    } else {
263
                        if ($numberState == 'inInteger') {
264
                            if ($c == ',') {
265
                                $groupingDistances[] = $currentGroupingDistance;
266
                                $currentGroupingDistance = 0;
267
                            } elseif ($c == '.') {
268
                                $groupingDistances[] = $currentGroupingDistance;
269
                                // if we have a dot we default to show the entire fractional part
270
                                $maxShowedFractionals = -1;
271
                                $numberState = 'inFraction';
272
                            } else {
273
                                // when the user has a pattern like 0##0 the 2 ## are mandatory too
274
                                // (basically everything after the first 0 is mandatory, so take care here)
275
                                if ($minShowedIntegrals > 0) {
276
                                    ++$minShowedIntegrals;
277
                                } elseif ($c == '0') {
278
                                    ++$minShowedIntegrals;
279
                                }
280
                                ++$currentGroupingDistance;
281
                            }
282
                        } elseif ($numberState == 'inFraction') {
283
                            if ($c == ',' || $c == '.') {
284
                                throw new Exception($c. ' is not allowed in the fraction part of the number');
285
                            } else {
286
                                if ($c == '#') {
287
                                    ++$skippedFractionals;
288
                                } elseif ($c == '0') {
289
                                    ++$minShowedFractionals;
290
                                    $minShowedFractionals += $skippedFractionals;
291
                                    $maxShowedFractionals = $minShowedFractionals;
292
                                    $skippedFractionals = 0;
293
                                }
294
                            }
295
                        }
296
                    }
297
298
                    break;
299
                }
300
            }
301
        }
302
303
        if ($state == self::IN_NUMBER) {
304
            if ($numberState == 'inInteger') {
305
                $groupingDistances[] = $currentGroupingDistance;
306
            }
307
            $formatStr .= '%1$s';
308
        }
309
310
        // when the user had 0.00# as format (the fractional part ended with an #)
311
        // the max numbers of the fractional part is unlimited
312
        if ($skippedFractionals) {
313
            $maxShowedFractionals = -1;
314
        }
315
316
        // we chop of the first element of the grouping distance which is
317
        // either the the number of chars until the first ',' or the only element
318
        // in case there was no grouping separator specified (which means that
319
        // there won't be grouping at all)
320
        array_shift($groupingDistances);
321
322
        // now we reverse the array so we can process it in natural order later
323
        $groupingDistances = array_reverse($groupingDistances);
324
325
        if (($pos = strpos($negativeFormat, '-')) !== false) {
326
            str_replace('-', '%2$s', $negativeFormat);
327
        }
328
        $hasMinus = true;
329
        $negativeFormat = preg_replace('/[' . preg_quote(implode('', $numberChars), '/') . ']+/', $formatStr, $negativeFormat);
330
        // replace the currency specifier from the old string if it was specified extra in the negative one
331
        if (($pos = strpos($negativeFormat, /*'¤'*/ chr(194) . chr(164))) !== false) {
332
            $negativeFormat = str_replace('%3$s', '', $negativeFormat);
333
            $negativeFormat = str_replace(chr(194) . chr(164), '%3$s', $negativeFormat);
334
        }
335
336
        // store all info
337
338
        $this->formatString = $formatStr;
339
        $this->negativeFormatString = $negativeFormat;
340
341
        $this->minShowedIntegrals = $minShowedIntegrals;
342
        $this->minShowedFractionals = $minShowedFractionals;
343
        $this->maxShowedFractionals = $maxShowedFractionals;
344
345
        $this->hasMinus = $hasMinus;
346
        $this->hasCurrency = $hasCurrency;
347
        $this->currencyType = $currencyType;
348
349
        $this->groupingDistances = $groupingDistances;
350
    }
351
352
    /**
353
     * Formats the given number with the information in this instance.
354
     *
355
     * @param      int|float A number to format.
356
     * @param      string    A currency symbol to be used.
357
     *
358
     * @return     array The number and some information in the desired format.
359
     *
360
     * @author     Dominik del Bondio <[email protected]>
361
     * @since      0.11.0
362
     */
363
    protected function prepareNumber($number, $currencySymbol)
364
    {
365
        $isNegative = false;
366
        $integralPart = '';
367
        $fractionalPart = '';
368
        if (is_float($number)) {
369
            // since we would overflow when converting to int and calculating the
370
            // parts ourselves we simply convert it to a string and let that method
371
            // handle it
372
            $number = (string) $number;
373
        }
374
        if (is_int($number)) {
375
            if (abs($number) != $number) {
376
                $isNegative = true;
377
                $number = abs($number);
378
            }
379
            $integralPart = (string) $number;
380
        } else {
381
            $number = trim($number);
382
            $len = strlen($number);
383
            // empty string will result in 0
384
            if ($len == 0) {
385
                $integralPart = '0';
386
            } else {
387
                $i = 0;
388
                if ($number[0] == '-') {
389
                    $isNegative = true;
390
                    ++$i;
391
                }
392
                $inIntegral = true;
393
                while ($i < $len) {
394
                    $c = $number[$i];
395
                    if ($inIntegral) {
396
                        if ($c == '.') {
397
                            $inIntegral = false;
398
                        } else {
399
                            $integralPart .= $c;
400
                        }
401
                    } else {
402
                        $fractionalPart .= $c;
403
                    }
404
                    ++$i;
405
                }
406
            }
407
        }
408
409
        $integralLen = strlen($integralPart);
410
        $fractionalLen = strlen($fractionalPart);
411
412
        if ($integralLen < $this->minShowedIntegrals) {
413
            $integralPart = str_repeat('0', $this->minShowedIntegrals - $integralLen) . $integralPart;
414
        }
415
416
        if ($fractionalLen < $this->minShowedFractionals) {
417
            $fractionalPart .= str_repeat('0', $this->minShowedFractionals - $fractionalLen);
418
        }
419
420
        if ($this->maxShowedFractionals >= 0 && strlen($fractionalPart) > $this->maxShowedFractionals) {
421
            $nextDigit = (int) $fractionalPart[$this->maxShowedFractionals];
422
            $fractionalPart = substr($fractionalPart, 0, $this->maxShowedFractionals);
423
            if ($this->roundingMode != self::ROUND_NONE) {
424
                $inIntegral = $this->maxShowedFractionals == 0;
425
426
                $roundUp = false;
427
                switch ($this->roundingMode) {
428
                    case self::ROUND_SCIENTIFIC:
429
                        $roundUp = $nextDigit > 4;
430
                        break;
431
                    case self::ROUND_FINANCIAL:
432
                        $roundUp = $nextDigit > 5;
433
                        break;
434
                    /* we don't need to do anything on floor
435
					case self::ROUND_FLOOR:
436
						break;
437
					*/
438
                    case self::ROUND_CEIL:
439
                        $roundUp = true;
440
                        break;
441
                }
442
443
                if ($roundUp) {
444
                    $integralLen = strlen($integralPart);
445
                    if ($inIntegral) {
446
                        $pos = $integralLen - 1;
447
                    } else {
448
                        $pos = strlen($fractionalPart) - 1;
449
                    }
450
                    do {
451
                        $roundUp = false;
452
                        if ($inIntegral) {
453 View Code Duplication
                            if ($pos < 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
454
                                // when we reached the left side of the integral part we insert
455
                                // 1 there and stop
456
                                $integralPart = '1'. $integralPart;
457
                            } else {
458
                                $digit = (int) $integralPart[$pos];
459
                                if ($digit == 9) {
460
                                    $roundUp = true;
461
                                    $digit = 0;
462
                                } else {
463
                                    ++$digit;
464
                                }
465
                                $integralPart[$pos] = $digit;
466
                                --$pos;
467
                            }
468 View Code Duplication
                        } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
469
                            $digit = (int) $fractionalPart[$pos];
470
                            if ($digit == 9) {
471
                                $roundUp = true;
472
                                $digit = 0;
473
                            } else {
474
                                ++$digit;
475
                            }
476
                            $fractionalPart[$pos] = $digit;
477
                            --$pos;
478
                            if ($pos < 0) {
479
                                $inIntegral = true;
480
                                $pos = $integralLen - 1;
481
                            }
482
                        }
483
                    } while ($roundUp);
484
                }
485
            }
486
        }
487
488
        $gd = $this->groupingDistances;
489
490
        if (($gdCount = count($gd)) > 0) {
491
            $newIntegralPart = '';
492
493
            $gdPos = 0;
494
            $stepsSinceLastGroup = 0;
495
            for ($i = strlen($integralPart) - 1; $i >= 0; --$i) {
496
                if ($stepsSinceLastGroup == $gd[$gdPos]) {
497
                    // we need to reverse the groupingSeparator here because else utf-8
498
                    // encoded chars would end up in reverse order in the output string
499
                    $newIntegralPart .= strrev($this->groupingSeparator);
500
                    $stepsSinceLastGroup = 0;
501
                    ++$gdPos;
502
                }
503
504
                $newIntegralPart .= $integralPart[$i];
505
                ++$stepsSinceLastGroup;
506
507
                // respect icu docs in regards to the interval (2 delimiter specifications and loop the 2nd)
508
                if ($gdPos > 1) {
509
                    $gdPos = 1;
510
                }
511
                if ($gdPos >= $gdCount) {
512
                    $gdPos = 0;
513
                }
514
            }
515
516
            $integralPart = strrev($newIntegralPart);
517
        }
518
519
        $number = $integralPart;
520
        if (strlen($fractionalPart) > 0) {
521
            $number .= $this->decimalSeparator . $fractionalPart;
522
        }
523
524
        if ($isNegative && !$this->hasMinus) {
525
            $number = '-' . $number;
526
        }
527
528
        return array($number, $isNegative ? '-' : '', $currencySymbol);
529
    }
530
531
    /**
532
     * Formats the given number and returns the formatted result.
533
     *
534
     * @param      int|float The number to be formatted.
535
     *
536
     * @return     string    The number formatted in the desired format.
537
     *
538
     * @author     Dominik del Bondio <[email protected]>
539
     * @since      0.11.0
540
     */
541
    public function formatNumber($number)
542
    {
543
        return vsprintf(($number < 0) ? $this->negativeFormatString : $this->formatString, $this->prepareNumber($number, ''));
544
    }
545
546
    /**
547
     * Formats the given currency and returns the formatted result.
548
     *
549
     * @param      int|float The number to be formatted.
550
     * @param      string    The currency symbol to be used when formatting.
551
     *
552
     * @return     string    The currency formatted in the desired format.
553
     *
554
     * @author     Dominik del Bondio <[email protected]>
555
     * @since      0.11.0
556
     */
557
    public function formatCurrency($number, $currencySymbol)
558
    {
559
        return vsprintf(($number < 0) ? $this->negativeFormatString : $this->formatString, $this->prepareNumber($number, $currencySymbol));
560
    }
561
562
    /**
563
     * Returns the rounding mode.
564
     *
565
     * @return     int The rounding mode.
566
     *
567
     * @author     Dominik del Bondio <[email protected]>
568
     * @since      0.11.0
569
     */
570
    public function getRoundingMode()
571
    {
572
        return $this->roundingMode;
573
    }
574
575
    /**
576
     * Sets the rounding mode.
577
     *
578
     * @return     string The rounding mode.
579
     *
580
     * @author     Dominik del Bondio <[email protected]>
581
     * @since      0.11.0
582
     */
583
    public function setRoundingMode($mode)
584
    {
585
        $this->roundingMode = $mode;
586
    }
587
588
    /**
589
     * Maps a string rounding mode definition to the rounding mode constants.
590
     *
591
     * @param      string    The mode string.
592
     *
593
     * @return     string    The rounding mode constant.
594
     *
595
     * @author     Dominik del Bondio <[email protected]>
596
     * @since      0.11.0
597
     */
598
    public function getRoundingModeFromString($mode)
599
    {
600
        static $map = array(
601
            'none' => self::ROUND_NONE,
602
            'scientific' => self::ROUND_SCIENTIFIC,
603
            'financial' => self::ROUND_FINANCIAL,
604
            'floor' => self::ROUND_FLOOR,
605
            'ceil' => self::ROUND_CEIL,
606
        );
607
608
        if (!isset($map[$mode])) {
609
            throw new \InvalidArgumentException('Unknown rounding mode "' . $mode . '"');
610
        }
611
612
        return $map[$mode];
613
    }
614
    
615
    protected static function getDecimalParseRegex(Locale $locale = null)
616
    {
617
        static $patternCache = array();
618
        
619
        if ($locale) {
620
            $localeId = $locale->getIdentifier();
621
        } else {
622
            $localeId = '';
623
        }
624
        
625
        if (isset($patternCache[$localeId])) {
626
            return $patternCache[$localeId];
627
        }
628
        
629
        if ($locale) {
630
            $decimalFormats = $locale->getDecimalFormats();
631
            $groupingSeparator = $locale->getNumberSymbolGroup();
632
            $decimalSeparator = $locale->getNumberSymbolDecimal();
633
            $minusSign = $locale->getNumberSymbolMinusSign();
634
        } else {
635
            $decimalFormats = array('#,##0.###');
636
            $groupingSeparator = ',';
637
            $decimalSeparator = '.';
638
            $minusSign = '-';
639
        }
640
        
641
        $patterns = array();
642
        
643
        foreach ($decimalFormats as $decimalFormatList) {
644
            $decimalFormatList = explode(';', $decimalFormatList, 2);
645
            if (count($decimalFormatList) == 1) {
646
                // no pattern for negative numbers
647
                // we need a copy of the format with a minus prefix
648
                $decimalFormatList[1] = '-' . $decimalFormatList[0];
649
            }
650
            foreach (array(true, false) as $withFraction) :
651
                foreach ($decimalFormatList as $decimalFormat) {
652
                    // we need to make three parts: number, decimal part and minus sign
653
                    $decimalFormatChunks = preg_split('/([\.\-])/', $decimalFormat, 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
654
                    // there is a minus sign at the beginning or end! find it!
655
                    $pastDecimalSeparator = false;
656
                    foreach ($decimalFormatChunks as &$decimalFormatChunk) {
657
                        if ($decimalFormatChunk == '-') {
658
                            // always allow "-" in addition to the minus sign supplied by the locale. we do this because some locales (e.g. da, fa, se, sv) have U+2212 (the "real" minus sign) defined in the locale data, but no human can really type that character on their keyboard. consider it lenient parsing ;) see ticket #1293
659
                            $decimalFormatChunk = '(?P<minus>' . preg_quote($minusSign, '#') . '|-)';
660
                        } elseif ($decimalFormatChunk == '.') {
661
                            $pastDecimalSeparator = true;
662
                            if ($withFraction) {
663
                                $decimalFormatChunk = preg_quote($decimalSeparator, '#');
664
                            } else {
665
                                $decimalFormatChunk = '';
666
                            }
667
                        } else {
668
                            $decimalFormatChunk = preg_replace('/[#0,]+/u', '[\d' . preg_quote($groupingSeparator, '#') . ']*', $decimalFormatChunk);
669
                            if (!$pastDecimalSeparator) {
670
                                if ($withFraction) {
671
                                    $decimalFormatChunk = '(?P<num>(?=[\d,]*\d)' . $decimalFormatChunk . '(\d|' . $decimalFormatChunk . '(?=\.[\d,]*\d)))?';
672
                                } else {
673
                                    $decimalFormatChunk = '(?P<num>(?=[\d,]*\d)' . $decimalFormatChunk . '\d)';
674
                                }
675
                            } else {
676
                                if ($withFraction) {
677
                                    $decimalFormatChunk = '(?P<dec>' . $decimalFormatChunk . '\d)?';
678
                                } else {
679
                                    $decimalFormatChunk = '';
680
                                }
681
                            }
682
                        }
683
                    }
684
                
685
                    $patterns[] = implode('', $decimalFormatChunks);
686
                }
687
            endforeach;
688
        }
689
        
690
        return $patternCache[$localeId] = '#(?J)^(' . implode('|', $patterns) . ')#u';
691
    }
692
693
    /**
694
     * Parses a string into float or int.
695
     *
696
     * @param      string The input number string.
697
     * @param      Locale An optional locale to get the separators from.
698
     * @param      bool An out value indicating whether there were additional
699
     *                  characters after the matched number.
700
     *
701
     * @return     mixed The result if parsing was successful or false when the
702
     *                   input was no number.
703
     *
704
     * @author     Dominik del Bondio <[email protected]>
705
     * @since      0.11.0
706
     */
707
    public static function parse($string, $locale = null, &$hasExtraChars = false)
708
    {
709
        $string = trim($string);
710
711
        $pattern = self::getDecimalParseRegex($locale);
712
713
        if ($locale) {
714
            $groupingSeparator = $locale->getNumberSymbolGroup();
715
        } else {
716
            $groupingSeparator = ',';
717
        }
718
        
719
        if (preg_match($pattern, $string, $matches)) {
720
            $num = '';
721 View Code Duplication
            if (isset($matches['num']) && $matches['num'] !== '') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
722
                $num = str_replace($groupingSeparator, '', $matches['num']);
723
            }
724
            $dec = '';
725 View Code Duplication
            if (isset($matches['dec']) && $matches['dec'] !== '') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
726
                $dec = str_replace($groupingSeparator, '', $matches['dec']);
727
            }
728
            
729
            if (strlen($matches[0]) < strlen($string)) {
730
                $hasExtraChars = true;
731
            }
732
733
            if ($num === '' && $dec === '') {
734
                if (strlen($string) > 0) {
735
                    $hasExtraChars = true;
736
                }
737
                return false;
738
            }
739
            
740
            if ($num === '') {
741
                $num = 0;
742
            }
743
            // don't cast to int... this here will cast the string to a float if it's too big
744
            $num += 0;
745
            
746
            if ($dec !== '') {
747
                $num += (float) ('0.' . $dec);
748
            }
749
750
            if (!empty($matches['minus'])) {
751
                $num = $num * -1;
752
            }
753
            
754
            return $num;
755
        }
756
        
757
        if (strlen($string) > 0) {
758
            $hasExtraChars = true;
759
        }
760
    
761
        return false;
762
    }
763
}
764