HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

「プロセスが使用中のファイルを調べる」で遊んでみた

ふと、何か(何)を見ていて、その関連で以下の記事をざっと読んで、ちょっと遊んでみたメモ(謎)。

なんで「PHP」というタグまで付いているかは読めば分かる(ぇ‥

事前準備

遊ぶためのCentOS 6環境にはlsofコマンドが無かったので、以下の手順で導入。

$ su -
# yum -y install lsof
# exit
$ 

本題

単にやってみたかっただけなので、PHPで以下のプログラムを書いてみた。

<?php

$fp1 = fopen(__FILE__, "r");
exec('lsof ' . __FILE__, $a1);
print_r($a1);

$fp2 = fopen(__FILE__, "a");
exec('lsof ' . __FILE__, $a2);
print_r($a2);

fclose($fp1);
fclose($fp2);

単に、PHPスクリプトファイル自身をfopenして、lsofを実行して‥というのを「"r"」「"a"」の2つのオープンモードで次々に開いて試しているだけ。(「"w"」モードで開いたらスクリプトファイルの内容が消えちゃうからね(謎))

で、これをhoge.phpに保存して、実行してみる。

$ php hoge.php
Array
(
    [0] => COMMAND  PID     USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
    [1] => php     3616 hhelibex    3r   REG  253,0      185 163270 /home/hhelibex/work/2015-0602-01/hoge.php
)
Array
(
    [0] => COMMAND  PID     USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
    [1] => php     3616 hhelibex    3r   REG  253,0      185 163270 /home/hhelibex/work/2015-0602-01/hoge.php
    [2] => php     3616 hhelibex    4r   REG  253,0      185 163270 /home/hhelibex/work/2015-0602-01/hoge.php
)
$ 

できたできた。それだけ(謎)。

蛇足

別のUbuntu 14.04 LTS環境では、何もしなくてもlsofコマンドが使えた。
その環境で必要なパッケージ等は入れているので、初期状態(minimal)で使えるかどうかは分からないが(ぇ‥

issetの罠(emptyの罠でもある)

PHPで(知らずに)以下のようなコードを書いていてはまったのでメモ。
実にくだらない話なんだが‥

<?php
$a = array(
    'hoge' => 'HOGEHOGE',
    'uga' => array(
        'text' => 'UGAUGA',
        'shortText' => 'UGA',
    ),
);

var_dump(PHP_VERSION);
$textList = array();
foreach (array('hoge', 'uga') as $key) {
    if (isset($a[$key]['text'])) {
        $textList[$key] = $a[$key]['text'];
    } else if (isset($a[$key])) {
        $textList[$key] = $a[$key];
    } else {
        $textList[$key] = $key;
    }
}
var_dump($textList);

ちなみに、実際のコードでは、Zend FrameworkでZend_Config_Iniを使って.iniファイルから読み込んだデータをPHPの配列に(toArray()で)変換している。それが上記コードの配列「$a」。
で、期待に胸を躍らせて(違)実行するわけだが‥

string(5) "5.3.3"
array(2) {
  ["hoge"]=>
  string(1) "H"
  ["uga"]=>
  string(6) "UGAUGA"
}

何かがおかしい‥「"hoge"」に対する値が「"H"」ってなんだ‥

というわけでマニュアルを見ると、書いてある‥


Example #2 isset() on String Offsets

PHP 5.4 changes how isset() behaves when passed string offsets.

<?php
$expected_array_got_string = 'somestring';
var_dump(isset($expected_array_got_string['some_key']));
var_dump(isset($expected_array_got_string[0]));
var_dump(isset($expected_array_got_string['0']));
var_dump(isset($expected_array_got_string[0.5]));
var_dump(isset($expected_array_got_string['0.5']));
var_dump(isset($expected_array_got_string['0 Mostel']));
?>

Output of the above example in PHP 5.3:

bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)

Output of the above example in PHP 5.4:

bool(false)
bool(true)
bool(true)
bool(true)
bool(false)
bool(false)

つまり、「$a['hoge']」がstringなので、PHP 5.3までは「$a['hoge']['text']」は「$a['hoge'][0]」と等価とみなされ、文字列"HOGEHOGE"の先頭の文字が返されるというわけである。実に傍迷惑である。
まぁ、「$a['hoge']」が配列だと期待して書くコードの場合、チェックするキーが「'0'」だとPHP 5.4以降も数値の「0」と等価とみなされるので、その可能性を確実に排除するためには以下のようなコードにしておくのが安全。さらに、まず「$a['hoge']」が配列かどうかをチェックする際に「$aが配列でかつキー『'hoge'』が存在するか」というチェックもしておかないとNoticeが出る可能性があるので、以下のような感じになる。

<?php
$a = array(
    'hoge' => 'HOGEHOGE',
    'uga' => array(
        'text' => 'UGAUGA',
        'shortText' => 'UGA',
    ),
);

var_dump(PHP_VERSION);
$textList = array();
foreach (array('hoge', 'uga') as $key) {
    if (is_array($a) && isset($a[$key])) {
        if (is_array($a[$key]) && isset($a[$key]['text'])) {
            $textList[$key] = $a[$key]['text'];
        } else {
            $textList[$key] = $a[$key];
        }
    } else {
        $textList[$key] = $key;
    }
}
var_dump($textList);

これで期待する結果が得られる。

string(5) "5.3.3"
array(2) {
  ["hoge"]=>
  string(8) "HOGEHOGE"
  ["uga"]=>
  string(6) "UGAUGA"
}

ついでに言うと、emptyにも同じ罠が仕掛けられている。

なお、issetの代わりにarray_key_existsを使うという方法もあるが‥

$textList = array();
foreach (array('hoge', 'uga') as $key) {
    if (is_array($a) && array_key_exists($key, $a)) {
        if (is_array($a[$key]) && array_key_exists('text', $a[$key])) {
            $textList[$key] = $a[$key]['text'];
        } else {
            $textList[$key] = $a[$key];
        }
    } else {
        $textList[$key] = $key;
    }
}
var_dump($textList);

結局は配列かどうかのチェックをしないとNoticeが出るので、よほどの事情(値がNULLの場合の評価結果を真としたい等)がない限り、大して変わらない。

まぁ、Zend Frameworkを使っているなら、Zend_Config_Iniオブジェクトをそのまま使えばいいじゃんって言われたら身も蓋もない‥

しかし、ガチのシステム開発スクリプト言語を使えば使うほどスクリプト言語が嫌いになっていくのは、データ型の曖昧性の許容が(自分の中での)主要因のような気がする‥

10分ベスト10

書くことが無いからというわけではないんだが、何か情報技術的なことを書かなきゃっていう呪縛にとらわれていた自分に気づいて、「考えてみたら、このブログは日々の記憶の記録じゃん」って思い出した(謎)。

ふとした思い付きで、3日前に「10分ベスト10」とか称して「自分の」「今の」「好きな曲ベスト10を」「10分間で」作ってみようと思って列挙したものをいまさら書き起こしているという‥言ってしまえば単なる自己満足記事(ぇ‥
3日前のものだけど、列挙は本当に10分間でやったものそのまま。
意外にパッと出てこなくて、比較的最近の曲と結構昔の曲が半々くらいになった上に、順位を付け難いって感じになってしまったので、順不同で挙げてみる。

(なお、以下では敬称略していますがご了承のほどよろしくお頼み申す(誰))

比較的最近の曲を5曲

リリース時期など含めて順不同だが、以下のものが挙がってきた。

心のカ・タ・チ(家入 レオ)

シングル「Bless You」のカップリング曲。

Bless You (初回限定盤A)

Bless You (初回限定盤A)

Bless You(通常盤)

Bless You(通常盤)

限定盤と通常盤の違いはDVDが付いているかいないか。
後発のシングルにライブ版の演奏が入っていたりするけど、アルバムに収録されたのは、最近発売されたばかりの「20」。
20 (初回限定盤)

20 (初回限定盤)

20 (通常盤)

20 (通常盤)

ただ「心のカ・タ・チ〜Another Story〜」とあるので、どんな形で入っているかは未確認(買ったのにまだ聴いてないやつ‥)。

家入レオつながりで出てくるそのほかのお気に入り曲は「Shine」と「希望の地球」。いずれもアップテンポな曲で、音楽を聴いて気を晴らしたいときによく聴いてる。

  • Shine(家入 レオ)

シングルおよび後発のアルバムに収録されている。いずれも限定盤と通常盤があるのでお好みで(謎)。
Shine(初回限定盤)Shine
LEO(初回限定盤)LEO(通常盤)

  • 希望の地球(家入 レオ)

2ndアルバムへの収録曲。同じく限定盤と通常盤があるのでお好みで(謎)。
a boy(初回限定盤)a boy(通常盤)
一瞬「奇跡の地球」(何)とタイトルを間違えそうになるのは内緒(謎)‥

全体的に限定盤+通常盤(しかも通常盤にのみ収録というのもある)という売り方をするのでレコード会社気に入らないって思ったりするのは内緒

未来(Kalafina)

実は私自身がKalafinaの存在を知ったのがごく最近というにわかファンなので、過去の曲なのか最近の曲なのかは把握していないという(ぇ‥
もう少し正確に言うと、名前をちらほら見かけるようになってどうしようかなぁって悩んでいたところにベスト盤の発売という話が飛び込んできてすぐに飛びついて聴き始めたというところ‥

THE BEST “Red

THE BEST “Red"

THE BEST “Blue

THE BEST “Blue"

「未来」が収録されているのは後者のBlueの方。アルバムを一通り聴いた後にすごく耳に残って、それ以来、ことあるごとに無限ループしている曲の1つ。

併せて無限ループしているのが「misterioso」。こちらは前者のRedに収録。アルバムの先頭の方に収録されていたというのもあると思うが、すごく耳に残っている曲。

約束(FictionJunction)

梶浦由紀つながりでずっと気になっていたけど手を出せずにいたFictionJunction。最近アルバムがリリースされたので、思わず購入した1枚。その収録曲。

elemental

elemental

他にどの曲がいいとかは挙がってこないんだけど(酷)、長めの外出なんかの時にはよく全体を通して聴いてる。

なないろスコア(霜月 はるか)

存在を知ったのがごく最近、とあるゲームの主題歌になっている曲の収録アルバムを買ったら、その曲よりもお気に入りになったという1曲。

なないろスコア (初回生産限定盤)

なないろスコア (初回生産限定盤)

なないろスコア

なないろスコア

私は「ぬいぐるみはいいかなぁ‥」と思って通常盤にしたけど、限定盤もまだ在庫があるっぽいので、欲しい人は是非(ぇ‥

もともとの購入目的だった「Startup」ももちろんよいですよ。

Your song*(Yun*chi)

NHKアニメ「ログ・ホライズン」のエンディングでその存在を知り、CD大人買いしたほどの(ry
「Your song*」はシングルとしてリリースされた後、後発の2枚のアルバムに収録されている。

Your song*

Your song*

Asterisk*

Asterisk*

Starlight*

Starlight*

「Starlight*」はミニアルバムだが、ここでの収録は別Remix。

同じくNHKアニメ「ログ・ホライズン」2ndシーズンのエンディング。

Wonderful Wonder World *

Wonderful Wonder World *

余談だけど、この曲が流れるエンディングでのアカツキ(誰)がかわいくて仕方ないのです(謎)。

さらに話は逸れるけど、「アニ*ゆん〜anime song cover〜」をどうしようか悩んでいるのは秘密(謎)‥

アニ*ゆん~anime song cover~

アニ*ゆん~anime song cover~

結構昔の曲を5曲

心を開いて(ZARD)

ZARDの曲は挙げ出すとキリがなくなるんだけど、まず真っ先に出てくるのがこの曲。どれだけ無限ループしたことか‥

心を開いて

心を開いて

やはりというか何というか、後発のアルバムへの収録が非常に多い。
TODAY IS ANOTHER DAY

TODAY IS ANOTHER DAY

ZARD BLEND?SUN&STONE

ZARD BLEND?SUN&STONE

BEST The Single Collection~軌跡~

BEST The Single Collection~軌跡~

  • Don't you see!(ZARD)

アニメ「ドラゴンボールGT」エンディングということで覚えているというのもあるが、ノリがよくてとても好きな曲。

Don’t you see!

Don’t you see!

こちらも、後発のアルバムへの収録が多い。
ZARD BLEND?SUN&STONE

ZARD BLEND?SUN&STONE

ZARD BEST?Request Memorial?

ZARD BEST?Request Memorial?

  • きっと忘れない(ZARD)

これはカラオケで歌うようになってからよく聴くようになったという逆パターン(謎)。

きっと忘れない

きっと忘れない

OH MY LOVE

OH MY LOVE

BEST The Single Collection~軌跡~

BEST The Single Collection~軌跡~

遠いティンパニ(See-Saw)

See-Sawの存在を知ったのが割と最近でいろいろ手遅れ(謎)という中で手にしたアルバム「See-Saw Early Best」に収録されている曲。

I HAVE A DREAM

I HAVE A DREAM

See-Saw Early Best

See-Saw Early Best

See-Saw Early Best」に入っている「素顔〜ノーメイク〜」もお気に入り。
‥なのだが、CD自体、通常の価格での入手が困難になっている感じ‥

今知る限りではJOYSOUND系でしか歌えないのがちょっと残念(謎)‥

一万メートルの景色(小松 未歩)

妙に印象に残っていて、いまだにお気に入りの上位に挙がってくる曲。カップリング曲なんだけどな‥

anybody’s game

anybody’s game

2nd?未来

2nd?未来

  • at him!(小松 未歩)

カラオケで歌うようになってから引っ張り出してよく聴くようになった曲。

小松未歩4?A thousand feelings?

小松未歩4?A thousand feelings?

夢ノート(azusa)

アニメ「もしドラ」のオープニング。一時期こればっかり聴いていた‥

TVアニメ「もしドラ」OPテーマ 夢ノート(特別盤)

TVアニメ「もしドラ」OPテーマ 夢ノート(特別盤)

TVアニメ「もしドラ」OPテーマ 夢ノート(通常盤)

TVアニメ「もしドラ」OPテーマ 夢ノート(通常盤)

特別盤には「Brass Band ver.」や「Band ver.」が入っていて結構楽しい。

会いたくて(石井 聖子)

岡本真夜プロデュースの、知る人ぞ知る(酷)歌手。

会いたくて・・・

会いたくて・・・

ANGELOPHANY

ANGELOPHANY

石井聖子 ベスト・コレクション

石井聖子 ベスト・コレクション

岡本真夜とのコーラスが絶妙。

  • ANNIVERSARY(石井 聖子)

石井聖子のデビューシングル。

ANNIVERSARY

ANNIVERSARY

後発のアルバム2枚にも収録されている(「会いたくて…」と同じ)。

  • スーパーマーケットのある街に住みたい(石井 聖子)

デビューからのシングル2枚が岡本真夜の提供だったが、この曲には彼女の「らしさ」が現れている感じがして、デビューアルバムの中でも結構好きな曲。

ANGELOPHANY

ANGELOPHANY

石井聖子 ベスト・コレクション

石井聖子 ベスト・コレクション

蛇足

もちろん、10曲という制限からはじき出された曲はたくさんありますよ、えぇ‥

いろんな理由(何)ではじき出された曲たち‥とくに深い意味・意図はないけど‥

東京VICTORY(サザンオールスターズ)
I'm Only Me When I'm With You(Taylor Swift)
あなたは 私の ENERGY(宇徳 敬子)
世界で一番 遠い場所(渡辺 美里)
On Your Mark(CHAGE&ASKA)

自動採番の列が存在するテーブルへのデータロード

バックアップからのリストアではなく、あるDBサーバーからデータをエクスポートして、別のDBサーバーでインポートするという場合に、邪魔になってくるのが自動採番するように定義した列の存在。
DB2で言えば「GENERATED ALWAYS AS IDENTITY」が列定義に付いているような場合。

そのような場合に、どういう手順で移行すればいいのかを忘れないためのメモ。

前提

以下のテーブルとデータが移行元のDBに存在するとする。
(便宜上、複数SQL文の区切り文字を「;」として記述する)

CREATE TABLE person(
      id INTEGER GENERATED ALWAYS AS IDENTITY
    , name VARCHAR(32) NOT NULL
    , CONSTRAINT person_pkey PRIMARY KEY(id)
);

CREATE TABLE phone(
      id INTEGER NOT NULL
    , phone_number VARCHAR(16) NOT NULL
    , CONSTRAINT phone_fkey FOREIGN KEY(id) REFERENCES person(id) ON DELETE CASCADE
);

INSERT INTO person(name) VALUES('John');
INSERT INTO phone(id, phone_number) VALUES(IDENTITY_VAL_LOCAL(), '012-345-6789');

INSERT INTO person(name) VALUES('ken');
DELETE FROM person WHERE name = 'ken';

INSERT INTO person(name) VALUES('Ken');
INSERT INTO phone(id, phone_number) VALUES(IDENTITY_VAL_LOCAL(), '012-987-6543');
INSERT INTO phone(id, phone_number) VALUES(IDENTITY_VAL_LOCAL(), '098-765-4321');

(NG)何も考えずにexport&load(1)

上記のCREATE TABLE文を、そのまま移行先のDBサーバーで実行し、以下のようにしてデータを移行しようとする。

<移行元>

db2 "EXPORT TO person.del OF DEL SELECT * FROM person"
db2 "EXPORT TO phone.del OF DEL SELECT * FROM phone"

<移行先>

db2 "LOAD FROM person.del OF DEL INSERT INTO person"
db2 "LOAD FROM phone.del OF DEL INSERT INTO phone"

そうすると、personテーブルのロードで以下のような警告が出て、ロードができない。

SQL3550W  The field value in row "1" and column "1" is not NULL, but the 
target column has been defined as GENERATED ALWAYS.

まぁ、システム(DBサーバー)側で自動生成する値だと定義しているのに、手動で値を入れることなど普通はできない。

(NG)何も考えずにexport&load(2)

それならば、と、自動採番される列以外の値をエクスポートすれば‥結果は分かりきっているが、一応‥

<移行元>

db2 "EXPORT TO person.del OF DEL SELECT name FROM person"
db2 "EXPORT TO phone.del OF DEL SELECT * FROM phone"

<移行先>

db2 "LOAD FROM person.del OF DEL INSERT INTO person(name)"
db2 "LOAD FROM phone.del OF DEL INSERT INTO phone"
db2 "SET INTEGRITY FOR phone IMMEDIATE CHECKED"

LOAD文では、参照制約などのチェックがされないので、SET INTEGRITY文でそのチェックを実行してやる必要がある。
で、そうすると、以下のようなnoticeが出る。

DB21034E  The command was processed as an SQL statement because it was not a 
valid Command Line Processor command.  During SQL processing it returned:
SQL3603N  Integrity processing through the SET INTEGRITY statement has found 
an integrity violation involving a constraint, a unique index, a generated 
column, or an index over an XML column. The associated object is identified by 
"DB2INST1.PHONE.PHONE_FKEY".  SQLSTATE=23514

要は、外部キー制約に違反しているということ。当然である。
(試しに、ファイルphone.delの中身と、コマンド「db2 "SELECT * FROM person"」の実行結果を見比べてみるとよく分かる。)

(OK)LOAD文でIDENTITYOVERRIDE修飾子を使ってみる

IBMdeveloperWorksで、LOAD文にIDENTITYOVERRIDEを指定する方法がある、というのを見つけた。

LOADユーティリティー

DB2 LOADユーティリティーは、IDENTITY列に関連して3つのファイル・タイプ修飾子をサポートしています。ロード・ユーティリティーは、identityignoreとidentitymissingのほか、identityoverride修飾子を受け入れます。
identityoverride修飾子は、GENERATED ALWAYS IDENTITY列のあるテーブルにデータをロードするときに、入力ファイルのIDENTITY値を使用するように指定します。この修飾子が指定されていると、IDENTITY列で値のない(またはnull値の)行は拒否されます。IDENTITY列がプライマリキーでないとき、またはIDENTITY列に一意のインデックスが定義されていないとき、この修飾子を使用するとGENERATED ALWAYS列の一意性プロパティーに違反する可能性があります。

これを使うと、以下のような手順になる。

<移行元>

db2 "EXPORT TO person.del OF DEL SELECT * FROM person"
db2 "EXPORT TO phone.del OF DEL SELECT * FROM phone"

<移行先>

db2 "LOAD FROM person.del OF DEL MODIFIED BY IDENTITYOVERRIDE INSERT INTO person"
db2 "LOAD FROM phone.del OF DEL INSERT INTO phone"
db2 "SET INTEGRITY FOR phone IMMEDIATE CHECKED"

基本的にはこれで問題ないのだが、注意点が2つある。

注意点:その1

ロードを実行した後、その接続を維持したままpersonテーブルに新たな行を挿入しようとすると、以下のようなエラーが発生することがある。(personテーブルへの挿入を行わないで接続の切断&再接続を行っても同様)

DB21034E  The command was processed as an SQL statement because it was not a 
valid Command Line Processor command.  During SQL processing it returned:
SQL0803N  One or more values in the INSERT statement, UPDATE statement, or 
foreign key update caused by a DELETE statement are not valid because the 
primary key, unique constraint or unique index identified by "1" constrains 
table "DB2INST1.PERSON" from having duplicate values for the index key.  
SQLSTATE=23505

これは、原因は分からないが、採番される値がテーブル作成時の初期値(ここでは未指定なので「1」になる)をpersonテーブルのid列の値として挿入しようとして、主キー制約違反になるためである。(IDENTITY_VAL_LOCAL()は「3」を返すのに‥)

これを解決するための方法を探っていたが、ギブアップして、以下のサイトで答えを見つけた。

要は(今回の例の場合)、以下のSQL文を実行すればよいらしい。

ALTER TABLE person ALTER id RESTART WITH 4
注意点:その2

上記で基本的には問題ないのだが、接続を一旦切って再接続し、personテーブルにデータを挿入すると、idの値が「24」などとなる。

これは、「GENERATED ALWAYS AS IDENTITY」の後ろに隠れたデフォルト値が関係している。
指定可能なものの中に「CACHE」があるが、これのデフォルト値が「20」となっていて、DBサーバーに接続したときに「CACHE」で指定した数だけ、IDENTITY値をキャッシュするらしい。
本当に連番を振りたいという場合は、理屈上は「GENERATED ALWAYS AS IDENTITY (NO CACHE)」と指定すればいいのだが、パフォーマンスに何らかの影響が出ることは考えられる。

(OK)「GENERATED ALWAYS」を一旦外す

解決案として最初に思いついたのがこれなのだが、IDENTITYOVERRIDE修飾子を見つけてしまったので、真面目に書く気がなくなってしまった(ぉ‥

要は、<移行先>で、

CREATE TABLE person(
      id INTEGER NOT NULL
    , name VARCHAR(32) NOT NULL
    , CONSTRAINT person_pkey PRIMARY KEY(id)
);

して、

db2 "LOAD FROM person.del OF DEL INSERT INTO person"
db2 "LOAD FROM phone.del OF DEL INSERT INTO phone"
db2 "SET INTEGRITY FOR phone IMMEDIATE CHECKED"

して、

ALTER TABLE person ALTER COLUMN id SET GENERATED ALWAYS AS IDENTITY ( START WITH 4 );

とするだけのこと。
personテーブルのid列は主キーとなっているため、CREATE TABLE時に「NOT NULL」を代わりに入れるのを忘れてはいけない。
また、採番の開始値の設定をALTER TABLE時に行っている。

crontabで実行時刻を適当に割り振るときの一案

どちらかというと、単なるネタ記事(何)。

cronで実行する処理で、以下の要件を満たすような場合に自分が使っている実行時刻の決め方の一つ。

  • 1日1回実行されればよい
  • 似たような処理が複数あるが、随時増えていく
    • (同じコマンドのパラメータ違いを別々にcrontabのエントリにするとか)
  • 実行時刻は問わないので、負荷分散のために適当にばらけさせたい

ただ単に「時」「分」を整然と並べても面白くないので、以下のように生成している。

  • 「時」は0から23まで順番に
  • 「分」は素数を順に使用(0から59までの間に17個ある)

せっかくなので(謎)、それを生成するPHPプログラムを書いてみた。

<?php
$h = range(0, 23, 1);
$m = array();
// 素数列を生成
for($p = 1; ($p = gmp_intval(gmp_nextprime($p))) < 60;) {
    $m[] = $p;
}

$mi = 0;
$hi = 0;
do {
    printf("%02d %02d *  *  *\n", $m[$mi], $h[$hi]);
    $mi = ($mi + 1) % count($m);
    $hi = ($hi + 1) % count($h);
} while ($mi != 0 || $hi != 0);

使用している関数については、以下を参考に。

実行結果を整形して表にしてみると、以下のような感じ。(表示の都合上、crontabに書く「日」「月」「曜日」は省略)

1周目 2周目 3周目 4周目 5周目 6周目 7周目 8周目 9周目 10周目
00時台 02 00 19 00 47 00 11 00 37 00 03 00 23 00 53 00 13 00 41 00
01時台 03 01 23 01 53 01 13 01 41 01 05 01 29 01 59 01 17 01 43 01
02時台 05 02 29 02 59 02 17 02 43 02 07 02 31 02 02 02 19 02 47 02
03時台 07 03 31 03 02 03 19 03 47 03 11 03 37 03 03 03 23 03 53 03
04時台 11 04 37 04 03 04 23 04 53 04 13 04 41 04 05 04 29 04 59 04
05時台 13 05 41 05 05 05 29 05 59 05 17 05 43 05 07 05 31 05 02 05
06時台 17 06 43 06 07 06 31 06 02 06 19 06 47 06 11 06 37 06 03 06
07時台 19 07 47 07 11 07 37 07 03 07 23 07 53 07 13 07 41 07 05 07
08時台 23 08 53 08 13 08 41 08 05 08 29 08 59 08 17 08 43 08 07 08
09時台 29 09 59 09 17 09 43 09 07 09 31 09 02 09 19 09 47 09 11 09
10時台 31 10 02 10 19 10 47 10 11 10 37 10 03 10 23 10 53 10 13 10
11時台 37 11 03 11 23 11 53 11 13 11 41 11 05 11 29 11 59 11 17 11
12時台 41 12 05 12 29 12 59 12 17 12 43 12 07 12 31 12 02 12 19 12
13時台 43 13 07 13 31 13 02 13 19 13 47 13 11 13 37 13 03 13 23 13
14時台 47 14 11 14 37 14 03 14 23 14 53 14 13 14 41 14 05 14 29 14
15時台 53 15 13 15 41 15 05 15 29 15 59 15 17 15 43 15 07 15 31 15
16時台 59 16 17 16 43 16 07 16 31 16 02 16 19 16 47 16 11 16 37 16
17時台 02 17 19 17 47 17 11 17 37 17 03 17 23 17 53 17 13 17 41 17
18時台 03 18 23 18 53 18 13 18 41 18 05 18 29 18 59 18 17 18 43 18
19時台 05 19 29 19 59 19 17 19 43 19 07 19 31 19 02 19 19 19 47 19
20時台 07 20 31 20 02 20 19 20 47 20 11 20 37 20 03 20 23 20 53 20
21時台 11 21 37 21 03 21 23 21 53 21 13 21 41 21 05 21 29 21 59 21
22時台 13 22 41 22 05 22 29 22 59 22 17 22 43 22 07 22 31 22 02 22
23時台 17 23 43 23 07 23 31 23 02 23 19 23 47 23 11 23 37 23 03 23
11周目 12周目 13周目 14周目 15周目 16周目 17周目
00時台 05 00 29 00 59 00 17 00 43 00 07 00 31 00
01時台 07 01 31 01 02 01 19 01 47 01 11 01 37 01
02時台 11 02 37 02 03 02 23 02 53 02 13 02 41 02
03時台 13 03 41 03 05 03 29 03 59 03 17 03 43 03
04時台 17 04 43 04 07 04 31 04 02 04 19 04 47 04
05時台 19 05 47 05 11 05 37 05 03 05 23 05 53 05
06時台 23 06 53 06 13 06 41 06 05 06 29 06 59 06
07時台 29 07 59 07 17 07 43 07 07 07 31 07 02 07
08時台 31 08 02 08 19 08 47 08 11 08 37 08 03 08
09時台 37 09 03 09 23 09 53 09 13 09 41 09 05 09
10時台 41 10 05 10 29 10 59 10 17 10 43 10 07 10
11時台 43 11 07 11 31 11 02 11 19 11 47 11 11 11
12時台 47 12 11 12 37 12 03 12 23 12 53 12 13 12
13時台 53 13 13 13 41 13 05 13 29 13 59 13 17 13
14時台 59 14 17 14 43 14 07 14 31 14 02 14 19 14
15時台 02 15 19 15 47 15 11 15 37 15 03 15 23 15
16時台 03 16 23 16 53 16 13 16 41 16 05 16 29 16
17時台 05 17 29 17 59 17 17 17 43 17 07 17 31 17
18時台 07 18 31 18 02 18 19 18 47 18 11 18 37 18
19時台 11 19 37 19 03 19 23 19 53 19 13 19 41 19
20時台 13 20 41 20 05 20 29 20 59 20 17 20 43 20
21時台 17 21 43 21 07 21 31 21 02 21 19 21 47 21
22時台 19 22 47 22 11 22 37 22 03 22 23 22 53 22
23時台 23 23 53 23 13 23 41 23 05 23 29 23 59 23

全部で408通りできる。
使用される素数の個数である「17」自体が素数なので、割とばらけているように見える(ぇ。

参照はあいまい

いわゆるJavaの初心者がどつぼにはまりがちな、「参照はあいまい」と言われて戸惑うケース。

(NGコード)

package t2014_1008_01;

import java.util.*;
import java.sql.*;

public class Main {

    public static void main(String[] args) {
        Date now = new Date();
        System.out.println(now);
        
        // なにかDBへアクセスする処理をこの後に書く想定・・
    }

}

これをコンパイルすると、次のようなエラーメッセージが出力されます。

  • 出力言語(環境)が日本語の場合
$ LANG=ja_JP.UTF-8 javac -d classes src/t2014_1008_01/Main.java 
src/t2014_1008_01/Main.java:9: Date の参照はあいまいです。java.sql の クラス java.sql.Date と java.util の クラス java.util.Date が両方適合します。
                Date now = new Date();
                ^
src/t2014_1008_01/Main.java:9: Date の参照はあいまいです。java.sql の クラス java.sql.Date と java.util の クラス java.util.Date が両方適合します。
                Date now = new Date();
                               ^
エラー 2 個
  • 出力言語(環境)が英語の場合
$ LANG=C javac -encoding UTF-8 -d classes src/t2014_1008_01/Main.java 
src/t2014_1008_01/Main.java:9: reference to Date is ambiguous, both class java.sql.Date in java.sql and class java.util.Date in java.util match
                Date now = new Date();
                ^
src/t2014_1008_01/Main.java:9: reference to Date is ambiguous, both class java.sql.Date in java.sql and class java.util.Date in java.util match
                Date now = new Date();
                               ^
2 errors

(Java6で試していますが、それ以前のバージョンだともっとメッセージが不親切だったりするかもしないかも・・)
(CentOS 6.5で試していますが、WindowsでもMacOSでも理屈は同じ。)

で、仮に、「java.util.Date」を使うことを想定しているとして、ネットでちゃちゃっと調べて、「あぁ、『java.util.』をつければいいんだな」と、以下のように修正します。

(NGコード)

package t2014_1008_01;

import java.util.*;
import java.sql.*;

public class Main {

    public static void main(String[] args) {
        java.util.Date now = new Date();
        System.out.println(now);
        
        // なにかDBへアクセスする処理をこの後に書く想定・・
    }

}

気づく人はすぐに気づくでしょうが、これだけでは解決しません。
「new Date()」の方の型名がまだ「あいまい」ですから・・
(コンパイラは、好意的な解釈はせず、あくまで機械的に解釈しますから・・)

(OKコード)

package t2014_1008_01;

import java.util.*;
import java.sql.*;

public class Main {

    public static void main(String[] args) {
        java.util.Date now = new java.util.Date();
        System.out.println(now);
        
        // なにかDBへアクセスする処理をこの後に書く想定・・
    }

}

このようにしないといけません。


「わぁ、エラーメッセージだ、ググってみよう」と一つの流れのように機械的に対処しようとすると、探し当てたサンプルが自分のはまっている問題にピッタリ合致しないものだった場合に、このような罠にはまります。

辛くても、最初のコードをコンパイルしたときのエラーメッセージを「しっかり」見てみると、「2個のエラーが出ている」ことが分かります。

  • 一つは、変数宣言で型を指定するときの「Date」があいまい。
  • もう一つは、オブジェクトをnewする際に指定する「Date」があいまい。


まぁ、そもそも、import文には、「ワイルドカード指定よりも個別指定の方が優先される」という仕様があるので、「java.util.Date」と「java.sql.Date」の両方を使う必要性がない限り、以下のように書けば十分なんですが・・

(OKコード)

package t2014_1008_01;

import java.util.Date; // 個別指定
import java.util.*;    // ワイルドカード指定
import java.sql.*;     // ワイルドカード指定

public class Main {

    public static void main(String[] args) {
        Date now = new Date();
        System.out.println(now);
        
        // なにかDBへアクセスする処理をこの後に書く想定・・
    }

}

そもそも、IDEも普及している現代では、私はワイルドカード指定「非」推奨派ですが・・
・・と言いながらも、時々めんどくさくなって、テキストエディタでコードを書くときがある奴(ぇ

switch文の罠

PHPで(知らずに)以下のようなコードを書いていてはまった。

<?php

$sum = 0;
foreach (range(1, 5) as $i) {
    $val = $i;

    printf("%3d\n", $val);
    switch ($val) {
    case 1:
        printf("%5d: %s\n", $val, "One");
        break;
    case 2:
        printf("%5d: %s\n", $val, "Two");
        break;
    case 3:
        printf("%5d: %s\n", $val, "Three");
        continue;
    case 4:
        printf("%5d: %s\n", $val, "Four");
    case 5:
        printf("%5d: %s\n", $val, "Five");
        break;
    }
    $sum += $val;
}
printf("sum=%d\n", $sum);

実行すると以下のような結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=15

‥あれ、「$val == 3」のときはスキップされるから、「sum=12」になるはず‥

ためしに色んな言語で(時には無理矢理(謎))同じことをしてみる。

Javaの場合

public class Hoge {
    public static void main(String[] args) {
        int sum = 0;
        for (int i = 0; i < 5; ++i) {
            int val = i + 1;

            System.out.printf("%3d%n", val);
            switch (val) {
            case 1:
                System.out.printf("%5d: %s%n", val, "One");
                break;
            case 2:
                System.out.printf("%5d: %s%n", val, "Two");
                break;
            case 3:
                System.out.printf("%5d: %s%n", val, "Three");
                continue;
            case 4:
                System.out.printf("%5d: %s%n", val, "Four");
            case 5:
                System.out.printf("%5d: %s%n", val, "Five");
                break;
            }
            sum += (val);
        }
        System.out.printf("sum=%d%n", sum);
    }
}

実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=12

うん、「sum=12」になる。

C言語の場合

#include <stdio.h>

int main(int argc, char* argv[]) {
    int i;
    int sum = 0;
    int val;

    for (i = 0; i < 5; ++i) {
        val = i + 1;

        printf("%3d\n", val);
        switch (val) {
        case 1:
            printf("%5d: %s\n", val, "One");
            break;
        case 2:
            printf("%5d: %s\n", val, "Two");
            break;
        case 3:
            printf("%5d: %s\n", val, "Three");
            continue;
        case 4:
            printf("%5d: %s\n", val, "Four");
        case 5:
            printf("%5d: %s\n", val, "Five");
            break;
        }
        sum += val;
    }
    printf("sum=%d\n", sum);
}

実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=12

うん、「sum=12」。

bash

#! /bin/bash

sum=0
for ((i = 0; i < 5; ++i)); do
    val=$((i + 1))

    printf "%3d\n" $val
    case $val in
        1)
            printf "%5d: %s\n" $val "One"
            ;;
        2)
            printf "%5d: %s\n" $val "Two"
            ;;
        3)
            printf "%5d: %s\n" $val "Three"
            continue;
            ;;
        4|5)
            case $val in
                4)
                    printf "%5d: %s\n" $val "Four"
                    ;;
            esac
            printf "%5d: %s\n" $val "Five"
            ;;
    esac
    sum=$((sum + val))
done
printf "sum=%d\n" $sum

break文に当たる「;;」を省略できないので、fall throughの部分は無理矢理だが。
実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=12

うん、「sum=12」

Perl

Perlは書けないので、以下の辺りを参考に。

use Switch;

my $i;
my $sum = 0;

for ($i = 0; $i < 5; ++$i) {
    my $val = $i + 1;

    printf("%3d\n", $val);
    switch ($val) {
        case 1 {
            printf("%5d: %s\n", $val, "One");
        }
        case 2 {
            printf("%5d: %s\n", $val, "Two");
        }
        case 3 {
            printf("%5d: %s\n", $val, "Three");
            next;
        }
        case [4, 5] {
            switch ($val) {
                case 4 {
                    printf("%5d: %s\n", $val, "Four");
                }
            }
            printf("%5d: %s\n", $val, "Five");
        }
    }
    $sum += $val;
}
printf("sum=%d\n", $sum);

同じくfall throughの部分は無理矢理。
実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=15

‥あれ、「sum=15」だ‥
下記で追調査。

Ruby

Rubyも書けないので、以下の辺りを参考に。

ちなみに、脱線するが、この辺りが面白かった。

sum=0
for i in 1..5 do
    val = i;

    printf("%3d\n", val);
    case val
    when 1
        printf("%5d: %s\n", val, "One");
    when 2
        printf("%5d: %s\n", val, "Two");
    when 3
        printf("%5d: %s\n", val, "Three");
        next;
    when 4, 5
        case val
        when 4
            printf("%5d: %s\n", val, "Four");
        end
        printf("%5d: %s\n", val, "Five");
    end
    sum += val;
end
printf("sum=%d\n", sum);

同じくfall throughの部分は無理矢理。
実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=12

うん、「sum=12」。

一旦まとめると‥

言語 switch(case)文の中でのループスキップ
PHP  ×(※1)
Java  ○
C言語  ○
bash  ○
Perl  ×(※2)
Ruby  ○

と、こんな感じになった。
‥って書いちゃうと誤解を招くので、さっさと追調査。

(※1)PHPの場合の仕様

実は、マニュアルの中にこんなことが書いてあった。


注意: 他の言語とは違って、 continue命令は switchにも適用され、breakと同じ動作をします。 ループの内部でswitchを使用しており、 外側のループの処理を続行させたい場合には、continue 2 を使用してください。

‥おぉ!
ということで、以下のように書き直してみる。

<?php

$sum = 0;
foreach (range(1, 5) as $i) {
    $val = $i;

    printf("%3d\n", $val);
    switch ($val) {
    case 1:
        printf("%5d: %s\n", $val, "One");
        break;
    case 2:
        printf("%5d: %s\n", $val, "Two");
        break;
    case 3:
        printf("%5d: %s\n", $val, "Three");
        continue 2;
    case 4:
        printf("%5d: %s\n", $val, "Four");
    case 5:
        printf("%5d: %s\n", $val, "Five");
        break;
    }
    $sum += $val;
}
printf("sum=%d\n", $sum);

実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=12

うん、「sum=12」。

この構文、もちろん単なる多重ループのときにも有効で、変なラベル(何)をつける必要がないというメリットがある。一方で、ぱっと見ではどこに飛ぶのかが分からず、ループの数を数え間違えるとひどい事になるというデメリットがある。

(※2)Perlの場合の仕様

(PHPと同じように書いてみたときのエラーメッセージからたどり着いたのは内緒(謎))

Perlでは、ラベルによって、どのループに対する「next」なのかを指定できる。

そもそも、PerlのSwitchモジュールにおける「next」はfall throughのためのものらしい。

つまり、先のPerlプログラム中の「case 3」よりも下に、$valが3のときにマッチするcase句があったら違う結果を引き起こしていたというわけだ。怖い怖い‥

というわけで、上記を踏まえて書き直したのが以下。

use Switch;

my $i;
my $sum = 0;

loop:
for ($i = 0; $i < 5; ++$i) {
    my $val = $i + 1;

    printf("%3d\n", $val);
    switch ($val) {
        case 1 {
            printf("%5d: %s\n", $val, "One");
        }
        case 2 {
            printf("%5d: %s\n", $val, "Two");
        }
        case 3 {
            printf("%5d: %s\n", $val, "Three");
            next loop;
        }
        case 4 {
            printf("%5d: %s\n", $val, "Four");
            next;
        }
        case [4, 5] {
            printf("%5d: %s\n", $val, "Five");
        }
    }
    $sum += $val;
}
printf("sum=%d\n", $sum);

Switchモジュールのfall throughの仕様をちゃんと使ってみた。
また、この「ラベル」の概念はJavaでも同じですね。(もっと言えば、悪しきモノとして封印されているC言語のgotoとか(略))
で、実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=12

うん、「sum=12」。

で‥

自分の場合、JavaC言語bashスクリプトに親しんでからPHPを使うようになった派なのでこのような罠にどっぷりはまったわけだが、「構文が似ているからと言って、同じ動作をするとは限らない」という教訓になる。
上層の人(誰)の「君はこのプログラム言語できるよね?こっちの言語も似たようなものだから余裕でしょ?ちょっとこっちの案件手伝ってくれる?あ、言語の習得は案件を進めながら合間にやってね」という台詞に、全力で立ち向かえることと思う。「似ているからこそ危険なんだよ!」と。