PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.2.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.2.0
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / NumberFormatter.php
matomo / app / core Last commit date
API 1 year ago Access 1 year ago Application 1 year ago Archive 1 year ago ArchiveProcessor 1 year ago Archiver 2 years ago AssetManager 1 year ago Auth 1 year ago Category 2 years ago Changes 1 year ago CliMulti 1 year ago Columns 1 year ago Concurrency 1 year ago Config 1 year ago Container 1 year ago CronArchive 1 year ago DataAccess 1 year ago DataFiles 2 years ago DataTable 1 year ago Db 1 year ago DeviceDetector 1 year ago Email 2 years ago Exception 1 year ago Http 1 year ago Intl 1 year ago Log 2 years ago Mail 1 year ago Measurable 1 year ago Menu 1 year ago Metrics 1 year ago Notification 1 year ago Period 1 year ago Plugin 1 year ago ProfessionalServices 1 year ago Report 1 year ago ReportRenderer 1 year ago Scheduler 1 year ago Segment 1 year ago Session 1 year ago Settings 1 year ago Tracker 1 year ago Translation 1 year ago Twig 1 year ago UpdateCheck 1 year ago Updater 1 year ago Updates 1 year ago Validators 1 year ago View 1 year ago ViewDataTable 1 year ago Visualization 1 year ago Widget 1 year ago .htaccess 2 years ago Access.php 1 year ago Archive.php 1 year ago ArchiveProcessor.php 1 year ago AssetManager.php 1 year ago Auth.php 2 years ago AuthResult.php 2 years ago BaseFactory.php 2 years ago Cache.php 2 years ago CacheId.php 1 year ago CliMulti.php 1 year ago Common.php 1 year ago Config.php 1 year ago Console.php 1 year ago Context.php 2 years ago Cookie.php 1 year ago CronArchive.php 1 year ago DI.php 1 year ago DataArray.php 1 year ago DataTable.php 1 year ago Date.php 1 year ago Db.php 1 year ago DbHelper.php 1 year ago Development.php 1 year ago ErrorHandler.php 1 year ago EventDispatcher.php 1 year ago ExceptionHandler.php 1 year ago FileIntegrity.php 1 year ago Filechecks.php 1 year ago Filesystem.php 1 year ago FrontController.php 1 year ago Http.php 1 year ago IP.php 1 year ago Log.php 2 years ago LogDeleter.php 1 year ago Mail.php 1 year ago Metrics.php 1 year ago NoAccessException.php 2 years ago Nonce.php 1 year ago Notification.php 1 year ago NumberFormatter.php 1 year ago Option.php 1 year ago Period.php 1 year ago Piwik.php 1 year ago Plugin.php 1 year ago Process.php 1 year ago Profiler.php 1 year ago ProxyHeaders.php 2 years ago ProxyHttp.php 1 year ago QuickForm2.php 1 year ago RankingQuery.php 1 year ago ReportRenderer.php 1 year ago Request.php 1 year ago Segment.php 1 year ago Sequence.php 2 years ago Session.php 1 year ago SettingsPiwik.php 1 year ago SettingsServer.php 1 year ago Singleton.php 2 years ago Site.php 1 year ago SiteContentDetector.php 1 year ago SupportedBrowser.php 2 years ago TCPDF.php 1 year ago Theme.php 1 year ago Timer.php 2 years ago Tracker.php 1 year ago Twig.php 1 year ago Unzip.php 1 year ago UpdateCheck.php 1 year ago Updater.php 1 year ago UpdaterErrorException.php 2 years ago Updates.php 1 year ago Url.php 1 year ago UrlHelper.php 1 year ago Version.php 1 year ago View.php 1 year ago bootstrap.php 1 year ago dispatch.php 2 years ago testMinimumPhpVersion.php 2 years ago
NumberFormatter.php
351 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik;
10
11 use Piwik\Container\StaticContainer;
12 use Piwik\Translation\Translator;
13 /**
14 * Class NumberFormatter
15 *
16 * Used to format numbers according to current language
17 */
18 class NumberFormatter
19 {
20 /** @var Translator */
21 protected $translator;
22 /** @var array cached patterns per language */
23 protected $patterns;
24 /** @var array cached symbols per language */
25 protected $symbols;
26 /**
27 * Loads all required data from Intl plugin
28 *
29 * TODO: instead of going directly through Translator, there should be a specific class
30 * that gets needed characters (ie, NumberFormatSource). The default implementation
31 * can use the Translator. This will make it easier to unit test NumberFormatter,
32 * w/o needing the Piwik Environment.
33 *
34 * @return NumberFormatter
35 */
36 public function __construct(Translator $translator)
37 {
38 $this->translator = $translator;
39 }
40 /**
41 * Parses the given pattern and returns patterns for positive and negative numbers
42 *
43 * @param string $pattern
44 * @return array
45 */
46 protected function parsePattern($pattern)
47 {
48 $patterns = explode(';', $pattern);
49 if (!isset($patterns[1])) {
50 // No explicit negative pattern was provided, construct it.
51 $patterns[1] = '-' . $patterns[0];
52 }
53 return $patterns;
54 }
55 /**
56 * Formats a given number or percent value (if $value starts or ends with a %)
57 *
58 * @param string|int|float $value
59 * @param int $maximumFractionDigits
60 * @param int $minimumFractionDigits
61 * @return mixed|string
62 */
63 public function format($value, $maximumFractionDigits = 0, $minimumFractionDigits = 0)
64 {
65 if (is_string($value) && trim($value, '%') != $value) {
66 return $this->formatPercent($value, $maximumFractionDigits, $minimumFractionDigits);
67 }
68 return $this->formatNumber($value, $maximumFractionDigits, $minimumFractionDigits);
69 }
70 /**
71 * Formats a given number
72 *
73 * @see \Piwik\NumberFormatter::format()
74 *
75 * @param string|int|float $value
76 * @param int $maximumFractionDigits
77 * @param int $minimumFractionDigits
78 * @return mixed|string
79 */
80 public function formatNumber($value, $maximumFractionDigits = 0, $minimumFractionDigits = 0)
81 {
82 $pattern = $this->getPattern($value, 'Intl_NumberFormatNumber');
83 return $this->formatNumberWithPattern($pattern, $value, $maximumFractionDigits, $minimumFractionDigits);
84 }
85 /**
86 * Formats a given number in compact format
87 *
88 * @see \Piwik\NumberFormatter::format()
89 *
90 * @param string|int|float $value
91 * @param int $maximumFractionDigits
92 * @param int $minimumFractionDigits
93 * @return mixed|string
94 */
95 public function formatNumberCompact($value)
96 {
97 [$compactPattern, $factor] = $this->determineCorrectCompactPattern('Intl_NumberFormatNumberCompact', $value);
98 // In case no special formatting should be used, we use the default number format
99 if (round($value) < 1000 || $compactPattern === '0') {
100 $maximumFractionDigits = $this->getMaxFractionDigitsForCompactFormat(round($value));
101 return $this->formatNumber($value, $maximumFractionDigits, 0);
102 }
103 return $this->formatCompact($compactPattern, $factor, $value);
104 }
105 /**
106 * Formats given number as percent value
107 * @param string|int|float $value
108 * @param int $maximumFractionDigits
109 * @param int $minimumFractionDigits
110 * @return mixed|string
111 */
112 public function formatPercent($value, $maximumFractionDigits = 0, $minimumFractionDigits = 0)
113 {
114 $newValue = trim($value, " \x00\v%");
115 if (!is_numeric($newValue)) {
116 return $value;
117 }
118 $pattern = $this->getPattern($value, 'Intl_NumberFormatPercent');
119 return $this->formatNumberWithPattern($pattern, $newValue, $maximumFractionDigits, $minimumFractionDigits);
120 }
121 /**
122 * Formats given number as percent value, but keep the leading + sign if found
123 *
124 * @param $value
125 * @return string
126 */
127 public function formatPercentEvolution($value)
128 {
129 $isPositiveEvolution = !empty($value) && ($value > 0 || substr($value, 0, 1) === '+');
130 $formatted = self::formatPercent($value);
131 if ($isPositiveEvolution) {
132 // $this->symbols has already been initialized from formatPercent().
133 $language = $this->translator->getCurrentLanguage();
134 return $this->symbols[$language]['+'] . $formatted;
135 }
136 return $formatted;
137 }
138 /**
139 * Formats given number as currency value
140 *
141 * @param string|int|float $value
142 * @param string $currency
143 * @param int $precision
144 * @return mixed|string
145 */
146 public function formatCurrency($value, $currency, $precision = 2)
147 {
148 $newValue = trim(strval($value), " \x00\v{$currency}");
149 if (!is_numeric($newValue)) {
150 return $value;
151 }
152 $pattern = $this->getPattern($value, 'Intl_NumberFormatCurrency');
153 if ($newValue == round($newValue)) {
154 // if no fraction digits available, don't show any
155 $value = $this->formatNumberWithPattern($pattern, $newValue, 0, 0);
156 } else {
157 // show given count of fraction digits otherwise
158 $value = $this->formatNumberWithPattern($pattern, $newValue, $precision, $precision);
159 }
160 return str_replace('¤', $currency, $value);
161 }
162 /**
163 * Formats a given number as currency value in compact format
164 *
165 * @see \Piwik\NumberFormatter::format()
166 *
167 * @param string|int|float $value
168 * @param int $maximumFractionDigits
169 * @param int $minimumFractionDigits
170 * @return mixed|string
171 */
172 public function formatCurrencyCompact($value, $currency)
173 {
174 [$compactPattern, $factor] = $this->determineCorrectCompactPattern('Intl_NumberFormatCurrencyCompact', $value);
175 // In case no special formatting should be used, we use the default number format
176 if (round($value) < 1000 || $compactPattern === '0') {
177 $maximumFractionDigits = $this->getMaxFractionDigitsForCompactFormat(round($value));
178 return $this->formatCurrency($value, $currency, 0);
179 }
180 return str_replace('¤', $currency, $this->formatCompact($compactPattern, $factor, $value));
181 }
182 private function getMaxFractionDigitsForCompactFormat(int $valueLength) : int
183 {
184 return $valueLength === 1 ? 1 : 0;
185 }
186 private function determineCorrectCompactPattern(string $patternPrefix, $value)
187 {
188 $finalFactor = 0;
189 $patternId = '';
190 if (round($value) < 1000) {
191 return ['0', 1];
192 }
193 for ($factor = 1000; $factor <= 1.0E+19; $factor *= 10) {
194 $patternOne = $patternPrefix . $factor . 'One';
195 $patternOther = $patternPrefix . $factor . 'Other';
196 if (round($value / $factor) === 1.0 && $this->translator->translate($patternOne) !== '') {
197 $finalFactor = $factor;
198 $patternId = $patternOne;
199 } elseif (round($value / $factor) >= 1 && $this->translator->translate($patternOther) !== '') {
200 $finalFactor = $factor;
201 $patternId = $patternOther;
202 }
203 if ($this->translator->translate($patternId) !== $patternId) {
204 $charCount = substr_count($this->translator->translate($patternId), '0');
205 if (round($value * pow(10, $charCount) / ($factor * 10)) < pow(10, $charCount)) {
206 break;
207 }
208 }
209 }
210 return [$this->translator->translate($patternId), $finalFactor];
211 }
212 private function formatCompact(string $pattern, int $factor, $value)
213 {
214 $charCount = substr_count($pattern, '0');
215 if ($charCount > 1) {
216 $factor /= pow(10, $charCount - 1);
217 }
218 $maximumFractionDigits = $this->getMaxFractionDigitsForCompactFormat($charCount);
219 // cut off numbers after a certain decimal, as formatNumber would round otherwise
220 $digitCountFactor = pow(10, $maximumFractionDigits);
221 $value = round($value / $factor * $digitCountFactor) / $digitCountFactor;
222 $formattedNumber = $this->formatNumber($value, $maximumFractionDigits, 0);
223 return preg_replace(['/(0+)/', '/(\'\\.\')/'], [$formattedNumber, '.'], $pattern);
224 }
225 /**
226 * Returns the relevant pattern for the given number.
227 *
228 * @param string $value
229 * @param string $translationId
230 * @return string
231 */
232 protected function getPattern($value, $translationId)
233 {
234 $language = $this->translator->getCurrentLanguage();
235 if (!isset($this->patterns[$language][$translationId])) {
236 $this->patterns[$language][$translationId] = $this->parsePattern($this->translator->translate($translationId));
237 }
238 list($positivePattern, $negativePattern) = $this->patterns[$language][$translationId];
239 $negative = $this->isNegative($value);
240 return $negative ? $negativePattern : $positivePattern;
241 }
242 /**
243 * Formats the given number with the given pattern
244 *
245 * @param string $pattern
246 * @param string|int|float $value
247 * @param int $maximumFractionDigits
248 * @param int $minimumFractionDigits
249 * @return mixed|string
250 */
251 protected function formatNumberWithPattern($pattern, $value, $maximumFractionDigits = 0, $minimumFractionDigits = 0)
252 {
253 if (!is_numeric($value)) {
254 return $value;
255 }
256 $usesGrouping = strpos($pattern, ',') !== \false;
257 // if pattern has number groups, parse them.
258 if ($usesGrouping) {
259 preg_match('/#+0/', $pattern, $primaryGroupMatches);
260 $primaryGroupSize = $secondaryGroupSize = strlen($primaryGroupMatches[0]);
261 $numberGroups = explode(',', $pattern);
262 // check for distinct secondary group size.
263 if (count($numberGroups) > 2) {
264 $secondaryGroupSize = strlen($numberGroups[1]);
265 }
266 }
267 // Ensure that the value is positive and has the right number of digits.
268 $negative = $this->isNegative($value);
269 $signMultiplier = $negative ? '-1' : '1';
270 $value = $value / $signMultiplier;
271 $value = round($value, $maximumFractionDigits);
272 // Split the number into major and minor digits.
273 $valueParts = explode('.', $value);
274 $majorDigits = $valueParts[0];
275 // Account for maximumFractionDigits = 0, where the number won't
276 // have a decimal point, and $valueParts[1] won't be set.
277 $minorDigits = isset($valueParts[1]) ? $valueParts[1] : '';
278 if ($usesGrouping) {
279 // Reverse the major digits, since they are grouped from the right.
280 $majorDigits = array_reverse(str_split($majorDigits));
281 // Group the major digits.
282 $groups = array();
283 $groups[] = array_splice($majorDigits, 0, $primaryGroupSize);
284 while (!empty($majorDigits)) {
285 $groups[] = array_splice($majorDigits, 0, $secondaryGroupSize);
286 }
287 // Reverse the groups and the digits inside of them.
288 $groups = array_reverse($groups);
289 foreach ($groups as &$group) {
290 $group = implode(array_reverse($group));
291 }
292 // Reconstruct the major digits.
293 $majorDigits = implode(',', $groups);
294 }
295 if ($minimumFractionDigits <= $maximumFractionDigits) {
296 // Strip any trailing zeroes.
297 $minorDigits = rtrim($minorDigits, '0');
298 if (strlen($minorDigits) < $minimumFractionDigits) {
299 // Now there are too few digits, re-add trailing zeroes
300 // until the desired length is reached.
301 $neededZeroes = $minimumFractionDigits - strlen($minorDigits);
302 $minorDigits .= str_repeat('0', $neededZeroes);
303 }
304 }
305 // Assemble the final number and insert it into the pattern.
306 $value = strlen($minorDigits) ? $majorDigits . '.' . $minorDigits : $majorDigits;
307 $value = preg_replace('/#(?:[\\.,]#+)*0(?:[,\\.][0#]+)*/', $value, $pattern);
308 // Localize the number.
309 $value = $this->replaceSymbols($value);
310 return $value;
311 }
312 /**
313 * Replaces number symbols with their localized equivalents.
314 *
315 * @param string $value The value being formatted.
316 *
317 * @return string
318 *
319 * @see http://cldr.unicode.org/translation/number-symbols
320 */
321 protected function replaceSymbols($value)
322 {
323 $language = $this->translator->getCurrentLanguage();
324 if (!isset($this->symbols[$language])) {
325 $this->symbols[$language] = array('.' => $this->translator->translate('Intl_NumberSymbolDecimal'), ',' => $this->translator->translate('Intl_NumberSymbolGroup'), '+' => $this->translator->translate('Intl_NumberSymbolPlus'), '-' => $this->translator->translate('Intl_NumberSymbolMinus'), '%' => $this->translator->translate('Intl_NumberSymbolPercent'));
326 }
327 return strtr($value, $this->symbols[$language]);
328 }
329 /**
330 * @param $value
331 * @return bool
332 */
333 protected function isNegative($value)
334 {
335 return $value < 0;
336 }
337 /**
338 * @deprecated
339 * @return self
340 */
341 public static function getInstance()
342 {
343 return StaticContainer::get(\Piwik\NumberFormatter::class);
344 }
345 public function clearCache()
346 {
347 $this->patterns = [];
348 $this->symbols = [];
349 }
350 }
351