날짜와 시간을 다루기(Swift)

날짜와 시간 다루기 (Swift)

Swift에서 날짜와 시간을 다루는 방법은 Foundation 프레임워크의 NSDate, NSCalendar, NSDateFormatter, NSDateComponents 등을 사용하게 되므로 Objective-C의 그것과 약간의 문법 차이만 있을 뿐이다.

날짜 계산의 기본 개념

날짜 계산의 단위가 되는 객체는 NSDate이다. 이 클래스는 특정한 시점을 가리키는 정보를 담고 있다. 기본적으로는 기준일시로부터 몇초가 지났는지를 담아서 타임라인 상의 특정한 지점을 가리키게 된다.

Foundation에서 NSDate는 특정한 시점을 가리키는 포인터에 지나지 않는다. 다른 언어들의 date/datetime 객체와 달리 NSDate 클래스 자체로부터 이 날이 무슨 요일인지 등의 정보는 구할 수 없다. 그 이유는 Foundation은 특정 국가나 문화권에 종속되는 규격이 아니기 때문에 어떤 로케일을 적용하느냐에 따라서 한달의 길이라든가 그런 것들이 다를 수 있기 때문이다.

다시 말해 NSDate는 단순히 기준일자로부터 경과한 시간의 양을 갖는 일종의 스칼라 값이다. 따라서 NSDate 객체로부터는 두 NSDate 객체 사이의 시간 간격이 몇 초인지 정보만 알 수 있을 뿐이다. 이로부터 이 날이 몇월 몇일이며 무슨 요일인지 등의 정보는 그 시점이 어떤 달력에 의해 평가되는지에 따라 달라질 수 있게 된다. 그래서 날짜를 계산할 때는 필수적으로 NSCalendar 객체가 필요해진다.

NSCalendar 객체는 NSDate 객체가 가리키는 특정 시점이 지정한 달력(그레고리안력 같은 달력의 종류)상에 어디에 위치하는지를 계산해낸다. 실질적인 날짜 계산을 수행하는 클래스이다.

달력을 이용해서 특정 시점을 날짜 단위로 변경하면 이 날짜는 여러 구성 요소로 나뉘어 진다. 년, 월, 일, 요일, 몇 째 주인지 등의 정보가 나오게 되는 것이다. 이러한 정보를 모아서 표시할 수 있도록 해주는 객체가 NSDateComponents이다. 단, 이 객체는 달력이 사용하는 단위 정보들로 쪼개어져 있을 뿐, 그러한 정보들의 연관 관계에 대한 정보를 가지고 있지 않다. 그래서 2015년 2월 30일과 같은 형태로 날짜 구성 정보를 각각 대입할 수도 있다.

만약 분리된 년도, 월, 일 정보를 가지고 있다면 이것이 언제[^1]인지 즉시 알지 못하지만, 이 정보들로 달력 상의 한날짜를 특정할 수 있따면 NSCalendar 객체를 이용해서 NSDate 정보를 환산할 수 있다.

날짜 객체 만들기

날짜 객체를 만드는 기본적인 방법은 다음과 같다.

let now = NSDate()

파라미터 없는 init()은 현재 시점으로 NSDate 객체를 생성한다. 그 외에 지금 이순간(?)이 아닌 다른 날짜를 만드려면 NSTimeInterval을 이용해 지금으로부터 몇 초 전/후라든지 특정 일자로부터 전/후의 시점을 생성할 수 있다.

  • init(timeIntervalSinceNow:)
  • init(timeInterval: sinceDate:)
  • init(timeIntervalSinceReferenceDate:)
  • init(timeINtervalSize1970:)

참고로 여기에 등장하는 Reference Date는 2001-01-01을 의미한다. 시간간격은 Int 값과 동일하기 때문에 음수로 설정하면 기준일자로부터 이전의 시점을 얻을 수 있다. 1970년의 의미는 모르겠다.1

let yesterday = NSDate(timeIntervalSinceNow:-24 * 60 * 60)
let tomorrow = NSDate(timeIntervalSinceNow:24 * 60 * 60)

특정 일자의 NSDate 객체 만들기

만약 오늘을 기준으로 상대적인 날짜를 만드는 것이 아니라 특정 일자를 기준으로 만들고 싶다면 init(string:) 초기화 메소드를 사용한다.2 이 때 날짜 포맷은 다음과 같다.

"YYYY-MM-DD hh:mm:ss +(-)XXXX"

1988년의 크리스마스는 다음과 같이 생성한다.

