// // SwiftDate // Parse, validate, manipulate, and display dates, time and timezones in Swift // // Created by Daniele Margutti // - Web: https://www.danielemargutti.com // - Twitter: https://twitter.com/danielemargutti // - Mail: hello@danielemargutti.com // // Copyright © 2019 Daniele Margutti. Licensed under MIT License. // import Foundation // MARK: - Date Components Extensions public extension Calendar.Component { /// Return a description of the calendar component in seconds. /// Note: Values for `era`,`weekday`,`weekdayOrdinal`, `yearForWeekOfYear`, `calendar`, `timezone` are `nil`. /// For `weekOfYear` it return the same value of `weekOfMonth`. var timeInterval: Double? { switch self { case .era: return nil case .year: return (Calendar.Component.day.timeInterval! * 365.0) case .month: return (Calendar.Component.minute.timeInterval! * 43800) case .day: return 86400 case .hour: return 3600 case .minute: return 60 case .second: return 1 case .quarter: return (Calendar.Component.day.timeInterval! * 91.25) case .weekOfMonth, .weekOfYear: return (Calendar.Component.day.timeInterval! * 7) case .nanosecond: return 1e-9 default: return nil } } /// Return the localized identifier of a calendar component /// /// - parameter unit: unit /// - parameter value: value /// /// - returns: return the plural or singular form of the time unit used to compose a valid identifier for search a localized /// string in resource bundle internal func localizedKey(forValue value: Int) -> String { let locKey = localizedKey let absValue = abs(value) switch absValue { case 0: // zero difference for this unit return "0\(locKey)" case 1: // one unit of difference return locKey default: // more than 1 unit of difference return "\(locKey)\(locKey)" } } internal var localizedKey: String { switch self { case .year: return "y" case .month: return "m" case .weekOfYear: return "w" case .day: return "d" case .hour: return "h" case .minute: return "M" case .second: return "s" default: return "" } } } public extension DateComponents { /// Shortcut for 'all calendar components'. static var allComponentsSet: Set { return [.era, .year, .month, .day, .hour, .minute, .second, .weekday, .weekdayOrdinal, .quarter, .weekOfMonth, .weekOfYear, .yearForWeekOfYear, .nanosecond, .calendar, .timeZone] } internal static let allComponents: [Calendar.Component] = [.nanosecond, .second, .minute, .hour, .day, .month, .year, .yearForWeekOfYear, .weekOfYear, .weekday, .quarter, .weekdayOrdinal, .weekOfMonth] /// This function return the absolute amount of seconds described by the components of the receiver. /// Note: evaluated value maybe not strictly exact because it ignore the context (calendar/date) of /// the date components. In details: /// - The following keys are ignored: `era`,`weekday`,`weekdayOrdinal`, /// `weekOfYear`, `yearForWeekOfYear`, `calendar`, `timezone /// /// Some other values dependant from dates are fixed. This is a complete table: /// - `year` is 365.0 `days` /// - `month` is 30.4167 `days` (or 43800 minutes) /// - `quarter` is 91.25 `days` /// - `weekOfMonth` is 7 `days` /// - `day` is 86400 `seconds` /// - `hour` is 3600 `seconds` /// - `minute` is 60 `seconds` /// - `nanosecond` is 1e-9 `seconds` var timeInterval: TimeInterval { var totalAmount: TimeInterval = 0 DateComponents.allComponents.forEach { if let multipler = $0.timeInterval, let value = value(for: $0), value != Int(NSDateComponentUndefined) { totalAmount += (TimeInterval(value) * multipler) } } return totalAmount } /// Create a new `DateComponents` instance with builder pattern. /// /// - Parameter builder: callback for builder /// - Returns: new instance static func create(_ builder: ((inout DateComponents) -> Void)) -> DateComponents { var components = DateComponents() builder(&components) return components } /// Return the current date plus the receive's interval /// The default calendar used is the `SwiftDate.defaultRegion`'s calendar. var fromNow: Date { return SwiftDate.defaultRegion.calendar.date(byAdding: (self as DateComponents) as DateComponents, to: Date() as Date)! } /// Returns the current date minus the receiver's interval /// The default calendar used is the `SwiftDate.defaultRegion`'s calendar. var ago: Date { return SwiftDate.defaultRegion.calendar.date(byAdding: -self as DateComponents, to: Date())! } /// - returns: the date that will occur once the receiver's components pass after the provide date. func from(_ date: DateRepresentable) -> Date? { return date.calendar.date(byAdding: self, to: date.date) } /// Return `true` if all interval components are zeroes var isZero: Bool { for component in DateComponents.allComponents { if let value = value(for: component), value != 0 { return false } } return true } /// Transform a `DateComponents` instance to a dictionary where key is the `Calendar.Component` and value is the /// value associated. /// /// - returns: a new `[Calendar.Component : Int]` dict representing source `DateComponents` instance internal func toDict() -> [Calendar.Component: Int] { var list: [Calendar.Component: Int] = [:] DateComponents.allComponents.forEach { component in let value = self.value(for: component) if value != nil && value != Int(NSDateComponentUndefined) { list[component] = value! } } return list } /// Alter date components specified into passed dictionary. /// /// - Parameter components: components dictionary with their values. internal mutating func alterComponents(_ components: [Calendar.Component: Int?]) { components.forEach { if let v = $0.value { setValue(v, for: $0.key) } } } /// Adds two NSDateComponents and returns their combined individual components. static func + (lhs: DateComponents, rhs: DateComponents) -> DateComponents { return combine(lhs, rhs: rhs, transform: +) } /// Subtracts two NSDateComponents and returns the relative difference between them. static func - (lhs: DateComponents, rhs: DateComponents) -> DateComponents { return lhs + (-rhs) } /// Applies the `transform` to the two `T` provided, defaulting either of them if it's /// `nil` internal static func bimap(_ a: T?, _ b: T?, default: T, _ transform: (T, T) -> T) -> T? { if a == nil && b == nil { return nil } return transform(a ?? `default`, b ?? `default`) } /// - returns: a new `NSDateComponents` that represents the negative of all values within the /// components that are not `NSDateComponentUndefined`. static prefix func - (rhs: DateComponents) -> DateComponents { var components = DateComponents() components.era = rhs.era.map(-) components.year = rhs.year.map(-) components.month = rhs.month.map(-) components.day = rhs.day.map(-) components.hour = rhs.hour.map(-) components.minute = rhs.minute.map(-) components.second = rhs.second.map(-) components.nanosecond = rhs.nanosecond.map(-) components.weekday = rhs.weekday.map(-) components.weekdayOrdinal = rhs.weekdayOrdinal.map(-) components.quarter = rhs.quarter.map(-) components.weekOfMonth = rhs.weekOfMonth.map(-) components.weekOfYear = rhs.weekOfYear.map(-) components.yearForWeekOfYear = rhs.yearForWeekOfYear.map(-) return components } /// Combines two date components using the provided `transform` on all /// values within the components that are not `NSDateComponentUndefined`. private static func combine(_ lhs: DateComponents, rhs: DateComponents, transform: (Int, Int) -> Int) -> DateComponents { var components = DateComponents() components.era = bimap(lhs.era, rhs.era, default: 0, transform) components.year = bimap(lhs.year, rhs.year, default: 0, transform) components.month = bimap(lhs.month, rhs.month, default: 0, transform) components.day = bimap(lhs.day, rhs.day, default: 0, transform) components.hour = bimap(lhs.hour, rhs.hour, default: 0, transform) components.minute = bimap(lhs.minute, rhs.minute, default: 0, transform) components.second = bimap(lhs.second, rhs.second, default: 0, transform) components.nanosecond = bimap(lhs.nanosecond, rhs.nanosecond, default: 0, transform) components.weekday = bimap(lhs.weekday, rhs.weekday, default: 0, transform) components.weekdayOrdinal = bimap(lhs.weekdayOrdinal, rhs.weekdayOrdinal, default: 0, transform) components.quarter = bimap(lhs.quarter, rhs.quarter, default: 0, transform) components.weekOfMonth = bimap(lhs.weekOfMonth, rhs.weekOfMonth, default: 0, transform) components.weekOfYear = bimap(lhs.weekOfYear, rhs.weekOfYear, default: 0, transform) components.yearForWeekOfYear = bimap(lhs.yearForWeekOfYear, rhs.yearForWeekOfYear, default: 0, transform) return components } /// Subscription support for `DateComponents` instances. /// ie. `cmps[.day] = 5` /// /// Note: This does not take into account any built-in errors, `Int.max` returned instead of `nil`. /// /// - Parameter component: component to get subscript(component: Calendar.Component) -> Int? { switch component { case .era: return era case .year: return year case .month: return month case .day: return day case .hour: return hour case .minute: return minute case .second: return second case .weekday: return weekday case .weekdayOrdinal: return weekdayOrdinal case .quarter: return quarter case .weekOfMonth: return weekOfMonth case .weekOfYear: return weekOfYear case .yearForWeekOfYear: return yearForWeekOfYear case .nanosecond: return nanosecond default: return nil // `calendar` and `timezone` are ignored in this context } } /// Express a `DateComponents` instance in another time unit you choose. /// /// - parameter component: time component /// - parameter calendar: context calendar to use /// /// - returns: the value of interval expressed in selected `Calendar.Component` func `in`(_ component: Calendar.Component, of calendar: CalendarConvertible? = nil) -> Int? { let cal = (calendar?.toCalendar() ?? SwiftDate.defaultRegion.calendar) let dateFrom = Date() let dateTo = (dateFrom + self) let components: Set = [component] let value = cal.dateComponents(components, from: dateFrom, to: dateTo).value(for: component) return value } /// Express a `DateComponents` instance in a set of time units you choose. /// /// - Parameters: /// - component: time component /// - calendar: context calendar to use /// - Returns: a dictionary of extract values. func `in`(_ components: Set, of calendar: CalendarConvertible? = nil) -> [Calendar.Component: Int] { let cal = (calendar?.toCalendar() ?? SwiftDate.defaultRegion.calendar) let dateFrom = Date() let dateTo = (dateFrom + self) let extractedCmps = cal.dateComponents(components, from: dateFrom, to: dateTo) return extractedCmps.toDict() } }