sprintfで使用する引数を直指定する方法

久々にPerl。いや、ほんと久しぶりよね。

printf '%d年%d月%d日', 2010, 12, 22; # 2010年12月22日

上記のような処理の時に、引数の数は変更せずに年を削って「12月22日」と表示したい場合、以下のようにすれば可能。

printf '%2$d月%3$d日', 2010, 12, 22; # 12月22日

「%」と「d」の間に「数値$」を追加することで使用する引数を直指定できる。上記の場合だと2番目と3番目の引数を使うという感じになる。

こんなsprintfの使い方なんて滅多なことではしないと思うけど、その滅多が来ちゃったので滅多ごめん。

滅多ごめん。


# ちなみにPHPでも可能。


参考:sprintf - perldoc.perl.org

Smartyのテンプレートで配列や連想配列を定義する方法

Smartyのテンプレートでは配列や連想配列を定義できない。そんな風に考えていた時期が俺にもありました。

さてググってみるとプラグインを利用する方法を発見しました。

前者は自作プラグインを作る方法、後者はsplitプラグインを利用する方法ですね。

しかしプラグインを利用する方法には以下の三つの問題点があります。

  1. 文字列の配列しか生成できない
  2. 多次元配列を生成できない
  3. メソッドに引数にそのまま渡せない

あくまで文字列を分割して配列を生成してるだけなのでオブジェクトの配列や多次元配列を生成できません。

またメソッドに渡せないというのは以下のようなケース

エラー。メソッドの引数にプラグインを使用できない
{$foo->bar(','|split:'a,b,c')}

一旦一時変数に入れないと渡せない。
{assign var='hoge' value=','|split:'a,b,c')}
{$foo->bar($hoge)}

さてこれらの問題を超無理やり解決するために僕が考えた方法はこれ。

<?php
class SmartyUtil {
    function ary () {
        $param = func_get_args();
        return $param;
    }
}

$smarty->assign('u',new SmartyUtil);
$smarty->display('test.tpl');

テンプレート側はこう。

{$u->ary('a','b')}

Smartyに常に渡すユーティリティクラスを用意しておいて、そこにaryというメソッドを用意しておく。

aryの実装は引数をそのまま配列にして返すだけのシンプルなもの。

これで文字でもオブジェクトでも何でも配列化が可能となる。

またメソッド呼び出しはネストが可能なので多次元配列も簡単に作れる

{$u->ary($u->ary('a','b'),$u->ary('c','d'))}

お次は連想配列。これもaryと同じく、ユーティリティクラスにメソッドを追加する。

<?php
class SmartyUtil {
    function ary_assoc () {
        $ret = array();
        $param = func_get_args();
        $count = count($param);
        if ($count  % 2 ) $param []= null;
        for($i=0;$i<$count;$i+=2) $ret[$param[$i]] = $param[$i+1];
        return $ret;
    }
}
{$u->ary_assoc('A','1','B','2')}

お後がよろしいようで。

PHPでスクリプト実行ファイルのディレクトリを得る方法

<?php

print dirname(__FILE__); // PHP5.3なら「__DIR__」でもOK

これだけで取ってこれる。

が、__FILE__定数はこの定数が書かれているファイルの絶対パスを取得するだけのものなので、この処理を別ファイルで関数化してもうまくいかない。

なのでrealpathと$argvの組み合わせを利用して関数化を実現する。

<?php

function get_script_dir () {
    return dirname(realpath($GLOBALS['argv'][0]));
}

これでOK。

PHPのDateTimeクラス

PHPのDateTimeクラスは標準装備なのでお手軽で便利なのだけど、若干使い勝手が悪い。

なのでDateTimeを継承してちょっとした拡張をしとくと便利かと。

<?php

