Published on

Intl.RelativeTimeFormat与相对时间

日常开发中经常会用到一些处理相对时间的场景,如输入一个标准时间和标准时间的偏移,返回相对标准时间的时间偏移描述,如"明天","1秒后","5天前","1年前"等类似的描述。

一般的,我们会使用一些处理时间的第三方包来协助:

import moment from 'moment';

function Relative(props) {
  return moment(props.date).fromNow();
}

可是,我们真的需要为简单的一个功能"花费20kb吗? import cost

但这个问题其实也并不简单,它需要解决几个复杂的点

  • 关于缩写

对于“一天后”,大多数人会说"明天",“昨天”大部分场景也不会说成“一天前”,“后天“,”明年“等等也同理。

  • 本地化

这是比较困难的一件事,跨多种语言的翻译不仅很复杂(包括符号的本地化差异),如果将这些处理到囊括到函数内,毫无疑问代码量会非常巨大。

这也是为什么moment.js看起来很大的原因,因为处理所有标准时间和日期非常复杂。

尝试写一下?

function nativeGetRelativeTime(locale, unit, amount) {
  if (locale === 'en-US') {
    const isPlural = amount !== 1 && amount !== -1
    const isPast = amount < 0
    if (amount === 1 && unit === 'day') return '明天'
    if (amount === -1 && unit === 'day') return '昨天'
    if (amount === 1 && unit === 'year') return '明年'
    if (isPast) {
      return `${amount} ${unit}${isPlural}`
    }
    return `${amount} ${day}${isPlural}`
  }
  if (locale === 'es-US') {

  }
  if (locale === 'cn') {

  }
}

但是有一个解决方案,内置在浏览器内。

Intl.RelativeTimeFormat

Intl 对象提供精确的字符串对比、数字格式化,和日期时间格式化等功能。对于上述场景,我们需要使用Intl.RelativeTimeFormat

const rtf = new Intl.RelativeTimeFormat('cn', {
  numeric: 'auto'
})

rtf.format(1, 'day') // 明天
rtf.format(-2, 'year') // 2年前
rtf.format(10, 'minute') // 10分钟后

你甚至可以只传递当前的navigator.language作为第一个参数:

const rtf = new Intl.RelativeTimeFormat(
  navigator.language // ✅
)

支持的单位包括: yearquartermonthweekdayhoursecond


回到我们最初的示例,我们想要的是能够自动选择要使用的合适单位,它将打印 1 天前的日期("昨天") ,和一年后的日期("明年")。

我们可以抽象一个简单的包装函数,例如:

function Relative(props) {
  const timeString = getRelativeTimeString(props.date)

  return <>
    {timeString}
  </>
}

现在有了Intl.RelativeTimeFormat ,实现就非常简单了。剩下的问题是我们应该为给定的时间增量选择什么单位( "hour" , "day" 等)

/**
 * 将日期转换为相对时间字符串
 */
function getRelativeTimeString(date, lang) {
  const rtf = new Intl.RelativeTimeFormat(lang, { numeric: "auto" });

  const timeMs = typeof date === "number" ? date : date.getTime();

  // 获取给定日期距离现在的秒数
  const deltaSeconds = Math.round((timeMs - Date.now()) / 1000); // 四舍五入,确保计算出的秒数能够在cutoffs数组内找到
  // 以秒为单位表示一分钟、小时、天、周、月、年等,并保存在数组内
  const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity];

  // 说需要的单位使用数组保存
  const units = ["second", "minute", "hour", "day", "week", "month", "year"];

  // 首先计算出距离当前的秒数,然后在数组中匹配一个与这个秒数最接近的元素
  const unitIndex = cutoffs.findIndex(cutoff => cutoff > Math.abs(deltaSeconds));

  const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1;

  return rtf.format(Math.floor(deltaSeconds / divisor), units[unitIndex]);
}

const result = getRelativeTimeString(+new Date() + 60000, 'zh-Hans-cn') // 1分钟后
const result2 = getRelativeTimeString(+new Date() - 58888, 'zh-Hans-cn') // 59秒钟前

End.