⏱️

Temporal.Duraionから◯日前みたいなlocale対応した文字列にする

2023/09/06に公開

Durationの抽出

まず二つのDateからDurationを抽出する。
今回はInstantに変換してsinceにして差分を取得する

const startInstant = Temporal.Instant
  .fromEpochMilliseconds(new Date('2021-01-01 00:00:00').getTime())
const targetInstant = Temporal.Instant
  .fromEpochMilliseconds(new Date('2023-01-14 12:34:56').getTime())

const relative = startInstant.toZonedDateTimeISO(Temporal.Now.timeZoneId())
const duration = targetInstant.since(startInstant).round({
  smallestUnit: "second",
  largestUnit: "year",
  relativeTo: relative,
})

Durationは、ISO8601の形式になっており、このままだと扱いづらい。

console.log(duration.toString())
// P2Y13DT12H34M56S

Durationから「◯日前」みたいなのを生成する

Durationから最大の値を取り出して表示したい場合(GitHubの最終更新日みたいなアレ)をやりたい場合は、RelativeTimeFormatを利用すると良い。

const units = ["year", "month", "day", "hour", "minute", "second"] as const

// durationから各unitの値を取り出して、値が存在する最初の単位を取得する
const unitValue = units.map(unit => {
  const multipler = `${unit}s` as const
  const value = duration[multipler]
  return { unit, value }
}).find(item => item.value > 0)

if (!unitValue) {
  return
}

const durationBefore = new Intl.RelativeTimeFormat(locale).format(unitValue.value, unitValue.unit)
console.log(durationBefore)
// => 3 か月前

これでlocaleに対応した◯日前 みたいなものができる

Durationから値を全部フォーマットしたい場合

最大の単位ではなくすべての値を細かく表示したい場合は、RelativeTimeFormatだと少し足りない。

Intl.DurationFormatがあれば一発でそれを利用できそうだが、今のところ利用できる環境もpolyfillも薄いので、自前でフォーマットする必要がある。
localeが不要などであれば愚直に対応表のようなものを用意してしまえばいいのだが、今回はIntl.NumberFormatを利用してみる。

const locale = "ja-JP"

// 値とのRecordを取り出す
const units = ["year", "month", "day", "hour", "minute", "second"] as const
const unitValues = units.map(unit => {
  const multipler = `${unit}s` as const
  const value = duration[multipler]
  return { unit, value }
})

// 取り出した値をフォーマットする
const formattedDuration = unitValues.map(({unit,value}) => {
  if (value < 1) return null
  return new Intl.NumberFormat(locale, {
    style: "unit",
    unit,
  }).format(value)
}).filter(item => item !== null).join(" ")

このようにすると下記のような形式が取れる

2 年 13 日 12 時間 34 分 56 秒

単位と文字の空白を消す

Intl.NumberFormatは日本語でも数値と単位の間に空白を入れてしまうので、若干気持ち悪い見た目の場合もある。

2年 13日 12時間 34分 56秒

のようにしたい場合は

new Intl.NumberFormat(locale, {
  style: "unit",
  unit,
}).format(value).replace(" ","")

replaceで消してしまうか、またはformatToPartsを利用するならちょっと遠回りだが

const [unit] = new Intl.NumberFormat(locale, {
  style: "unit",
  unit,
}).formatToParts(value).filter(item => item.type === "unit").map(item => item.value)
return `${value}${unit}` // localeによってはこのルールだと不都合な可能性はある

というのも考えられる。

補足

ISO8601 Durationの変換

今回は自前でDurationから値を取り出したが、下記のようなライブラリを利用する手段もある。ただし、localeを考慮してくれるようなものはなさそうだった。

duration.toString()によってISO8601形式が取得できるので、上記ライブラリで再変換する

unitの取り出し

unitsは無理をすればIntl.DateTimeFormatから再現することも出来なくはない。
ただし、secondが入ってこなかったり余計なキーが入ってきたりするので、そんなにおすすめはしない。

const units = new Intl.DateTimeFormat("ja-JP", {
  dateStyle: "short",
  timeStyle: "short",
})
  .formatToParts(new Date())
  .filter(value => value.type !== "literal").map(value => value.type)
// => ["year", "month", "day", "hour", "minute"]
GitHubで編集を提案

Discussion