DEV Community

Chidera
Chidera

Posted on

Relative Time String

I recently came across this blog post by Steve Sewell and it really helped me in building a better time string function. Hopefully this can be a quick read for you.

Before I had this function getTimeText that was returning the time string relative to the current time:

export function getTimeText(current_date: Date, due_date: Date) {
    const _MS_PER_DAY = 1000 * 60 * 60 * 24
    // Discard the time and time-zone information.
    const utc1 = Date.UTC(current_date.getFullYear(), current_date.getMonth(), current_date.getDate())
    const utc2 = Date.UTC(due_date.getFullYear(), due_date.getMonth(), due_date.getDate())
    const days = Math.floor((utc2 - utc1) / _MS_PER_DAY)

    if (current_date.getFullYear() === due_date.getFullYear()) {
        if (current_date.getMonth() === due_date.getMonth()) {
            if (current_date.getDate() === due_date.getDate()) {
                if (current_date.getHours() === due_date.getHours()) {
                    if (current_date.getMinutes() === due_date.getMinutes()) {
                        return ['Right Now', false]
                    } else {
                        const diffInMinutes = due_date.getMinutes() - current_date.getMinutes()

                        if (diffInMinutes == 1) {
                            return ['In a Minute', false]
                        } else if (diffInMinutes == -1) {
                            return ['A Minute Ago', true]
                        } else if (diffInMinutes < 0) {
                            return [`${Math.abs(diffInMinutes)} Minutes Ago`, true]
                        } else return [`In ${diffInMinutes} Minutes`, false]
                    }
                } else {
                    const diffInHours = due_date.getHours() - current_date.getHours()

                    if (diffInHours == 1) {
                        return ['In an Hour', false]
                    } else if (diffInHours == -1) {
                        return ['An Hour Ago', true]
                    } else if (diffInHours < 0) {
                        return [`${Math.abs(diffInHours)} Hours Ago`, true]
                    } else return [`In ${diffInHours} Hours`, false]
                }
            } else {
                if (days == 1) {
                    return ['Tomorrow', false]
                } else if (days == -1) {
                    return ['Yesterday', true]
                } else if (days < 0) {
                    return [`${Math.abs(days)} Days Ago`, true]
                } else return [`In ${days} Days`, false]
            }
        } else {
            const diffInMonth = due_date.getMonth() - current_date.getMonth()

            if (diffInMonth == 1) {
                return ['Next Month', false]
            } else if (diffInMonth == -1) {
                return ['A Month Ago', true]
            } else if (diffInMonth < 0) {
                return [`${Math.abs(diffInMonth)} Months Ago`, true]
            } else return [`In ${diffInMonth} Months`, false]
        }
    } else {
        const diffInYear = due_date.getFullYear() - current_date.getFullYear()

        if (diffInYear == 1) {
            return ['Next Year', false]
        } else if (diffInYear == -1) {
            return ['A Year Ago', true]
        } else if (diffInYear < 0) {
            return [`${Math.abs(diffInYear)} Years Ago`, true]
        } else return [`In ${diffInYear} Years`, false]
    }
}
Enter fullscreen mode Exit fullscreen mode

Although this worked, I started finding some bugs.

For instance in a Todo List; if a todo was due at 11:59am and the current time is 12:05pm, the function returns 'an hour ago' instead of '6 minutes ago'. You see the problem. I eventually ignored it and said I'll fix it another time until I came across this blog post.

Basic Summary
I ended up changing my previous code to this and it fixed the edge cases.

/**
 * Convert a date to a relative time string, such as
 * "a minute ago", "in 2 hours", "yesterday", "3 months ago", etc.
 * using Intl.RelativeTimeFormat
 */
export function getRelativeTimeString(
  date: Date | number,
  lang = navigator.language
): string {
  // Allow dates or times to be passed
  const timeMs = typeof date === "number" ? date : date.getTime();

  // Get the amount of seconds between the given date and now
  const deltaSeconds = Math.round((timeMs - Date.now()) / 1000);

  // Array reprsenting one minute, hour, day, week, month, etc in seconds
  const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity];

  // Array equivalent to the above but in the string representation of the units
  const units: Intl.RelativeTimeFormatUnit[] = ["second", "minute", "hour", "day", "week", "month", "year"];

  // Grab the ideal cutoff unit
  const unitIndex = cutoffs.findIndex(cutoff => cutoff > Math.abs(deltaSeconds));

  // Get the divisor to divide from the seconds. E.g. if our unit is "day" our divisor
  // is one day in seconds, so we can divide our seconds by this to get the # of days
  const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1;

  // Intl.RelativeTimeFormat do its magic
  const rtf = new Intl.RelativeTimeFormat(lang, { numeric: "auto" });
  return rtf.format(Math.floor(deltaSeconds / divisor), units[unitIndex]);
}
Enter fullscreen mode Exit fullscreen mode

Let me know in the comments if this helped you in any way 👌🏾. And you can read the blog post to understand how this solution came about.

Top comments (0)