Skip to content

Instantly share code, notes, and snippets.

@NewEXE
Last active January 7, 2022 00:23
Show Gist options
  • Save NewEXE/00307eb470dba735826fc123cf722f0c to your computer and use it in GitHub Desktop.
Save NewEXE/00307eb470dba735826fc123cf722f0c to your computer and use it in GitHub Desktop.
Abbreviate large numbers (1000 -> 1K). Fast approach. PHP
<?php
/**
* Convert long integer number into abbreviated string.
* Fast approach without math functions usage.
* Integers up to 999 Uncentillions (1 Un = 1e306) are supported.
*
* getAbbreviatedNumber('1024'); // '1.02K'
* getAbbreviatedNumber('-123456', 3); // '-123.456K'
* getAbbreviatedNumber('1000000'); // '1M'
*
* Incorrect calls:
* getAbbreviatedNumber('1 000 000 '); // Unexpected behavior! Remove whitespaces first.
* getAbbreviatedNumber(1024); // Unexpected behavior! Number must be as string.
*
* @param string $integerNum The integer number to abbreviation. MUST BE without whitespaces and any other separators.
* @param int $decimals The number of decimal digits.
* @param string $decimalSeparator
* @return ?string Abbreviated number or NULL when the integer is greater than 1e308
*/
function getAbbreviatedNumber(string $integerNum, int $decimals = 2, string $decimalSeparator = '.'): ?string
{
/* Comments for number '1204567' (1 204 567). It's '1.2M'. */
$minusSign = '';
if ($integerNum[0] === '-') {
$minusSign = '-';
$integerNum = ltrim($integerNum, '-');
}
$numLen = strlen($integerNum); /* 7 */
if ($numLen <= 3) {
// Trap for numbers <= 999
return $minusSign . $integerNum;
}
/**
* @var array [
* 'K' => 6,
* 'M' => 9,
* 'B' => 12,
* // and so on...
* ]
*/
static $rules = [];
if ($rules === []) {
$abbreviations = [
# Million (up to 1e30)
'K', // Thousand (Kilo) 1000 (10^3)
'M', // Million (10^6)
'B', // Billion (10^9)
'T', // Trillion (10^12)
'Qa', 'Qi', 'Sx', 'Sp', 'Oc', 'No',
# Decillion (1e33 - 1e60)
'Dc', 'Ud', 'Dd', 'Td', 'Qad', 'Qid', 'Sxd', 'Spd', 'Ocd', 'Nod',
# Vigintillion (1e63 - 1e90)
'Vg', 'Uvg', 'Dvg', 'Tvg', 'Qavg', 'Qivg', 'Sxvg', 'Spvg', 'Ocvg', 'Novg',
# Trigintillion (1e93 - 1e120)
'Tg', 'Utg', 'Dtg', 'Ttg', 'Qatg', 'Qitg', 'Sxtg', 'Sptg', 'Octg', 'Notg',
# Quadragintillion (1e123 - 1e150)
'Qag', 'Uqag', 'Dqag', 'Tqag', 'Qaqag', 'Qiqag', 'Sxqag', 'Spqag', 'Ocqag', 'Noqag',
# Quinquagintillion (1e153 - 1e180)
'Qig', 'UQig', 'DQig', 'TQig', 'QaQig', 'QiQig', 'SxQig', 'SpQig', 'OcQig', 'NoQig',
# Sexagintillion (1e183 - 1e210)
'Sxg', 'USxg', 'DSxg', 'TSxg', 'QaSxg', 'QiSxg', 'SxSxg', 'SpSxg', 'OcSxg', 'NoSxg',
# Septuagintillion (1e213 - 1e240)
'Spg', 'USpg', 'DSpg', 'TSpg', 'QaSpg', 'QiSpg', 'SxSpg', 'SpSpg', 'OcSpg', 'NoSpg',
# Octogintillion (1e243 - 1e270)
'Ocg', 'UOcg', 'DOcg', 'TOcg', 'QaOcg', 'QiOcg', 'SxOcg', 'SpOcg', 'OcOcg', 'NoOcg',
# Nonagintillion (1e273 - 1e300)
'Nog', 'UNog', 'DNog', 'TNog', 'QaNog', 'QiNog', 'SxNog', 'SpNog', 'OcNog', 'NoNog',
# Centillion (1e303 - 1e306)
'C', 'Uc',
// Can be continued...
];
$totalWidth = 3;
foreach ($abbreviations as $abbreviation) {
$totalWidth += 3;
if (isset($rules[$abbreviation])) {
throw new \RuntimeException("Rules: abbreviation '$abbreviation' is duplicated!");
}
$rules[$abbreviation] = $totalWidth;
}
}
$abbreviation = null;
$paddingTotalWidth = 0;
foreach ($rules as $abbrev => $totalWidth) {
if ($numLen <= $totalWidth) {
$abbreviation = $abbrev; /* M */
$paddingTotalWidth = $totalWidth; /* 9 */
break;
}
}
if ($abbreviation === null) {
// It's astronomically, very big number
return null;
}
$integerNum = str_pad($integerNum, $paddingTotalWidth, '0', STR_PAD_LEFT); /* '001204567' */
$beforeSeparator = substr($integerNum, 0, 3); /* '001' */
$beforeSeparator = ltrim($beforeSeparator, '0'); /* '1' */
$afterSeparator = substr($integerNum, 3, $decimals); /* '20' */
$afterSeparator = rtrim($afterSeparator, '0'); /* '2' */
if ($afterSeparator === '') {
$decimalSeparator = '';
}
/* '' + '1' + '.' + '2' + 'M' */
return $minusSign . $beforeSeparator . $decimalSeparator . $afterSeparator . $abbreviation;
}
/*
* Testing
*/
echo '<pre>';
$nums = [
'99' => '99',
'-99' => '-99',
'999' => '999',
'1000' => '1K',
'1900' => '1.9K',
'1090' => '1.09K',
'1009' => '1K',
'1999' => '1.99K',
'1249' => '1.24K',
'12499' => '12.49K',
'124999' => '124.99K',
'1234567' => '1.23M',
'1204567' => '1.2M',
'1249999' => '1.24M',
'12499999' => '12.49M',
'124999999' => '124.99M',
'1249999999' => '1.24B',
'1232973425' => '1.23B',
'12499999999' => '12.49B',
'124999999999' => '124.99B',
'1249999999999' => '1.24T',
'12499999999999' => '12.49T',
'124999999999999' => '124.99T',
'-124999999999999' => '-124.99T',
'1249999999999999' => '1.24Qa',
str_repeat('9', 309) => '999.99Uc',
str_repeat('9', 310) => null,
];
var_dump('number => abbreviated number => is assert?');
foreach ($nums as $num => $abbreviatedNum) {
$calculatedAbbreviatedNum = getAbbreviatedNumber($num);
$assert = $calculatedAbbreviatedNum === $abbreviatedNum;
var_dump("$num => $calculatedAbbreviatedNum => " . ($assert ? 'true' : 'FALSE'));
}
/*
* Performance testing
*/
$start = microtime(true);
// 100 000 iteration of 999 uncentillions converting
$n = str_repeat('9', 309);
for ($i = 0; $i <= 100000; $i++) {
getAbbreviatedNumber($n);
}
$finish = round(microtime(true) - $start, 2);
var_dump($finish . ' sec'); // 0.26 sec on PHP 7.4
echo '</pre>';
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment