PHP 上で何かしらを読み込み、メモリ上に展開したり、レスポンスとして返すことがあります。そういった時、次の様にひたすら同じ値が続くデータをまとめると処理が軽くなることがあります。
0,0,0,0,0,0,0,0,...// 1000個の0が並んでいるとして
↓ 値(value)が0, 始まり(start)が0番インデックス, 終わり(end)が999番インデックスという情報に変換
{
'v' => 0,
's' => 0,
'e' => 999,
}
素の BMP 画像やエンコード前の動画が普段見かけるサイズより格段に大きいのを、圧縮するのと同じ様な感じです。これを行うコードを紹介します。
ドット状配列(ここでは数値がひたすら並んでいる配列を指します)をベクトル状配列(ここでは値、始まり、終わりの情報を持つ構造体の配列を指します)に変換するのは次でできます。
/**
* ある値の繰り返しを start,end の記録に畳み込み
* @param array<int> $dots
* @return array
*/
function dots_to_vectors(array $dots): array
{
$vectors = [];// ベクトルを格納する領域
$preValue = null;// 値の連続を確認するための一時変数
foreach ($dots as $index => $currentValue) {
if ($preValue !== $currentValue) {
// 前回までの値と今回の値が連続していないならばベクトルを更新
if (! empty($vectors)) {
// ベクトルがまっさらでないならば、値の連続の終端を記録
$vectors[count($vectors) - 1]['e'] = $index - 1;
}
// 新たなベクトルを追加
$vectors[] = [
's' => $index,
'e' => null,
'v' => $currentValue,
];
}
// 今回の値を前回の値として保持して次ループへ移動
$preValue = $currentValue;
}
// 終端は決まっているのでループ外でセット
$vectors[count($vectors) - 1]['e'] = count($dots) - 1;
return $vectors;
}
これで値を圧縮するとメモリなり、通信路なりで使われる値のデータ量が減り、うれしいことが起きる場合がります。値を一次元配列に復元する場合は次です。
/**
* start,end の記録に畳み込まれている値をある値の繰り返しにする
* @param array<{s: int; e: int; v: mixed}> $vectors
* @return array
*/
function vectors_to_dots(array $vectors): array
{
$dotsParts = [];// 連続した値の塊を保持する配列
foreach ($vectors as $vector) {
// ベクトルを元に array_fill 関数で連続した値の配列を生成して dotsParts に格納
// @see https://www.php.net/manual/ja/function.array-fill.php
$dotsParts[] = array_fill($vector['s'], $vector['e'] - $vector['s'] + 1, $vector['v']);
}
$dots = [];
// array_walk_recursive で配列中の全ての要素を一つ一つ見て、一次元配列である dots の中に格納していく
// @see https://www.php.net/manual/ja/function.array-walk-recursive.php
array_walk_recursive($dotsParts, static function ($e) use (&$dots) {
$dots[] = $e;
});
return $dots;
}
これでデータを欠けさせることなくベクトルとドットを行き来させられます。前振りではデータ量について述べましたが、ベクトル状にすると他単位との変換がしやすくなる利点もあります。例えば、”ベクトルの持つ長さ ÷ 元々のドット配列の長さ = 同じ値の連続する割合”となり、グラフに起こしやすくなります。