HHeLiBeXの日記 正道編

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

DateTimeクラスの罠

PHPにDateTimeクラスとDateIntervalクラスなる便利なものがあると知って、喜び勇んで‥

こんな感じで、日時の加算や減算のテストプログラムを作ってみた。

  • DateTimeTest.php
<?php

class DateTimeTest {
    public static function test($date1, $date2, $target = null) {
        $intervals = array(
            '' => new DateInterval('P1Y'),
            '' => new DateInterval('P1M'),
            '' => new DateInterval('P1W'),
            '' => new DateInterval('P1D'),
            '' => new DateInterval('PT1H'),
            '' => new DateInterval('PT1M'),
            '' => new DateInterval('PT1S'),
        );
        if (!$target) {
            $target = array_keys($intervals);
        }

        printf("     <add>                         <sub>\n");
        foreach ($target as $k) {
            if (isset($intervals[$k])) {
                $interval = $intervals[$k];

                printf("%s===%s===%s\n", $k, $interval->format('%Y-%M-%D %H:%I:%S'), $k);

                printf("%5s%20s|%5s%20s\n", "", $date1->format('Y-m-d H:i:s(D)'), "", $date2->format('Y-m-d H:i:s(D)'));
                $date1->add($interval);
                $date2->sub($interval);
                printf("  => %20s|  => %20s\n" , $date1->format('Y-m-d H:i:s(D)') , $date2->format('Y-m-d H:i:s(D)'));
            }
        }
    }
}
<?php

date_default_timezone_set('Asia/Tokyo');

require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'DateTimeTest.php');

DateTimeTest::test(new DateTime(), new DateTime());

要は、指定したフィールドの加算・減算を指定した分だけ行うというもの。

いざ実行。

$ php test1.php
     <add>                         <sub>
年===01-00-00 00:00:00===年
     2015-07-09 06:05:35(Thu)|     2015-07-09 06:05:35(Thu)
  => 2016-07-09 06:05:35(Sat)|  => 2014-07-09 06:05:35(Wed)
月===00-01-00 00:00:00===月
     2016-07-09 06:05:35(Sat)|     2014-07-09 06:05:35(Wed)
  => 2016-08-09 06:05:35(Tue)|  => 2014-06-09 06:05:35(Mon)
週===00-00-07 00:00:00===週
     2016-08-09 06:05:35(Tue)|     2014-06-09 06:05:35(Mon)
  => 2016-08-16 06:05:35(Tue)|  => 2014-06-02 06:05:35(Mon)
日===00-00-01 00:00:00===日
     2016-08-16 06:05:35(Tue)|     2014-06-02 06:05:35(Mon)
  => 2016-08-17 06:05:35(Wed)|  => 2014-06-01 06:05:35(Sun)
時===00-00-00 01:00:00===時
     2016-08-17 06:05:35(Wed)|     2014-06-01 06:05:35(Sun)
  => 2016-08-17 07:05:35(Wed)|  => 2014-06-01 05:05:35(Sun)
分===00-00-00 00:01:00===分
     2016-08-17 07:05:35(Wed)|     2014-06-01 05:05:35(Sun)
  => 2016-08-17 07:06:35(Wed)|  => 2014-06-01 05:04:35(Sun)
秒===00-00-00 00:00:01===秒
     2016-08-17 07:06:35(Wed)|     2014-06-01 05:04:35(Sun)
  => 2016-08-17 07:06:36(Wed)|  => 2014-06-01 05:04:34(Sun)
$ 

おぉ、これは便利。

‥と思って使っていたのだが、思わぬ罠が待っていた‥

<?php

date_default_timezone_set('Asia/Tokyo');

require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'DateTimeTest.php');

DateTimeTest::test(new DateTime('2015-01-31'), new DateTime('2015-01-31'),
        array('', '', '', '', '', '', '', '', '', '', '', ''));

要は、「1月末」に1ヶ月ずつ加算・減算をしていくわけだが‥

$ php test2.php
     <add>                         <sub>
