罠
全角を2,半角を1として数え、指定した幅で切り取ってくれる便利なmb_strimwidth()
ですが、以下のように数え間違えに見える罠が存在します。
mb_strimwidth("123456", 0, 5, "…", "UTF-8"); // 1234…
mb_strimwidth("…123456", 0, 5, "", "UTF-8"); // …1234
それぞれ「123…」「…123」になって欲しいのです。
mb_strwidth
単体で確認すると3点リーダが半角1文字として数えられてることがわかります。
mb_strwidth("A", "UTF-8"); // 1
mb_strwidth("あ", "UTF-8"); // 2
mb_strwidth("…", "UTF-8"); // 1
何故?
これはUnicode附属書#11で3点リーダ(U+2026)の幅はコンテキストによってA(Ambiguous; 曖昧)と定義され、さらにコンテキストが分からない場合は狭い文字として扱えと指示されています。
PHPでもそれに従おうという経緯があり、3点リーダなどは1として数えられるようになったようです。
実際自分の環境ではQiitaでの表示も「A」と同じ幅になっており間違いと言い切れないことが分かります。
でも困る
こういった文字長で切る処理はWebの場合はCSSで対応すれば楽ですが、そうでない帳票などの場合に困ります。
しかもそういった場合は大抵MSゴシックといった「…」を全角で表現するフォントが使われ、件の数え方では不都合が生じます。
代替案
その場合の代替案としてmb_strcut
をSJIS文字列に対して適用するのが簡単だと思います。
mb_strcut
は全角半角の幅ではなくマルチバイト境界を認識しつつバイト数で切ってくれます。
そこで「…」など全角になって欲しい文字が2バイト、半角カナが1バイトになるSJISエンコーディングを使えば都合が良いというわけです。
その理屈を使ってmb_strimwidthを実装しなおすとこんな感じでしょうか。
function mb_strimwidth2($str, $start, $width, $trimmarker="", $encoding=null) {
if (is_null($encoding)) {
$encoding = mb_internal_encoding();
}
$str = mb_substr($str, $start, null, $encoding);
$str_ = mb_convert_encoding($str, "SJIS-win", $encoding);
$trimmarker_ = mb_convert_encoding($trimmarker, "SJIS-win", $encoding);
if (strlen($str_) > $width) {
$result = mb_strcut($str_, 0, $width - strlen($trimmarker_), "SJIS-win") . $trimmarker_;
return mb_convert_encoding($result, $encoding, "SJIS-win");
} else {
return $str;
}
}
mb_strimwidth
の動きに合わせてみましたが、実際の場面ではSJISだったりすると思うのでmb_convert_encoding
などは使わないかも知れません。
また、$start
パラメータの扱いがmb_strcut
ではバイト、mb_strimwidth
では文字数だったので合わせるためmb_substr
使ってますがこれも0を指定するなら要らない所です。
(mb_strimwidth
の$start
が文字幅じゃないというのはそれはそれで罠かも知れません。)
ちなみに
1とか2ではなくプロポーショナルフォントにも対応した本当の文字幅が欲しい時はTTFフォントで文字描画する機能を持った画像orPDFライブラリを使うことになると思います。
「bounding box」「metrics」「measure」的な単語の入ってるメソッドが大抵あります。