先日PHP8でstr_containsが導入されることが決まったばかりですが、さらにもっと直接的な『〇〇で始まる』『〇〇で終わる』関数までも導入されることになりました。
Add str_starts_with() and str_ends_with() functionsというRFCが投票中です。
2020/05/04時点では賛成50反対4で、ほぼ導入確定です。
PHP RFC: Add str_starts_with() and str_ends_with() functions
Introduction
・str_starts_with
は、文字列が指定の文字列で始まるか否かをチェックし、結果をbool値で返します。
・str_ends_with
は、文字列が指定の文字列で終わるか否かをチェックし、結果をbool値で返します。
これらの機能は既存の文字列関数、たとえばsubstrやstrpos/strrpos、そしてstrncmpにsubstr_compare、あまつさえstrlenなどを駆使して実装されてきました。
これらユーザランドの実装には、様々な問題点があります。
str_starts_withとstr_ends_withの需要は高く、Symfony、Laravel、Yii、FuelPHP、そしてPhalconと、あらゆるフレームワークによってサポートされています。
文字列の始めと終わりをチェックすることは非常に一般的なタスクであり、簡単に行えるべきです。
多くのフレームワークがこのタスクを実装しているということは、このタスクを実行することが簡単ではないことを意味しています。
JavaScript/Java/Haskell/Matlabといった多くの高水準言語が標準でこの機能を実装している理由でもあります。
文字列の開始と終了をチェックすることは、これだけのためにフレームワークを導入したり、ユーザランドで最適ではない(どころかバグが入るかもしれない)実装を行ったりする必要のあるべき作業ではありません。
Downsides of Common Userland Approaches
この機能のアドホックな実装は、専用関数に比べると直感的ではありません。
PHPのニュービーや、他言語と同時開発する開発者にとっては特にそうです。
また、特に===
を含む場合、実装を簡単に間違えます。
さらに多くのユーザランド実装はパフォーマンス上の問題があります。
注意:以下の実装には、E_WARNINGを防ぐために$needle === "" ||
やstrlen($needle) <= strlen($haystack) &&
のようなガードを入れましょう。
str_starts_with
substr($haystack, 0, strlen($needle)) === $needle
$haystack
の無駄なコピーが発生するため、メモリ効率が良くありません。
strpos($haystack, $needle) === 0
$needle
が見つからなかった場合に$haystack
を最後まで調べてしまうため、CPU効率が悪くなります。
strncmp($haystack, $needle, strlen($needle)) === 0 // generic
strncmp($subject, "prefix", 6) === 0 // ad hoc
これは効率的ですが、$needle
の文字数を別に渡す必要があり冗長です。
str_ends_with
substr($haystack, -strlen($needle)) === $needle
str_starts_with同様、メモリ効率がよくありません。
strpos(strrev($haystack), strrev($needle)) === 0
str_starts_with同様CPU効率が悪いだけでなく、文字列反転処理まで入るので、さらに非効率です。
strrpos($haystack, $needle) === strlen($haystack) - strlen($needle)
冗長であり、CPURL効率が悪くなることがあります。
substr_compare($haystack, $needle, -strlen($needle)) === 0 // generic
substr_compare($subject, "suffix", -6) === 0 // ad hoc
効率的ですが、冗長です。
Proposal
2つの関数、str_starts_with()
とstr_ends_with()
を導入します。
str_starts_with ( string $haystack , string $needle ) : bool
str_ends_with ( string $haystack , string $needle ) : bool
str_starts_with()
は、$haystack
が$needle
で始まるかどうかを調べます。
strlen($needle) > strlen($haystack)
であれば即座にfalseを返し、そうでなければ両文字列を比較し、先頭一致すればtrueを、一致しなければfalseを返します。
str_ends_with()
も同じですが、後方一致です。
以下に例を示します。
$str = "beginningMiddleEnd";
if (str_starts_with($str, "beg")) echo "printed\n"; // true
if (str_starts_with($str, "Beg")) echo "not printed\n"; // false
if (str_ends_with($str, "End")) echo "printed\n"; // true
if (str_ends_with($str, "end")) echo "not printed\n"; // false
// 空文字
if (str_starts_with("a", "")) echo "printed\n"; // true
if (str_starts_with("", "")) echo "printed\n"; // true
if (str_starts_with("", "a")) echo "not printed\n"; // false
if (str_ends_with("a", "")) echo "printed\n"; // true
if (str_ends_with("", "")) echo "printed\n"; // true
if (str_ends_with("", "a")) echo "not printed\n"; // false
空文字に関しては、受理済のstr_containsのRFCの挙動に従います。
これはJavaやPythonなどと共通の動作です。
Backward Incompatible Changes
ユーザランドに同名の関数がある場合は競合します。
Proposed PHP Version(s)
PHP8
RFC Impact
・SAPI:全てのPHP環境に関数が追加されます
・エクステンション:無し
:Opcache:無し
・New Constants:無し
・php.ini Defaults:無し
Votes
投票は2020/05/04まで。
投票者の2/3+1の賛成で受理されます。
Patches and Tests
https://github.com/php/php-src/pull/5300
References
他言語の類似機能
・JavaScript: String#startsWith() / String#endsWith()
・Python: str#startswith() / str#endswith()
・Java: String#startsWith() / String#endsWith()
・Ruby: String#start_with?() / String#end_with?()
・Go: strings.HasPrefix() / strings.HasSuffix()
・Haskell: Data.String.Utils.startswith / Data.String.Utils.endswith
・MATLAB: startsWith()) / endsWith()
bugs.php.net
・bug #50434 / bug #60630 / bug #67035 / bug #74449
過去のRFC
・PHP RFC: rfc:add_str_begin_and_end_functions
Rejected Features
大文字小文字を区別しない版とマルチバイト版は、以前のRFCには含まれていましたが、このRFCでは廃止されました。
理由はstr_containsを参照してください。
感想
PHPの文字列関数ってやたら大量に用意されてるわりに意外と基本的なところが抜けていたのですが、PHP8でstr_continsとこの関数が追加されたことによって、テキスト処理に必要なものは出揃うことになったのではないでしょうか。
他に必要なのって何かありますかね。デフォルト関数の命名規則とか?