날짜 시간 소인을 작성하고 ISO 8601, RFC 3339, UTC 시간대로 형식을 지정하는 방법은 무엇입니까?


186

ISO 8601RFC 3339 의 형식 표준을 사용하여 날짜 시간 소인을 생성하는 방법은 무엇입니까?

목표는 다음과 같은 문자열입니다.

"2015-01-01T00:00:00.000Z"

체재:

  • 년, 월, 일, "XXXX-XX-XX"
  • 구분 기호로 문자 "T"
  • "XX : XX : XX.XXX"와 같은 시간, 분, 초, 밀리 초
  • 영점 오프셋, 일명 UTC, GMT, 줄 루어 시간의 영역 지정자 인 문자 "Z".

가장 좋은 경우 :

  • 간단하고 짧고 간단한 스위프트 소스 코드.
  • 추가 프레임 워크, 하위 프로젝트, cocoapod, C 코드 등을 사용할 필요가 없습니다.

StackOverflow, Google, Apple 등을 검색했지만 이에 대한 Swift 답변을 찾지 못했습니다.

가장 유망한 보인다 클래스는 NSDate, NSDateFormatter, NSTimeZone.

관련 Q & A : iOS에서 ISO 8601 날짜는 어떻게 얻습니까?

지금까지 내가 찾은 최고는 다음과 같습니다.

var now = NSDate()
var formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0)
println(formatter.stringFromDate(now))

6
참고 것을 iOS10 + 단순히 ISO 8601를 포함 내장 .. 그것은 단지 당신을 위해 자동으로 완성됩니다.
Fattie

2
@Fattie 그리고-타임 스탬프의 마지막 .234Z 밀리 초 Zulu / UTC 부분을 어떻게 처리 할 수 ​​있습니까? 답변 : Matt Longs @ stackoverflow.com/a/42101630/3078330
smat88dd

1
@ smat88dd-환상적인 팁, 감사합니다. 나는 "포맷터의 옵션"이 이상하고 거칠다는 단서가 없었다!
Fattie

리눅스에서 작동하는 솔루션을 찾고 있습니다.
neoneye

@neoneye 이전 버전 (일반 DateFormatter)을 사용하고 달력 iso8601을 gregorian stackoverflow.com/a/28016692/2303865로
Leo

답변:


392

스위프트 4 • iOS 11.2.1 이상

extension ISO8601DateFormatter {
    convenience init(_ formatOptions: Options, timeZone: TimeZone = TimeZone(secondsFromGMT: 0)!) {
        self.init()
        self.formatOptions = formatOptions
        self.timeZone = timeZone
    }
}

extension Formatter {
    static let iso8601withFractionalSeconds = ISO8601DateFormatter([.withInternetDateTime, .withFractionalSeconds])
}

extension Date {
    var iso8601withFractionalSeconds: String { return Formatter.iso8601withFractionalSeconds.string(from: self) }
}

extension String {
    var iso8601withFractionalSeconds: Date? { return Formatter.iso8601withFractionalSeconds.date(from: self) }
}

용법:

Date().description(with: .current)  //  Tuesday, February 5, 2019 at 10:35:01 PM Brasilia Summer Time"
let dateString = Date().iso8601withFractionalSeconds   //  "2019-02-06T00:35:01.746Z"

if let date = dateString.iso8601withFractionalSeconds {
    date.description(with: .current) // "Tuesday, February 5, 2019 at 10:35:01 PM Brasilia Summer Time"
    print(date.iso8601withFractionalSeconds)           //  "2019-02-06T00:35:01.746Z\n"
}

iOS 9 • 스위프트 3 이상

