条件分岐を状態遷移として解釈し、多段 if 文からプログラマーを救う

 PHP  条件分岐を状態遷移として解釈し、多段 if 文からプログラマーを救う はコメントを受け付けていません
3月 272012
 

多段 if 文はプログラムの可読性を損いバグの温床となるため、多くのプログラマーがそれによって悶え苦しみ、日夜命を落としています。
今日は、そんな憎き多段 if 文をやっつける方法を考えたいと思います。

たとえば、次のような条件分岐があるとします。
flowchart

これを、素直に次のようにコーディングします。

code 1

// $condition1~5 には条件式が入るとします。つまり boolean が入ります。
// $goal に数値を入れているだけですが、実際にはここに具体的な処理を置きます。

$goal = 0;

if ($condition1) {
	if ($condition2) {
		$goal = 1;
	} else {
		if ($condition4) {
			if ($condition5) {
				$goal = 1;
			} else {
				$goal = 2;
			}
		} else {
			$goal = 3;
		}
	}
} else {
	if ($condition3) {
		$goal = 4;
	} else {
		if ($condition4) {
			if ($condition5) {
				$goal = 1;
			} else {
				$goal = 2;
			}
		} else {
			$goal = 3;
		}
	}
}

多段 if 文の下地ができました。これを応用してネストを深くしていくとプログラマーを殺すことができます。
さらにコードの 7 ~ 15 行目と 21 ~ 29 行目が重複しています。
もし、条件4と条件5の間に新しい条件6が挿入される、つまり
・条件4が真のときに条件6の判定を行う
・条件6が真のときに条件5の判定を行う
という仕様変更が発生すると2カ所にまったく同じ修正が必要になり、プログラマーは死にます。

条件分岐を見るとついこのように書きたくなりますが、一寸ここでフローチャートを眺めてみると、だんだんと次のように見えてきます。
state transition diagram

なんと、状態遷移図っぽくなりましたね!
while 文の中で状態を遷移させていき、それぞれの状態を switch-case 文で記述するような感じで、この図の処理を次のようにコーディングしてみましょう。

code 2

$state = 1;
$goal = -1;
while ($goal == -1) {
	switch ($state) {
		case 1:
			if ($condition1) {
				$state = 2;
			} else {
				$state = 3;
			}
			break;
		case 2:
			if ($condition2) {
				$goal = 1;
			} else {
				$state = 4;
			}
			break;
		case 3:
			if ($condition3) {
				$goal = 4;
			} else {
				$state = 4;
			}
			break;
		case 4:
			if ($condition4) {
				$state = 5;
			} else {
				$goal = 3;
			}
			break;
		case 5:
			if ($condition5) {
				$goal = 1;
			} else {
				$goal = 2;
			}
			break;
		default:
			$goal = 0;
			break;
	}
}

それぞれの状態で条件の判定を行い、次に移るべき状態がある場合は $state に対応する値を代入し、ゴールに到達すれば $goal に値を代入してループを抜けます。
行数は増えましたが、シンプルな case 文によって構成されたコードになりネストが浅くなりました。また、条件4の重複もなくなりました。
条件4と条件5の間に条件6が挿入される場合は、$condition4 が true のときに $state に条件6を差す別の値を代入し、それに対応する case を書いてやればOKです。図でいうと矢印の行き先を変更するイメージです。

常にこの方法が適用できるわけではないかと思いますが、上手くいくこともあるんじゃないか、ということで。

 Posted by at 1:56 AM

聖闘士星矢で学ぶ数値添え字の配列の順番についての油断

 PHP  聖闘士星矢で学ぶ数値添え字の配列の順番についての油断 はコメントを受け付けていません
2月 242012
 

すでに要素が格納されている配列に後から添え字 0 の要素を作っても、添え字 0 の要素は先頭の要素にはなりません。
そのため、「その配列から添え字 0 の要素を取り除き」「添え字を 0 から振り直す」ために array_shift を使用すると、意図した結果になりません。

この場合は添え字 0 の要素があれば unset し、なければそのままで array_values を使用すると添え字を 0 から振り直せます。

結論は以上ですが、詳しく見るために以下の要件を想定します。

  • 配列は for 文で処理するため 0 から 1 ずつ添え字が増えていく形で作りたい。
  • 場合によって 0 番目に優先的に入る要素がある。

