Skip to content

Instantly share code, notes, and snippets.

@minorbug
Created November 7, 2014 15:28
Show Gist options
  • Save minorbug/468790060810e0d29545 to your computer and use it in GitHub Desktop.
Save minorbug/468790060810e0d29545 to your computer and use it in GitHub Desktop.
"Time ago" function for Swift (based on MatthewYork's DateTools for Objective-C)
func timeAgoSinceDate(date:NSDate, numericDates:Bool) -> String {
let calendar = NSCalendar.currentCalendar()
let unitFlags = NSCalendarUnit.CalendarUnitMinute | NSCalendarUnit.CalendarUnitHour | NSCalendarUnit.CalendarUnitDay | NSCalendarUnit.CalendarUnitWeekOfYear | NSCalendarUnit.CalendarUnitMonth | NSCalendarUnit.CalendarUnitYear | NSCalendarUnit.CalendarUnitSecond
let now = NSDate()
let earliest = now.earlierDate(date)
let latest = (earliest == now) ? date : now
let components:NSDateComponents = calendar.components(unitFlags, fromDate: earliest, toDate: latest, options: nil)
if (components.year >= 2) {
return "\(components.year) years ago"
} else if (components.year >= 1){
if (numericDates){
return "1 year ago"
} else {
return "Last year"
}
} else if (components.month >= 2) {
return "\(components.month) months ago"
} else if (components.month >= 1){
if (numericDates){
return "1 month ago"
} else {
return "Last month"
}
} else if (components.weekOfYear >= 2) {
return "\(components.weekOfYear) weeks ago"
} else if (components.weekOfYear >= 1){
if (numericDates){
return "1 week ago"
} else {
return "Last week"
}
} else if (components.day >= 2) {
return "\(components.day) days ago"
} else if (components.day >= 1){
if (numericDates){
return "1 day ago"
} else {
return "Yesterday"
}
} else if (components.hour >= 2) {
return "\(components.hour) hours ago"
} else if (components.hour >= 1){
if (numericDates){
return "1 hour ago"
} else {
return "An hour ago"
}
} else if (components.minute >= 2) {
return "\(components.minute) minutes ago"
} else if (components.minute >= 1){
if (numericDates){
return "1 minute ago"
} else {
return "A minute ago"
}
} else if (components.second >= 3) {
return "\(components.second) seconds ago"
} else {
return "Just now"
}
}
@germanger
Copy link

cool, do you have a gist that considers future dates too? "In a day", "In 1 day", "In 2 weeks"

@briannezhad
Copy link

I get different results in return, can you tell me what causes this?
Example:
Return 9223372036854775807 months ago

@jacks205
Copy link

@arnaudlamy
Copy link

@sebastienboulogne
Copy link

Hi, do you think we could have a version for Swift 3 ? Thx

@msamoylov
Copy link

@tneginareb
Copy link

Swift 3

 func timeAgoSinceDate(date:NSDate, numericDates:Bool) -> String {
        let calendar = NSCalendar.current
        let unitFlags: Set<Calendar.Component> = [.minute, .hour, .day, .weekOfYear, .month, .year, .second]
        let now = NSDate()
        let earliest = now.earlierDate(date as Date)
        let latest = (earliest == now as Date) ? date : now
        let components = calendar.dateComponents(unitFlags, from: earliest as Date,  to: latest as Date)

        if (components.year! >= 2) {
            return "\(components.year!) years ago"
        } else if (components.year! >= 1){
            if (numericDates){
                return "1 year ago"
            } else {
                return "Last year"
            }
        } else if (components.month! >= 2) {
            return "\(components.month!) months ago"
        } else if (components.month! >= 1){
            if (numericDates){
                return "1 month ago"
            } else {
                return "Last month"
            }
        } else if (components.weekOfYear! >= 2) {
            return "\(components.weekOfYear!) weeks ago"
        } else if (components.weekOfYear! >= 1){
            if (numericDates){
                return "1 week ago"
            } else {
                return "Last week"
            }
        } else if (components.day! >= 2) {
            return "\(components.day!) days ago"
        } else if (components.day! >= 1){
            if (numericDates){
                return "1 day ago"
            } else {
                return "Yesterday"
            }
        } else if (components.hour! >= 2) {
            return "\(components.hour!) hours ago"
        } else if (components.hour! >= 1){
            if (numericDates){
                return "1 hour ago"
            } else {
                return "An hour ago"
            }
        } else if (components.minute! >= 2) {
            return "\(components.minute!) minutes ago"
        } else if (components.minute! >= 1){
            if (numericDates){
                return "1 minute ago"
            } else {
                return "A minute ago"
            }
        } else if (components.second! >= 3) {
            return "\(components.second!) seconds ago"
        } else {
            return "Just now"
        }

    }

@cshireman
Copy link

Awesome! You just saved me an hour.

@ReeceKenney
Copy link

Perfect thanks a bunch!

@masterravi
Copy link

Great,
Thanks guys for the efforts .Thats why I love gist will surely start contributing as much as possible soon .

@balitax
Copy link

balitax commented Dec 30, 2016

Very Nice!
Thanks Man, awesome code.

@sp71
Copy link

sp71 commented Jan 11, 2017

Sweet!

@toddkramer
Copy link

I made a Swift 3 framework for this. Has support for CocoaPods, Carthage, SPM, all platforms (iOS, macOS, tvOS, watchOS), and 42 localizations. Check it out and let me know what you think! https://github.com/toddkramer/LocalizedTimeAgo

@nikolay-dementiev
Copy link

Thanks

@heinrisch
Copy link

Using Date instead of NSDate:

func timeAgoSinceDate(_ date:Date, numericDates:Bool = false) -> String {
        let calendar = NSCalendar.current
        let unitFlags: Set<Calendar.Component> = [.minute, .hour, .day, .weekOfYear, .month, .year, .second]
        let now = Date()
        let earliest = now < date ? now : date
        let latest = (earliest == now) ? date : now
        let components = calendar.dateComponents(unitFlags, from: earliest,  to: latest)
        
        if (components.year! >= 2) {
            return "\(components.year!) years ago"
        } else if (components.year! >= 1){
            if (numericDates){
                return "1 year ago"
            } else {
                return "Last year"
            }
        } else if (components.month! >= 2) {
            return "\(components.month!) months ago"
        } else if (components.month! >= 1){
            if (numericDates){
                return "1 month ago"
            } else {
                return "Last month"
            }
        } else if (components.weekOfYear! >= 2) {
            return "\(components.weekOfYear!) weeks ago"
        } else if (components.weekOfYear! >= 1){
            if (numericDates){
                return "1 week ago"
            } else {
                return "Last week"
            }
        } else if (components.day! >= 2) {
            return "\(components.day!) days ago"
        } else if (components.day! >= 1){
            if (numericDates){
                return "1 day ago"
            } else {
                return "Yesterday"
            }
        } else if (components.hour! >= 2) {
            return "\(components.hour!) hours ago"
        } else if (components.hour! >= 1){
            if (numericDates){
                return "1 hour ago"
            } else {
                return "An hour ago"
            }
        } else if (components.minute! >= 2) {
            return "\(components.minute!) minutes ago"
        } else if (components.minute! >= 1){
            if (numericDates){
                return "1 minute ago"
            } else {
                return "A minute ago"
            }
        } else if (components.second! >= 3) {
            return "\(components.second!) seconds ago"
        } else {
            return "Just now"
        }
        
    }

@begimai
Copy link

begimai commented Feb 16, 2017

thank you, guys!

@MHX792
Copy link

MHX792 commented Feb 17, 2017

@heinrisch let calendar = NSCalendar.current should be changed to let calendar = Calendar.current otherwise the month, year, week components were 0 for me.

@jinthagerman
Copy link

jinthagerman commented Feb 26, 2017

I rewrote this to be more swift-y because I needed future dates as well. Naming could be better I guess

struct DateComponentUnitFormatter {
    
    private struct DateComponentUnitFormat {
        let unit: Calendar.Component
        
        let singularUnit: String
        let pluralUnit: String
        
        let futureSingular: String
        let pastSingular: String
    }
    
    private let formats: [DateComponentUnitFormat] = [
        
        DateComponentUnitFormat(unit: .year,
                                singularUnit: "year",
                                pluralUnit: "years",
                                futureSingular: "Next year",
                                pastSingular: "Last year"),
        
        DateComponentUnitFormat(unit: .month,
                                singularUnit: "month",
                                pluralUnit: "months",
                                futureSingular: "Next month",
                                pastSingular: "Last month"),
        
        DateComponentUnitFormat(unit: .weekOfYear,
                                singularUnit: "week",
                                pluralUnit: "weeks",
                                futureSingular: "Next week",
                                pastSingular: "Last week"),
        
        DateComponentUnitFormat(unit: .day,
                                singularUnit: "day",
                                pluralUnit: "days",
                                futureSingular: "Tomorrow",
                                pastSingular: "Yesterday"),
        
        DateComponentUnitFormat(unit: .hour,
                                singularUnit: "hour",
                                pluralUnit: "hours",
                                futureSingular: "In an hour",
                                pastSingular: "An hour ago"),
        
        DateComponentUnitFormat(unit: .minute,
                                singularUnit: "minute",
                                pluralUnit: "minutes",
                                futureSingular: "In a minute",
                                pastSingular: "A minute ago"),
        
        DateComponentUnitFormat(unit: .second,
                                singularUnit: "second",
                                pluralUnit: "seconds",
                                futureSingular: "Just now",
                                pastSingular: "Just now"),
        
        ]
    
    func string(forDateComponents dateComponents: DateComponents, useNumericDates: Bool) -> String {
        for format in self.formats {
            let unitValue: Int
            
            switch format.unit {
            case .year:
                unitValue = dateComponents.year ?? 0
            case .month:
                unitValue = dateComponents.month ?? 0
            case .weekOfYear:
                unitValue = dateComponents.weekOfYear ?? 0
            case .day:
                unitValue = dateComponents.day ?? 0
            case .hour:
                unitValue = dateComponents.hour ?? 0
            case .minute:
                unitValue = dateComponents.minute ?? 0
            case .second:
                unitValue = dateComponents.second ?? 0
            default:
                assertionFailure("Date does not have requried components")
                return ""
            }
            
            switch unitValue {
            case 2 ..< Int.max:
                return "\(unitValue) \(format.pluralUnit) ago"
            case 1:
                return useNumericDates ? "\(unitValue) \(format.singularUnit) ago" : format.pastSingular
            case -1:
                return useNumericDates ? "In \(-unitValue) \(format.singularUnit)" : format.futureSingular
            case Int.min ..< -1:
                return "In \(-unitValue) \(format.pluralUnit)"
            default:
                break
            }
        }
        
        return "Just now"
    }
}

extension Date {
    
    func timeAgoSinceNow(useNumericDates: Bool = false) -> String {
        
        let calendar = Calendar.current
        let unitFlags: Set<Calendar.Component> = [.minute, .hour, .day, .weekOfYear, .month, .year, .second]
        let now = Date()
        let components = calendar.dateComponents(unitFlags, from: self, to: now)
        
        let formatter = DateComponentUnitFormatter()
        return formatter.string(forDateComponents: components, useNumericDates: useNumericDates)
    }
}

https://gist.github.com/jinthagerman/009c85b7bbd0a40dcbba747e89a501bf

@FeleciaGenet
Copy link

Golden. Thanks for the time saver.

If anyone found themselves here and needed usage:



if let timeAgo = timeAgoSinceDate(yourInfoAsDateFormat) {
                    
                   yourLabel.text = timeAgo
                    
                }

@oleander
Copy link

Here's a another version

import Foundation

extension Date {
  fileprivate struct Item {
    let multi: String
    let single: String
    let last: String
    let value: Int?
  }

  fileprivate var components: DateComponents {
    return Calendar.current.dateComponents(
      [.minute, .hour, .day, .weekOfYear, .month, .year, .second],
      from: Calendar.current.date(byAdding: .second, value: -1, to: Date())!,
      to: self
    )
  }

  fileprivate var items: [Item] {
    return [
      Item(multi: "years ago", single: "1 year ago", last: "Last year", value: components.year),
      Item(multi: "months ago", single: "1 month ago", last: "Last month", value: components.month),
      Item(multi: "weeks ago", single: "1 week ago", last: "Last week", value: components.weekday),
      Item(multi: "days ago", single: "1 day ago", last: "Last day", value: components.day),
      Item(multi: "minutes ago", single: "1 minute ago", last: "Last minute", value: components.minute),
      Item(multi: "seconds ago", single: "Just now", last: "Last second", value: components.second)
    ]
  }

  public func timeAgo(numericDates: Bool = false) -> String {
    for item in items {
      switch (item.value, numericDates) {
      case let (.some(step), _) where step == 0:
        continue
      case let (.some(step), true) where step == 1:
        return item.last
      case let (.some(step), false) where step == 1:
        return item.single
      case let (.some(step), _):
        return String(step) + " " + item.multi
      default:
        continue
      }
    }

    return "Just now"
  }
}

print(Calendar.current.date(byAdding: .day, value: 1, to: Date())!.timeAgo())

@dariukas
Copy link

dariukas commented Aug 6, 2018

A Swift 4 version without force unwrapping:

extension Date {
    func timeAgoSinceDate(numericDates:Bool) -> String {
        let calendar = Calendar.current
        let now = Date()
        let earliest = self < now ? self : now
        let latest =  self > now ? self : now
        
        let unitFlags: Set<Calendar.Component> = [.minute, .hour, .day, .weekOfMonth, .month, .year, .second]
        let components: DateComponents = calendar.dateComponents(unitFlags, from: earliest, to: latest)
        
        if let year = components.year {
            if (year >= 2) {
                return "\(year) years ago"
            } else if (year >= 1) {
                return stringToReturn(flag: numericDates, strings: ("1 year ago", "Last year"))
            }
        }
        
        if let month = components.month {
            if (month >= 2) {
                return "\(month) months ago"
            } else if (month >= 2) {
                return stringToReturn(flag: numericDates, strings: ("1 month ago", "Last month"))
            }
        }
        
        if let weekOfYear = components.weekOfYear {
            if (weekOfYear >= 2) {
                return "\(weekOfYear) months ago"
            } else if (weekOfYear >= 2) {
                return stringToReturn(flag: numericDates, strings: ("1 week ago", "Last week"))
            }
        }
        
        if let day = components.day {
            if (day >= 2) {
                return "\(day) days ago"
            } else if (day >= 2) {
                return stringToReturn(flag: numericDates, strings: ("1 day ago", "Yesterday"))
            }
        }
        
        if let hour = components.hour {
            if (hour >= 2) {
                return "\(hour) hours ago"
            } else if (hour >= 2) {
                return stringToReturn(flag: numericDates, strings: ("1 hour ago", "An hour ago"))
            }
        }
        
        if let minute = components.minute {
            if (minute >= 2) {
                return "\(minute) minutes ago"
            } else if (minute >= 2) {
                return stringToReturn(flag: numericDates, strings: ("1 minute ago", "A minute ago"))
            }
        }
        
        if let second = components.second {
            if (second >= 3) {
                return "\(second) seconds ago"
            }
        }
        
        return "Just now"
    }
    
    private func stringToReturn(flag:Bool, strings: (String, String)) -> String {
        if (flag){
            return strings.0
        } else {
            return strings.1
        }
    }
}

@oluckyman
Copy link

Sorry for interrupting the party, but seems like "time ago" feature is implemented in DateTools with locales and just works:
https://github.com/MatthewYork/DateTools#time-ago

let timeAgoDate = 2.days.earlier
print("Time Ago: ", timeAgoDate.timeAgoSinceNow)
print("Time Ago: ", timeAgoDate.shortTimeAgoSinceNow)

//Output:
//Time Ago: 2 days ago
//Time Ago: 2d

@dariukas
Copy link

dariukas commented Aug 16, 2018

The framework looks nice. However, sometimes only a feature is needed, not the whole framework = why to buy the whole cow if you need only milk.

@merlos
Copy link

merlos commented Sep 23, 2018

This is my version. It is based on @dariukas, but fixes some bugs

import Foundation

extension Date {
    ///
    /// Provides a humanised date. For instance: 1 minute, 1 week ago, 3 months ago
    ///
    /// - Parameters:
    //      - numericDates: Set it to true to get "1 year ago", "1 month ago" or false if you prefer "Last year", "Last month"
    ///
    func timeAgo(numericDates:Bool) -> String {
        let calendar = Calendar.current
        let now = Date()
        let earliest = self < now ? self : now
        let latest =  self > now ? self : now
        
        let unitFlags: Set<Calendar.Component> = [.minute, .hour, .day, .weekOfMonth, .month, .year, .second]
        let components: DateComponents = calendar.dateComponents(unitFlags, from: earliest, to: latest)
        //print("")
        //print(components)
        if let year = components.year {
            if (year >= 2) {
                return "\(year) years ago"
            } else if (year >= 1) {
                return numericDates ?  "1 year ago" : "Last year"
            }
        }
        if let month = components.month {
            if (month >= 2) {
                return "\(month) months ago"
            } else if (month >= 1) {
                return numericDates ? "1 month ago" : "Last month"
            }
        }
        if let weekOfMonth = components.weekOfMonth {
            if (weekOfMonth >= 2) {
                return "\(weekOfMonth) weeks ago"
            } else if (weekOfMonth >= 1) {
                return numericDates ? "1 week ago" : "Last week"
            }
        }
        if let day = components.day {
            if (day >= 2) {
                return "\(day) days ago"
            } else if (day >= 1) {
                return numericDates ? "1 day ago" : "Yesterday"
            }
        }
        if let hour = components.hour {
            if (hour >= 2) {
                return "\(hour) hours ago"
            } else if (hour >= 1) {
                return numericDates ? "1 hour ago" : "An hour ago"
            }
        }
        if let minute = components.minute {
            if (minute >= 2) {
                return "\(minute) minutes ago"
            } else if (minute >= 1) {
                return numericDates ? "1 minute ago" : "A minute ago"
            }
        }
        if let second = components.second {
            if (second >= 3) {
                return "\(second) seconds ago"
            }
        }
        return "Just now"
    }
}

@dx777
Copy link

dx777 commented Sep 23, 2018

@merlos nice, thank you !

@demonukg
Copy link

@merlos awesome! thank you

@rafalwojcik
Copy link

rafalwojcik commented Dec 4, 2018

My version in Swift 4.2:

import Foundation

extension Date {
    func timeAgo(numericDates: Bool) -> String {
        let calendar = Calendar.current
        let now = Date()
        let earliest = self < now ? self : now
        let latest =  self > now ? self : now

        let unitFlags: Set<Calendar.Component> = [.minute, .hour, .day, .weekOfMonth, .month, .year, .second]
        let components: DateComponents = calendar.dateComponents(unitFlags, from: earliest, to: latest)

        let year = components.year ?? 0
        let month = components.month ?? 0
        let weekOfMonth = components.weekOfMonth ?? 0
        let day = components.day ?? 0
        let hour = components.hour ?? 0
        let minute = components.minute ?? 0
        let second = components.second ?? 0

        switch (year, month, weekOfMonth, day, hour, minute, second) {
            case (let year, _, _, _, _, _, _) where year >= 2: return "\(year) years ago"
            case (let year, _, _, _, _, _, _) where year == 1 && numericDates: return "1 year ago"
            case (let year, _, _, _, _, _, _) where year == 1 && !numericDates: return "Last year"
            case (_, let month, _, _, _, _, _) where month >= 2: return "\(month) months ago"
            case (_, let month, _, _, _, _, _) where month == 1 && numericDates: return "1 month ago"
            case (_, let month, _, _, _, _, _) where month == 1 && !numericDates: return "Last month"
            case (_, _, let weekOfMonth, _, _, _, _) where weekOfMonth >= 2: return "\(weekOfMonth) weeks ago"
            case (_, _, let weekOfMonth, _, _, _, _) where weekOfMonth == 1 && numericDates: return "1 week ago"
            case (_, _, let weekOfMonth, _, _, _, _) where weekOfMonth == 1 && !numericDates: return "Last week"
            case (_, _, _, let day, _, _, _) where day >= 2: return "\(day) days ago"
            case (_, _, _, let day, _, _, _) where day == 1 && numericDates: return "1 day ago"
            case (_, _, _, let day, _, _, _) where day == 1 && !numericDates: return "Yesterday"
            case (_, _, _, _, let hour, _, _) where hour >= 2: return "\(hour) hours ago"
            case (_, _, _, _, let hour, _, _) where hour == 1 && numericDates: return "1 hour ago"
            case (_, _, _, _, let hour, _, _) where hour == 1 && !numericDates: return "An hour ago"
            case (_, _, _, _, _, let minute, _) where minute >= 2: return "\(minute) minutes ago"
            case (_, _, _, _, _, let minute, _) where minute == 1 && numericDates: return "1 minute ago"
            case (_, _, _, _, _, let minute, _) where minute == 1 && !numericDates: return "A minute ago"
            case (_, _, _, _, _, _, let second) where second >= 3: return "\(second) seconds ago"
            default: return "Just now"
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment