奇特なブログ

「殊勝に値する行いや心掛け」を意味する、奇特な人になる為のブログです

PHPのコーディングにおけるベストプラクティスを考える:is_numeric関数

PHPのコーディングにおけるベストプラクティスを考える準備記事 - 奇特なブログ

さて、今回は上記の記事の「第二弾」ですね。
以下の、前回のempty()に続き、結構使用頻度の多い関数だと思います。
そして、emptyと同じぐらいかそれ以上に、取り扱い注意な関数かなとも。

PHPのコーディングにおけるベストプラクティスを考える:empty関数 - 奇特なブログ

では、まずは関数のマニュアルを見てみましょう。

PHP: is_numeric - Manual


指定した変数が数値または 数値形式の文字列であるかどうかを調べます。

で、この「数値形式の文字列」というのが、
どういったものかが、以下に書かれていると。

PHP: 数値形式の文字列 - Manual


PHP 8.0.0 より前のバージョンでは、 先頭に ホワイトスペースがある場合にだけ、 文字列は数値と見なされていました。 数値の後に ホワイトスペースがある場合は、 その文字列は 先頭から始まる 数値形式の文字列とみなされていました。

上記の他の部分も見て、「思ったより複雑だな」と思ったわけですが、
特に、個人的に気になったのは上記の引用箇所ですね。

以下が、今回の検証用のソースで、そこにも書かれているんですが、
「 1」が、上記のルール(だと思われる)に則って、is_numericが「true」になると。

https://github.com/kitoku-magic/other/blob/master/php_coding_best_practice/is_numeric.php

ただ、上記に8.0.0と書いているので、バージョン8からは変わったのかもしれませんが、
それは別にしても、なんという仕様でしょうか(苦笑)
どういう理由で、こうしているんでしょうね?
そこが一番気になるんですが。


この関数の問題点として、上記の値もそうなんですが、
「trueになる値が多過ぎる」というのと「どうして、この値がtrueになる?」というのが多いと。
ちょっと、この辺はemptyに似ていますね。


ただ、emptyも含めて思う事なんですけど、
「一つの関数にしては、多機能過ぎる」と。
ちょっと細分化し過ぎと思いますが、
「0以上の整数をチェックする関数」、
「負の整数をチェックする関数」、
「0以上の浮動小数をチェックする関数」、
「負の浮動小数をチェックする関数」、
みたく分けても良いと思っているんですよね。


以下は、お気に入りの本の抜粋だと思いますが、
特に2の「1つのプログラムには1つのことをうまくやらせる」なんかが、
emptyにもis_numericにも当てはまっていない感がしますね。

UNIXという考え方 The UNIX philosophy · GitHub

あと、emptyの時にも思いましたけど、
単純に、「trueになる値」覚えていられないでしょ?(苦笑)
is_numericは、以下(探せば、まだあると思うけど)ですよ(苦笑)


// is_numeric()に渡した結果、結果がtrueになるのは以下の値
// int型とfloat型の値は全て(INFとNANも)true
// '-1'
// '0'
// '1'
// '+0'
// '+1'
// '-1.1'
// '-1.0'
// '-0.1'
// '0.0'
// '0.1'
// '1.0'
// '1.1'
// '+0.0'
// '+0.1'
// '+1.0'
// '+1.1'
// '00'
// '01'
// '.0'
// '.1'
// '0.'
// '1.'
// '-9223372036854775809'
// '-9223372036854775808'
// '9223372036854775807'
// '9223372036854775808'
// (-1 * PHP_FLOAT_MAX) - 1の文字列
// (-1 * PHP_FLOAT_MAX)の文字列
// PHP_FLOAT_MAXの文字列
// PHP_FLOAT_MAX + 1の文字列
// ' 1'
// ' 0'

あと、前回のemptyの記事を読み直しましたけど、
ソースを「書いている時」と「読んでいる時(特に、他人の)」の話は、
今回のis_numericも同じだなと思いましたね。


で、じゃあどうすれば良いのかについて、
若干、前回のempty()の前言撤回になるんですけど、
「仕様(やりたい事)による」んだと思いますね。
それと、再掲しますが以下の検証コードの結果を踏まえて考えると。

