HHeLiBeXの日記 正道編

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

Zend_Dateクラスを効率よく使うチャレンジ

Zend Frameworkに含まれているZend_Dateクラスは、インスタンス生成コストがとにかく高い。

どれくらい高いかというと、以下の2つのプログラムで比較してみるとなんとなく分かる。

<?php

$n = 10000;
if ($argc >= 2) {
    $n = (int)$argv[1];
}

set_include_path(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'library');
require_once('Zend/Date.php');

date_default_timezone_set('Asia/Tokyo');

$start = microtime(true);
for ($i = 0; $i < $n; ++$i) {
    $zd = new Zend_Date('2015-08-01 03:04:05');
}
$end = microtime(true);
printf("%8.5lf\n", $end - $start);
<?php

$n = 10000;
if ($argc >= 2) {
    $n = (int)$argv[1];
}

set_include_path(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'library');
require_once('Zend/Date.php');

date_default_timezone_set('Asia/Tokyo');

$start = microtime(true);
for ($i = 0; $i < $n; ++$i) {
    $zd = new DateTime('2015-08-01 03:04:05');
}
$end = microtime(true);
printf("%8.5lf\n", $end - $start);
  • test0-3.php: Zend_Dateオブジェクトをキャッシュしておいて、strtotimeとsetTimestampで対応
<?php

$n = 10000;
if ($argc >= 2) {
    $n = (int)$argv[1];
}

set_include_path(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'library');
require_once('Zend/Date.php');

date_default_timezone_set('Asia/Tokyo');

$zd = new Zend_Date();
$start = microtime(true);
for ($i = 0; $i < $n; ++$i) {
    $time = strtotime('2015-08-01 03:04:05');
    $zd->setTimestamp($time);
}
$end = microtime(true);
printf("%8.5lf\n", $end - $start);

これらを実行する。

$ for i in {1,2,5,8}{,00,000} ; do
>     printf "%d" $i
>     for t in test0-1.php test0-2.php test0-3.php ; do
>         php ${t} ${i} | awk '{printf(",%s", $0);}'
>     done
>     printf "\n"
> done
1, 0.03093, 0.00011, 0.00004
100, 0.23281, 0.00048, 0.00074
1000, 2.32891, 0.00825, 0.01088
2, 0.01160, 0.00012, 0.00005
200, 0.48956, 0.00336, 0.00224
2000, 5.51129, 0.01458, 0.02203
5, 0.02411, 0.00018, 0.00012
500, 1.08754, 0.00409, 0.00425
5000,11.39618, 0.03983, 0.06424
8, 0.03674, 0.00031, 0.00000
800, 2.20668, 0.00417, 0.00931
8000,19.13784, 0.07557, 0.11488
$ 

並べ替えてグラフにしてみるとこんな感じ。

f:id:hhelibex:20150801161410p:plain

とりあえず、欲しいものは「日時計算をして、その結果を日時文字列にしたもの」ということで話を進める。

Zend_Dateオブジェクトをキャッシュして使いまわすとよさそうということで、上記「test0-3.php」をベースに、以下のような計測をしてみる。

<?php

$n = 10000;
if ($argc >= 2) {
    $n = (int)$argv[1];
}

set_include_path(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'library');
require_once('Zend/Date.php');

date_default_timezone_set('Asia/Tokyo');

function _test(Zend_Date $zd) {
    return $zd->toString('YYYY-MM-dd');
}

$zd = new Zend_Date('2015-08-01 03:04:05');
//var_dump(_test($zd));
$start = microtime(true);
for ($i = 0; $i < $n; ++$i) {
    _test($zd);
}
$end = microtime(true);
printf("%8.5lf\n", $end - $start);
<?php

$n = 10000;
if ($argc >= 2) {
    $n = (int)$argv[1];
}

set_include_path(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'library');
require_once('Zend/Date.php');

date_default_timezone_set('Asia/Tokyo');

function _test(Zend_Date $zd) {
    $ts = $zd->getTimestamp();
    return date('Y-m-d', $ts);
}

$zd = new Zend_Date('2015-08-01 03:04:05');
//var_dump(_test($zd));
$start = microtime(true);
for ($i = 0; $i < $n; ++$i) {
    _test($zd);
}
$end = microtime(true);
printf("%8.5lf\n", $end - $start);
  • test1-3.php: Zend_Dateのgetメソッドで各フィールドを取得+sprintf関数
<?php

$n = 10000;
if ($argc >= 2) {
    $n = (int)$argv[1];
}

set_include_path(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'library');
require_once('Zend/Date.php');

date_default_timezone_set('Asia/Tokyo');