月===00-01-00 00:00:00===月
     2015-01-31 00:00:00(Sat)|     2015-01-31 00:00:00(Sat)
  => 2015-03-03 00:00:00(Tue)|  => 2014-12-31 00:00:00(Wed)
月===00-01-00 00:00:00===月
     2015-03-03 00:00:00(Tue)|     2014-12-31 00:00:00(Wed)
  => 2015-04-03 00:00:00(Fri)|  => 2014-12-01 00:00:00(Mon)
月===00-01-00 00:00:00===月
     2015-04-03 00:00:00(Fri)|     2014-12-01 00:00:00(Mon)
  => 2015-05-03 00:00:00(Sun)|  => 2014-11-01 00:00:00(Sat)
月===00-01-00 00:00:00===月
     2015-05-03 00:00:00(Sun)|     2014-11-01 00:00:00(Sat)
  => 2015-06-03 00:00:00(Wed)|  => 2014-10-01 00:00:00(Wed)
月===00-01-00 00:00:00===月
     2015-06-03 00:00:00(Wed)|     2014-10-01 00:00:00(Wed)
  => 2015-07-03 00:00:00(Fri)|  => 2014-09-01 00:00:00(Mon)
月===00-01-00 00:00:00===月
     2015-07-03 00:00:00(Fri)|     2014-09-01 00:00:00(Mon)
  => 2015-08-03 00:00:00(Mon)|  => 2014-08-01 00:00:00(Fri)
月===00-01-00 00:00:00===月
     2015-08-03 00:00:00(Mon)|     2014-08-01 00:00:00(Fri)
  => 2015-09-03 00:00:00(Thu)|  => 2014-07-01 00:00:00(Tue)
月===00-01-00 00:00:00===月
     2015-09-03 00:00:00(Thu)|     2014-07-01 00:00:00(Tue)
  => 2015-10-03 00:00:00(Sat)|  => 2014-06-01 00:00:00(Sun)
月===00-01-00 00:00:00===月
     2015-10-03 00:00:00(Sat)|     2014-06-01 00:00:00(Sun)
  => 2015-11-03 00:00:00(Tue)|  => 2014-05-01 00:00:00(Thu)
月===00-01-00 00:00:00===月
     2015-11-03 00:00:00(Tue)|     2014-05-01 00:00:00(Thu)
  => 2015-12-03 00:00:00(Thu)|  => 2014-04-01 00:00:00(Tue)
月===00-01-00 00:00:00===月
     2015-12-03 00:00:00(Thu)|     2014-04-01 00:00:00(Tue)
  => 2016-01-03 00:00:00(Sun)|  => 2014-03-01 00:00:00(Sat)
月===00-01-00 00:00:00===月
     2016-01-03 00:00:00(Sun)|     2014-03-01 00:00:00(Sat)
  => 2016-02-03 00:00:00(Wed)|  => 2014-02-01 00:00:00(Sat)
$ 

「2015-01-31」に「1ヶ月」を足したところで既に残念な結果「2015-03-03」になっている。

「2014-12-31」から「1ヶ月」を引いたところも残念な結果「2014-12-01」になっている。

まぁ、単純に処理したら「2015-02-31⇒2015-03-03」で分からんこともないんだが、各月の日数が違うはずなのにその後の処理が問題なく進行しているところを見ると、単純な文字列処理しかしていないんだという事が分かる。

結局、現場(何処)では、「月」が変わるまでループで「日」を加算・減算をするなどというアホなことを一部やっているが、どうにかしたいものだ‥ (ループの回数は、「月」が変わるまでループで「週」を加算・減算して、その後に「月」が戻る直前まで「日」を引いたほうが平均的には少なくなる‥のか(そういう問題じゃない))

日付文字列のフォーマットのメモ

RFC 2822に従った日付文字列の生成

一発で出力できる指定子があることに気付かず、試行錯誤したのだが、せっかくなのでメモしておく。

RFC 2822に従った日付文字列は以下のような形式。

Thu, 18 Jun 2015 16:43:47 +0900

メールのヘッダなんかに使われることになっているもの。 ものによっては以下の形式も見かける。

Thu, 18 Jun 2015 16:43:47 +0900 (JST)