https://github.com/kitoku-magic/other/blob/master/php_coding_best_practice/is_numeric.php

いや、例えば「if (empty($value))」みたいなコードっていうのは、
当たり前ですが「条件分岐」で、
で、その「条件」と、empty()がtrueになる「条件」が、
「完全に一致」していれば、上記のコードがベストじゃないですかって。
ただそこで、「そんなケース有るんですか?」って、僕も思うんですが、
ただ、有った場合には、そうじゃない?と。
あと、「empty()でtrueになる値の中で、0だけfalseにして欲しいけど、他はtrueで良い」みたいな、
完全ではないけど一致している場合なんかも、AND条件を追加して書けば良いでしょうし。
だから、is_numericというか、条件分岐の判定条件というのが、
そういうものなんだろうと思いましたね。
まず、これが一つ。


あと、前回のempty()も改めて実施したんですが、
速度は良好なんですよね、empty()もis_numeric()も(検証コード内に記載)。
「速いっていっても、そのぐらいならねえ」って話で、
パフォーマンス目的で、他に改善の余地が「本当に」無いならアリなんじゃと思いますが。
ほら、パフォーマンスの大会とかあるじゃないですか、ああいうのだったらアリなんじゃと思いますね。
ここまでが、empty()とis_numeric()を使う理由としてはあるかなと。


逆に使えない点は、もう散々書いてきましたが、
別の切り口として、「関数の引数のmixed」を取り上げてみたいかなと。
empty()もis_numeric()も、どちらもmixedでしたし。

どうやら、PHP8からはunion型というmixed型を実現した様なものが登場した様ですが。

PHP: 型宣言 - Manual

ただ、ここでいつも思うのは、

1.データ型として、mixedという型があるわけじゃない。あくまでも、関数の引数として、どんな型でも受け付けるというだけ
2.関数の引数がmixedで宣言されていたとしても、関数に引数を「渡す時点」では、必ずintやstringなどの何らかのデータ型になっているので型は指定できる

と。
まあ上記2は、レアケース?として、
「関数に渡す変数が、条件によって型が変わる」ので、
何の型になっているか分からないというのはあるかと思いますが、
ただ、あまり良い設計ではないと思うんですけどね。
引数としては受け付けたとしても、
最悪関数内で、先に型チェックをしてから処理を行いたいかなと。


というのがあるので、前回のempty()では書き忘れましたが、
検証コードでは、まず「is_int()」などをしてからチェックしています。
単純に、チェックの書き方が型によって違うからもありますけどね。


で、is_numeric()の代わりにどれが良いのかは、
「受け付けるのは整数なのか小数なのか両方なのか」「負の値も受け付けるのか」「00の様な値も受け付けるのか」によるので、
「仕様次第」かなと。
だから、is_numeric()も場合によってはアリでは?と思ったんですけどね。


ただ、型がintとfloatの場合は特に悩まないと思いましたが、
stringはねえ、速度はやっぱり遅かったですが、
仕様次第では「正規表現もアリ」じゃないかと思いますね。
「許容したい値を、厳密に定義可能」なのが、正規表現の良い所じゃないかなと。
他の代替案の、ctype_digitやfilter_varは、それが出来ないので。
速度については、超爆速システムじゃなくて且つ、ループ内で書かなければとは思いましたね。
他も含めて、決定打になる書き方が無かったからというのもあるんですけど。
いやあ、数値チェックは結構難しいじゃないかというのが、今回の最大の収穫ですね。


あと、もっと良い書き方がある等ありましたら、教えて頂けるとありがたいですね。
特に、今回の検証コードだと、コメントアウトしてますが、
bccompやgmp_cmpの小数の比較は気になってますね。
この辺、なんか上手い方法ないかなあと。


あと、最後に以下以外もありますが、
大変参考になりました、ありがとうございます。

PHP:文字列が整数かの検証にはis_numeric, ctype_digit, filter_varどれが適切か? - Qiita

PHPのis_numeric関数は使うべきでないという話 - hnwの日記

楽に検証!値をチェックしてくれるfilter関数

便利なFilter関数を使いこなそう その3 - ZDNet Japan