Friction River Software

  • お問い合わせ

CakePHP5入門【WebAPI編⑲】ナンバーズ3を予想③

A子

いよいよこのアプリの集大成!

ナンバーズ3の当選番号を予想して、大儲けしていくよ

B美

取らぬ狸の皮算用ね(苦笑)

そんなにうまくいくわけがないじゃない…

C菜

でもでも、元々の目的は『予想の的中』なんですから良いと思いますよ~

それで『予想』の方針は決まってるんですか~?

A子

以前「src/Controller/VerificationController.php」を作ったじゃん
(【WebAPI編⑮】を参照)

あれでスコアと的中結果を比較・検討してみたら何となく法則が分かったんで、こういうアルゴリズムを考えてみたよ

C菜

ぜひ教えてください~

A子

ふっふっふ
刮目せよ!

1.スコアが1以上である数字は、3個から6個のパターンがある
2.3個~4個の場合、的中数字は1個であることが多い
3.5個~6個の場合、的中数字は2個であることが多い
4.5個の場合のみ、下記のように予想する
5.5個の中から2個を選択する(5C2=10通り)
6.スコアゼロである残りの5個と組み合わせて、50通りの予想を作成する
7.その50通りについて、過去の出現時期を調べ、最も古いものから10通りを選出する

これでどうよ(ドヤ顔)

あと、スコアは「引っ張り」のみで算出して、曜日や六曜の最頻値は考慮してないよ
(「VerificationController.php」がそうだったからね)

B美

うーん
悪くは無いと思うんだけど、4番ってどうなの?

予想できない日が多過ぎるかも…

C菜

スコアがゼロでない数字の個数が6個のときも、同様に候補を作りましょう~

6C2=15通りになるので、残り(スコアゼロ)の4個と組み合わせて60通り

どうでしょうか~?

B美

だったら、スコアがゼロでない数字の個数が4個のときって、その反対になるんじゃない?

6C2=15通りになるので、残り(スコアが1以上)の4個と組み合わせて60通り

A子

なるほどね
まとめると、こんな感じかな

パターンスコア組み合わせ作成方法
0である数字の個数1以上である数字の個数
A646C2×4=60通り
B555C2×5=50通り
C466C2×4=60通り

B美

パターンAのみスコアゼロの数字から2個を選び、パターンBパターンCはスコアが1以上の数字から2個を選ぶ…ってことね

良いんじゃないかしら?

C菜

で、具体的な実装はどうするんですか~?

A子

「src/Controller/ApiController.php」の中の「num3Prediction」メソッドを参考にして、「src/Controller/TopController.php」の中に新たなメソッドを作ろう
(誰でも実行できるように…)

1.日時チェック(年末年始も考慮)
2.スコア計算は「連続加点版」アルゴリズムのみ(曜日・六曜加算は行わない)
3.スコアがゼロでない数字の個数が3個以下または7個以上の場合は終了
4.「TopController.php」のprivateメソッド「combination」を使って数字の組み合わせを作成
 4-1.パターンA…スコアゼロの6個の数字から2個を作成
 4-2.パターンB…スコア1以上の5個の数字から2個を作成
 4-3.パターンC…スコア1以上の6個の数字から2個を作成
5.残りの数字と組み合わせる
 5-1.パターンA…15個×4個(スコア1以上)=60個
 5-2.パターンB…10個×5個(スコアゼロ)=50個
 5-3.パターンC…15個×4個(スコアゼロ)=60個
6.候補数字の過去の出現時期を調査し、最も古いものから10通りを画面表示
7.PDFファイルとしての出力もサポート

こんな感じでどうかな?

B美

5番までなら候補数字の多さに比例して的中率も上がりそうだけど、問題は6番ね
(古いものを優先して選ぶ…って考え方)

はたして妥当なのかどうか…

C菜

とりあえずやってみないと判断できないと思いますよ~

1年間くらい実験データを取ってみて、妥当性を検証すれば良いんじゃないでしょうか~?

