ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
NumberFormatter.php
Go to the documentation of this file.
1 <?php
2 
4 
7 
9 {
10  private const NUMBER_REGEX = '/(0+)(\\.?)(0*)/';
11 
12  private static function mergeComplexNumberFormatMasks($numbers, $masks): array
13  {
14  $decimalCount = strlen($numbers[1]);
15  $postDecimalMasks = [];
16 
17  do {
18  $tempMask = array_pop($masks);
19  if ($tempMask !== null) {
20  $postDecimalMasks[] = $tempMask;
21  $decimalCount -= strlen($tempMask);
22  }
23  } while ($tempMask !== null && $decimalCount > 0);
24 
25  return [
26  implode('.', $masks),
27  implode('.', array_reverse($postDecimalMasks)),
28  ];
29  }
30 
31  private static function processComplexNumberFormatMask($number, $mask): string
32  {
33  $result = $number;
34  $maskingBlockCount = preg_match_all('/0+/', $mask, $maskingBlocks, PREG_OFFSET_CAPTURE);
35 
36  if ($maskingBlockCount > 1) {
37  $maskingBlocks = array_reverse($maskingBlocks[0]);
38 
39  $offset = 0;
40  foreach ($maskingBlocks as $block) {
41  $size = strlen($block[0]);
42  $divisor = 10 ** $size;
43  $offset = $block[1];
44 
45  $blockValue = sprintf("%0{$size}d", fmod($number, $divisor));
46  $number = floor($number / $divisor);
47  $mask = substr_replace($mask, $blockValue, $offset, $size);
48  }
49  if ($number > 0) {
50  $mask = substr_replace($mask, $number, $offset, 0);
51  }
52  $result = $mask;
53  }
54 
55  return $result;
56  }
57 
58  private static function complexNumberFormatMask($number, $mask, $splitOnPoint = true): string
59  {
60  $sign = ($number < 0.0) ? '-' : '';
61  $number = abs($number);
62 
63  if ($splitOnPoint && strpos($mask, '.') !== false && strpos($number, '.') !== false) {
64  $numbers = explode('.', $number);
65  $masks = explode('.', $mask);
66  if (count($masks) > 2) {
67  $masks = self::mergeComplexNumberFormatMasks($numbers, $masks);
68  }
69  $integerPart = self::complexNumberFormatMask($numbers[0], $masks[0], false);
70  $decimalPart = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1]), false));
71 
72  return "{$sign}{$integerPart}.{$decimalPart}";
73  }
74 
75  $result = self::processComplexNumberFormatMask($number, $mask);
76 
77  return "{$sign}{$result}";
78  }
79 
80  private static function formatStraightNumericValue($value, $format, array $matches, $useThousands): string
81  {
82  $left = $matches[1];
83  $dec = $matches[2];
84  $right = $matches[3];
85 
86  // minimun width of formatted number (including dot)
87  $minWidth = strlen($left) + strlen($dec) + strlen($right);
88  if ($useThousands) {
89  $value = number_format(
90  $value,
91  strlen($right),
94  );
95 
96  return preg_replace(self::NUMBER_REGEX, $value, $format);
97  }
98 
99  if (preg_match('/[0#]E[+-]0/i', $format)) {
100  // Scientific format
101  return sprintf('%5.2E', $value);
102  } elseif (preg_match('/0([^\d\.]+)0/', $format) || substr_count($format, '.') > 1) {
103  if ($value == (int) $value && substr_count($format, '.') === 1) {
104  $value *= 10 ** strlen(explode('.', $format)[1]);
105  }
106 
107  return self::complexNumberFormatMask($value, $format);
108  }
109 
110  $sprintf_pattern = "%0$minWidth." . strlen($right) . 'f';
111  $value = sprintf($sprintf_pattern, $value);
112 
113  return preg_replace(self::NUMBER_REGEX, $value, $format);
114  }
115 
116  public static function format($value, $format): string
117  {
118  // The "_" in this string has already been stripped out,
119  // so this test is never true. Furthermore, testing
120  // on Excel shows this format uses Euro symbol, not "EUR".
121  //if ($format === NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE) {
122  // return 'EUR ' . sprintf('%1.2f', $value);
123  //}
124 
125  // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
126  $format = str_replace(['"', '*'], '', $format);
127 
128  // Find out if we need thousands separator
129  // This is indicated by a comma enclosed by a digit placeholder:
130  // #,# or 0,0
131  $useThousands = preg_match('/(#,#|0,0)/', $format);
132  if ($useThousands) {
133  $format = preg_replace('/0,0/', '00', $format);
134  $format = preg_replace('/#,#/', '##', $format);
135  }
136 
137  // Scale thousands, millions,...
138  // This is indicated by a number of commas after a digit placeholder:
139  // #, or 0.0,,
140  $scale = 1; // same as no scale
141  $matches = [];
142  if (preg_match('/(#|0)(,+)/', $format, $matches)) {
143  $scale = 1000 ** strlen($matches[2]);
144 
145  // strip the commas
146  $format = preg_replace('/0,+/', '0', $format);
147  $format = preg_replace('/#,+/', '#', $format);
148  }
149  if (preg_match('/#?.*\?\/\?/', $format, $m)) {
150  if ($value != (int) $value) {
151  $value = FractionFormatter::format($value, $format);
152  }
153  } else {
154  // Handle the number itself
155 
156  // scale number
157  $value = $value / $scale;
158  // Strip #
159  $format = preg_replace('/\\#/', '0', $format);
160  // Remove locale code [$-###]
161  $format = preg_replace('/\[\$\-.*\]/', '', $format);
162 
163  $n = '/\\[[^\\]]+\\]/';
164  $m = preg_replace($n, '', $format);
165  if (preg_match(self::NUMBER_REGEX, $m, $matches)) {
166  // There are placeholders for digits, so inject digits from the value into the mask
167  $value = self::formatStraightNumericValue($value, $format, $matches, $useThousands);
168  } elseif ($format !== NumberFormat::FORMAT_GENERAL) {
169  // Yes, I know that this is basically just a hack;
170  // if there's no placeholders for digits, just return the format mask "as is"
171  $value = str_replace('?', '', $format ?? '');
172  }
173  }
174 
175  if (preg_match('/\[\$(.*)\]/u', $format, $m)) {
176  // Currency or Accounting
177  $currencyCode = $m[1];
178  [$currencyCode] = explode('-', $currencyCode);
179  if ($currencyCode == '') {
180  $currencyCode = StringHelper::getCurrencyCode();
181  }
182  $value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value);
183  }
184 
185  return $value;
186  }
187 }
$size
Definition: RandomTest.php:84
$format
Definition: metadata.php:141
$result
static getCurrencyCode()
Get the currency code.
$mask
Definition: example_042.php:90
$n
Definition: RandomTest.php:85
static complexNumberFormatMask($number, $mask, $splitOnPoint=true)
static getThousandsSeparator()
Get the thousands separator.
static getDecimalSeparator()
Get the decimal separator.
static formatStraightNumericValue($value, $format, array $matches, $useThousands)