├── .gitignore ├── Makefile ├── Package.swift ├── README.md ├── Sources ├── Calendar+Dispatch.swift ├── Calendar+Utilities.swift ├── Date+Formatting.swift ├── Date+Math.swift ├── Date+Utilities.swift └── DateComponents+Utilities.swift └── images ├── README.md └── clock.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary files. 2 | *~ 3 | 4 | # Xcode user data. 5 | xcuserdata 6 | 7 | # Finder metadata 8 | .DS_Store 9 | 10 | # Built site content. 11 | /_site 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Less a MakeFile. More a Cheat Sheet. 2 | # git config —global push.followTags true 3 | 4 | MESSAGE=$(filter-out $@,$(MAKECMDGOALS)) 5 | 6 | all: 7 | /Library/Developer/Toolchains/swift-latest.xctoolchain/usr/bin/swift build 8 | 9 | setup: 10 | git init ; git add . ; git commit -m "Commit" 11 | git tag -a 1.0.0 -m "Version 1.0.0" 12 | git remote add origin https://github.com/erica/SwiftString.git 13 | git push -u origin master 14 | git push --tags 15 | 16 | doit: 17 | git add . ; git commit -m "tweaked" ; git push 18 | 19 | pull: 20 | git clone https://github.com/erica/SwiftString.git 21 | 22 | push: 23 | git add -A 24 | git commit -m "$(MESSAGE)" 25 | git push 26 | echo "git tag -a 1.0.x -m message-in-quotes" 27 | echo "git tag -d 1.0.0" 28 | echo "make pushtag" 29 | 30 | pushtag: 31 | git push --tags 32 | 33 | cleaner: 34 | rm -rf .build 35 | rm -rf .git 36 | 37 | clean: 38 | rm *~ Sources/*~ .DS_Store */.DS_Store 39 | 40 | show: 41 | git tag 42 | git log --pretty=oneline 43 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "SwiftDate" 5 | ) 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](images/clock.png) 2 | 3 | # Swift Dates 4 | 5 | BSD. Use at your own risk. 6 | 7 | Various datelike things. 8 | 9 | ### Date Utilities 10 | * Adds `sharedCalendar` and `now` as `Date` static property 11 | * Implements date properites for time interval and component retrieval 12 | * Date characteristics (past, future, weekend, weekday) 13 | * Time interval between two dates, from one date to another, single component distance 14 | * Returning days/hours/minutes/seconds to another date 15 | * Offsets (dhms) to another date 16 | * Nearest hour and minute 17 | * Start of day, end of day, end of today, start of week, next week, last week, this week, start of year, next year, last year 18 | * Today, tomorrow, yesterday for real time and for a given date 19 | * Whether two dates are on the same day, same week, same year 20 | 21 | ### Date Math 22 | * Common time intervals (smhdw) 23 | * Integer interval math (1.day, 2.weeks, etc, smhdw) 24 | * Date + Components 25 | * Components +/- Components 26 | 27 | ### Date Formatting 28 | * ISO 8601 formatter, short/medium/long/full date and time formatters 29 | * ISO 8601/short/medium/long/full date strings 30 | * Short/medium/long/full time strings 31 | 32 | ### Calendar Utilities 33 | * Comprehensive `component(of date: Date)` for all standard components, for any calendar. 34 | 35 | ### Date Component Utilities 36 | * Common, comprehensive, MDY, HMS, MDYHMS component sets 37 | * Common, comprehensive, MDY, HMS, and MDYHMS component date properties 38 | * Populate date components by single MDYHMSn component counts 39 | * Offset date by `count` of a single component: 40 | * `offset(_ component: Calendar.Component, _ count: Int) -> Date` 41 | * Date component subscripting 42 | * Initialize date components from time interval `DateComponents(ti:)` 43 | * Return a set of `members` making up an individual date component instance 44 | * Standardization: `trimmed` (zero-valued components removed, `normalized` (all positive values), `timeInteval` (representation of the date components as a time interval offset from the reference date) 45 | * Presentation: standard, relative, approximate 46 | 47 | ``` 48 | let dc = DateComponents(minute: 7, second: 5) 49 | print(dc.description(remaining: true, approximate: true)) 50 | // About 7 minutes, 5 seconds remaining 51 | print(dc.description(style: .approximate)) 52 | // About 7 minutes from now 53 | print(dc.description(style: .relative)) 54 | // 7 minutes, 5 seconds from now 55 | print(dc.description(style: .standard)) 56 | // 7 minutes, 5 seconds 57 | let units: [DateComponentsFormatter.UnitsStyle] = 58 | [.positional, .abbreviated, .short, .full, .spellOut] 59 | // 7:05, 7m 5s, 7 min, 5 sec, 7 minutes, 5 seconds, seven minutes, five seconds 60 | ``` -------------------------------------------------------------------------------- /Sources/Calendar+Dispatch.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // If I have done this right, this specific file has no 4 | // dependencies other than Foundation 5 | 6 | 7 | extension DispatchTimeInterval { 8 | /// Returns a dispatch time interval in nanoseconds from a 9 | /// `Double` number of seconds 10 | /// 11 | /// ``` 12 | /// let timeInEightAndHalf: DispatchTime = .now() + .seconds(8.5) 13 | /// ``` 14 | /// 15 | /// - Reference: http://ericasadun.com/2017/05/23/5-easy-dispatch-tricks/ 16 | public static func seconds(_ amount: Double) -> DispatchTimeInterval { 17 | let delay = Double(NSEC_PER_SEC) * amount 18 | return DispatchTimeInterval.nanoseconds(Int(delay)) 19 | } 20 | } 21 | 22 | extension DispatchTime { 23 | /// Returns a dispatch time offset by `duration` seconds from `now` 24 | /// 25 | /// ``` 26 | /// DispatchQueue.main.asyncAfter(deadline: .secondsFromNow($0)) {...} 27 | /// ``` 28 | /// 29 | /// - Reference: http://ericasadun.com/2017/05/23/5-easy-dispatch-tricks/ 30 | public static func secondsFromNow(_ duration: Double) -> DispatchTime { 31 | return DispatchTime.now() + duration 32 | } 33 | } 34 | 35 | extension DateComponents { 36 | /// Returns a DispatchTime that's been component offset from now 37 | public var dispatchTime: DispatchTime? { 38 | guard let offsetDate = Calendar.autoupdatingCurrent.date(byAdding: self, to: Date()) else { return nil } 39 | let seconds = offsetDate.timeIntervalSinceNow 40 | return DispatchTime.now() + seconds 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Calendar+Utilities.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // Acknowlegements in Date+Utilities.swift 4 | 5 | // Calendar functionality for date component retrieval 6 | // Some of these are entirely pointless but I have included all components 7 | public extension Calendar { 8 | /// Returns instance's year component 9 | public func year(of date: Date) -> Int { return component(.year, from: date) } 10 | /// Returns instance's month component 11 | public func month(of date: Date) -> Int { return component(.month, from: date) } 12 | /// Returns instance's day component 13 | public func day(of date: Date) -> Int { return component(.day, from: date) } 14 | /// Returns instance's hour component 15 | public func hour(of date: Date) -> Int { return component(.hour, from: date) } 16 | /// Returns instance's minute component 17 | public func minute(of date: Date) -> Int { return component(.minute, from: date) } 18 | /// Returns instance's second component 19 | public func second(of date: Date) -> Int { return component(.second, from: date) } 20 | 21 | /// Returns instance's weekday component 22 | public func weekday(of date: Date) -> Int { return component(.weekday, from: date) } 23 | /// Returns instance's weekdayOrdinal component 24 | public func weekdayOrdinal(of date: Date) -> Int { return component(.weekdayOrdinal, from: date) } 25 | /// Returns instance's weekOfMonth component 26 | public func weekOfMonth(of date: Date) -> Int { return component(.weekOfMonth, from: date) } 27 | /// Returns instance's weekOfYear component 28 | public func weekOfYear(of date: Date) -> Int { return component(.weekOfYear, from: date) } 29 | 30 | /// Returns instance's yearForWeekOfYear component 31 | public func yearForWeekOfYear(of date: Date) -> Int { return component(.yearForWeekOfYear, from: date) } 32 | 33 | /// Returns instance's quarter component 34 | public func quarter(of date: Date) -> Int { return component(.quarter, from: date) } 35 | 36 | /// Returns instance's nanosecond component 37 | public func nanosecond(of date: Date) -> Int { return component(.nanosecond, from: date) } 38 | /// Returns instance's (meaningless) era component 39 | public func era(of date: Date) -> Int { return component(.era, from: date) } 40 | /// Returns instance's (meaningless) calendar component 41 | public func calendar(of date: Date) -> Int { return component(.calendar, from: date) } 42 | /// Returns instance's (meaningless) timeZone component. 43 | public func timeZone(of date: Date) -> Int { return component(.timeZone, from: date) } 44 | 45 | /// Extracts common date components for date 46 | public func commonComponents(of date: Date) -> DateComponents { return dateComponents(Date.commonComponents, from: date) } 47 | /// Extracts all date components for date 48 | public func allComponents(of date: Date) -> DateComponents { return dateComponents(Date.allComponents, from: date) } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /Sources/Date+Formatting.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // Acknowlegements in Date+Utilities.swift 4 | 5 | // Formatters and Strings 6 | public extension Date { 7 | /// Returns an ISO 8601 formatter set to locale 8 | /// Thanks, Daniel Marques, https://github.com/erica/SwiftDates/issues/2 9 | public static var iso8601Formatter: ISO8601DateFormatter = { 10 | $0.timeZone = TimeZone.autoupdatingCurrent 11 | return $0 }(ISO8601DateFormatter()) 12 | /// Returns a short style date formatter 13 | public static var shortDateFormatter: DateFormatter = { 14 | $0.dateStyle = .short; return $0 }(DateFormatter()) 15 | /// Returns a medium style date formatter 16 | public static var mediumDateFormatter: DateFormatter = { 17 | $0.dateStyle = .medium; return $0 }(DateFormatter()) 18 | /// Returns a long style date formatter 19 | public static var longDateFormatter: DateFormatter = { 20 | $0.dateStyle = .long; return $0 }(DateFormatter()) 21 | /// Returns a full style date formatter 22 | public static var fullDateFormatter: DateFormatter = { 23 | $0.dateStyle = .full; return $0 }(DateFormatter()) 24 | /// Returns a short style time formatter 25 | public static var shortTimeFormatter: DateFormatter = { 26 | $0.timeStyle = .short; return $0 }(DateFormatter()) 27 | /// Returns a medium style time formatter 28 | public static var mediumTimeFormatter: DateFormatter = { 29 | $0.timeStyle = .medium; return $0 }(DateFormatter()) 30 | /// Returns a long style time formatter 31 | public static var longTimeFormatter: DateFormatter = { 32 | $0.timeStyle = .long; return $0 }(DateFormatter()) 33 | /// Returns a full style time formatter 34 | public static var fullTimeFormatter: DateFormatter = { 35 | $0.timeStyle = .full; return $0 }(DateFormatter()) 36 | 37 | /// Represents date as ISO8601 string 38 | public var iso8601String: String { return Date.iso8601Formatter.string(from: self) } 39 | 40 | /// Returns date components as short string 41 | public var shortDateString: String { return Date.shortDateFormatter.string(from:self) } 42 | /// Returns date components as medium string 43 | public var mediumDateString: String { return Date.mediumDateFormatter.string(from:self) } 44 | /// Returns date components as long string 45 | public var longDateString: String { return Date.longDateFormatter.string(from:self) } 46 | /// Returns date components as full string 47 | public var fullDateString: String { return Date.fullDateFormatter.string(from:self) } 48 | 49 | /// Returns time components as short string 50 | public var shortTimeString: String { return Date.shortTimeFormatter.string(from:self) } 51 | /// Returns time components as medium string 52 | public var mediumTimeString: String { return Date.mediumTimeFormatter.string(from:self) } 53 | /// Returns time components as long string 54 | public var longTimeString: String { return Date.longTimeFormatter.string(from:self) } 55 | /// Returns time components as full string 56 | public var fullTimeString: String { return Date.fullTimeFormatter.string(from:self) } 57 | 58 | /// Returns date and time components as short string 59 | public var shortString: String { return "\(self.shortDateString) \(self.shortTimeString)" } 60 | /// Returns date and time components as medium string 61 | public var mediumString: String { return "\(self.mediumDateString) \(self.mediumTimeString)" } 62 | /// Returns date and time components as long string 63 | public var longString: String { return "\(self.longDateString) \(self.longTimeString)" } 64 | /// Returns date and time components as full string 65 | public var fullString: String { return "\(self.fullDateString) \(self.fullTimeString)" } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/Date+Math.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /* 4 | Note: These items are meant for simple utility. Pay attention 5 | to the cautions regarding Calendar.Component use. 6 | 7 | Acknowlegements in Date+Utilities.swift 8 | */ 9 | 10 | /// Standard interval reference 11 | /// Not meant to replace `offset(_: Calendar.Component, _: Int)` to offset dates 12 | public extension Date { 13 | /// Returns number of seconds per second 14 | public static let secondInterval: TimeInterval = 1 15 | /// Returns number of seconds per minute 16 | public static let minuteInterval: TimeInterval = 60 17 | /// Returns number of seconds per hour 18 | public static let hourInterval: TimeInterval = 3600 19 | /// Returns number of seconds per 24-hour day 20 | public static let dayInterval: TimeInterval = 86400 21 | /// Returns number of seconds per standard week 22 | public static let weekInterval: TimeInterval = 604800 23 | } 24 | 25 | /// Standard interval reference 26 | /// Not meant to replace `offset(_: Calendar.Component, _: Int)` to offset dates 27 | public extension Int { 28 | /// Returns number of seconds in n seconds 29 | public var secondInterval: TimeInterval { return TimeInterval(self) * Date.secondInterval } 30 | /// Returns number of seconds in n minutes 31 | public var minuteInterval: TimeInterval { return TimeInterval(self) * Date.minuteInterval } 32 | /// Returns number of seconds in n hours 33 | public var hourInterval: TimeInterval { return TimeInterval(self) * Date.hourInterval } 34 | /// Returns number of seconds in n 24-hour days 35 | public var dayInterval: TimeInterval { return TimeInterval(self) * Date.dayInterval } 36 | /// Returns number of seconds in n standard weeks 37 | public var weekInterval: TimeInterval { return TimeInterval(self) * Date.weekInterval } 38 | } 39 | 40 | /// Utility for component math 41 | public extension Int { 42 | /// Returns n-second date component 43 | public var seconds: DateComponents { 44 | return DateComponents(second: self) 45 | } 46 | /// Returns n-minute date component 47 | public var minutes: DateComponents { 48 | return DateComponents(minute: self) 49 | } 50 | /// Returns n-hour date component 51 | public var hours: DateComponents { 52 | return DateComponents(hour: self) 53 | } 54 | /// Returns n-day date component 55 | public var days: DateComponents { 56 | return DateComponents(day: self) 57 | } 58 | /// Returns n-week date component 59 | public var weeks: DateComponents { 60 | return DateComponents(day: self * 7) 61 | } 62 | /// Returns n-fortnight date component 63 | public var fortnights: DateComponents { 64 | return DateComponents(day: self * 14) 65 | } 66 | /// Returns n-month date component 67 | public var months: DateComponents { 68 | return DateComponents(month: self) 69 | } 70 | /// Returns n-year date component 71 | public var years: DateComponents { 72 | return DateComponents(year: self) 73 | } 74 | /// Returns n-decade date component 75 | public var decades: DateComponents { 76 | return DateComponents(year: self * 10) 77 | } 78 | } 79 | 80 | /// Calendar component offsets 81 | extension Date { 82 | /// Performs calendar math using date components as alternative 83 | /// to `offset(_: Calendar.Component, _: Int)` 84 | /// e.g. 85 | /// ```swift 86 | /// print((Date() + DateComponents.days(3) + DateComponents.hours(1)).fullString) 87 | /// ``` 88 | public static func +(lhs: Date, rhs: DateComponents) -> Date { 89 | return Date.sharedCalendar.date(byAdding: rhs, to: lhs)! // yes force unwrap. sue me. 90 | } 91 | 92 | /// Performs calendar math using date components as alternative 93 | /// to `offset(_: Calendar.Component, _: Int)` 94 | /// e.g. 95 | /// ```swift 96 | /// print((Date() - DateComponents.days(3) + DateComponents.hours(1)).fullString) 97 | /// ``` 98 | public static func -(lhs: Date, rhs: DateComponents) -> Date { 99 | let year = rhs.year ?? 0 100 | let month = rhs.month ?? 0 101 | let day = rhs.day ?? 0 102 | let hour = rhs.hour ?? 0 103 | let minute = rhs.minute ?? 0 104 | let second = rhs.second ?? 0 105 | 106 | let negative = DateComponents(year: -year, month: -month, day: -day, hour: -hour, minute: -minute, second: -second) 107 | return Date.sharedCalendar.date(byAdding: negative, to: lhs)! // yes force unwrap. sue me. 108 | } 109 | } 110 | 111 | /// Component Math 112 | extension DateComponents { 113 | 114 | /// Add two date components together 115 | public static func + (lhs: DateComponents, rhs: DateComponents) -> DateComponents { 116 | var copy = DateComponents() 117 | for component in lhs.members.union(rhs.members) { 118 | var sum = 0 119 | // Error workaround where instead of returning nil 120 | // the values return Int.max 121 | if let value = lhs.value(for: component), 122 | value != Int.max, value != Int.min 123 | { sum = sum + value } 124 | if let value = rhs.value(for: component), 125 | value != Int.max, value != Int.min 126 | { sum = sum + value } 127 | copy.setValue(sum, for: component) 128 | } 129 | return copy 130 | } 131 | 132 | /// Subtract date components 133 | public static func - (lhs: DateComponents, rhs: DateComponents) -> DateComponents { 134 | var copy = DateComponents() 135 | for component in lhs.members.union(rhs.members) { 136 | var result = 0 137 | // Error workaround where instead of returning nil 138 | // the values return Int.max 139 | if let value = lhs.value(for: component), 140 | value != Int.max, value != Int.min 141 | { result = result + value } 142 | if let value = rhs.value(for: component), 143 | value != Int.max, value != Int.min 144 | { result = result - value } 145 | copy.setValue(result, for: component) 146 | } 147 | return copy 148 | } 149 | } 150 | 151 | extension Date { 152 | public static func - (lhs: Date, rhs: Date) -> DateComponents { 153 | return Date.sharedCalendar.dateComponents(commonComponents, from: rhs, to: lhs) 154 | } 155 | } 156 | 157 | 158 | -------------------------------------------------------------------------------- /Sources/Date+Utilities.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // Thanks: AshFurrow, sstreza, Scott Lawrence, Kevin Ballard, NoOneButMe, Avi`, August Joki, Lily Vulcano, jcromartiej, Blagovest Dachev, Matthias Plappert, Slava Bushtruk, Ali Servet Donmez, Ricardo1980, pip8786, Danny Thuerin, Dennis Madsen, Greg Titus, Jim Morrison, aclark, Marcin Krzyzanowski, dmitrydims, Sebastian Celis, Seyithan Teymur, 4 | 5 | /// Shared static properties 6 | public extension Date { 7 | /// Returns common shared calendar, user's preferred calendar 8 | /// This calendar tracks changes to user’s preferred calendar identifier 9 | /// unlike `current`. 10 | public static var sharedCalendar = Calendar.autoupdatingCurrent 11 | /// Returns the current time 12 | public static var now: Date { return Date() } 13 | } 14 | 15 | 16 | /// Inherent date properties / component retrieval 17 | /// Some of these are entirely pointless but I have included all components 18 | public extension Date { 19 | 20 | /// Returns date's time interval since reference date 21 | public var interval: TimeInterval { return self.timeIntervalSinceReferenceDate } 22 | 23 | 24 | // MARK: YMD 25 | 26 | /// Returns instance's year component 27 | public var year: Int { return Date.sharedCalendar.component(.year, from: self) } 28 | /// Returns instance's month component 29 | public var month: Int { return Date.sharedCalendar.component(.month, from: self) } 30 | /// Returns instance's day component 31 | public var day: Int { return Date.sharedCalendar.component(.day, from: self) } 32 | /// Returns instance's hour component 33 | 34 | 35 | // MARK: HMS 36 | 37 | public var hour: Int { return Date.sharedCalendar.component(.hour, from: self) } 38 | /// Returns instance's minute component 39 | public var minute: Int { return Date.sharedCalendar.component(.minute, from: self) } 40 | /// Returns instance's second component 41 | public var second: Int { return Date.sharedCalendar.component(.second, from: self) } 42 | /// Returns instance's nanosecond component 43 | public var nanosecond: Int { return Date.sharedCalendar.component(.nanosecond, from: self) } 44 | 45 | // MARK: Weeks 46 | 47 | /// Returns instance's weekday component 48 | public var weekday: Int { return Date.sharedCalendar.component(.weekday, from: self) } 49 | /// Returns instance's weekdayOrdinal component 50 | public var weekdayOrdinal: Int { return Date.sharedCalendar.component(.weekdayOrdinal, from: self) } 51 | /// Returns instance's weekOfMonth component 52 | public var weekOfMonth: Int { return Date.sharedCalendar.component(.weekOfMonth, from: self) } 53 | /// Returns instance's weekOfYear component 54 | public var weekOfYear: Int { return Date.sharedCalendar.component(.weekOfYear, from: self) } 55 | /// Returns instance's yearForWeekOfYear component 56 | public var yearForWeekOfYear: Int { return Date.sharedCalendar.component(.yearForWeekOfYear, from: self) } 57 | 58 | // MARK: Other 59 | 60 | /// Returns instance's quarter component 61 | public var quarter: Int { return Date.sharedCalendar.component(.quarter, from: self) } 62 | /// Returns instance's (meaningless) era component 63 | public var era: Int { return Date.sharedCalendar.component(.era, from: self) } 64 | /// Returns instance's (meaningless) calendar component 65 | public var calendar: Int { return Date.sharedCalendar.component(.calendar, from: self) } 66 | /// Returns instance's (meaningless) timeZone component. 67 | public var timeZone: Int { return Date.sharedCalendar.component(.timeZone, from: self) } 68 | } 69 | 70 | // Date characteristics 71 | extension Date { 72 | /// Returns true if date falls before current date 73 | public var isPast: Bool { return self < Date() } 74 | 75 | /// Returns true if date falls after current date 76 | public var isFuture: Bool { return self > Date() } 77 | 78 | /// Returns true if date falls on typical weekend 79 | public var isTypicallyWeekend: Bool { 80 | return Date.sharedCalendar.isDateInWeekend(self) 81 | } 82 | /// Returns true if date falls on typical workday 83 | public var isTypicallyWorkday: Bool { return !self.isTypicallyWeekend } 84 | } 85 | 86 | // Date distances 87 | public extension Date { 88 | /// Returns the time interval between two dates 89 | public static func interval(_ date1: Date, _ date2: Date) -> TimeInterval { 90 | return date2.interval - date1.interval 91 | } 92 | 93 | /// Returns a time interval between the instance and another date 94 | public func interval(to date: Date) -> TimeInterval { 95 | return Date.interval(self, date) 96 | } 97 | 98 | /// Returns the distance between two dates 99 | /// using the user's preferred calendar 100 | /// e.g. 101 | /// ``` 102 | /// let date1 = Date.shortDateFormatter.date(from: "1/1/16")! 103 | /// let date2 = Date.shortDateFormatter.date(from: "3/13/16")! 104 | /// Date.distance(date1, to: date2, component: .day) // 72 105 | /// ``` 106 | /// - Warning: Returns 0 for bad components rather than crashing 107 | public static func distance(_ date1: Date, to date2: Date, component: Calendar.Component) -> Int { 108 | return Date.sharedCalendar.dateComponents([component], from: date1, to: date2)[component] ?? 0 109 | } 110 | 111 | /// Returns the distance between the instance and another date 112 | /// using the user's preferred calendar 113 | /// e.g. 114 | /// ``` 115 | /// let date1 = Date.shortDateFormatter.date(from: "1/1/16")! 116 | /// let date2 = Date.shortDateFormatter.date(from: "3/13/16")! 117 | /// date1.distance(to: date2, component: .day) // 72 118 | /// ``` 119 | /// - Warning: Returns 0 for bad components rather than crashing 120 | public func distance(to date: Date, component: Calendar.Component) -> Int { 121 | return Date.sharedCalendar.dateComponents([component], from: self, to: date)[component] ?? 0 122 | } 123 | 124 | /// Returns the number of days between the instance and a given date. May be negative 125 | public func days(to date: Date) -> Int { return distance(to: date, component: .day) } 126 | /// Returns the number of hours between the instance and a given date. May be negative 127 | public func hours(to date: Date) -> Int { return distance(to: date, component: .hour) } 128 | /// Returns the number of minutes between the instance and a given date. May be negative 129 | public func minutes(to date: Date) -> Int { return distance(to: date, component: .minute) } 130 | /// Returns the number of seconds between the instance and a given date. May be negative 131 | public func seconds(to date: Date) -> Int { return distance(to: date, component: .second) } 132 | 133 | /// Returns a (days, hours, minutes, seconds) tuple representing the 134 | /// time remaining between the instance and a target date. 135 | /// Not for exact use. For example: 136 | /// 137 | /// ``` 138 | /// let test = Date().addingTimeInterval(5.days + 3.hours + 2.minutes + 10.seconds) 139 | /// print(Date().offsets(to: test)) 140 | /// // prints (5, 3, 2, 10 or possibly 9 but rounded up) 141 | /// ``` 142 | /// 143 | /// - Warning: returns 0 for any error when fetching component 144 | public func offsets(to date: Date) -> (days: Int, hours: Int, minutes: Int, seconds: Int) { 145 | let components = Date.sharedCalendar 146 | .dateComponents([.day, .hour, .minute, .second], 147 | from: self, to: date.addingTimeInterval(0.5)) // round up 148 | return ( 149 | days: components[.day] ?? 0, 150 | hours: components[.hour] ?? 0, 151 | minutes: components[.minute] ?? 0, 152 | seconds: components[.second] ?? 0 153 | ) 154 | } 155 | } 156 | 157 | // Utility 158 | public extension Date { 159 | /// Return the nearest hour using a 24 hour clock 160 | public var nearestHour: Int { return (self.offset(.minute, 30)).hour } 161 | 162 | /// Return the nearest minute 163 | public var nearestMinute: Int { return (self.offset(.second, 30)).minute } 164 | } 165 | 166 | // Canonical dates 167 | extension Date { 168 | 169 | /// Returns a date representing midnight at the start of this day 170 | public var startOfDay: Date { 171 | let midnight = DateComponents(year: components.year, month: components.month, day: components.day) 172 | // If offset is not possible, return unmodified date 173 | return Date.sharedCalendar.date(from: midnight) ?? self 174 | } 175 | /// Returns a date representing midnight at the start of this day. 176 | /// Is synonym for `startOfDay`. 177 | public var today: Date { return self.startOfDay } 178 | /// Returns a date representing midnight at the start of tomorrow 179 | public var tomorrow: Date { return self.today.offset(.day, 1) } 180 | /// Returns a date representing midnight at the start of yesterday 181 | public var yesterday: Date { return self.today.offset(.day, -1) } 182 | 183 | /// Returns today's date at the midnight starting the day 184 | public static var today: Date { return Date().startOfDay } 185 | /// Returns tomorrow's date at the midnight starting the day 186 | public static var tomorrow: Date { return Date.today.offset(.day, 1) } 187 | /// Returns yesterday's date at the midnight starting the day 188 | public static var yesterday: Date { return Date.today.offset(.day, -1) } 189 | 190 | /// Returns a date representing a second before midnight at the end of the day 191 | public var endOfDay: Date { return self.tomorrow.startOfDay.offset(.second, -1) } 192 | /// Returns a date representing a second before midnight at the end of today 193 | public static var endOfToday: Date { return Date().endOfDay } 194 | 195 | /// Determines whether two days share the same date 196 | public static func sameDate(_ date1: Date, _ date2: Date) -> Bool { 197 | return Date.sharedCalendar.isDate(date1, inSameDayAs: date2) 198 | } 199 | 200 | /// Returns true if this date is the same date as today for the user's preferred calendar 201 | public var isToday: Bool { return Date.sharedCalendar.isDateInToday(self) } 202 | /// Returns true if this date is the same date as tomorrow for the user's preferred calendar 203 | public var isTomorrow: Bool { return Date.sharedCalendar.isDateInTomorrow(self) } 204 | /// Returns true if this date is the same date as yesterday for the user's preferred calendar 205 | public var isYesterday: Bool { return Date.sharedCalendar.isDateInYesterday(self) } 206 | 207 | /// Returns the start of the instance's week of year for user's preferred calendar 208 | public var startOfWeek: Date { 209 | let components = self.allComponents 210 | let startOfWeekComponents = DateComponents(weekOfYear: components.weekOfYear, yearForWeekOfYear: components.yearForWeekOfYear) 211 | // If offset is not possible, return unmodified date 212 | return Date.sharedCalendar.date(from: startOfWeekComponents) ?? self 213 | } 214 | /// Returns the start of the current week of year for user's preferred calendar 215 | public static var thisWeek: Date { 216 | return Date().startOfWeek 217 | } 218 | 219 | /// Returns the start of next week of year for user's preferred calendar 220 | public var nextWeek: Date { return self.offset(.weekOfYear, 1) } 221 | /// Returns the start of last week of year for user's preferred calendar 222 | public var lastWeek: Date { return self.offset(.weekOfYear, -1) } 223 | /// Returns the start of next week of year for user's preferred calendar relative to date 224 | public static var nextWeek: Date { return Date().offset(.weekOfYear, 1) } 225 | /// Returns the start of last week of year for user's preferred calendar relative to date 226 | public static var lastWeek: Date { return Date().offset(.weekOfYear, -1) } 227 | 228 | /// Returns true if two weeks likely fall within the same week of year 229 | /// in the same year, or very nearly the same year 230 | public static func sameWeek(_ date1: Date, _ date2: Date) -> Bool { 231 | return date1.startOfWeek == date2.startOfWeek 232 | } 233 | 234 | /// Returns true if date likely falls within the current week of year 235 | public var isThisWeek: Bool { return Date.sameWeek(self, Date.thisWeek) } 236 | /// Returns true if date likely falls within the next week of year 237 | public var isNextWeek: Bool { return Date.sameWeek(self, Date.nextWeek) } 238 | /// Returns true if date likely falls within the previous week of year 239 | public var isLastWeek: Bool { return Date.sameWeek(self, Date.lastWeek) } 240 | 241 | /// Returns the start of month for the user's preferred calendar 242 | public static var thisMonth: Date { 243 | let components = Date().components 244 | let themonth = DateComponents(year: components.year, month: components.month) 245 | // If offset is not possible, return unmodified date 246 | return Date.sharedCalendar.date(from: themonth) ?? Date() 247 | } 248 | /// Returns the start of next year for the user's preferred calendar 249 | public static var nextMonth: Date { return thisMonth.offset(.month, 1) } 250 | /// Returns the start of previous year for the user's preferred calendar 251 | public static var lastMonth: Date { return thisMonth.offset(.month, -1) } 252 | 253 | /// Returns true if two dates share the same month component 254 | public static func sameMonth(_ date1: Date, _ date2: Date) -> Bool { 255 | return (date1.allComponents.year == date2.allComponents.year) && 256 | (date1.allComponents.month == date2.allComponents.month) 257 | } 258 | 259 | /// Returns true if date falls within this month for the user's preferred calendar 260 | public var isThisMonth: Bool { return Date.sameMonth(self, Date.thisMonth) } 261 | /// Returns true if date falls within next month for the user's preferred calendar 262 | public var isNextMonth: Bool { return Date.sameMonth(self, Date.nextMonth) } 263 | /// Returns true if date falls within previous month for the user's preferred calendar 264 | public var isLastMonth: Bool { return Date.sameMonth(self, Date.nextMonth) } 265 | 266 | /// Returns the start of year for the user's preferred calendar 267 | public static var thisYear: Date { 268 | let components = Date().components 269 | let theyear = DateComponents(year: components.year) 270 | // If offset is not possible, return unmodified date 271 | return Date.sharedCalendar.date(from: theyear) ?? Date() 272 | } 273 | /// Returns the start of next year for the user's preferred calendar 274 | public static var nextYear: Date { return thisYear.offset(.year, 1) } 275 | /// Returns the start of previous year for the user's preferred calendar 276 | public static var lastYear: Date { return thisYear.offset(.year, -1) } 277 | 278 | /// Returns true if two dates share the same year component 279 | public static func sameYear(_ date1: Date, _ date2: Date) -> Bool { 280 | return date1.allComponents.year == date2.allComponents.year 281 | } 282 | 283 | /// Returns true if date falls within this year for the user's preferred calendar 284 | public var isThisYear: Bool { return Date.sameYear(self, Date.thisYear) } 285 | /// Returns true if date falls within next year for the user's preferred calendar 286 | public var isNextYear: Bool { return Date.sameYear(self, Date.nextYear) } 287 | /// Returns true if date falls within previous year for the user's preferred calendar 288 | public var isLastYear: Bool { return Date.sameYear(self, Date.lastYear) } 289 | } 290 | -------------------------------------------------------------------------------- /Sources/DateComponents+Utilities.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // Acknowlegements in Date+Utilities.swift 4 | 5 | /// Components 6 | public extension Date { 7 | /// Returns set of common date components 8 | public static var commonComponents: Set = [.year, .month, .day, .hour, .minute, .second] 9 | 10 | /// Returns set of exhaustive date components 11 | public static var allComponents: Set = [.era, .year, .month, .day, .hour, .minute, .second, .weekday, .weekdayOrdinal, .quarter, .weekOfMonth, .weekOfYear, .yearForWeekOfYear, .nanosecond, .calendar, .timeZone] 12 | 13 | /// Returns set of MDY date components 14 | public static var dateComponents: Set = [.year, .month, .day] 15 | 16 | /// Returns set of HMS 17 | public static var timeComponents: Set = [.hour, .minute, .second, ] 18 | 19 | /// Returns set of MDYHMS components 20 | public static var dateAndTimeComponents: Set = [.hour, .minute, .second, .year, .month, .day] 21 | } 22 | 23 | /// Components from Dates 24 | public extension Date { 25 | 26 | /// Extracts of MDY components 27 | var dateComponents: DateComponents { 28 | return Date.sharedCalendar.dateComponents([.month, .day, .year], from: self) 29 | } 30 | 31 | /// Extracts HMS components 32 | var timeComponents: DateComponents { 33 | return Date.sharedCalendar.dateComponents([.hour, .minute, .second], from: self) 34 | } 35 | 36 | /// Extracts MDYHMS components 37 | var dateAndTimeComponents: DateComponents { 38 | return Date.sharedCalendar.dateComponents([.month, .day, .year, .hour, .minute, .second], from: self) 39 | } 40 | 41 | /// Extracts common date components for date 42 | public var components: DateComponents { return Date.sharedCalendar.dateComponents(Date.commonComponents, from: self) } 43 | 44 | /// Extracts all date components for date 45 | public var allComponents: DateComponents { return Date.sharedCalendar.dateComponents(Date.allComponents, from: self) } 46 | } 47 | 48 | 49 | /// Alternative offset approach that constructs date components for offset duty 50 | /// I find this more verbose, less readable, less functional but your mileage may vary 51 | extension DateComponents { 52 | /// Returns components populated by n years 53 | public static func years(_ count: Int) -> DateComponents { return DateComponents(year: count) } 54 | /// Returns components populated by n months 55 | public static func months(_ count: Int) -> DateComponents { return DateComponents(month: count) } 56 | /// Returns components populated by n days 57 | public static func days(_ count: Int) -> DateComponents { return DateComponents(day: count) } 58 | /// Returns components populated by n hours 59 | public static func hours(_ count: Int) -> DateComponents { return DateComponents(hour: count) } 60 | /// Returns components populated by n minutes 61 | public static func minutes(_ count: Int) -> DateComponents { return DateComponents(minute: count) } 62 | /// Returns components populated by n seconds 63 | public static func seconds(_ count: Int) -> DateComponents { return DateComponents(second: count) } 64 | /// Returns components populated by n nanoseconds 65 | public static func nanoseconds(_ count: Int) -> DateComponents { return DateComponents(nanosecond: count) } 66 | } 67 | 68 | /// Date and Component Utility 69 | extension Date { 70 | /// Offset a date by n calendar components. Can be functionally chained 71 | /// For example: 72 | /// 73 | /// ``` 74 | /// let afterThreeDays = date.offset(.day, 3) 75 | /// print(Date().offset(.day, 3).offset(.hour, 1).fullString) 76 | /// ``` 77 | /// 78 | /// Not all components or offsets are useful 79 | public func offset(_ component: Calendar.Component, _ count: Int) -> Date { 80 | var newComponent: DateComponents = DateComponents(second: 0) 81 | switch component { 82 | case .era: newComponent = DateComponents(era: count) 83 | case .year: newComponent = DateComponents(year: count) 84 | case .month: newComponent = DateComponents(month: count) 85 | case .day: newComponent = DateComponents(day: count) 86 | case .hour: newComponent = DateComponents(hour: count) 87 | case .minute: newComponent = DateComponents(minute: count) 88 | case .second: newComponent = DateComponents(second: count) 89 | case .weekday: newComponent = DateComponents(weekday: count) 90 | case .weekdayOrdinal: newComponent = DateComponents(weekdayOrdinal: count) 91 | case .quarter: newComponent = DateComponents(quarter: count) 92 | case .weekOfMonth: newComponent = DateComponents(weekOfMonth: count) 93 | case .weekOfYear: newComponent = DateComponents(weekOfYear: count) 94 | case .yearForWeekOfYear: newComponent = DateComponents(yearForWeekOfYear: count) 95 | case .nanosecond: newComponent = DateComponents(nanosecond: count) 96 | // These items complete the component vocabulary but cannot be used in this way 97 | // case .calendar: newComponent = DateComponents(calendar: count) 98 | // case .timeZone: newComponent = DateComponents(timeZone: count) 99 | default: break 100 | } 101 | 102 | // If offset is not possible, return unmodified date 103 | return Date.sharedCalendar.date(byAdding: newComponent, to: self) ?? self 104 | } 105 | } 106 | 107 | /// Subscripting 108 | extension DateComponents { 109 | /// Introduces date component subscripting 110 | /// This does not take into account any built-in errors 111 | /// Where Int.max returned instead of nil 112 | public subscript(component: Calendar.Component) -> Int? { 113 | switch component { 114 | case .era: return era 115 | case .year: return year 116 | case .month: return month 117 | case .day: return day 118 | case .hour: return hour 119 | case .minute: return minute 120 | case .second: return second 121 | case .weekday: return weekday 122 | case .weekdayOrdinal: return weekdayOrdinal 123 | case .quarter: return quarter 124 | case .weekOfMonth: return weekOfMonth 125 | case .weekOfYear: return weekOfYear 126 | case .yearForWeekOfYear: return yearForWeekOfYear 127 | case .nanosecond: return nanosecond 128 | // case .calendar: return self.calendar 129 | // case .timeZone: return self.timeZone 130 | default: return nil 131 | } 132 | } 133 | } 134 | 135 | // For Frederic B 136 | // e.g. 137 | // let dc = DateComponents(ti: 1.weekInteval + 1.dayInterval 138 | // + 3.hourInterval + 5.minuteInterval + 4.secondInterval) 139 | extension DateComponents { 140 | public init(ti: TimeInterval) { 141 | let referenceDate = Date(timeIntervalSinceReferenceDate: 0) 142 | let offsetDate = Date(timeIntervalSinceReferenceDate: ti) 143 | self = Date.sharedCalendar 144 | .dateComponents(Date.commonComponents, 145 | from: referenceDate, 146 | to: offsetDate) 147 | } 148 | } 149 | 150 | // Members 151 | extension DateComponents { 152 | /// Returns the Int-bearing Calendar.Component members 153 | /// that make up the date 154 | public var members: Set { 155 | 156 | // See bug https://bugs.swift.org/browse/SR-2671 157 | // Error workaround where instead of returning nil 158 | // the values return Int.max 159 | func validateMember(_ value: Int?) -> Bool { 160 | guard let value = value, value != Int.max, value != Int.min 161 | else { return false } 162 | return true 163 | } 164 | 165 | var components: Set = [] 166 | if validateMember(era) { components.insert(.era) } 167 | if validateMember(year) { components.insert(.year) } 168 | if validateMember(month) { components.insert(.month) } 169 | if validateMember(day) { components.insert(.day) } 170 | if validateMember(hour) { components.insert(.hour) } 171 | if validateMember(minute) { components.insert(.minute) } 172 | if validateMember(second) { components.insert(.second) } 173 | if validateMember(weekday) { components.insert(.weekday) } 174 | if validateMember(weekdayOrdinal) { components.insert(.weekdayOrdinal) } 175 | if validateMember(quarter) { components.insert(.quarter) } 176 | if validateMember(weekOfMonth) { components.insert(.weekOfMonth) } 177 | if validateMember(weekOfYear) { components.insert(.weekOfYear) } 178 | if validateMember(yearForWeekOfYear) { components.insert(.yearForWeekOfYear) } 179 | if validateMember(nanosecond) { components.insert(.nanosecond) } 180 | // if validate(calendar) { set.insert(.calendar) } 181 | // if validate(timeZone) { set.insert(.timeZone) } 182 | return components 183 | } 184 | } 185 | 186 | /// Standardization 187 | extension DateComponents { 188 | /// Returns copy with zero-valued components removed 189 | public var trimmed: DateComponents { 190 | var copy = DateComponents() 191 | for component in members { 192 | // Error workaround where instead of returning nil 193 | // the values return Int.max 194 | guard let value = value(for: component), 195 | value != Int.max, value != Int.min 196 | else { continue } 197 | if value != 0 { copy.setValue(value, for: component) } 198 | } 199 | return copy 200 | } 201 | 202 | /// Returns a copy with normalized (positive) values 203 | public var normalized: DateComponents { 204 | let referenceDate = Date(timeIntervalSinceReferenceDate: 0) 205 | guard let adjusted = Date.sharedCalendar.date(byAdding: self, to: referenceDate) else { return self } 206 | let copy = NSCalendar.current.dateComponents(Date.commonComponents, from: referenceDate, to: adjusted) 207 | return copy.trimmed 208 | } 209 | 210 | /// Representation of the date components difference as a time interval 211 | public var timeInterval: TimeInterval { 212 | let referenceDate = Date(timeIntervalSinceReferenceDate: 0) 213 | guard let adjusted = Date.sharedCalendar.date(byAdding: self, to: referenceDate) else { return 0 } 214 | return adjusted.timeIntervalSinceReferenceDate 215 | } 216 | } 217 | 218 | /// Component Presentation 219 | extension DateComponents { 220 | /// Component Presentation Styles 221 | public enum PresentationStyle { case standard, relative, approximate } 222 | 223 | /// Returns a string representation of the date components 224 | /// ``` 225 | /// let dc = DateComponents(minute: 7, second: 5) 226 | /// print(dc.description(remaining: true, approximate: true)) // About 7 minutes, 5 seconds remaining 227 | /// print(dc.description(style: .approximate)) // About 7 minutes from now 228 | /// print(dc.description(style: .relative)) // 7 minutes, 5 seconds from now 229 | /// print(dc.description(style: .standard)) // 7 minutes, 5 seconds 230 | /// let units: [DateComponentsFormatter.UnitsStyle] = [.positional, .abbreviated, .short, .full, .spellOut] 231 | /// // 7:05, 7m 5s, 7 min, 5 sec, 7 minutes, 5 seconds, seven minutes, five seconds 232 | /// for unit in units { 233 | /// print(dc.description(units: unit, style: .standard)) 234 | /// } 235 | /// // print(dc.description(units: .brief, style: .standard)) // 10.12 and later 236 | public func description( 237 | units: DateComponentsFormatter.UnitsStyle = .full, 238 | remaining: Bool = false, 239 | approximate: Bool = false, 240 | style: PresentationStyle = .standard 241 | ) -> String { 242 | 243 | let formatter: DateComponentsFormatter = { 244 | $0.calendar = Date.sharedCalendar 245 | $0.unitsStyle = units 246 | $0.includesTimeRemainingPhrase = remaining 247 | $0.includesApproximationPhrase = approximate 248 | return $0 249 | }(DateComponentsFormatter()) 250 | 251 | /// Caution: Use relative presentation only when all 252 | /// component signs are uniform. Use 'normalize' to 253 | /// normalize the component. 254 | if style == .relative { 255 | guard var string = formatter.string(from: self) else { return "\(self)" } 256 | if let newTime = Date.sharedCalendar.date(byAdding: self, to: Date()) { 257 | if newTime.isFuture { string += " from now" } 258 | else if newTime.isPast { string += " ago" } 259 | } 260 | return string 261 | } 262 | 263 | if style == .approximate { 264 | let ti = abs(self.timeInterval) 265 | if ti < 3 { return "just now" } 266 | var string = "About " 267 | if ti < Date.minuteInterval { string += "\(lrint(ti)) seconds" } 268 | else if abs(ti - Date.minuteInterval) <= 3.secondInterval { string += "a minute" } 269 | else if ti < Date.hourInterval { string += "\(lrint(ti / Date.minuteInterval)) minutes" } 270 | else if abs(ti - Date.hourInterval) <= (30.minuteInterval) { string += "an hour" } 271 | else if ti < Date.dayInterval { string += "\(lrint(ti / Date.hourInterval)) hours" } 272 | else if abs(ti - Date.dayInterval) <= (12.hourInterval) { string += "a day" } 273 | else if ti < Date.weekInterval { string += "\(lrint(ti / Date.dayInterval)) days" } 274 | else { string += "\(lrint(ti / (Date.weekInterval))) weeks" } 275 | if let newTime = Date.sharedCalendar.date(byAdding: self, to: Date()) { 276 | if newTime.isFuture { string += " from now" } 277 | else if newTime.isPast { string += " ago" } 278 | } 279 | return string 280 | } 281 | 282 | guard let string = formatter.string(from: self) 283 | else { return "\(self)" } 284 | return string 285 | } 286 | } 287 | 288 | -------------------------------------------------------------------------------- /images/README.md: -------------------------------------------------------------------------------- 1 | Clock image is public domain, http://www.clipartlord.com/wp-content/uploads/2013/09/clock5.png 2 | -------------------------------------------------------------------------------- /images/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erica/SwiftDates/dec194cc544ca519038be1f03b6127809570ffe4/images/clock.png --------------------------------------------------------------------------------