これをPHPで出力しようと思ったら、以下のように書けばいい。なお、PHPの初期設定をちゃんとやっていない環境なので余計な処理が冒頭に入っているが気にしない(謎)。

<?php
date_default_timezone_set('Asia/Tokyo');
mb_internal_encoding('UTF-8');

$time = strtotime('2015-06-09 17:02:03');

printf("===date===\n");
printf("    %s\n", 'RFC 2822');
printf("        %s\n", date('r', $time));
printf("        %s\n", date('D, d M Y H:i:s O', $time));

printf("    %s\n", 'RFC 2822 with Timezone abbreviation');
printf("        %s\n", date('r (T)', $time));
printf("        %s\n", date('D, d M Y H:i:s O (T)', $time));

出力は以下のような感じ。

===date===
    RFC 2822
        Tue, 09 Jun 2015 17:02:03 +0900
        Tue, 09 Jun 2015 17:02:03 +0900
    RFC 2822 with Timezone abbreviation
        Tue, 09 Jun 2015 17:02:03 +0900 (JST)
        Tue, 09 Jun 2015 17:02:03 +0900 (JST)

ISO 8601拡張形式

ついでなのでやってみた(謎)。

ISO 8601拡張形式に従った日付文字列は以下のような形式。

2015-06-18T16:43:47+0900

これも、一発で出力できる指定子がある。

<?php
date_default_timezone_set('Asia/Tokyo');
mb_internal_encoding('UTF-8');

$time = strtotime('2015-06-09 17:02:03');

printf("===date===\n");
printf("    %s\n", 'ISO 8601 date');
printf("        %s\n", date('c', $time));
printf("        %s\n", date('Y-m-d\TH:i:sP', $time));

出力。

===date===
    ISO 8601 date
        2015-06-09T17:02:03+09:00
        2015-06-09T17:02:03+09:00

(ダメ)strftimeでRFC 2822形式

さらについでなので、strftime関数で色々やってみた。setLocale呼び出しに影響を受ける関数なので、当然ながら汎用性はないのだが‥

<?php
date_default_timezone_set('Asia/Tokyo');
mb_internal_encoding('UTF-8');

$time = strtotime('2015-06-09 17:02:03');

printf("===strftime===\n");
$locales = array(
    '英語(US)            ' => 'en_US',
    '英語(UK)            ' => 'en_GB',
    '日本語              ' => 'ja_JP',
    'ドイツ語            ' => 'de_DE',
    'オランダ語          ' => 'nl_NL',
    'フランス語(フランス)' => 'fr_FR',
    'フランス語(カナダ)  ' => 'fr_CA',
    'スペイン語          ' => 'es_ES',
    'ポルトガル語        ' => 'pt_PT',
    '中国語(繁体字)      ' => 'zh_TW',
    '中国語(簡体字)      ' => 'zh_CN',
    '韓国語              ' => 'ko_KR',
);
$formats = array(
    'RFC 2822'
        => '%a, %d %b %Y %H:%M:%S %z',
    'RFC 2822 with Timezone abbreviation'
        => '%a, %d %b %Y %H:%M:%S %z (%Z)',
);
foreach ($formats as $name => $format) {
    printf("%s\n", $name);
    foreach ($locales as $country => $locale) {
        printf("    %s(%s): ", setLocale(LC_ALL, $locale . '.UTF-8'), $country);

        printf("        %s\n", strftime($format, $time));
    }
}

出力。

