PHPで文の中間を省略して一定の幅に収める

「文の中間を省略して一定の幅に収める」処理は需要がありそうですが、検索してみた限りでは見つからなかったので作ってみました。

PHPには、文字列が一定の長さ(幅)を超えたら丸める関数 mb_strimwidth()が標準で有ります。しかし、この関数だけでは文字列の中間を省略する事はできませんので、この関数を応用して、一定の幅を超えたら指定位置の文字列を省略する関数にしました。少し長いですが、ソースコードは下記の通りです。

<?php
// 文字列の途中を省略して最大幅以内にする
// string $str        = 対象文字列
// int    $maxWidth   = 最大幅 (mb_strwidth準拠)
// int    $cutPos     = 省略位置。正の値は先頭から(0が先頭)、負の値は末尾からの幅。Falseで中央。
// string $trimMarker = 省略記号にする文字列
// @return string
function sMidTrim ($str, $maxWidth, $cutPos = False, $trimMarker = '..') {

    // 文字列の幅を取得
    $strWidth = mb_strwidth($str);
    if($strWidth <= $maxWidth) { return $str; }

    // マーカーの幅を取得
    $tmWidth = mb_strwidth($trimMarker);
    if($tmWidth + 2 > $maxWidth) { return $str; }

    // カットすべき幅を算出
    $cutoffWidth = $tmWidth + $strWidth - $maxWidth;

    // カットの始点(幅)を算出
    $remain = $strWidth - $cutoffWidth;
    if(!is_int($cutPos)) {
        // 無指定時は中央
        $cutBeginWidth = ceil( $remain / 2 );
    }
    elseif($cutPos >= 0) {
        // 正の値
        if($cutPos > $remain) { $cutPos = $remain; }
        $cutBeginWidth = $cutPos;
    }
    else {
        // 負の値
        $cutBeginWidth = $remain + $cutPos;
        if(0 > $cutBeginWidth) { $cutBeginWidth = 0; }
    }

    // 先頭文字列を切り出す
    $headStr = mb_strimwidth($str, 0, $cutBeginWidth);

    // 末尾文字列を切り出す
    $pos = mb_strlen( mb_strimwidth($str, 0, $cutBeginWidth + $cutoffWidth) );
    $tailStr = mb_substr($str, $pos);

    return $headStr.$trimMarker.$tailStr;
}

解説

出来る限りソースコードのみで解るように書いたつもりですが、いくつか補足します。

引数のうち、第2及び第3引数は「幅」単位で指定するようになっています。 これはmb_strimwidth()で使われる幅と同じものです。

第3引数で指定された位置に省略記号(第4引数で指定可)が挿入されます。 なお、末尾に省略記号を付ける動作はmb_strimwidth()で可能ですので省きました。

第1引数で渡された文字列が第2引数の幅に満たない場合は、何も処理を行わず文字列をそのまま返します。 省略記号が付加されるのは、省略しないと指定された幅に収まらない場合のみです。

関連記事 PHPのmb_strimwidthが意図通り動かない問題

利用時の注意

PHP5.6で動作確認しました。PHP5以降であれば恐らく動くと思います。

マルチバイト文字列関数(mb_*)を呼び出していますので、実際の挙動はそれらの関数に依存します。 予めmb_internal_encoding()で適切な内部文字エンコーディングを設定してからご利用ください。

コードは非商用に限り、そのままご利用頂いても構いません。
適切に動作するよう心がけましたが、不具合があるかもしれません。無保証ですのでその点はご了承願います。利用やバグ等の報告は頂ければ嬉しいです。

テーマ: プログラミング - ジャンル: コンピュータ

PHPのmb_strimwidthが意図通り動かない問題

PHPのmb_strimwidth()は文字列を指定した幅に丸めることが出来る便利な関数ですが、この関数に固有の特殊な点がいくつかあり、意図通りに動作しないことも多いかと思います。それらの点について解説します。

mb_strimwidthの引数
string mb_strimwidth ( string $str , int $start , int $width [, string $trimmarker = "" [, string $encoding = mb_internal_encoding() ]] )

mb_strimwidthの「幅」とは

PHPのマルチバイト文字列関数(mb_*)の多くは文字列をバイトではなく文字単位で扱い、まさにそれがこの関数群を利用するメリットなわけですが、mb_strimwidth()mb_strwidth()は文字列の「幅」を扱うようになっています。

これらの関数が扱う「幅」とは、文字数やピクセル数のことではありません。1バイト文字の幅を1、全角文字を含むマルチバイト文字の幅を2として計算した、画面に表示される際のおおよその推定幅を意味します。
実際の表示においては文字列のピクセル幅はフォント等に依存しますので、きっちり同じ幅にはなりませんが、ある程度揃えば良い場合には便利な単位です。

第2引数 $startの注意点

$startで指定するのは、丸めの開始位置ではなく切り出し始めの位置です。従って、文字列の先頭を切る必要がなく、末尾だけを丸めたい場合は 0と指定します。

また、単位が「幅」ではなく「文字数」である点にも注意が必要です。続く第3引数は幅で指定しますから余計に解りにくくなっています。 $startを幅で指定したい場合は一旦mb_strimwidth()で任意の幅を切出し、その文字数をmb_strlen()でカウントしてからもう一度mb_strimwidth()を使う必要があります(ちょっと困りものですね)。

<?php
// 3文字目から幅6相当の文字列を取得
$s = mb_strimwidth('さんぷるabcde', 2, 6);

// $s = 'ぷるab'

第4引数 $trimmarkerの注意点

$trimmarkerの幅は第3引数$widthに含まれます。

<?php
$s = mb_strimwidth('さんぷるabcde', 2, 6, '..');

// $s = 'ぷる..'