let xmas80 = NSDate(string:"1980-12-25 00:00:00 +0000")!

주의할 것은 이 방법은 포맷문자열의 파싱을 실패하면 nil을 리턴하기 때문에 NSDate? 타입의 객체를 리턴한다는 점이다.

init(string:)은 OSX10.10부터 사용하지 않도록 권고(아직 제거되지는 않았음)되는 API이기 때문에 다른 방식을 택해야 한다. 굳이 없애는 이유가 좀 궁금하긴한데, NSDateFormatter에 포맷 스트링을 지정하면 문자열을 파싱해서 날짜 객체를 얻을 수 있다.

let formatter = NSDateFormatter()
formatter.dateFormat = "yyyy'-'MM'-'dd"
let someDate = formatter.dateFromString("2014-12-25")
if someDate != nil { println(someDate!) }

이때 날짜 포맷 문자열은 다음과 같은 요소들로 쓴다.

  • yyyy,MM,dd 등은 유니코드 포맷 문자열 표준3을 따른다.
  • 중간에 패스할 문자는 작은 따옴표로 감싼다.
  • NSDateFormatter는 디폴트 포맷 스트링을 따로 가지지 않는다. 따라서 dateFormat을 세팅하지 않으면 날짜를 파싱하지 못한다.

NSCalender를 이용한 날짜만들기

캘린더 객체는 달력상의 날짜 정보를 토대로 계산한 NSDate 객체를 만들어 낼 수 있다.

NSCalendar의 객체 생성시 달력 식별자의 사용방법이 변경되었다. 또, 식별자를 통한 달력 생성자는 옵셔널 타입을 리턴한다.

func date(# year:Int, # month:Int, # day:Int) -> NSDate? {
    /*  create NSDate with given year, month, day
    year: the year
    month: the month(1~12)
    day: the day(1~31)
    */
    let cal = NSCalendar(identifier:NSCaleanderIdentifierGregorian)!
    let comp = NSDateComponents()
    comp.year = year
    comp.month = month
    comp.day = day
    return cal!.dateFromComponents(comp)
}

println(date(year:2014, month:12, day:25))

날짜 정보 확인하기

날짜 정보를 확인하기 위해서는 NSDate 객체로부터 NSDateComponents 객체를 얻으면 된다. 물론 이 계산은 캘린더 객체가 대신하게 된다. 캘린더의 components(_:fromDate:) 메소드가 이를 가능하게 해준다. 여기에 들어가는 첫번째 인자는 날짜 요소의 어떤 부분을 선택할 것인지를 지시하는 옵션셋 타입으로 정의되며, 이전보다 훨씬 간편하게 생성할 수 있다.

각 비트는 상수로 다음과 같이 정해져있다.

  • EraCalendarUnit
  • YearCalendarUnit
  • MonthCalendarUnit
  • DayCalendarUnit
  • HourCalendarUnit
  • MinuteCalendarUnit
  • SecondCalendarUnit
  • WeekCalendarUnit
  • WeekdayCalendarUnit
  • WeekdayOrdinalCalendarUnit
  • QuarterCalendarUnit
  • WeekOfMonthCalendarUnit
  • WeekOfYearCalendarUnit
  • YearForWeekOfYearCalendarUnit
  • CalendarCalendarUnit
  • TimeZoneCalendarUnit

특히 요일의 경우 1이 일요일, 7일 토요일이 된다. NSDateComponents 객체는 날짜의 정보를 분해한 정보를 담는 동시에, 이를 기반으로 날짜 객체를 만들 수도 있다. 다음의 예는 날짜의 구성 요소를 이용하는 방법 중 하나이다.

let someDate = NSDate(string:"2015-12-25 00:00:00 +0000")
let cal = NSCalendar(calendarIdentifier:NSGregorianCalendar)!
let comps = cal.components([.Year, .Month, .Day, .Weekday], fromDate:someDate)
print(comps.year)
print(comps.month)
print(comps.day)
print(comps.weekday)

앞서, 날짜 구성 정보들을 이용해서 날짜를 구하는 예를 만들었는데, NSDateComponents는 날짜를 분해한 각 요소 정보를 담는 역할도 하지만, 반대로 이 정보들을 모아서 날짜를 만드는 수단이기도 하다. 물론 NSDateComponents 자체로는 이 일을 수행할 수 없고, 날짜 계산 엔진인 NSCalendar가 있어야 한다.

간단한 날짜 비교 계산

