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