こういうのでいいんだよ、な JavaScript日付時刻ライブラリ Qrono

2021/11/03に公開

作った。

https://github.com/urin/qrono#qrono----just-right-date-time-library

Qronoの特徴 🎨

  • タイムゾーンとロケールの対応を捨てた軽い日付時刻ライブラリ。
    • 他のライブラリはタイムゾーンとロケールに対応するためにコードベースが巨大になったり使い方が複雑になったりしがち。
    • ロケール対応はECMAScript® Internationalization APIを使うだけで良い。
    • ほとんどの場合、クライアント環境のタイムゾーンにだけ対応できれば十分。
      Luxonによる説明は、まさにその核心をついている。
      1. Don't make servers think about local times. Configure them to use UTC and write your server's code to work in UTC. Times can often be thought of as a simple count of epoch milliseconds; what you would call that time (e.g. 9:30) in what zone doesn't (again, often) matter.
      2. Communicate times between systems in ISO 8601, like 2017-05-15T13:30:34Z where possible (it doesn't matter if you use Z or some local offset; the point is that it precisely identifies the millisecond on the global timeline).
      3. Where possible, only think of time zones as a formatting concern; your application ideally never knows that the time it's working with is called "9:00" until it's being rendered to the user.
      4. Barring 3, do as much manipulation of the time (say, adding an hour to the current time) in the client code that's already running in the time zone where the results will matter.
        All those things will make it less likely you ever need to work explicitly with time zones and may also save you plenty of other headaches.
  • デフォルトはUTCとして時刻を扱う。
    • デフォルトをローカルタイムに切り替えることもできる。
  • 厳密に夏時間を扱う。
    • 他のライブラリは、夏時間開始・終了時のいわゆる「あいまいな時間」を自在には扱えない。
    • 夏時間を厳密に扱うことは、複数タイムゾーンの対応を捨てたからこそできる芸当。

他のライブラリなど

Moment.js

https://github.com/moment/moment
非常に広く使われている(デファクトスタンダードだった)素晴らしいライブラリだが2020年にメンテナンスモードに移行。
ミュータブルオブジェクトとしての挙動がバグを生みやすいという根本的な問題を抱えており、以下に紹介する後発の日付時刻ライブラリは全てイミュータブルな設計となっている。

Luxon

https://github.com/moment/luxon
Moment.jsのメンテナが作成したイミュータブルでリッチなライブラリ。洗練されていて機能も豊富。コードが読みやすい。
デフォルトはローカルタイムで時刻を扱い、あいまいな時間を厳密には扱えない
ただし、あいまいな時間に対してどのような挙動となるかを明確にドキュメントに示している点で、他のライブラリとは一線を画している。

Day.js

https://github.com/iamkun/dayjs
最小2KBを謳うMoment.js互換のライブラリ。GitHubスター数が多く、デファクトスタンダードになりつつある。コードが読みにくい。
タイムゾーンとロケール対応のためにコードベースは非常に大きくなっている(2021-11-02時点でソースファイル数は178)が、必要なプラグインを宣言して使うスタイルのためtree-shakingが有効であれば実効サイズは小さくなる。
ただデフォルトで実装していてもよさそうな機能も細かくプラグインになっているので、都度プラグインの宣言をするのが面倒。
大量のIssueを消化しきれない中、メジャーバージョンアップを計画している。
バグを見つけたが、まだPull requestしていない。

date-fns

https://github.com/date-fns/date-fns
名前の通りJavaScriptのDateオブジェクトを操作するための200を超える純粋な関数群を提供。TypeScriptで実装されておりtree-shakingが有効。
JavaScriptのDateオブジェクトが主役になるため、ミュータブル、月が0始まりなどといった問題はそのまま継承されてしまう。

The ECMA TC39 Temporal Proposal

https://tc39.es/proposal-temporal/docs/index.html
将来標準となるかもしれないECMAScript® API提案。厳密かつ壮大でjava.timeにインスパイアされた仕様になっている。

Qronoの導入 📥

ブラウザ

<script src="path/to/qrono.min.js"></script>
<!-- from UNPKG -->
<script src="https://unpkg.com/qrono/dist/qrono.min.js"></script>

Node.jsなど

npm install qrono
// as module
import { qrono } from 'qrono'
// or CommonJS
const { qrono } = require('qrono')

使用例 🚀

コンストラクタ

// now
qrono()
// from various type of arguments
qrono('2022-12-31') // => 2022-12-31T00:00:00.000Z
qrono(new Date())
// followings are same 2022-12-31T15:23:11.321Z
qrono('2022-12-31 15:23:11.321')
qrono(1672500191321)
qrono(2022, 12, 31, 15, 23, 11, 321)
qrono([2022, 12, 31, 15, 23, 11, 321])
qrono({ year: 2022, month: 12, day: 31, hour: 15, minute: 23, second: 11, millisecond: 321 })

アクセサ

const time = qrono(2022, 12, 31, 15, 23, 11, 321)
time.year()        // => 2022
time.month()       // => 12
time.day()         // => 31
time.hour()        // => 15
time.minute()      // => 23
time.second()      // => 11
time.millisecond() // => 321

time.second(0) // => returns new Qrono instance

タイムゾーン

// UTC as default
qrono('2022-12-31 15:23:11.321').toString() // => "2022-12-31T15:23:11.321Z"
// set default to local time
qrono.asLocaltime()
qrono('2022-12-31 15:23:11.321').toString()     // => "2022-12-31T15:23:11.321-04:00"
qrono('2022-12-31 15:23:11.321').asUtc().hour() // => 11 as UTC
qrono('2022-12-31 15:23:11.321').hour()         // => 15 as local time

変換

qrono('2000-01-01').numeric() // => 946,684,800,000 milliseconds from UNIX epoch
  === +qrono('2000-01-01')    // => true
const time = qrono('2000-01-02 03:04:05.006')
time.toObject()   // => { year: 2000, month: 1, day: 2, hour: 3, minute: 4, second: 5, millisecond: 6 }
time.toArray()    // => [2000, 1, 2, 3, 4, 5, 6]
time.nativeDate() // => JavaScript native Date instance

計算

qrono('2000-01-01 01:00:00.000') - qrono('2000-01-01') // => 3,600,000 milliseconds = 1 hour
qrono('2000-01-01 01:00:00.000') < qrono('2000-01-01') // => false
qrono('2000-01-01').plus(7200000).minus(3600000)       // => 2000-01-01T01:00:00.000Z
qrono('2000-01-01').minus({ hour: 1, minute: 30 })     // => 1999-12-31T22:30:00.000Z

const today = qrono()
const yesterday = today.minus({ day: 1 })
const tommorrow = today.plus({ day: 1 })
today.isBetween(yesterday, tommorrow) // => true

ショートハンド

const time = qrono('2000-01-02 03:04:05.006')
time.startOfYear()   // => 2000-01-01T00:00:00.000Z
time.startOfMonth()  // => 2000-01-01T00:00:00.000Z
time.startOfDay()    // => 2000-01-02T00:00:00.000Z
time.startOfHour()   // => 2000-01-02T03:00:00.000Z
time.startOfMinute() // => 2000-01-02T03:04:00.000Z
time.startOfSecond() // => 2000-01-02T03:04:05.000Z
time.dayOfWeek()     // => 7 === qrono.sunday
time.dayOfYear()     // => 2
time.isLeapYear()    // => true
time.daysInMonth()   // => 31
time.daysInYear()    // => 366
// ISO week number. See https://en.wikipedia.org/wiki/ISO_week_date
time.weeksInYear()   // => 52
time.weekOfYear()    // => 52
time.yearOfWeek()    // => 1999
// Daylight saving time stuff that is meaningful in case of local time
const localtime = time.asLocaltime()
localtime.hasDstInYear()
localtime.isInDst()
localtime.isDstTransitionDay()
localtime.minutesInDay()

日付型 QronoDate

qrono.date(...)Qrono とほぼ同じメソッド群を持つ QronoDate インスタンスを返す。

qrono.date('2000-01-02').toString()       // => "2000-01-02"
qrono('2000-01-02 23:04:05.006').toDate() // => QronoDate instance 2000-01-02
qrono.date('2000-01-02').numeric()        // => 10958 days from UNIX epoch

最後に

要望・バグなどあればIssueをお願いします。

Discussion