diff --git a/CHANGELOG.md b/CHANGELOG.md index 80dcafe7..bda9e352 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,12 @@ ## CHANGELOG -* Version **[4.0.14](#414)** -* Version **[4.0.13](#413)** -* Version **[4.0.12](#412)** -* Version **[4.0.11](#411)** -* Version **[4.0.10](#410)** +* Version **[4.1.0](#410)** +* Version **[4.0.14](#4014)** +* Version **[4.0.13](#4013)** +* Version **[4.0.12](#4012)** +* Version **[4.0.11](#4011)** +* Version **[4.0.10](#4010)** * Version **[4.0.9](#409)** * Version **[4.0.8](#408)** * Version **[4.0.7](#407)** @@ -22,17 +23,91 @@ * Version **[4.0.0](#400)** - + + +## SwiftDate 4.1.0 +--- +- **Release Date**: 2017/03/31 +- **Zipped Version**: [Download 4.1.0](https://github.com/malcommac/SwiftDate/releases/tag/4.1.0) + +#### New Features +- [#402](https://github.com/malcommac/SwiftDate/pull/402) Added Greek localization (thanks to @dimmdesign) +- [#399](https://github.com/malcommac/SwiftDate/pull/399) `colloquialSinceNow` also allows to set `unitsStyle` params to specify the type of values you want to print. +- [#400](https://github.com/malcommac/SwiftDate/pull/400) `DateInRegion` has a class func named `date(formats:fromRegion)` which allows parsing a single string with multiple formats (the first one that succeeds returns the instance of the `DateInRegion`). Also available as `String` extension (with the same name). +- [#223](https://github.com/malcommac/SwiftDate/pull/223) `ISO8601DateTimeFormatter` now recognize the timezone of an ISO string and create a date with the correct value. +- [#407](https://github.com/malcommac/SwiftDate/pull/407) SwiftDate now can parse ISO8601 strings without specifyng the ISO format; it evaluates the best format automatically. Also the parser faster than the previous built one. Since now `.iso8601` parsing format is used only as formatter (from date to string, viceversa any given value is ignored. You are encouraged to use `.iso8601Auto` instead). + + The following ISO8601 variants are supported: + +``` +YYYYMMDD +YYYY-MM-DD +YYYY-MM +YYYY +YY //century +//Implied century: YY is 00-99 +YYMMDD +YY-MM-DD +-YYMM +-YY-MM +-YY +//Implied year +--MMDD +--MM-DD +--MM +//Implied year and month +---DD +//Ordinal dates: DDD is the number of the day in the year (1-366) +YYYYDDD +YYYY-DDD +YYDDD +YY-DDD +-DDD +//Week-based dates: ww is the number of the week, and d is the number (1-7) of the day in the week +yyyyWwwd +yyyy-Www-d +yyyyWww +yyyy-Www +yyWwwd +yy-Www-d +yyWww +yy-Www +//Year of the implied decade +-yWwwd +-y-Www-d +-yWww +-y-Www +//Week and day of implied year +-Wwwd +-Www-d +//Week only of implied year +-Www +//Day only of implied week +-W-d +``` + + +#### Fixes +- [#405](https://github.com/malcommac/SwiftDate/pull/405) Fixed some translation issues in Swedish (thanks to @deville) +- [#368](https://github.com/malcommac/SwiftDate/pull/368) Deprecated `at(unitsWithValues dict: [Calendar.Component : Int])` in `Date` and `DateInRegion` and replaced with functional `at(values: [Calendar.Component : Int], keep: Set)` +- [#392](https://github.com/malcommac/SwiftDate/pull/392) Fixed an issue with report negative interval when making operation with dates `a` and `b` where `a - b < 0 iff a < b`. +- [#397](https://github.com/malcommac/SwiftDate/pull/397) Fixed an issue with `colloquial` func which report wrong difference of `1 day` when two dates are distant < 24h but in two different days. + + + + + + ## SwiftDate 4.0.14 --- -- **Release Date**: - +- **Release Date**: 2017/03/29 - **Zipped Version**: [Download 4.0.14](https://github.com/malcommac/SwiftDate/releases/tag/4.0.14) - [#404](https://github.com/malcommac/SwiftDate/pull/404) Compatibility with Swift 3.1 - + ## SwiftDate 4.0.13 --- @@ -43,7 +118,7 @@ - [#384](https://github.com/malcommac/SwiftDate/pull/384) Added Arabic translation (thanks to @abdualrhmanIO) - [#356](https://github.com/malcommac/SwiftDate/pull/356) Added a new formatter option called `strict`. Using `strict` instead of `custom` disable heuristics date guessing of the formatter (ie. 1999-02-31 become an invalid date to parse, while with heuristics enabled guessing date 1999-03-03 is returned instead). - + ## SwiftDate 4.0.12 --- @@ -60,7 +135,7 @@ - [#381](https://github.com/malcommac/SwiftDate/pull/381) Replaced `useImminentInterval` in `DateInRegionFormatter` with a configurable value called `imminentInterval`. With a default value of 5 it fallback to `just now` version. If `nil` fallback is disabled. - [#380](https://github.com/malcommac/SwiftDate/pull/380) `DateInRegionFormatter` is now able to load custom localization both from `LocaleName` and custom `.strings` files (just set the `formatter.localization = Localization(path: [PATH_TO_YOUR_STRINGS_FILE]`) - + ## SwiftDate 4.0.11 --- @@ -74,7 +149,7 @@ #### New Features - [#365](https://github.com/malcommac/SwiftDate/issues/365) Brazilian Portuguese support (thanks to @ipedro) - + ## SwiftDate 4.0.10 --- diff --git a/README.md b/README.md index bd60eab4..0b7be9dc 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Take a look here: ## Documentation * **On [http://malcommac.github.io/SwiftDate/index.html](http://malcommac.github.io/SwiftDate/index.html) to learn more about all available functions with a comprehensive list of examples** -* The **latest [full class documentation is available here](http://cocoadocs.org/docsets/SwiftDate/4.0.8/)** +* The **latest [full class documentation is available here](http://cocoadocs.org/docsets/SwiftDate/4.1.0/)** Code is documented for Xcode, so you can use the built-in documentation panel to learn more about the library. @@ -59,7 +59,7 @@ You can also generate the latest documentation using [Jazzy](https://github.com/ ## Current Release -Latest release is: 4.0.14 [Download here](https://github.com/malcommac/SwiftDate/releases/tag/4.0.14). +Latest release is: 4.1.0 [Download here](https://github.com/malcommac/SwiftDate/releases/tag/4.1.0). A complete list of changes for each release is available in the [CHANGELOG](CHANGELOG.md) file. @@ -86,8 +86,9 @@ Currently SwiftDate supports: * Japanese (made by [bati668](https://github.com/bati668), since 4.0.9) * Brazilian Portuguese (made by [ipedro](https://github.com/ipedro), since 4.0.11) * Hebrew (made by [@ilandbt](https://github.com/ilandbt), since 4.0.12) -* Swedish (made by [@traneHead](https://github.com/traneHead), since 4.0.12) +* Swedish (made by [@traneHead](https://github.com/traneHead) and [@deville](https://github.com/deville), since 4.0.12, updated in 4.1.0) * Arabic (made by [@abdualrhmanIO](https://github.com/abdualrhmanIO), since 4.0.13) +* Greek (made by [@dimmdesign](https://github.com/dimmdesign), since 4.10) Make a pull request and add your language! @@ -119,6 +120,8 @@ target 'TargetName' do end ``` +(use 4.0.13 for Swift 3.0) + Then, run the following command: ```bash @@ -148,7 +151,7 @@ Run `carthage` to build the framework and drag the built `SwiftDate.framework` i Current version is compatible with: -* Swift 3.0+ +* Swift 3.1 (4.0.13 is the latest version compatibile with Swift 3) * iOS 8 or later * macOS 10.10 or later * watchOS 2.0 or later @@ -157,8 +160,9 @@ Current version is compatible with: Are you searching for an old (unsupported) SwiftDate version? Check out: +* Swift 3.0: The latest version compatible is 4.0.13 * [Swift 2.3 Branch](https://github.com/malcommac/SwiftDate/tree/feature/swift_23) -* Swift 2.2: Use version 3.0.8 in CocoaPods +* Swift 2.2: The latest version compatible is 3.0.8 ## Credits & License diff --git a/Sources/SwiftDate/Commons.swift b/Sources/SwiftDate/Commons.swift index 1fe7f0f0..62eeee29 100644 --- a/Sources/SwiftDate/Commons.swift +++ b/Sources/SwiftDate/Commons.swift @@ -93,7 +93,9 @@ public enum DateError: Error { /// - strict: strict format is like custom but does not apply heuristics to guess at the date which is intended by the string. /// So, if you pass an invalid date (like 1999-02-31) formatter fails instead of returning guessing date (in our case /// 1999-03-03). -/// - iso8601: iso8601 date format (see https://en.wikipedia.org/wiki/ISO_8601) +/// - iso8601: ISO8601 date format (see https://en.wikipedia.org/wiki/ISO_8601). +/// - iso8601Auto: ISO8601 date format. You should use it to parse a date (parsers evaluate automatically the format of +/// the ISO8601 string). Passed as options to transform a date to string it's equal to [.withInternetDateTime] options. /// - extended: extended date format ("eee dd-MMM-yyyy GG HH:mm:ss.SSS zzz") /// - rss: RSS and AltRSS date format /// - dotNET: .NET date format @@ -101,6 +103,7 @@ public enum DateFormat { case custom(String) case strict(String) case iso8601(options: ISO8601DateTimeFormatter.Options) + case iso8601Auto case extended case rss(alt: Bool) case dotNET diff --git a/Sources/SwiftDate/Date+Components.swift b/Sources/SwiftDate/Date+Components.swift index 4f69726a..7e681884 100644 --- a/Sources/SwiftDate/Date+Components.swift +++ b/Sources/SwiftDate/Date+Components.swift @@ -355,10 +355,21 @@ public extension Date { /// /// - parameter dict: a dictionary with `Calendar.Component` and it's value /// - /// - throws: throw a `FailedToSetComponent` exception. + /// - throws: throw a `FailedToCalculate` exception. /// /// - returns: a new `Date` object calculated at given units values + @available(*, deprecated: 4.1.0, message: "This method has know issues. Use at(values:keep:) instead") public func at(unitsWithValues dict: [Calendar.Component : Int]) throws -> Date { return try self.inDateDefaultRegion().at(unitsWithValues: dict).absoluteDate } + + /// Create a new instance of the date by keeping passed calendar components and alter + /// + /// - Parameters: + /// - values: values to alter in new instance + /// - keep: values to keep from self instance + /// - Returns: a new instance of `DateInRegion` with passed altered values + public func at(values: [Calendar.Component : Int], keep: Set) -> Date? { + return self.inDateDefaultRegion().at(values: values, keep: keep)?.absoluteDate + } } diff --git a/Sources/SwiftDate/Date+Formatter.swift b/Sources/SwiftDate/Date+Formatter.swift index 7d2b66bc..16d83e21 100644 --- a/Sources/SwiftDate/Date+Formatter.swift +++ b/Sources/SwiftDate/Date+Formatter.swift @@ -93,7 +93,7 @@ public extension Date { /// - returns: colloquial string representation public func colloquialSinceNow(in region: Region? = nil, unitStyle: DateComponentsFormatter.UnitsStyle = .short, max: Int? = nil, zero: DateZeroBehaviour? = nil, separator: String? = nil) throws -> (colloquial: String, time: String?) { let srcRegion = region ?? DateDefaultRegion - return try DateInRegion(absoluteDate: self, in: srcRegion).colloquialSinceNow() + return try DateInRegion(absoluteDate: self, in: srcRegion).colloquialSinceNow(style: unitStyle) } /// This method produces a colloquial representation of time elapsed between this `DateInRegion` (`self`) and @@ -114,22 +114,6 @@ public extension Date { return try DateInRegion(absoluteDate: self, in: srcRegion).colloquial(toDate: toDateInRegion) } - /// This method produces a string by printing the interval between self and current Date and output a string where each - /// calendar component is printed. - /// - /// - parameter unitStyle: style of the output string - /// - parameter max: max number of the time components to write (nil means no limit) - /// - parameter zero: the behaviour to use with zero value components - /// - parameter separator: separator string between components (default is ',') - /// - /// - throws: throw an exception if time components cannot be evaluated - /// - /// - returns: string with each time component - @available(*, deprecated: 4.0.3, message: "Use timeComponentsSinceNow(options:shared:) instead") - public func timeComponentsSinceNow(unitStyle: DateComponentsFormatter.UnitsStyle = .short, max: Int? = nil, zero: DateZeroBehaviour? = nil, separator: String? = nil) throws -> String { - return try self.timeComponents(to: Date(), unitStyle: unitStyle, max: max, zero: zero, separator: separator) - } - /// This method produces a string by printing the interval between self and current Date and output a string where each /// calendar component is printed. /// @@ -145,28 +129,6 @@ public extension Date { return try self.timeComponents(to: Date(), options: options, shared: shared) } - /// This method produces a string by printing the interval between self and another date and output a string where each - /// calendar component is printed. - /// - /// - parameter to: date to compare - /// - parameter region: region in which both dates will be expressed in - /// - parameter unitStyle: style of the output string - /// - parameter max: max number of the time components to write (nil means no limit) - /// - parameter zero: the behaviour to use with zero value components - /// - parameter separator: separator string between components (default is ',') - /// - /// - throws: throw an exception if time components cannot be evaluated - /// - /// - returns: string with each time component - @available(*, deprecated: 4.0.3, message: "Use timeComponents(to:options:shared:) instead") - public func timeComponents(to: Date, in region: Region? = nil, unitStyle: DateComponentsFormatter.UnitsStyle = .short, max: Int? = nil, zero: DateZeroBehaviour? = nil, separator: String? = nil) throws -> String { - - let srcRegion = region ?? DateDefaultRegion - let fromDateInRegion = DateInRegion(absoluteDate: self, in: srcRegion) - let toDateInRegion = DateInRegion(absoluteDate: to, in: srcRegion) - return try fromDateInRegion.timeComponents(toDate: toDateInRegion, unitStyle: unitStyle, max: max, zero: zero, separator: separator) - } - /// This method produces a string by printing the interval between self and another date and output a string where each /// calendar component is printed. /// diff --git a/Sources/SwiftDate/DateComponents+Extension.swift b/Sources/SwiftDate/DateComponents+Extension.swift index 91880206..5dd381a8 100644 --- a/Sources/SwiftDate/DateComponents+Extension.swift +++ b/Sources/SwiftDate/DateComponents+Extension.swift @@ -102,11 +102,7 @@ public extension DateComponents { /// It's the same of `DateInRegion(components:)` init func but it may return nil (instead of throwing an exception) /// if a valid date cannot be produced. public var dateInRegion: DateInRegion? { - do { - return try DateInRegion(components: self) - } catch { - return nil - } + return DateInRegion(components: self) } diff --git a/Sources/SwiftDate/DateInRegion+Components.swift b/Sources/SwiftDate/DateInRegion+Components.swift index da369737..c7230115 100644 --- a/Sources/SwiftDate/DateInRegion+Components.swift +++ b/Sources/SwiftDate/DateInRegion+Components.swift @@ -514,14 +514,35 @@ extension DateInRegion { } + /// Create a new instance of the date by keeping passed calendar components and alter + /// + /// - Parameters: + /// - values: values to alter in new instance + /// - keep: values to keep from self instance + /// - Returns: a new instance of `DateInRegion` with passed altered values + public func at(values: [Calendar.Component : Int], keep: Set) -> DateInRegion? { + let calendar = self.region.calendar + var newComponents = calendar.dateComponents(keep, from: self.absoluteDate) + + values.forEach { newComponents.setValue($0.value, for: $0.key) } + + guard let calculatedDate = calendar.date(from: newComponents) else { + return nil + } + return DateInRegion(absoluteDate: calculatedDate, in: self.region) + + } + /// Create a new instance calculated by setting a list of components of a given date to given values (components /// are evaluated serially - in order), while trying to keep lower components the same. /// /// - parameter dict: a dictionary with `Calendar.Component` and it's value /// - /// - throws: throw a `FailedToSetComponent` exception. + /// - throws: throw a `FailedToCalculate` exception. /// /// - returns: a new `DateInRegion` object calculated at given units values + @available(*, deprecated: 4.1.0, message: "This method has know issues. Use at(values:keep:) instead") + @discardableResult public func at(unitsWithValues dict: [Calendar.Component : Int]) throws -> DateInRegion { var calculatedDate = self.absoluteDate try DateComponents.allComponents.forEach { component in diff --git a/Sources/SwiftDate/DateInRegion+Formatter.swift b/Sources/SwiftDate/DateInRegion+Formatter.swift index a806fb52..66e10779 100644 --- a/Sources/SwiftDate/DateInRegion+Formatter.swift +++ b/Sources/SwiftDate/DateInRegion+Formatter.swift @@ -61,8 +61,11 @@ public extension DateInRegion { case .strict(let format): return self.formatters.dateFormatter(format: format).string(from: self.absoluteDate) case .iso8601(let options): - let formatter = self.formatters.isoFormatter(options: options) - return formatter.string(from: self.absoluteDate) + let formatter = self.formatters.isoFormatter() + return formatter.string(from: self.absoluteDate, options: options) + case .iso8601Auto: + let formatter = self.formatters.isoFormatter() + return formatter.string(from: self.absoluteDate, options: [.withInternetDateTime]) case .rss(let isAltRSS): let format = (isAltRSS ? "d MMM yyyy HH:mm:ss ZZZ" : "EEE, d MMM yyyy HH:mm:ss ZZZ") return self.formatters.dateFormatter(format: format).string(from: self.absoluteDate) @@ -106,49 +109,33 @@ public extension DateInRegion { return formatter.string(from: self.absoluteDate) } - /// This method produces a colloquial representation of time elapsed between this `DateInRegion` (`self`) and /// the current date (`Date()`) /// - /// - throws: throw an exception is colloquial string cannot be evaluated - /// + /// - parameter style: style of output. If not specified `.full` is used /// - returns: colloquial string representation - public func colloquialSinceNow() throws -> (colloquial: String, time: String?) { + /// - throws: throw an exception is colloquial string cannot be evaluated + public func colloquialSinceNow(style: DateComponentsFormatter.UnitsStyle? = nil) throws -> (colloquial: String, time: String?) { let now = DateInRegion(absoluteDate: Date(), in: self.region) - return try self.colloquial(toDate: now) + return try self.colloquial(toDate: now, style: style) } /// This method produces a colloquial representation of time elapsed between this `DateInRegion` (`self`) and /// another passed date. /// - /// - parameter date: date to compare - /// - /// - throws: throw an exception is colloquial string cannot be evaluated - /// + /// - Parameters: + /// - parameter date: date to compare + /// - parameter style: style of output. If not specified `.full` is used /// - returns: colloquial string representation - public func colloquial(toDate date: DateInRegion) throws -> (colloquial: String, time: String?) { + /// - throws: throw an exception is colloquial string cannot be evaluated + public func colloquial(toDate date: DateInRegion, style: DateComponentsFormatter.UnitsStyle? = nil) throws -> (colloquial: String, time: String?) { let formatter = DateInRegionFormatter() formatter.localization = Localization(locale: self.region.locale) + formatter.unitStyle = style ?? .full return try formatter.colloquial(from: self, to: date) } - /// This method produces a string by printing the interval between self and current Date and output a string where each - /// calendar component is printed. - /// - /// - parameter unitStyle: style of the output string - /// - parameter max: max number of the time components to write (nil means no limit) - /// - parameter zero: the behaviour to use with zero value components - /// - parameter separator: separator string between components (default is ',') - /// - /// - throws: throw an exception if time components cannot be evaluated - /// - /// - returns: string with each time component - @available(*, deprecated: 4.0.3, message: "Use timeComponentsSinceNow(options:) instead") - public func timeComponentsSinceNow(unitStyle: DateComponentsFormatter.UnitsStyle = .short, max: Int? = nil, zero: DateZeroBehaviour? = nil, separator: String? = nil) throws -> String { - let now = DateInRegion(absoluteDate: Date(), in: self.region) - return try self.timeComponents(toDate: now, unitStyle: unitStyle, max: max, zero: zero, separator: separator) - } // This method produces a string by printing the interval between self and current Date and output a string where each /// calendar component is printed. @@ -165,28 +152,6 @@ public extension DateInRegion { return try interval.string(options: optionsStruct, shared: self.formatters.useSharedFormatters) } - /// This method produces a string by printing the interval between self and another date and output a string where each - /// calendar component is printed. - /// - /// - parameter toDate: date to compare - /// - parameter unitStyle: style of the output string - /// - parameter max: max number of the time components to write (nil means no limit) - /// - parameter zero: the behaviour to use with zero value components - /// - parameter separator: separator string between components (default is ',') - /// - /// - throws: throw an exception if time components cannot be evaluated - /// - /// - returns: string with each time component - @available(*, deprecated: 4.0.3, message: "Use timeComponents(toDate:,options:) instead") - public func timeComponents(toDate date: DateInRegion, unitStyle: DateComponentsFormatter.UnitsStyle = .short, max: Int? = nil, zero: DateZeroBehaviour? = nil, separator: String? = nil) throws -> String { - let formatter = DateInRegionFormatter() - formatter.localization = Localization(locale: self.region.locale) - formatter.maxComponentCount = max - formatter.unitStyle = unitStyle - formatter.zeroBehavior = zero ?? .dropAll - formatter.unitSeparator = separator ?? "," - return try formatter.timeComponents(from: self, to: date) - } /// This method produces a string by printing the interval between self and another date and output a string where each /// calendar component is printed. diff --git a/Sources/SwiftDate/DateInRegion.swift b/Sources/SwiftDate/DateInRegion.swift index 5f2789f1..5b3cc154 100644 --- a/Sources/SwiftDate/DateInRegion.swift +++ b/Sources/SwiftDate/DateInRegion.swift @@ -75,13 +75,7 @@ public class DateInRegion: CustomStringConvertible { self.locale = region.locale } - /// Return an `ISO8601DateTimeFormatter` instance. Returned instance is the one shared along calling thread - /// if `.useSharedFormatters = false`; otherwise a reserved instance is created for this `DateInRegion` - /// - /// - parameter options: options to set for formatter - /// - /// - returns: a new instance of the formatter - public func isoFormatter(options: ISO8601DateTimeFormatter.Options) -> ISO8601DateTimeFormatter { + public func isoFormatter() -> ISO8601DateTimeFormatter { var formatter: ISO8601DateTimeFormatter? = nil if useSharedFormatters == true { let name = "SwiftDate_\(NSStringFromClass(ISO8601DateTimeFormatter.self))" @@ -94,9 +88,7 @@ public class DateInRegion: CustomStringConvertible { formatter = customISO8601Formatter } } - formatter!.formatOptions = options - formatter!.timeZone = self.timeZone - formatter!.locale = self.locale + formatter!.locale = self.locale return formatter! } @@ -106,7 +98,7 @@ public class DateInRegion: CustomStringConvertible { /// - parameter format: if not nil a new `.dateFormat` is also set /// /// - returns: a new instance of the formatter - public func dateFormatter(format: String? = nil, heuristics: Bool = true) -> DateFormatter { + public func dateFormatter(format: String? = nil, heuristics: Bool = true) -> DateFormatter { var formatter: DateFormatter? = nil if useSharedFormatters == true { let name = "SwiftDate_\(NSStringFromClass(DateFormatter.self))" @@ -183,15 +175,13 @@ public class DateInRegion: CustomStringConvertible { /// /// - parameter components: `DateComponents` with valid components used to generate a new date /// - /// - throws: throw an exception when `DateComponents` does not include required components used to generate a valid date (it must also include information about timezone, calendar and locale) - /// /// - returns: a new `DateInRegion` from given components - public init(components: DateComponents) throws { + public init?(components: DateComponents) { guard let srcRegion = Region(components: components) else { - throw DateError.MissingCalTzOrLoc + return nil } guard let absDate = srcRegion.calendar.date(from: components) else { - throw DateError.FailedToParse + return nil } self.absoluteDate = absDate self.region = srcRegion @@ -205,63 +195,87 @@ public class DateInRegion: CustomStringConvertible { /// - parameter components: calendar components keys and values to assign /// - parameter region: region in which the date is expressed. If `nil` local region will used instead (`Region.Local()`) /// - /// - throws: throw a `FailedToParse` exception if date cannot be generated with given set of values - /// - /// - returns: a new `DateInRegion` instance expressed in passed region - public init(components: [Calendar.Component : Int], fromRegion region: Region? = nil) throws { + /// - returns: a new `DateInRegion` instance expressed in passed region, `nil` if parse fails + public init?(components: [Calendar.Component : Int], fromRegion region: Region? = nil) { let srcRegion = region ?? Region.Local() self.formatters = Formatters(region: srcRegion) let cmp = DateInRegion.componentsFrom(values: components, setRegion: srcRegion) guard let absDate = srcRegion.calendar.date(from: cmp) else { - throw DateError.FailedToParse + return nil } self.absoluteDate = absDate self.region = srcRegion } + + /// Parse a string using given formats; the first format which produces a valid `DateInRegion` + /// instance returns parsed instance. If none of passed formats can produce a valid region `nil` + /// is returned. + /// + /// - Parameters: + /// - string: string to parse + /// - formats: formats used for parsing. Formats are evaluated in order. + /// - parameter region: region in which the date is expressed. If `nil` local region will used instead (`Region.Local()`). When `.iso8601` or `.iso8601Auto` is used, `region` parameter is ignored (timezone is set automatically by reading the string. + /// - returns: a new `DateInRegion` instance expressed in passed region, `nil` if parse fails + public class func date(string: String, formats: [DateFormat], fromRegion region: Region? = nil) -> DateInRegion? { + for format in formats { + if let date = DateInRegion(string: string, format: format, fromRegion: region) { + return date + } + } + return nil + } + /// Initialize a new `DateInRegion` created from passed format rexpressed in specified region. /// /// - parameter string: string with date to parse /// - parameter format: format in which the date is expressed (see `DateFormat`) /// - parameter region: region in which the date should be expressed (if nil `Region.Local()` will be used instead) - /// - /// - throws: throw an `FailedToParse` exception if date cannot be parsed - /// + /// When `.iso8601` or `.iso8601Auto` is used, `region` parameter is ignored (timezone is set automatically by reading the string. /// - returns: a new DateInRegion from given string - public init(string: String, format: DateFormat, fromRegion region: Region? = nil) throws { - let srcRegion = region ?? Region.Local() + public init?(string: String, format: DateFormat, fromRegion region: Region? = nil) { + var srcRegion = region ?? Region.Local() self.formatters = Formatters(region: srcRegion) switch format { case .custom(let format): guard let date = self.formatters.dateFormatter(format: format).date(from: string) else { - throw DateError.FailedToParse + return nil } self.absoluteDate = date case .strict(let format): guard let date = self.formatters.dateFormatter(format: format, heuristics: false).date(from: string) else { - throw DateError.FailedToParse + return nil } self.absoluteDate = date - case .iso8601(let options): - guard let date = self.formatters.isoFormatter(options: options).date(from: string) else { - throw DateError.FailedToParse + case .iso8601(_), .iso8601Auto: + do { + let configuration = ISO8601Configuration(calendar: srcRegion.calendar) + guard let date = try ISO8601Parser(string, config: configuration).parsedDate else { + return nil + } + self.absoluteDate = date + if srcRegion != Region.GMT() { // region is ignored + print("Region is read from the string when ISO8601 parser is used") + } + srcRegion = Region.GMT() + } catch { + return nil } - self.absoluteDate = date case .extended: let format = "eee dd-MMM-yyyy GG HH:mm:ss.SSS zzz" guard let date = self.formatters.dateFormatter(format: format).date(from: string) else { - throw DateError.FailedToParse + return nil } self.absoluteDate = date case .rss(let isAltRSS): let format = (isAltRSS ? "d MMM yyyy HH:mm:ss ZZZ" : "EEE, d MMM yyyy HH:mm:ss ZZZ") guard let date = self.formatters.dateFormatter(format: format).date(from: string) else { - throw DateError.FailedToParse + return nil } self.absoluteDate = date case .dotNET: guard let secsSince1970 = string.dotNETParseSeconds() else { - throw DateError.FailedToParse + return nil } self.absoluteDate = Date(timeIntervalSince1970: secsSince1970) } diff --git a/Sources/SwiftDate/DateInRegionFormatter.swift b/Sources/SwiftDate/DateInRegionFormatter.swift index 6fb824bb..d1f9782d 100644 --- a/Sources/SwiftDate/DateInRegionFormatter.swift +++ b/Sources/SwiftDate/DateInRegionFormatter.swift @@ -189,6 +189,7 @@ public class DateInRegionFormatter { let cal = fDate.region.calendar let cmp = cal.dateComponents(self.allowedComponents, from: fDate.absoluteDate, to: tDate.absoluteDate) let isFuture = (fDate > tDate) + let diff_in_seconds = abs(fDate.absoluteDate.timeIntervalSince(tDate.absoluteDate)) if cmp.year != nil && (cmp.year != 0 || !hasLowerAllowedComponents(than: .year)) { let colloquial_time = try self.colloquial_time(forUnit: .year, withValue: cmp.year!, date: fDate) @@ -202,7 +203,12 @@ public class DateInRegionFormatter { return (colloquial_date,colloquial_time) } - if cmp.day != nil { + // This was introduced in order to take care when two dates are different in days + // but the distance is less than 24 hour (ie. 2017/01/01 at 23:00 and 2017/01/02 at 01:00 + // difference is 2 hours and not 1 day). + let diff_in_hours = (diff_in_seconds / 60 / 60) + + if cmp.day != nil && diff_in_hours >= 24 { if abs(cmp.day!) >= DAYS_IN_WEEK { let colloquial_time = try self.colloquial_time(forUnit: .day, withValue: cmp.day!, date: fDate) let weeksNo = (abs(cmp.day!) / DAYS_IN_WEEK) diff --git a/Sources/SwiftDate/DateTimeInterval.swift b/Sources/SwiftDate/DateTimeInterval.swift index bdd351bb..02cb0363 100644 --- a/Sources/SwiftDate/DateTimeInterval.swift +++ b/Sources/SwiftDate/DateTimeInterval.swift @@ -64,7 +64,7 @@ public struct DateTimeInterval : Comparable { /// Initialize a `DateTimeInterval` with the specified start and end date. public init(start: Date, end: Date) { self.start = start - duration = end.timeIntervalSince(start) + duration = start.timeIntervalSince(end) } /// Initialize a `DateTimeInterval` with the specified start date and duration. diff --git a/Sources/SwiftDate/Extensions.swift b/Sources/SwiftDate/Extensions.swift index 8a1ffcca..3739bb1e 100644 --- a/Sources/SwiftDate/Extensions.swift +++ b/Sources/SwiftDate/Extensions.swift @@ -33,11 +33,22 @@ public extension String { /// - parameter format: format of the date string /// - parameter region: region in which you want to describe the date /// - /// - throws: throw an exception if DateInRegion cannot be created + /// - returns: a new DateInRegion representing passed string in given region + public func date(format: DateFormat, fromRegion region: Region? = nil) -> DateInRegion? { + return DateInRegion(string: self, format: format, fromRegion: region) + } + + + /// Attempt to parse a string with multiple date formats. Parsing operation is executed in order + /// and when the first format ends successfully it stops the parsing chain and return the instance + /// of `DateInRegion`. /// + /// - Parameters: + /// - formats: formats to use + /// - parameter region: region in which you want to describe the date /// - returns: a new DateInRegion representing passed string in given region - public func date(format: DateFormat, fromRegion region: Region? = nil) throws -> DateInRegion { - return try DateInRegion(string: self, format: format, fromRegion: region) + public func date(formats: [DateFormat], fromRegion region: Region? = nil) -> DateInRegion? { + return DateInRegion.date(string: self, formats: formats, fromRegion: region) } } @@ -104,7 +115,6 @@ public extension Int { return dateComponents } - /// Create a `DateComponents` with `self` value set as nanoseconds public var nanoseconds: DateComponents { return self.toDateComponents(type: .nanosecond) diff --git a/Sources/SwiftDate/ISO8601DateTimeFormatter.swift b/Sources/SwiftDate/ISO8601DateTimeFormatter.swift index d3759f6f..ab4e178b 100644 --- a/Sources/SwiftDate/ISO8601DateTimeFormatter.swift +++ b/Sources/SwiftDate/ISO8601DateTimeFormatter.swift @@ -27,6 +27,7 @@ import Foundation /// MARK: - ISO8601DateTimeFormatter /// This is a re-implementation of the ISO8601DateFormatter which is compatible with iOS lower than version 10. + public class ISO8601DateTimeFormatter { public struct Options: OptionSet { @@ -81,12 +82,100 @@ public class ISO8601DateTimeFormatter { // The format used for internet date times; it's similar to .withInternetDateTime // but include milliseconds ('yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ'). public static let withInternetDateTimeExtended = ISO8601DateTimeFormatter.Options(rawValue: 1 << 11) + + /// Evaluate formatting string + public var formatterString: String { + if self.contains(.withInternetDateTimeExtended) { + return "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" + } + + if self.contains(.withInternetDateTime) { + return "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + } + + var format: String = "" + if self.contains(.withFullDate) { + format += "yyyy-MM-dd" + } else { + if self.contains(.withYear) { + if self.contains(.withWeekOfYear) { + format += "YYYY" + } else if self.contains(.withMonth) || self.contains(.withDay) { + format += "yyyy" + } else { + // not valid + } + } + if self.contains(.withMonth) { + if self.contains(.withYear) || self.contains(.withDay) || self.contains(.withWeekOfYear) { + format += "MM" + } else { + // not valid + } + } + if self.contains(.withWeekOfYear) { + if self.contains(.withDay) { + format += "'W'ww" + } else { + if self.contains(.withYear) || self.contains(.withMonth) { + if self.contains(.withDashSeparatorInDate) { + format += "-'W'ww" + } else { + format += "'W'ww" + } + } else { + // not valid + } + } + } + + if self.contains(.withDay) { + if self.contains(.withWeekOfYear) { + format += "FF" + } else if self.contains(.withMonth) { + format += "dd" + } else if self.contains(.withYear) { + if self.contains(.withDashSeparatorInDate) { + format += "-DDD" + } else { + format += "DDD" + } + } else { + // not valid + } + } + } + + let hasDate = (self.contains(.withFullDate) || self.contains(.withMonth) || self.contains(.withDay) || self.contains(.withWeekOfYear) || self.contains(.withYear)) + if hasDate && (self.contains(.withFullTime) || self.contains(.withTimeZone)) { + if self.contains(.withSpaceBetweenDateAndTime) { + format += " " + } else { + format += "'T'" + } + } + + if self.contains(.withFullTime) { + format += "HH:mm:ssZZZZZ" + } else { + if self.contains(.withTime) { + format += "HH:mm:ss" + } + if self.contains(.withTimeZone) { + format += "ZZZZZ" + } + } + + return format + } } /// Options for generating and parsing ISO 8601 date representations. + @available(*, deprecated: 4.1.0, message: "This property is not used anymore. Use string(from:options:) to format a date to string or class func date(from:config:) to transform a string to date") public var formatOptions: ISO8601DateTimeFormatter.Options = ISO8601DateTimeFormatter.Options(rawValue: 0) /// The time zone used to create and parse date representations. When unspecified, GMT is used. + @available(*, deprecated: 4.1.0, message: "This property is not used anymore. Parsing is done automatically by reading specified timezone. If not specified UTC is used.") public var timeZone: TimeZone? { set { self.formatter.timeZone = newValue ?? TimeZone(secondsFromGMT: 0) @@ -108,17 +197,16 @@ public class ISO8601DateTimeFormatter { /// formatter instance used for date private var formatter: DateFormatter = DateFormatter() - public init() { - self.timeZone = TimeZone(secondsFromGMT: 0)! - } + public init() { } /// Creates and returns an ISO 8601 formatted string representation of the specified date. /// /// - parameter date: The date to be represented. /// /// - returns: A user-readable string representing the date. + @available(*, deprecated: 4.1.0, message: "Use string(from:options:) function instead") public func string(from date: Date) -> String { - self.formatter.dateFormat = self.formatterString + self.formatter.dateFormat = self.formatOptions.formatterString return self.formatter.string(from: date) } @@ -127,9 +215,37 @@ public class ISO8601DateTimeFormatter { /// - parameter string: The ISO 8601 formatted string representation of a date. /// /// - returns: A date object, or nil if no valid date was found. + @available(*, deprecated: 4.1.0, message: "Use ISO8601DateTimeFormatter class func date(from:config) instead") public func date(from string: String) -> Date? { - self.formatter.dateFormat = self.formatterString - return self.formatter.date(from: string) + //self.formatter.dateFormat = self.formatOptions.formatterString + //return self.formatter.date(from: string) + return ISO8601DateTimeFormatter.date(from: string) + } + + + /// Creates and return a date object from the specified ISO8601 formatted string representation + /// + /// - Parameters: + /// - string: valid ISO8601 string to parse + /// - config: configuration to use. `nil` uses default configuration + /// - Returns: a valid `Date` object or `nil` if parse fails + public class func date(from string: String, config: ISO8601Configuration = ISO8601Configuration()) -> Date? { + do { + return try ISO8601Parser(string, config: config).parsedDate + } catch { + return nil + } + } + + /// Creates and returns an ISO 8601 formatted string representation of the specified date. + /// + /// - Parameters: + /// - date: The date to be represented. + /// - options: Formastting style + /// - Returns: a string description of the date + public func string(from date: Date, options: ISO8601DateTimeFormatter.Options = [.withInternetDateTime]) -> String { + self.formatter.dateFormat = options.formatterString + return self.formatter.string(from: date) } /// Creates a representation of the specified date with a given time zone and format options. @@ -139,96 +255,21 @@ public class ISO8601DateTimeFormatter { /// - parameter formatOptions: The options used. For possible values, see ISO8601DateTimeFormatter.Options. /// /// - returns: A user-readable string representing the date. - class func string(from date: Date, timeZone: TimeZone, formatOptions: ISO8601DateTimeFormatter.Options = []) -> String { - let formatter = ISO8601DateTimeFormatter() - formatter.locale = LocaleName.englishUnitedStatesComputer.locale // fix for 12/24h - formatter.formatOptions = formatOptions - return formatter.string(from: date) + @available(*, deprecated: 4.1.0, message: "Use ISO8601DateTimeFormatter class func string(from:options:) instead") + public class func string(from date: Date, timeZone: TimeZone, formatOptions: ISO8601DateTimeFormatter.Options = []) -> String { + return ISO8601DateTimeFormatter.string(from: date, options: formatOptions) } - /// Evaluate formatting string - public var formatterString: String { - if formatOptions.contains(.withInternetDateTimeExtended) { - return "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" - } - - if formatOptions.contains(.withInternetDateTime) { - return "yyyy-MM-dd'T'HH:mm:ssZZZZZ" - } - - var format: String = "" - if formatOptions.contains(.withFullDate) { - format += "yyyy-MM-dd" - } else { - if formatOptions.contains(.withYear) { - if formatOptions.contains(.withWeekOfYear) { - format += "YYYY" - } else if formatOptions.contains(.withMonth) || formatOptions.contains(.withDay) { - format += "yyyy" - } else { - // not valid - } - } - if formatOptions.contains(.withMonth) { - if formatOptions.contains(.withYear) || formatOptions.contains(.withDay) || formatOptions.contains(.withWeekOfYear) { - format += "MM" - } else { - // not valid - } - } - if formatOptions.contains(.withWeekOfYear) { - if formatOptions.contains(.withDay) { - format += "'W'ww" - } else { - if formatOptions.contains(.withYear) || formatOptions.contains(.withMonth) { - if formatOptions.contains(.withDashSeparatorInDate) { - format += "-'W'ww" - } else { - format += "'W'ww" - } - } else { - // not valid - } - } - } - - if formatOptions.contains(.withDay) { - if formatOptions.contains(.withWeekOfYear) { - format += "FF" - } else if formatOptions.contains(.withMonth) { - format += "dd" - } else if formatOptions.contains(.withYear) { - if formatOptions.contains(.withDashSeparatorInDate) { - format += "-DDD" - } else { - format += "DDD" - } - } else { - // not valid - } - } - } - - let hasDate = (formatOptions.contains(.withFullDate) || formatOptions.contains(.withMonth) || formatOptions.contains(.withDay) || formatOptions.contains(.withWeekOfYear) || formatOptions.contains(.withYear)) - if hasDate && (formatOptions.contains(.withFullTime) || formatOptions.contains(.withTimeZone)) { - if formatOptions.contains(.withSpaceBetweenDateAndTime) { - format += " " - } else { - format += "'T'" - } - } - - if formatOptions.contains(.withFullTime) { - format += "HH:mm:ssZZZZZ" - } else { - if formatOptions.contains(.withTime) { - format += "HH:mm:ss" - } - if formatOptions.contains(.withTimeZone) { - format += "ZZZZZ" - } - } - - return format + + /// Creates a representation of the specified date with a given time zone and format options. + /// + /// - Parameters: + /// - date: The date to be represented. + /// - options: The options used. For possible values, see ISO8601DateTimeFormatter.Options. + /// - returns: A user-readable string representing the date. + public class func string(from date: Date, options: ISO8601DateTimeFormatter.Options = []) -> String { + let formatter = ISO8601DateTimeFormatter() + formatter.locale = LocaleName.englishUnitedStatesComputer.locale // fix for 12/24h + return formatter.string(from: date, options: options) } } diff --git a/Sources/SwiftDate/ISO8601Parser.swift b/Sources/SwiftDate/ISO8601Parser.swift new file mode 100644 index 00000000..9fc35394 --- /dev/null +++ b/Sources/SwiftDate/ISO8601Parser.swift @@ -0,0 +1,880 @@ +// +// SwiftDate, Full featured Swift date library for parsing, validating, manipulating, and formatting dates and timezones. +// Created by: Daniele Margutti +// Main contributors: Jeroen Houtzager +// +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + + +/// This defines all possible errors you can encounter parsing ISO8601 string +/// +/// - eof: end of file +/// - notDigit: expected digit, value cannot be parsed as int +/// - notDouble: expected double digit, value cannot be parsed as double +/// - invalid: invalid state reached. Something in the format is not correct +public enum ISO8601ParserError: Error { + case eof + case notDigit + case notDouble + case invalid +} + + +// MARK: - Internal Extension for UnicodeScalar type + +internal extension UnicodeScalar { + + /// return `true` if current character is a digit (arabic), `false` otherwise + var isDigit: Bool { + return "0"..."9" ~= self + } + + /// return `true` if current character is a space + var isSpace: Bool { + return CharacterSet.whitespaces.contains(self) + } + +} + + +// MARK: - Internal Extension for Int type + +internal extension Int { + + /// Return `true` if current year is a leap year, `false` otherwise + var isLeapYear: Bool { + return ((self % 4) == 0) && (((self % 100) != 0) || ((self % 400) == 0)) + } + +} + + +/// Parser configuration +/// This configuration can be used to define custom behaviour of the parser itself. +public struct ISO8601Configuration { + + /// Time separator character. By default is `:`. + var time_separator: ISO8601Parser.ISOChar = ":" + + /// Strict parsing. By default is `false`. + var strict: Bool = false + + /// Calendar used to generate the date. By default is the current system calendar + var calendar: Calendar = Calendar.current + + init(strict: Bool = false, calendar: Calendar? = nil) { + self.strict = strict + self.calendar = calendar ?? Calendar.current + } +} + + +/// Internal structure +internal enum Weekday: Int { + case monday = 0 + case tuesday = 1 + case wednesday = 2 + case thursday = 3 +} + + +/// This is the ISO8601 Parser class: it evaluates automatically the format of the ISO8601 date +/// and attempt to parse it in a valid `Date` object. +/// Resulting date also includes Time Zone settings and a property which allows you to inspect +/// single date components. +/// +/// This work is inspired to the original ISO8601DateFormatter class written in ObjC by +/// Peter Hosey (available here https://bitbucket.org/boredzo/iso-8601-parser-unparser). +/// I've made a Swift porting and fixed some issues when parsing several ISO8601 date variants. +public class ISO8601Parser { + + /// Some typealias to make the code cleaner + public typealias ISOString = String.UnicodeScalarView + public typealias ISOIndex = String.UnicodeScalarView.Index + public typealias ISOChar = UnicodeScalar + + + /// This represent the internal parser status representation + public struct ParsedDate { + + /// Type of date parsed + /// + /// - monthAndDate: month and date style + /// - week: date with week number + /// - dateOnly: date only + public enum DateStyle { + case monthAndDate + case week + case dateOnly + } + + /// Parsed year value + var year: Int = 0 + + /// Parsed month or week number + var month_or_week: Int = 0 + + /// Parsed day value + var day: Int = 0 + + /// Parsed hour value + var hour: Int = 0 + + /// Parsed minutes value + var minute: TimeInterval = 0.0 + + /// Parsed seconds value + var seconds: TimeInterval = 0.0 + + /// Parsed weekday number (1=monday, 7=sunday) + /// If `nil` source string has not specs about weekday. + var weekday: Int? = nil + + + /// Timezone parsed hour value + var tz_hour: Int = 0 + + /// Timezone parsed minute value + var tz_minute: Int = 0 + + /// Type of parsed date + var type: DateStyle = .monthAndDate + + /// Parsed timezone object + var timezone: TimeZone? + } + + + /// Source raw parsed values + private var date: ParsedDate = ParsedDate() + + /// Source string represented as unicode scalars + private let string: ISOString + + /// Current position of the parser in source string. + /// Initially is equal to `string.startIndex` + private var cIdx: ISOIndex + + /// Just a shortcut to the last index in source string + private var eIdx: ISOIndex + + /// Lenght of the string + private var length: Int + + /// Number of hyphens characters found before any value + /// Consequential "-" are used to define implicit values in dates. + private var hyphens: Int = 0 + + /// Private date components used for default values + private var now_cmps: DateComponents + + /// Configuration used for parser + private var cfg: ISO8601Configuration + + /// Date components parsed + private(set) var date_components: DateComponents? + + /// Parsed date + private(set) var parsedDate: Date? + + /// Formatter used to transform a date to a valid ISO8601 string + private(set) var formatter: DateFormatter = DateFormatter() + + /// Initialize a new parser with a source ISO8601 string to parse + /// Parsing is done during initialization; any exception is reported + /// before allocating. + /// + /// - Parameters: + /// - src: source ISO8601 string + /// - config: configuration used for parsing + /// - Throws: throw an `ISO8601Error` if parsing operation fails + public init(_ src: String, config: ISO8601Configuration = ISO8601Configuration()) throws { + let src_trimmed = src.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + guard src_trimmed.characters.count > 0 else { + throw ISO8601ParserError.invalid + } + self.string = src_trimmed.unicodeScalars + self.length = src_trimmed.characters.count + self.cIdx = string.startIndex + self.eIdx = string.endIndex + self.cfg = config + self.now_cmps = cfg.calendar.dateComponents([.year,.month,.day], from: Date()) + + var idx = self.cIdx + while idx < self.eIdx { + if string[idx] == "-" { hyphens += 1 } + else { break } + idx = string.index(after: idx) + } + + try self.parse() + } + + + /// Return a date parsed from a valid ISO8601 string + /// + /// - Parameter string: source string + /// - Returns: a valid `Date` object or `nil` if date cannot be parsed + public static func date(from string: String) -> Date? { + do { + return try ISO8601Parser(string).parsedDate + } catch { + return nil + } + } + + //MARK: - Internal Parser + + /// Private parsing function + /// + /// - Throws: throw an `ISO8601Error` if parsing operation fails + private func parse() throws { + + // PARSE DATE + + if current() == "T" { + // There is no date here, only a time. + // Set the date to now; then we'll parse the time. + next() + guard current().isDigit else { + throw ISO8601ParserError.invalid + } + + date.year = now_cmps.year! + date.month_or_week = now_cmps.month! + date.day = now_cmps.day! + } else { + moveUntil(is: "-") + + var (num_digits,segment) = try read_int() + switch num_digits { + case 0: try parse_digits_0(num_digits, &segment) + case 8: try parse_digits_8(num_digits, &segment) + case 6: try parse_digits_6(num_digits, &segment) + case 4: try parse_digits_4(num_digits, &segment) + case 1: try parse_digits_1(num_digits, &segment) + case 2: try parse_digits_2(num_digits, &segment) + case 7: try parse_digits_7(num_digits, &segment) //YYYY DDD (ordinal date) + case 3: try parse_digits_3(num_digits, &segment) //--DDD (ordinal date, implicit year) + default: throw ISO8601ParserError.invalid + } + } + + var hasTime = false + if current().isSpace || current() == "T" { + hasTime = true + next() + } + + // PARSE TIME + + if current().isDigit == true { + let time_sep = cfg.time_separator + let hasTimeSeparator = string.contains(time_sep) + + date.hour = try read_int(2).value + + if hasTimeSeparator == false && hasTime{ + date.minute = TimeInterval(try read_int(2).value) + } + else if current() == time_sep { + next() + + if time_sep == "," || time_sep == "." { + //We can't do fractional minutes when '.' is the segment separator. + //Only allow whole minutes and whole seconds. + date.minute = TimeInterval(try read_int(2).value) + if current() == time_sep { + next() + date.seconds = TimeInterval(try read_int(2).value) + } + } else { + //Allow a fractional minute. + //If we don't get a fraction, look for a seconds segment. + //Otherwise, the fraction of a minute is the seconds. + date.minute = try read_double().value + + if current() != ":" { + var int_part: Double = 0.0 + var frac_part: Double = 0.0 + frac_part = modf(date.minute, &int_part) + date.minute = int_part + date.seconds = frac_part + if date.seconds > Double.ulpOfOne { + // Convert fraction (e.g. .5) into seconds (e.g. 30). + date.seconds = date.seconds * 60 + } else if current() == time_sep { + next() + date.seconds = try read_double().value + } + } else { + // fractional minutes + next() + date.seconds = try read_double().value + } + } + } + + if cfg.strict == false { + if current().isSpace == true { + next() + } + } + + switch current() { + case "Z": + date.timezone = TimeZone(abbreviation: "UTC") + + case "+","-": + let is_negative = current() == "-" + next() + if current().isDigit == true { + //Read hour offset. + date.tz_hour = try read_int(2).value + if is_negative == true { date.tz_hour = -date.tz_hour } + + // Optional separator + if current() == time_sep { + next() + } + + if current().isDigit { + // Read minute offset + date.tz_minute = try read_int(2).value + if is_negative == true { date.tz_minute = -date.tz_minute } + } + + let timezone_offset = (date.tz_hour * 3600) + (date.tz_minute * 60) + date.timezone = TimeZone(secondsFromGMT: timezone_offset) + } + default: + break + } + } + + + self.date_components = DateComponents() + self.date_components!.year = date.year + self.date_components!.day = date.day + self.date_components!.hour = date.hour + self.date_components!.minute = Int(date.minute) + self.date_components!.second = Int(date.seconds) + + switch date.type { + case .monthAndDate: + self.date_components!.month = date.month_or_week + case .week: + //Adapted from . + //This works by converting the week date into an ordinal date, then letting the next case handle it. + let prevYear = date.year - 1 + let YY = prevYear % 100 + let C = prevYear - YY + let G = YY + YY / 4 + let isLeapYear = (((C / 100) % 4) * 5) + let Jan1Weekday = ((isLeapYear + G) % 7) + + var day = ((8 - Jan1Weekday) + (7 * (Jan1Weekday > Weekday.thursday.rawValue ? 1 : 0))) + day += (date.day - 1) + (7 * (date.month_or_week - 2)) + + if let weekday = date.weekday { + //self.date_components!.weekday = weekday + self.date_components!.day = day + weekday + } else { + self.date_components!.day = day + } + case .dateOnly: //An "ordinal date". + break + + } + + self.cfg.calendar.timeZone = date.timezone ?? TimeZone(identifier: "UTC")! + self.parsedDate = self.cfg.calendar.date(from: self.date_components!) + + } + + private func parse_digits_3(_ num_digits: Int, _ segment: inout Int) throws { + //Technically, the standard only allows one hyphen. But it says that two hyphens is the logical implementation, and one was dropped for brevity. So I have chosen to allow the missing hyphen. + if hyphens < 1 || (hyphens > 2 && cfg.strict == false) { + throw ISO8601ParserError.invalid + } + + date.day = segment + date.year = now_cmps.year! + date.type = .dateOnly + if cfg.strict == true && (date.day > (365 + (date.year.isLeapYear ? 1 : 0))) { + throw ISO8601ParserError.invalid + } + } + + private func parse_digits_7(_ num_digits: Int, _ segment: inout Int) throws { + guard hyphens == 0 else { throw ISO8601ParserError.invalid } + + date.day = segment % 1000 + date.year = segment / 1000 + date.type = .dateOnly + if cfg.strict == true && (date.day > (365 + (date.year.isLeapYear ? 1 : 0))) { + throw ISO8601ParserError.invalid + } + } + + private func parse_digits_2(_ num_digits: Int, _ segment: inout Int) throws { + + func parse_hyphens_3(_ num_digits: Int, _ segment: inout Int) throws { + date.year = now_cmps.year! + date.month_or_week = now_cmps.month! + date.day = segment + } + + func parse_hyphens_2(_ num_digits: Int, _ segment: inout Int) throws { + date.year = now_cmps.year! + date.month_or_week = segment + if current() == "-" { + next() + date.day = try read_int(2).value + } + } + + func parse_hyphens_1(_ num_digits: Int, _ segment: inout Int) throws { + let current_year = now_cmps.year! + let current_century = (current_year % 100) + date.year = segment + (current_year - current_century) + if num_digits == 1 { // implied decade + date.year += current_century - (current_year % 10) + } + + if current() == "-" { + next() + if current() == "W" { + next() + date.type = .week + } + date.month_or_week = try read_int(2).value + + if current() == "-" { + next() + if date.type == .week { + // weekday number + let weekday = try read_int().value + if weekday > 7 { + throw ISO8601ParserError.invalid + } + date.weekday = weekday + } else { + date.day = try read_int().value + } + } else { + date.day = 1 + } + } else { + date.month_or_week = 1 + date.day = 1 + } + } + + func parse_hyphens_0(_ num_digits: Int, _ segment: inout Int) throws { + if current() == "-" { + // Implicit century + date.year = now_cmps.year! + date.year -= (date.year % 100) + date.year += segment + + next() + if current() == "W" { + try parseWeekAndDay() + } else if current().isDigit == false { + try centuryOnly(&segment) + } else { + // Get month and/or date. + let (v_count,v_seg) = try read_int() + switch v_count { + case 4: // YY-MMDD + date.day = v_seg % 100 + date.month_or_week = v_seg / 100 + case 1: // YY-M; YY-M-DD (extension) + if cfg.strict == true { + throw ISO8601ParserError.invalid + } + case 2: // YY-MM; YY-MM-DD + date.month_or_week = v_seg + if current() == "-" { + next() + if current().isDigit == true { + date.day = try read_int(2).value + } else { + date.day = 1 + } + } else { + date.day = 1 + } + case 3: // Ordinal date + date.day = v_seg + date.type = .dateOnly + default: + break + } + } + } else if current() == "W" { + date.year = now_cmps.year! + date.year -= (date.year % 100) + date.year += segment + + try parseWeekAndDay() + } else { + try centuryOnly(&segment) + } + } + + switch hyphens { + case 0: try parse_hyphens_0(num_digits, &segment) + case 1: try parse_hyphens_1(num_digits, &segment) //-YY; -YY-MM (implicit century) + case 2: try parse_hyphens_2(num_digits, &segment) //--MM; --MM-DD + case 3: try parse_hyphens_3(num_digits, &segment) //---DD + default: throw ISO8601ParserError.invalid + } + } + + private func parse_digits_1(_ num_digits: Int, _ segment: inout Int) throws { + if cfg.strict == true { + // Two digits only - never just one. + guard hyphens == 1 else { throw ISO8601ParserError.invalid } + if current() == "-" { + next() + } + next() + guard current() == "W" else { throw ISO8601ParserError.invalid } + + date.year = now_cmps.year! + date.year -= (date.year % 10) + date.year += segment + } else { + try parse_digits_2(num_digits, &segment) + } + } + + private func parse_digits_4(_ num_digits: Int, _ segment: inout Int) throws { + + func parse_hyphens_0(_ num_digits: Int, _ segment: inout Int) throws { + date.year = segment + if current() == "-" { + next() + } + + if current().isDigit == false { + if current() == "W" { + try parseWeekAndDay() + } else { + date.month_or_week = 1 + date.day = 1 + } + } else { + let (v_num,v_seg) = try read_int() + switch v_num { + case 4: // MMDD + date.day = v_seg % 100 + date.month_or_week = v_seg / 100 + case 2: // MM + date.month_or_week = v_seg + + if current() == "-" { + next() + } + if current().isDigit == false { + date.day = 1 + } else { + date.day = try read_int().value + } + case 3: // DDD + date.day = v_seg % 1000 + date.type = .dateOnly + if cfg.strict == true && (date.day > 365 + (date.year.isLeapYear ? 1 : 0)) { + throw ISO8601ParserError.invalid + } + default: + throw ISO8601ParserError.invalid + } + } + } + + func parse_hyphens_1(_ num_digits: Int, _ segment: inout Int) throws { + date.month_or_week = segment % 100 + date.year = segment / 100 + + if current() == "-" { + next() + } + if current().isDigit == false { + date.day = 1 + } else { + date.day = try read_int().value + } + } + + func parse_hyphens_2(_ num_digits: Int, _ segment: inout Int) throws { + date.day = segment % 100 + date.month_or_week = segment / 100 + date.year = now_cmps.year! + } + + switch hyphens { + case 0: try parse_hyphens_0(num_digits, &segment) // YYYY + case 1: try parse_hyphens_1(num_digits, &segment) // YYMM + case 2: try parse_hyphens_2(num_digits, &segment) // MMDD + default: throw ISO8601ParserError.invalid + } + + } + + private func parse_digits_6(_ num_digits: Int, _ segment: inout Int) throws { + // YYMMDD (implicit century) + guard hyphens == 0 else { throw ISO8601ParserError.invalid } + + date.day = segment % 100 + segment /= 100 + date.month_or_week = segment % 100 + date.year = now_cmps.year! + date.year -= (date.year % 100) + date.year += (segment / 100) + } + + private func parse_digits_8(_ num_digits: Int, _ segment: inout Int) throws { + // YYYY MM DD + guard hyphens == 0 else { + throw ISO8601ParserError.invalid + } + + date.day = segment % 100 + segment /= 100 + date.month_or_week = segment % 100 + date.year = segment / 100 + } + + private func parse_digits_0(_ num_digits: Int, _ segment: inout Int) throws { + guard current() == "W" else { + throw ISO8601ParserError.invalid + } + + if seek(1) == "-" && isDigit(seek(2)) && + ((hyphens == 1 || hyphens == 2) && cfg.strict == false) { + + date.year = now_cmps.year! + date.month_or_week = 1 + next(2) + try parseDayAfterWeek() + } else if hyphens == 1 { + date.year = now_cmps.year! + try parseDayAfterWeek() + } else { + throw ISO8601ParserError.invalid + } + } + + private func parseWeekAndDay() throws { + next() + if current().isDigit == false { + //Not really a week-based date; just a year followed by '-W'. + guard cfg.strict == false else { + throw ISO8601ParserError.invalid + } + date.month_or_week = 1 + date.day = 1 + } else { + date.month_or_week = try read_int(2).value + if current() == "-" { + next() + } + let weekday = try read_int().value + if weekday > 7 { + throw ISO8601ParserError.invalid + } + date.type = .week + date.weekday = weekday + // try parseDayAfterWeek() + } + } + + private func parseDayAfterWeek() throws { + date.day = current().isDigit == true ? try read_int(2).value : 1 + date.type = .week + } + + private func centuryOnly(_ segment: inout Int) throws { + date.year = segment * 100 + now_cmps.year! % 100 + date.month_or_week = 1 + date.day = 1 + } + + + /// Return `true` if given character is a char + /// + /// - Parameter char: char to evaluate + /// - Returns: `true` if char is a digit, `false` otherwise + private func isDigit(_ char: UnicodeScalar?) -> Bool { + guard let char = char else { return false } + return char.isDigit + } + + /// MARK: - Scanner internal functions + + + /// Get the value at specified offset from current scanner position without + /// moving the current scanner's index. + /// + /// - Parameter offset: offset to move + /// - Returns: char at given position, `nil` if not found + @discardableResult + public func seek(_ offset: Int = 1) -> ISOChar? { + let move_idx = string.index(cIdx, offsetBy: offset) + guard move_idx < eIdx else { + return nil + } + return string[move_idx] + } + + /// Return the char at the current position of the scanner + /// + /// - Parameter next: if `true` return the current char and move to the next position + /// - Returns: the char sat the current position of the scanner + @discardableResult + public func current(_ next: Bool = false) -> ISOChar { + let current = string[cIdx] + if next == true { cIdx = string.index(after: cIdx) } + return current + } + + /// Move by `offset` characters the index of the scanner and return the char at the current + /// position. If EOF is reached `nil` is returned. + /// + /// - Parameter offset: offset value (use negative number to move backwards) + /// - Returns: character at the current position. + @discardableResult + private func next(_ offset: Int = 1) -> ISOChar? { + let next = string.index(cIdx, offsetBy: offset) + guard next < eIdx else { + return nil + } + cIdx = next + return string[cIdx] + } + + + /// Read from the current scanner index and parse the value as Int. + /// + /// - Parameter max_count: number of characters to move. If nil scanners continues until a non + /// digit value is encountered. + /// - Returns: parsed value + /// - Throws: throw an exception if parser fails + @discardableResult + private func read_int(_ max_count: Int? = nil) throws -> (count: Int, value: Int) { + var move_idx = cIdx + var count = 0 + while move_idx < eIdx { + if let max = max_count, count >= max { break } + if string[move_idx].isDigit == false { break } + count += 1 + move_idx = string.index(after: move_idx) + } + + let raw_value = String(string[cIdx.. (count: Int, value: Double) { + var move_idx = cIdx + var count = 0 + var fractional_start = false + while move_idx < eIdx { + let char = string[move_idx] + if char == "." || char == "," { + if fractional_start == true { throw ISO8601ParserError.notDouble } + else { fractional_start = true } + } else { + if char.isDigit == false { break } + } + count += 1 + move_idx = string.index(after: move_idx) + } + + let raw_value = String(string[cIdx.. Int { + var move_idx = cIdx + var count = 0 + while move_idx < eIdx { + guard string[move_idx] == char else { break } + move_idx = string.index(after: move_idx) + count += 1 + } + cIdx = move_idx + return count + } + + + /// Move the current scanner index to the next position until passed `char` value is + /// encountered or `eof` is reached. + /// + /// - Parameter char: char + /// - Returns: the number of characters passed + @discardableResult + private func moveUntil(isNot char: UnicodeScalar) -> Int { + var move_idx = cIdx + var count = 0 + while move_idx < eIdx { + guard string[move_idx] != char else { break } + move_idx = string.index(after: move_idx) + count += 1 + } + cIdx = move_idx + return count + } + +} diff --git a/Sources/SwiftDate/SwiftDate.bundle/de-AT.lproj/SwiftDate.strings b/Sources/SwiftDate/SwiftDate.bundle/de-AT.lproj/SwiftDate.strings new file mode 100644 index 00000000..770999c1 --- /dev/null +++ b/Sources/SwiftDate/SwiftDate.bundle/de-AT.lproj/SwiftDate.strings @@ -0,0 +1,56 @@ +// COLLOQUIAL STRINGD +"colloquial_f_y" = "nächstes Jahr"; // year,future,singular: "next year" +"colloquial_f_yy" = "in %d"; // year,future,plural: "on 2016" +"colloquial_p_y" = "letztes JAHR"; // year,past,singular: "last year" +"colloquial_p_yy" = "%d"; // year,past,plural: "2015" + +"colloquial_f_m" = "nächster Monat"; // month,future,singular: "next month" +"colloquial_f_mm" = "in %d Monaten"; // month,future,plural: "in 3 months" +"colloquial_p_m" = "letzten Monat"; // month,past,singular: "past month" +"colloquial_p_mm" = "vor %d Monaten"; // month,past,plural: "3 months ago" + +"colloquial_f_w" = "nächste Woche"; // week,future,singular: "next week" +"colloquial_f_ww" = "in %d Wochen"; // week,future,plural: "in 3 weeks" +"colloquial_p_w" = "letzte Woche"; // week,past,singular: "past week" +"colloquial_p_ww" = "vor %d Wochen"; // week,past,plural: "in 3 weeks" + +"colloquial_f_d" = "morgen"; // day,future,singular: "tomorrow" +"colloquial_f_dd" = "in %d Tagen"; // day,future,plural: "in 3 days" +"colloquial_p_d" = "gestern"; // day,past,singular: "yesterday" +"colloquial_p_dd" = "vor %d Tagen"; // day,past,plural: "3 days ago" + +"colloquial_f_h" = "in einer Stunde"; // hour,future,singular: "in one hour" +"colloquial_f_hh" = "in %d Stunden"; // hour,future,plural: "in 3 hours" +"colloquial_p_h" = "vor einer Stunde"; // hour,past,singular: "one hour ago" +"colloquial_p_hh" = "vor %d Stunden"; // hour,past,plural: "3 hours ago" + +"colloquial_f_M" = "in einer Minute"; // minute,future,singular: "in one minute" +"colloquial_f_MM" = "in %d Minuten"; // minute,future,plural: "in 3 minutes" +"colloquial_p_M" = "vor einer Minute"; // minute,past,singular: "one minute ago" +"colloquial_p_MM" = "vor %d Minuten"; // minute,past,plural: "3 minutes ago" + +"colloquial_now" = "gerade eben"; // less than 5 minutes if .allowsNowOnColloquial is set + +"colloquial_n_0y" = "dieses Jahr"; // this year +"colloquial_n_0m" = "diesen Monat"; // this month +"colloquial_n_0w" = "diese Woche"; // this week +"colloquial_n_0d" = "heute"; // this day +"colloquial_n_0h" = "gerade eben"; // this hour +"colloquial_n_0M" = "gerade eben"; // this minute +"colloquial_n_0s" = "gerade eben"; // this second + +// RELEVANT TIME TO PRINT ALONG COLLOQUIAL STRING WHEN .includeRelevantTime = true +"relevanttime_y" = "MMM yyyy"; // for colloquial year (=+-1) adds a time string like this:"(Feb 2016)" +"relevanttime_yy" = "MMM yyyy"; // for colloquial years (>1) adds a time string like this:"(Feb 2016)" +"relevanttime_m" = "MMM, dd yyyy"; // for colloquial month (=+-1) adds a time string like this:"(Feb 17, 2016)" +"relevanttime_mm" = "MMM, dd yyyy"; // for colloquial months (>1) adds a time string like this: "(Feb 17, 2016)" +"relevanttime_w" = "EEE, MMM dd"; // for colloquial months (>1) adds a time string like this: "(Wed Feb 17)" +"relevanttime_ww" = "EEE, MMM dd"; // for colloquial months (>1) adds a time string like this: "(Wed Feb 17)" +"relevanttime_d" = "EEE, MMM dd"; // for colloquial day (=+-1) adds a time string like this: "(Wed Feb 17)" +"relevanttime_dd" = "EEE, MMM dd"; // for colloquial days (>1) adds a time string like this: "(Wed Feb 17)" +"relevanttime_h" = "'at' HH:mm"; // for colloquial day (=+-1) adds a time string like this: "(At 13:20)" +"relevanttime_hh" = "'at' HH:mm"; // for colloquial days (>1) adds a time string like this: "(At 13:20)" +"relevanttime_M" = ""; // for colloquial minute(s) we have not any relevant time to print +"relevanttime_MM" = ""; // for colloquial minute(s) we have not any relevant time to print +"relevanttime_s" = ""; // for colloquial seconds(s) we have not any relevant time to print +"relevanttime_ss" = ""; // for colloquial seconds(s) we have not any relevant time to print diff --git a/Sources/SwiftDate/SwiftDate.bundle/el_GR.lproj/SwiftDate.strings b/Sources/SwiftDate/SwiftDate.bundle/el_GR.lproj/SwiftDate.strings new file mode 100644 index 00000000..8bb0eccb --- /dev/null +++ b/Sources/SwiftDate/SwiftDate.bundle/el_GR.lproj/SwiftDate.strings @@ -0,0 +1,56 @@ +// COLLOQUIAL STRINGD +"colloquial_f_y" = "επόμενο έτος"; // year,future,singular: "next year" +"colloquial_f_yy" = "το %d"; // year,future,plural: "on 2016" +"colloquial_p_y" = "πέρυσι"; // year,past,singular: "last year" +"colloquial_p_yy" = "%d"; // year,past,plural: "2015" + +"colloquial_f_m" = "έπομενος μήνας"; // month,future,singular: "next month" +"colloquial_f_mm" = "σε %d μήνες"; // month,future,plural: "in 3 months" +"colloquial_p_m" = "προηγούμενος μήνας"; // month,past,singular: "past month" +"colloquial_p_mm" = "πριν %d μήνες"; // month,past,plural: "3 months ago" + +"colloquial_f_w" = "επόμενη εβδομάδα"; // week,future,singular: "next week" +"colloquial_f_ww" = "σε %d εβδομάδες"; // week,future,plural: "in 3 weeks" +"colloquial_p_w" = "προηγούμενη εβδομάδα"; // week,past,singular: "past week" +"colloquial_p_ww" = "πριν %d εβδομάδες"; // week,past,plural: "in 3 weeks" + +"colloquial_f_d" = "αύριο"; // day,future,singular: "tomorrow" +"colloquial_f_dd" = "σε %d μέρες"; // day,future,plural: "in 3 days" +"colloquial_p_d" = "χθές"; // day,past,singular: "yesterday" +"colloquial_p_dd" = "πριν %d μέρες"; // day,past,plural: "3 days ago" + +"colloquial_f_h" = "σε μία ώρα"; // hour,future,singular: "in one hour" +"colloquial_f_hh" = "σε %d ώρες"; // hour,future,plural: "in 3 hours" +"colloquial_p_h" = "πριν μία ώρα"; // hour,past,singular: "one hour ago" +"colloquial_p_hh" = "πριν %d ώρες"; // hour,past,plural: "3 hours ago" + +"colloquial_f_M" = "σε ένα λεπτό"; // minute,future,singular: "in one minute" +"colloquial_f_MM" = "σε %d λεπτά"; // minute,future,plural: "in 3 minutes" +"colloquial_p_M" = "πριν ένα λεπτό"; // minute,past,singular: "one minute ago" +"colloquial_p_MM" = "πριν %d λεπτά"; // minute,past,plural: "3 minutes ago" + +"colloquial_now" = "μόλις τώρα"; // less than 5 minutes if .allowsNowOnColloquial is set + +"colloquial_n_0y" = "φέτος"; // this year +"colloquial_n_0m" = "αυτόν το μήνα"; // this month +"colloquial_n_0w" = "αυτή την εβδομάδα"; // this week +"colloquial_n_0d" = "σήμερα"; // this day +"colloquial_n_0h" = "τώρα"; // this hour +"colloquial_n_0M" = "τώρα"; // this minute +"colloquial_n_0s" = "τώρα"; // this second + +// RELEVANT TIME TO PRINT ALONG COLLOQUIAL STRING WHEN .includeRelevantTime = true +"relevanttime_y" = "MMM yyyy"; // for colloquial year (=+-1) adds a time string like this:"(Feb 2016)" +"relevanttime_yy" = "MMM yyyy"; // for colloquial years (>1) adds a time string like this:"(Feb 2016)" +"relevanttime_m" = "MMM, dd yyyy"; // for colloquial month (=+-1) adds a time string like this:"(Feb 17, 2016)" +"relevanttime_mm" = "MMM, dd yyyy"; // for colloquial months (>1) adds a time string like this: "(Feb 17, 2016)" +"relevanttime_w" = "EEE, MMM dd"; // for colloquial months (>1) adds a time string like this: "(Wed Feb 17)" +"relevanttime_ww" = "EEE, MMM dd"; // for colloquial months (>1) adds a time string like this: "(Wed Feb 17)" +"relevanttime_d" = "EEE, MMM dd"; // for colloquial day (=+-1) adds a time string like this: "(Wed Feb 17)" +"relevanttime_dd" = "EEE, MMM dd"; // for colloquial days (>1) adds a time string like this: "(Wed Feb 17)" +"relevanttime_h" = "'at' HH:mm"; // for colloquial day (=+-1) adds a time string like this: "(At 13:20)" +"relevanttime_hh" = "'at' HH:mm"; // for colloquial days (>1) adds a time string like this: "(At 13:20)" +"relevanttime_M" = ""; // for colloquial minute(s) we have not any relevant time to print +"relevanttime_MM" = ""; // for colloquial minute(s) we have not any relevant time to print +"relevanttime_s" = ""; // for colloquial seconds(s) we have not any relevant time to print +"relevanttime_ss" = ""; // for colloquial seconds(s) we have not any relevant time to print diff --git a/Sources/SwiftDate/SwiftDate.bundle/sv-SE.lproj/SwiftDate.strings b/Sources/SwiftDate/SwiftDate.bundle/sv-SE.lproj/SwiftDate.strings index e2f96a84..d973550b 100644 --- a/Sources/SwiftDate/SwiftDate.bundle/sv-SE.lproj/SwiftDate.strings +++ b/Sources/SwiftDate/SwiftDate.bundle/sv-SE.lproj/SwiftDate.strings @@ -14,10 +14,10 @@ "colloquial_p_w" = "förra veckan"; // week,past,singular: "past week" "colloquial_p_ww" = "%d veckor sedan"; // week,past,plural: "in 3 weeks" -"colloquial_f_d" = "tomorrow"; // day,future,singular: "tomorrow" -"colloquial_f_dd" = "in %d days"; // day,future,plural: "in 3 days" -"colloquial_p_d" = "yesterday"; // day,past,singular: "yesterday" -"colloquial_p_dd" = "%d days ago"; // day,past,plural: "3 days ago" +"colloquial_f_d" = "imorgon"; // day,future,singular: "tomorrow" +"colloquial_f_dd" = "om %d dagar"; // day,future,plural: "in 3 days" +"colloquial_p_d" = "igår"; // day,past,singular: "yesterday" +"colloquial_p_dd" = "%d dagar sedan"; // day,past,plural: "3 days ago" "colloquial_f_h" = "om en timme"; // hour,future,singular: "in one hour" "colloquial_f_hh" = "om %d timmar"; // hour,future,plural: "in 3 hours" @@ -31,6 +31,14 @@ "colloquial_now" = "just nu"; // less than 5 minutes if .allowsNowOnColloquial is set +"colloquial_n_0y" = "detta året"; // this year +"colloquial_n_0m" = "denna månaden"; // this month +"colloquial_n_0w" = "denna veckan"; // this week +"colloquial_n_0d" = "idag"; // this day +"colloquial_n_0h" = "just nu"; // this hour +"colloquial_n_0M" = "just nu"; // this minute +"colloquial_n_0s" = "just nu"; // this second + // RELEVANT TIME TO PRINT ALONG COLLOQUIAL STRING WHEN .includeRelevantTime = true "relevanttime_y" = "MMM yyyy"; // for colloquial year (=+-1) adds a time string like this:"(Feb 2016)" "relevanttime_yy" = "MMM yyyy"; // for colloquial years (>1) adds a time string like this:"(Feb 2016)" diff --git a/Sources/SwiftDate/TimeInterval+Extensions.swift b/Sources/SwiftDate/TimeInterval+Extensions.swift index 8f8f780f..35a088a0 100644 --- a/Sources/SwiftDate/TimeInterval+Extensions.swift +++ b/Sources/SwiftDate/TimeInterval+Extensions.swift @@ -58,28 +58,6 @@ public extension TimeInterval { let value = cal.calendar.dateComponents(components, from: dateFrom, to: dateTo).value(for: component) return value } - - /// Represent a time interval in a string - /// - /// - parameter unitStyle: unit style of the output - /// - parameter max: max number of components to print - /// - parameter zero: how to threat wuth zero values - /// - parameter separator: separator between components - /// - parameter locale: locale to use - /// - /// - throws: throw an exception if output string cannot be produced - /// - /// - returns: a string representing the time interval - @available(*, deprecated: 4.0.3, message: "Use string(options:) instead") - public func string(unitStyle: DateComponentsFormatter.UnitsStyle = .short, max: Int? = nil, zero: DateZeroBehaviour? = nil, separator: String? = nil, locale: Locale? = nil) throws -> String? { - let formatter = DateInRegionFormatter() - formatter.localization = Localization(locale: locale) - formatter.maxComponentCount = max - formatter.unitStyle = unitStyle - formatter.zeroBehavior = zero ?? .dropAll - formatter.unitSeparator = separator ?? "," - return try formatter.timeComponents(interval: self) - } /// Represent a time interval in a string /// diff --git a/SwiftDate.podspec b/SwiftDate.podspec index 69e265ed..ac7f9375 100644 --- a/SwiftDate.podspec +++ b/SwiftDate.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'SwiftDate' - spec.version = '4.0.14' + spec.version = '4.1.0' spec.summary = 'The best way to deal with Dates & Time Zones in Swift' spec.homepage = 'https://github.com/malcommac/SwiftDate' spec.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/SwiftDate/SwiftDate.xcodeproj/project.pbxproj b/SwiftDate/SwiftDate.xcodeproj/project.pbxproj index 5a127b48..27b55e10 100644 --- a/SwiftDate/SwiftDate.xcodeproj/project.pbxproj +++ b/SwiftDate/SwiftDate.xcodeproj/project.pbxproj @@ -107,6 +107,8 @@ 08EC22CB1DA03E6600B6DFC6 /* TimeZoneName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08EC22C11DA03E6600B6DFC6 /* TimeZoneName.swift */; }; 08EC22CC1DA03E6600B6DFC6 /* TimeZoneName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08EC22C11DA03E6600B6DFC6 /* TimeZoneName.swift */; }; 08EC22CD1DA03E6600B6DFC6 /* TimeZoneName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08EC22C11DA03E6600B6DFC6 /* TimeZoneName.swift */; }; + 213A2A641E8D2A2E00408313 /* ISO8601Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 213A2A631E8D2A2E00408313 /* ISO8601Parser.swift */; }; + 213A2A661E8D3E2700408313 /* valid_ISO8601_strings.txt in Resources */ = {isa = PBXBuildFile; fileRef = 213A2A651E8D3E2700408313 /* valid_ISO8601_strings.txt */; }; 21C3012E1D829D7900B0E02C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21C3012D1D829D7900B0E02C /* AppDelegate.swift */; }; 21C301301D829D7900B0E02C /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21C3012F1D829D7900B0E02C /* ViewController.swift */; }; 21C301331D829D7900B0E02C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 21C301311D829D7900B0E02C /* Main.storyboard */; }; @@ -194,6 +196,8 @@ 08EC22C11DA03E6600B6DFC6 /* TimeZoneName.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TimeZoneName.swift; path = ../../Sources/SwiftDate/TimeZoneName.swift; sourceTree = ""; }; 08EC22EF1DA03F9D00B6DFC6 /* SwiftDateTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftDateTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 08EC22F31DA03F9D00B6DFC6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 213A2A631E8D2A2E00408313 /* ISO8601Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ISO8601Parser.swift; path = ../../Sources/SwiftDate/ISO8601Parser.swift; sourceTree = ""; }; + 213A2A651E8D3E2700408313 /* valid_ISO8601_strings.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = valid_ISO8601_strings.txt; path = ../../Tests/SwiftDateTests/valid_ISO8601_strings.txt; sourceTree = ""; }; 21C3010B1D829C8B00B0E02C /* SwiftDate.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftDate.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 21C3010E1D829C8B00B0E02C /* SwiftDate-iOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SwiftDate-iOS.h"; path = "SwiftDate/SwiftDate-iOS.h"; sourceTree = ""; }; 21C3010F1D829C8B00B0E02C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = SwiftDate/Info.plist; sourceTree = ""; }; @@ -309,6 +313,7 @@ children = ( 08EC229C1DA03E4000B6DFC6 /* DateInRegionFormatter.swift */, 08EC229D1DA03E4000B6DFC6 /* ISO8601DateTimeFormatter.swift */, + 213A2A631E8D2A2E00408313 /* ISO8601Parser.swift */, ); name = Formatters; sourceTree = ""; @@ -351,6 +356,7 @@ 08209C991DA041830016B271 /* TestDateInRegion+Components.swift */, 08209C9A1DA041830016B271 /* TestDateInRegion+Formatter.swift */, 08EC22F31DA03F9D00B6DFC6 /* Info.plist */, + 213A2A651E8D3E2700408313 /* valid_ISO8601_strings.txt */, ); name = "Unit Test Bundle"; path = SwiftDateTests; @@ -690,6 +696,7 @@ buildActionMask = 2147483647; files = ( 08EC22981DA03E0B00B6DFC6 /* SwiftDate.bundle in Resources */, + 213A2A661E8D3E2700408313 /* valid_ISO8601_strings.txt in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -806,6 +813,7 @@ buildActionMask = 2147483647; files = ( 08EC229E1DA03E4000B6DFC6 /* DateInRegionFormatter.swift in Sources */, + 213A2A641E8D2A2E00408313 /* ISO8601Parser.swift in Sources */, 08EC22821DA03DF200B6DFC6 /* Date+Compare.swift in Sources */, 08E253831E3F9EBA00432D2C /* Localization.swift in Sources */, 08EC226E1DA03DE700B6DFC6 /* DateInRegion+Compare.swift in Sources */, @@ -1063,7 +1071,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 14; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -1117,7 +1125,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 14; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; diff --git a/SwiftDate/SwiftDate/Info.plist b/SwiftDate/SwiftDate/Info.plist index 28b19814..566e57bf 100644 --- a/SwiftDate/SwiftDate/Info.plist +++ b/SwiftDate/SwiftDate/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 4.0 + 4.1 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/Tests/SwiftDateTests/TestDateInRegion+Formatter.swift b/Tests/SwiftDateTests/TestDateInRegion+Formatter.swift index 61fcf8b7..49cafd2a 100644 --- a/Tests/SwiftDateTests/TestDateInRegion+Formatter.swift +++ b/Tests/SwiftDateTests/TestDateInRegion+Formatter.swift @@ -135,4 +135,31 @@ class TestDateInRegion_Formatter: XCTestCase { let string_11 = testDate.string(format: .iso8601(options: options)) XCTAssertEqual(string_11, "2001-02-03T+01:00", "Failed get ISO8601 #11 representation of the string") } + + + /// This tests evaluate the result of ISO8601Parser which parses valid ISO8601 strings without passing a valid formatter + public func test_ISO8601Strings() { + let bundle = Bundle(for: ISO8601Parser.self) + let path = bundle.path(forResource: "valid_ISO8601_strings", ofType: "txt")! + let valid_strings = try! String(contentsOfFile: path).components(separatedBy: "\n") + + valid_strings.forEach { test_line in + if test_line.hasPrefix("#") == false && test_line.characters.count > 0 { + do { + let input_values = test_line.components(separatedBy: " = ") + let input_date = input_values.first! + let expected_output = input_values.last! + + let parser = try ISO8601Parser(input_date) + let output = parser.parsedDate!.description + + if expected_output != output { + XCTFail("Failed to validate input string '\(test_line)'. It should be '\(expected_output)' but it is '\(output)'") + } + } catch { + XCTFail("Failed to validate input string '\(test_line)'.") + } + } + } + } } diff --git a/Tests/SwiftDateTests/valid_ISO8601_strings.txt b/Tests/SwiftDateTests/valid_ISO8601_strings.txt new file mode 100644 index 00000000..bf7362fc --- /dev/null +++ b/Tests/SwiftDateTests/valid_ISO8601_strings.txt @@ -0,0 +1,95 @@ +060224 = 2006-02-24 00:00:00 +0000 +06-W22 = 2006-05-28 00:00:00 +0000 +06-W2 = 2006-01-08 00:00:00 +0000 +06-W2-1 = 2006-01-09 00:00:00 +0000 +-6-02-24 = 2016-02-24 00:00:00 +0000 +-6-12 = 2016-12-01 00:00:00 +0000 +-6 = 2016-01-01 00:00:00 +0000 +-6- = 2015-11-30 00:00:00 +0000 +-6-02- = 2016-01-31 00:00:00 +0000 +-6-0224 = 2016-02-02 00:00:00 +0000 +-602-24 = 2018-08-25 00:00:00 +0000 +-6-W22 = 2016-05-30 00:00:00 +0000 +-6-W2 = 2016-01-11 00:00:00 +0000 +-6-W2-1 = 2016-01-11 00:00:00 +0000 +--0224 = 2017-02-24 00:00:00 +0000 +--02-24 = 2017-02-24 00:00:00 +0000 +--2-24 = 2017-02-24 00:00:00 +0000 +--02-2 = 2017-02-02 00:00:00 +0000 +--02 = 2017-01-31 00:00:00 +0000 +---24 = 2017-03-24 00:00:00 +0000 +-W2 = 2016-12-26 00:00:00 +0000 +-W2-11 = 2016-12-26 00:00:00 +0000 +-W-3 = 2017-01-04 00:00:00 +0000 +--W-3 = 2017-01-04 00:00:00 +0000 +2006-001 = 2006-01-01 00:00:00 +0000 +2006-002 = 2006-01-02 00:00:00 +0000 +2006-032 = 2006-02-01 00:00:00 +0000 +2006-055 = 2006-02-24 00:00:00 +0000 +2006-365 = 2006-12-31 00:00:00 +0000 +2004-001 = 2004-01-01 00:00:00 +0000 +2004-366 = 2004-12-31 00:00:00 +0000 +-055 = 2017-02-24 00:00:00 +0000 +--055 = 2017-02-24 00:00:00 +0000 +2006T = 2006-01-01 00:00:00 +0000 +T22:63:24+50:70 = 2017-03-30 23:03:24 +0000 +T22:1:2 = 2017-03-30 22:01:02 +0000 +T22:1Z = 2017-03-30 22:01:00 +0000 +T22: = 2017-03-30 22:00:00 +0000 +T22 = 2017-03-30 22:00:00 +0000 +T2 = 2017-03-30 02:00:00 +0000 +T2:2:2 = 2017-03-30 02:02:02 +0000 +2006-02-24T02:43:24 = 2006-02-24 02:43:24 +0000 +2006-02-24T22:43:24 = 2006-02-24 22:43:24 +0000 +2006-02-24T22:63:24 = 2006-02-24 23:03:24 +0000 +2006-12T12:34 = 2006-12-01 12:34:00 +0000 +2006T22 = 2006-01-01 22:00:00 +0000 +2006-02-24T22:63:24Z = 2006-02-24 23:03:24 +0000 +2006-02-24T22:63:24-1 = 2006-02-25 00:03:24 +0000 +2006-02-24T22:63:24-01 = 2006-02-25 00:03:24 +0000 +2006-02-24T22:63:24-01:32 = 2006-02-25 00:35:24 +0000 +2006-02-24T22:63:24-01:0 = 2006-02-25 00:03:24 +0000 +2006-02-24T22:63:24-01:00 = 2006-02-25 00:03:24 +0000 +2006-02-24T22:63:24-01:01 = 2006-02-25 00:04:24 +0000 +2006-02-24T22:63:24-01:11 = 2006-02-25 00:14:24 +0000 +2006-02-24T22:63:24-11:21 = 2006-02-25 10:24:24 +0000 +2009-12T12:34 = 2009-12-01 12:34:00 +0000 +2009 = 2009-01-01 00:00:00 +0000 +2009-05-19 = 2009-05-19 00:00:00 +0000 +2009-05-19 = 2009-05-19 00:00:00 +0000 +20090519 = 2009-05-19 00:00:00 +0000 +2009123 = 2009-05-03 00:00:00 +0000 +2009-05 = 2009-05-01 00:00:00 +0000 +2009-123 = 2009-05-03 00:00:00 +0000 +2009-222 = 2009-08-10 00:00:00 +0000 +2009-001 = 2009-01-01 00:00:00 +0000 +2009-W01-1 = 2008-12-29 00:00:00 +0000 +2009-W51-1 = 2009-12-14 00:00:00 +0000 +2009-W511 = 2009-12-14 00:00:00 +0000 +2009-W33 = 2009-08-09 00:00:00 +0000 +2009W511 = 2009-12-14 00:00:00 +0000 +2009-05-19 = 2009-05-19 00:00:00 +0000 +2009-05-19 00:00 = 2009-05-19 00:00:00 +0000 +2009-05-19 14 = 2009-05-19 14:00:00 +0000 +2009-05-19 14:31 = 2009-05-19 14:31:00 +0000 +2009-05-19 14:39:22 = 2009-05-19 14:39:22 +0000 +2009-05-19T14:39Z = 2009-05-19 14:39:00 +0000 +2009-W21-2 = 2009-05-19 00:00:00 +0000 +2009-W21-2T01:22 = 2009-05-19 01:22:00 +0000 +2009-139 = 2009-05-19 00:00:00 +0000 +2009-05-19 14:39:22-06:00 = 2009-05-19 20:39:22 +0000 +2009-05-19 14:39:22+0600 = 2009-05-19 08:39:22 +0000 +2009-05-19 14:39:22-01 = 2009-05-19 15:39:22 +0000 +20090621T0545Z = 2009-06-21 05:45:00 +0000 +2007-04-06T00:00 = 2007-04-06 00:00:00 +0000 +2007-04-05T24:00 = 2007-04-06 00:00:00 +0000 +2010-02-18T16:23:48.5 = 2010-02-18 16:23:48 +0000 +2010-02-18T16:23:48,444 = 2010-02-18 16:23:48 +0000 +2010-02-18T16:23:48,3-06:00 = 2010-02-18 22:23:48 +0000 +2010-02-18T16:23.4 = 2010-02-18 16:23:23 +0000 +2010-02-18T16:23,25 = 2010-02-18 16:23:15 +0000 +2010-02-18T16:23.33+0600 = 2010-02-18 10:23:19 +0000 +2010-02-18T16.23334444 = 2010-02-18 16:00:00 +0000 +2010-02-18T16,2283 = 2010-02-18 16:00:00 +0000 +2009-05-19 143922.500 = 2009-05-19 14:39:00 +0000 +2009-05-19 1439,55 = 2009-05-19 14:39:00 +0000