Displaying Relative Time Difference in (int)(unit) Format e.g. 1w, 4d, 2h

Viewed 536

I'm trying to display the relative difference between a certain date and now in a shorthand manner.

Format

Given yesterday, would return 1d

Given 4 days ago, would return 4d

Given 2 hours ago, would return 2h

Given 3 weeks ago, would return 3w

I've tried using DateFormatter's in-built relative date formatting but this produces a longer result e.g. yesterday, or 1 week ago. My code is something like as follows:

let timeFormatter = DateFormatter()
timeFormatter.doesRelativeDateFormatting = true
timeFormatter.timeStyle = .short
timeFormatter.dateStyle = .short
timeFormatter.string(from: someDate)

This produces the output explained above.

I can't see any parameters in DateFormatter that would shorten this. Is there any way to do this within DateFormatter or will I need to build my own function to achieve this?

2 Answers

A couple options to consider:

Modify the output of a RelativeDateTimeFormatter

Following the suggestion of @vadian, you might consider using RelativeDateTimeFormatter to start, but will need to change its output.

For example, consider this code:

extension Date
{
    func relativeDateAsString() -> String
    {
        let df: RelativeDateTimeFormatter = RelativeDateTimeFormatter()
        var dateString: String = df.localizedString(for: self, relativeTo: Date())
        dateString = dateString.replacingOccurrences(of: "months", with: "M")
                               .replacingOccurrences(of: "month", with: "M")
                               .replacingOccurrences(of: "weeks", with: "w")
                               .replacingOccurrences(of: "week", with: "w")
                               .replacingOccurrences(of: "days", with: "d")
                               .replacingOccurrences(of: "day", with: "d")
                               .replacingOccurrences(of: "minutes", with: "m")
                               .replacingOccurrences(of: "minute", with: "m")
                               .replacingOccurrences(of: "hours", with: "h")
                               .replacingOccurrences(of: "hour", with: "h")
                               .replacingOccurrences(of: " ", with: "")
                               .replacingOccurrences(of: "ago", with: "")
                
        return dateString
    }
}

i.e. implemented as an Extension of the Date class, then consider this:

let anotherDate = Date(timeIntervalSinceNow: -2000000)
print ("\(anotherDate.relativeDateAsString())")

This above example returns "3w" instead of "3 weeks ago".

The RelativeDateTimeFormatter will return a string like "3 weeks ago", or "2 hours ago". Then use simple String substitutions to modify the text, replacing "weeks" with "w", removing "ago" and all spaces. Hopefully you get the idea...

This example doesn't consider a few things:

  • timespans greater than months or less than minutes
  • localization/other languages

Lastly, be aware that RelativeDateTimeFormatter requires iOS13+.

Use DateComponentsFormatter

Please also consider DateComponentsFormatter, which appears to come close to what you want without the string substitutions.

extension Date
{
    func relativeDateAsString() -> String
    {
        let dcf: DateComponentsFormatter = DateComponentsFormatter()
        dcf.includesApproximationPhrase = false
        dcf.includesTimeRemainingPhrase = false
        dcf.allowsFractionalUnits = false
        dcf.collapseLargestUnit = true
        dcf.maximumUnitCount = 1
        dcf.unitsStyle = .abbreviated
        dcf.allowedUnits = [.second, .minute, .hour, .day, .month, .year]
        return dcf.string(from: self, to: Date())
    }
}

This doesn't seem to handle the concept of "weeks" though, but from my experimentation does exactly what you want for the other units. For the previous example, this outputs "23d" and not "3w".

DateComponentsFormatter is supported since iOS 8.

This doesn't exist as far as I can tell. I tried to make this too a while ago similar to how PokemonGo displays dates ("1 day ago", "more than 2 days ago", "less than 1 day ago"), and it turned out that for displaying dates like these no built-in solutions exist.

Related