【PHP】組み合わせ生成ツールPHP-Combination

PHP

以前の記事で書いた組み合わせ生成関数やペア生成関数をPHP-Combinationとしてライブラリ化して、今回Version1.1.0までアップデートしました。

GitHubリポジトリで公開しており、Composerでインストールできるようにしています。

GitHub - macocci7/PHP-Combination: A simple PHP library to make combinations from array elements
A simple PHP library to make combinations from array elements - macocci7/PHP-Combination

このツールで出来ること

  • 配列の要素同士の全組み合わせ生成
  • 配列の要素同士の全ペア生成
  • 配列の要素同士の指定個数の組み合わせ生成
  • 配列の要素同士の指定範囲個数の組み合わせ生成
  • 複数配列の要素同士の全組み合わせ生成

PHPUnitでの使用例も掲載しています。

前提条件

  • Ubuntuで作業しています。
  • PHP 8.1以降インストール済
  • Composerインストール済

インストール

作業ディレクトリ上でコマンドを実行します。

mkdir php-combination
cd php-combination
composer require macocci7/php-combination

examplesのコピー

使い方を知るには、examplesフォルダをコピーしていじってみると良いと思います。

cp -ra vendor/macocci7/php-combination/examples/ ./

フォルダ内に入ってみましょう。

cd examples/

フォルダ内にはサンプルコードが用意されています。

基本的な使い方

BasicUsage.php を開いて見ましょう。

<?php

require_once('../vendor/autoload.php');

use Macocci7\PhpCombination\Combination;

$c = new Combination();
$items = [ 'A', 'B', 'C', ];

foreach ($c->all($items) as $index => $item) {
    echo sprintf(
        "%d: (%s)\n",
        $index,
        implode(', ', $item)
    );
}

実行してみます。

php -f BasicUsage.php

▼実行結果

0: (A, B, C)
1: (A, B)
2: (A, C)
3: (A)
4: (B, C)
5: (B)
6: (C)

配列 [ ‘A’, ‘B’, ‘C’, ] の3つの要素の全組み合わせが表示されました。

PHPコードを見ていきましょう。

require_once('../vendor/autoload.php');

use Macocci7\PhpCombination\Combination;

$c = new Combination();

composerのオートローダー「autoload.php」を取り込み、

use 宣言をしてからインスタンス生成します。

$c->all($items)

「all()」メソッドの引数に配列を渡すことで、

配列の要素同士の全組み合わせを配列として受け取ることができます。

第二引数に「true」を指定することで、文字列ソートした配列を受け取ることができます。

$c->all($items, true)

▼実行結果

0: (A)
1: (A, B)
2: (A, B, C)
3: (A, C)
4: (B)
5: (B, C)
6: (C)

組み合わせ結果の配列要素を順に結合して文字列ソートした結果が返っています。

ペアの取得

「BasicUsage.php」をコピーして「Pairs.php」を作成し、

次のように編集してみましょう。

<?php

require_once('../vendor/autoload.php');

use Macocci7\PhpCombination\Combination;

$c = new Combination();
$items = [ 'A', 'B', 'C', 'D' ];

foreach ($c->pairs($items) as $index => $item) {
    echo sprintf(
        "%d: (%s)\n",
        $index,
        implode(', ', $item)
    );
}

配列要素 ‘D’ を追加し、「all()」を「pairs()」に変更します。

▼実行結果

0: (A, B)
1: (A, C)
2: (A, D)
3: (B, C)
4: (B, D)
5: (C, D)

配列要素「A, B, C, D」の全てのペアが表示されました。

アルゴリズムの特性上、ソートは不要なので実装していません。

指定個数の組み合わせ取得

配列要素の内、指定個数の要素の組み合わせを取得できます。

例えば、配列要素が [ ‘A’, ‘B’, ‘C’, ‘D’, ‘E’, ] のように5個あるとします。

指定個数が3のときには、(A, B, C) のように3個の要素の組み合わせのパターンを取得します。

「Pairs.php」をコピーして「OfN.php」を作成し、次のように編集します。

<?php

require_once('../vendor/autoload.php');

use Macocci7\PhpCombination\Combination;

$c = new Combination();
$items = [ 'A', 'B', 'C', 'D', 'E', ];

foreach ($c->ofN($items, 3) as $index => $item) {
    echo sprintf(
        "%d: (%s)\n",
        $index,
        implode(', ', $item)
    );
}

配列要素 ‘E’ を追加し、「pairs()」を「ofN()」に変更し、第二引数に3を指定しています。

▼実行結果

0: (A, B, C)
1: (A, B, D)
2: (A, B, E)
3: (A, C, D)
4: (A, C, E)
5: (A, D, E)
6: (B, C, D)
7: (B, C, E)
8: (B, D, E)
9: (C, D, E)

配列 [ ‘A’, ‘B’, ‘C’, ‘D’, ‘E’, ] の5要素の内、3要素同士の組み合わせが表示されました。

指定範囲個数の組み合わせ取得

例えば、3~4個の要素同士の組み合わせを取得することができます。

「OfN.php」をコピーして「OfA2B.php」を作成し編集します。

<?php

require_once('../vendor/autoload.php');

use Macocci7\PhpCombination\Combination;

$c = new Combination();
$items = [ 'A', 'B', 'C', 'D', 'E', ];

foreach ($c->ofA2B($items, 3, 4) as $index => $item) {
    echo sprintf(
        "%d: (%s)\n",
        $index,
        implode(', ', $item)
    );
}

「ofN()」を「ofA2B()」に変更し、第三引数に4を指定します。

▼実行結果

1: (A, B, C, E)
2: (A, B, C)
3: (A, B, D, E)
4: (A, B, D)
5: (A, B, E)
6: (A, C, D, E)
7: (A, C, D)
8: (A, C, E)
9: (A, D, E)
10: (B, C, D, E)
11: (B, C, D)
12: (B, C, E)
13: (B, D, E)
14: (C, D, E)

配列 [ ‘A’, ‘B’, ‘C’, ‘D’, ‘E’, ] の5要素の内、3~4個の要素同士の組み合わせが表示されました。

「ofA2B()」メソッドの第四引数に「true」を指定することで

文字列ソートされた結果を取得することができます。

$c->ofA2B($items, 3, 4, true)

▼実行結果

0: (A, B, C)
1: (A, B, C, D)
2: (A, B, C, E)
3: (A, B, D)
4: (A, B, D, E)
5: (A, B, E)
6: (A, C, D)
7: (A, C, D, E)
8: (A, C, E)
9: (A, D, E)
10: (B, C, D)
11: (B, C, D, E)
12: (B, C, E)
13: (B, D, E)
14: (C, D, E)

複数配列の要素同士の組み合わせ取得

例えば、アイテム「A, B, C」、色「白, 黒」、個数「1, 2」

の3配列の要素同素の (A, 白, 1) のような組み合わせを取得します。

「BasicUsage.php」をコピーして「FromArrays.php」を作成し編集します。

<?php

require_once('../vendor/autoload.php');

use Macocci7\PhpCombination\Combination;

$c = new Combination();
$items1 = [ 'A', 'B', 'C', ];
$colors = [ '白', '黒' ];
$amounts = [ 1, 2, ];

foreach ($c->fromArrays([ $items1, $colors, $amounts, ]) as $index => $item) {
    echo sprintf(
        "%d: (%s)\n",
        $index,
        implode(', ', $item)
    );
}

配列 $colors, $amounts を追加し、

「all()」を「fromArrays()」に変更し、

引数に配列 [ $items, $colors, $amounts, ] を指定します。

▼実行結果

0: (A, 白, 1)
1: (A, 白, 2)
2: (A, 黒, 1)
3: (A, 黒, 2)
4: (B, 白, 1)
5: (B, 白, 2)
6: (B, 黒, 1)
7: (B, 黒, 2)
8: (C, 白, 1)
9: (C, 白, 2)
10: (C, 黒, 1)
11: (C, 黒, 2)

アイテム「A, B, C」、色「白, 黒」、個数「1, 2」

の3配列の要素同素の組み合わせを取得することができました。

アルゴリズムの特性上、ソートは実装していません。

PHPUnitでの使用例

PHPUnitによるテストで、データプロバイダーに利用することができます。

例として、fromArrays() を使ってみます。

環境準備として、PHPUnitをインストールします。

作業フォルダトップでコマンドを実行します。

composer require --dev phpunit/phpunit:^10.5

examplesフォルダ内のサンプルコードを見てみましょう。

▼テスト対象のクラス「UseInPhpUnit.class.php」

<?php

namespace Macocci7\PhpCombination\Examples;

use Monolog\Level;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;

class UseInPhpUnit
{
    private Logger $log;

    public function __construct()
    {
        $this->log = new Logger('UseInPhpUnit');
        $this->log->pushHandler(
            new StreamHandler(__DIR__ . '/UseInPhpUnit.log', Level::Debug)
        );
    }

    public function order(
        int $productId,
        string $size,
        string $color,
        int $amount
    ) {
        $this->log->info('Adding a new order', [
            'productId' => $productId,
            'size' => $size,
            'color' => $color,
            'amount' => $amount,
        ]);
        return true;
    }
}

▼テストクラス「UseInPhpUnitTest.php」

<?php

declare(strict_types=1);

namespace Macocci7\PhpCombination;

