Merge pull request #19780 from MauricioFauth/numberFormat-refactor

Refactor the Util::formatNumber() method
This commit is contained in:
Maurício Meneghini Fauth 2025-07-25 14:08:35 -03:00 committed by GitHub
commit bf125dd99c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 144 additions and 147 deletions

View File

@ -2884,7 +2884,13 @@ parameters:
path: src/Controllers/Server/Status/StatusController.php
-
message: '#^Parameter \#1 \$value of static method PhpMyAdmin\\Util\:\:formatNumber\(\) expects float\|int\|string, mixed given\.$#'
message: '#^Parameter \#1 \$value of static method PhpMyAdmin\\Util\:\:formatNumber\(\) expects float\|int\|numeric\-string, float\|int\|string given\.$#'
identifier: argument.type
count: 4
path: src/Controllers/Server/Status/StatusController.php
-
message: '#^Parameter \#1 \$value of static method PhpMyAdmin\\Util\:\:formatNumber\(\) expects float\|int\|numeric\-string, mixed given\.$#'
identifier: argument.type
count: 5
path: src/Controllers/Server/Status/StatusController.php
@ -6058,7 +6064,7 @@ parameters:
path: src/Engines/Innodb.php
-
message: '#^Parameter \#1 \$value of static method PhpMyAdmin\\Util\:\:formatNumber\(\) expects float\|int\|string, mixed given\.$#'
message: '#^Parameter \#1 \$value of static method PhpMyAdmin\\Util\:\:formatNumber\(\) expects float\|int\|numeric\-string, mixed given\.$#'
identifier: argument.type
count: 11
path: src/Engines/Innodb.php
@ -13888,7 +13894,7 @@ parameters:
path: src/StorageEngine.php
-
message: '#^Parameter \#1 \$value of static method PhpMyAdmin\\Util\:\:formatNumber\(\) expects float\|int\|string, mixed given\.$#'
message: '#^Parameter \#1 \$value of static method PhpMyAdmin\\Util\:\:formatNumber\(\) expects float\|int\|numeric\-string, mixed given\.$#'
identifier: argument.type
count: 1
path: src/StorageEngine.php
@ -15012,18 +15018,6 @@ parameters:
count: 1
path: src/Util.php
-
message: '#^Loose comparison via "\!\=" is not allowed\.$#'
identifier: notEqual.notAllowed
count: 2
path: src/Util.php
-
message: '#^Loose comparison via "\=\=" is not allowed\.$#'
identifier: equal.notAllowed
count: 3
path: src/Util.php
-
message: '#^Method PhpMyAdmin\\Util\:\:getDbInfo\(\) return type has no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
@ -16888,7 +16882,7 @@ parameters:
path: tests/unit/Controllers/Server/Status/QueriesControllerTest.php
-
message: '#^Parameter \#1 \$value of static method PhpMyAdmin\\Util\:\:formatNumber\(\) expects float\|int\|string, mixed given\.$#'
message: '#^Parameter \#1 \$value of static method PhpMyAdmin\\Util\:\:formatNumber\(\) expects float\|int\|numeric\-string, mixed given\.$#'
identifier: argument.type
count: 1
path: tests/unit/Controllers/Server/Status/QueriesControllerTest.php
@ -19140,12 +19134,6 @@ parameters:
count: 4
path: tests/unit/UtilTest.php
-
message: '#^Casting to string something that''s already string\.$#'
identifier: cast.useless
count: 1
path: tests/unit/UtilTest.php
-
message: '#^Parameter \#2 \$array of static method PHPUnit\\Framework\\Assert\:\:assertArrayNotHasKey\(\) expects array\<mixed\>\|ArrayAccess\<\(int\|string\), mixed\>, mixed given\.$#'
identifier: argument.type

View File

