JavaScript时间戳转换为相对时间

102
我正在寻找一个不错的JS代码片段,将时间戳(例如来自Twitter API)转换为友好的相对时间(例如2秒前,一周前等)。 有人愿意分享他们最喜欢的方法吗?最好不要使用插件。

查看此链接-https://formatjs.io/docs/polyfills/intl-relativetimeformat - vsync
17个回答

194

如果你不是过于关注准确性,那么很容易做到。普通方法有什么问题吗?

function timeDifference(current, previous) {

    var msPerMinute = 60 * 1000;
    var msPerHour = msPerMinute * 60;
    var msPerDay = msPerHour * 24;
    var msPerMonth = msPerDay * 30;
    var msPerYear = msPerDay * 365;

    var elapsed = current - previous;

    if (elapsed < msPerMinute) {
         return Math.round(elapsed/1000) + ' seconds ago';   
    }

    else if (elapsed < msPerHour) {
         return Math.round(elapsed/msPerMinute) + ' minutes ago';   
    }

    else if (elapsed < msPerDay ) {
         return Math.round(elapsed/msPerHour ) + ' hours ago';   
    }

    else if (elapsed < msPerMonth) {
        return 'approximately ' + Math.round(elapsed/msPerDay) + ' days ago';   
    }

    else if (elapsed < msPerYear) {
        return 'approximately ' + Math.round(elapsed/msPerMonth) + ' months ago';   
    }

    else {
        return 'approximately ' + Math.round(elapsed/msPerYear ) + ' years ago';   
    }
}

这里提供了一个工作示例,点击这里查看

如果您对单数值的处理不满意(例如使用“1天”而不是“1天”),则可以进行微调。


3
由于四舍五入的原因,这里有一个小问题,如果时间不到24小时但比23小时接近24小时,它会显示“24小时前”,而不是“1天前”。使用Math.floor代替Math.round可以解决这个问题。 - user457586
OP被要求提供XX之前的时间戳。您的脚本没有使用时间戳而是日期。请修复。 - user198989
看起来你只需要去掉 * 1000 如果你想使用时间戳(秒),并更新变量,例如 msPerMinute 到 sPerMinute。另外,如果使用 Date.now(),需要仅使用前10位数字(匹配时间戳的长度)。 - Jacob David C. Cunningham

71

更新于2021年4月4日:

我已将以下代码转换为Node包。这是存储库


Intl.RelativeTimeFormat - 本地API

[✔] (2018年12月) 第三阶段提案,并已在Chrome 71中实现
[✔] (2020年10月) 第四阶段完成),准备纳入正式的ECMAScript标准中

// in miliseconds
var units = {
  year  : 24 * 60 * 60 * 1000 * 365,
  month : 24 * 60 * 60 * 1000 * 365/12,
  day   : 24 * 60 * 60 * 1000,
  hour  : 60 * 60 * 1000,
  minute: 60 * 1000,
  second: 1000
}

var rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })

var getRelativeTime = (d1, d2 = new Date()) => {
  var elapsed = d1 - d2

  // "Math.abs" accounts for both "past" & "future" scenarios
  for (var u in units) 
    if (Math.abs(elapsed) > units[u] || u == 'second') 
      return rtf.format(Math.round(elapsed/units[u]), u)
}

// test-list of dates to compare with current date
[
  '10/20/1984',
  '10/20/2015',
  +new Date() - units.year,
  +new Date() - units.month,
  +new Date() - units.day,
  +new Date() - units.hour,
  +new Date() - units.minute,
  +new Date() + units.minute*2,
  +new Date() + units.day*7,
]
.forEach(d => console.log(   
  new Date(d).toLocaleDateString(),
  new Date(d).toLocaleTimeString(), 
  '(Relative to now) →',
  getRelativeTime(+new Date(d))
))

Intl.RelativeTimeFormat 是在V8 v7.1.179和Chrome71中默认可用的。随着该API变得更加普及,您将发现Moment.jsGlobalizedate-fns等库放弃对硬编码CLDR数据库的依赖,转而使用本地相对时间格式化功能,从而提高加载时间性能、解析和编译时间性能、运行时性能和内存使用率。

