$str = 'あ';
$str = str_pad($str, 5, 'い');
var_dump($str);
その後ろの変なのはなんだ?
ということでマルチバイト文字列関数にめでたくmb_str_pad
が追加される運びになりました。
以下は該当のRFC、mb_str_padの日本語訳です。
PHP RFC: mb_str_pad
Introduction
PHPでは、様々な文字列関数についてふたつのバリアントが存在します。
すなわち1バイト文字とマルチバイト文字です。
しかし、何故かマルチバイト文字列関数にはstr_padに相当するmb_str_padが存在しません。
str_padはマルチバイト文字をサポートしていないため、UTF-8などのマルチバイト文字を使う場合に問題が発生します。
本RFCでは関数mb_str_pad()
の追加を提案します。
Proposal
本Proposalでは、新しいマルチバイト文字列関数mb_str_pad
を導入します。
入力文字とパディング文字のいずれもがマルチバイト文字列である可能性があります。
構文はstr_pad
と同じですが、追加で文字コード引数を受け取ります。
文字コード引数は、他のマルチバイト文字関数と同じであり、入力文字とパディング文字両方に適用されます。
引数がない場合はデフォルトの文字コードが使用されます。
引数$pad_type
はstr_pad
と同じくSTR_PAD_LEFT
・STR_PAD_RIGHT
・STR_PAD_BOTH
のいずれかです。
function mb_str_pad(
string $string,
int $length,
string $pad_string = " ",
int $pad_type = STR_PAD_RIGHT,
?string $encoding = null
): string {}
Error conditions
mb_str_pad
のエラーは、基本的にstr_pad
と同じ条件で発生します。
・$pad
は空白であってはならない。
・$pad_type
はSTR_PAD_LEFT
・STR_PAD_RIGHT
・STR_PAD_BOTH
のいずれかでなければならない。
追加のエラーが一つ存在します。
・$encoding
は有効であり、サポートされている文字コードでなければならない。
Examples and Comparison Against str_pad()
マルチバイト文字列へのstr_pad()
とmb_str_pad()
の動作のちがいを示します。
str_pad()
では一部の言語や記号において使われているマルチバイト文字で問題が発生します。
最初の例はFrançais
という単語に対するものです。
これは一見8バイトですが、ç
は2バイトであるため実際は9バイトの長さになっています。
従ってmb_str_pad()
では正しく動作しますが、str_pad()
では想定外の挙動になります。
// 全長が10バイトになる
var_dump(str_pad('Français', 10, '_', STR_PAD_RIGHT)); // string(10) "Français_"
var_dump(str_pad('Français', 10, '_', STR_PAD_LEFT)); // string(10) "_Français"
var_dump(str_pad('Français', 10, '_', STR_PAD_BOTH)); // string(10) "Français_"
// 全長が10文字になる
var_dump(mb_str_pad('Français', 10, '_', STR_PAD_RIGHT));// string(11) "Français__"
var_dump(mb_str_pad('Français', 10, '_', STR_PAD_LEFT)); // string(11) "__Français"
var_dump(mb_str_pad('Français', 10, '_', STR_PAD_BOTH)); // string(11) "_Français_"
この問題は、アルファベット以外、たとえばギリシャ語などではさらに顕著になります。
var_dump(str_pad('Δεν μιλάω ελληνικά.', 21, '_', STR_PAD_RIGHT)); // string(35) "Δεν μιλάω ελληνικά."
var_dump(str_pad('Δεν μιλάω ελληνικά.', 21, '_', STR_PAD_LEFT)); // string(35) "Δεν μιλάω ελληνικά."
var_dump(str_pad('Δεν μιλάω ελληνικά.', 21, '_', STR_PAD_BOTH)); // string(35) "Δεν μιλάω ελληνικά."
var_dump(mb_str_pad('Δεν μιλάω ελληνικά.', 21, '_', STR_PAD_RIGHT)); // string(37) "Δεν μιλάω ελληνικά.__"
var_dump(mb_str_pad('Δεν μιλάω ελληνικά.', 21, '_', STR_PAD_LEFT)); // string(37) "__Δεν μιλάω ελληνικά."
var_dump(mb_str_pad('Δεν μιλάω ελληνικά.', 21, '_', STR_PAD_BOTH)); // string(37) "_Δεν μιλάω ελληνικά._"
絵文字や記号にも必要です。
これはオリジナルのIssueの例です。
var_dump(str_pad('▶▶', 6, '❤❓❇', STR_PAD_RIGHT)); // string(6) "▶▶"
var_dump(str_pad('▶▶', 6, '❤❓❇', STR_PAD_LEFT)); // string(6) "▶▶"
var_dump(str_pad('▶▶', 6, '❤❓❇', STR_PAD_BOTH)); // string(6) "▶▶"
var_dump(mb_str_pad('▶▶', 6, '❤❓❇', STR_PAD_RIGHT)); // string(18) "▶▶❤❓❇❤"
var_dump(mb_str_pad('▶▶', 6, '❤❓❇', STR_PAD_LEFT)); // string(18) "❤❓❇❤▶▶"
var_dump(mb_str_pad('▶▶', 6, '❤❓❇', STR_PAD_BOTH)); // string(18) "❤❓▶▶❤❓"
Backward Incompatible Changes
mb_str_pad
自体は新しい関数であり、既存関数に変更はないので、後方互換性のない変更はありません。
ユーザランドでmb_str_pad
を実装していた場合は致命的なエラー"Cannot redeclare mb_str_pad()"が発生します。
GitHubのコード検索で軽く調べたところ、326件がヒットしました。
このうち12個がmb_str_pad
の定義前に存在チェックを行っており、42個が存在チェックを行っていませんでした。
従って、そのままアップグレードすると42個の致命的エラーが発生します。
対応方法は、単に関数定義を削除するだけです。
なお、これらの関数実装について自動テストしてみたところ、36個は正しく実装されている可能性が高く、65個は正しく動きませんでした。
このように、正しく実装することは少々むつかしい機能であるということがわかります。
Proposed PHP Version(s)
PHP8.3
RFC Impact
To Existing Extensions
mbstringエクステンションに新関数mb_str_pad
が追加されます。
既存関数に変更はありません。
Unaffected PHP Functionality
mbstring以外の全てのPHP機能は、影響を受けません。
Future Scope
将来の展望であり、本PFCでは実装されません。
コードポイントではなく書記素クラスタ単位で数えるgrapheme_str_pad()
関数を追加したい。
Proposed Voting Choices
投票期間は2023/06/05から2023/06/19であり、全投票の2/3の賛成で受理されます。
2023/06/12時点では賛成9反対1の賛成多数であり、可決される可能性が高いでしょう。
Patches and Tests
References
感想
これまでも一応mb_substrやmb_strlenなどを使って実装することはできていたのですが、PHP本体に実装されることによって今後の運用が楽になります。
ここまできたらもう文字列関数全てにマルチバイト文字列関数を対応させてほしいところですね。
ちなみにユーザコントリビュートに載っていた最も評価の高いユーザランド実装はこちらです。
function mb_str_pad($str, $pad_len, $pad_str = ' ', $dir = STR_PAD_RIGHT, $encoding = NULL)
{
$encoding = $encoding === NULL ? mb_internal_encoding() : $encoding;
$padBefore = $dir === STR_PAD_BOTH || $dir === STR_PAD_LEFT;
$padAfter = $dir === STR_PAD_BOTH || $dir === STR_PAD_RIGHT;
$pad_len -= mb_strlen($str, $encoding);
$targetLen = $padBefore && $padAfter ? $pad_len / 2 : $pad_len;
$strToRepeatLen = mb_strlen($pad_str, $encoding);
$repeatTimes = ceil($targetLen / $strToRepeatLen);
$repeatedString = str_repeat($pad_str, max(0, $repeatTimes)); // safe if used with valid utf-8 strings
$before = $padBefore ? mb_substr($repeatedString, 0, floor($targetLen), $encoding) : '';
$after = $padAfter ? mb_substr($repeatedString, 0, ceil($targetLen), $encoding) : '';
return $before . $str . $after;
}
PHP本体で実装されるのは大変ありがたいですね。
その他
マルチバイト文字列関数、合字には無力です。
$str = '👨🏻🦱';
echo mb_str_pad($str, 6, '♡'); // '👨🏻🦱'
echo mb_strlen($str); // 6
👨🏻🦱♡♡♡♡♡
ってなってほしかったのですが、何故か1文字も入りません。
👨🏻🦱
← こいつ一見1文字なのですが、実際は6文字なんですよ。
意味がわかりませんね。
ZWJはマジで魔界。
どうしてこんな悪夢みたいな作りになっているんでしょうか。
一般人としてはそんな内部事情など知ったこっちゃありませんから、早いところ見た目の文字数で判定できる関数grapheme_XXX
の登場が待たれるところですね。