Back to Blog

Macros in Common Lisp

Programming A Fuzzy Time Function

updated: Thu 7 Feb 2019


While programming the comment section functionality of my website, I came across the issue of how best to display the time when a post was made. There were many choices to make, such as what time zone to use, should the date come before or after the time, or perhaps use ISO 8601 formatting.

After giving the above some thought, I decided to check out how popular sites display the time of posts. As it turns out, there is a popular time format that solves most of the above issues. I couldn't find an official name for the format, so I'll be referring to it as fuzzy time.

What Is Fuzzy Time

Fuzzy time represents the largest integer time reference that can fit into the difference between the current time and the posted time, where the time references are seconds, minutes, days, weeks, months, and year.

The fuzzy time equation:

fuzzy_time = (current_time - posted_time) / time_reference

Some examples of fuzzy time:

32 seconds ago
4 minutes ago
1 day ago
3 weeks ago
8 months ago
2 years ago

An advantage of using the fuzzy time format is that it doesn't rely on time zones, or any particular day, month, year ordering. Showing the difference is also more relevant to others looking at how long ago a post was made, instead of when it was posted. The caveat is that is is fuzzy, meaning that if a post is 1.5 days old, it will show as posted 1 day ago. Sites like twitter seem to work around this by using fuzzy time up until 24 hours have past, then switch to showing the month in string form along with the numeric day of the month. Following that method would be a good option if more granularity is required.

Some sites that use fuzzy time:

  • reddit
  • twitter
  • hacker news
  • github

Programming A Fuzzy Time Function

Now let's get into the programming portion and write the fuzzy time function. I'll be using C++ and JavaScript. Following along, it should be relatively easy to implement the same in your language of choice.

Using Unix Time

To make the calculations easier, all the timestamps will be stored and represented in unix time. Unix epoch time is the number of seconds elapsed since January 1, 1970. Each day is considered to have 86400 seconds, ignoring leap seconds. It is a good way of storing time, as it makes calculations simple, and is easy to change into other formats.

The Function

The function will take a single parameter of type integer that is large enough to handle a unix timestamp. This parameter, sec, will be the time the post was originally made. The function will return a string in the format n time_reference ago, such as 8 minutes ago.

C++

1 2 3 4 5 6 7 8
std::string fuzzy_time(long int const sec)
{
  std::string res;

  ...

  return res;
}

JS

1 2 3 4 5 6 7 8
function fuzzy_time(sec)
{
  let res = '';

  ...

  return res;
}

The code snippets that follow are implied to be inside their respective function.

At the end of this post is the full source code for each complete function.

Initialize The Time References

First calculate all the time references in seconds.

C++

1 2 3 4 5 6 7 8 9 10 11
...

long int constexpr t_second {1};
long int constexpr t_minute {t_second * 60};
long int constexpr t_hour   {t_minute * 60};
long int constexpr t_day    {t_hour * 24};
long int constexpr t_week   {t_day * 7};
long int constexpr t_month  (t_day * 30.4);
long int constexpr t_year   {t_month * 12};

...

JS

1 2 3 4 5 6 7 8 9 10 11
...

const t_second = 1;
const t_minute = t_second * 60;
const t_hour   = t_minute * 60;
const t_day    = t_hour * 24;
const t_week   = t_day * 7;
const t_month  = Math.floor(t_day * 30.4);
const t_year   = t_month * 12;

...

The month is calculated as t_day * 30.4, because the average year in the Gregorian calendar has 365.2425 days, and 365.2425 / 12 = 30.4. While this will lose some precision, remember that we are dealing with fuzzy time.

Calculate The Difference

Next, get the current time now in unix time, and calculate the difference with the post time sec. The variable dif now contains the number of seconds since the post was created.

In the JavaScript implementation, Date.now() returns unix time in milliseconds. To convert this into seconds, divide by 1000 and remove the decimal portion with Math.floor().

C++

1 2 3 4 5 6
...

std::time_t const now {std::time(nullptr)};
long int const dif {now - sec};

...

JS

1 2 3 4 5 6
...

const now = Math.floor(Date.now() / 1000);
const dif = now - sec;

...

Find The Correct Time Reference

Now find the largest time referece that can fit into the difference. Using conditional statements, compare the difference to the time references in order of greatest to least. This ensures the match will yield the largest time reference.

C++

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
...

if (dif >= t_year)
  // dif is 1 or more years
else if (dif >= t_month)
  // dif is 1 or more months
else if (dif >= t_week)
  // dif is 1 or more weeks
else if (dif >= t_day)
  // dif is 1 or more days
else if (dif >= t_hour)
  // dif is 1 or more hours
else if (dif >= t_minute)
  // dif is 1 or more minutes
else if (dif >= t_second)
  // dif is 1 or more seconds
else
  // dif is 0

...

JS

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
...

if (dif >= t_year)
  // dif is 1 or more years
else if (dif >= t_month)
  // dif is 1 or more months
else if (dif >= t_week)
  // dif is 1 or more weeks
else if (dif >= t_day)
  // dif is 1 or more days
else if (dif >= t_hour)
  // dif is 1 or more hours
else if (dif >= t_minute)
  // dif is 1 or more minutes
else if (dif >= t_second)
  // dif is 1 or more seconds
else
  // dif is 0

...

Calculate The Fuzzy Time

