Last active
January 7, 2022 00:23
-
-
Save NewEXE/00307eb470dba735826fc123cf722f0c to your computer and use it in GitHub Desktop.
Abbreviate large numbers (1000 -> 1K). Fast approach. PHP
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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