@ -1925,6 +1925,12 @@
</PossiblyUnusedReturnValue>
</file>
<file src="src/Controllers/Server/Status/StatusController.php">
<ArgumentTypeCoercion>
<code><![CDATA[$this->data->status['Aborted_clients']]]></code>
<code><![CDATA[$this->data->status['Aborted_connects']]]></code>
<code><![CDATA[$this->data->status['Connections']]]></code>
<code><![CDATA[$this->data->status['Max_used_connections']]]></code>
</ArgumentTypeCoercion>
<PossiblyInvalidOperand>
<code><![CDATA[$this->data->status['Aborted_clients']]]></code>
<code><![CDATA[$this->data->status['Aborted_clients']]]></code>
@ -9256,6 +9262,10 @@
</MixedArgument>
</file>
<file src="src/Util.php">
<ArgumentTypeCoercion>
<code><![CDATA[$value]]></code>
<code><![CDATA[$value]]></code>
</ArgumentTypeCoercion>
<DeprecatedMethod>
<code><![CDATA[Config::getInstance()]]></code>
<code><![CDATA[Config::getInstance()]]></code>
@ -9275,9 +9285,6 @@
<code><![CDATA[DatabaseInterface::getInstance()]]></code>
<code><![CDATA[DatabaseInterface::getInstance()]]></code>
</DeprecatedMethod>
<InvalidArrayOffset>
<code><![CDATA[$units[$d]]]></code>
</InvalidArrayOffset>
<MixedArgument>
<code><![CDATA[$array]]></code>
</MixedArgument>
@ -9296,12 +9303,9 @@
<MixedAssignment>
<code><![CDATA[$array]]></code>
<code><![CDATA[$columnNames[]]]></code>
<code><![CDATA[$unit]]></code>
<code><![CDATA[$value]]></code>
</MixedAssignment>
<MixedOperand>
<code><![CDATA[$unit]]></code>
<code><![CDATA[$unit]]></code>
<code><![CDATA[$value]]></code>
</MixedOperand>
<MixedReturnStatement>
@ -11924,7 +11928,6 @@
<code><![CDATA[mixed[]]]></code>
<code><![CDATA[mixed[]]]></code>
<code><![CDATA[mixed[]]]></code>
<code><![CDATA[mixed[]]]></code>
</MixedReturnStatement>
<PossiblyInvalidArgument>
<code><![CDATA[testBackquote]]></code>

View File

@ -47,6 +47,7 @@ use function is_object;
use function is_scalar;
use function is_string;
use function log10;
use function max;
use function mb_detect_encoding;
use function mb_strlen;
use function mb_strpos;
@ -365,11 +366,11 @@ class Util
* echo formatNumber(0, 6); // 0
* </code>
*
* @param float|int|string $value the value to format
* @param int $digitsLeft number of digits left of the comma
* @param int $digitsRight number of digits right of the comma
* @param bool $onlyDown do not reformat numbers below 1
* @param bool $noTrailingZero removes trailing zeros right of the comma (default: true)
* @param float|int|numeric-string $value the value to format
* @param int $digitsLeft number of digits left of the comma
* @param int $digitsRight number of digits right of the comma
* @param bool $onlyDown do not reformat numbers below 1
* @param bool $noTrailingZero removes trailing zeros right of the comma (default: true)
*
* @return string the formatted value and its unit
*/
@ -381,57 +382,26 @@ class Util
bool $onlyDown = false,
bool $noTrailingZero = true,
): string {
if ($value == 0) {
$value = (float) $value;
if ($value === 0.0) {
return '0';
}
if (is_string($value)) {
$value = (float) $value;
}
$originalValue = $value;
//number_format is not multibyte safe, str_replace is safe
if ($digitsLeft === 0) {
$value = number_format(
(float) $value,
$digitsRight,
/* l10n: Decimal separator */
__('.'),
/* l10n: Thousands separator */
__(','),
);
if ($originalValue != 0 && (float) $value == 0) {
return ' <' . (1 / 10 ** $digitsRight);
}
return $value;
}
// this units needs no translation, ISO
$units = [
-8 => 'y',
-7 => 'z',
-6 => 'a',
-5 => 'f',
-4 => 'p',
-3 => 'n',
-2 => 'µ',
-1 => 'm',
0 => ' ',
1 => 'k',
2 => 'M',
3 => 'G',
4 => 'T',
5 => 'P',
6 => 'E',
7 => 'Z',
8 => 'Y',
];
/* l10n: Decimal separator */
$decimalSep = __('.');
/* l10n: Thousands separator */
$thousandsSep = __(',');
//number_format is not multibyte safe, str_replace is safe
if ($digitsLeft === 0) {
$value = number_format($value, $digitsRight, $decimalSep, $thousandsSep);
if ((float) $value === 0.0) {
return '<' . number_format((1 / 10 ** $digitsRight), $digitsRight, $decimalSep, $thousandsSep);
}
return $value;
}
// check for negative value to retain sign
if ($value < 0) {
$sign = '-';
@ -443,9 +413,9 @@ class Util
$dh = 10 ** $digitsRight;
// This gives us the right SI prefix already, but $digits_left parameter not incorporated
$d = floor(log10((float) $value) / 3);
$d = floor(log10($value) / 3);
// Lowering the SI prefix by 1 gives us an additional 3 zeros
// So if we have 3,6,9,12.. free digits ($digits_left - $cur_digits) to use, then lower the SI prefix
// So if we have 3,6,9,12... free digits ($digits_left - $cur_digits) to use, then lower the SI prefix
$curDigits = floor(log10($value / 1000 ** $d) + 1);
if ($digitsLeft > $curDigits) {
$d -= floor(($digitsLeft - $curDigits) / 3);
@ -455,6 +425,32 @@ class Util
$d = 0;
}
// SI unit prefixes need no translation
$units = [
-10 => 'q', // quecto
-9 => 'r', // ronto
-8 => 'y', // yocto
-7 => 'z', // zepto
-6 => 'a', // atto
-5 => 'f', // femto
-4 => 'p', // pico
-3 => 'n', // nano
-2 => 'µ', // micro
-1 => 'm', // milli
0 => '',
1 => 'k', // kilo
2 => 'M', // mega
3 => 'G', // giga
4 => 'T', // tera
5 => 'P', // peta
6 => 'E', // exa
7 => 'Z', // zetta
8 => 'Y', // yotta
9 => 'R', // ronna
10 => 'Q', // quetta
];
$d = min(max((int) $d, -10), 10);
$value = round($value / (1000 ** $d / $dh)) / $dh;
$unit = $units[$d];
@ -465,11 +461,13 @@ class Util
$formattedValue = preg_replace('/' . preg_quote($decimalSep, '/') . '?0+$/', '', $formattedValue);
}
if ($originalValue != 0 && $value == 0) {
return ' <' . number_format(1 / 10 ** $digitsRight, $digitsRight, $decimalSep, $thousandsSep) . ' ' . $unit;
if ($value === 0.0) {
return '<'
. number_format(1 / 10 ** $digitsRight, $digitsRight, $decimalSep, $thousandsSep)
. ($unit === '' ? '' : ' ' . $unit);
}
return $sign . $formattedValue . ' ' . $unit;
return $sign . $formattedValue . ($unit === '' ? '' : ' ' . $unit);
}
/**

View File

@ -194,7 +194,7 @@ class InnodbTest extends AbstractTestCase
' </tr>' . "\n" .
' <tr>' . "\n" .
' <th scope="row">Read misses in %</th>' . "\n" .
' <td class="font-monospace text-end">50 %' . "\n" .
' <td class="font-monospace text-end">50 %' . "\n" .
'</td>' . "\n" .
' </tr>' . "\n" .
' <tr>' . "\n" .

View File

@ -538,53 +538,25 @@ class UtilTest extends AbstractTestCase
];
}
/**
* Core test for formatNumber
*
* @param float|int|string $a Value to format
* @param int $b Sensitiveness
* @param int $c Number of decimals to retain
* @param string $d Expected value
*/
private function assertFormatNumber(float|int|string $a, int $b, int $c, string $d): void
{
self::assertSame(
$d,
Util::formatNumber(
$a,
$b,
$c,
false,
),
);
}
/**
* format number test, globals are defined
*
* @param float|int|string $a Value to format
* @param int $b Sensitiveness
* @param int $c Number of decimals to retain
* @param string $d Expected value
*/
/** @psalm-param list{0: float|int|numeric-string, 1?: int, 2?: int, 3?: bool, 4?: bool} $arguments */
#[DataProvider('providerFormatNumber')]
public function testFormatNumber(float|int|string $a, int $b, int $c, string $d): void
public function testFormatNumber(string $expected, array $arguments): void
{
$this->assertFormatNumber($a, $b, $c, $d);
self::assertSame($expected, Util::formatNumber(...$arguments));
// Test with various precisions
$oldPrecision = (string) ini_get('precision');
$oldPrecision = ini_get('precision');
try {
ini_set('precision', '20');
$this->assertFormatNumber($a, $b, $c, $d);
self::assertSame($expected, Util::formatNumber(...$arguments));
ini_set('precision', '14');
$this->assertFormatNumber($a, $b, $c, $d);
self::assertSame($expected, Util::formatNumber(...$arguments));
ini_set('precision', '10');
$this->assertFormatNumber($a, $b, $c, $d);
self::assertSame($expected, Util::formatNumber(...$arguments));
ini_set('precision', '5');
$this->assertFormatNumber($a, $b, $c, $d);
self::assertSame($expected, Util::formatNumber(...$arguments));
ini_set('precision', '-1');
$this->assertFormatNumber($a, $b, $c, $d);
self::assertSame($expected, Util::formatNumber(...$arguments));
} finally {
ini_set('precision', $oldPrecision);
}
@ -593,17 +565,28 @@ class UtilTest extends AbstractTestCase
$translator = Loader::getInstance()->getTranslator();
try {
$translator->setTranslation('.', '/');
self::assertSame(
str_replace('.', '/', $expected),
Util::formatNumber(...$arguments),
'Decimal separator should be escaped for regex.',
);
// German
$translator->setTranslation(',', '.');
$translator->setTranslation('.', ',');
$expected = str_replace([',', 'X'], ['.', ','], str_replace('.', 'X', $d));
$this->assertFormatNumber($a, $b, $c, $expected);
self::assertSame(
str_replace([',', 'X'], ['.', ','], str_replace('.', 'X', $expected)),
Util::formatNumber(...$arguments),
);
// Czech
$translator->setTranslation(',', ' ');
$translator->setTranslation('.', ',');
$expected = str_replace([',', 'X'], [' ', ','], str_replace('.', 'X', $d));
$this->assertFormatNumber($a, $b, $c, $expected);
self::assertSame(
str_replace([',', 'X'], [' ', ','], str_replace('.', 'X', $expected)),
Util::formatNumber(...$arguments),
);
} finally {
// Restore
$translator->setTranslation(',', ',');
@ -611,32 +594,57 @@ class UtilTest extends AbstractTestCase
}
}
/**
* format number data provider
*
* @return mixed[]
*/
/** @psalm-return array<array{string, list{0: float|int|numeric-string, 1?: int, 2?: int, 3?: bool, 4?: bool}}> */
public static function providerFormatNumber(): array
{
return [
[10, 2, 2, '10 '],
[100, 2, 0, '100 '],
[100, 2, 2, '100 '],
['100', 2, 2, '100 '],
[-1000.454, 4, 2, '-1,000.45 '],
['-1000.454', 4, 2, '-1,000.45 '],
[0.00003, 3, 2, '30 µ'],
[0.003, 3, 3, '3 m'],
[-0.003, 6, 0, '-3,000 µ'],
[100.98, 0, 2, '100.98'],
[21010101, 0, 2, '21,010,101.00'],
[1100000000, 5, 0, '1,100 M'],
['1100000000', 5, 0, '1,100 M'],
[20000, 2, 2, '20 k'],
[20011, 2, 2, '20.01 k'],
[123456789, 6, 0, '123,457 k'],
[-123456789, 4, 2, '-123.46 M'],
[0, 6, 0, '0'],
['10', [10, 2, 2]],
['100', [100, 2, 0]],
['100', [100, 2, 2]],
['100', ['100', 2, 2]],
['-1,000.45', [-1000.454, 4, 2]],
['-1,000.45', ['-1000.454', 4, 2]],
['30 µ', [0.00003, 3, 2]],
['3 m', [0.003, 3, 3]],
['-3,000 µ', [-0.003, 6, 0]],
['100.98', [100.98, 0, 2]],
['21,010,101.00', [21010101, 0, 2]],
['1,100 M', [1100000000, 5, 0]],
['1,100 M', ['1100000000', 5, 0]],
['20 k', [20000, 2, 2]],
['20.01 k', [20011, 2, 2]],
['123,457 k', [123456789, 6, 0]],
['-123.46 M', [-123456789, 4, 2]],
['0', [0]],
['0', [0.0]],
['0', ['0']],
['0', ['0.0']],
['<0.001', [0.000001, 0, 3]],
['5 m', [0.005]],
['5 µ', [0.000005]],
['5 n', [0.000000005]],
['5 p', [0.000000000005]],
['5 f', [0.000000000000005]],
['5 a', [0.000000000000000005]],
['5 z', [0.000000000000000000005]],
['5 y', [0.000000000000000000000005]],
['5 r', [0.000000000000000000000000005]],
['5 q', [0.000000000000000000000000000005]],
['<1 q', [0.000000000000000000000000000000005]],
['5 k', [5000]],
['5 M', [5000000]],
['5 G', [5000000000]],
['5 T', [5000000000000]],
['5 P', [5000000000000000]],
['5 E', [5000000000000000000]],
['5 Z', [5000000000000000000000]],
['5 Y', [5000000000000000000000000]],
['5 R', [5000000000000000000000000000]],
['5 Q', [5000000000000000000000000000000]],
['5,000 Q', [5000000000000000000000000000000000]],
['100 m', [0.1, 3, 0]],
['<1', [0.1, 3, 0, true]],
['1.000 k', [1000, 3, 3, false, false]],
];
}