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)