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