Mobile Factory Tech Blog

技術好きな方へ!モバイルファクトリーのエンジニアたちが楽しい技術話をお届けします!

dayjsで相対時間を操る

皆さんこんにちは、最近ずっとポットのお湯を沸かし続けないと寒くて耐えられないエンジニアの id:Dozi0116 です。 今回は、 dayjs で相対時間を求める方法、自由自在に操る方法を紹介します。

TL; DR

以下は今日紹介する出力をいじるための設定と、利用例です。

import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime.js"
import updateLocale from "dayjs/plugin/updateLocale.js"

import "dayjs/locale/ja.js"

// 基準になる時刻を調整
// l: relativeTimeで使うkey
// r: dで判定した時、この値以下までがこの閾値になる
// d: 判定に利用する時間単位、省略した場合は直前の判定に用いた単位
const relativeTimeConfig = {
  thresholds: [
    { l: "s", r: 59, d: "second" }, // 0〜59秒
    { l: "m", r: 1 }, // 1分表示用: 単数用の処理があるため、これを書かないと1分が1秒と表示されてしまう
    { l: "m", r: 59, d: "minute" }, // 1分〜59分
    { l: "mm", r: 60 * 24 - 1 }, // 1時間〜23時間59分
    { l: "d", r: 1 }, // 1日
    { l: "d", d: "day" }, // 2日〜
  ],
  rounding: Math.floor, // 閾値判定時に用いる丸め関数
}

// 日本語で扱えるように
dayjs.locale("ja")

// プラグインを利用する
dayjs.extend(updateLocale)
dayjs.extend(relativeTime, relativeTimeConfig)

// 相対時間の表示ルール設定
dayjs.updateLocale("ja", {
  relativeTime: {
    future: "%s後",
    past: "%s前",
    s: "数秒", // %d を時間に置換して表示。%dがなくてもOK
    m: "%d分",
    mm: (abs) => {
      // 関数でカスタマイズも可能
      if (abs % 60 === 0) {
        return `${abs / 60}時間`
      }

      return `${Math.floor(abs / 60)}時間${abs % 60}分`
    },
    d: "%d日",
  },
})

////////////////////

const baseTime = dayjs("2021-01-01 00:00:00")
const targetTime1 = dayjs("2021-01-01 00:00:01")
const targetTime2 = dayjs("2021-01-01 00:01:00")
const targetTime3 = dayjs("2021-01-01 00:59:59")
const targetTime4 = dayjs("2021-01-01 01:00:00")
const targetTime5 = dayjs("2021-01-01 01:20:30")
const targetTime6 = dayjs("2021-01-02 00:00:00")
const targetTime7 = dayjs("2021-02-01 00:00:00")

console.log(targetTime1.from(baseTime)) // 数秒後
console.log(targetTime2.from(baseTime)) // 1分後
console.log(targetTime3.from(baseTime)) // 59分後
console.log(targetTime4.from(baseTime)) // 1時間後
console.log(targetTime5.from(baseTime)) // 1時間20分後
console.log(targetTime6.from(baseTime)) // 1日後
console.log(targetTime7.from(baseTime)) // 31日後

動作環境

今日紹介するコードは

  • node v20.1.0
  • dayjs v1.11.0

で動作確認をしています。

dayjs とは

https://day.js.org/

dayjs とは、 Moment.jsという非推奨になってしまった時刻を扱うパッケージと同じインターフェースを備えているかつ、Moment.js より軽い構造になっていることが特徴のパッケージです。

以下のように書くことで、簡単に時刻を用意して扱うことが可能です。

import dayjs from "dayjs"

const now = dayjs()
console.log(now.format("YYYY-MM-DD(ddd)")) // -> 2023-12-22(Fri) (今日の日付)

(オプション) dayjs で日本語表示をする

dayjs は言語のデフォルトが英語になっているため、相対時間を表示しようとすると英語で表示されてしまいます。 今回の記事では日本語で相対時間を操るため、日本語のセットアップをしておきます。

import dayjs from "dayjs"

import "dayjs/locale/ja.js"

// 表示言語を日本語に設定
dayjs.locale("ja")

const time = dayjs("2023-12-01")

console.log(time.format("ddd")) // 金

以降この記事では特に書かれていないところでも locale を ja に設定して進めていきます。

dayjs で相対時間を扱うための準備

relativeTime プラグインの準備

dayjs は必要最低限の機能のみの実装でコードを小さくしているため、 dayjs の import だけでは相対時間を求められません。 しかし、公式がパッケージと共に提供しているプラグイン relativeTimeを import することによって相対時間を扱えるようになります。

import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime.js"

dayjs.extend(relativeTime)

実際に時刻を計算する場合は to もしくは from というメソッドを使うだけです。