B美

たしかにそうね(苦笑)

あ、でもそれならコマンドクラスを作って、それをcronクーロン実行したほうが良いんじゃないかしら?
(毎日チェックするのが大変…というか、確認するのを忘れそう(苦笑))

A子

あー、だったら「TopController」内のprivateメソッドである「combination」はコンポーネント化しておく?
(コマンドクラスからも使えるように…)

C菜

良いですね~

それなら同じコンポーネント内に「permutation」メソッドも入れちゃいましょう~

A子

「permutation」は「順列」で、「combination」は「組み合わせ」…だよね?

cd html/numapp[Enter]
bin/cake bake component PermutationAndCombination[Enter]

これで「src/Controller/Component/PermutationAndCombinationComponent.php」ができるはず…
(【WebAPI編④】を参照)

C菜

この中に二つのpublicメソッドを記述していくわけですね~

あ、せっかくなので今回は「static」ではなく、インスタンス化が必要な形にしましょうよ~

A子

OK

/**
* Combination 組み合わせ nCr (r ≦ n)
*
* @param array $arr 元となる配列
* @param int $r 1セットあたりの要素数
* @return null|array
*/
public function combination(array $arr, int $r): ?array
{
    //数値添字配列にする
    $arr = array_values($arr);

    $n = count($arr);
    $result = [];

    //条件不一致
    if ($n < 1 || $n < $r) {
        return null;
    }

    //1個選ぶ場合
    if ($r === 1) {
        foreach ($arr as $item) {
            $result[] = [$item];
        }
    }

    //2個以上
    if ($r > 1) {
        foreach ($arr as $key => $item) {
            $newArr = array_slice($arr, $key + 1);

            //再帰
            $recursion = $this->combination($newArr, $r - 1);

            if ($recursion !== null) {
                foreach ($recursion as $one_set) {
                    array_unshift($one_set, $item);
                    $result[] = $one_set;
                }
            }
        }
    }

    return $result;
}

上記(combination)は前に作ったやつとほとんど同じ(「private」を「public」に変更しただけ)なんだけど、順列(permutation)のほうは下記になるよ

/**
* Permutation 順列 nPr (r ≦ n)
*
* @param array $arr 並べる元となる配列
* @param int $r 並べる1列(1セット)あたりの要素数
* @return null|array
*/
public function permutation(array $arr, int $r): ?array
{
    //数値添字配列にする
    $arr = array_values($arr);

    $n = count($arr);
    $result = [];//最終的に二次元配列にして返す

    //nPr の条件に一致していなければ null を返す
    if ($n < 1 || $n < $r) { return null; }

    if ($r === 1) {
        foreach ($arr as $item) {
            $result[] = [$item];
        }
    }

    if ($r > 1) {
        //$itemが先頭になる順列を算出する
        foreach ($arr as $key => $item) {
            //$itemを除いた配列を作成
            $newArr = array_filter($arr, function($k) use($key) {
                return $k !== $key;
            }, ARRAY_FILTER_USE_KEY);

            //再帰
            $recursion = $this->permutation($newArr, $r - 1);

            foreach ($recursion as $one_set) {
                array_unshift($one_set, $item);
                $result[] = $one_set;
            }
        }
    }

    return $result;
}




C菜

それでは以前書いた「TopController.php」内にある「combination」メソッドについては削除しておきますね~

あと、それに伴い、「initialize」メソッドと「combinationResult」メソッドの修正も~


A子

staticじゃないから「::ダブルコロン」じゃなくて「->アロー」なんだね

$box_set = $this->PermutationAndCombination->combination($digits, 3);

B美

その反応って、まさか…

忘れてたわね(笑)

A子

ぎくっ!
…んなことはいいから、次はコマンドクラスを作ろう

bin/cake bake command num3box[Enter]

これで「src/Command/Num3boxCommand.php」が出来上がるよ

C菜

まずはuse文を記述しますね~