Once a match is found, we'll pass the selected time_referece along with a string representation of the time reference to a lambda, where the response string will be built.

Instead of a lambda, this could be a function call, or done inline under each conditional statement. Factoring it out into a lambda prevents repetition, while also keeping the scope contained within the function, as it will not be used anywhere else.

In the lambda, calculate the fuzzy time by finding out how many times the time reference fits into the difference.

Referring back to the fuzzy time equation:

fuzzy_time = (current_time - posted_time) / time_reference

This can be calculated as dif / time_reference, discarding the remainder.

The response string is built by converting the fuzzy time value into a string, and concatenating it to the time reference string. To handle plurality, an 's' character must be added if the fuzzy time value is not singular. This is done by checking if the fuzzy time is not equal to one. If that's true, add an 's' character to the end of the string.

C++

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
...

auto const fuzzy_string = [&](long int const time_ref, std::string const time_str)
{
  long int const fuzzy (dif / time_ref);

  res += std::to_string(fuzzy) + " " + time_str;
  if (fuzzy != 1)
  {
    res += "s";
  }
  res += " ago";
};

if (dif >= t_year)
  fuzzy_string(t_year, "year");
else if (dif >= t_month)
  fuzzy_string(t_month, "month");
else if (dif >= t_week)
  fuzzy_string(t_week, "week");
else if (dif >= t_day)
  fuzzy_string(t_day, "day");
else if (dif >= t_hour)
  fuzzy_string(t_hour, "hour");
else if (dif >= t_minute)
  fuzzy_string(t_minute, "minute");
else if (dif >= t_second)
  fuzzy_string(t_second, "second");
else
  res = "now";

...

JS

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
...

const fuzzy_string = (time_ref, time_str) =>
{
  const fuzzy = Math.floor(dif / time_ref);

  res += fuzzy + ' ' + time_str;
  if (fuzzy != 1)
  {
    res += 's';
  }
  res += ' ago';
}

if (dif >= t_year)
  fuzzy_string(t_year, 'year');
else if (dif >= t_month)
  fuzzy_string(t_month, 'month');
else if (dif >= t_week)
  fuzzy_string(t_week, 'week');
else if (dif >= t_day)
  fuzzy_string(t_day, 'day');
else if (dif >= t_hour)
  fuzzy_string(t_hour, 'hour');
else if (dif >= t_minute)
  fuzzy_string(t_minute, 'minute');
else if (dif >= t_second)
  fuzzy_string(t_second, 'second');
else
  res = 'now';

...

Success

That's it! You now have a fully functioning fabulous fuzzy time function. Below is the full source code for each complete function.

C++

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
std::string fuzzy_time(long int const sec)
{
  std::string res;

  long int constexpr t_second {1};
  long int constexpr t_minute {t_second * 60};
  long int constexpr t_hour   {t_minute * 60};
  long int constexpr t_day    {t_hour * 24};
  long int constexpr t_week   {t_day * 7};
  long int constexpr t_month  (t_day * 30.4);
  long int constexpr t_year   {t_month * 12};

  std::time_t const now {std::time(nullptr)};
  long int const dif {now - sec};

  auto const fuzzy_string = [&](long int const time_ref, std::string const time_str)
  {
    long int const fuzzy (dif / time_ref);

    res += std::to_string(fuzzy) + " " + time_str;
    if (fuzzy != 1)
    {
      res += "s";
    }
    res += " ago";
  };

  if (dif >= t_year)
    fuzzy_string(t_year, "year");
  else if (dif >= t_month)
    fuzzy_string(t_month, "month");
  else if (dif >= t_week)
    fuzzy_string(t_week, "week");
  else if (dif >= t_day)
    fuzzy_string(t_day, "day");
  else if (dif >= t_hour)
    fuzzy_string(t_hour, "hour");
  else if (dif >= t_minute)
    fuzzy_string(t_minute, "minute");
  else if (dif >= t_second)
    fuzzy_string(t_second, "second");
  else
    res = "now";

  return res;
}

JS

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
function fuzzy_time(sec)
{
  let res = '';

  const t_second = 1;
  const t_minute = t_second * 60;
  const t_hour   = t_minute * 60;
  const t_day    = t_hour * 24;
  const t_week   = t_day * 7;
  const t_month  = Math.floor(t_day * 30.4);
  const t_year   = t_month * 12;

  const now = Math.floor(Date.now() / 1000);
  const dif = now - sec;

  const fuzzy_string = (time_ref, time_str) =>
  {
    const fuzzy = Math.floor(dif / time_ref);

    res += fuzzy + ' ' + time_str;
    if (fuzzy != 1)
    {
      res += 's';
    }
    res += ' ago';
  }

  if (dif >= t_year)
    fuzzy_string(t_year, 'year');
  else if (dif >= t_month)
    fuzzy_string(t_month, 'month');
  else if (dif >= t_week)
    fuzzy_string(t_week, 'week');
  else if (dif >= t_day)
    fuzzy_string(t_day, 'day');
  else if (dif >= t_hour)
    fuzzy_string(t_hour, 'hour');
  else if (dif >= t_minute)
    fuzzy_string(t_minute, 'minute');
  else if (dif >= t_second)
    fuzzy_string(t_second, 'second');
  else
    res = 'now';

  return res;
}
Back to Top

Back to Blog

Macros in Common Lisp