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]
}
}
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]);
}
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)