class MyDateTime extends DateTime {
    function year () {
        return $this->format('Y');
    }
    function month () {
        return $this->format('n');
    }
    function day () {
        return $this->format('j');
    }
    function hour () {
        return $this->format('H');
    }
    function minute () {
        return $this->format('i');
    }
    function second () {
        return $this->format('s');
    }
    function week () {
        return $this->format('w');
    }
    function week_str () {
        static $hash = array('','','','','','','');
        return $hash[$this->format('w')];
    }
    function ymd ($sep='-') {
        return $this->format('Y'.$sep.'m'.$sep.'d');
    }
    function ym ($sep='-') {
        return $this->format('Y'.$sep.'m');
    }
    function next_days ($num=1) {
        $this->modify("+$num day");
        return $this;
    }
    function prev_days ($num=1) {
        $this->modify("-$num day");
        return $this;
    }
    function next_months ($num=1) {
        $this->modify("+$num month");
        return $this;
    }
    function prev_months ($num=1) {
        $this->modify("-$num month");
        return $this;
    }
    function next_years ($num=1) {
        $this->modify("+$num year");
        return $this;
    }
    function prev_years ($num=1) {
        $this->modify("-$num year");
        return $this;
    }
    function set_year($year) {
        $this->setDate($year,$this->month(),$this->day());
        return $this;
    }
    function set_month($month) {
        $this->setDate($this->year(),$month,$this->day());
        return $this;
    }
    function set_day($day) {
        $this->setDate($this->year(),$this->month(),$day);
        return $this;
    }

    // 月末に設定する
    function set_lday_of_month() {
        $this->setDate($this->year(),$this->month(),'01');
        $this->modify('+1 month -1 day');
        return $this;
    }
    
    // 引数で指定した日付までの年単位のリストを返す
    function year_range ($end_dt) {
        $start_dt = clone $this;
        
        $end_str = $end_dt->format('Y');
        $dt = array();
        while( $start_dt->format('Y') <= $end_str ) {
            $dt []= clone $start_dt;
            $start_dt->modify('+1 year');
        }
        
        return $dt;
    }
    
    // 引数で指定した日付までの月単位のリストを返す
    function month_range ($end_dt) {
        $start_dt = clone $this;
        
        $end_str = $end_dt->format('Ym');
        $dt = array();
        while( $start_dt->format('Ym') <= $end_str ) {
            $dt []= clone $start_dt;
            $start_dt->modify('+1 month');
        }
        
        return $dt;
    }
    
    // 引数で指定した日付までの日単位のリストを返す
    function day_range ($end_dt) {
        $start_dt = clone $this;
        
        $end_str = $end_dt->format('Ymd');
        $dt = array();
        while( $start_dt->format('Ymd') <= $end_str ) {
            $dt []= clone $start_dt;
            $start_dt->modify('+1 day');
        }
        
        return $dt;
    }
    
    // YYYY01〜YYYY12までのリストを返す
    function month_list () {
        $start_dt = clone $this;
        $start_dt->setDate($this->year(),'01','01');
        $end_dt = clone $start_dt;
        $end_dt->setDate($this->year(),'12','01');
        
        return $start_dt->month_range($end_dt);
    }
    
    // YYYYMM01〜YYYYMM31(月によって変わる)までのリストを返す
    function day_list () {
        $start_dt = clone $this;
        $start_dt->setDate($this->year(),$this->month(),'01');
        $end_dt = clone $start_dt;
        $end_dt->modify('+1 month -1 day');
        
        return $start_dt->day_range($end_dt);
    }
    
    function hour_list () {
        $start_dt = clone $this;
        $start_dt->setTime(0,$this->minute(),$this->second());
        $end_dt = clone $start_dt;
        $end_dt->setTime(23,$this->minute(),$this->second());
        
        $dt = array();
        while( $start_dt->format('YmdHis') <= $end_dt->format('YmdHis') ) {
            $dt []= clone $start_dt;
            $start_dt->modify('+1 hour');
        }
        
        return $dt;
    }
    
    function copy() {
        return clone $this;
    }
    
    function __clone() {}
    
    function __toString(){
        return $this->format('Y-m-d H:i:s');
    }
}

使用例

<?php

require_once 'MyDateTime.class.php';

$now = new MyDateTime; // 現在の時間

$now->year();  // 年
$now->month(); // 月
$now->day();   // 日
$now->ymd();   // 年-月-日