function _test(Zend_Date $zd) {
    $year = $zd->get(Zend_Date::YEAR);
    $month = $zd->get(Zend_Date::MONTH);
    $day = $zd->get(Zend_Date::DAY);
    return sprintf("%04d-%02d-%02d", $year, $month, $day);
}

$zd = new Zend_Date('2015-08-01 03:04:05');
$start = microtime(true);
for ($i = 0; $i < $n; ++$i) {
    _test($zd);
}
$end = microtime(true);
printf("%8.5lf\n", $end - $start);

さて、実行。

$ for i in {1,2,5,8}{,00,000,0000} ; do
>     printf "%d" $i
>     for t in test1-1.php test1-2.php test1-3.php ; do
>         php ${t} ${i} | awk '{printf(",%s", $0);}'
>     done
>     printf "\n"
> done

結果。

1, 0.00020, 0.00002, 0.00009
100, 0.01489, 0.00010, 0.01203
1000, 0.15603, 0.00634, 0.17088
10000, 1.28451, 0.04945, 1.37297
2, 0.00009, 0.00002, 0.00063
200, 0.02691, 0.00093, 0.04130
2000, 0.30510, 0.01436, 0.27488
20000, 2.93000, 0.12693, 3.37226
5, 0.00057, 0.00008, 0.00116
500, 0.08533, 0.00256, 0.07560
5000, 0.85879, 0.03124, 0.98879
50000,11.33206, 0.39485, 7.20063
8, 0.00063, 0.00009, 0.00074
800, 0.10203, 0.00917, 0.09432
8000, 0.86644, 0.03868, 0.81726
80000,12.23691, 0.57610,14.96780

f:id:hhelibex:20150801161436p:plain

さすがに各フィールドの値を取るのはコストが高いが、「test1-2.php」のケースが使えそうなレベル。

また、コードは省略するが、「test2-1.php」「test2-2.php」「test2-3.php」は時分秒まで含めるケース(「HH:mm:ss」「H:i:s」等の追加による)。

「test1-2.php/test2-2.php」はフォーマットに関係なくいいパフォーマンスを出しているが、Zend_Dateは時分秒まで含めるとそれだけコストが高くなる。

Zend_Dateクラスとは適度に付き合うのがよさそう(謎)。

HTML5のinputタグにおけるtype属性のサポート状況

ブラウザ標準の機能でカレンダーコンポーネントを使ったコードを書きたくて、ついでに単純なHTMLを書いて検証してみた。

試したのは以下のブラウザ。

書いたのは以下のHTMLコード。

<!DOCTYPE html>
<html>
<head></head>
<body>
<form action="#" method="post">
<table>
<tbody>
    <tr>
        <th>text</th>
        <td><input type="text" name="f_text" /></td>
    </tr>
    <tr>
        <th>search</th>
        <td><input type="search" name="f_search" /></td>
    </tr>
    <tr>
        <th>tel</th>
        <td><input type="tel" name="f_tel" /></td>
    </tr>
    <tr>
        <th>url</th>
        <td><input type="url" name="f_url" /></td>
    </tr>
    <tr>
        <th>email</th>
        <td><input type="email" name="f_email" /></td>
    </tr>
    <tr>
        <th>datetime</th>
        <td><input type="datetime" name="f_datetime" /></td>
    </tr>
    <tr>
        <th>date</th>
        <td><input type="date" name="f_date" /></td>
    </tr>
    <tr>
        <th>month</th>
        <td><input type="month" name="f_month" /></td>
    </tr>
    <tr>
        <th>week</th>
        <td><input type="week" name="f_week" /></td>
    </tr>
    <tr>
        <th>time</th>
        <td><input type="time" name="f_time" /></td>
    </tr>
    <tr>
        <th>datetime-local</th>
        <td><input type="datetime-local" name="f_datetime-local" /></td>
    </tr>
    <tr>
        <th>number</th>
        <td><input type="number" name="f_number" /></td>
    </tr>
    <tr>
        <th>range</th>
        <td><input type="range" name="f_range" /></td>
    </tr>
    <tr>
        <th>color</th>
        <td><input type="color" name="f_color" /></td>
    </tr>
</tbody>
</table>
<input type="submit" />
</form>
</body>
</html>

これを、ちゃんとWebサーバーを通して各ブラウザで表示させ、状況を見てみた。

サポートされているかどうかの判定は、type="text"との挙動の違いがあるかどうか。なので、最初は適当にaaaとか入力して、その場で怒られるものもあれば、submitボタンを押したときにバリデーションエラーを出すものもある。

サポート状況は大体こんな感じ。

