Skip to content

Instantly share code, notes, and snippets.

@bok-
Created October 14, 2020 05:19
Show Gist options
  • Save bok-/b2f930f5f7f1106d511dc4ab20091e8c to your computer and use it in GitHub Desktop.
Save bok-/b2f930f5f7f1106d511dc4ab20091e8c to your computer and use it in GitHub Desktop.
import Foundation
public extension JSONDecoder.DateDecodingStrategy {
static var betterISO8601: JSONDecoder.DateDecodingStrategy {
return .custom({ try decodeDate(from: $0, calendar: Calendar(identifier: .gregorian)) })
}
static func betterISO8601 (calendar: Calendar) -> JSONDecoder.DateDecodingStrategy {
return .custom({ try decodeDate(from: $0, calendar: calendar) })
}
}
private func decodeDate (from decoder: Decoder, calendar: Calendar) throws -> Date {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
guard let date = Date(string: string) else {
throw DateDecodingError.invalidDate(string)
}
return date
}
private enum DateDecodingError: Swift.Error {
case invalidDate(String)
}
extension DateComponents
{
/// An initializer to create an instance of `NSDateComponents` from an ISO8601-compatible string.
///
/// - Parameters:
/// - iso8601String string: An ISO8601-compatible date string.
///
public init? (string: String)
{
self.init()
let scanner = Scanner(string: string)
scanner.charactersToBeSkipped = nil
// Date
guard let year = scanner.scanInt() else { return nil }
self.year = year
guard scanner.scanString("-") != nil, let month = scanner.scanInt() else { return }
self.month = month
guard scanner.scanString("-") != nil, let day = scanner.scanInt() else { return }
self.day = day
// There should now be a 'T', for time.
guard scanner.scanCharacters(from: CharacterSet(charactersIn: "T ")) != nil else { return }
guard let hour = scanner.scanInt() else { return }
self.hour = hour
guard scanner.scanString(":") != nil, let minute = scanner.scanInt() else { return }
self.minute = minute
let index = scanner.currentIndex
// seconds?
if scanner.scanString(":") != nil, let second = scanner.scanInt() {
self.second = second
// support for fractions of seconds
let i = scanner.currentIndex
if scanner.scanString(".") != nil {
scanner.currentIndex = i
guard let fraction = scanner.scanDouble() else { return }
self.nanosecond = Int(fraction * 1000000000)
}
// no seconds, undo that
} else {
scanner.currentIndex = index
}
self.timeZone = scanner.scanTimeZone()
}
}
extension Date
{
public init? (string: String?, calendar: Calendar = Calendar.current) {
guard let string = string else { return nil }
guard let components = DateComponents(string: string) else { return nil }
var calendar = calendar
calendar.timeZone = components.timeZone ?? TimeZone(identifier: "UTC")!
guard let date = calendar.date(from: components) else { return nil }
self.init(timeInterval: 0, since: date);
}
}
extension Scanner {
/// Scans a TimeZone out of the receiver, or returns nil if not found.
///
/// Supports the following TimeZone formats:
/// • Z (for UTC)
/// • +hh:mm
///
func scanTimeZone () -> TimeZone? {
let start = self.currentIndex
// check for Zulu
if self.scanString("Z") != nil {
return TimeZone(identifier: "UTC")
}
// Check for the + or - that denotes the start of a TimeZone specifier.
self.currentIndex = start
let signs = CharacterSet(charactersIn: "+-")
_ = self.scanUpToCharacters(from: signs)
guard let sign = self.scanCharacters(from: signs) else { return nil }
// scan the number of hours
guard var hours = self.scanInt() else { return nil }
// check for colon and therefore minutes
var minutes: Int
if self.scanString(":") == nil && hours > 14 {
minutes = hours % 100;
hours = Int(floor(Double(hours) / 100.0));
} else {
minutes = scanInt() ?? 0
}
var offset = (hours * 3600) + (minutes * 60)
if sign == "-" {
offset *= -1
}
return TimeZone(secondsFromGMT: offset);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment