LoginSignup
17
5

More than 1 year has passed since last update.

【PHP8.3】日本語でもstr_padできるようになるよ

Last updated at Posted at 2023-06-12
$str = 'あ';
$str = str_pad($str, 5, 'い');
var_dump($str);

01.png

その後ろの変なのはなんだ?

ということでマルチバイト文字列関数にめでたく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_typestr_padと同じくSTR_PAD_LEFTSTR_PAD_RIGHTSTR_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_typeSTR_PAD_LEFTSTR_PAD_RIGHTSTR_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

オリジナルのIssue

感想

これまでも一応mb_substrmb_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の登場が待たれるところですね。

17
5
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
5