JavaScript
js
ネタ
ワンライナー
es2015

JavaScriptでうるう年判定関数をワンライナーで書いてみた【前半編】

まえがき

久々の投稿です。ご無沙汰しております。
ここ3年半くらい携わっていた案件のメイン言語がPHPとJavaだったため、JSの学習はすっかりさぼっていました。(言い訳)
数か月前に案件が久々に変わってJSでガリガリ書くようになったので、ネタも含めてうるう年判定関数のワンライナーを書いてみました。ワンライナーなのでコード量は短いですが、色々な発見があり、思った以上に面白かったので共有したいなと思って記事を書きます。

うるう年のルール(グレゴリオ暦)

  1. 西暦年が4で割り切れる年は閏年。
  2. ただし、西暦年が100で割り切れる年は平年。
  3. ただし、西暦年が400で割り切れる年は閏年。

Wikipedia: 閏年より

※特に今回は「1582年から施行されたグレゴリオ暦の暦法を、1582年以前にも適用した」先発グレゴリオ暦を対象とします。つまり西暦0年や4年も、うるう年として扱います。

I/O

入力: Number型の自然数 or String型の自然数 のみ
出力: Boolean型(true or false)

完成品はこちら

isLeapYear.js
const isLeapYear = y => !!(+y===parseInt(y)&&!y.pop&&(y%4)^(y%100)|!(y%400));

こんなコードを書いたらレビュアーの人が悲しむのが目に見えています。使用は自己責任で。

ネタ(解説とか、発見とか)

Boolean型へのキャスト

オペランドの先頭に論理否定演算子(!)を2回続けた!!を付けるとオペランドを評価した結果がBoolean型にキャストされる。
falseとして評価される、いわゆる"falsy"な値は数が決まっていて、それ以外は常にtrueとして評価される。Errorですらtrueとして評価されるのは、感覚的にやや難しいところがあるように思う。

const falsyList = [false, 0, "", null, undefined, NaN];
falsyList.forEach(e => console.log(`${e} is ${!!e}`));  // 全てfalse

const truthyList = [
  true, 1, -1, Infinity, "a", {}, [],
  /a/, function(){}, () => {}, new Error(),
];
truthyList.forEach(e => console.log(`${e} is ${!!e}`)); // 全てtrue

// !!(式) で、必ずtrueかfalseかを返す。

+""0となる

単項正値演算子は、そのオペランドを Number 型に変換します。(MDNより)

先程の"falsy"な値たちの中でこの挙動を見せるのは+false+0+""+nullの4つ。
+undefined+NaNは、NaNとして評価される。(NaNということでしょう…)

console.log(+"");       // 0
console.log(+undefined) // NaN

+[0]は0となるが、+[0, 1]NaNとなる

配列の中身が複数の場合は必ずNaNとして評価されるので問題は無いが、+[0]+[""]+[null]+[undefined]の4つは0として評価されてしまった。
+[false]+[NaN]NaNを返す。先程の挙動とは一貫性がないので、個人的にすごく気持ち悪いと感じた。

console.log(+[0]);    // 0
console.log(+[0, 1]); // NaN

parseInt("")NaNとなるがparseInt([0])NaNとならない

今まで単項正値演算子(+)があまり当てにならないケースばかり見てきた。ここで真打、parseInt()という、多くの人がお世話になったことであろうグローバル関数を紹介したい。
ワンライナーなので、なるべく関数は使いたくないが、これを使えば多くの型を殺すことが出来る。false""nullundefinedなど、単項正値演算子が0として評価してしまう値をNaNとして評価してくれる。ここで唯一、[0]0と評価されてしまうことが例外的に残った。

console.log(parseInt(""));  // NaN
console.log(parseInt([0])); // 0

うるう年判定の前半部の解説

前フリが長くなりましたが、ここで一旦本題のうるう年判定の前半部分を見てみましょう。

!!(+y===parseInt(y)&&!y.pop&&  ...(省略)
  1. !!(式)true or false に変換する
  2. ===parseInt(y)false""nullundefinedNaNの可能性を消す
     => NaNとの同値演算(===)がtrueになることはないため
  3. +y で String型の場合はNumber型に変換してから比較する
  4. !y.pop&&[0]などの1要素の配列の可能性を消す
     => popはArray.prototypeの中で文字数が短いものを選んだだけで、mapでも同じ
     => 短さにこだわらなければ、Array.isArray()の方がわかりやすい(※ES2015~)

parseInt()を使うことで、Number型の0を弾かず、0以外のfalsyな値を弾くところがミソです。

前半部まとめ

  • Boolean型へのキャストは!!がお手軽で便利
  • 単項正値演算子(+)はお手軽なNumber型キャストとしては有用だが、挙動にばらつきがあるので注意
  • 厳密にNumber型か、String型の数値かをチェックしたい場合はparseInt()を使おう(※)
  • parseInt([0])など、単一要素の配列は例外的にNaNにならないケースがあるので注意
  • NaNが割と一貫的な挙動で優秀(悪口はまた後日...)

parseInt("0あ")0になるので、厳密は言いすぎかもしれません

Gist

https://gist.github.com/matsuby/fb96ea99539801e2049a955e555cbd74