PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / trunk
Matomo Analytics – Powerful, Privacy-First Insights for WordPress vtrunk
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 month ago Access 3 months ago Application 1 month ago Archive 1 month ago ArchiveProcessor 1 month ago Archiver 2 years ago AssetManager 1 month ago Auth 6 months ago Category 6 months ago Changes 1 month ago CliMulti 1 year ago Columns 1 month ago Concurrency 1 month ago Config 1 month ago Container 1 month ago CronArchive 3 months ago DataAccess 1 month ago DataFiles 2 years ago DataTable 2 weeks ago Db 2 weeks 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 1 month ago Metrics 3 months ago Notification 6 months ago Period 1 month ago Plugin 2 weeks ago Policy 1 month ago ProfessionalServices 1 year ago Report 1 year ago ReportRenderer 3 months ago Request 3 months ago Scheduler 1 month ago Segment 1 month ago Session 2 weeks ago Settings 1 month ago Tracker 2 weeks ago Translation 1 month ago Twig 1 year ago UpdateCheck 3 months ago Updater 1 month ago Updates 3 days ago Validators 1 year ago View 1 month ago ViewDataTable 2 weeks ago Visualization 1 year ago Widget 1 month ago .htaccess 2 years ago Access.php 1 month ago Archive.php 1 month ago ArchiveProcessor.php 1 month ago AssetManager.php 1 month 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 1 month ago Common.php 2 weeks ago Config.php 1 month ago Console.php 3 months ago Context.php 2 years ago Cookie.php 1 year ago CronArchive.php 1 month ago DI.php 3 months ago DataArray.php 1 month ago DataTable.php 1 month ago Date.php 1 month ago Db.php 1 month ago DbHelper.php 1 month ago Development.php 1 year ago ErrorHandler.php 6 months ago EventDispatcher.php 1 month ago ExceptionHandler.php 4 months ago FileIntegrity.php 1 month ago Filechecks.php 1 year ago Filesystem.php 1 month ago FrontController.php 4 months ago Http.php 1 month ago IP.php 1 year ago Log.php 3 months ago LogDeleter.php 1 year ago Mail.php 1 year ago Metrics.php 1 month ago NoAccessException.php 2 years ago Nonce.php 6 months ago Notification.php 1 month ago NumberFormatter.php 5 months ago Option.php 5 months ago Period.php 1 month ago Piwik.php 1 month ago Plugin.php 1 month ago Process.php 1 month ago Profiler.php 6 months ago ProxyHeaders.php 4 months ago ProxyHttp.php 5 months ago QuickForm2.php 3 months ago RankingQuery.php 1 month ago ReportRenderer.php 1 month ago Request.php 1 month ago Segment.php 1 month ago Sequence.php 6 months ago Session.php 2 weeks ago SettingsPiwik.php 1 month ago SettingsServer.php 1 year ago Singleton.php 2 years ago Site.php 1 month ago SiteContentDetector.php 1 month ago SupportedBrowser.php 2 years ago TCPDF.php 1 year ago Theme.php 1 year ago Timer.php 1 month ago Tracker.php 1 month ago Twig.php 1 month ago Unzip.php 1 year ago UpdateCheck.php 1 month ago Updater.php 1 month ago UpdaterErrorException.php 2 years ago Updates.php 3 months ago Url.php 3 months ago UrlHelper.php 1 month ago Version.php 3 days ago View.php 1 month 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