$date = $now->copy(); // 複製作成(clone $nowと同じ意味)

$date->next_months(3)->set_day(1); // 3ヶ月進めて、1日にセット

メソッドチェインができるので割と重宝してます。

ちなみにPHP5.3だとDateTime周りも色々と拡張されてるみたいですが、まだまだPHP5.2の環境も多いと思うのでこういうクラスを拵えておくと良いんじゃないでしょうか

PHPでPath_Classを使ってらくらくディレクトリorファイル操作

先日の記事だけを見ると、Perlを使ったこと無い人からすれば何がどう便利なのか良く分からないと思うのでここらでPath_Classの解説。

まずPath_Classの大きな特徴として、引数で与えたパスが、実際にファイルとして存在してなくても良いというところがあげられるでしょう。

<?php

require_once 'Path/Class.class.php';

// 存在しないパスを与えてもまったく問題なく動く
$dir = cdir('/foo/bar/baz');

// 一個戻ったり
$dir->parent(); # /foo/bar

// 一個進んだり
$dir->subdir('hoge'); # /foo/bar/baz/hoge

// ディレクトリ生成しちゃったり
$dir->mkpath(); # /foo/bar/baz/というディレクトリが作られる。

// ファイルのパスを設定してみたり
$file = $dir->file('aaa.txt'); # /foo/bar/baz/aaa.txt

// 実際に書き込んでみたり
$file->put_contents('hogehoge'); # /foo/bar/baz/aaa.txtにhogehogeと書き込まれる

// 読み込んでみたり
$data = $file->slurp(); # hogehoge

更に「..」を与えてもちゃんと展開するようになっています。

<?php

require_once 'Path/Class.class.php';

// ..も展開してくれる
$dir = cdir('/foo/../bar/baz'); # /bar/baz

PHPにはこのような「..」の展開をやってくれる関数としてrealpathという標準関数が存在していますが、これは実際にそのパスが存在していないと思ったとおりの動作にならないのが難点です。

さてお次は、実際の使用例を上げてみましょう。

日付のディレクトリ名を作り、そこにログを吐くプログラムを考えてみましょう。

まず普通にやってみた場合

<?php

$now = '20100914';
$log_root = '/home/bar/log/';
$log_file = 'log.txt';
$log_data = 'log data';


if ( !file_exists($log_root.DIRECTORY_SEPARATOR.$now) ) {
    // 日付のディレクトリが無ければ作る
    mkdir($log_root.DIRECTORY_SEPARATOR.$now,0777,true);
}

// 書き込み
file_put_contents($log_root.DIRECTORY_SEPARATOR.$now.DIRECTORY_SEPARATOR.$log_file,$log_data);

こんな感じになりますよね。DIRECTORY_SEPARATORが長ったらしくてウザイことこの上ない。

しかも$log_rootは最後に「/」が入ってるのにDIRECTORY_SEPARATORをつけちゃうと「//」になっちゃうので微妙ですよね。かといって毎回最後に「/」が入ってるかどうかをチェックするのもメンドクサイ。

この処理がPath_Classを使うことによってどう変化するか?

<?php
require_once 'Path/Class.class.php';

$now = '20100914';
$log_root = '/home/bar/log/';
$log_file = 'log.txt';
$log_data = 'log data';

$dir = cdir(array($log_root,$now));

if ( !$dir->exists() ) {
    // 日付のディレクトリが無ければ作る
    $dir->mkpath();
}

// 書き込み
$dir->file($log_file)->put_contents($log_data);

はい!どうです?ね?簡単でしょう?

え?行数増えてるだって?おいおい何処を見てるんだよ。行数は確かに増えてるけどタイプ数減ってるだろ!

DIRECTORY_SEPARATORから解放されるだけで物凄くコードが簡素になりましたね。

またディレクトリを再帰的に評価していくのも簡単です。recurseというメソッドに引数としてコールバック用の関数を与えて使います。

<?php

require_once 'Path/Class.class.php';

function foo ($path) {
    print $path->basename()."\n"; // ファイル名のみ表示
}

$dir = cdir('./');
$dir->recurse('foo');