$widthの幅に$trimmarkerを含みたくない場合は、下記のようにします。

<?php
$s = mb_strimwidth('さんぷるabcde', 2, 6).'..';

// $s = 'ぷるab..'

内部文字エンコーディングの指定を忘れずに

マルチバイト文字列関数による文字数及び幅の処理は、内部文字エンコーディングに依存しています。内部文字エンコーディングと扱う文字列の文字コードが一致しない場合は正常に動作しませんので、必ず指定しましょう。また、ソースコード自体の文字コードの不一致も文字化けの原因になりますので注意しましょう。

予めmb_internal_encoding()で一度指定しておけば、以降内部文字エンコーディングに依存する関数は全てこれに従います。また、関数の引数でその都度文字コードを指定することもできます。

<?php
// UTF-8の場合
mb_internal_encoding('UTF-8');

// SJIS(Windows)の場合
mb_internal_encoding('CP932');

CSSを使う方法もある

ウェブブラウザ上で特定サイズ内に文字列を収めたい場合は、mb_strimwidth()ではなくCSSを使う方法もあります。

CSSでは、text-overflow: ellipsisoverflow: hiddenを併用することで、指定されたボックス内に収まらないテキストは自動的に省略されます。こちらのほうがPHPで処理するよりも綺麗に収まりますが、複数行に対応させようとすると記述が複雑になる等の難点もあります。これらのプロパティの詳細は下記URLをご参照ください。

テーマ: プログラミング - ジャンル: コンピュータ

PHPで今日の0時のunixタイムを取得する

PHPで記事の投稿日時などを処理する時、その日時が今日なのか、昨日あるいはそれ以前なのかを判定したい場合があります。
そこで、今日の0時0分0秒のタイムスタンプを取得・比較することで判定する方法を考えてみます。

strtotime()で今日の0時0分0秒のUnixタイムスタンプを取得

strtotime('today') で今日0時のタイムスタンプを取得して、その前後を if()で比較判別する。
取得した値に 3600*24*n を加減算すれば n日前などの判別も可能。

strtotime()では他にも、'yesterday' で昨日、'tomorrow' で明日など、相対的な書式指定で様々なUnixタイムスタンプを取得できる。
また、月末の日を取得する 'last day of' も便利。

mktime()で上記と同じUnixタイムスタンプを取得

mktime(0,0,0) でも strtotime('today') と同じ結果が得られる。
mktime(8,0,0) で今日の8時丁度を取得する等、任意の時間を取得することも可能。
文字列の変換が無いので、たぶん一番軽い。

パラメータは mktime(時, 分, 秒, 月, 日, 年) で全て省略可能、省略時は現在の日時になる。

今日かどうかだけを判別する

タイムスタンプを使わずに、date()で今日現在の日付と $unix_timeの日付を比較して判定。
簡易的だが、2回date()で変換するので多少重いかもしれない。

if( date('Ymd') == date('Ymd', $unix_time) ){ ... }

Windows版PHPで日本語ファイル名が正しく扱えない問題について

Windows版のPHP(5.3~7.0)では、日本語等の全角文字(非ASCII文字)を含むファイル名の取得や指定に失敗することがあります。
例えば glob()や scandir()、RecursiveDirectoryIteratorなどのイテレータを使ってファイル名一覧を取得した時、一部のファイルが抜け落ちてしまいます。

原因

原因はどうやら二つあるようです。
一つは、Windows版PHPはShift_JISでファイル名を扱うようで、Unicodeを含むファイル名を無視してしまう為です。
もう一つは、いわゆる5c問題です。Shift_JISでは全角文字の2バイト目が \記号(16進の0x5c, バックスラッシュ)を含むことがあり、これがエスケープ文字やディレクトリ名の区切りと誤認識されてしまう事に起因する不具合です。

解決策

これに対する解決策はないかと、様々試行錯誤してみたのですが解決できませんでした。
glob()を使ってもイテレータを使ってもダメだったので、setlocale(LC_ALL, 'Japanese_Japan.932')や mb_internal_encoding('CP932')等で5c問題だけでも解決するかもしれないと思い試してみましたが、こちらもダメでした。
よって、Windows版PHP(5.3~7.0)では非ASCII文字のファイル名を全て正しく扱うことは残念ながら出来ないというのが結論です。

ただ、色々調べていて解ったことがあります。
実は、PHP5.2までは(恐らく5c問題については)大丈夫だったようなのです。
つまりこれは、PHP5.3以降、PHP7.0までに固有のバグだということです。
更にこちらの記事によれば、このバグはPHP7.1で遂に修正され、喜ばしいことにShift_JISの0x5cやUnicodeを含むファイル名もUTF-8で正しく扱えるようになるとの事です!やったー(´;ω;`)ウッ…

サーバ運用では日本語ファイル名をどうしても扱わなければならないケースというのはあまり無いとは思いますが、やはり正しく扱えれば有り難い事ですね。

テスト環境

PHP 5.6.15
Windows 10

参考


多元配列の内容でソート出来るPHP関数

PHPには、2次元配列の任意のキーの内容でソートが出来る大変便利な関数があります。
array_multisort() がソレですが、公式マニュアルの説明ではいまいち使い方が解りにくい。
で、検索してみたら使い方を解り易く説明してあるサイトがいくつかありました。

Blog.okuryu : PHP の array_multisort で多次元配列をソートする
PHP array_multisortの例文 - Blog::R.1000Leaf
PHPのarray_multisort関数が激便利だったので紹介 : akiyan.com

Blog.okuryuさんの説明が個人的には理解しやすかったです。
foreachで予め準備しなきゃいけないのが多少面倒ではありますが、それでもこれだけ簡潔に書けるのは有り難いですね。

テーマ: プログラミング - ジャンル: コンピュータ