PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.6.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.6.0
5.13.0 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 8 months ago Access 8 months ago Application 1 year ago Archive 8 months ago ArchiveProcessor 8 months ago Archiver 2 years ago AssetManager 8 months ago Auth 8 months ago Category 8 months ago Changes 8 months ago CliMulti 1 year ago Columns 8 months ago Concurrency 8 months ago Config 8 months ago Container 8 months ago CronArchive 8 months ago DataAccess 8 months ago DataFiles 2 years ago DataTable 8 months ago Db 8 months ago DeviceDetector 1 year ago Email 2 years ago Exception 1 year ago Http 1 year ago Intl 8 months ago Log 2 years ago Mail 1 year ago Measurable 8 months ago Menu 8 months ago Metrics 8 months ago Notification 8 months ago Period 8 months ago Plugin 8 months ago Policy 8 months ago ProfessionalServices 1 year ago Report 1 year ago ReportRenderer 1 year ago Request 8 months ago Scheduler 8 months ago Segment 8 months ago Session 8 months ago Settings 8 months ago Tracker 8 months ago Translation 1 year ago Twig 1 year ago UpdateCheck 8 months ago Updater 8 months ago Updates 8 months ago Validators 1 year ago View 8 months ago ViewDataTable 8 months ago Visualization 1 year ago Widget 1 year ago .htaccess 2 years ago Access.php 8 months ago Archive.php 8 months ago ArchiveProcessor.php 8 months ago AssetManager.php 1 year ago Auth.php 8 months ago AuthResult.php 8 months ago BaseFactory.php 2 years ago Cache.php 2 years ago CacheId.php 1 year ago CliMulti.php 8 months ago Common.php 8 months ago Config.php 1 year ago Console.php 1 year ago Context.php 2 years ago Cookie.php 1 year ago CronArchive.php 8 months ago DI.php 1 year ago DataArray.php 1 year ago DataTable.php 8 months ago Date.php 8 months ago Db.php 1 year ago DbHelper.php 8 months ago Development.php 1 year ago ErrorHandler.php 8 months ago EventDispatcher.php 1 year ago ExceptionHandler.php 8 months ago FileIntegrity.php 1 year ago Filechecks.php 1 year ago Filesystem.php 1 year ago FrontController.php 8 months ago Http.php 8 months ago IP.php 1 year ago Log.php 8 months ago LogDeleter.php 1 year ago Mail.php 1 year ago Metrics.php 8 months ago NoAccessException.php 2 years ago Nonce.php 8 months ago Notification.php 8 months ago NumberFormatter.php 1 year ago Option.php 1 year ago Period.php 1 year ago Piwik.php 8 months ago Plugin.php 1 year ago Process.php 1 year ago Profiler.php 8 months ago ProxyHeaders.php 2 years ago ProxyHttp.php 1 year ago QuickForm2.php 1 year ago RankingQuery.php 8 months ago ReportRenderer.php 1 year ago Request.php 1 year ago Segment.php 8 months ago Sequence.php 8 months ago Session.php 1 year ago SettingsPiwik.php 1 year ago SettingsServer.php 1 year ago Singleton.php 2 years ago Site.php 8 months 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 8 months 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 8 months ago UrlHelper.php 1 year ago Version.php 8 months ago View.php 8 months ago bootstrap.php 1 year ago dispatch.php 2 years ago testMinimumPhpVersion.php 8 months 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 https://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