const baseTime = dayjs("2023-12-01")
const targetTime = dayjs("2023-12-05")

console.log(targetTime.from(baseTime)) // -> 4日後

fromNowtoNow というメソッドを使えば現在時刻からの相対時間を求められます。

const time = dayjs("2023-12-01")

console.log(time.fromNow()) // -> 21日前 (今日の日付依存)
console.log(time.toNow()) // -> 21日後 (今日の日付依存)

おめでとうございます!…?

これだけで相対時間を扱えるようになりました! 他の日付も試してみましょう。

const baseTime = dayjs("2023-12-01 12:00:00")
const targetTime1 = dayjs("2023-12-02 09:00:00")
const targetTime2 = dayjs("2023-12-02 10:00:00")

console.log(targetTime1.from(baseTime)) // -> 21時間後
console.log(targetTime2.from(baseTime)) // -> 1日後

21 時間を境に、1 日前という判定になってしまいました。

厳密に判定するには

実はdayjsrelativeTimeプラグインは結構アバウトな時間管理を行なっています。

実装ロジックを実際に見てみると、このような判定になっていることがわかります。

d: 判定に利用する時間単位
r: dで判定した時、この値以下までがこの閾値になる
範囲 表示
〜44 秒 n 秒
45〜89 秒 1 分
90 秒〜44 分 n 分
45 分〜89 分 1 時間
90 分〜21 時間 n 時間
22 時間〜35 時間 1 日
36 時間〜25 日 n 日
26 日〜45 日 1 ヶ月
46 日〜10 ヶ月 n ヶ月
11 ヶ月〜17 ヶ月 1 年
18 ヶ月〜 n 年

そのため、先ほどの例では 21 時間後と 22 時間後で表示が異なってしまったのでした。

これを厳密に扱いたい場合は公式が example を出してくれているように設定する必要があります。

https://day.js.org/docs/en/customization/relative-time#relative-time-thresholds-and-rounding

import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime.js"

// from: https://day.js.org/docs/en/customization/relative-time#relative-time-thresholds-and-rounding
const thresholds = [
  { l: "s", r: 59, d: "second" }, // ここだけ微調整: r: 59 / d: 'second' としないと秒周りがおかしくなるので注意!
  { l: "m", r: 1 },
  { l: "mm", r: 59, d: "minute" },
  { l: "h", r: 1 },
  { l: "hh", r: 23, d: "hour" },
  { l: "d", r: 1 },
  { l: "dd", r: 29, d: "day" },
  { l: "M", r: 1 },
  { l: "MM", r: 11, d: "month" },
  { l: "y", r: 1 },
  { l: "yy", d: "year" },
]

dayjs.extend(relativeTime, { thresholds })

const baseTime = dayjs("2023-12-01 12:00:00")
const targetTime1 = dayjs("2023-12-02 09:00:00")
const targetTime2 = dayjs("2023-12-02 10:00:00")

console.log(targetTime1.from(baseTime)) // -> 21時間後
console.log(targetTime2.from(baseTime)) // -> 22時間後

表示部分より細かいところも厳密にする

もっと細かい表示を見てみます。

const baseTime = dayjs("2023-12-02 10:00:00")
const targetTime1 = dayjs("2023-12-02 14:00:00")
const targetTime2 = dayjs("2023-12-02 14:29:59")
const targetTime3 = dayjs("2023-12-02 14:30:00")

console.log(targetTime1.from(baseTime)) // -> 4時間後
console.log(targetTime2.from(baseTime)) // -> 4時間後
console.log(targetTime3.from(baseTime)) // -> 5時間後

4 時間 30 分を境に 4 時間後 → 5 時間後という切り替わりが起こっています。 これはデフォルトの diff を求めるロジックが Math.round であることに由来しており、4.5 時間の diff が丸められて 5 時間になっているためです。

これを解消するのも config が活躍してくれます。

config の rounding という項目に、判定に用いる関数を渡してあげることで、丸め方を指示できます。 今回は切り捨てである Math.floor を指定しました。

// 小数切り捨てのdiffで計算する
dayjs.extend(relativeTime, { rounding: Math.floor })

const baseTime = dayjs("2023-12-02 10:00:00")
const targetTime1 = dayjs("2023-12-02 14:00:00")
const targetTime2 = dayjs("2023-12-02 14:29:59")
const targetTime3 = dayjs("2023-12-02 14:30:00")

console.log(targetTime1.from(baseTime)) // -> 4時間後
console.log(targetTime2.from(baseTime)) // -> 4時間後
console.log(targetTime3.from(baseTime)) // -> 4時間後

r: 1 の意味

先ほど参考にした thresholds では、 r: 1 という設定がありました。 これは、他の言語用などに用意されている単数系の表示をするために用意されています。

