Swift: как создать штамп даты и времени в формате ISO 8601, RFC 3339, часовой пояс UTC?

как создать штамп даты и времени, используя стандарты формата для ISO 8601 и RFC 3339?

цель-строка, которая выглядит так:

"2015-01-01T00:00:00.000Z"
:
  • год, месяц, день, как "XXXX-XX-XX"
  • буква "т" в качестве разделителя
  • час, минута, секунды, миллисекунды, как " XX: XX: XX.XXX".
  • буква " Z " как обозначение зоны для смещения нуля, a.к. a. UTC, GMT, Зулусское время.

лучший вариант:

  • Swift исходный код, который является простым, коротким и простым.
  • не нужно использовать никаких дополнительных рамок, подпроект, cocoapod, с кодом и т. д.

Я искал StackOverflow, Google, Apple и т. д. и не нашел быстрого ответа.

классы, которые кажутся наиболее перспективными являются NSDate, NSDateFormatter, NSTimeZone.

В Ответ: как получить дату ISO 8601 в iOS?

9 ответов


Xcode 9 * Swift 4 * iOS 11 или более поздняя версия

как упоминалось в комментариях @keno ISO8601DateFormatter теперь поддерживает .withFractionalSeconds formatOptions в iOS11 или более поздней версии:

extension Formatter {
    static let iso8601: ISO8601DateFormatter = {
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        return formatter
    }()
}

площадка для испытаний:

let dateString = Formatter.iso8601.string(from: Date())   // "2018-01-23T03:06:46.232Z"
if let date = Formatter.iso8601.date(from: dateString)  {
    print(date)   // "2018-01-23 03:06:46 +0000\n"
}

Xcode 8.2 * Swift 3.0.2

extension Formatter {
    static let iso8601: DateFormatter = {
        let formatter = DateFormatter()
        formatter.calendar = Calendar(identifier: .iso8601)
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.timeZone = TimeZone(secondsFromGMT: 0)
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
        return formatter
    }()
}
extension Date {
    var iso8601: String {
        return Formatter.iso8601.string(from: self)
    }
}

extension String {
    var dateFromISO8601: Date? {
        return Formatter.iso8601.date(from: self)   // "Mar 22, 2017, 10:22 AM"
    }
}

использование:

let stringFromDate = Date().iso8601    // "2017-03-22T13:22:13.933Z"

if let dateFromString = stringFromDate.dateFromISO8601 {
    print(dateFromString.iso8601)      // "2017-03-22T13:22:13.933Z"
}

enter image description here


не забудьте установить язык, на en_US_POSIX как описано в Технические Q&A1480. В Swift 3:

let date = Date()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.locale = Locale(identifier: "en_US_POSIX")
print(formatter.string(from: date))

проблема в том, что если вы находитесь на устройстве, которое использует не Григорианский календарь, год не будет соответствовать RFC3339/ISO8601, если вы не укажете locale а также timeZone и dateFormat строку.

или вы можете использовать ISO8601DateFormatter чтобы вытащить вас из сорняков установки locale и timeZone сам себе:

let date = Date()
let formatter = ISO8601DateFormatter()
formatter.formatOptions.insert(.withFractionalSeconds)  // this is only available effective iOS 11 and macOS 10.13
print(formatter.string(from: date))

Для Swift 2 исполнение, см. предыдущая редакция этого ответа.


Если вы хотите использовать ISO8601DateFormatter() С датой из Rails 4+ JSON feed (и, конечно, не нужно millis), вам нужно установить несколько параметров на форматере, чтобы он работал правильно, иначе date(from: string) функция вернет nil. Вот что я использую:

extension Date {
    init(dateString:String) {
        self = Date.iso8601Formatter.date(from: dateString)!
    }

    static let iso8601Formatter: ISO8601DateFormatter = {
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withFullDate,
                                          .withTime,
                                          .withDashSeparatorInDate,
                                          .withColonSeparatorInTime]
        return formatter
    }()
}

вот результат использования опций стихов не на скриншоте детской площадки:

enter image description here