Firefox Chrome Opera Safari IE 11 Android
text
search
tel
url
email
datetime
date
month
week ○(*1)
time
datetime-local
number
range
color
  • (*1):それっぽく表示されるけど動作しなかった‥

‥酷いな‥画面キャプチャを貼り付けるのも面倒になるくらい酷い‥

もう、普通のテキストボックスで年月日を分けますよ、えぇ(謎)‥

PHPのrequire_onceが遅い話

もはや専門家の間では有名な話なのだろうが、今頃意識し始めて、ちょっと計ってみるかという気になったので計ってみる。

なんせ、Zend Frameworkのページでもパフォーマンスガイドとして書いてあるくらいだし。

計るにあたっては、「ノートPC上のVMでやりました」では話にならないだろうということで(それでも比率を見れば傾向はつかめると思うが)、とあるVPS上で計測をした。

ちなみに、「『Zend Frameworkと同じ命名規則でクラス名をつける』というルールの元で作ったものに対する計測」ってことでそのあたりはご注意を。

実験環境

環境はこんな感じ。

$ cat /etc/redhat-release
CentOS release 6.6 (Final)
$ uname -r
2.6.32-504.16.2.el6.x86_64
$ free
             total       used       free     shared    buffers     cached
Mem:       1922192    1480608     441584        536     202304     748092
-/+ buffers/cache:     530212    1391980
Swap:      2097148       6472    2090676
$ php -v
PHP 5.3.3 (cli) (built: Jul  9 2015 17:39:00) 
Copyright (c) 1997-2010 The PHP Group
Zend Engine v2.3.0, Copyright (c) 1998-2010 Zend Technologies
$ php -r 'var_dump(get_include_path());'
string(32) ".:/usr/share/pear:/usr/share/php"
$ 

ちなみにCPUは「Intel Xeon E312xx (Sandy Bridge)」の3コアらしい。

使用するプログラム

  • App/Hoge0.php
<?php
class App_Hoge0 {
    public static function test() {}
}
  • test0.php
    • 1回だけrequire_onceする。
<?php
printf("%s\n", __FILE__);

$n = 1000000;
if ($argc >= 2) {
    $n = (int)$argv[1];
}

$startTime = microtime(true);

require_once('App/Hoge0.php');
for ($i = 0; $i < $n; ++$i) {
    App_Hoge0::test();
}

$endTime = microtime(true);
printf("%8.5lf\n", $endTime - $startTime);
  • test1.php
    • ループのたびにrequire_onceする。
<?php
printf("%s\n", __FILE__);

$n = 1000000;
if ($argc >= 2) {
    $n = (int)$argv[1];
}

$startTime = microtime(true);

for ($i = 0; $i < $n; ++$i) {
    require_once('App/Hoge0.php');
    App_Hoge0::test();
}

$endTime = microtime(true);
printf("%8.5lf\n", $endTime - $startTime);
  • test2.php
    • spl_autoload_registerで似非クラスローダを登録
<?php
printf("%s\n", __FILE__);

$n = 1000000;
if ($argc >= 2) {
    $n = (int)$argv[1];
}

$startTime = microtime(true);

