@ledsun blog

無味の味は佳境に入らざればすなわち知れず

JavaScriptの日付に1日足したり2時間引いたり、計算するライブラリを作りました

IE9以降のブラウザとNede.jsで動きます。 AMDでも動くはずです。 試していません。動かなかったら教えてください。

どんなライブラリか?

こんな感じ

dateAdder(new Date(2014,10,27), {days: 1})
//Thu Nov 28 2014 00:00:00 GMT+0900 (JST)

dateAdder(new Date(2014,10,27), {hours: 1})
//Thu Nov 27 2014 01:00:00 GMT+0900 (JST)

dateAdder(new Date(2014,10,27), {days: 1, hours: 1})
//Thu Nov 28 2014 01:00:00 GMT+0900 (JST)

JavaScriptのDateオブジェクトに足し算したり引き算したりする関数です。

なぜ作ったか?

ledsun/generate-google-calendar-url · GitHubを使ってGoogleカレンダー用のURLを作る時に使おうと思いました。 予定に開始時刻だけあって終了時刻が無かったら、終了時刻を開始時刻の1時間後に設定します。

どうすればJavaScriptで一時間後の時刻が得られるでしょうか?

標準的なJavaScript

一般にJavaScriptで時間を足す処理を書くとこうです。

var newEnd = new Date(date.getTime())
newEnd.setHours(date.getHours() + value)

二行必要なのが、ちょっと嫌いです。

StackOverflowで検索しても似た感じです。

var today = new Date();
var tomorrow = new Date(today);
tomorrow.setDate(today.getDate()+1);

npmパッケージ

111247パッケージを誇るnpmを検索してみます。

parseメソッドやformatメソッドはちょっと余計です。 addメソッドの実装 は参考になりそうです。

1つのメソッドでget/setを両方やるjQueryぽいインターフェースは頂けません。 他のインターフェースと混ぜると辛いです。 実装やテストもコストが高いです。

Math Functionsに加算があります。新しいオブジェクトを返す、非破壊的なメソッドなのも良いです。 ただ、

  • 加算と減産を分ける価値が分かりません
  • 比較メソッドの価値も分かりません。getTimeすれば良くないです?
  • min/maxはreduceで十分な気もします
  • サポートしている単位が多いのも気になります。decadeって使います?10年足せば良くないですか?

名前がかっこいいです。 でも欲しいのはDateオブジェクトの代替えではありません。

そんな訳で、手頃なものが無いので作りました。

作っていた時の四方山話

Dateの標準API

主にゲッターセッター。 妙に統一感がありません。

特に年月日

  • getFullYear
  • getDate
  • getMonth

時以下は単位複数形で統一されて居るのに

英語的には正しいのかもしれませんね・・・。

日付のテストコードの話

https://twitter.com/ledsun/status/538131617382817792

変更した日付の値が期待通りかassertで比べたいです。 ところがassert.equalもstrictEqualも時刻の比較はしてくれない。 オブジェクトが同一かどうかを比較します。

結論からいうと頭に+を付けて数値に変換して比較しました。

オレオレassertはダメ

こんなの

var assertDate = function(expct, actual, message){
    assert.equal(expect.getTime(), actual.getTime(), message)
}

原則的にオレオレassertは悪手です。

  • オレオレassertの動作を理解しなくてはいけません
  • 関連のあるテストコード以外のコードも読まなくてはいけません

テストコードは定石がすくないので、 他人が書いたテストコードはメンテナンスしづらいです。 テストコードを書くコストが減っても、読むコストが増えると状況は悪化します。

getTime

最少単位で比較すれば良いのでミリ秒に変換して比較します。

assert.equal(expect.getTime(), actual.getTime())

毎回getTimeを書くのは面倒です。

Date系ライブラリのテストコードを見てみましょう。

valueOf派

https://github.com/JerrySievert/date-utils/blob/master/test/date-new-test.js#L11

assert.equal(expect.valueOf(), actual.valueOf())

プリミティブ値の比較は仕様上好ましそうです。 getTimeとメソッド名の長さも得られる値も同じです。

数値にキャスト派

https://github.com/jquense/date-math/blob/master/test.js#L27 https://github.com/timruffles/immutable-date/blob/master/test.js#L11

assert.equal(+expect, +actual)

JavaScriptの標準から外れずに、最も短く書けるのでこの記法にしました。

開発環境を自慢

saucelabsでクロスブラウザテスト

ソースコードはES5対応です。 実際に各ブラウザで動くかはsaucelabsで確認しました。 mochaで書いたテストコードをzuulでブラウザで動かしました。

最初はもっとたくさんのブラウザでテストしました。 時間掛かかりました。 各ブラウザの動作可能な最も古いバージョンだけテストすることにしました。 1セット約2分掛かかります。

こんなバッジがもらえます。 Sauce Test Status

最初はtestling使う予定でした。 ローカルでテストして、travis.ci設定して、githubにpushしても反応がありません。 調べてみたら ci.testling.com Service Timeout · Issue #88 · substack/testling · GitHub でした。

UMD.js

generate-google-calendar-url もブラウザとNode.jsで動きます。 EventEmitter2を真似て手で書きました。

UMD.jsという標準を知り、 従うことにしました。 ソフトウェア開発のコツは「解決したい問題以外は他人に押し付ける」です。

手で書いたら、ソースコードが読みにくくなったのでgulp-umdを使いました。

元が1ファイルだったので楽でした。 複数ファイルに分けるとどうなるんでしょうね?

Jxck_さんの教え

npm で依存もタスクも一元化する - Qiita

"script" を "npm run" で実行する場合は、パスが自動で通る

必要なコマンドはすべてnpmから実行出来るようにしました。

  • npm run build
  • npm test
  • npm run browser
  • npm run saucelabs

npm run saucelabstravis.ci用なので人間はあまり実行しないはずです。

t-wadaさんの教え

細かすぎて伝わらない package.json 小ネタ三選 - t-wadaのブログ

  • package.json の files フィールド
  • package.json のフィールド並び順に悩むくらいなら fixpack
  • ライセンス、 MIT で良いなら mit-license.org

一通りやりました。

もちろんpower-assert使いました。 途中でテストコマンド間違えてエラー詳細で無くなったらパニックに陥りました。 知らず知らずのうちに頼り切りです。

まとめ

npmモジュールの開発は簡単ですね<3

ソースコードは40行しかないのに、こんなに書くことがある!