287 lines
9.4 KiB
Swift
287 lines
9.4 KiB
Swift
//
|
|
// 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 extension DateFormatter {
|
|
|
|
/// Return the local thread shared formatter initialized with the configuration of the region passed.
|
|
///
|
|
/// - Parameters:
|
|
/// - region: region used to pre-configure the cell.
|
|
/// - format: optional format used to set the `dateFormat` property.
|
|
/// - Returns: date formatter instance
|
|
static func sharedFormatter(forRegion region: Region?, format: String? = nil) -> DateFormatter {
|
|
let name = "SwiftDate_\(NSStringFromClass(DateFormatter.self))"
|
|
let formatter: DateFormatter = threadSharedObject(key: name, create: { return DateFormatter() })
|
|
if let region = region {
|
|
formatter.timeZone = region.timeZone
|
|
formatter.calendar = region.calendar
|
|
formatter.locale = region.locale
|
|
}
|
|
formatter.dateFormat = (format ?? DateFormats.iso8601)
|
|
return formatter
|
|
}
|
|
|
|
/// Returned number formatter instance shared along calling thread to format ordinal numbers.
|
|
///
|
|
/// - Parameter locale: locale to set
|
|
/// - Returns: number formatter instance
|
|
@available(iOS 9.0, macOS 10.11, *)
|
|
static func sharedOrdinalNumberFormatter(locale: LocaleConvertible) -> NumberFormatter {
|
|
var formatter: NumberFormatter?
|
|
let name = "SwiftDate_\(NSStringFromClass(NumberFormatter.self))"
|
|
formatter = threadSharedObject(key: name, create: { return NumberFormatter() })
|
|
formatter!.numberStyle = .ordinal
|
|
formatter!.locale = locale.toLocale()
|
|
return formatter!
|
|
}
|
|
|
|
}
|
|
|
|
/// This function create (if necessary) and return a thread singleton instance of the
|
|
/// object you want.
|
|
///
|
|
/// - Parameters:
|
|
/// - key: identifier of the object.
|
|
/// - create: create routine used the first time you are about to create the object in thread.
|
|
/// - Returns: instance of the object for caller's thread.
|
|
internal func threadSharedObject<T: AnyObject>(key: String, create: () -> T) -> T {
|
|
if let cachedObj = Thread.current.threadDictionary[key] as? T {
|
|
return cachedObj
|
|
} else {
|
|
let newObject = create()
|
|
Thread.current.threadDictionary[key] = newObject
|
|
return newObject
|
|
}
|
|
}
|
|
|
|
/// Style used to format month, weekday, quarter symbols.
|
|
/// Stand-alone properties are for use in places like calendar headers.
|
|
/// Non-stand-alone properties are for use in context (for example, “Saturday, November 12th”).
|
|
///
|
|
/// - `default`: Default formatter (ie. `4th quarter` for quarter, `April` for months and `Wednesday` for weekdays)
|
|
/// - defaultStandalone: See `default`; See `short`; stand-alone properties are for use in places like calendar headers.
|
|
/// - short: Short symbols (ie. `Jun` for months, `Fri` for weekdays, `Q1` for quarters).
|
|
/// - veryShort: Very short symbols (ie. `J` for months, `F` for weekdays, for quarter it just return `short` variant).
|
|
/// - standaloneShort: See `short`; stand-alone properties are for use in places like calendar headers.
|
|
/// - standaloneVeryShort: See `veryShort`; stand-alone properties are for use in places like calendar headers.
|
|
public enum SymbolFormatStyle {
|
|
case `default`
|
|
case defaultStandalone
|
|
case short
|
|
case veryShort
|
|
case standaloneShort
|
|
case standaloneVeryShort
|
|
}
|
|
|
|
/// Encapsulate the logic to use date format strings
|
|
public struct DateFormats {
|
|
|
|
/// This is the built-in list of all supported formats for auto-parsing of a string to a date.
|
|
internal static let builtInAutoFormat: [String] = [
|
|
DateFormats.iso8601,
|
|
"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'",
|
|
"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'",
|
|
"yyyy-MM-dd'T'HH:mm:ss.SSSZ",
|
|
"yyyy-MM-dd HH:mm:ss",
|
|
"yyyy-MM-dd HH:mm",
|
|
"yyyy-MM-dd",
|
|
"h:mm:ss A",
|
|
"h:mm A",
|
|
"MM/dd/yyyy",
|
|
"MMMM d, yyyy",
|
|
"MMMM d, yyyy LT",
|
|
"dddd, MMMM D, yyyy LT",
|
|
"yyyyyy-MM-dd",
|
|
"yyyy-MM-dd",
|
|
"yyyy-'W'ww-E",
|
|
"GGGG-'['W']'ww-E",
|
|
"yyyy-'W'ww",
|
|
"GGGG-'['W']'ww",
|
|
"yyyy'W'ww",
|
|
"yyyy-ddd",
|
|
"HH:mm:ss.SSSS",
|
|
"HH:mm:ss",
|
|
"HH:mm",
|
|
"HH"
|
|
]
|
|
|
|
/// This is the ordered list of all formats SwiftDate can use in order to attempt parsing a passaed
|
|
/// date expressed as string. Evaluation is made in order; you can add or remove new formats as you wish.
|
|
/// In order to reset the list call `resetAutoFormats()` function.
|
|
public static var autoFormats: [String] = DateFormats.builtInAutoFormat
|
|
|
|
/// Default ISO8601 format string
|
|
public static let iso8601: String = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
|
|
|
|
/// Extended format
|
|
public static let extended: String = "eee dd-MMM-yyyy GG HH:mm:ss.SSS zzz"
|
|
|
|
/// The Alternative RSS formatted date "d MMM yyyy HH:mm:ss ZZZ" i.e. "09 Sep 2011 15:26:08 +0200"
|
|
public static let altRSS: String = "d MMM yyyy HH:mm:ss ZZZ"
|
|
|
|
/// The RSS formatted date "EEE, d MMM yyyy HH:mm:ss ZZZ" i.e. "Fri, 09 Sep 2011 15:26:08 +0200"
|
|
public static let rss: String = "EEE, d MMM yyyy HH:mm:ss ZZZ"
|
|
|
|
/// The http header formatted date "EEE, dd MMM yyyy HH:mm:ss zzz" i.e. "Tue, 15 Nov 1994 12:45:26 GMT"
|
|
public static let httpHeader: String = "EEE, dd MMM yyyy HH:mm:ss zzz"
|
|
|
|
/// A generic standard format date i.e. "EEE MMM dd HH:mm:ss Z yyyy"
|
|
public static let standard: String = "EEE MMM dd HH:mm:ss Z yyyy"
|
|
|
|
/// SQL date format
|
|
public static let sql: String = "yyyy-MM-dd'T'HH:mm:ss.SSSX"
|
|
|
|
/// Reset the list of auto formats to the initial settings.
|
|
public static func resetAutoFormats() {
|
|
autoFormats = DateFormats.builtInAutoFormat
|
|
}
|
|
|
|
/// Parse a new string optionally passing the format in which is encoded. If no format is passed
|
|
/// an attempt is made by cycling all the formats set in `autoFormats` property.
|
|
///
|
|
/// - Parameters:
|
|
/// - string: date expressed as string.
|
|
/// - suggestedFormat: optional format of the date expressed by the string (set it if you can in order to optimize the parse task).
|
|
/// - region: region in which the date is expressed.
|
|
/// - Returns: parsed absolute `Date`, `nil` if parse fails.
|
|
public static func parse(string: String, format: String?, region: Region) -> Date? {
|
|
let formats = (format != nil ? [format!] : DateFormats.autoFormats)
|
|
return DateFormats.parse(string: string, formats: formats, region: region)
|
|
}
|
|
|
|
public static func parse(string: String, formats: [String], region: Region) -> Date? {
|
|
let formatter = DateFormatter.sharedFormatter(forRegion: region)
|
|
|
|
var parsedDate: Date?
|
|
for format in formats {
|
|
formatter.dateFormat = format
|
|
formatter.locale = region.locale
|
|
if let date = formatter.date(from: string) {
|
|
parsedDate = date
|
|
break
|
|
}
|
|
}
|
|
return parsedDate
|
|
}
|
|
}
|
|
|
|
// MARK: - Calendar Extension
|
|
|
|
public extension Calendar.Component {
|
|
|
|
internal static func toSet(_ src: [Calendar.Component]) -> Set<Calendar.Component> {
|
|
var l: Set<Calendar.Component> = []
|
|
src.forEach { l.insert($0) }
|
|
return l
|
|
}
|
|
|
|
internal var nsCalendarUnit: NSCalendar.Unit {
|
|
switch self {
|
|
case .era: return NSCalendar.Unit.era
|
|
case .year: return NSCalendar.Unit.year
|
|
case .month: return NSCalendar.Unit.month
|
|
case .day: return NSCalendar.Unit.day
|
|
case .hour: return NSCalendar.Unit.hour
|
|
case .minute: return NSCalendar.Unit.minute
|
|
case .second: return NSCalendar.Unit.second
|
|
case .weekday: return NSCalendar.Unit.weekday
|
|
case .weekdayOrdinal: return NSCalendar.Unit.weekdayOrdinal
|
|
case .quarter: return NSCalendar.Unit.quarter
|
|
case .weekOfMonth: return NSCalendar.Unit.weekOfMonth
|
|
case .weekOfYear: return NSCalendar.Unit.weekOfYear
|
|
case .yearForWeekOfYear: return NSCalendar.Unit.yearForWeekOfYear
|
|
case .nanosecond: return NSCalendar.Unit.nanosecond
|
|
case .calendar: return NSCalendar.Unit.calendar
|
|
case .timeZone: return NSCalendar.Unit.timeZone
|
|
@unknown default:
|
|
fatalError("Unsupported type \(self)")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Rounding mode for dates.
|
|
/// Round off/up (ceil) or down (floor) target date.
|
|
public enum RoundDateMode {
|
|
case to5Mins
|
|
case to10Mins
|
|
case to30Mins
|
|
case toMins(_: Int)
|
|
case toCeil5Mins
|
|
case toCeil10Mins
|
|
case toCeil30Mins
|
|
case toCeilMins(_: Int)
|
|
case toFloor5Mins
|
|
case toFloor10Mins
|
|
case toFloor30Mins
|
|
case toFloorMins(_: Int)
|
|
}
|
|
|
|
/// Related type enum to get derivated date from a receiver date.
|
|
public enum DateRelatedType {
|
|
case startOfDay
|
|
case endOfDay
|
|
case startOfWeek
|
|
case endOfWeek
|
|
case startOfMonth
|
|
case endOfMonth
|
|
case tomorrow
|
|
case tomorrowAtStart
|
|
case yesterday
|
|
case yesterdayAtStart
|
|
case nearestMinute(minute:Int)
|
|
case nearestHour(hour:Int)
|
|
case nextWeekday(_: WeekDay)
|
|
case nextDSTDate
|
|
case prevMonth
|
|
case nextMonth
|
|
case prevWeek
|
|
case nextWeek
|
|
case nextYear
|
|
case prevYear
|
|
case nextDSTTransition
|
|
}
|
|
|
|
public struct TimeCalculationOptions {
|
|
|
|
/// Specifies the technique the search algorithm uses to find result
|
|
public var matchingPolicy: Calendar.MatchingPolicy
|
|
|
|
/// Specifies the behavior when multiple matches are found
|
|
public var repeatedTimePolicy: Calendar.RepeatedTimePolicy
|
|
|
|
/// Specifies the direction in time to search
|
|
public var direction: Calendar.SearchDirection
|
|
|
|
public init(matching: Calendar.MatchingPolicy = .nextTime,
|
|
timePolicy: Calendar.RepeatedTimePolicy = .first,
|
|
direction: Calendar.SearchDirection = .forward) {
|
|
self.matchingPolicy = matching
|
|
self.repeatedTimePolicy = timePolicy
|
|
self.direction = direction
|
|
}
|
|
}
|
|
|
|
// MARK: - compactMap for Swift 4.0 (not necessary > 4.0)
|
|
|
|
#if swift(>=4.1)
|
|
#else
|
|
extension Collection {
|
|
func compactMap<ElementOfResult>(
|
|
_ transform: (Element) throws -> ElementOfResult?
|
|
) rethrows -> [ElementOfResult] {
|
|
return try flatMap(transform)
|
|
}
|
|
}
|
|
#endif
|