function _myLoader($className) {
    return @include(str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php');
}
spl_autoload_register('_myLoader');

for ($i = 0; $i < $n; ++$i) {
    App_Hoge0::test();
}

$endTime = microtime(true);
printf("%8.5lf\n", $endTime - $startTime);

実行

面倒なので、以下のようなスクリプトを書いて実行。

for i in {1,2,5}{,00,000,0000,00000,000000} ; do
    printf "%d" $i
    for t in test0.php test1.php test2.php ; do
        php ${t} ${i} | grep -v /home/ | awk '{printf(",%s", $0);}'
    done
    printf "\n"
done

結果(生データ)

1, 0.00011, 0.00011, 0.00014
100, 0.00013, 0.00030, 0.00016
1000, 0.00037, 0.00217, 0.00040
10000, 0.00264, 0.01997, 0.00298
100000, 0.02665, 0.20718, 0.02599
1000000, 0.24950, 1.90784, 0.25750
2, 0.00012, 0.00011, 0.00020
200, 0.00019, 0.00051, 0.00019
2000, 0.00062, 0.00419, 0.00065
20000, 0.00557, 0.04184, 0.00566
200000, 0.05527, 0.41456, 0.05114
2000000, 0.51434, 3.97734, 0.54890
5, 0.00014, 0.00013, 0.00015
500, 0.00025, 0.00227, 0.00036
5000, 0.00145, 0.00972, 0.00154
50000, 0.01421, 0.11185, 0.01292
500000, 0.13399, 0.98989, 0.13024
5000000, 1.27753,10.18646, 1.28340

結果(グラフ)

生データでもなんとなく分かると思うが、一応並べ替えてグラフ化してみる。

f:id:hhelibex:20150721160945p:plain

まぁ、火を見るより明らかというか‥。自前クラスローダ版(test2)が意外と健闘している。

そんなわけで(謎)、これからZend Framework本体のrequire_onceを消しまくる作業をしようと思う(と言っても、シェルコマンドのワンライナーで一括だが(謎))‥

2015/07/23追記

上記の検証だと、「require_onceが遅い」という証明にはならない気がしてきた。実行される命令数が違うのだから遅いのは当然。

ということで、test0.phpを以下のようにしてみた。

  • test0.php
    • 「何かの適当な処理」をする
<?php
printf("%s\n", __FILE__);

$n = 1000000;
if ($argc >= 2) {
    $n = (int)$argv[1];
}

$startTime = microtime(true);

require_once('App/Hoge0.php');
for ($i = 0; $i < $n; ++$i) {
    何かの適当な処理
    App_Hoge0::test();
}

$endTime = microtime(true);
printf("%8.5lf\n", $endTime - $startTime);

「何かの適当な処理」には以下の2通りのコードを入れて試してみた。

  1. require('./dummy.php'); (「dummy.php」は空のファイル)
  2. defined("___{$i}___");

そうしたら、こんな感じになった。

  • require('./dummy.php');

f:id:hhelibex:20150723145451p:plain

  • defined("___{$i}___");

f:id:hhelibex:20150723153641p:plain

Zend Frameworkのソースを見ると、確かにメソッドの中で、例えば例外を投げる直前にrequire_once('Zend/Exception');していたりするので(そういうのが何箇所もある)、それは無駄だろうが、諸悪の根源とまではいかないようだ‥

配列の統合時の先勝ち後勝ちの話

2つの配列$a$bをマージした結果として$expectedのようなものが欲しくて‥

<?php

$a = array(
            '11',
    'k2' => '22',
    'k4' => '44',
    'k6' => '66',
    'k8' => '88',
);
$b = array(
            '111',
    'k2' => '222',
    'k3' => '333',
    'k5' => '555',
    'k7' => '777',
);
$expected = array (
            '111',
    'k2' => '222',
    'k4' => '44',
    'k6' => '66',
    'k8' => '88',
    'k3' => '333',
    'k5' => '555',
    'k7' => '777',
);

+演算子array_merge関数を使ってみるが‥

<?php

$a = array(
            '11',
    'k2' => '22',
    'k4' => '44',
    'k6' => '66',
    'k8' => '88',
);
$b = array(
            '111',
    'k2' => '222',
    'k3' => '333',
    'k5' => '555',
    'k7' => '777',
);
$expected = array (
            '111',
    'k2' => '222',
    'k4' => '44',
    'k6' => '66',
    'k8' => '88',
    'k3' => '333',
    'k5' => '555',
    'k7' => '777',
);

printf("===%s===\n", 'expected');
var_dump($expected);
printf("===%s===\n", '$a + $b');
var_dump($a + $b);
printf("===%s===\n", '$b + $a');
var_dump($b + $a);
printf("===%s===\n", 'array_merge($a, $b)');
var_dump(array_merge($a, $b));
printf("===%s===\n", 'array_merge($b, $a)');
var_dump(array_merge($b, $a));

何かが違う‥

===expected===
array(8) {
  [0]=>
  string(3) "111"
  ["k2"]=>
  string(3) "222"
  ["k4"]=>
  string(2) "44"
  ["k6"]=>
  string(2) "66"
  ["k8"]=>
  string(2) "88"
  ["k3"]=>
  string(3) "333"
  ["k5"]=>
  string(3) "555"
  ["k7"]=>
  string(3) "777"
}
===$a + $b===
array(8) {
  [0]=>
  string(2) "11"
  ["k2"]=>
  string(2) "22"
  ["k4"]=>
  string(2) "44"
  ["k6"]=>
  string(2) "66"
  ["k8"]=>
  string(2) "88"
  ["k3"]=>
  string(3) "333"
  ["k5"]=>
  string(3) "555"
  ["k7"]=>
  string(3) "777"
}
===$b + $a===
array(8) {
  [0]=>
  string(3) "111"
  ["k2"]=>
  string(3) "222"
  ["k3"]=>
  string(3) "333"
  ["k5"]=>
  string(3) "555"
  ["k7"]=>
  string(3) "777"
  ["k4"]=>
  string(2) "44"
  ["k6"]=>
  string(2) "66"
  ["k8"]=>
  string(2) "88"
}
===array_merge($a, $b)===
array(9) {
  [0]=>
  string(2) "11"
  ["k2"]=>
  string(3) "222"
  ["k4"]=>
  string(2) "44"
  ["k6"]=>
  string(2) "66"
  ["k8"]=>
  string(2) "88"
  [1]=>
  string(3) "111"
  ["k3"]=>
  string(3) "333"
  ["k5"]=>
  string(3) "555"
  ["k7"]=>
  string(3) "777"
}
===array_merge($b, $a)===
array(9) {
  [0]=>
  string(3) "111"
  ["k2"]=>
  string(2) "22"
  ["k3"]=>
  string(3) "333"
  ["k5"]=>
  string(3) "555"
  ["k7"]=>
  string(3) "777"
  [1]=>
  string(2) "11"
  ["k4"]=>
  string(2) "44"
  ["k6"]=>
  string(2) "66"
  ["k8"]=>
  string(2) "88"
}

まぁ$b + $aは、内容的には合っているんだけど、ベースは$aで後勝ちしたいのでちょっと違和感‥

array_mergeは惜しいが違うし‥

見た目の順番が同じものが得られないかな‥と思い、意味的にはこんな感じで、

<?php

$a = array(
            '11',
    'k2' => '22',
    'k4' => '44',
    'k6' => '66',
    'k8' => '88',
);
$b = array(
            '111',
    'k2' => '222',
    'k3' => '333',
    'k5' => '555',
    'k7' => '777',
);

printf("===%s===\n", 'foreach');
$r = $a;
foreach ($b as $k => $v) {
    $r[$k] = $v;
}
var_dump($r);

こんな感じの結果が得られるやつ。

===foreach===
array(8) {
  [0]=>
  string(3) "111"
  ["k2"]=>
  string(3) "222"
  ["k4"]=>
  string(2) "44"
  ["k6"]=>
  string(2) "66"
  ["k8"]=>
  string(2) "88"
  ["k3"]=>
  string(3) "333"
  ["k5"]=>
  string(3) "555"
  ["k7"]=>
  string(3) "777"
}

で、いろいろ試行錯誤して、

<?php

$a = array(
            '11',
    'k2' => '22',
    'k4' => '44',
    'k6' => '66',
    'k8' => '88',
);
$b = array(
            '111',
    'k2' => '222',
    'k3' => '333',
    'k5' => '555',
    'k7' => '777',
);

printf("===%s===\n", 'array_merge(array_diff($a, array_intersect_key($a, $b)), $b)');
$r = array_merge(array_diff($a, array_intersect_key($a, $b)), $b);
var_dump($r);

で、見た目の順番は違うけど同じ結果が得られた。

array(8) {
  ["k4"]=>
  string(2) "44"
  ["k6"]=>
  string(2) "66"
  ["k8"]=>
  string(2) "88"
  [0]=>
  string(3) "111"
  ["k2"]=>
  string(3) "222"
  ["k3"]=>
  string(3) "333"
  ["k5"]=>
  string(3) "555"
  ["k7"]=>
  string(3) "777"
}

‥と喜んでいたら、array_replaceなるものがあった件。

<?php

$a = array(
            '11',
    'k2' => '22',
    'k4' => '44',
    'k6' => '66',
    'k8' => '88',
);
$b = array(
            '111',
    'k2' => '222',
    'k3' => '333',
    'k5' => '555',
    'k7' => '777',
);

printf("===%s===\n", 'array_replace($a, $b)');
$r = array_replace($a, $b);
var_dump($r);

見た目の順番まで期待通り。

===array_replace($a, $b)===
array(8) {
  [0]=>
  string(3) "111"
  ["k2"]=>
  string(3) "222"
  ["k4"]=>
  string(2) "44"
  ["k6"]=>
  string(2) "66"
  ["k8"]=>
  string(2) "88"
  ["k3"]=>
  string(3) "333"
  ["k5"]=>
  string(3) "555"
  ["k7"]=>
  string(3) "777"
}

まとめ

私が知る限りの配列の「足し算」をする演算/関数の違いの簡単なまとめ。

演算/関数 数値インデックス 文字列インデックス
$a + $b 先勝ち 先勝ち
array_merge($a, $b) 続きから追記 後勝ち
array_replace($a, $b) 後勝ち 後勝ち

知らない事がまだまだ多いな‥

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対象に入れて、パターン一覧を書いたファイルをバージョン管理するっていう矛盾したサンプル(何)を見つけて混乱したので‥