в будущем формат может потребоваться изменить, что может быть небольшой головной болью с датой.dateFromISO8601 вызывает везде в приложении. Используйте класс и протокол для обертывания реализации, изменение формата даты и времени вызова в одном месте будет проще. Используйте RFC3339, если это возможно, его более полное представление. DateFormatProtocol и DateFormat отлично подходят для инъекций зависимостей.

class AppDelegate: UIResponder, UIApplicationDelegate {

    internal static let rfc3339DateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
    internal static let localeEnUsPosix = "en_US_POSIX"
}

import Foundation

protocol DateFormatProtocol {

    func format(date: NSDate) -> String
    func parse(date: String) -> NSDate?

}


import Foundation

class DateFormat:  DateFormatProtocol {

    func format(date: NSDate) -> String {
        return date.rfc3339
    }

    func parse(date: String) -> NSDate? {
        return date.rfc3339
    }

}


extension NSDate {

    struct Formatter {
        static let rfc3339: NSDateFormatter = {
            let formatter = NSDateFormatter()
            formatter.calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierISO8601)
            formatter.locale = NSLocale(localeIdentifier: AppDelegate.localeEnUsPosix)
            formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0)
            formatter.dateFormat = rfc3339DateFormat
            return formatter
        }()
    }

    var rfc3339: String { return Formatter.rfc3339.stringFromDate(self) }
}

extension String {
    var rfc3339: NSDate? {
        return NSDate.Formatter.rfc3339.dateFromString(self)
    }
}



class DependencyService: DependencyServiceProtocol {

    private var dateFormat: DateFormatProtocol?

    func setDateFormat(dateFormat: DateFormatProtocol) {
        self.dateFormat = dateFormat
    }

    func getDateFormat() -> DateFormatProtocol {
        if let dateFormatObject = dateFormat {

            return dateFormatObject
        } else {
            let dateFormatObject = DateFormat()
            dateFormat = dateFormatObject

            return dateFormatObject
        }
    }

}

для дальнейшего комплимента Андрес Торрес Маррокин и Лео Дабус, у меня есть версия, которая сохраняет доли секунды. Я не могу найти его нигде, но Apple усекает дробные секунды до микросекунды( 3 цифры точности) как на входе, так и на выходе (хотя указано с помощью SSSSSSS, вопреки Unicode tr35-31).

Я должен подчеркнуть, что это, вероятно, не обязательно для большинства случаев использования. Даты в интернете обычно не нужны миллисекундная точность, и когда они это делают, часто лучше использовать другой формат данных. Но иногда нужно взаимодействовать с уже существующей системой определенным образом.

Xcode 8/9 и Swift 3.0-3.2

extension Date {
    struct Formatter {
        static let iso8601: DateFormatter = {
            let formatter = DateFormatter()
            formatter.calendar = Calendar(identifier: .iso8601)
            formatter.locale = Locale(identifier: "en_US_POSIX")
            formatter.timeZone = TimeZone(identifier: "UTC")
            formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXXXX"
            return formatter
        }()
    }

    var iso8601: String {
        // create base Date format 
         var formatted = DateFormatter.iso8601.string(from: self)

        // Apple returns millisecond precision. find the range of the decimal portion
         if let fractionStart = formatted.range(of: "."),
             let fractionEnd = formatted.index(fractionStart.lowerBound, offsetBy: 7, limitedBy: formatted.endIndex) {
             let fractionRange = fractionStart.lowerBound..<fractionEnd
            // replace the decimal range with our own 6 digit fraction output
             let microseconds = self.timeIntervalSince1970 - floor(self.timeIntervalSince1970)
             var microsecondsStr = String(format: "%.06f", microseconds)
             microsecondsStr.remove(at: microsecondsStr.startIndex)
             formatted.replaceSubrange(fractionRange, with: microsecondsStr)
        }
         return formatted
    }
}