2
@Dayd - 更新了关于天数的答案示例,但如果您想以稳健的方式比较日期,则必须首先创建两个日期之间的比较方法,并了解边距是小时等。然后只需将该结果传递给Intl.RelativeTimeFormat方法即可。您所问的是一个完全不同的主题。 - vsync
@DarylMalibiran - 为什么是 2020?你在代码中看到了 2020 吗?请再次查看,一旦发现错误,请删除您的评论。 - vsync
1
@vsync 我昨天正要修改我的评论,但是 Stack Overflow 正在维护中。'10/20/1984' 是你的一个示例日期。今天是 '10-02-2021',不等于或超过 '10-20-2021',所以我将 2020 减去了 1984。我意识到 Intl.RelativeTimeFormat 可能会四舍五入日期 / 生成近似日期(例如,36 和 11 个月会被四舍五入为 37 年)。现在我说对不起,你的代码没有错误。 - Daryl Malibiran
虽然不是什么大问题,但使用 for...in 遍历对象的键可能是不可预测的。由于您的逻辑依赖于从最大单位(“年”)开始并按特定顺序迭代,因此最好将单位存储在数组中。 - Shaun Scovil
@ShaunScovil - 这些迭代的顺序已经可预测多年了。 - vsync
显示剩余10条评论

31
以下是模仿 Twitter 时间格式的代码,不需要使用插件:

function timeSince(timeStamp) {
  var now = new Date(),
    secondsPast = (now.getTime() - timeStamp) / 1000;
  if (secondsPast < 60) {
    return parseInt(secondsPast) + 's';
  }
  if (secondsPast < 3600) {
    return parseInt(secondsPast / 60) + 'm';
  }
  if (secondsPast <= 86400) {
    return parseInt(secondsPast / 3600) + 'h';
  }
  if (secondsPast > 86400) {
    day = timeStamp.getDate();
    month = timeStamp.toDateString().match(/ [a-zA-Z]*/)[0].replace(" ", "");
    year = timeStamp.getFullYear() == now.getFullYear() ? "" : " " + timeStamp.getFullYear();
    return day + " " + month + year;
  }
}

const currentTimeStamp = new Date().getTime();

console.log(timeSince(currentTimeStamp));

Gist https://gist.github.com/timuric/11386129

Fiddle http://jsfiddle.net/qE8Lu/1/

希望对你有所帮助。


4
这不应该是一个答案。您的脚本输出了“2001年9月9日”,他想要的是类似于“XX分钟/秒/小时/天”之前的时间。 - user198989
这是一个更新的版本,它将始终返回“timeAgo”格式,包括月份和年份:http://jsfiddle.net/u3p9s8kn/65/ - Money_Badger

10

Typescript和Intl.RelativeTimeFormat(2020)

综合了@vsync和@kigiri的方法,使用Web APIRelativeTimeFormat实现的Typescript

const units: {unit: Intl.RelativeTimeFormatUnit; ms: number}[] = [
    {unit: "year", ms: 31536000000},
    {unit: "month", ms: 2628000000},
    {unit: "day", ms: 86400000},
    {unit: "hour", ms: 3600000},
    {unit: "minute", ms: 60000},
    {unit: "second", ms: 1000},
];
const rtf = new Intl.RelativeTimeFormat("en", {numeric: "auto"});

/**
 * Get language-sensitive relative time message from Dates.
 * @param relative  - the relative dateTime, generally is in the past or future
 * @param pivot     - the dateTime of reference, generally is the current time
 */
export function relativeTimeFromDates(relative: Date | null, pivot: Date = new Date()): string {
    if (!relative) return "";
    const elapsed = relative.getTime() - pivot.getTime();
    return relativeTimeFromElapsed(elapsed);
}

/**
 * Get language-sensitive relative time message from elapsed time.
 * @param elapsed   - the elapsed time in milliseconds
 */