===strftime===
RFC 2822
    en_US.UTF-8(英語(US)            ):         Tue, 09 Jun 2015 17:02:03 +0900
    en_GB.UTF-8(英語(UK)            ):         Tue, 09 Jun 2015 17:02:03 +0900
    ja_JP.UTF-8(日本語              ):         火, 09  6月 2015 17:02:03 +0900
    de_DE.UTF-8(ドイツ語            ):         Di, 09 Jun 2015 17:02:03 +0900
    nl_NL.UTF-8(オランダ語          ):         di, 09 jun 2015 17:02:03 +0900
    fr_FR.UTF-8(フランス語(フランス)):         mar., 09 juin 2015 17:02:03 +0900
    fr_CA.UTF-8(フランス語(カナダ)  ):         mar, 09 jun 2015 17:02:03 +0900
    es_ES.UTF-8(スペイン語          ):         mar, 09 jun 2015 17:02:03 +0900
    pt_PT.UTF-8(ポルトガル語        ):         Ter, 09 Jun 2015 17:02:03 +0900
    zh_TW.UTF-8(中国語(繁体字)      ):         二, 09  6月 2015 17:02:03 +0900
    zh_CN.UTF-8(中国語(簡体字)      ):         二, 09 6月 2015 17:02:03 +0900
    ko_KR.UTF-8(韓国語              ):         화, 09  6월 2015 17:02:03 +0900
RFC 2822 with Timezone abbreviation
    en_US.UTF-8(英語(US)            ):         Tue, 09 Jun 2015 17:02:03 +0900 (JST)
    en_GB.UTF-8(英語(UK)            ):         Tue, 09 Jun 2015 17:02:03 +0900 (JST)
    ja_JP.UTF-8(日本語              ):         火, 09  6月 2015 17:02:03 +0900 (JST)
    de_DE.UTF-8(ドイツ語            ):         Di, 09 Jun 2015 17:02:03 +0900 (JST)
    nl_NL.UTF-8(オランダ語          ):         di, 09 jun 2015 17:02:03 +0900 (JST)
    fr_FR.UTF-8(フランス語(フランス)):         mar., 09 juin 2015 17:02:03 +0900 (JST)
    fr_CA.UTF-8(フランス語(カナダ)  ):         mar, 09 jun 2015 17:02:03 +0900 (JST)
    es_ES.UTF-8(スペイン語          ):         mar, 09 jun 2015 17:02:03 +0900 (JST)
    pt_PT.UTF-8(ポルトガル語        ):         Ter, 09 Jun 2015 17:02:03 +0900 (JST)
    zh_TW.UTF-8(中国語(繁体字)      ):         二, 09  6月 2015 17:02:03 +0900 (JST)
    zh_CN.UTF-8(中国語(簡体字)      ):         二, 09 6月 2015 17:02:03 +0900 (JST)
    ko_KR.UTF-8(韓国語              ):         화, 09  6월 2015 17:02:03 +0900 (JST)

試した環境では「ja_JP.UTF-8」のようにしないと、マルチバイト文字がうまく見えなかったが、きっと環境のせい。

英語なんかだと、当然ながら期待する出力になっている。 ただし、期待する出力になっているlocaleでも、Windows環境など、環境によっては全然違う文字列が生成されることもあるので注意するようにということがマニュアルには書いてある。

(ダメ)strftimeでISO 8601拡張形式

