-
Notifications
You must be signed in to change notification settings - Fork 11.5k
[10.x] Add a Number
utility class
#48845
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b5fb4a9
fb0e6ce
66655b4
8290812
21ed5e0
d59942b
8151a80
9a2704f
d2adff7
692f0e4
cd6117b
fa8e3b8
4b69ee5
3f2fc5f
31285d1
ec8ba4b
6d3a6c2
47da238
79be940
5ff420d
cf1bc44
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
<?php | ||
|
||
namespace Illuminate\Support; | ||
|
||
use Illuminate\Support\Traits\Macroable; | ||
use NumberFormatter; | ||
use RuntimeException; | ||
|
||
class Number | ||
{ | ||
use Macroable; | ||
|
||
/** | ||
* The current default locale. | ||
* | ||
* @var string | ||
*/ | ||
protected static $locale = 'en'; | ||
|
||
/** | ||
* Format the given number according to the current locale. | ||
* | ||
* @param int|float $number | ||
* @param ?string $locale | ||
* @return string|false | ||
*/ | ||
public static function format(int|float $number, ?string $locale = null) | ||
{ | ||
static::ensureIntlExtensionIsInstalled(); | ||
|
||
$formatter = new NumberFormatter($locale ?? static::$locale, NumberFormatter::DECIMAL); | ||
|
||
return $formatter->format($number); | ||
} | ||
|
||
/** | ||
* Convert the given number to its percentage equivalent. | ||
* | ||
* @param int|float $number | ||
* @param int $precision | ||
* @param ?string $locale | ||
* @return string|false | ||
*/ | ||
public static function toPercentage(int|float $number, int $precision = 0, ?string $locale = null) | ||
{ | ||
static::ensureIntlExtensionIsInstalled(); | ||
|
||
$formatter = new NumberFormatter($locale ?? static::$locale, NumberFormatter::PERCENT); | ||
|
||
$formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $precision); | ||
|
||
return $formatter->format($number / 100); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm dividing the supplied value by 100 so that 50 = 50% as that's how Rails does it, and that's what Taylor linked to in his suggestion for this class. The default number formatter would consider 0.5 to be 50% and 50 to be 5000%. I'm not sure which option is best, so I went with the Rails format. I would very much like some thoughts on this matter. |
||
} | ||
|
||
/** | ||
* Convert the given number to its currency equivalent. | ||
* | ||
* @param int|float $number | ||
* @param string $currency | ||
* @param ?string $locale | ||
* @return string|false | ||
*/ | ||
public static function toCurrency(int|float $number, string $currency = 'USD', ?string $locale = null) | ||
{ | ||
static::ensureIntlExtensionIsInstalled(); | ||
|
||
$formatter = new NumberFormatter($locale ?? static::$locale, NumberFormatter::CURRENCY); | ||
|
||
return $formatter->formatCurrency($number, $currency); | ||
} | ||
|
||
/** | ||
* Convert the given number to its file size equivalent. | ||
* | ||
* @param int|float $bytes | ||
* @param int $precision | ||
* @return string | ||
*/ | ||
public static function toFileSize(int|float $bytes, int $precision = 0) | ||
{ | ||
$units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; | ||
|
||
for ($i = 0; ($bytes / 1024) > 0.9 && ($i < count($units) - 1); $i++) { | ||
$bytes /= 1024; | ||
} | ||
|
||
return sprintf('%s %s', number_format($bytes, $precision), $units[$i]); | ||
} | ||
Comment on lines
+79
to
+88
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was the idea to use the binary prefixes (KiB, MiB, etc.) abandoned? |
||
|
||
/** | ||
* Convert the number to its human readable equivalent. | ||
* | ||
* @param int $number | ||
* @param int $precision | ||
* @return string | ||
*/ | ||
public static function forHumans(int|float $number, int $precision = 0) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about other languages? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, maybe the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
{ | ||
$units = [ | ||
3 => 'thousand', | ||
6 => 'million', | ||
9 => 'billion', | ||
12 => 'trillion', | ||
15 => 'quadrillion', | ||
]; | ||
|
||
switch (true) { | ||
case $number === 0: | ||
return '0'; | ||
case $number < 0: | ||
return sprintf('-%s', static::forHumans(abs($number), $precision)); | ||
case $number >= 1e15: | ||
return sprintf('%s quadrillion', static::forHumans($number / 1e15, $precision)); | ||
} | ||
|
||
$numberExponent = floor(log10($number)); | ||
$displayExponent = $numberExponent - ($numberExponent % 3); | ||
$number /= pow(10, $displayExponent); | ||
|
||
return trim(sprintf('%s %s', number_format($number, $precision), $units[$displayExponent])); | ||
} | ||
|
||
/** | ||
* Execute the given callback using the given locale. | ||
* | ||
* @param string $locale | ||
* @param callable $callback | ||
* @return mixed | ||
*/ | ||
public static function withLocale(string $locale, callable $callback) | ||
{ | ||
$previousLocale = static::$locale; | ||
|
||
static::useLocale($locale); | ||
|
||
return tap($callback(), fn () => static::useLocale($previousLocale)); | ||
} | ||
|
||
/** | ||
* Set the default locale. | ||
* | ||
* @param string $locale | ||
* @return void | ||
*/ | ||
public static function useLocale(string $locale) | ||
{ | ||
static::$locale = $locale; | ||
} | ||
|
||
/** | ||
* Ensure the "intl" PHP exntension is installed. | ||
* | ||
* @return void | ||
*/ | ||
protected static function ensureIntlExtensionIsInstalled() | ||
{ | ||
if (! extension_loaded('intl')) { | ||
throw new RuntimeException('The "intl" PHP extension is required to use this method.'); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
<?php | ||
|
||
namespace Illuminate\Tests\Support; | ||
|
||
use Illuminate\Support\Number; | ||
use PHPUnit\Framework\TestCase; | ||
|
||
class SupportNumberTest extends TestCase | ||
{ | ||
public function testFormat() | ||
{ | ||
$this->needsIntlExtension(); | ||
|
||
$this->assertSame('0', Number::format(0)); | ||
$this->assertSame('1', Number::format(1)); | ||
$this->assertSame('10', Number::format(10)); | ||
$this->assertSame('25', Number::format(25)); | ||
$this->assertSame('100', Number::format(100)); | ||
$this->assertSame('100,000', Number::format(100000)); | ||
$this->assertSame('123,456,789', Number::format(123456789)); | ||
|
||
$this->assertSame('-1', Number::format(-1)); | ||
$this->assertSame('-10', Number::format(-10)); | ||
$this->assertSame('-25', Number::format(-25)); | ||
|
||
$this->assertSame('0.2', Number::format(0.2)); | ||
$this->assertSame('1.23', Number::format(1.23)); | ||
$this->assertSame('-1.23', Number::format(-1.23)); | ||
$this->assertSame('123.456', Number::format(123.456)); | ||
|
||
$this->assertSame('∞', Number::format(INF)); | ||
$this->assertSame('NaN', Number::format(NAN)); | ||
} | ||
|
||
public function testFormatWithDifferentLocale() | ||
{ | ||
$this->needsIntlExtension(); | ||
|
||
$this->assertSame('123,456,789', Number::format(123456789, 'en')); | ||
$this->assertSame('123.456.789', Number::format(123456789, 'de')); | ||
$this->assertSame('123 456 789', Number::format(123456789, 'fr')); | ||
$this->assertSame('123 456 789', Number::format(123456789, 'ru')); | ||
$this->assertSame('123 456 789', Number::format(123456789, 'sv')); | ||
} | ||
|
||
public function testFormatWithAppLocale() | ||
{ | ||
$this->needsIntlExtension(); | ||
|
||
$this->assertSame('123,456,789', Number::format(123456789)); | ||
|
||
Number::useLocale('de'); | ||
|
||
$this->assertSame('123.456.789', Number::format(123456789)); | ||
|
||
Number::useLocale('en'); | ||
} | ||
|
||
public function testToPercent() | ||
{ | ||
$this->needsIntlExtension(); | ||
|
||
$this->assertSame('0%', Number::toPercentage(0, precision: 0)); | ||
$this->assertSame('0%', Number::toPercentage(0)); | ||
$this->assertSame('1%', Number::toPercentage(1)); | ||
$this->assertSame('10.00%', Number::toPercentage(10, precision: 2)); | ||
$this->assertSame('100%', Number::toPercentage(100)); | ||
$this->assertSame('100.00%', Number::toPercentage(100, precision: 2)); | ||
|
||
$this->assertSame('300%', Number::toPercentage(300)); | ||
$this->assertSame('1,000%', Number::toPercentage(1000)); | ||
|
||
$this->assertSame('2%', Number::toPercentage(1.75)); | ||
$this->assertSame('1.75%', Number::toPercentage(1.75, precision: 2)); | ||
$this->assertSame('1.750%', Number::toPercentage(1.75, precision: 3)); | ||
$this->assertSame('0%', Number::toPercentage(0.12345)); | ||
$this->assertSame('0.12%', Number::toPercentage(0.12345, precision: 2)); | ||
$this->assertSame('0.1235%', Number::toPercentage(0.12345, precision: 4)); | ||
} | ||
|
||
public function testToCurrency() | ||
{ | ||
$this->needsIntlExtension(); | ||
|
||
$this->assertSame('$0.00', Number::toCurrency(0)); | ||
$this->assertSame('$1.00', Number::toCurrency(1)); | ||
$this->assertSame('$10.00', Number::toCurrency(10)); | ||
|
||
$this->assertSame('€0.00', Number::toCurrency(0, 'EUR')); | ||
$this->assertSame('€1.00', Number::toCurrency(1, 'EUR')); | ||
$this->assertSame('€10.00', Number::toCurrency(10, 'EUR')); | ||
|
||
$this->assertSame('-$5.00', Number::toCurrency(-5)); | ||
$this->assertSame('$5.00', Number::toCurrency(5.00)); | ||
$this->assertSame('$5.32', Number::toCurrency(5.325)); | ||
} | ||
|
||
public function testToCurrencyWithDifferentLocale() | ||
{ | ||
$this->needsIntlExtension(); | ||
|
||
$this->assertSame('1,00 €', Number::toCurrency(1, 'EUR', 'de')); | ||
$this->assertSame('1,00 $', Number::toCurrency(1, 'USD', 'de')); | ||
$this->assertSame('1,00 £', Number::toCurrency(1, 'GBP', 'de')); | ||
|
||
$this->assertSame('123.456.789,12 $', Number::toCurrency(123456789.12345, 'USD', 'de')); | ||
$this->assertSame('123.456.789,12 €', Number::toCurrency(123456789.12345, 'EUR', 'de')); | ||
$this->assertSame('1 234,56 $US', Number::toCurrency(1234.56, 'USD', 'fr')); | ||
} | ||
|
||
public function testBytesToHuman() | ||
{ | ||
$this->assertSame('0 B', Number::toFileSize(0)); | ||
$this->assertSame('1 B', Number::toFileSize(1)); | ||
$this->assertSame('1 KB', Number::toFileSize(1024)); | ||
$this->assertSame('2 KB', Number::toFileSize(2048)); | ||
$this->assertSame('2.00 KB', Number::toFileSize(2048, precision: 2)); | ||
$this->assertSame('1.23 KB', Number::toFileSize(1264, precision: 2)); | ||
$this->assertSame('1.234 KB', Number::toFileSize(1264, 3)); | ||
$this->assertSame('5 GB', Number::toFileSize(1024 * 1024 * 1024 * 5)); | ||
$this->assertSame('10 TB', Number::toFileSize((1024 ** 4) * 10)); | ||
$this->assertSame('10 PB', Number::toFileSize((1024 ** 5) * 10)); | ||
$this->assertSame('1 ZB', Number::toFileSize(1024 ** 7)); | ||
$this->assertSame('1 YB', Number::toFileSize(1024 ** 8)); | ||
$this->assertSame('1,024 YB', Number::toFileSize(1024 ** 9)); | ||
} | ||
|
||
public function testToHuman() | ||
{ | ||
$this->assertSame('1', Number::forHumans(1)); | ||
$this->assertSame('10', Number::forHumans(10)); | ||
$this->assertSame('100', Number::forHumans(100)); | ||
$this->assertSame('1 thousand', Number::forHumans(1000)); | ||
$this->assertSame('1 million', Number::forHumans(1000000)); | ||
$this->assertSame('1 billion', Number::forHumans(1000000000)); | ||
$this->assertSame('1 trillion', Number::forHumans(1000000000000)); | ||
$this->assertSame('1 quadrillion', Number::forHumans(1000000000000000)); | ||
$this->assertSame('1 thousand quadrillion', Number::forHumans(1000000000000000000)); | ||
|
||
$this->assertSame('123', Number::forHumans(123)); | ||
$this->assertSame('1 thousand', Number::forHumans(1234)); | ||
$this->assertSame('1.23 thousand', Number::forHumans(1234, precision: 2)); | ||
$this->assertSame('12 thousand', Number::forHumans(12345)); | ||
$this->assertSame('1 million', Number::forHumans(1234567)); | ||
$this->assertSame('1 billion', Number::forHumans(1234567890)); | ||
$this->assertSame('1 trillion', Number::forHumans(1234567890123)); | ||
$this->assertSame('1.23 trillion', Number::forHumans(1234567890123, precision: 2)); | ||
$this->assertSame('1 quadrillion', Number::forHumans(1234567890123456)); | ||
$this->assertSame('1.23 thousand quadrillion', Number::forHumans(1234567890123456789, precision: 2)); | ||
$this->assertSame('490 thousand', Number::forHumans(489939)); | ||
$this->assertSame('489.9390 thousand', Number::forHumans(489939, precision: 4)); | ||
$this->assertSame('500.00000 million', Number::forHumans(500000000, precision: 5)); | ||
|
||
$this->assertSame('1 million quadrillion', Number::forHumans(1000000000000000000000)); | ||
$this->assertSame('1 billion quadrillion', Number::forHumans(1000000000000000000000000)); | ||
$this->assertSame('1 trillion quadrillion', Number::forHumans(1000000000000000000000000000)); | ||
$this->assertSame('1 quadrillion quadrillion', Number::forHumans(1000000000000000000000000000000)); | ||
$this->assertSame('1 thousand quadrillion quadrillion', Number::forHumans(1000000000000000000000000000000000)); | ||
|
||
$this->assertSame('0', Number::forHumans(0)); | ||
$this->assertSame('-1', Number::forHumans(-1)); | ||
$this->assertSame('-10', Number::forHumans(-10)); | ||
$this->assertSame('-100', Number::forHumans(-100)); | ||
$this->assertSame('-1 thousand', Number::forHumans(-1000)); | ||
$this->assertSame('-1 million', Number::forHumans(-1000000)); | ||
$this->assertSame('-1 billion', Number::forHumans(-1000000000)); | ||
$this->assertSame('-1 trillion', Number::forHumans(-1000000000000)); | ||
$this->assertSame('-1 quadrillion', Number::forHumans(-1000000000000000)); | ||
$this->assertSame('-1 thousand quadrillion', Number::forHumans(-1000000000000000000)); | ||
} | ||
|
||
protected function needsIntlExtension() | ||
{ | ||
if (! extension_loaded('intl')) { | ||
$this->markTestSkipped('The intl extension is not installed. Please install the extension to enable '.__CLASS__); | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.