// // 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 TimeInterval { struct ComponentsFormatterOptions { /// Fractional units may be used when a value cannot be exactly represented using the available units. /// For example, if minutes are not allowed, the value “1h 30m” could be formatted as “1.5h”. public var allowsFractionalUnits: Bool? /// Specify the units that can be used in the output. public var allowedUnits: NSCalendar.Unit? /// A Boolean value indicating whether to collapse the largest unit into smaller units when a certain threshold is met. public var collapsesLargestUnit: Bool? /// The maximum number of time units to include in the output string. /// If 0 does not cause the elimination of any units. public var maximumUnitCount: Int? /// The formatting style for units whose value is 0. public var zeroFormattingBehavior: DateComponentsFormatter.ZeroFormattingBehavior? /// The preferred style for units. public var unitsStyle: DateComponentsFormatter.UnitsStyle? /// Locale of the formatter public var locale: LocaleConvertible? { set { calendar.locale = newValue?.toLocale() } get { return calendar.locale } } /// Calendar public var calendar = Calendar.autoupdatingCurrent public func apply(toFormatter formatter: DateComponentsFormatter) { formatter.calendar = calendar if let allowsFractionalUnits = self.allowsFractionalUnits { formatter.allowsFractionalUnits = allowsFractionalUnits } if let allowedUnits = self.allowedUnits { formatter.allowedUnits = allowedUnits } if let collapsesLargestUnit = self.collapsesLargestUnit { formatter.collapsesLargestUnit = collapsesLargestUnit } if let maximumUnitCount = self.maximumUnitCount { formatter.maximumUnitCount = maximumUnitCount } if let zeroFormattingBehavior = self.zeroFormattingBehavior { formatter.zeroFormattingBehavior = zeroFormattingBehavior } if let unitsStyle = self.unitsStyle { formatter.unitsStyle = unitsStyle } } public init() {} } /// Return the local thread shared formatter for date components private static func sharedFormatter() -> DateComponentsFormatter { let name = "SwiftDate_\(NSStringFromClass(DateComponentsFormatter.self))" return threadSharedObject(key: name, create: { let formatter = DateComponentsFormatter() formatter.includesApproximationPhrase = false formatter.includesTimeRemainingPhrase = false return formatter }) } //@available(*, deprecated: 5.0.13, obsoleted: 5.1, message: "Use toIntervalString function instead") func toString(options callback: ((inout ComponentsFormatterOptions) -> Void)? = nil) -> String { return self.toIntervalString(options: callback) } /// Format a time interval in a string with desidered components with passed style. /// /// - Parameters: /// - units: units to include in string. /// - style: style of the units, by default is `.abbreviated` /// - Returns: string representation func toIntervalString(options callback: ((inout ComponentsFormatterOptions) -> Void)? = nil) -> String { let formatter = DateComponentsFormatter() var options = ComponentsFormatterOptions() callback?(&options) options.apply(toFormatter: formatter) let formattedValue = formatter.string(from: self)! if options.zeroFormattingBehavior?.contains(.pad) ?? false { // for some strange reason padding is not added at the very beginning positional item. // we'll add it manually if necessaru if let index = formattedValue.firstIndex(of: ":"), index.utf16Offset(in: formattedValue) < 2 { return "0\(formattedValue)" } } return formattedValue } /// Format a time interval in a string with desidered components with passed style. /// /// - Parameter options: options for formatting. /// - Returns: string representation func toString(options: ComponentsFormatterOptions) -> String { let formatter = TimeInterval.sharedFormatter() options.apply(toFormatter: formatter) return (formatter.string(from: self) ?? "") } /// Return a string representation of the time interval in form of clock countdown (ie. 57:00:00) /// /// - Parameter zero: behaviour with zero. /// - Returns: string representation func toClock(zero: DateComponentsFormatter.ZeroFormattingBehavior = [.pad, .dropLeading]) -> String { return toIntervalString(options: { $0.collapsesLargestUnit = true $0.maximumUnitCount = 0 $0.unitsStyle = .positional $0.locale = Locales.englishUnitedStatesComputer $0.zeroFormattingBehavior = zero }) } /// Extract requeste time units components from given interval. /// Reference date's calendar is used to make the extraction. /// /// NOTE: /// Extraction is calendar/date based; if you specify a `refDate` calculation is made /// between the `refDate` and `refDate + interval`. /// If `refDate` is `nil` evaluation is made from `now()` and `now() + interval` in the context /// of the `SwiftDate.defaultRegion` set. /// /// - Parameters: /// - units: units to extract /// - from: starting reference date, `nil` means `now()` in the context of the default region set. /// - Returns: dictionary with extracted components func toUnits(_ units: Set, to refDate: DateInRegion? = nil) -> [Calendar.Component: Int] { let dateTo = (refDate ?? DateInRegion()) let dateFrom = dateTo.addingTimeInterval(-self) let components = dateFrom.calendar.dateComponents(units, from: dateFrom.date, to: dateTo.date) return components.toDict() } /// Express a time interval (expressed in seconds) in another time unit you choose. /// Reference date's calendar is used to make the extraction. /// /// - parameter component: time unit in which you want to express the calendar component /// - parameter from: starting reference date, `nil` means `now()` in the context of the default region set. /// /// - returns: the value of interval expressed in selected `Calendar.Component` func toUnit(_ component: Calendar.Component, to refDate: DateInRegion? = nil) -> Int? { return toUnits([component], to: refDate)[component] } }