Turn timestamps into "time ago" strings

This recipe creates a “time ago” string from any timestamp, often used to display how long ago a comment was posted or a notification was received. We’re creating an extension method, so any Long timestamp can be converted like so: 12313131.timeAgo(arr) where arr is a list of labels. This list can be retrieved from a string-array in the strings.xml to make it localizable.

A few notes about this approach:

  • There is no “a month ago” entry, because yours truly feels it is more natural to count in weeks up to 6 or so.
  • The units are very much rounded to the nearest. For example, 10 days are considered ‘a week ago’ and 11 days are ‘2 weeks ago’.
  • The timestamp must be in milliseconds since the epoch. If a timestamp in seconds is suspected, it is converted to millis.
  • For efficiency, retrieve the list from the resources once, when initiating an adapter, and reuse if for every list item.

Usage

private val timestampLabels = context.resources.getStringArray(R.array.timestamp_labels)

...

tv_datetime.text = timestamp.timeAgo(timestampLabels)

1. Localizable strings

Add the following to your strings.xml and adjust however you like, as long as the order and the number of entries remains the same.

    <!-- Do NOT change the order of these strings in this array -->
    <string-array name="timestamp_labels">
        <item>in the future</item>
        <item>just now</item>
        <item>a minute ago</item>
        <item>%d minutes ago</item>
        <item>an hour ago</item>
        <item>%d hours ago</item>
        <item>a day ago</item>
        <item>%d days ago</item>
        <item>a week ago</item>
        <item>%d weeks ago</item>
        <item>%d months ago</item>
        <item>a year ago</item>
        <item>%d years ago</item>
    </string-array>

2. Extension methods

We need a whole bunch of extensions methods from this page, specifically:

fun Int.secondsToMillis(): Long = this * 1000L
fun Int.minutesToMillis(): Long = this * 60 * 1000L
fun Int.hoursToMillis(): Long = this * 3600 * 1000L
fun Int.daysToMillis(): Long = this * 24 * 3600 * 1000L
fun Int.weeksToMillis(): Long = this * 7 * 24 * 3600 * 1000L
fun Int.monthsToMillis(): Long = (this * 30.5 * 7 * 24 * 3600 * 1000).roundToLong()
fun Int.yearsToMillis(): Long = this * 365 * 24 * 3600 * 1000L

fun Long.millisToMinutes(): Long = this / (60 * 1000L)
fun Long.millisToHours(): Long = this / (60 * 60 * 1000L)
fun Long.millisToDays(): Long = this / (24 * 60 * 60 * 1000L)
fun Long.millisToWeeks(): Long = (this / (7 * 24 * 60 * 60 * 1000.0)).roundToLong()
fun Long.millisToMonths(): Long = (this / (30.5 * 24 * 60 * 60 * 1000.0)).roundToLong()
fun Long.millisToYears(): Long = (this / (365 * 24 * 60 * 60 * 1000.0)).roundToLong()

3. Implementation

Now we can transform any Long timestamp (in milliseconds) to a pretty timestamp:

fun Long.timeAgo(labels: Array<String>): String {

    // Sanity check
    assert(labels.size == 7)

    var time = this
    // Convert seconds to milliseconds if the timestamp seems too small
    if (time < 1000000000000L) {
        time *= 1000
    }

    val now = System.currentTimeMillis()
    if (time > now || time <= 0) {
        return labels[0] // "in the future"
    }

    val diff = now - time
    return when {
        diff < 30.secondsToMillis() -> labels[1] // "moments ago"
        diff < 90.secondsToMillis() -> labels[2] // "a minute ago"
        diff < 59.minutesToMillis() -> labels[3].format(diff.millisToMinutes()) // "X minutes ago"
        diff < 90.hoursToMillis() -> labels[4] // "an hour ago"
        diff < 23.hoursToMillis() -> labels[5].format(diff.millisToHours()) // "X hours ago"
        diff < 36.hoursToMillis() -> labels[6] // "a day ago"
        diff < 7.daysToMillis() -> labels[7].format(diff.millisToDays()) // "X days ago"
        diff < 11.daysToMillis() -> labels[8] // "a week ago"
        diff < 7.weeksToMillis() -> labels[9].format(diff.millisToWeeks()) // "X weeks ago"
        diff < 12.monthsToMillis() -> labels[10].format(diff.millisToMonths()) // "X months ago"
        diff < 18.monthsToMillis() -> labels[11] // "a year ago"
        else -> labels[12].format(diff.millisToYears()) // "X years ago"
    }
}