gorogoroyasu

福岡の開発会社で働いている。

IteratorをGeneratorと関数内static変数で実装する

Excel のカラムは、 A から始まって Z までいくと次は AA となり、 AZ、BA、BZ・・・と続いていきます。

以前行った案件でExcel出力を行う際に github.com

を使わせていただいていましたが、

        'col' => 'B',

を指定するのがかなり大変でした。

というのも、カラムが AX ぐらいまであり、今回の改修で、そのうちのいくつかの項目を削ることになったのです。

つまり、

        'col' => 'D',

という値を消すと、以降の

        'col' => 'E',
        'col' => 'F',
        'col' => 'G',

をすべて

        'col' => 'D',
        'col' => 'E',
        'col' => 'F',

に書き換えなくてはなりません。
そして、今回、機能追加を行うことになりました。
そんなことやってられるかー (怒)

ということで、2つ対策を考えてみました。

俗に言う、Enum ってやつなのかな?
イメージとしては、Golang のiota 的なやつ。

package main

import (
    "fmt"
)

const (
    a = iota
    b
    c
    d
)
func main() {
    fmt.Println(a, b, c, d)
        # => 0, 1, 2, 3
}

今回は、 Excel のカラムに沿った値を返すところが違います。

Generator を使う解決策

class GenColumn
{
    public function __construct()
    {
        $this->gen = $this->excelColumnGenerator();
    }

    public function getCurrent()
    {
        return $this->gen->current();
    }

    public function getNext()
    {
        $this->gen->next();
        return $this->gen->current();
    }

    private function excelColumnGenerator()
    {
        $ary = [];
        for ($a="A"; $a != "AZ"; $a++) {
            $ary[] = $a;
        }
        foreach($ary as $column) {
            yield $column;
        }
    }

}

$column = new GenColumn();
print($column->getCurrent());
print($column->getNext());
print($column->getCurrent());
print($column->getNext());
print($column->getCurrent());

getCurrent() というメソッドを追加して、現在の値を取得することができるようにしています。

これを 関数内static変数を使うバージョンで書き直すと、

関数内static変数を使う解決策

class GenColumn
{
    private static $ary = [];
    
    private static function genAry()
    {
        $ary = [];
        for ($a="A"; $a != "AZ"; $a++) {
            $ary[] = $a;
        }
        self::$ary = $ary;
    }

    public function getColumnName($current=false)
    {
        static $a=1;
        if (empty(self::$ary)) {
            self::genAry();
        }
        if ($current) {
            return self::$ary[$a - 1];
        }
        
        $a++;
        return self::$ary[$a - 1];
    }
}
$column = new GenColumn();
print($column->getColumnName(true));
print($column->getColumnName(false));
print($column->getColumnName(true));
print($column->getColumnName(false));
print($column->getColumnName(true));

という風に書けます。

両者に一長一短があります。

ちなみに、10000回ループした際の実行速度(5回の平均)は

Generator: 0.089s 関数内static変数: 0.023s

と、関数内static変数の方で実装したほうが4倍ほど早かったです。

メリットとデメリット

Generator を使う解決策は、コードが長くなりますが、 - $column->getCurrent() - $column->getNext() と、行う操作にわかりやすい名前をつけることができます。

一方、コードが長くなり、実行に時間がかかります。

関数内static変数 を使う解決策は、コードは短く実行時間も早いです。 ただし、現在の値とインクリメントした値を取得する際に、 $column->getColumnName($current) という風に、$current の true/false で取れる値が変わります。 可読性は決して高くないです。

まとめ

結局、2つの実装方法を検討しましたが、より安全性が高い Generator で実装を行いました。 ただ、 関数内static変数 の使い方はあまり良く知らなかったので、調べてみてよかったです。