extension String {
    var dateFromISO8601: Date? {
        guard let parsedDate = Date.Formatter.iso8601.date(from: self) else {
            return nil
        }

        var preliminaryDate = Date(timeIntervalSinceReferenceDate: floor(parsedDate.timeIntervalSinceReferenceDate))

        if let fractionStart = self.range(of: "."),
            let fractionEnd = self.index(fractionStart.lowerBound, offsetBy: 7, limitedBy: self.endIndex) {
            let fractionRange = fractionStart.lowerBound..<fractionEnd
            let fractionStr = self.substring(with: fractionRange)

            if var fraction = Double(fractionStr) {
                fraction = Double(floor(1000000*fraction)/1000000)
                preliminaryDate.addTimeInterval(fraction)
            }
        }
        return preliminaryDate
    }
}

в моем случае я должен преобразовать столбец DynamoDB-lastUpdated (метка времени Unix) в нормальное время.

начальное значение lastUpdated было: 1460650607601-преобразовано до 2016-04-14 16:16: 47 +0000 через:

   if let lastUpdated : String = userObject.lastUpdated {

                let epocTime = NSTimeInterval(lastUpdated)! / 1000 // convert it from milliseconds dividing it by 1000

                let unixTimestamp = NSDate(timeIntervalSince1970: epocTime) //convert unix timestamp to Date
                let dateFormatter = NSDateFormatter()
                dateFormatter.timeZone = NSTimeZone()
                dateFormatter.locale = NSLocale.currentLocale() // NSLocale(localeIdentifier: "en_US_POSIX")
                dateFormatter.dateFormat =  "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
                dateFormatter.dateFromString(String(unixTimestamp))

                let updatedTimeStamp = unixTimestamp
                print(updatedTimeStamp)

            }

есть новый ISO8601DateFormatter класс, который позволяет вам создать строку только с одной строкой. Для обратной совместимости я использовал старую C-библиотеки. Надеюсь, это кому-то пригодится.

Swift 3.0

extension Date {
    var iso8601: String {
        if #available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
            return ISO8601DateFormatter.string(from: self, timeZone: TimeZone.current, formatOptions: .withInternetDateTime)
        } else {
            var buffer = [CChar](repeating: 0, count: 25)
            var time = time_t(self.timeIntervalSince1970)
            strftime_l(&buffer, buffer.count, "%FT%T%z", localtime(&time), nil)
            return String(cString: buffer)
        }
    }
}

использует ISO8601DateFormatter на iOS10 или новее.

использует DateFormatter на iOS9 или старше.

Swift 4

protocol DateFormatterProtocol {
    func string(from date: Date) -> String
    func date(from string: String) -> Date?
}

extension DateFormatter: DateFormatterProtocol {}

@available(iOS 10.0, *)
extension ISO8601DateFormatter: DateFormatterProtocol {}

struct DateFormatterShared {
    static let iso8601: DateFormatterProtocol = {
        if #available(iOS 10, *) {
            return ISO8601DateFormatter()
        } else {
            // iOS 9
            let formatter = DateFormatter()
            formatter.calendar = Calendar(identifier: .iso8601)
            formatter.locale = Locale(identifier: "en_US_POSIX")
            formatter.timeZone = TimeZone(secondsFromGMT: 0)
            formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
            return formatter
        }
    }()
}

чтобы дополнить версию Leo Dabus, я добавил поддержку проектов, написанных Swift и Objective-C, а также добавил поддержку дополнительных миллисекунд, вероятно, не лучший, но вы получите точку:

Xcode 8 и Swift 3

extension Date {
    struct Formatter {
        static let iso8601: DateFormatter = {
            let formatter = DateFormatter()
            formatter.calendar = Calendar(identifier: .iso8601)
            formatter.locale = Locale(identifier: "en_US_POSIX")
            formatter.timeZone = TimeZone(secondsFromGMT: 0)
            formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
            return formatter
        }()
    }

    var iso8601: String {
        return Formatter.iso8601.string(from: self)
    }
}


extension String {
    var dateFromISO8601: Date? {
        var data = self
        if self.range(of: ".") == nil {
            // Case where the string doesn't contain the optional milliseconds
            data = data.replacingOccurrences(of: "Z", with: ".000000Z")
        }
        return Date.Formatter.iso8601.date(from: data)
    }
}


extension NSString {
    var dateFromISO8601: Date? {
        return (self as String).dateFromISO8601
    }
}