これで「./」以下の全てのファイルを出力できます。

また、あるディレクトリの中身だけが欲しい場合はchildrenメソッドが利用できます。

<?php
require_once 'Path/Class.class.php';

$dir = cdir('./');
foreach($dir->children() as $file) {
    print $file->basename()."\n";
}

recurseと違って、配列の戻り値を返すので、何かとそのまま利用するのに便利です。

あとcdirやcfileという関数が勝手に定義されるが微妙だなという人はPath/Class.class.phpをrequireせずに、Path/Class/Dir.class.php等をrequireすればOKです。

<?php

require_once 'Path/Class/Dir.class.php';

$dir = new Path_Class_Dir('./');

結局のところcdirやcfileという関数はnew Path_Class_Dirやnew Path_Class_Fileの単なるショートカットです。

さてさて、大体主要なメソッドに関してはこれくらいかな。

残りの細かいメソッドに関しては実装見れば大体わかるかなと思います。まぁいずれドキュメントちゃんと書こうとはおもってますがっ・・・!

PHP版Path::Class

え?あぁ。Perlerなら皆さんご存知のPath::Classですが、それのPHP版です。欲しかったので作りました。

以下使い方例。

<?php
require_once 'Path/Class.class.php';

$dir  = cdir(array('foo', 'bar'));      # Path_Class_Dirオブジェクト
$file = cfile(array('bob','file.txt')); # Path_Class_Fileオブジェクト

print "dir: $dir\n";  # foo/bar
print "dir: $file\n"; # bob/file.txt

$subdir  = $dir->subdir('baz');  # foo/bar/baz
$parent  = $subdir->parent();    # foo/bar
$parent2 = $parent->parent();    # foo

$dir2 = $file->dir();            # bob

# DirectoryIteratorオブジェクトを返す
$dir_handle = $dir->open();

# SplFileObjectオブジェクトを返す
$file_handle = $file->open();

まんまですね。

Perl版と違うのはショートカット関数がcfileやcdirのようにcが先頭についているところです。これはfileやdirだとPHPの場合、既に標準関数で存在しているからです。

childrenやrecurseもあるし、PHP版のみの機能としてget_contentsやput_contentsも用意してる親切設計ッ!

さて、今日始めてgitを使ってみました。が、正直全然わかりません!

でもgithubをどうしても使ってみたかったので色々調べて一応なんとかなりました。

また、github初めて記念としてPHP版HTML_FillInFormやPHPSQL_Abstractもアップしてみました。

実はこれらのPHPクラス、今までにちょこちょこバージョンアップしてたのですが、いかんせん放置気味だったので一旦区切りを付けるという意味でgithubにアップしてみた次第です。

使ってる人はそんなにいないかもしれませんが、今後ともよろしゅうお願いします。

・・・と、思ってたらid:naoyaさんがSQL_Abstract関連の物凄いタイムリーな記事をアップしててちょっとビビッた。俺がgithubにアップするために今日一日色々準備してたのを知っていたというのかッ!

とまぁそういうことで、約半年ぶりの更新でした。

Path_Class解説→PHPでPath_Classを使ってらくらくディレクトリorファイル操作 - Unknown::Programming

ArrayObjectと多次元配列とisset

<?php
error_reporting( E_ALL );

$ary = array();

if ( isset($ary['foo']['bar']) ) {}

は警告は出ないが

<?php
error_reporting( E_ALL );

$ary = new ArrayObject();

// Notice: Undefined index:  foo
if ( isset($ary['foo']['bar']) ) {}

は警告が出る。

ArrayObjectの場合、あくまでも配列っぽい動きをエミュレートしてるだけなので上記のような多次元の配列に対してissetをしてもArrayObject::offsetGetの時点で警告が出てしまう。

回避方法はぶっちゃけ無くて地道に

<?php
error_reporting( E_ALL );

$ary = new ArrayObject();

if ( isset($ary['foo']) && isset($ary['foo']['bar']) ) {}

としなければならないっぽい。

楽するために使ってるArrayObjectのはずなのに、楽じゃなくなってる謎。