시간적으로 많이 떨어지지 않은 두 시점간의 차이는 일정한 스칼라량으로 구하는 것이 나을 수 있고 따라서 수초에서 수천초 가량의 시간 차이 혹은 ‘몇 개월 몇 일’과 같은 식으로 단위를 굳이 나눌 필요가 없는 시간 간격을 계산하는 것은 단순히 NSDate의 값 비교만으로도 가능하다.

시간 비교

NSDate에는 다음과 같은 메소드들이 있어서 시간의 전후를 비교하기 간편하다.

  • isEqualToDate(_:)
  • earlierDate(_:)
  • laterDate(_:)
  • compare(_:)

compare(_:)의 경우 NSOrderedSame, NSOrderedDescceding, NSOrderedAscending 등의 순서 상수값을 리턴한다.

또한 두 시간 사이의 시간 간격은 다음 메소드들로 구할 수 있다. (뒤에 괄호가 붙지 않은 것은 프로퍼티이다.)

  • timeIntervalSinceDate(_:)
  • timeIntervalSinceNow
  • timeIntervalSinceReferenceDate()
  • timeIntervalSinceReferenceDate
  • timeIntervalSince1970

두 날짜 객체 사이의 인터벌 값은 경과한 초로 구하게 되며, 만약 비교 대상보다 앞선 날짜라면 음수를 받게 된다.

순전히 날짜에 날짜가 아닌 날짜에 특정 시간 요소를 더하거나, 혹은 두 날짜의 시간 요소별 차이를 구하기 위해서는 NSCalendar의 도움을 받아야 한다.

  • components(_:fromDate:toDate:options)
  • dateByAddingComponents(_:fromDate:options)

두 날짜를 계산할 때 NSCalendar는 옵션 값을 요구하는데, 이 옵션은 현재까지 문서화가 안되어 있다. 0을 넘기면 된다고 하는데 NSCalendarOptions(0)으로 넘겨야 한다.

두 날짜의 시간 차이를 구하는 다음 예제를 보자.

let d1 = NSDate(string:"2012-10-13 00:00:00 +0000")!
let d2 = NSDate(string:"2015-01-14 00:00:00 +0000")!

let cal = NSCalendar(calendarIdentifier:NSGregorianCalendar)!

let c1 = cal.components([.Day], fromDate:d1, toDate:d2, options:[])
let c2 = cal.components([.Day, .Month], fromDate:d1, toDate:d2, options:[])
let c3 = cal.components([.Day, .Month, .Year], fromDate:d1, toDate:d2, options:[])

print(c1.day)
print(c2.month)
print(c2.day)
print("\(c3.year)year(s),\(c3.month)month(s),\(c3.day)day(s)")

위 예제에서 같은 두 날짜(2013-10-13, 2015-01-14)에 대해 비교를 하는데, 날짜만으로 차이를 구하면 822일, 월과 일로 차이를 구하면 27개월 0일, 년까지 포함하는 경우 2년 3개월 0일의 차이가 나게 된다.

차이를 통한 새로운 날짜 찾기

시작일자로부터 100일이 되는 날짜들을 차례로 구해보자.

let d1 = NSDate(string:"2010-10-11 00:00:00 -0900")
let cal = NSCalendar(identifier:NSCalendarIdentifierGregorian)

// 1
let offset = NSDateComponents()
offset.day = 100

var d:NSDate? = d1

for _ in 0..<10 {
    d = cal!.dateByAddingComponents(offset, 
                toDate:d!, 
                options:[]
                )
    if let d100 = d {
        print(d)
    }
}

위 코드를 간단히 설명하면 다음과 같다.

  1. 100일 간격의 날짜를 계산할 것이기 때문에 NSDateComponents 객체를 하나 만들고 다른 필드는 없이 day 속성만 100이라고 준다. 이는 100일만큼의 오프셋을 의미하게 된다.
  2. 최초 생성한 날로부터 1.에서 만든 오프셋을 더해 새로운 날짜를 생성한다. 두 날짜의 차이를 계산하거나 한 날짜로부터 다른 날짜를 만드는데는 NSCalendarOptions 객체가 필요하다.
  3. 100일 후 날짜가 구해지면 이를 출력한다. (NSDate? 타입이 리턴되기 때문에 nil인지 체크해야 한다. )
  4. 이를 10회 반복한다.

  1. 제록스가 팔로알토 연구센터(PARC)를 설립한 년도이다. 
  2. 이 메소드는 OSX10.10에서 deprecated 되었다. 대신 NSDateFormatter를 이용해야 한다. 
  3. http://www.unicode.org/reports/tr35/tr35-31/tr35-dates.html#Date_Format_Patterns