それってどんなとき?聖闘士星矢で説明してくれ。

  • 4 人の聖闘士が戦っている。
  • ある人物がピンチのときに助けにきて大活躍してくれる聖闘士がいる。
  • 戦いの中で全員がセブンセンシズに目覚める。

ということで、以下のコードを書き、4 人もしくは 5 人の聖闘士をセブンセンシズに目覚めさせてみましょう。

// 聖闘士集合
$saints = array(
    'Pegasus' => 'Seiya', 
    'Dragon' => 'Shiryu', 
    'Cyguns' => 'Hyouga', 
    'Andromeda' => 'Shun'
);
// 聖闘士、闘いに赴く
$fightingSaints = array();
$i = 0;
foreach ($saints as $saint) {
    $fightingSaints[++$i] = $saint;
}
// 瞬がピンチだ
if ($shunIsInAPinch) {
    // 兄貴、颯爽と現れ先頭に躍り出る
    $fightingSaints[0] = 'Ikki';
} else {
    $fightingSaints[0] = null;
    array_shift($fightingSaints);
}
// 聖闘士、セブンセンシズに目覚める
for ($i = 0; $i < count($fightingSaints); $i++) {
    seventhSense($fightingSaints&#91;$i&#93;);
}
&#91;/PHP&#93;

もし $shunIsInAPinch が true の場合は
<ol>
<li>Ikki</li>
<li>Seiya</li>
<li>Shiryu</li>
<li>Hyouga</li>
<li>Shun</li>
</ol>
の 5 人全員がセブンセンシズに目覚めることができます。
じゃあ $shunIsInAPinch が false の場合どうなるか。Ikki が来ないので 4 人がセブンセンシズに目覚めるはずです。ところが…
<ol>
<li>Shiryu</li>
<li>Hyouga</li>
<li>Shun</li>
</ol>
哀れ、Seiya はアナザーディメンションで異次元に飛ばされました。
何が悪かったのでしょうか?
コードの 21 行目の直後で $fightingSaints の中身を見てみましょう。

$shunIsInAPinch が true のとき
[PHP]
Array
(
    [1] => Seiya
    [2] => Shiryu
    [3] => Hyouga
    [4] => Shun
    [0] => Ikki
)

先頭に躍り出たと思われた Ikki は一番後ろに。鳳凰幻魔拳を受けましたね。

$shunIsInAPinch が false のとき

Array
(
    [0] => Shiryu
    [1] => Hyouga
    [2] => Shun
    [3] => 
)

$shunIsInAPinch が false のとき、コードの 19 行目では何が起きているのか?

Array
(
    [1] => Seiya
    [2] => Shiryu
    [3] => Hyouga
    [4] => Shun
    [0] => 
)

添え字 0 の要素を後から作っても、先頭は相変わらず添え字 1 の Seiya で、array_shift によって 先頭の Seiya がアナザーディメンションされてしまう、というわけですね。
19 ~ 20 行目は素直にこう書き換えましょう。

$fightingSaints = array_values($fightingSaints);

これで Ikki が来なくてもみんなセブンセンシズに目覚めることができます。

  1. Seiya
  2. Shiryu
  3. Hyouga
  4. Shun

聖闘士は二度同じ技は受けませんので、このロジックで失敗することはもうありませんね。

 Posted by at 1:13 AM

PHPで機種依存文字判定ツールを作ってみた

 PHP  PHPで機種依存文字判定ツールを作ってみた はコメントを受け付けていません
11月 302011
 

近年はWebで文字化けを見ることがずいぶんと減ったかと思いますが、古いシステムを使っていたり、メールを送信したり、ガラケー向けのWebサイトを作ったりする場合に、機種依存文字(NEC機種依存文字、IBM拡張文字など)に困ることがあります。

ちなみに携帯Twitterでは機種依存文字がガラケーで表示されません。
Twitter / @loglesslove: 濵﨑髙德(はまさきたかのり)さんは携帯Twitterで表示される?

そんなわけで、全部の機種依存文字を判定するにはどうすればいいんだ?と思ってPHPで作ってみました。
機種依存文字判定ツール

判定は文字を SJIS と SJIS-win にそれぞれエンコードして、その結果の文字列を比較して行っています。

以下のクラスのインスタンスを作成して、文字列を与えて処理する形です。

   	class ExternalString
	{
		private $_encoding = 'UTF-8';

		/**
		 * 外字インスタンスの配列
		 * @var array
		 */
		private $_externalCharacters;

		/**
		 * コンストラクタ
		 * 文字列をセットすると外字インスタンスの配列を作る
		 * @param string $string 文字列
		 */
		public function __construct($string = null) {
			$this->_string = $string;
			$this->_externalCharacters = $this->_partStringAndJudgeExternal($string);
		}
		/**
		 * 文字列をセットするついでに外字インスタンスの配列を作る
		 * @param string $string
		 */
		public function setString($string) {
			$this->_externalCharacters = $this->_partStringAndJudgeExternal($string);
		}

		/**
		 * 文字列を文字に分解して外字インスタンスをつくり、その配列として返す
		 * @param string $string
		 */
		private function _partStringAndJudgeExternal($string) {
			$chars = array();
			$length = mb_strlen($string, $this->_encoding);
			for ($i = 0; $i < $length; $i++) {
				$char = mb_substr($string, $i, 1, $this->_encoding);
				$exChar = new ExternalCharacter($char);
				$external = strcmp(mb_convert_encoding($char, 'SJIS', $this->_encoding), mb_convert_encoding($char, 'SJIS-win', $this->_encoding)) !== 0;
				$exChar->setExternal($external);
				$chars[$i] = $exChar;
			}
			return $chars;
		}

		/**
		 * 外字を取り除く
		 * @return array
		 */
		public function filter() {
			return $this->_getEitherChars(false);
		}

		/**
		 * 外字だけ返す
		 * @return array
		 */
		public function pickup() {
			return $this->_getEitherChars(true);
		}

		/**
		 * 外字か通常の文字かのどちらかを配列として返す
		 * @param boolean $external
		 * @return array
		 */

		private function _getEitherChars($external) {
			$chars = array();
			foreach ($this->_externalCharacters as $key => $exChar) {
				if ($exChar->isExternal() === $external) {
					$chars[] = $exChar->getCharacter();
				}
			}
			return $chars;
		}

		/**
		 * 外字に色(のスタイル)を付けて文字列として返す
		 * @param string $color
		 * @return string
		 */
		public function getColoredString($color = '#ffff00') {
			if (preg_match('/^#[0-9a-f]{6}$/', $color) == 0) {
				$color = '#ffff00';
			}
			$chars = array();
			foreach ($this->_externalCharacters as $key => $exChar) {
				$char = htmlspecialchars($exChar->getCharacter());
				if ($exChar->isExternal()) {
					$char = '<span style="background-color: ' . $color . ';">' . $char . '</span>';
				}
				$chars[] = $char;
			}
			$string = implode('', $chars);
			return $string;
		}

	}


	class ExternalCharacter
	{
		/**
		 * 文字
		 * @var string
		 */
		private $_character;
		/**
		 * 外字フラグ
		 * @var boolean
		 */
		private $_external;
		/**
		 * コンストラクタ
		 * @param string $character
		 */
		public function __construct($character = null) {
			$this->_character = $character;
			$this->_external = false;
		}

		/**
		 * 文字setter
		 * @param string $character
		 */
		public function setCharacter($character) {
			$this->_character = $character;
		}

		/**
		 * 外字フラグsetter
		 * @param boolean $external
		 */
		public function setExternal($external) {
			$this->_external = $external;
		}

		/**
		 * 文字getter
		 * @return string
		 */
		public function getCharacter() {
			return $this->_character;
		}

		/**
		 * 外字かどうかを言う
		 * @return boolean
		 */
		public function isExternal() {
			return $this->_external;
		}
	}

あとは以下のようにして文字を処理して取り出しています。


	$xStr = new ExternalString($string); // インスタンス生成
	$coloredString = $xStr->getColoredString(); // 色付き文字列
	$regularString = implode("", $xStr->filter()); // 機種依存文字除く文字
	$externalString = implode("", $xStr->pickup()); // 機種依存文字

※ソース・コメントはもっといい書き方があれば修正すると思います。(文字列で返すか配列で返すかというところもそのうち。)

参考サイト:
Windowsの機種依存文字
PHP で機種依存文字をフィルタリングする関数を作ってみた | ウェブル
floatingdays: PHPの SJISと SJIS-WINの違い
 Posted by at 12:10 AM