NaNのみ除外する数値判定
JavaScriptの闇が深い真偽値の話。NaNを含む数値列に対して一括計算するときにどうする?という問題。
isNaN()の場合
NaNのみ判定したければisNaN()という関数がある。ECMAScript 1stから対応。とりあえず評価用に次のようなtest関数を作ってみる。 developer.mozilla.org
function test(func) { function testunit(val) { return ('' + func).replace(/^\s*function\s*([^\(]*)[\S\s]+$/im, '$1') + "(" + val + ") is " + func(val) + "<br/>"; } var str = ""; str += testunit(true); str += testunit(false); str += testunit(null); str += testunit(undefined); str += testunit(0); str += testunit(1); str += testunit(-3.1); str += testunit(-3.0); str += testunit(012);//8進数で10 str += testunit(0xA);//16進数で10 str += testunit(Math.PI); str += testunit(Infinity); str += testunit(NaN); str += testunit(""); str += testunit(" "); str += testunit("abc"); str += testunit("123"); str += testunit(console); document.write(str); }
デバッグ環境はIE11。Function.nameプロパティが使えないため、関数名の取得は正規表現で取っている。test(isNaN)を実行すると、
//isNaNの実行結果 isNaN(true) is false isNaN(false) is false isNaN(null) is false isNaN(undefined) is true isNaN(0) is false isNaN(1) is false isNaN(-3.1) is false isNaN(-3) is false isNaN(10) is false isNaN(10) is false isNaN(3.141592653589793) is false isNaN(Infinity) is false isNaN(NaN) is true isNaN() is false isNaN( ) is false isNaN(abc) is true isNaN(123) is false isNaN([object Console]) is true
NaNのほかundefinedや空白以外の文字列、オブジェクトもtrueとなっているのが注意。この理由は書いてあった。
true を返す場合、x を使用すると全ての算術式で NaN を返すことになります。これはつまり、JavaScriptにおいて isNaN(x) == true という式は、x - 0 という式が NaN を返すかどうか、というケースと同等である(JavaScript では x - 0 == NaN は常に false を返すため、このことを確認できませんが)ということです。
なるほど、算術式で計算したときにNaNとなるのがisNaN()であると。
ちなみに、Number.isNaN()という関数もあり、型変換の問題の影響がないらしい。ただし、Number.isNaNはECMAScript 2015以降なので、IEは非対応。Google Chromeが対応しているのでそれでデバッグをする。test(Number.isNaN)の実行結果。
//Number.isNaN()の場合(IE非対応) isNaN(true) is false isNaN(false) is false isNaN(null) is false isNaN(undefined) is false isNaN(0) is false isNaN(1) is false isNaN(-3.1) is false isNaN(-3) is false isNaN(10) is false isNaN(10) is false isNaN(3.141592653589793) is false isNaN(Infinity) is false isNaN(NaN) is true isNaN() is false isNaN( ) is false isNaN(abc) is false isNaN(123) is false isNaN([object Object]) is false
isNaN()とNumber.isNaN()の結果が違うだと…。感覚的にはNumber.isNaN()のほうがマッチしやすくて、こちらは本当にNaNだけtrueを返す
isFinite()の場合
似たような関数に、有限数かどうかを判定するisFinite()がある。ECMAScript 3rdから。 developer.mozilla.org test(isFinite)を実行すると、
//isFinite()の場合 isFinite(true) is true isFinite(false) is true isFinite(null) is true isFinite(undefined) is false isFinite(0) is true isFinite(1) is true isFinite(-3.1) is true isFinite(-3) is true isFinite(10) is true isFinite(10) is true isFinite(3.141592653589793) is true isFinite(Infinity) is false isFinite(NaN) is false isFinite() is true isFinite( ) is true isFinite(abc) is false isFinite(123) is true isFinite([object Console]) is false
trueやfalse、null、空白文字列や"123"のような文字列の数値がtrueとなっている。nullではtrueなのに、undefinedだとfalseなのが非常にいやらしい。Mozillaのページを読んでいくと「より堅牢性の高いNumber.isFiniteでは~」という記述がある。Number.isNaNと同様に、Number.isFiniteはECMAScript 2015以降なので、IEは非対応。Google Chromeでのtest(Number.isFinite)の実行結果。
//Number.isFinite()の場合(IE非対応) isFinite(true) is false isFinite(false) is false isFinite(null) is false isFinite(undefined) is false isFinite(0) is true isFinite(1) is true isFinite(-3.1) is true isFinite(-3) is true isFinite(10) is true isFinite(10) is true isFinite(3.141592653589793) is true isFinite(Infinity) is false isFinite(NaN) is false isFinite() is false isFinite( ) is false isFinite(abc) is false isFinite(123) is false isFinite([object Object]) is false
やはりisFinite()とNumber.isFinite()の結果が違う。Number.isFinite()のほうが感覚的にマッチしやすい。IEのことをばっさり切り捨ててしまうなら、数値判定はNumber.isFinite()で十分だろう。もしInfinityを入れたいのなら、条件でプラスしてやればよさそうだ(もう少しいいやり方あるかもしれない)。
typeofを使う
数値判定一般なら、Number.isFinite()を使えばいいことがわかったが、悲しいことにIE非対応なのでどうにかする方法を考える。先程のNumber.isNaNのMozillaのページを見ると「ポリフィル」のところに面白いコードがあった。
Number.isNaN = Number.isNaN || function(value) { return typeof value === "number" && value !== value; }
これ初めてみたときは目からうろこで、なるほどtypeofを使うのか!確かにtypeofならECMAScript 1stからなので、IEでも十分いけそう。「value !== value」でなぜNaNが除外できるかというと、 developer.mozilla.org
NaN は別の NaN 値を含むあらゆる数と同じではないと比較されます(==、!=、===、!== によって)。ある値が NaN かどうかを的確に判定するには Number.isNaN() か isNaN() を使用してください。あるいは自己比較を実行しましょう。NaNだけが、自身と同等ではないと比較評価されます。
ということは、IEでもこんな関数を定義してやればNaNは弾けることになる。
function isValidNumber(val) { return typeof val === "number" && val === val; } test(isValidNumber);
isValidNumber(true) is false isValidNumber(false) is false isValidNumber(null) is false isValidNumber(undefined) is false isValidNumber(0) is true isValidNumber(1) is true isValidNumber(-3.1) is true isValidNumber(-3) is true isValidNumber(10) is true isValidNumber(10) is true isValidNumber(3.141592653589793) is true isValidNumber(Infinity) is true isValidNumber(NaN) is false isValidNumber() is false isValidNumber( ) is false isValidNumber(abc) is false isValidNumber(123) is false isValidNumber([object Console]) is false
ようやく望む結果が出てきた。ちなみに&&以降を!isNaN(val)で置き換えても同じ結果になる。処理時間計測してみたらほとんど変わらなかったんでお好みで。Infinityを除外したければ&&以降をisFinite(val)でやればOK。
linq.jsで使う
なんでこんなことを長々と調べてたかというと、linq.jsでNaNが入っているデータに対して平均を求める問題を考えたかったため。このライブラリは計算途中をトレースする機能があるが、LINQ自体計算がブラックボックス化しがちなので、JavaScriptのように独特の型仕様が入ると思わぬバグでハマりそうだった。
やっちゃダメな例だけど、if(x)で変な数値除外できればいいやとか適当なことやってると、平均でやばいことになる。(この例の正しい平均は1.5)
<script type="text/javascript" src="Scripts/linq.js"></script> <script type="text/javascript"> var array = [1, 2, 3, NaN, 0]; var sum = Enumerable.From(array).Where("$").Sum(); document.write("sum = " + sum + "<br/>"); var average = Enumerable.From(array).Where("$").Average(); document.write("average = " + average + "<br/>"); </script> //sum = 6 ← 合計は何も問題ない //average = 2 ← 平均で0が除外されてしまっている!!
簡単な例だからすぐ気づくが、データ数が多くなったり複雑な計算させると多分気づかないと思う(自分はこれバグだって気付ける自信ないです…)。
なのでこうしよう。
<script type="text/javascript"> function isValidNumber(val) { return typeof val === "number" && val === val; } var array = [1, 2, 3, NaN, 0]; var sum = Enumerable.From(array).Where("isValidNumber($)").Sum(); document.write("sum = " + sum + "<br/>"); var average = Enumerable.From(array).Where("isValidNumber($)").Average(); document.write("average = " + average + "<br/>"); </script> //sum = 6 //average = 1.5←正しい値に
JavaScriptの動的型付けは飛び道具的なことできて面白いこともあるが、すごく独特の挙動を示すので気をつけないとほんとしねる。