PHP 8.3のmb_str_padで日本語を埋める|文字数と表示幅のズレに注意

mb_str_pad は、マルチバイト文字列を指定した「文字数」になるまで埋める PHP 8.3 追加の関数です。str_pad がバイト数で数えるため日本語では崩れていた処理を、文字(コードポイント)単位で行えます。

日本語を str_pad で桁揃えして、「あれ、全然揃わない」となった経験は、PHP を触っていればたぶん一度はあると思います。私も CLI の出力を整えようとして、パディングがまったく入らず首をひねったことがあります。mb_str_pad で解決しますが、実は「揃う」とは言い切れない落とし穴がもう1つあります。

なぜstr_padは日本語で崩れるのか

str_pad は文字数ではなくバイト数で長さを判断します。UTF-8 の日本語は1文字が3バイトなので、「あい」はすでに6バイトあり、目標を6にすると「もう足りている」と見なされて何も起きません。

<?php
$s = str_pad('あい', 6, '*');
var_dump($s);          // string(6) "あい"  ← パディングなし
var_dump(strlen($s));  // int(6)

エラーも警告も出ず、ただ何も起きないのがやっかいなところです。見た目が揃わないだけなので、テストを書いていないと気づきにくいかもしれません。

mb_str_padはどう書くのか

PHP 8.3 以降なら mb_str_pad が使えます。シグネチャは str_pad にエンコーディングの引数が加わった形で、第2引数の長さはコードポイント数として扱われます。

<?php
// mb_str_pad(string $string, int $length, string $pad_string = " ",
//            int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string
var_dump(mb_str_pad('あい', 6, '*'));                  // string(10) "あい****"
var_dump(mb_str_pad('あい', 6, '*', STR_PAD_LEFT));    // string(10) "****あい"
var_dump(mb_str_pad('あい', 7, '*', STR_PAD_BOTH));    // string(11) "**あい***"
var_dump(mb_str_pad('あい', 1, '*'));                  // string(6) "あい"

STR_PAD_BOTH で余りが出るときは、str_pad と同じく右側に多く入ります。長さが元の文字列以下なら、切り詰められることはなく元の文字列がそのまま返ります。これは str_pad と同じ挙動なので、移行時に身構えなくて大丈夫です。

空文字を埋め草に渡すと ValueError になる点も str_pad と共通です。可変の値を渡すときだけ、頭の片隅に置いておけば十分だと思います。

なぜ全角と半角が混ざると揃わないのか

ここが今回いちばん言いたかったところです。mb_str_pad が数えるのは「文字数」であって、画面上の「幅」ではありません。等幅フォントでは、全角文字は半角2つ分の幅を取るので、文字数で揃えても行末はガタつきます。

<?php
var_dump(mb_strlen('あい'));    // int(2)  文字数
var_dump(mb_strwidth('あい'));  // int(4)  表示幅(全角は2)
var_dump(mb_strwidth('ab'));    // int(2)

$rows = ['りんご' => '120', 'Apple' => '80'];
foreach ($rows as $name => $price) {
    echo mb_str_pad($name, 12, '.') . $price . "\n";
}
// りんご.........120   ← 全角3文字+9個で、見た目は15桁ぶん
// Apple.......80       ← 半角5文字+7個で、見た目は12桁ぶん

CLI の表や固定幅のログなど、見た目の桁を揃えたいときは、mb_strwidth で幅を測って自前で埋めるほうが目的に合います。

表示幅で揃えるにはどう書くのか

mb_strwidth は全角を2、半角を1として数えます。目標の幅から現在の幅を引いた分だけ埋め草を足せば、等幅フォントで揃った出力になります。

<?php
function padWidth(string $s, int $width, string $pad = ' '): string
{
    $diff = $width - mb_strwidth($s);
    return $diff > 0 ? $s . str_repeat($pad, $diff) : $s;
}

$rows = ['りんご' => '120', 'Apple' => '80', 'みかん' => '5'];
foreach ($rows as $name => $price) {
    echo padWidth($name, 12, '.') . $price . "\n";
}
// りんご......120
// Apple.......80
// みかん......5

この関数では埋め草を半角1文字で想定しています。全角の埋め草を使うと幅の端数が合わなくなるので、半角に限るのが無難です。何が嬉しいかというと、出力の見た目が「文字数ではなく幅」で決まるので、全角と半角の混在でも崩れなくなる点ですね。

ひとつ補足すると、mb_strwidth の判定は Unicode の East Asian Width に基づいていて、絵文字や結合文字は端末やフォントによって実際の見た目と合わないことがあります。そこまで厳密に揃えたいなら、そもそも等幅の出力に頼らない設計のほうが楽かもしれません。

PHP 8.2以前ではどうするのか

mb_str_pad は PHP 8.3 で入った関数なので、8.2 以前の環境では未定義エラーになります。ライブラリを書いている場合は、function_exists で存在を確認してから使うか、symfony/polyfill-mbstring のようなポリフィルに任せる方法があります。

<?php
if (!function_exists('mb_str_pad')) {
    // 8.2以前向けの簡易な代替(右埋めのみ)
    function mb_str_pad(string $s, int $length, string $pad = ' '): string
    {
        $diff = $length - mb_strlen($s);
        return $diff > 0 ? $s . mb_substr(str_repeat($pad, $diff), 0, $diff) : $s;
    }
}

自作のポリフィルは、左埋めや両側埋めまで対応しようとすると意外と手間が増えます。必要なのが右埋めだけなら上のように割り切るのも手ですが、複数の向きが必要なら実績のあるポリフィルを使うほうが安心だと思います。

まとめ

mb_str_pad は、str_pad の「バイトで数える」問題を文字数ベースに直してくれる関数です。ただし揃うのは文字数であって、全角と半角が混ざる表示の桁揃えには mb_strwidth を使った自前の埋め処理が向いています。どちらを使うかは、揃えたいのが文字数か見た目の幅か、で決めると迷いにくい気がしています。

よくある質問

Q. mb_str_pad は PHP 8.3 より前でも使えますか?
A. 標準では使えません。PHP 8.3 で追加された関数なので、8.2 以前では未定義エラーになります。ポリフィルを入れるか、function_exists で分岐してください。

Q. 全角と半角が混ざる表を揃えるにはどうすればよいですか?
A. mb_strwidth で表示幅を測り、目標幅との差だけ半角スペースなどを足すのが確実です。mb_str_pad は文字数で数えるため、全角が混ざると桁がずれます。

Q. str_pad で日本語が崩れるのはなぜですか?
A. str_pad がバイト数で長さを判断するからです。UTF-8 の日本語は1文字3バイトなので、指定した長さに届いたと見なされ、埋め草が入らないことがあります。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です