// // 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 public protocol TimePeriodProtocol { /// The start date for a TimePeriod representing the starting boundary of the time period var start: DateInRegion? { get set } /// The end date for a TimePeriod representing the ending boundary of the time period var end: DateInRegion? { get set } } public extension TimePeriodProtocol { /// Return `true` if time period has both start and end dates var hasFiniteRange: Bool { guard start != nil && end != nil else { return false } return true } /// Return `true` if period has a start date var hasStart: Bool { return (start != nil) } /// Return `true` if period has a end date var hasEnd: Bool { return (end != nil) } /// Check if receiver is equal to given period (both start/end groups are equals) /// /// - Parameter period: period to compare against to. /// - Returns: true if are equals func equals(_ period: TimePeriodProtocol) -> Bool { return (start == period.start && end == period.end) } /// If the given `TimePeriod`'s beginning is before `beginning` and /// if the given 'TimePeriod`'s end is after `end`. /// /// - Parameter period: The time period to compare to self /// - Returns: True if self is inside of the given `TimePeriod` func isInside(_ period: TimePeriodProtocol) -> Bool { guard hasFiniteRange, period.hasFiniteRange else { return false } return (period.start! <= start! && period.end! >= end!) } /// If the given Date is after `beginning` and before `end`. /// /// - Parameters: /// - date: The time period to compare to self /// - interval: Whether the edge of the date is included in the calculation /// - Returns: True if the given `TimePeriod` is inside of self func contains(date: DateInRegion, interval: IntervalType = .closed) -> Bool { guard hasFiniteRange else { return false } switch interval { case .closed: return (start! <= date && end! >= date) case .open: return (start! < date && end! > date) } } /// If the given `TimePeriod`'s beginning is after `beginning` and /// if the given 'TimePeriod`'s after is after `end`. /// /// - Parameter period: The time period to compare to self /// - Returns: True if the given `TimePeriod` is inside of self func contains(_ period: TimePeriodProtocol) -> Bool { guard hasFiniteRange, period.hasFiniteRange else { return false } if period.start! < start! && period.end! > start! { return true // Outside -> Inside } else if period.start! >= start! && period.end! <= end! { return true // Enclosing } else if period.start! < end! && period.end! > end! { return true // Inside -> Out } return false } /// If self and the given `TimePeriod` share any sub-`TimePeriod`. /// /// - Parameter period: The time period to compare to self /// - Returns: True if there is a period of time that is shared by both `TimePeriod`s func overlaps(with period: TimePeriodProtocol) -> Bool { if period.start! < start! && period.end! > start! { return true // Outside -> Inside } else if period.start! >= start! && period.end! <= end! { return true // Enclosing } else if period.start! < end! && period.end! > end! { return true // Inside -> Out } return false } /// If self and the given `TimePeriod` overlap or the period's edges touch. /// /// - Parameter period: The time period to compare to self /// - Returns: True if there is a period of time or moment that is shared by both `TimePeriod`s func intersects(with period: TimePeriodProtocol) -> Bool { let relation = self.relation(to: period) return (relation != .after && relation != .before) } /// If self is before the given `TimePeriod` chronologically. (A gap must exist between the two). /// /// - Parameter period: The time period to compare to self /// - Returns: True if self is after the given `TimePeriod` func isBefore(_ period: TimePeriodProtocol) -> Bool { return (relation(to: period) == .before) } /// If self is after the given `TimePeriod` chronologically. (A gap must exist between the two). /// /// - Parameter period: The time period to compare to self /// - Returns: True if self is after the given `TimePeriod` func isAfter(_ period: TimePeriodProtocol) -> Bool { return (relation(to: period) == .after) } /// The period of time between self and the given `TimePeriod` not contained by either. /// /// - Parameter period: The time period to compare to self /// - Returns: The gap between the periods. Zero if there is no gap. func hasGap(between period: TimePeriodProtocol) -> Bool { return (isBefore(period) || isAfter(period)) } /// The period of time between self and the given `TimePeriod` not contained by either. /// /// - Parameter period: The time period to compare to self /// - Returns: The gap between the periods. Zero if there is no gap. func gap(between period: TimePeriodProtocol) -> TimeInterval { guard hasFiniteRange, period.hasFiniteRange else { return TimeInterval.greatestFiniteMagnitude } if end! < period.start! { return abs(end!.timeIntervalSince(period.start!)) } else if period.end! < start! { return abs(end!.timeIntervalSince(start!)) } return 0 } /// In place, shift the `TimePeriod` by a `TimeInterval` /// /// - Parameter timeInterval: The time interval to shift the period by mutating func shift(by timeInterval: TimeInterval) { start?.addTimeInterval(timeInterval) end?.addTimeInterval(timeInterval) } /// In place, lengthen the `TimePeriod`, anchored at the beginning, end or center /// /// - Parameters: /// - timeInterval: The time interval to lengthen the period by /// - anchor: The anchor point from which to make the change mutating func lengthen(by timeInterval: TimeInterval, at anchor: TimePeriodAnchor) { switch anchor { case .beginning: end?.addTimeInterval(timeInterval) case .end: start?.addTimeInterval(timeInterval) case .center: start = start?.addingTimeInterval(-timeInterval / 2.0) end = end?.addingTimeInterval(timeInterval / 2.0) } } /// In place, shorten the `TimePeriod`, anchored at the beginning, end or center /// /// - Parameters: /// - timeInterval: The time interval to shorten the period by /// - anchor: The anchor point from which to make the change mutating func shorten(by timeInterval: TimeInterval, at anchor: TimePeriodAnchor) { switch anchor { case .beginning: end?.addTimeInterval(-timeInterval) case .end: start?.addTimeInterval(timeInterval) case .center: start?.addTimeInterval(timeInterval / 2.0) end?.addTimeInterval(-timeInterval / 2.0) } } /// The relationship of the self `TimePeriod` to the given `TimePeriod`. /// Relations are stored in Enums.swift. Formal defnitions available in the provided /// links: /// [GitHub](https://github.com/MatthewYork/DateTools#relationships), /// [CodeProject](http://www.codeproject.com/Articles/168662/Time-Period-Library-for-NET) /// /// - Parameter period: The time period to compare to self /// - Returns: The relationship between self and the given time period func relation(to period: TimePeriodProtocol) -> TimePeriodRelation { //Make sure that all start and end points exist for comparison guard hasFiniteRange, period.hasFiniteRange else { return .none } //Make sure time periods are of positive durations guard start! < end! && period.start! < period.end! else { return .none } //Make comparisons if period.start! < start! { return .after } else if period.end! == start! { return .startTouching } else if period.start! < start! && period.end! < end! { return .startInside } else if period.start! == start! && period.end! > end! { return .insideStartTouching } else if period.start! == start! && period.end! < end! { return .enclosingStartTouching } else if period.start! > start! && period.end! < end! { return .enclosing } else if period.start! > start! && period.end! == end! { return .enclosingEndTouching } else if period.start == start! && period.end! == end! { return .exactMatch } else if period.start! < start! && period.end! > end! { return .inside } else if period.start! < start! && period.end! == end! { return .insideEndTouching } else if period.start! < end! && period.end! > end! { return .endInside } else if period.start! == end! && period.end! > end! { return .endTouching } else if period.start! > end! { return .before } return .none } /// Return `true` if period is zero-seconds long or less than specified precision. /// /// - Parameter precision: precision in seconds; by default is 0. /// - Returns: true if start/end has the same value or less than specified precision func isMoment(precision: TimeInterval = 0) -> Bool { guard hasFiniteRange else { return false } return (abs(start!.date.timeIntervalSince1970 - end!.date.timeIntervalSince1970) <= precision) } /// Returns the duration of the receiver expressed with given time unit. /// If time period has not a finite range it returns `nil`. /// /// - Parameter unit: unit of the duration /// - Returns: duration, `nil` if period has not a finite range func durationIn(_ units: Set) -> DateComponents? { guard hasFiniteRange else { return nil } return start!.calendar.dateComponents(units, from: start!.date, to: end!.date) } /// Returns the duration of the receiver expressed with given time unit. /// If time period has not a finite range it returns `nil`. /// /// - Parameter unit: unit of the duration /// - Returns: duration, `nil` if period has not a finite range func durationIn(_ unit: Calendar.Component) -> Int? { guard hasFiniteRange else { return nil } return start!.calendar.dateComponents([unit], from: start!.date, to: end!.date).value(for: unit) } /// The duration of the `TimePeriod` in years. /// Returns the `Int.max` if beginning or end are `nil`. var years: Int { guard let b = start, let e = end else { return Int.max } return b.toUnit(.year, to: e) } /// The duration of the `TimePeriod` in months. /// Returns the `Int.max` if beginning or end are `nil`. var months: Int { guard let b = start, let e = end else { return Int.max } return b.toUnit(.month, to: e) } /// The duration of the `TimePeriod` in weeks. /// Returns the `Int.max` if beginning or end are `nil`. var weeks: Int { guard let b = start, let e = end else { return Int.max } return b.toUnit(.weekOfMonth, to: e) } /// The duration of the `TimePeriod` in days. /// Returns the `Int.max` if beginning or end are `nil`. var days: Int { guard let b = start, let e = end else { return Int.max } return b.toUnit(.day, to: e) } /// The duration of the `TimePeriod` in hours. /// Returns the `Int.max` if beginning or end are `nil`. var hours: Int { guard let b = start, let e = end else { return Int.max } return b.toUnit(.hour, to: e) } /// The duration of the `TimePeriod` in years. /// Returns the `Int.max` if beginning or end are `nil`. var minutes: Int { guard let b = start, let e = end else { return Int.max } return b.toUnit(.minute, to: e) } /// The duration of the `TimePeriod` in seconds. /// Returns the `Int.max` if beginning or end are `nil`. var seconds: Int { guard let b = start, let e = end else { return Int.max } return b.toUnit(.second, to: e) } /// The length of time between the beginning and end dates of the /// `TimePeriod` as a `TimeInterval`. /// If intervals are not nil returns `Double.greatestFiniteMagnitude` var duration: TimeInterval { guard let b = start, let e = end else { return TimeInterval(Double.greatestFiniteMagnitude) } return abs(b.date.timeIntervalSince(e.date)) } }