判定ロジックを見てみると、「diff が 1 以下の場合、index が 1 つ手前の閾値を採用する」という処理になっています。 そのため単数系の区別がない日本語などでも、 r: 1 という設定がないと 1 つ前の設定、つまり 1 分を出すはずが 1 秒という表示になってしまうので注意してください。

カスタマイズしたい!

ここまで来ると、閾値を自由に操って表示を切り替えられるようになったはずです。しかしこのままではまだ完全に操れるようになったとは言えないでしょう。 なぜならこのプラグインの力だけでは、表示形式を変えることはできないからです。

const baseTime = dayjs("2023-12-01 12:00:00")
const targetTime = dayjs("2023-12-01 13:30:00")

// 1時間後や2時間後だったり、90分後だったりはできるけど 1時間30分後という表示にできない!
console.log(targetTime.from(baseTime))

これを叶えてくれるのが updateLocale プラグインです。(リンク先は relativeTime ですが、こちらの方に細かい使い方が載っています) これを使うと、求めた閾値に応じた出力をカスタマイズできます。

import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime.js"
import updateLocale from "dayjs/plugin/updateLocale.js"

import "dayjs/locale/ja.js"

const thresholds = [
  { l: "s", r: 59, d: "second" }, // ここは r: 59 / d: 'second' としないと秒周りがおかしくなるので注意!
  { l: "m", r: 1 },
  { l: "mm", r: 59, d: "minute" },
  { l: "mmm", r: 60 * 24 - 1 }, // n時間を廃止して、1439分まで見れるようにした
  { l: "d", r: 1 },
  { l: "dd", r: 29, d: "day" },
  { l: "M", r: 1 },
  { l: "MM", r: 11, d: "month" },
  { l: "y", r: 1 },
  { l: "yy", d: "year" },
]

dayjs.locale("ja")
dayjs.extend(updateLocale)
dayjs.extend(relativeTime, { thresholds, rounding: Math.floor })

// 出力のカスタマイズ
// カスタマイズしないところも書く必要がある
dayjs.updateLocale("ja", {
  relativeTime: {
    // 未来の場合、過去の場合につける文言のカスタマイズ
    future: "%s後",
    past: "%s前",

    // 時間出力のカスタマイズ
    // thresholdsで指定した `l` の値に応じた出力をする
    s: "数十秒", // 1分未満は全部数十秒と表示させる
    m: "%d分",
    mm: "%d分",
    mmm: (abs) => {
      // 関数で指定することも可能。第一引数にはdiffの値がそのまま来る
      if (abs % 60 === 0) {
        return `${abs / 60}時間`
      }

      return `${Math.floor(abs / 60)}時間${abs % 60}分`
    },
    d: "%d日",
    dd: "%d日",
    M: "%dヶ月",
    MM: "%dヶ月",
    y: "%d年",
    yy: "%d年",
  },
})

updateLocale プラグインは、s m などの thresholds で設定した l の値ごとに、出力する内容を設定できます。 その時に string を設定すれば、 %d -> diff に変換したものが、function を設定すれば diff を受け取ってカスタマイズした返り値を出力することができます。

このように指定することで、出力のカスタマイズも可能になりました。

const baseTime = dayjs("2023-12-01 12:00:00")
const targetTime1 = dayjs("2023-12-01 12:00:30")
const targetTime2 = dayjs("2023-12-01 13:30:00")

console.log(targetTime1.from(baseTime)) // 数十秒後
console.log(targetTime2.from(baseTime)) // 1時間30分後

まとめ

だいぶ長くなってしまいましたが、 dayjs というライブラリと、それを用いた相対時刻の操作について

  • relativeTime プラグインを用いて相対時刻の判定ができる
    • 厳密に判定したい場合は config の thresholdsrounding を調整
    • 単数系の処理に注意
  • updateLocale プラグインを用いて相対時刻の表示方法をカスタマイズできる
    • string を渡して簡易テンプレートを作ったり、関数を渡してより細かい制御をしたりできる

ことを紹介しました。

この記事が誰かの助けになれば幸いです。みなさまもよき時刻判定ライフを〜

参考にしたサイト

とても参考になりました、ありがとうございました!

Day.js で相対日時を厳密に表示する(thresholds) https://zenn.dev/catnose99/articles/ba540f5c233847

dayjs - RelativeTime https://day.js.org/docs/en/plugin/relative-time

dayjs - Relative Time https://day.js.org/docs/en/customization/relative-time

GitHub - dayjs/src/plugin/relativeTime/index.js https://github.com/iamkun/dayjs/blob/f2e479006a9a49bc0917f8620101d40ac645f7f2/src/plugin/relativeTime/index.js