大きな数値を多く取り扱う時、順序だけ維持してデータ量を小さくしたい時があります。そうした際は使う文字を0~9からより広い範囲に広げるN進数への変換で値を維持したままデータ量を小さくすることができます。これを実装するためのN進数に変換するコードを紹介します。
具体的なコードは次です。
/**
* 10進数をN進数に変換する
* @param int $num 10進数で表された数
* @param string $chars N進数で使用される文字のセット(デフォルトは0-9, a-z, A-Z)
* @return string N進数で表された数を文字列として返す
*/
function decToN(int $num, string $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'): string
{
$len = strlen($chars); // N進数の基数を計算(使用する文字の数)
$ret = ''; // 結果を格納する文字列を初期化
while ($num > 0) { // 与えられた数が0より大きい間、ループを続ける
$remainder = $num % $len; // 現在の数を基数で割った余りを計算
$ret = $chars[$remainder] . $ret; // 余りに対応する文字を結果の先頭に追加
$num = (int)($num / $len); // 現在の数を基数で割り、次の桁へ移動
}
return $ret; // 計算されたN進数の文字列を返す
}
// 使用例
$decimalNumber = 20240108155522;
$base62Number = decToN($decimalNumber);
echo $base62Number . "\n"; // 5kKz19P8
割り算の筆算に近いイメージです。残りの数から割る数でいくつ取れるかを繰り返していくアレです。こうすると順序を維持したままデータ量を減らせます。
このN進数は表現方法が10進数と異なるだけで値の持っている情報は10進数の時と同じです。ですので復元もできます。これは次のようにできます。
/**
* N進数を10進数に変換する
* @param string $str N進数で表された数の文字列
* @param string $chars N進数で使用される文字のセット(デフォルトは0-9, A-Z, a-z)
* @return int 10進数で表された数
*/
function nToDec(string $str, string $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'): int
{
$baseLen = strlen($chars); // N進数の基数を計算(使用する文字の数)
$num = 0; // 結果を格納する数値を初期化
$inputLen = strlen($str);
$basePower = 1; // 基数のべき乗。最初は 基数 ** 0 = 1
// 一桁目から順に計算して合計を出す
for ($i = $inputLen - 1; 0 <= $i; $i--) {
$num += strpos($chars, $str[$i]) * $basePower; // 現在の文字に対応する数値を加算
$basePower *= $baseLen; // 基数をかけて桁を上げる
}
return $num; // 計算された10進数の値を返す
}
// 使用例
$base62Number = '5kKz19P8';
$decimalNumber = nToDec($base62Number);
echo $decimalNumber . "\n"; // 20240108155522
こちらはひたすら足し算です。この文字がx番目の文字で、今y桁目だから、この値を基数のy乗倍して合計に追加、という計算を繰り返しています。
このようなN進数で順序を維持する際には使用する文字セットを気にする必要があります。何とはなしに記号を入れると次のような部分で予期せぬ並び順になりやすいです。
$c = ['a','A','/','['];
sort($c);
echo implode(', ', $c) . "\n"; # /, A, [, a
人の目で読みやすい範囲としては数字、大文字、小文字の62進数が無難です。もしより多くの文字を使ってN進数を作るのであれば、次のようなコードポイントを扱うコードを用いて文字セットを作ると便利です。
$chars = '';
$end = ord('~'); // 読める文字の終わりのコードポイントを取得。ここではUTF-8
for($i = ord('!'); $i <= $end; $i++) {
$chars.= chr($i);
}
// この範囲の外は目視できないと考えるべき(エディタによっては表示してくれる)
echo $chars . "\n"; // !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
この記事ではstrpos, strlenなどのシングルバイト文字用の関数を用いています。もし日本語等を用いて更にNを大きくするならmb_strpos, mb_strlen, mb_str_splitといったマルチバイト文字列用の関数を使う必要があります。