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"
}
}