require_once('../vendor/autoload.php');
require_once('./UseInPhpUnit.class.php');

use PHPUnit\Framework\TestCase;
use Macocci7\PhpCombination\Examples\UseInPhpUnit;
use Macocci7\PhpCombination\Combination;

final class UseInPhpUnitTest extends TestCase
{
    public static function provide_order_can_order_correctly(): array
    {
        $products = [ 1101, 1102, ];
        $sizes = [ 'S', 'M', 'L', ];
        $colors = [ 'White', 'Black', ];
        $amount = [ 1, 2, ];
        $c = new Combination();
        $data = [];
        foreach (
            $c->fromArrays([$products, $sizes, $colors, $amount]) as $e
        ) {
            $data[implode(', ', $e)] = $e;
        }
        return $data;
    }

    /**
     * @dataProvider provide_order_can_order_correctly
     */
    public function test_order_can_order_correctly(
        int $productId,
        string $size,
        string $color,
        int $amount
    ): void {
        $u = new UseInPhpUnit();
        $this->assertTrue($u->order(
            $productId,
            $size,
            $color,
            $amount
        ));
    }
}

「UseInPhpUnit.log」を削除します。

rm UseInPhpUnit.log

examplesフォルダ内でPHPUnitを実行してみましょう。

../vendor/bin/phpunit ./UseInPhpUnitTest.php --color=auto --testdox

▼実行結果

PHPUnit 10.5.13 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.2.13

........................                                          24 / 24 (100%)

Time: 00:00.056, Memory: 8.00 MB

Use In Php Unit (Macocci7\PhpCombination\UseInPhpUnit)
 ✔ Order can order correctly with 1101,·S,·White,·1
 ✔ Order can order correctly with 1101,·S,·White,·2
 ✔ Order can order correctly with 1101,·S,·Black,·1
 ✔ Order can order correctly with 1101,·S,·Black,·2
 ✔ Order can order correctly with 1101,·M,·White,·1
 ✔ Order can order correctly with 1101,·M,·White,·2
 ✔ Order can order correctly with 1101,·M,·Black,·1
 ✔ Order can order correctly with 1101,·M,·Black,·2
 ✔ Order can order correctly with 1101,·L,·White,·1
 ✔ Order can order correctly with 1101,·L,·White,·2
 ✔ Order can order correctly with 1101,·L,·Black,·1
 ✔ Order can order correctly with 1101,·L,·Black,·2
 ✔ Order can order correctly with 1102,·S,·White,·1
 ✔ Order can order correctly with 1102,·S,·White,·2
 ✔ Order can order correctly with 1102,·S,·Black,·1
 ✔ Order can order correctly with 1102,·S,·Black,·2
 ✔ Order can order correctly with 1102,·M,·White,·1
 ✔ Order can order correctly with 1102,·M,·White,·2
 ✔ Order can order correctly with 1102,·M,·Black,·1
 ✔ Order can order correctly with 1102,·M,·Black,·2
 ✔ Order can order correctly with 1102,·L,·White,·1
 ✔ Order can order correctly with 1102,·L,·White,·2
 ✔ Order can order correctly with 1102,·L,·Black,·1
 ✔ Order can order correctly with 1102,·L,·Black,·2

OK (24 tests, 24 assertions)

テストケースとして、

        $products = [ 1101, 1102, ];
        $sizes = [ 'S', 'M', 'L', ];
        $colors = [ 'White', 'Black', ];
        $amount = [ 1, 2, ];

の4つの配列それぞれの要素の組み合わせ

2 × 3 × 2 × 2 = 24 パターン全ての組み合わせについて

テストを実施することができました。

エビデンスとしてログ「UseInPhpUnit.log」を見てみましょう。

[2024-03-17T08:01:50.963124+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1101,"size":"S","color":"White","amount":1} []
[2024-03-17T08:01:50.966436+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1101,"size":"S","color":"White","amount":2} []
[2024-03-17T08:01:50.967362+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1101,"size":"S","color":"Black","amount":1} []
[2024-03-17T08:01:50.968215+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1101,"size":"S","color":"Black","amount":2} []
[2024-03-17T08:01:50.969077+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1101,"size":"M","color":"White","amount":1} []
[2024-03-17T08:01:50.969915+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1101,"size":"M","color":"White","amount":2} []
[2024-03-17T08:01:50.970724+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1101,"size":"M","color":"Black","amount":1} []
[2024-03-17T08:01:50.971633+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1101,"size":"M","color":"Black","amount":2} []
[2024-03-17T08:01:50.972856+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1101,"size":"L","color":"White","amount":1} []
[2024-03-17T08:01:50.974738+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1101,"size":"L","color":"White","amount":2} []
[2024-03-17T08:01:50.976568+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1101,"size":"L","color":"Black","amount":1} []
[2024-03-17T08:01:50.977895+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1101,"size":"L","color":"Black","amount":2} []
[2024-03-17T08:01:50.979210+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1102,"size":"S","color":"White","amount":1} []
[2024-03-17T08:01:50.980535+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1102,"size":"S","color":"White","amount":2} []
[2024-03-17T08:01:50.981562+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1102,"size":"S","color":"Black","amount":1} []
[2024-03-17T08:01:50.982502+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1102,"size":"S","color":"Black","amount":2} []
[2024-03-17T08:01:50.984654+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1102,"size":"M","color":"White","amount":1} []
[2024-03-17T08:01:50.987465+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1102,"size":"M","color":"White","amount":2} []
[2024-03-17T08:01:50.988878+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1102,"size":"M","color":"Black","amount":1} []
[2024-03-17T08:01:50.992121+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1102,"size":"M","color":"Black","amount":2} []
[2024-03-17T08:01:50.995148+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1102,"size":"L","color":"White","amount":1} []
[2024-03-17T08:01:50.997991+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1102,"size":"L","color":"White","amount":2} []
[2024-03-17T08:01:51.000360+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1102,"size":"L","color":"Black","amount":1} []
[2024-03-17T08:01:51.003754+00:00] UseInPhpUnit.INFO: Adding a new order {"productId":1102,"size":"L","color":"Black","amount":2} []

しっかり記録されていますね。

配列要素数の上限

以前の記事でも書きましたが、組み合わせ結果として生成されるPHP配列の要素数の上限があります。

配列の各要素は整数インデックスで管理されているため、

PHPで扱える整数の上限値と関わりがあります。

fromArrays() 以外のメソッドについては、

引数にする配列の要素数上限は次の通りです。

  • 32bit-system: 30個
  • 64bit-system: 62個

fromArrays() メソッドについては、

引数にする配列の各要素数の積が上記の上限以下であることが求められます。

例えば、

fromArrays([ $a1, $a2, $a3, ])

のように引数を指定した場合、

$a1の要素数 × $a2の要素数 × $a3の要素数

が上記の上限以下であることが求められます。

Generator版

上記で使用したクラスは全て、返り値が配列ですが、

Generatorオブジェクトを返り値として返すクラスを別に用意しています。

Macocci7\PhpCombination\CombinationGenerator

Generatorの詳細はPHP公式サイトを参照してください。

PHP: ジェネレータ - Manual

Generator版の使用例はexamplesフォルダ内にサンプルコードがあります。

▼「examples/UseCombinationGenerator.php」

<?php

require_once('../vendor/autoload.php');

use Macocci7\PhpCombination\CombinationGenerator;

// Create an Instance
$c = new CombinationGenerator();

// All Items
$items = [ 'A', 'B', 'C', 'D', 'E', ];
echo sprintf("All Items:\n\t(%s)\n\n", implode(", ", $items));

// Common Format
$fmt = "\t(%s)\n";

// All Combinations
echo "All Combinations:\n";
foreach ($c->all($items) as $e) {
    echo sprintf($fmt, implode(', ', $e));
}
echo "\n";

// All Pairs
echo "All Pairs:\n";
foreach ($c->pairs($items) as $e) {
    echo sprintf($fmt, implode(', ', $e));
}
echo "\n";

// All Combinations of $n elements
$n = 3;
echo sprintf("All Combinations of %d elements:\n", $n);
foreach ($c->ofN($items, $n) as $e) {
    echo sprintf($fmt, implode(', ', $e));
}
echo "\n";

// All Combinations of $a to $b elements
$a = 3;
$b = 4;
echo sprintf(
    "All Combinations of %d to %d elements:\n",
    $a,
    $b,
);
foreach ($c->ofA2B($items, $a, $b) as $e) {
    echo sprintf($fmt, implode(', ', $e));
}
echo "\n";

返り値が配列ではなくGeneratorオブジェクトですが、

foreachでループを回すことができます。

配列版の場合、生成される結果配列の要素数が増えると

メモリを消費してしまい、メモリ上限を超えることがあります。

Generator版の場合は、生成される結果の個数が増えても、メモリ上限を超えることはありません。

ただし、Generatorオブジェクトのループは、配列のループよりも格段に遅くなります。

場合によっては数倍の時間が掛かります。

配列版でメモリ上限を超えてしまう場合は、時間が掛かっても問題が無ければGenerator版を使うといった選択が可能です。

Generator版で利用可能はメソッドは基本的に配列版と同じですが、

fromArrays()メソッドはアルゴリズムの特性上、実装されていません。

また、その特性上、ソート機能も実装されていません。

以上です。

コメント

タイトルとURLをコピーしました