extension Formatter {
    static let iso8601withFractionalSeconds: 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 JSONDecoder.DateDecodingStrategy {
    static let iso8601withFractionalSeconds = custom {
        let container = try $0.singleValueContainer()
        let string = try container.decode(String.self)
        guard let date = Formatter.iso8601withFractionalSeconds.date(from: string) else {
            throw DecodingError.dataCorruptedError(in: container,
                  debugDescription: "Invalid date: " + string)
        }
        return date
    }
}

인코딩 전략

extension JSONEncoder.DateEncodingStrategy {
    static let iso8601withFractionalSeconds = custom {
        var container = $1.singleValueContainer()
        try container.encode(Formatter.iso8601withFractionalSeconds.string(from: $0))
    }
}

운동장 테스트

let dates = [Date()]   // ["Feb 8, 2019 at 9:48 PM"]

부호화

let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601withFractionalSeconds
let data = try! encoder.encode(dates)
print(String(data: data, encoding: .utf8)!)

디코딩

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601withFractionalSeconds
let decodedDates = try! decoder.decode([Date].self, from: data)  // ["Feb 8, 2019 at 9:48 PM"]

여기에 이미지 설명을 입력하십시오


3
반대 전환 확장을 추가하는 것이 유용합니다.extension String { var dateFormattedISO8601: NSDate? {return NSDate.Date.formatterISO8601.dateFromString(self)} }
Vive

1
이것은 약간의 정밀도를 잃어 버렸으므로 timeInterval이 아니라 생성 된 문자열을 통해 날짜의 동등성을 비교하는 것이 중요합니다. let now = NSDate() let stringFromDate = now.iso8601 let dateFromString = stringFromDate.dateFromISO8601! XCTAssertEqual(now.timeIntervalSince1970, dateFromString.timeIntervalSince1970)
pixelrevision

7
말할 필요도없이, 밀리 초가 필요하지 않으면 새로운 iOS 10 ISO8601DateFormatter이 프로세스를 단순화합니다. Apple에 버그 보고서 (27242248)를 발행하여 밀리 초를 지정할 수있는 기능을 제공하기 위해이 새로운 포맷터를 확장하도록 요청했습니다 (이 새로운 포맷터는 밀리 초가없는 많은 사람들에게 사용되지 않기 때문에).
Rob

1
에서 RFC3339 우리는 메모 찾을 수 있습니다 "참고 : ISO 8601을 정의 날짜와 시간에 의해 분리 된"T "이 구문을 사용하여 응용 프로그램, 가독성을 위해, 선택할 수 있습니다 전체 날짜와 풀 타임으로 구분 지정 (예를 들어). 공백 문자. " T예를 들어 다음 과 같은 날짜 형식을 포함하지 2016-09-21 21:05:10+00:00않습니까?
manRo

3
@LeoDabus 예, 그러나 이것은 "Swift iso8601"의 첫 번째 결과입니다. 저의 의견은 앞으로이 문제를 발견하고 OP를 다루지 않은 다른 개발자에게 경고하기위한 것입니다.
thislooksfun

38

기술 Q & A1480에en_US_POSIX 설명 된대로 로케일을 설정해야합니다 . 스위프트 3에서 :

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

문제는 그레고리력이 아닌 달력을 사용하는 장치를 사용하는 locale경우 timeZonedateFormat문자열 을 지정하지 않으면 연도가 RFC3339 / ISO8601을 준수하지 않는다는 것 입니다.

또는 당신 ISO8601DateFormatter은 설정의 잡초 localetimeZone자신 에서 당신을 얻을 수 있습니다 :

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 변환에 대해서는 이 답변의 이전 개정판을 참조하십시오 .


왜 로케일을 en_US_POSIX로 설정해야합니까? 우리가 미국에 없더라도?
mnemonic23

2
글쎄, 당신은해야 할 몇 가지 가 제공하는 해당 형식을 일치 로케일과 ISO 8601 / RFC 3999 표준의 규칙입니다 en_US_POSIX. 그것은의 공용어 웹에 날짜를 교환. 날짜 문자열을 저장할 때 장치에서 하나의 달력을 사용하고 나중에 문자열을 다시 읽을 때 다른 달력을 사용하면 날짜를 잘못 해석 할 수 없습니다. 또한 절대로 변경 en_US_POSIX되지 않도록 보장되는 형식이 필요합니다 (그래서 사용 하지 않는 이유 en_US). 자세한 내용은 기술 Q & A 1480 또는 해당 RFC / ISO 표준을 참조하십시오.
Rob

24

ISO8601DateFormatter()Rails 4+ JSON 피드의 날짜와 함께 날짜 를 사용하려면 (물론 밀리 초가 필요하지 않음) 포맷터에서 몇 가지 옵션을 설정해야 작동합니다. 그렇지 않으면 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
    }()
}

놀이터 스크린 샷이 아닌 옵션 구절을 사용한 결과는 다음과 같습니다.

여기에 이미지 설명을 입력하십시오


옵션에 포함시켜야 .withFractionalSeconds하지만 이미 시도했지만 오류가 계속 발생 libc++abi.dylib: terminating with uncaught exception of type NSException합니다.
Leo Dabus 1

@MEnnabah Swift 4에서 제대로 작동합니다. 오류가 발생합니까?
Matt Long

@ LeoDabus, 당신과 같은 오류가 발생했습니다, 당신은 그것을 해결 했습니까?
freeman

사용자 정의 JSONDecoder DateDecodingStrategy stackoverflow.com/a/46458771/2303865
Leo

@freeman 소수의 초 단위로 날짜를 유지하려면 서버에 날짜를 저장 / 수신 할 때 두 배 (참조 날짜 이후 시간 간격)를 사용하는 것이 좋습니다. 그리고 기본 날짜 디코딩 전략을 사용 .deferredToDateCodable 프로토콜을 사용하는 경우
레오 버스들이시길를

6

스위프트 5

