DateComponents+Extras.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. //
  2. // SwiftDate
  3. // Parse, validate, manipulate, and display dates, time and timezones in Swift
  4. //
  5. // Created by Daniele Margutti
  6. // - Web: https://www.danielemargutti.com
  7. // - Twitter: https://twitter.com/danielemargutti
  8. // - Mail: hello@danielemargutti.com
  9. //
  10. // Copyright © 2019 Daniele Margutti. Licensed under MIT License.
  11. //
  12. import Foundation
  13. // MARK: - Date Components Extensions
  14. public extension Calendar.Component {
  15. /// Return a description of the calendar component in seconds.
  16. /// Note: Values for `era`,`weekday`,`weekdayOrdinal`, `yearForWeekOfYear`, `calendar`, `timezone` are `nil`.
  17. /// For `weekOfYear` it return the same value of `weekOfMonth`.
  18. var timeInterval: Double? {
  19. switch self {
  20. case .era: return nil
  21. case .year: return (Calendar.Component.day.timeInterval! * 365.0)
  22. case .month: return (Calendar.Component.minute.timeInterval! * 43800)
  23. case .day: return 86400
  24. case .hour: return 3600
  25. case .minute: return 60
  26. case .second: return 1
  27. case .quarter: return (Calendar.Component.day.timeInterval! * 91.25)
  28. case .weekOfMonth, .weekOfYear: return (Calendar.Component.day.timeInterval! * 7)
  29. case .nanosecond: return 1e-9
  30. default: return nil
  31. }
  32. }
  33. /// Return the localized identifier of a calendar component
  34. ///
  35. /// - parameter unit: unit
  36. /// - parameter value: value
  37. ///
  38. /// - returns: return the plural or singular form of the time unit used to compose a valid identifier for search a localized
  39. /// string in resource bundle
  40. internal func localizedKey(forValue value: Int) -> String {
  41. let locKey = localizedKey
  42. let absValue = abs(value)
  43. switch absValue {
  44. case 0: // zero difference for this unit
  45. return "0\(locKey)"
  46. case 1: // one unit of difference
  47. return locKey
  48. default: // more than 1 unit of difference
  49. return "\(locKey)\(locKey)"
  50. }
  51. }
  52. internal var localizedKey: String {
  53. switch self {
  54. case .year: return "y"
  55. case .month: return "m"
  56. case .weekOfYear: return "w"
  57. case .day: return "d"
  58. case .hour: return "h"
  59. case .minute: return "M"
  60. case .second: return "s"
  61. default:
  62. return ""
  63. }
  64. }
  65. }
  66. public extension DateComponents {
  67. /// Shortcut for 'all calendar components'.
  68. static var allComponentsSet: Set<Calendar.Component> {
  69. return [.era, .year, .month, .day, .hour, .minute,
  70. .second, .weekday, .weekdayOrdinal, .quarter,
  71. .weekOfMonth, .weekOfYear, .yearForWeekOfYear,
  72. .nanosecond, .calendar, .timeZone]
  73. }
  74. internal static let allComponents: [Calendar.Component] = [.nanosecond, .second, .minute, .hour,
  75. .day, .month, .year, .yearForWeekOfYear,
  76. .weekOfYear, .weekday, .quarter, .weekdayOrdinal,
  77. .weekOfMonth]
  78. /// This function return the absolute amount of seconds described by the components of the receiver.
  79. /// Note: evaluated value maybe not strictly exact because it ignore the context (calendar/date) of
  80. /// the date components. In details:
  81. /// - The following keys are ignored: `era`,`weekday`,`weekdayOrdinal`,
  82. /// `weekOfYear`, `yearForWeekOfYear`, `calendar`, `timezone
  83. ///
  84. /// Some other values dependant from dates are fixed. This is a complete table:
  85. /// - `year` is 365.0 `days`
  86. /// - `month` is 30.4167 `days` (or 43800 minutes)
  87. /// - `quarter` is 91.25 `days`
  88. /// - `weekOfMonth` is 7 `days`
  89. /// - `day` is 86400 `seconds`
  90. /// - `hour` is 3600 `seconds`
  91. /// - `minute` is 60 `seconds`
  92. /// - `nanosecond` is 1e-9 `seconds`
  93. var timeInterval: TimeInterval {
  94. var totalAmount: TimeInterval = 0
  95. DateComponents.allComponents.forEach {
  96. if let multipler = $0.timeInterval, let value = value(for: $0), value != Int(NSDateComponentUndefined) {
  97. totalAmount += (TimeInterval(value) * multipler)
  98. }
  99. }
  100. return totalAmount
  101. }
  102. /// Create a new `DateComponents` instance with builder pattern.
  103. ///
  104. /// - Parameter builder: callback for builder
  105. /// - Returns: new instance
  106. static func create(_ builder: ((inout DateComponents) -> Void)) -> DateComponents {
  107. var components = DateComponents()
  108. builder(&components)
  109. return components
  110. }
  111. /// Return the current date plus the receive's interval
  112. /// The default calendar used is the `SwiftDate.defaultRegion`'s calendar.
  113. var fromNow: Date {
  114. return SwiftDate.defaultRegion.calendar.date(byAdding: (self as DateComponents) as DateComponents, to: Date() as Date)!
  115. }
  116. /// Returns the current date minus the receiver's interval
  117. /// The default calendar used is the `SwiftDate.defaultRegion`'s calendar.
  118. var ago: Date {
  119. return SwiftDate.defaultRegion.calendar.date(byAdding: -self as DateComponents, to: Date())!
  120. }
  121. /// - returns: the date that will occur once the receiver's components pass after the provide date.
  122. func from(_ date: DateRepresentable) -> Date? {
  123. return date.calendar.date(byAdding: self, to: date.date)
  124. }
  125. /// Return `true` if all interval components are zeroes
  126. var isZero: Bool {
  127. for component in DateComponents.allComponents {
  128. if let value = value(for: component), value != 0 {
  129. return false
  130. }
  131. }
  132. return true
  133. }
  134. /// Transform a `DateComponents` instance to a dictionary where key is the `Calendar.Component` and value is the
  135. /// value associated.
  136. ///
  137. /// - returns: a new `[Calendar.Component : Int]` dict representing source `DateComponents` instance
  138. internal func toDict() -> [Calendar.Component: Int] {
  139. var list: [Calendar.Component: Int] = [:]
  140. DateComponents.allComponents.forEach { component in
  141. let value = self.value(for: component)
  142. if value != nil && value != Int(NSDateComponentUndefined) {
  143. list[component] = value!
  144. }
  145. }
  146. return list
  147. }
  148. /// Alter date components specified into passed dictionary.
  149. ///
  150. /// - Parameter components: components dictionary with their values.
  151. internal mutating func alterComponents(_ components: [Calendar.Component: Int?]) {
  152. components.forEach {
  153. if let v = $0.value {
  154. setValue(v, for: $0.key)
  155. }
  156. }
  157. }
  158. /// Adds two NSDateComponents and returns their combined individual components.
  159. static func + (lhs: DateComponents, rhs: DateComponents) -> DateComponents {
  160. return combine(lhs, rhs: rhs, transform: +)
  161. }
  162. /// Subtracts two NSDateComponents and returns the relative difference between them.
  163. static func - (lhs: DateComponents, rhs: DateComponents) -> DateComponents {
  164. return lhs + (-rhs)
  165. }
  166. /// Applies the `transform` to the two `T` provided, defaulting either of them if it's
  167. /// `nil`
  168. internal static func bimap<T>(_ a: T?, _ b: T?, default: T, _ transform: (T, T) -> T) -> T? {
  169. if a == nil && b == nil { return nil }
  170. return transform(a ?? `default`, b ?? `default`)
  171. }
  172. /// - returns: a new `NSDateComponents` that represents the negative of all values within the
  173. /// components that are not `NSDateComponentUndefined`.
  174. static prefix func - (rhs: DateComponents) -> DateComponents {
  175. var components = DateComponents()
  176. components.era = rhs.era.map(-)
  177. components.year = rhs.year.map(-)
  178. components.month = rhs.month.map(-)
  179. components.day = rhs.day.map(-)
  180. components.hour = rhs.hour.map(-)
  181. components.minute = rhs.minute.map(-)
  182. components.second = rhs.second.map(-)
  183. components.nanosecond = rhs.nanosecond.map(-)
  184. components.weekday = rhs.weekday.map(-)
  185. components.weekdayOrdinal = rhs.weekdayOrdinal.map(-)
  186. components.quarter = rhs.quarter.map(-)
  187. components.weekOfMonth = rhs.weekOfMonth.map(-)
  188. components.weekOfYear = rhs.weekOfYear.map(-)
  189. components.yearForWeekOfYear = rhs.yearForWeekOfYear.map(-)
  190. return components
  191. }
  192. /// Combines two date components using the provided `transform` on all
  193. /// values within the components that are not `NSDateComponentUndefined`.
  194. private static func combine(_ lhs: DateComponents, rhs: DateComponents, transform: (Int, Int) -> Int) -> DateComponents {
  195. var components = DateComponents()
  196. components.era = bimap(lhs.era, rhs.era, default: 0, transform)
  197. components.year = bimap(lhs.year, rhs.year, default: 0, transform)
  198. components.month = bimap(lhs.month, rhs.month, default: 0, transform)
  199. components.day = bimap(lhs.day, rhs.day, default: 0, transform)
  200. components.hour = bimap(lhs.hour, rhs.hour, default: 0, transform)
  201. components.minute = bimap(lhs.minute, rhs.minute, default: 0, transform)
  202. components.second = bimap(lhs.second, rhs.second, default: 0, transform)
  203. components.nanosecond = bimap(lhs.nanosecond, rhs.nanosecond, default: 0, transform)
  204. components.weekday = bimap(lhs.weekday, rhs.weekday, default: 0, transform)
  205. components.weekdayOrdinal = bimap(lhs.weekdayOrdinal, rhs.weekdayOrdinal, default: 0, transform)
  206. components.quarter = bimap(lhs.quarter, rhs.quarter, default: 0, transform)
  207. components.weekOfMonth = bimap(lhs.weekOfMonth, rhs.weekOfMonth, default: 0, transform)
  208. components.weekOfYear = bimap(lhs.weekOfYear, rhs.weekOfYear, default: 0, transform)
  209. components.yearForWeekOfYear = bimap(lhs.yearForWeekOfYear, rhs.yearForWeekOfYear, default: 0, transform)
  210. return components
  211. }
  212. /// Subscription support for `DateComponents` instances.
  213. /// ie. `cmps[.day] = 5`
  214. ///
  215. /// Note: This does not take into account any built-in errors, `Int.max` returned instead of `nil`.
  216. ///
  217. /// - Parameter component: component to get
  218. subscript(component: Calendar.Component) -> Int? {
  219. switch component {
  220. case .era: return era
  221. case .year: return year
  222. case .month: return month
  223. case .day: return day
  224. case .hour: return hour
  225. case .minute: return minute
  226. case .second: return second
  227. case .weekday: return weekday
  228. case .weekdayOrdinal: return weekdayOrdinal
  229. case .quarter: return quarter
  230. case .weekOfMonth: return weekOfMonth
  231. case .weekOfYear: return weekOfYear
  232. case .yearForWeekOfYear: return yearForWeekOfYear
  233. case .nanosecond: return nanosecond
  234. default: return nil // `calendar` and `timezone` are ignored in this context
  235. }
  236. }
  237. /// Express a `DateComponents` instance in another time unit you choose.
  238. ///
  239. /// - parameter component: time component
  240. /// - parameter calendar: context calendar to use
  241. ///
  242. /// - returns: the value of interval expressed in selected `Calendar.Component`
  243. func `in`(_ component: Calendar.Component, of calendar: CalendarConvertible? = nil) -> Int? {
  244. let cal = (calendar?.toCalendar() ?? SwiftDate.defaultRegion.calendar)
  245. let dateFrom = Date()
  246. let dateTo = (dateFrom + self)
  247. let components: Set<Calendar.Component> = [component]
  248. let value = cal.dateComponents(components, from: dateFrom, to: dateTo).value(for: component)
  249. return value
  250. }
  251. /// Express a `DateComponents` instance in a set of time units you choose.
  252. ///
  253. /// - Parameters:
  254. /// - component: time component
  255. /// - calendar: context calendar to use
  256. /// - Returns: a dictionary of extract values.
  257. func `in`(_ components: Set<Calendar.Component>, of calendar: CalendarConvertible? = nil) -> [Calendar.Component: Int] {
  258. let cal = (calendar?.toCalendar() ?? SwiftDate.defaultRegion.calendar)
  259. let dateFrom = Date()
  260. let dateTo = (dateFrom + self)
  261. let extractedCmps = cal.dateComponents(components, from: dateFrom, to: dateTo)
  262. return extractedCmps.toDict()
  263. }
  264. }