export function relativeTimeFromElapsed(elapsed: number): string {
    for (const {unit, ms} of units) {
        if (Math.abs(elapsed) >= ms || unit === "second") {
            return rtf.format(Math.round(elapsed / ms), unit);
        }
    }
    return "";
}

1
我编辑了这个答案,将比较从 Math.abs(elasped) > ms 更改为 Math.abs(elapsed) >= ms。前者会显示像“24小时前”/“365天前”这样的内容。后者(即编辑中反映的内容)则会显示“一天前”/“一年前”。 - Hooray Im Helping

7

灵感来自Diego Castillo的回答timeago.js插件,我为此编写了自己的原始插件。

var timeElement = document.querySelector('time'),
    time = new Date(timeElement.getAttribute('datetime'));

timeElement.innerText = TimeAgo.inWords(time.getTime());

var TimeAgo = (function() {
  var self = {};
  
  // Public Methods
  self.locales = {
    prefix: '',
    sufix:  'ago',
    
    seconds: 'less than a minute',
    minute:  'about a minute',
    minutes: '%d minutes',
    hour:    'about an hour',
    hours:   'about %d hours',
    day:     'a day',
    days:    '%d days',
    month:   'about a month',
    months:  '%d months',
    year:    'about a year',
    years:   '%d years'
  };
  
  self.inWords = function(timeAgo) {
    var seconds = Math.floor((new Date() - parseInt(timeAgo)) / 1000),
        separator = this.locales.separator || ' ',
        words = this.locales.prefix + separator,
        interval = 0,
        intervals = {
          year:   seconds / 31536000,
          month:  seconds / 2592000,
          day:    seconds / 86400,
          hour:   seconds / 3600,
          minute: seconds / 60
        };
    
    var distance = this.locales.seconds;
    
    for (var key in intervals) {
      interval = Math.floor(intervals[key]);
      
      if (interval > 1) {
        distance = this.locales[key + 's'];
        break;
      } else if (interval === 1) {
        distance = this.locales[key];
        break;
      }
    }
    
    distance = distance.replace(/%d/i, interval);
    words += distance + separator + this.locales.sufix;

    return words.trim();
  };
  
  return self;
}());


// USAGE
var timeElement = document.querySelector('time'),
    time = new Date(timeElement.getAttribute('datetime'));

timeElement.innerText = TimeAgo.inWords(time.getTime());
<time datetime="2016-06-13"></time>


6
const units = [
  ['year', 31536000000],
  ['month', 2628000000],
  ['day', 86400000],
  ['hour', 3600000],
  ['minute', 60000],
  ['second', 1000],
]

const rtf = new Intl.RelativeTimeFormat('en', { style:'narrow'})
const relatime = elapsed => {
  for (const [unit, amount] of units) {
    if (Math.abs(elapsed) > amount || unit === 'second') {
      return rtf.format(Math.round(elapsed/amount), unit)
    }
  }
}

我打了一些有趣的高尔夫 192b 呵呵

const relatime = e=>{for(let[u,a]of Object.entries({year:31536e6,month:2628e6,day:864e5,hour:36e5,minute:6e4,second:1e3})){if(Math.abs(e)>a||a===1e3){return new Intl.RelativeTimeFormat('en',{style:'narrow'}).format(~~(e/a),u)}}}

我在打高尔夫球的时候也测试了一个功能版本:

const rtf = new Intl.RelativeTimeFormat('en', { style:'narrow'})
const relatime = Object.entries({year:31536e6,month:2628e6,day:864e5,hour:36e5,minute:6e4,second:1e3})
  .reduce((f, [unit, amount]) => amount === 1e3
    ? f(elapsed => rtf.format(Math.round(elapsed/amount), unit))
    : next => f(e => Math.abs(e) < amount
      ? next(elapsed)
      : rtf.format(Math.round(elapsed/amount), unit)), _=>_)

好的,我现在必须回到工作中了...


1
这可能是编写最短代码的有趣练习,但任何阅读此内容的人都应避免复制粘贴此答案;这不是可维护的代码。 - MSOACC
感谢您的投票和有见地的评论 @MSOACC。如果您想要可维护性,可以使用长版本。 - kigiri

3

MomentJS答案


对于Moment.js的用户,它提供了fromNow()函数,可以从当前日期/时间返回“x天前”或“x小时前”的信息。

moment([2007, 0, 29]).fromNow();     // 4 years ago
moment([2007, 0, 29]).fromNow(true); // 4 years

1
Moment.js不再是被支持的库。您应该考虑使用其他库。 - sunpietro

2
与OP一样,我倾向于避免使用插件和包来编写看似微不足道的代码。当然,这样做会导致我自己编写包。
我创建了这个NPM包,以便可以轻松地将任何日期转换为相对时间字符串(例如“昨天”,“上周”,“2年前”),并带有国际化(i18n)和本地化(l10n)的翻译。
随意查看源代码;它是一个相当小的单文件。存储库中的大部分内容都是用于单元测试、版本控制和发布到NPM。
export default class RTF {
    formatters;
    options;

    /**
     * @param options {{localeMatcher: string?, numeric: string?, style: string?}} Intl.RelativeTimeFormat() options
     */
    constructor(options = RTF.defaultOptions) {
        this.options = options;
        this.formatters = { auto: new Intl.RelativeTimeFormat(undefined, this.options) };
    }

    /**
     * Add a formatter for a given locale.
     *
     * @param locale {string} A string with a BCP 47 language tag, or an array of such strings
     * @returns {boolean} True if locale is supported; otherwise false
     */
    addLocale(locale) {
        if (!Intl.RelativeTimeFormat.supportedLocalesOf(locale).includes(locale)) {
            return false;
        }
        if (!this.formatters.hasOwnProperty(locale)) {
            this.formatters[locale] = new Intl.RelativeTimeFormat(locale, this.options);
        }
        return true;
    }

    /**
     * Format a given date as a relative time string, with support for i18n.
     *
     * @param date {Date|number|string} Date object (or timestamp, or valid string representation of a date) to format
     * @param locale {string?} i18n code to use (e.g. 'en', 'fr', 'zh'); if omitted, default locale of runtime is used
     * @returns {string} Localized relative time string (e.g. '1 minute ago', '12 hours ago', '3 days ago')
     */
    format(date, locale = "auto") {
        if (!(date instanceof Date)) {
            date = new Date(Number.isNaN(date) ? Date.parse(date) : date);
        }
        if (!this.formatters.hasOwnProperty(locale) && !this.addLocale(locale)) {
            locale = "auto";
        }

        const elapsed = date - Date.now();

        for (let i = 0; i < RTF.units.length; i++) {
            const { unit, value } = RTF.units[i];
            if (unit === 'second' || Math.abs(elapsed) >= value) {
                return this.formatters[locale].format(Math.round(elapsed/value), unit);
            }
        }
    }

    /**
     * Generate HTTP middleware that works with popular frameworks and i18n tools like Express and i18next.
     *
     * @param rtf {RTF?} Instance of RTF to use; defaults to a new instance with default options
     * @param reqProp {string?} Property name to add to the HTTP request context; defaults to `rtf`
     * @param langProp {string?} Property of HTTP request context where language is stored; defaults to `language`
     * @returns {function(*, *, *): *} HTTP middleware function
     */
    static httpMiddleware(rtf = new RTF(), reqProp = "rtf", langProp = "language") {
        return (req, res, next) => {
            req[reqProp] = (date) => rtf.format(date, req[langProp]);
            next();
        };
    }

    /**
     * Default options object used by Intl.RelativeTimeFormat() constructor.
     *
     * @type {{localeMatcher: string, numeric: string, style: string}}
     */
    static defaultOptions = {
        localeMatcher: "best fit",
        numeric: "auto", // this intentionally differs from Intl.RelativeTimeFormat(), because "always" is dumb
        style: "long",
    };

    /**
     * Used to determine the arguments to pass to Intl.RelativeTimeFormat.prototype.format().
     */
    static units = [
        { unit: "year", value: 365 * 24 * 60 * 60 * 1000 },
        { unit: "month", value: 365 / 12 * 24 * 60 * 60 * 1000 },
        { unit: "week", value: 7 * 24 * 60 * 60 * 1000 },
        { unit: "day", value: 24 * 60 * 60 * 1000 },
        { unit: "hour", value: 60 * 60 * 1000 },
        { unit: "minute", value: 60 * 1000 },
        { unit: "second", value: 1000 },
    ];

    /**
     * Enumerated values for options object used by Intl.RelativeTimeFormat() constructor.
     *
     * @type {{localeMatcher: {lookup: string, default: string, bestFit: string}, numeric: {always: string, default: string, auto: string}, style: {default: string, short: string, narrow: string, long: string}}}
     */
    static opt = {
        localeMatcher: {
            bestFit: "best fit",
            lookup: "lookup",
        },
        numeric: {
            always: "always",
            auto: "auto",
        },
        style: {
            long: "long",
            narrow: "narrow",
            short: "short",
        },
    };
}

主要特点:
  • 使用本地 JavaScript Intl.RelativeTimeFormat,无需依赖。
  • 格式化任何 Date 对象、时间戳或可由 Date.parse() 解析的有效日期字符串表示。
  • 提供 HTTP 中间件,与流行的 REST 框架(如Express)和 i18n 工具(如i18next)兼容。

为什么要使用它而不是 Intl.RelativeTimeFormat.prototype.format()?

Intl.RelativeTimeFormat.prototype.format() 接受两个参数:值和单位。

const rtf = new Intl.RelativeTimeFormat("en", { style: "narrow" });

expect(rtf.format(-1, "day")).toBe("1 day ago");
expect(rtf.format(10, "seconds")).toBe("in 10 sec.");

为了将一个Date对象、时间戳或日期字符串转换,您需要编写一堆样板代码。这个库可以帮助您省去这些麻烦,并且还可以用来生成一个中间件函数,用于您的REST API,与您的i18n库一起使用。

1
如果您需要多语言支持,但不想添加像moment这样的大型库,intl-relativeformat是雅虎提供的一个不错的解决方案。
var rf = new IntlRelativeFormat('en-US');

var posts = [
    {
        id   : 1,
        title: 'Some Blog Post',
        date : new Date(1426271670524)
    },
    {
        id   : 2,
        title: 'Another Blog Post',
        date : new Date(1426278870524)
    }
];

posts.forEach(function (post) {
    console.log(rf.format(post.date));
});
// => "3 hours ago"
// => "1 hour ago"

1

如果有人感兴趣,我最终创建了一个Handlebars助手来完成这个任务。 用法:

    {{#beautify_date}}
        {{timestamp_ms}}
    {{/beautify_date}}

助手:

    Handlebars.registerHelper('beautify_date', function(options) {
        var timeAgo = new Date(parseInt(options.fn(this)));

        if (Object.prototype.toString.call(timeAgo) === "[object Date]") {
            if (isNaN(timeAgo.getTime())) {
                return 'Not Valid';
            } else {
                var seconds = Math.floor((new Date() - timeAgo) / 1000),
                intervals = [
                    Math.floor(seconds / 31536000),
                    Math.floor(seconds / 2592000),
                    Math.floor(seconds / 86400),
                    Math.floor(seconds / 3600),
                    Math.floor(seconds / 60)
                ],
                times = [
                    'year',
                    'month',
                    'day',
                    'hour',
                    'minute'
                ];

                var key;
                for(key in intervals) {
                    if (intervals[key] > 1)  
                        return intervals[key] + ' ' + times[key] + 's ago';
                    else if (intervals[key] === 1) 
                        return intervals[key] + ' ' + times[key] + ' ago';
                }

                return Math.floor(seconds) + ' seconds ago';
            }
        } else {
            return 'Not Valid';
        }
    });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接