PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.8.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.8.2
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 3 months ago Access 3 months ago Application 3 months ago Archive 3 months ago ArchiveProcessor 3 months ago Archiver 2 years ago AssetManager 3 months ago Auth 6 months ago Category 6 months ago Changes 3 months ago CliMulti 1 year ago Columns 3 months ago Concurrency 3 months ago Config 3 months ago Container 3 months ago CronArchive 3 months ago DataAccess 3 months ago DataFiles 2 years ago DataTable 3 months ago Db 3 months ago DeviceDetector 1 year ago Email 2 years ago Exception 4 months ago Http 4 months ago Intl 3 months ago Log 2 years ago Mail 1 year ago Measurable 6 months ago Menu 3 months ago Metrics 3 months ago Notification 6 months ago Period 3 months ago Plugin 3 months ago Policy 3 months ago ProfessionalServices 1 year ago Report 1 year ago ReportRenderer 3 months ago Request 3 months ago Scheduler 3 months ago Segment 3 months ago Session 3 months ago Settings 3 months ago Tracker 3 months ago Translation 3 months ago Twig 1 year ago UpdateCheck 3 months ago Updater 4 months ago Updates 3 months ago Validators 1 year ago View 6 months ago ViewDataTable 3 months ago Visualization 1 year ago Widget 3 months ago .htaccess 2 years ago Access.php 3 months ago Archive.php 3 months ago ArchiveProcessor.php 4 months ago AssetManager.php 3 months ago Auth.php 6 months ago AuthResult.php 6 months ago BaseFactory.php 2 years ago Cache.php 2 years ago CacheId.php 4 months ago CliMulti.php 3 months ago Common.php 3 months ago Config.php 3 months ago Console.php 3 months ago Context.php 2 years ago Cookie.php 1 year ago CronArchive.php 3 months ago DI.php 3 months ago DataArray.php 5 months ago DataTable.php 3 months ago Date.php 3 months ago Db.php 3 months ago DbHelper.php 3 months ago Development.php 1 year ago ErrorHandler.php 6 months ago EventDispatcher.php 1 year ago ExceptionHandler.php 4 months ago FileIntegrity.php 3 months ago Filechecks.php 1 year ago Filesystem.php 3 months ago FrontController.php 4 months ago Http.php 4 months ago IP.php 1 year ago Log.php 3 months ago LogDeleter.php 1 year ago Mail.php 1 year ago Metrics.php 3 months ago NoAccessException.php 2 years ago Nonce.php 6 months ago Notification.php 6 months ago NumberFormatter.php 5 months ago Option.php 5 months ago Period.php 3 months ago Piwik.php 3 months ago Plugin.php 3 months ago Process.php 1 year ago Profiler.php 6 months ago ProxyHeaders.php 4 months ago ProxyHttp.php 5 months ago QuickForm2.php 3 months ago RankingQuery.php 5 months ago ReportRenderer.php 3 months ago Request.php 3 months ago Segment.php 3 months ago Sequence.php 6 months ago Session.php 3 months ago SettingsPiwik.php 3 months ago SettingsServer.php 1 year ago Singleton.php 2 years ago Site.php 4 months ago SiteContentDetector.php 3 months ago SupportedBrowser.php 2 years ago TCPDF.php 1 year ago Theme.php 1 year ago Timer.php 2 years ago Tracker.php 3 months ago Twig.php 3 months ago Unzip.php 1 year ago UpdateCheck.php 3 months ago Updater.php 3 months ago UpdaterErrorException.php 2 years ago Updates.php 3 months ago Url.php 3 months ago UrlHelper.php 3 months ago Version.php 3 months ago View.php 3 months ago bootstrap.php 1 year ago dispatch.php 2 years ago testMinimumPhpVersion.php 6 months ago
NumberFormatter.php
347 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 * @return mixed|string
92 */
93 public function formatNumberCompact($value)
94 {
95 [$compactPattern, $factor] = $this->determineCorrectCompactPattern('Intl_NumberFormatNumberCompact', $value);
96 // In case no special formatting should be used, we use the default number format
97 if (round($value) < 1000 || $compactPattern === '0') {
98 $maximumFractionDigits = $this->getMaxFractionDigitsForCompactFormat(round($value));
99 return $this->formatNumber($value, $maximumFractionDigits, 0);
100 }
101 return $this->formatCompact($compactPattern, $factor, $value);
102 }
103 /**
104 * Formats given number as percent value
105 * @param string|int|float $value
106 * @param int $maximumFractionDigits
107 * @param int $minimumFractionDigits
108 * @return mixed|string
109 */
110 public function formatPercent($value, $maximumFractionDigits = 0, $minimumFractionDigits = 0)
111 {
112 $newValue = trim($value, " \x00\v%");
113 if (!is_numeric($newValue)) {
114 return $value;
115 }
116 $pattern = $this->getPattern($value, 'Intl_NumberFormatPercent');
117 return $this->formatNumberWithPattern($pattern, $newValue, $maximumFractionDigits, $minimumFractionDigits);
118 }
119 /**
120 * Formats given number as percent value, but keep the leading + sign if found
121 *
122 * @param $value
123 * @return string
124 */
125 public function formatPercentEvolution($value)
126 {
127 $isPositiveEvolution = !empty($value) && ($value > 0 || substr($value, 0, 1) === '+');
128 $formatted = self::formatPercent($value);
129 if ($isPositiveEvolution) {
130 // $this->symbols has already been initialized from formatPercent().
131 $language = $this->translator->getCurrentLanguage();
132 return $this->symbols[$language]['+'] . $formatted;
133 }
134 return $formatted;
135 }
136 /**
137 * Formats given number as currency value
138 *
139 * @param string|int|float $value
140 * @param string $currency
141 * @param int $precision
142 * @return mixed|string
143 */
144 public function formatCurrency($value, $currency, $precision = 2)
145 {
146 $newValue = trim(strval($value), " \x00\v{$currency}");
147 if (!is_numeric($newValue)) {
148 return $value;
149 }
150 $pattern = $this->getPattern($value, 'Intl_NumberFormatCurrency');
151 if ($newValue == round($newValue)) {
152 // if no fraction digits available, don't show any
153 $value = $this->formatNumberWithPattern($pattern, $newValue, 0, 0);
154 } else {
155 // show given count of fraction digits otherwise
156 $value = $this->formatNumberWithPattern($pattern, $newValue, $precision, $precision);
157 }
158 return str_replace('¤', $currency, $value);
159 }
160 /**
161 * Formats a given number as currency value in compact format
162 *
163 * @see \Piwik\NumberFormatter::format()
164 *
165 * @param string|int|float $value
166 * @return mixed|string
167 */
168 public function formatCurrencyCompact($value, $currency)
169 {
170 [$compactPattern, $factor] = $this->determineCorrectCompactPattern('Intl_NumberFormatCurrencyCompact', $value);
171 // In case no special formatting should be used, we use the default number format
172 if (round($value) < 1000 || $compactPattern === '0') {
173 $maximumFractionDigits = $this->getMaxFractionDigitsForCompactFormat(round($value));
174 return $this->formatCurrency($value, $currency, 0);
175 }
176 return str_replace('¤', $currency, $this->formatCompact($compactPattern, $factor, $value));
177 }
178 private function getMaxFractionDigitsForCompactFormat(int $valueLength) : int
179 {
180 return $valueLength === 1 ? 1 : 0;
181 }
182 private function determineCorrectCompactPattern(string $patternPrefix, $value)
183 {
184 $finalFactor = 0;
185 $patternId = '';
186 if (round($value) < 1000) {
187 return ['0', 1];
188 }
189 for ($factor = 1000; $factor <= 1.0E+19; $factor *= 10) {
190 $patternOne = $patternPrefix . $factor . 'One';
191 $patternOther = $patternPrefix . $factor . 'Other';
192 if (round($value / $factor) === 1.0 && $this->translator->translate($patternOne) !== '') {
193 $finalFactor = $factor;
194 $patternId = $patternOne;
195 } elseif (round($value / $factor) >= 1 && $this->translator->translate($patternOther) !== '') {
196 $finalFactor = $factor;
197 $patternId = $patternOther;
198 }
199 if ($this->translator->translate($patternId) !== $patternId) {
200 $charCount = substr_count($this->translator->translate($patternId), '0');
201 if (round($value * pow(10, $charCount) / ($factor * 10)) < pow(10, $charCount)) {
202 break;
203 }
204 }
205 }
206 return [$this->translator->translate($patternId), $finalFactor];
207 }
208 private function formatCompact(string $pattern, int $factor, $value)
209 {
210 $charCount = substr_count($pattern, '0');
211 if ($charCount > 1) {
212 $factor /= pow(10, $charCount - 1);
213 }
214 $maximumFractionDigits = $this->getMaxFractionDigitsForCompactFormat($charCount);
215 // cut off numbers after a certain decimal, as formatNumber would round otherwise
216 $digitCountFactor = pow(10, $maximumFractionDigits);
217 $value = round($value / $factor * $digitCountFactor) / $digitCountFactor;
218 $formattedNumber = $this->formatNumber($value, $maximumFractionDigits, 0);
219 return preg_replace(['/(0+)/', '/(\'\\.\')/'], [$formattedNumber, '.'], $pattern);
220 }
221 /**
222 * Returns the relevant pattern for the given number.
223 *
224 * @param string $value
225 * @param string $translationId
226 * @return string
227 */
228 protected function getPattern($value, $translationId)
229 {
230 $language = $this->translator->getCurrentLanguage();
231 if (!isset($this->patterns[$language][$translationId])) {
232 $this->patterns[$language][$translationId] = $this->parsePattern($this->translator->translate($translationId));
233 }
234 list($positivePattern, $negativePattern) = $this->patterns[$language][$translationId];
235 $negative = $this->isNegative($value);
236 return $negative ? $negativePattern : $positivePattern;
237 }
238 /**
239 * Formats the given number with the given pattern
240 *
241 * @param string $pattern
242 * @param string|int|float $value
243 * @param int $maximumFractionDigits
244 * @param int $minimumFractionDigits
245 * @return mixed|string
246 */
247 protected function formatNumberWithPattern($pattern, $value, $maximumFractionDigits = 0, $minimumFractionDigits = 0)
248 {
249 if (!is_numeric($value)) {
250 return $value;
251 }
252 $usesGrouping = strpos($pattern, ',') !== \false;
253 // if pattern has number groups, parse them.
254 if ($usesGrouping) {
255 preg_match('/#+0/', $pattern, $primaryGroupMatches);
256 $primaryGroupSize = $secondaryGroupSize = strlen($primaryGroupMatches[0]);
257 $numberGroups = explode(',', $pattern);
258 // check for distinct secondary group size.
259 if (count($numberGroups) > 2) {
260 $secondaryGroupSize = strlen($numberGroups[1]);
261 }
262 }
263 // Ensure that the value is positive and has the right number of digits.
264 $negative = $this->isNegative($value);
265 $signMultiplier = $negative ? '-1' : '1';
266 $value = $value / $signMultiplier;
267 $value = round($value, $maximumFractionDigits);
268 // Split the number into major and minor digits.
269 $valueParts = explode('.', $value);
270 $majorDigits = $valueParts[0];
271 // Account for maximumFractionDigits = 0, where the number won't
272 // have a decimal point, and $valueParts[1] won't be set.
273 $minorDigits = isset($valueParts[1]) ? $valueParts[1] : '';
274 if ($usesGrouping) {
275 // Reverse the major digits, since they are grouped from the right.
276 $majorDigits = array_reverse(str_split($majorDigits));
277 // Group the major digits.
278 $groups = array();
279 $groups[] = array_splice($majorDigits, 0, $primaryGroupSize);
280 while (!empty($majorDigits)) {
281 $groups[] = array_splice($majorDigits, 0, $secondaryGroupSize);
282 }
283 // Reverse the groups and the digits inside of them.
284 $groups = array_reverse($groups);
285 foreach ($groups as &$group) {
286 $group = implode(array_reverse($group));
287 }
288 // Reconstruct the major digits.
289 $majorDigits = implode(',', $groups);
290 }
291 if ($minimumFractionDigits <= $maximumFractionDigits) {
292 // Strip any trailing zeroes.
293 $minorDigits = rtrim($minorDigits, '0');
294 if (strlen($minorDigits) < $minimumFractionDigits) {
295 // Now there are too few digits, re-add trailing zeroes
296 // until the desired length is reached.
297 $neededZeroes = $minimumFractionDigits - strlen($minorDigits);
298 $minorDigits .= str_repeat('0', $neededZeroes);
299 }
300 }
301 // Assemble the final number and insert it into the pattern.
302 $value = strlen($minorDigits) ? $majorDigits . '.' . $minorDigits : $majorDigits;
303 $value = preg_replace('/#(?:[\\.,]#+)*0(?:[,\\.][0#]+)*/', $value, $pattern);
304 // Localize the number.
305 $value = $this->replaceSymbols($value);
306 return $value;
307 }
308 /**
309 * Replaces number symbols with their localized equivalents.
310 *
311 * @param string $value The value being formatted.
312 *
313 * @return string
314 *
315 * @see https://cldr.unicode.org/translation/number-symbols
316 */
317 protected function replaceSymbols($value)
318 {
319 $language = $this->translator->getCurrentLanguage();
320 if (!isset($this->symbols[$language])) {
321 $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'));
322 }
323 return strtr($value, $this->symbols[$language]);
324 }
325 /**
326 * @param $value
327 * @return bool
328 */
329 protected function isNegative($value)
330 {
331 return $value < 0;
332 }
333 /**
334 * @deprecated
335 * @return self
336 */
337 public static function getInstance()
338 {
339 return StaticContainer::get(\Piwik\NumberFormatter::class);
340 }
341 public function clearCache()
342 {
343 $this->patterns = [];
344 $this->symbols = [];
345 }
346 }
347