add all api to sort by month
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
/// Time period chains serve as a tightly coupled set of time periods.
|
||||
/// They are always organized by start and end date, and have their own characteristics like
|
||||
/// a StartDate and EndDate that are extrapolated from the time periods within.
|
||||
/// Time period chains do not allow overlaps within their set of time periods.
|
||||
/// This type of group is ideal for modeling schedules like sequential meetings or appointments.
|
||||
open class TimePeriodChain: TimePeriodGroup {
|
||||
|
||||
// MARK: - Chain Existence Manipulation
|
||||
|
||||
/**
|
||||
* Append a TimePeriodProtocol to the periods array and update the Chain's
|
||||
* beginning and end.
|
||||
*
|
||||
* - parameter period: TimePeriodProtocol to add to the collection
|
||||
*/
|
||||
public func append(_ period: TimePeriodProtocol) {
|
||||
let beginning = (periods.count > 0) ? periods.last!.end! : period.start
|
||||
|
||||
let newPeriod = TimePeriod(start: beginning!, duration: period.duration)
|
||||
periods.append(newPeriod)
|
||||
|
||||
//Update updateExtremes
|
||||
if periods.count == 1 {
|
||||
start = period.start
|
||||
end = period.end
|
||||
} else {
|
||||
end = end?.addingTimeInterval(period.duration)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a TimePeriodProtocol array to the periods array and update the Chain's
|
||||
* beginning and end.
|
||||
*
|
||||
* - parameter periodArray: TimePeriodProtocol list to add to the collection
|
||||
*/
|
||||
public func append<G: TimePeriodGroup>(contentsOf group: G) {
|
||||
for period in group.periods {
|
||||
let beginning = (periods.count > 0) ? periods.last!.end! : period.start
|
||||
|
||||
let newPeriod = TimePeriod(start: beginning!, duration: period.duration)
|
||||
periods.append(newPeriod)
|
||||
|
||||
//Update updateExtremes
|
||||
if periods.count == 1 {
|
||||
start = period.start
|
||||
end = period.end
|
||||
} else {
|
||||
end = end?.addingTimeInterval(period.duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert period into periods array at given index.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - period: The period to insert
|
||||
/// - index: Index to insert period at
|
||||
public func insert(_ period: TimePeriodProtocol, at index: Int) {
|
||||
//Check for special zero case which takes the beginning date
|
||||
if index == 0 && period.start != nil && period.end != nil {
|
||||
//Insert new period
|
||||
periods.insert(period, at: index)
|
||||
} else if period.start != nil && period.end != nil {
|
||||
//Insert new period
|
||||
periods.insert(period, at: index)
|
||||
} else {
|
||||
print("All TimePeriods in a TimePeriodChain must contain a defined start and end date")
|
||||
return
|
||||
}
|
||||
|
||||
//Shift all periods after inserted period
|
||||
for i in 0..<periods.count {
|
||||
if i > index && i > 0 {
|
||||
let currentPeriod = TimePeriod(start: period.start, end: period.end)
|
||||
periods[i].start = periods[i - 1].end
|
||||
periods[i].end = periods[i].start!.addingTimeInterval(currentPeriod.duration)
|
||||
}
|
||||
}
|
||||
|
||||
updateExtremes()
|
||||
}
|
||||
|
||||
/// Remove from period array at the given index.
|
||||
///
|
||||
/// - Parameter index: The index in the collection to remove
|
||||
public func remove(at index: Int) {
|
||||
//Retrieve duration of period to be removed
|
||||
let duration = periods[index].duration
|
||||
|
||||
//Remove period
|
||||
periods.remove(at: index)
|
||||
|
||||
//Shift all periods after inserted period
|
||||
for i in index..<periods.count {
|
||||
periods[i].shift(by: -duration)
|
||||
}
|
||||
updateExtremes()
|
||||
}
|
||||
|
||||
/// Remove all periods from period array.
|
||||
public func removeAll() {
|
||||
periods.removeAll()
|
||||
updateExtremes()
|
||||
}
|
||||
|
||||
// MARK: - Chain Content Manipulation
|
||||
|
||||
/// In place, shifts all chain time periods by a given time interval
|
||||
///
|
||||
/// - Parameter duration: The time interval to shift the period by
|
||||
public func shift(by duration: TimeInterval) {
|
||||
for var period in periods {
|
||||
period.shift(by: duration)
|
||||
}
|
||||
start = start?.addingTimeInterval(duration)
|
||||
end = end?.addingTimeInterval(duration)
|
||||
}
|
||||
|
||||
public override func map<T>(_ transform: (TimePeriodProtocol) throws -> T) rethrows -> [T] {
|
||||
return try periods.map(transform)
|
||||
}
|
||||
|
||||
public override func filter(_ isIncluded: (TimePeriodProtocol) throws -> Bool) rethrows -> [TimePeriodProtocol] {
|
||||
return try periods.filter(isIncluded)
|
||||
}
|
||||
|
||||
internal override func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, TimePeriodProtocol) throws -> Result) rethrows -> Result {
|
||||
return try periods.reduce(initialResult, nextPartialResult)
|
||||
}
|
||||
|
||||
/// Removes the last object from the `TimePeriodChain` and returns it
|
||||
public func pop() -> TimePeriodProtocol? {
|
||||
let period = periods.popLast()
|
||||
updateExtremes()
|
||||
|
||||
return period
|
||||
}
|
||||
|
||||
internal func updateExtremes() {
|
||||
start = periods.first?.start
|
||||
end = periods.last?.end
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
/// Sort type
|
||||
///
|
||||
/// - ascending: sort in ascending order
|
||||
/// - descending: sort in descending order
|
||||
public enum SortMode {
|
||||
case ascending
|
||||
case descending
|
||||
}
|
||||
|
||||
/// Sorting type
|
||||
///
|
||||
/// - start: sort by start date
|
||||
/// - end: sort by end date
|
||||
/// - duration: sort by duration
|
||||
/// - custom: sort using custom function
|
||||
public enum SortType {
|
||||
case start(_: SortMode)
|
||||
case end(_: SortMode)
|
||||
case duration(_: SortMode)
|
||||
case custom(_: ((TimePeriodProtocol, TimePeriodProtocol) -> Bool))
|
||||
}
|
||||
|
||||
/// Time period collections serve as loose sets of time periods.
|
||||
/// They are unorganized unless you decide to sort them, and have their own characteristics
|
||||
/// like a `start` and `end` that are extrapolated from the time periods within.
|
||||
/// Time period collections allow overlaps within their set of time periods.
|
||||
open class TimePeriodCollection: TimePeriodGroup {
|
||||
|
||||
// MARK: - Collection Manipulation
|
||||
|
||||
/// Append a TimePeriodProtocol to the periods array and check if the Collection's start and end should change.
|
||||
///
|
||||
/// - Parameter period: TimePeriodProtocol to add to the collection
|
||||
public func append(_ period: TimePeriodProtocol) {
|
||||
periods.append(period)
|
||||
updateExtremes(period: period)
|
||||
}
|
||||
|
||||
/// Append a TimePeriodProtocol array to the periods array and check if the Collection's
|
||||
/// start and end should change.
|
||||
///
|
||||
/// - Parameter periodArray: TimePeriodProtocol list to add to the collection
|
||||
public func append(_ periodArray: [TimePeriodProtocol]) {
|
||||
for period in periodArray {
|
||||
periods.append(period)
|
||||
updateExtremes(period: period)
|
||||
}
|
||||
}
|
||||
|
||||
/// Append a TimePeriodGroup's periods array to the periods array of self and check if the Collection's
|
||||
/// start and end should change.
|
||||
///
|
||||
/// - Parameter newPeriods: TimePeriodGroup to merge periods arrays with
|
||||
public func append<C: TimePeriodGroup>(contentsOf newPeriods: C) {
|
||||
for period in newPeriods as TimePeriodGroup {
|
||||
periods.append(period)
|
||||
updateExtremes(period: period)
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert period into periods array at given index.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - newElement: The period to insert
|
||||
/// - index: Index to insert period at
|
||||
public func insert(_ newElement: TimePeriodProtocol, at index: Int) {
|
||||
periods.insert(newElement, at: index)
|
||||
updateExtremes(period: newElement)
|
||||
}
|
||||
|
||||
/// Remove from period array at the given index.
|
||||
///
|
||||
/// - Parameter at: The index in the collection to remove
|
||||
public func remove(at: Int) {
|
||||
periods.remove(at: at)
|
||||
updateExtremes()
|
||||
}
|
||||
|
||||
/// Remove all periods from period array.
|
||||
public func removeAll() {
|
||||
periods.removeAll()
|
||||
updateExtremes()
|
||||
}
|
||||
|
||||
// MARK: - Sorting
|
||||
|
||||
/// Sort elements in place using given method.
|
||||
///
|
||||
/// - Parameter type: sorting method
|
||||
public func sort(by type: SortType) {
|
||||
switch type {
|
||||
case .duration(let mode): periods.sort(by: sortFuncDuration(mode))
|
||||
case .start(let mode): periods.sort(by: sortFunc(byStart: true, type: mode))
|
||||
case .end(let mode): periods.sort(by: sortFunc(byStart: false, type: mode))
|
||||
case .custom(let f): periods.sort(by: f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a new `TimePeriodCollection` where items are sorted with specified method.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - type: sorting method
|
||||
/// - Returns: collection ordered by given function
|
||||
public func sorted(by type: SortType) -> TimePeriodCollection {
|
||||
var sortedList: [TimePeriodProtocol]!
|
||||
switch type {
|
||||
case .duration(let mode): sortedList = periods.sorted(by: sortFuncDuration(mode))
|
||||
case .start(let mode): sortedList = periods.sorted(by: sortFunc(byStart: true, type: mode))
|
||||
case .end(let mode): sortedList = periods.sorted(by: sortFunc(byStart: false, type: mode))
|
||||
case .custom(let f): sortedList = periods.sorted(by: f)
|
||||
}
|
||||
return TimePeriodCollection(sortedList)
|
||||
}
|
||||
|
||||
// MARK: - Collection Relationship
|
||||
|
||||
/// Returns from the `TimePeriodCollection` a sub-collection of `TimePeriod`s
|
||||
/// whose start and end dates fall completely inside the interval of the given `TimePeriod`.
|
||||
///
|
||||
/// - Parameter period: The period to compare each other period against
|
||||
/// - Returns: Collection of periods inside the given period
|
||||
public func periodsInside(period: TimePeriodProtocol) -> TimePeriodCollection {
|
||||
return TimePeriodCollection(periods.filter({ $0.isInside(period) }))
|
||||
}
|
||||
|
||||
// Returns from the `TimePeriodCollection` a sub-collection of `TimePeriod`s containing the given date.
|
||||
///
|
||||
/// - Parameter date: The date to compare each period to
|
||||
/// - Returns: Collection of periods intersected by the given date
|
||||
public func periodsIntersected(by date: DateInRegion) -> TimePeriodCollection {
|
||||
return TimePeriodCollection(periods.filter({ $0.contains(date: date, interval: .closed) }))
|
||||
}
|
||||
|
||||
/// Returns from the `TimePeriodCollection` a sub-collection of `TimePeriod`s
|
||||
/// containing either the start date or the end date--or both--of the given `TimePeriod`.
|
||||
///
|
||||
/// - Parameter period: The period to compare each other period to
|
||||
/// - Returns: Collection of periods intersected by the given period
|
||||
public func periodsIntersected(by period: TimePeriodProtocol) -> TimePeriodCollection {
|
||||
return TimePeriodCollection(periods.filter({ $0.intersects(with: period) }))
|
||||
}
|
||||
|
||||
/// Returns an instance of DTTimePeriodCollection with all the time periods in the receiver that overlap a given time period.
|
||||
/// Overlap with the given time period does NOT include other time periods that simply touch it.
|
||||
/// (i.e. one's start date is equal to another's end date)
|
||||
///
|
||||
/// - Parameter period: The time period to check against the receiver's time periods.
|
||||
/// - Returns: Collection of periods overlapped by the given period
|
||||
public func periodsOverlappedBy(_ period: TimePeriodProtocol) -> TimePeriodCollection {
|
||||
return TimePeriodCollection(periods.filter({ $0.overlaps(with: period) }))
|
||||
}
|
||||
|
||||
// MARK: - Map
|
||||
|
||||
public func map(_ transform: (TimePeriodProtocol) throws -> TimePeriodProtocol) rethrows -> TimePeriodCollection {
|
||||
var mappedArray = [TimePeriodProtocol]()
|
||||
mappedArray = try periods.map(transform)
|
||||
let mappedCollection = TimePeriodCollection()
|
||||
for period in mappedArray {
|
||||
mappedCollection.periods.append(period)
|
||||
mappedCollection.updateExtremes(period: period)
|
||||
}
|
||||
return mappedCollection
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func sortFuncDuration(_ type: SortMode) -> ((TimePeriodProtocol, TimePeriodProtocol) -> Bool) {
|
||||
switch type {
|
||||
case .ascending: return { $0.duration < $1.duration }
|
||||
case .descending: return { $0.duration > $1.duration }
|
||||
}
|
||||
}
|
||||
|
||||
private func sortFunc(byStart start: Bool = true, type: SortMode) -> ((TimePeriodProtocol, TimePeriodProtocol) -> Bool) {
|
||||
return {
|
||||
let date0 = (start ? $0.start : $0.end)
|
||||
let date1 = (start ? $1.start : $1.end)
|
||||
if date0 == nil && date1 == nil {
|
||||
return false
|
||||
} else if date0 == nil {
|
||||
return true
|
||||
} else if date1 == nil {
|
||||
return false
|
||||
} else {
|
||||
return (type == .ascending ? date1! > date0! : date0! > date1!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateExtremes(period: TimePeriodProtocol) {
|
||||
//Check incoming period against previous start and end date
|
||||
guard count != 1 else {
|
||||
start = period.start
|
||||
end = period.end
|
||||
return
|
||||
}
|
||||
start = nilOrEarlier(date1: start, date2: period.start)
|
||||
end = nilOrLater(date1: end, date2: period.end)
|
||||
}
|
||||
|
||||
private func updateExtremes() {
|
||||
guard periods.count > 0 else {
|
||||
start = nil
|
||||
end = nil
|
||||
return
|
||||
}
|
||||
|
||||
start = periods.first!.start
|
||||
end = periods.first!.end
|
||||
for i in 1..<periods.count {
|
||||
start = nilOrEarlier(date1: start, date2: periods[i].start)
|
||||
end = nilOrEarlier(date1: end, date2: periods[i].end)
|
||||
}
|
||||
}
|
||||
|
||||
private func nilOrEarlier(date1: DateInRegion?, date2: DateInRegion?) -> DateInRegion? {
|
||||
guard date1 != nil && date2 != nil else { return nil }
|
||||
return date1!.earlierDate(date2!)
|
||||
}
|
||||
|
||||
private func nilOrLater(date1: DateInRegion?, date2: DateInRegion?) -> DateInRegion? {
|
||||
guard date1 != nil && date2 != nil else { return nil }
|
||||
return date1!.laterDate(date2!)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
/// Time period groups are the final abstraction of date and time in DateTools.
|
||||
/// Here, time periods are gathered and organized into something useful.
|
||||
/// There are two main types of time period groups, `TimePeriodCollection` and `TimePeriodChain`.
|
||||
open class TimePeriodGroup: Sequence, Equatable {
|
||||
|
||||
/// Array of periods that define the group.
|
||||
internal var periods: [TimePeriodProtocol] = []
|
||||
|
||||
/// The earliest beginning date of a `TimePeriod` in the group.
|
||||
/// `nil` if any `TimePeriod` in group has a nil beginning date (indefinite).
|
||||
public internal(set) var start: DateInRegion?
|
||||
|
||||
/// The latest end date of a `TimePeriod` in the group.
|
||||
/// `nil` if any `TimePeriod` in group has a nil end date (indefinite).
|
||||
public internal(set) var end: DateInRegion?
|
||||
|
||||
/// The total amount of time between the earliest and latest dates stored in the periods array.
|
||||
/// `nil` if any beginning or end date in any contained period is `nil`.
|
||||
public var duration: TimeInterval? {
|
||||
guard let start = start, let end = end else { return nil }
|
||||
return end.timeIntervalSince(start)
|
||||
}
|
||||
|
||||
/// The number of periods in the periods array.
|
||||
public var count: Int {
|
||||
return periods.count
|
||||
}
|
||||
|
||||
// MARK: - Equatable
|
||||
|
||||
public static func == (lhs: TimePeriodGroup, rhs: TimePeriodGroup) -> Bool {
|
||||
return TimePeriodGroup.hasSameElements(array1: lhs.periods, rhs.periods)
|
||||
}
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
public init(_ periods: [TimePeriodProtocol]? = nil) {
|
||||
self.periods = (periods ?? [])
|
||||
}
|
||||
|
||||
// MARK: - Sequence Protocol
|
||||
|
||||
public func makeIterator() -> IndexingIterator<[TimePeriodProtocol]> {
|
||||
return periods.makeIterator()
|
||||
}
|
||||
|
||||
public func map<T>(_ transform: (TimePeriodProtocol) throws -> T) rethrows -> [T] {
|
||||
return try periods.map(transform)
|
||||
}
|
||||
|
||||
public func filter(_ isIncluded: (TimePeriodProtocol) throws -> Bool) rethrows -> [TimePeriodProtocol] {
|
||||
return try periods.filter(isIncluded)
|
||||
}
|
||||
|
||||
public func forEach(_ body: (TimePeriodProtocol) throws -> Void) rethrows {
|
||||
return try periods.forEach(body)
|
||||
}
|
||||
|
||||
public func split(maxSplits: Int, omittingEmptySubsequences: Bool, whereSeparator isSeparator: (TimePeriodProtocol) throws -> Bool) rethrows -> [AnySequence<TimePeriodProtocol>] {
|
||||
return try periods.split(maxSplits: maxSplits, omittingEmptySubsequences: omittingEmptySubsequences, whereSeparator: isSeparator).map(AnySequence.init)
|
||||
}
|
||||
|
||||
subscript(index: Int) -> TimePeriodProtocol {
|
||||
get {
|
||||
return periods[index]
|
||||
}
|
||||
}
|
||||
|
||||
internal func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, TimePeriodProtocol) throws -> Result) rethrows -> Result {
|
||||
return try periods.reduce(initialResult, nextPartialResult)
|
||||
}
|
||||
|
||||
// MARK: - Internal Helper Functions
|
||||
|
||||
internal static func hasSameElements(array1: [TimePeriodProtocol], _ array2: [TimePeriodProtocol]) -> Bool {
|
||||
guard array1.count == array2.count else {
|
||||
return false // No need to sorting if they already have different counts
|
||||
}
|
||||
|
||||
let compArray1: [TimePeriodProtocol] = array1.sorted { (period1: TimePeriodProtocol, period2: TimePeriodProtocol) -> Bool in
|
||||
if period1.start == nil && period2.start == nil {
|
||||
return false
|
||||
} else if period1.start == nil {
|
||||
return true
|
||||
} else if period2.start == nil {
|
||||
return false
|
||||
} else {
|
||||
return period2.start! < period1.start!
|
||||
}
|
||||
}
|
||||
let compArray2: [TimePeriodProtocol] = array2.sorted { (period1: TimePeriodProtocol, period2: TimePeriodProtocol) -> Bool in
|
||||
if period1.start == nil && period2.start == nil {
|
||||
return false
|
||||
} else if period1.start == nil {
|
||||
return true
|
||||
} else if period2.start == nil {
|
||||
return false
|
||||
} else {
|
||||
return period2.start! < period1.start!
|
||||
}
|
||||
}
|
||||
for x in 0..<compArray1.count {
|
||||
if !compArray1[x].equals(compArray2[x]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
/// There may come a need, say when you are making a scheduling app, when
|
||||
/// it might be good to know how two time periods relate to one another
|
||||
/// Are they the same? Is one inside of another?
|
||||
/// All these questions may be asked using the relationship method of TimePeriod.
|
||||
public enum TimePeriodRelation {
|
||||
case after
|
||||
case startTouching
|
||||
case startInside
|
||||
case insideStartTouching
|
||||
case enclosingStartTouching
|
||||
case enclosing
|
||||
case enclosingEndTouching
|
||||
case exactMatch
|
||||
case inside
|
||||
case insideEndTouching
|
||||
case endInside
|
||||
case endTouching
|
||||
case before
|
||||
case none
|
||||
}
|
||||
|
||||
/// Whether the time period is Open or Closed
|
||||
///
|
||||
/// - open: The boundary moment of time is included in calculations.
|
||||
/// - closed: The boundary moment of time represents a boundary value which is excluded in regard to calculations.
|
||||
public enum IntervalType {
|
||||
case open
|
||||
case closed
|
||||
}
|
||||
|
||||
/// When a time periods is lengthened or shortened, it does so anchoring one date
|
||||
/// of the time period and then changing the other one. There is also an option to
|
||||
/// anchor the centerpoint of the time period, changing both the start and end dates.
|
||||
public enum TimePeriodAnchor {
|
||||
case beginning
|
||||
case center
|
||||
case end
|
||||
}
|
||||
199
Sources/App/Libraries/SwiftDate/TimePeriod/TimePeriod.swift
Normal file
199
Sources/App/Libraries/SwiftDate/TimePeriod/TimePeriod.swift
Normal file
@@ -0,0 +1,199 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
/// Time periods are represented by the TimePeriodProtocol protocol.
|
||||
/// Required variables and method impleementations are bound below.
|
||||
/// An inheritable implementation of the TimePeriodProtocol is available through the TimePeriod class.
|
||||
open class TimePeriod: TimePeriodProtocol {
|
||||
|
||||
/// The start date for a TimePeriod representing the starting boundary of the time period
|
||||
public var start: DateInRegion?
|
||||
|
||||
/// The end date for a TimePeriod representing the ending boundary of the time period
|
||||
public var end: DateInRegion?
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
public init() { }
|
||||
|
||||
/// Create a new time period with given date range.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - start: start date
|
||||
/// - end: end date
|
||||
public init(start: DateInRegion?, end: DateInRegion?) {
|
||||
self.start = start
|
||||
self.end = end
|
||||
}
|
||||
|
||||
/// Create a new time period with given start and a length specified in number of seconds.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - start: start of the period
|
||||
/// - duration: duration of the period expressed in seconds
|
||||
public init(start: DateInRegion, duration: TimeInterval) {
|
||||
self.start = start
|
||||
self.end = DateInRegion(start.date.addingTimeInterval(duration), region: start.region)
|
||||
}
|
||||
|
||||
/// Create a new time period which ends at given date and start date is back on time by given interval.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - end: end date
|
||||
/// - duration: duration expressed in seconds (it will be subtracted from start date)
|
||||
public init(end: DateInRegion, duration: TimeInterval) {
|
||||
self.end = end
|
||||
self.start = end.addingTimeInterval(-duration)
|
||||
}
|
||||
|
||||
/// Return a new instance of the TimePeriod that starts on the provided start date and is of the
|
||||
/// size provided.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - start: start of the period
|
||||
/// - duration: length of the period (ie. `2.days` or `14.hours`...)
|
||||
public init(start: DateInRegion, duration: DateComponents) {
|
||||
self.start = start
|
||||
self.end = (start + duration)
|
||||
}
|
||||
|
||||
/// Return a new instance of the TimePeriod that starts at end time minus given duration.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - end: end date
|
||||
/// - duration: duration (it will be subtracted from end date in order to provide the start date)
|
||||
public init(end: DateInRegion, duration: DateComponents) {
|
||||
self.start = (end - duration)
|
||||
self.end = end
|
||||
}
|
||||
|
||||
/// Returns a new instance of DTTimePeriod that represents the largest time period available.
|
||||
/// The start date is in the distant past and the end date is in the distant future.
|
||||
///
|
||||
/// - Returns: a new time period
|
||||
public static func infinity() -> TimePeriod {
|
||||
return TimePeriod(start: DateInRegion.past(), end: DateInRegion.future())
|
||||
}
|
||||
|
||||
// MARK: - Shifted
|
||||
|
||||
/// Shift the `TimePeriod` by a `TimeInterval`
|
||||
///
|
||||
/// - Parameter timeInterval: The time interval to shift the period by
|
||||
/// - Returns: The new, shifted `TimePeriod`
|
||||
public func shifted(by timeInterval: TimeInterval) -> TimePeriod {
|
||||
let timePeriod = TimePeriod()
|
||||
timePeriod.start = start?.addingTimeInterval(timeInterval)
|
||||
timePeriod.end = end?.addingTimeInterval(timeInterval)
|
||||
return timePeriod
|
||||
}
|
||||
|
||||
/// Shift the `TimePeriod` by the specified components value.
|
||||
/// ie. `let shifted = period.shifted(by: 3.days)`
|
||||
///
|
||||
/// - Parameter components: components to shift
|
||||
/// - Returns: new period
|
||||
public func shifted(by components: DateComponents) -> TimePeriod {
|
||||
let timePeriod = TimePeriod()
|
||||
timePeriod.start = (hasStart ? (start! + components) : nil)
|
||||
timePeriod.end = (hasEnd ? (end! + components) : nil)
|
||||
return timePeriod
|
||||
}
|
||||
|
||||
// MARK: - Lengthen / Shorten
|
||||
|
||||
/// Lengthen the `TimePeriod` by a `TimeInterval`
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - timeInterval: The time interval to lengthen the period by
|
||||
/// - anchor: The anchor point from which to make the change
|
||||
/// - Returns: The new, lengthened `TimePeriod`
|
||||
public func lengthened(by timeInterval: TimeInterval, at anchor: TimePeriodAnchor) -> TimePeriod {
|
||||
let timePeriod = TimePeriod()
|
||||
switch anchor {
|
||||
case .beginning:
|
||||
timePeriod.start = start
|
||||
timePeriod.end = end?.addingTimeInterval(timeInterval)
|
||||
case .center:
|
||||
timePeriod.start = start?.addingTimeInterval(-timeInterval)
|
||||
timePeriod.end = end?.addingTimeInterval(timeInterval)
|
||||
case .end:
|
||||
timePeriod.start = start?.addingTimeInterval(-timeInterval)
|
||||
timePeriod.end = end
|
||||
}
|
||||
return timePeriod
|
||||
}
|
||||
|
||||
/// Shorten the `TimePeriod` by a `TimeInterval`
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - timeInterval: The time interval to shorten the period by
|
||||
/// - anchor: The anchor point from which to make the change
|
||||
/// - Returns: The new, shortened `TimePeriod`
|
||||
public func shortened(by timeInterval: TimeInterval, at anchor: TimePeriodAnchor) -> TimePeriod {
|
||||
let timePeriod = TimePeriod()
|
||||
switch anchor {
|
||||
case .beginning:
|
||||
timePeriod.start = start
|
||||
timePeriod.end = end?.addingTimeInterval(-timeInterval)
|
||||
case .center:
|
||||
timePeriod.start = start?.addingTimeInterval(-timeInterval / 2)
|
||||
timePeriod.end = end?.addingTimeInterval(timeInterval / 2)
|
||||
case .end:
|
||||
timePeriod.start = start?.addingTimeInterval(timeInterval)
|
||||
timePeriod.end = end
|
||||
}
|
||||
return timePeriod
|
||||
}
|
||||
|
||||
// MARK: - Operator Overloads
|
||||
|
||||
/// Default anchor = beginning
|
||||
/// Operator overload for lengthening a `TimePeriod` by a `TimeInterval`
|
||||
public static func + (leftAddend: TimePeriod, rightAddend: TimeInterval) -> TimePeriod {
|
||||
return leftAddend.lengthened(by: rightAddend, at: .beginning)
|
||||
}
|
||||
|
||||
/// Default anchor = beginning
|
||||
/// Operator overload for shortening a `TimePeriod` by a `TimeInterval`
|
||||
public static func - (minuend: TimePeriod, subtrahend: TimeInterval) -> TimePeriod {
|
||||
return minuend.shortened(by: subtrahend, at: .beginning)
|
||||
}
|
||||
|
||||
/// Operator overload for checking if a `TimePeriod` is equal to a `TimePeriodProtocol`
|
||||
public static func == (left: TimePeriod, right: TimePeriodProtocol) -> Bool {
|
||||
return left.equals(right)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension TimePeriod {
|
||||
|
||||
/// The start date of the time period
|
||||
var startDate: Date? {
|
||||
return start?.date
|
||||
}
|
||||
|
||||
/// The end date of the time period
|
||||
var endDate: Date? {
|
||||
return end?.date
|
||||
}
|
||||
|
||||
/// Create a new time period with the given start date, end date and region (default is UTC)
|
||||
convenience init(startDate: Date, endDate: Date, region: Region = Region.UTC) {
|
||||
let start = DateInRegion(startDate, region: region)
|
||||
let end = DateInRegion(endDate, region: region)
|
||||
self.init(start: start, end: end)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
//
|
||||
// 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<Calendar.Component>) -> 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))
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user