PDFファイルとして出力するので、「mPDF」のuse文も必要です~
あと、DateTimeやConnectionManagerも~

use Cake\Controller\ComponentRegistry;
use App\Controller\Component\PermutationAndCombinationComponent;
use Mpdf\Mpdf;
use Mpdf\HTMLParserMode;
use Mpdf\Config\ConfigVariables;
use Mpdf\Config\FontVariables;
use Cake\I18n\DateTime;
use Cake\Datasource\ConnectionManager;

…で、次は「initialize」メソッド(必要なフィールドも…)ですよ~

class Num3boxCommand extends Command
{
    private $Numbers;
    private $PermutationAndCombination;

    public function initialize(): void
    {
        parent::initialize();

        $this->Numbers = $this->fetchTable('Numbers');
        $this->PermutationAndCombination = new PermutationAndCombinationComponent(new ComponentRegistry());

    }

テーブルのフェッチとコンポーネントのロードですね~

A子

さて、ここから「execute」メソッドの中身を書いていくわけだけど、まずは日時チェックからだね

B美

平日かどうかのチェックと時間のチェックは要らないわよ

だって、cronクーロン実行するんだからね
(月~金の0時30分に実行する…って感じ)

C菜

あ、でも「年末年始」のチェックだけは要るんじゃないですか~?

シェルスクリプトを書いて、そちらで実行判断しても良いですけど~

A子

うーん、どっちが良いんだろう?

B美

コマンドクラスをcronで直接実行するなら「年末年始」チェックは必要ね

とりあえずシェルスクリプト無しを前提に組んでみましょう

A子

分かったよ

んじゃ、とりあえず「ApiController」クラスの中の「num3Prediction」メソッドの中身を「src/Command/Num3boxCommand.php」の「execute」メソッドにコピーしてっと…
このあと、不要な部分を削ったりして修正していこう




C菜

では、「スコアが1以上の数字の配列」と「スコアゼロの数字の配列」を作りましょう~

//各数字のスコアを判定
$scoreMore = []; //スコア1以上
$scoreLess = []; //スコアゼロ
foreach ($record as $idx => $val) {
    if ($val > 0) {
        array_push($scoreMore, $idx);
    } else {
        array_push($scoreLess, $idx);
    }
}

例えば、$recordの中身が[3,5,0,0,4,0,0,3,0,5]だった場合、$scoreMoreは[0,1,4,7,9]、$scoreLessは[2,3,5,6,8]になりますよ~

A子

ふむ、次はさっきのパターンAからCの切り分けだね

//スコア有無の数で処理を切り分ける
$box_set = [];
$scoreMoreCount = count($scoreMore);
$scoreLessCount = count($scoreLess);
if ($scoreMoreCount == 4 && $scoreLessCount == 6) {
    //パターンA
    $combi = $this->PermutationAndCombination->combination($scoreLess, 2);
    foreach ($combi as $row) {
        foreach ($scoreMore as $val) {
            array_push($box_set, [$row[0], $row[1], $val]);
        }
    }
} elseif ($scoreMoreCount == 5 && $scoreLessCount == 5) {
    //パターンB
    $combi = $this->PermutationAndCombination->combination($scoreMore, 2);
    foreach ($combi as $row) {
        foreach ($scoreLess as $val) {
            array_push($box_set, [$row[0], $row[1], $val]);
        }
    }
} elseif ($scoreMoreCount == 6 && $scoreLessCount == 4) {
    //パターンC
    $combi = $this->PermutationAndCombination->combination($scoreMore, 2);
    foreach ($combi as $row) {
        foreach ($scoreLess as $val) {
            array_push($box_set, [$row[0], $row[1], $val]);
        }
    }
}

これで$box_setには要素3個の一次元配列が配列要素として格納されるはず…
(いわゆる「配列の配列」だね)

B美

このあとはボックス値を組み立てるわけだけど、「TopController」の「combinationResult」メソッドの一部をコピペするだけね

//ボックス値の組み立て
$box = [];
if (count($box_set) > 0) {
    foreach ($box_set as $row) {
        sort($row, SORT_NUMERIC);
        $candidate = ($row[0] * 100) + ($row[1] * 10) + ($row[2] * 1);
        array_push($box, sprintf("%03d", $candidate)); //ゼロパディングした3けたの文字列
    }
}

A子

このボックス値にもとづいてデータベース検索を行うわけだけど、どうしようか?

C菜

「AppController」のprotectedメソッドである「doCombination」をこちらに丸ごとコピーしたらどうでしょうか~?

あ、もちろん「protected」は「private」に変更しますけど~


A子

だったら「execute」メソッド内にはこう書けるね

//直近の出現時期と出現頻度をデータベース検索
$result = $this->doCombination($box);

C菜

それでは、あとはPDFファイルにするだけですね~

//フォントディレクトリ(デフォルト)
$defaultConfig = (new ConfigVariables())->getDefaults();
$fontDirs = $defaultConfig['fontDir'];

//フォント(デフォルト)
$defaultFontConfig = (new FontVariables())->getDefaults();
$fontData = $defaultFontConfig['fontdata'];

//mPDFのインスタンス化
$mpdf = new Mpdf([
    'tempDir' => TMP.'mpdf',
    'fontDir' => array_merge($fontDirs, [ROOT.DS.'fonts']),
    'format' => 'A4',
    'orientation' => 'P',
    'fontdata' => $fontData + [
        'ipaexg' => [
            'R' => 'ipaexg.ttf'
        ],
        'ipaexm' => [
            'R' => 'ipaexm.ttf'
        ],
    ],
    'default_font' => 'ipaexg',
]);

「PdfController」の「makepdf」メソッドから(一部を)コピーしただけですよ~

B美

ちょっと待って!

それでは(多分)パーミッションエラーになるわよ

A子

え?
なんでさ

以前のコードと全く同じはずだけど…

B美

前回「numapp/tmp」の下に「mpdf」ディレクトリを作ってオーナーをWebサーバ(www-data)にしたわよね

でも、cronって誰の権限で動かすんだっけ?

C菜

はっ!

ユーザ権限です~
(この開発環境では「bimi」ですね~)

B美

そういうこと
まぁ、「tmp/mpdf」のパーミッションを「757」にしても良いんだけど、セキュリティを考慮して別ディレクトリを作りましょう

cd html/numapp/tmp[Enter]
mkdir mpdf2[Enter]

だから、Mpdfをインスタンス化する際は今作ったこっちを指定します

$mpdf = new Mpdf([
    'tempDir' => TMP.'mpdf2',

…ってことね

A子

なるほどねぇ

んじゃ、あとはヘッダーとコンテンツを作ってPDF化するだけだね

C菜

それは任せてくださいです~

//ヘッダー
$css =<<<EOF
table {
    border-collapse: collapse;
    width: 80%;
    margin: 0 auto;
    font-size: 9px;
}

table th{
    border:1px solid black;
    text-align: center;
    background-color: #afffaf;
    line-height: 0;
}

table td{
    border:1px solid black;
    text-align: center;
    line-height: 0;
}

.top10{
    background-color: yellow;
}
EOF;

//コンテンツ
$content = '';
if (count($result) == 0) {
    $content .=<<<EOF
    <div style="width: 100%;text-align: center;">
        <h3>ボックス値(予想)</h3>
        【{$year}年{$month}月{$day}日】
        <br /><br /><br />
        <span style="color: red;">スコアの状態が予想可能な範囲ではありません。</span>
    EOF;
} else {
    $content .=<<<EOF
    <div style="width: 100%;text-align: center;">
        <h3>ボックス値(予想)一覧</h3>
        【{$year}年{$month}月{$day}日】
        <br />
        <table>
            <tbody>
                <tr>
                    <th style="width: 50px;">順位</th>
                    <th style="width: 100px;">ボックス値</th>
                    <th style="width: 200px;">直近の出現時期</th>
                    <th style="width: 100px;">直近の当選番号<br />(ストレート)</th>
                    <th style="width: 100px;">このボックス値<br />の出現回数</th>
                </tr>
    EOF;

    foreach ($result as $idx => $val) {
        $rank = $idx + 1;
        if ($rank <=10) {
            $content .= "<tr><td class='top10'>{$rank}</td><td class='top10'>{$val[0]}</td><td class='top10'>{$val[1]}(第{$val[2]}回)</td><td class='top10'>{$val[3]}</td><td class='top10'>{$val[4]}回</td></tr>";
        } else {
            $content .= "<tr><td>{$rank}</td><td>{$val[0]}</td><td>{$val[1]}(第{$val[2]}回)</td><td>{$val[3]}</td><td>{$val[4]}回</td></tr>";
        }
    }
    $content .=<<<EOF
            </tbody>
        </table>
    </div>
    EOF;
}

(別ファイルとして)HTMLファイルを作れば(ブラウザ上で)表示確認できるのが良いですよね~

A子

次は、これをPDF化するんだったよね?

//PDFへの書き出し
$mpdf->WriteHTML($css, HTMLParserMode::HEADER_CSS);
$mpdf->WriteHTML($content, HTMLParserMode::HTML_BODY);

B美

前回はこの結果(PDFデータ)をブラウザに表示するようにレスポンスしたわけだけど、今回はファイルに保存するわよ

//格納ディレクトリ
$dir = WWW_ROOT."pdf".DS;
if (!is_dir($dir)) {
    mkdir($dir, 0755, true);
}

//ファイル名の決定と保存
$fileName = $dir.'numbers3box_'.date('Ymd').'.pdf';
$mpdf->Output($fileName, 'F');

ファイル保存のやり方は超簡単!
「Output」メソッドの第一引数に「ファイル名」、第二引数に「F」を渡すだけ…

C菜

「numapp/webroot」の下に「pdf」というディレクトリが(自動的に)作られて、そこに「numbers3box_年月日.pdf」というファイル名で保存されるわけですね~

あと、「execute」メソッドの全体像をこの下に表示しておきましょう~










B美

うん、良い感じね
(コード量がかなり多くなっちゃったけど…(苦笑))

それじゃ、実行確認してみましょうか
MATE端末」を開いて、次のコマンドを打ち込んでみてね

cd html/numapp[Enter]
bin/cake num3box[Enter]

A子

エラーが出なかったってことは成功だね

んじゃ、「numapp/webroot/pdf」ディレクトリを確認してみよう



C菜

完璧です~

当たるかどうかは別として…(笑)

B美

あ、一つ注意しておくけど…

このアルゴリズムでは、ダブルやトリプルが当選番号になったときって、100%はずれるからね
(「112」や「595」、「777」等)

A子

だねー

あくまでも三つの数字が全て異なることが前提だから、それは仕方ない…

C菜

それではこれを本番環境(VPSサーバ)に設置デプロイして、cronクーロン設定も行っておきますね~
(月~金の午前0時30分に実行しますよ~)

数ヶ月後の結果が楽しみです~

A子

一応、シェルから直接実行して、動作確認しておこう

あれ?
エラーかな?

B美

エラーではないわ

非推奨のメソッドを使ってるから、警告が出ただけよ
(「order」ではなく「orderBy」を使うべき…ってこと)

C菜

もしかして、これまでもずっと「error.log」にこの警告が吐き出されていたのでしょうか~?

B美

それは知らない…(苦笑)
ま、とにかくこの警告を止めたければ「config/app.php」のエラーレベルを書き換えるか、非推奨のメソッドを使うのをやめるかなんだけど…

面倒だからエラーレベルを書き換えましょう
(下記の赤線箇所を追加しただけ…)

A子

手抜きね(笑)

C菜

…っぽいです~

B美

うっ、うるさい!

面倒くさいのよ(苦笑)