さらにさらについでなので、strftime関数で色々やってみた。当然ながら(ry

<?php

/*
 * http://php.net/manual/ja/function.strftime.php
 * http://php.net/manual/ja/function.date.php
 */

date_default_timezone_set('Asia/Tokyo');
mb_internal_encoding('UTF-8');

$time = strtotime('2015-06-09 17:02:03');

printf("===strftime===\n");
$locales = array(
    '英語(US)            ' => 'en_US',
    '英語(UK)            ' => 'en_GB',
    '日本語              ' => 'ja_JP',
    'ドイツ語            ' => 'de_DE',
    'オランダ語          ' => 'nl_NL',
    'フランス語(フランス)' => 'fr_FR',
    'フランス語(カナダ)  ' => 'fr_CA',
    'スペイン語          ' => 'es_ES',
    'ポルトガル語        ' => 'pt_PT',
    '中国語(繁体字)      ' => 'zh_TW',
    '中国語(簡体字)      ' => 'zh_CN',
    '韓国語              ' => 'ko_KR',
);
$formats = array(
    'ISO 8601 date'
        => '%Y-%m-%dT%H:%M:%S%z',
);
foreach ($formats as $name => $format) {
    printf("%s\n", $name);
    foreach ($locales as $country => $locale) {
        printf("    %s(%s): ", setLocale(LC_ALL, $locale . '.UTF-8'), $country);

        printf("        %s\n", strftime($format, $time));
    }
}

出力。

===strftime===
ISO 8601 date
    en_US.UTF-8(英語(US)            ):         2015-06-09T17:02:03+0900
    en_GB.UTF-8(英語(UK)            ):         2015-06-09T17:02:03+0900
    ja_JP.UTF-8(日本語              ):         2015-06-09T17:02:03+0900
    de_DE.UTF-8(ドイツ語            ):         2015-06-09T17:02:03+0900
    nl_NL.UTF-8(オランダ語          ):         2015-06-09T17:02:03+0900
    fr_FR.UTF-8(フランス語(フランス)):         2015-06-09T17:02:03+0900
    fr_CA.UTF-8(フランス語(カナダ)  ):         2015-06-09T17:02:03+0900
    es_ES.UTF-8(スペイン語          ):         2015-06-09T17:02:03+0900
    pt_PT.UTF-8(ポルトガル語        ):         2015-06-09T17:02:03+0900
    zh_TW.UTF-8(中国語(繁体字)      ):         2015-06-09T17:02:03+0900
    zh_CN.UTF-8(中国語(簡体字)      ):         2015-06-09T17:02:03+0900
    ko_KR.UTF-8(韓国語              ):         2015-06-09T17:02:03+0900

こっちは、localeに依存しない指定子しかつかっていないからか、localeに関係なく、試した範囲では同じ出力だった。 まぁ、変な心配をするより、素直にdate関数を使えという話だが‥

svn:ignoreで無視されるもの

なんか混乱したので、忘れないようにメモ。
当たり前と言えば当たり前すぎるのだが‥

事前準備

前提として、「/var/lib/svn」の下にリポジトリディレクトリを作成するものとする。

# cd /var/lib/svn
# sudo -u apache svnadmin create test

実験

Webサーバー経由でSVNリポジトリにアクセスする準備は整っているものとする。

$ svn --version --quiet
1.6.11
$ svn checkout http://localhost/svn/test
リビジョン 0 をチェックアウトしました。
$ cd test
$ mkdir files
$ svn add files
$ cd files
$ for f in committed committed.edited ignored.committed ignored.committed.edited
> do
>     echo a > ${f}
> done
$ svn add committed committed.edited ignored.committed ignored.committed.edited
$ svn commit -m '' . committed committed.edited ignored.committed ignored.committed.edited
追加しています              files
追加しています              files/committed
追加しています              files/committed.edited
追加しています              files/ignored.committed
追加しています              files/ignored.committed.edited
ファイルのデータを送信しています ....
リビジョン 1 をコミットしました。
$ svn propset svn:ignore 'ignored.*' .
属性 'svn:ignore' を '.' に設定しました
$ svn commit -m '' .
送信しています              files

リビジョン 2 をコミットしました。
$ for f in added ignored.added
> do
>     echo a > ${f}
> done
$ svn add added ignored.added
$ for f in notmanaged ignored.notmanaged
> do
>     echo a > ${f}
> done
$ echo b >> committed.edited
$ echo b >> ignored.committed.edited
$ cd ..
$ pwd
/home/hhelibex/tmp/test
$ ls -1 files
added
committed
committed.edited
ignored.added
ignored.committed
ignored.committed.edited
ignored.notmanaged
notmanaged
$ svn status
?       files/notmanaged
A       files/added
M       files/committed.edited
A       files/ignored.added
M       files/ignored.committed.edited
$ svn status --no-ignore
I       files/ignored.notmanaged
?       files/notmanaged
A       files/added
M       files/committed.edited
A       files/ignored.added
M       files/ignored.committed.edited
$ 

結論

svn:ignore」で無視されるのは、バージョン管理下に置かれていない(commitはもちろん、addすらされていない)ファイルに限定される。
‥当たり前だ‥
‥いや、無視するパターンをファイルに書いて、そのファイル自身もignore対象に入れて、パターン一覧を書いたファイルをバージョン管理するっていう矛盾したサンプル(何)を見つけて混乱したので‥

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

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

なんで「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時に行っている。