iOS 11.0+ / macOS 10.13+를 대상 ISO8601DateFormatter으로하는 경우withInternetDateTimewithFractionalSeconds같이 옵션 .

let date = Date()

let iso8601DateFormatter = ISO8601DateFormatter()
iso8601DateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let string = iso8601DateFormatter.string(from: date)

// string looks like "2020-03-04T21:39:02.112Z"

5

용도 ISO8601DateFormatteriOS10 이상에서 .

용도 DateFormatteriOS9 이상에서 .

스위프트 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
        }
    }()
}

5

Andrés Torres Marroquín과 Leo Dabus를 더 칭찬하기 위해 분수 초를 유지하는 버전이 있습니다. 어디서나 문서화 할 수는 없지만 Apple은 입력 및 출력 모두에서 소수 초를 마이크로 초 (정밀도 3 자릿수)로 자릅니다 ( 유니 코드 tr35-31 과 달리 SSSSSSS를 사용하여 지정하더라도 ).

나는 이것이 대부분의 유스 케이스에 필요하지 않을 것이라고 강조해야한다 . 온라인 날짜는 일반적으로 밀리 초 정밀도가 필요하지 않으며, 필요할 때 다른 데이터 형식을 사용하는 것이 좋습니다. 그러나 때로는 기존 방식과 특정 방식으로 상호 운용해야합니다.

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
    }
}

이것은 다른 모든 솔루션이 밀리 초에서 잘리는 마이크로 초 수준의 정밀도에 도달 할 수 있다는 점에서 가장 좋은 대답입니다.
Michael A. McCloskey

날짜를 모든 소수 초로 유지하려면 서버에 날짜를 저장 / 수신 할 때 두 배 (참조 날짜 이후 시간 간격)를 사용해야합니다.
Leo Dabus

전체 시스템을 제어하고 상호 운용 할 필요가없는 경우 @LeoDabus 예. 대답에서 말했듯이 이것은 대부분의 사용자에게 필요하지 않습니다. 그러나 우리 모두가 항상 웹 API에서 데이터 형식을 제어 할 수있는 것은 아니며, Android와 Python (최소한)은 6 자리의 소수 정밀도를 유지하기 때문에 때때로 따라야하는 경우가 있습니다.
Eli Burke

3

필자의 경우 DynamoDB-lastUpdated 열 (Unix Timestamp)을 표준시로 변환해야합니다.

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)

            }

3

앞으로 형식이 변경되어야 할 수도 있습니다. 날짜는 date.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
        }
    }

}

3

ISO8601DateFormatter단 한 줄로 문자열을 만들 수 있는 새로운 클래스가 있습니다. 이전 버전과의 호환성을 위해 이전 C 라이브러리를 사용했습니다. 나는 이것이 누군가에게 유용하기를 바랍니다.

스위프트 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)
        }
    }
}

1

Leo Dabus 버전을 보완하기 위해 Swift 및 Objective-C로 작성된 프로젝트에 대한 지원을 추가하고 밀리 초 (옵션)에 대한 지원도 추가했습니다.

Xcode 8 및 스위프트 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
    }
}

0

일부 수동 문자열 마스크 또는 TimeFormatter가없는 경우

import Foundation

struct DateISO: Codable {
    var date: Date
}

extension Date{
    var isoString: String {
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        guard let data = try? encoder.encode(DateISO(date: self)),
        let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as?  [String: String]
            else { return "" }
        return json?.first?.value ?? ""
    }
}

let dateString = Date().isoString

이것은 좋은 대답이지만 사용 .iso8601에는 밀리 초가 포함되지 않습니다.
Stefan Arentz

0

객체 패러다임의 수용 가능한 대답에 기초

class ISO8601Format
{
    let format: ISO8601DateFormatter

    init() {
        let format = ISO8601DateFormatter()
        format.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        format.timeZone = TimeZone(secondsFromGMT: 0)!
        self.format = format
    }

    func date(from string: String) -> Date {
        guard let date = format.date(from: string) else { fatalError() }
        return date
    }

    func string(from date: Date) -> String { return format.string(from: date) }
}


class ISO8601Time
{
    let date: Date
    let format = ISO8601Format() //FIXME: Duplication

    required init(date: Date) { self.date = date }

    convenience init(string: String) {
        let format = ISO8601Format() //FIXME: Duplication
        let date = format.date(from: string)
        self.init(date: date)
    }

    func concise() -> String { return format.string(from: date) }

    func description() -> String { return date.description(with: .current) }
}

콜 사이트

let now = Date()
let time1 = ISO8601Time(date: now)
print("time1.concise(): \(time1.concise())")
print("time1: \(time1.description())")


let time2 = ISO8601Time(string: "2020-03-24T23:16:17.661Z")
print("time2.concise(): \(time2.concise())")
print("time2: \(time2.description())")
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.