2010-11-24

日付文字列の変換 - RFC3339 から NSDate へ

実装

まずは、今回作った Objective-C による実装から示す。関数の機能は単純で、RFC3399 形式で表現された日時を元に NSDate のオブジェクトを生成する。関数内で NSDate オブジェクトを alloc しているが、返す前に autorelease しているため、荻原(2.0)本 (p.96) で言う「一時的なオブジェクト」となっている。また、関数を呼び出す側で自動解放プールを用意する必要がある。通常の Cocoa アプリの場合は、NSApplication がイベントループの管理とともに自動解放プールも用意してくれる(→ 「荻原(2.0)本」p.98)。

#import <Foundation/Foundation.h>
// convert a RFC3399 date (& time) into a NSDate object
// NOTE: This function ignores fractions of a second in the RFC3339
// representation.
NSDate *getDateObject(NSString *rfc3339)
{
// Date and Time representation in RFC3399:
// Pattern #1: "YYYY-MM-DDTHH:MM:SSZ"
// 1
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
// [Y|Y|Y|Y|-|M|M|-|D|D|T|H|H|:|M|M|:|S|S|Z]
//
// Pattern #2: "YYYY-MM-DDTHH:MM:SS.sssZ"
// 1 2
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
// [Y|Y|Y|Y|-|M|M|-|D|D|T|H|H|:|M|M|:|S|S|.|s|s|s|Z]
// NOTE: The number of digits in the "sss" part is not defined.
//
// Pattern #3: "YYYY-MM-DDTHH:MM:SS+HH:MM"
// 1 2
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4
// [Y|Y|Y|Y|-|M|M|-|D|D|T|H|H|:|M|M|:|S|S|+|H|H|:|M|M]
//
// Pattern #4: "YYYY-MM-DDTHH:MM:SS.sss+HH:MM"
// 1 2
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8
// [Y|Y|Y|Y|-|M|M|-|D|D|T|H|H|:|M|M|:|S|S|.|s|s|s|+|H|H|:|M|M]
// NOTE: The number of digits in the "sss" part is not defined.
// NSDate format: "YYYY-MM-DD HH:MM:SS +HHMM".
NSCharacterSet *setOfT = [NSCharacterSet characterSetWithCharactersInString:@"tT"];
NSRange tMarkPos = [rfc3339 rangeOfCharacterFromSet:setOfT];
if (tMarkPos.location == NSNotFound) return nil;
// extract date and time part:
NSString *datePart = [rfc3339 substringToIndex:tMarkPos.location];
NSString *timePart = [rfc3339 substringWithRange:NSMakeRange(tMarkPos.location + tMarkPos.length, 8)];
NSString *restPart = [rfc3339 substringFromIndex:tMarkPos.location + tMarkPos.length + 8];
// extract time offset part:
NSString *tzSignPart, *tzHourPart, *tzMinPart;
NSCharacterSet *setOfZ = [NSCharacterSet characterSetWithCharactersInString:@"zZ"];
NSRange tzPos = [restPart rangeOfCharacterFromSet:setOfZ];
if (tzPos.location == NSNotFound) { // Pattern #3 or #4
NSCharacterSet *setOfSign = [NSCharacterSet characterSetWithCharactersInString:@"+-"];
NSRange tzSignPos = [restPart rangeOfCharacterFromSet:setOfSign];
if (tzSignPos.location == NSNotFound) return nil;
tzSignPart = [restPart substringWithRange:tzSignPos];
tzHourPart = [restPart substringWithRange:NSMakeRange(tzSignPos.location + tzSignPos.length, 2)];
tzMinPart = [restPart substringFromIndex:tzSignPos.location + tzSignPos.length + 2 + 1];
} else { // Pattern #1 or #2
// "Z" means UTC.
tzSignPart = @"+";
tzHourPart = @"00";
tzMinPart = @"00";
}
// construct a date string in the NSDate format
NSString *dateStr = [NSString stringWithFormat:@"%@ %@ %@%@%@", datePart, timePart, tzSignPart, tzHourPart, tzMinPart];
NSDate *dateObj = [[NSDate alloc] initWithString:dateStr];
[dateObj autorelease];
return dateObj;
}
void testGetDateObject(NSString *rfc3339)
{
NSLog(@"----------------------------------------");
NSLog(@"RFC3339 Format: %@", rfc3339);
NSDate *date = getDateObject(rfc3339);
NSLog(@"NSDate object: %@", date);
NSString *dateFormat = @"%Y-%m-%d %H:%M:%S %z";
NSString *dateInDefaultTZ = [date descriptionWithCalendarFormat:dateFormat timeZone:nil locale:nil];
NSLog(@"Default TZ: %@", dateInDefaultTZ);
NSTimeZone *aTimeZone = [NSTimeZone timeZoneWithName:@"UTC"];
NSString *dateInUTC = [date descriptionWithCalendarFormat:dateFormat timeZone:aTimeZone locale:nil];
NSLog(@"UTC: %@", dateInUTC);
}
int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
// insert code here...
testGetDateObject(@"2010-11-22T12:34:56Z");
testGetDateObject(@"2010-11-22t12:34:56z");
testGetDateObject(@"2010-11-22T12:34:56.123Z");
testGetDateObject(@"2010-11-22t12:34:56.123z");
testGetDateObject(@"2010-11-22T12:34:56+09:00");
testGetDateObject(@"2010-11-22t12:34:56+09:00");
testGetDateObject(@"2010-11-22T12:34:56.123+09:00");
testGetDateObject(@"2010-11-22t12.34.56.123+09:00");
testGetDateObject(@"2010-11-22T12:34:56-05:00");
testGetDateObject(@"2010-11-22T12:34:56.123-05:00");
[pool drain];
return 0;
}
view raw convertDate.m hosted with ❤ by GitHub

この実装では NSString の機能のうち、以下のメソッドを多用している。

- (NSRange)rangeOfCharacterFromSet:(NSCharacterSet*)aSet
aSet で渡された文字集合中の文字が最初に見つかった場所を返す。
- (NSString*)substringToIndex:(NSUInteger)aIndex
先頭から位置 aIndex までの部分文字列を返す。
- (NSString*)substringWithRange:(NSRange)aRange
aRange.location を開始位置とし、aRange.length を長さとする部分文字列を返す。
- (NSString*)substringFromIndex:(NSUInteger)
aIndex を開始位置としする末尾までの部分文字列を返す。

上記の substring...の戻り値はいずれも一時的なオブジェクトになっている。

参照仕様

GData API の Protocl Reference によれば、各種日付には RFC3339 フォーマットが使われる、とある。これは GData API が使うデータフォーマット(Atom 形式; RFC 4287 で定義されている)の仕様によるものだ。

(「RFC 4287」より)
A Date construct is an element whose content MUST conform to the "date-time" production in [RFC3339]. In addition, an uppercase "T" character MUST be used to separate date and time, and an uppercase "Z" character MUST be present in the absence of a numeric time zone offset.

[...snip...]

Example Date constructs:
<updated>2003-12-13T18:30:02Z</updated>
<updated>2003-12-13T18:30:02.25Z</updated>
<updated>2003-12-13T18:30:02+01:00</updated>
<updated>2003-12-13T18:30:02.25+01:00</updated>

上記の中で例として挙げられている 4 つのパターンを見れば書式のおよそは理解できる。

より詳細な 書式の定義は RFC 3339 にある。

date-fullyear   = 4DIGIT
date-month      = 2DIGIT  ; 01-12
date-mday       = 2DIGIT  ; 01-28, 01-29, 01-30, 01-31 based on
                          ; month/year
time-hour       = 2DIGIT  ; 00-23
time-minute     = 2DIGIT  ; 00-59
time-second     = 2DIGIT  ; 00-58, 00-59, 00-60 based on leap second
                          ; rules
time-secfrac    = "." 1*DIGIT
time-numoffset  = ("+" / "-") time-hour ":" time-minute
time-offset     = "Z" / time-numoffset

partial-time    = time-hour ":" time-minute ":" time-second
                  [time-secfrac]
full-date       = date-fullyear "-" date-month "-" date-mday
full-time       = partial-time time-offset

date-time       = full-date "T" full-time

この定義から、日時の表現は「日付」と「時刻」に二分され、その区切りが文字「T」であること、また「時刻」は「時刻本体」と「時差」に分かれることが読みとれる。

「時差」については、時刻による差分で表現したもの(「+09:00」や「-5:00」など)か、あるいは文字「Z」を用いる(時差として文字「Z」を指定した場合、その時刻は UTC を意味する)。

さらに「時刻本体」の表現については、「時」「分」「秒」をそれぞれ 2 桁の十進数で表現する(1 桁の場合は先頭に「0」を付加する)。また「秒」については 1 秒以下を小数表現で「秒」に付けても良い、となっている。

GData API での時刻の取扱い

実際に GData API が返すフィードを見ると、updated 要素等は以下のようになっていて、確かに上記の書式に従っていることがわかる。

<published>2010-10-03T21:18:00.000+09:00</published>
<updated>2010-10-03T21:18:00.840+09:00</updated>

一方、GData Objective-C Library が返す時刻データは GDataDateTime クラスのインスタンスになっていて、そこから文字列データを取り出すと RFC3339 フォーマットの文字列となる。ただし、それを作る部分は以下のようになっているため、「秒数」の小数点以下が無視されている(GData Objective-C Client ver. 1.10.0)。これは、Cocoa の NSDateComponents が小数点以下の秒数をサポートしていないことによるものだと思われる(以下のコード中の dateComponents は NSDateComponents のインスタンスを保持している)。

- (NSString *)stringValue {
  return [self RFC3339String];
}

- (NSString *)RFC3339String {
  NSDateComponents *dateComponents = [self dateComponents];
  NSInteger offset = [self offsetSeconds];

  NSString *timeString = @""; // timeString like "T15:10:46-08:00"

  if ([self hasTime]) {

    NSString *timeOffsetString; // timeOffsetString like "-08:00"

    if ([self isUniversalTime]) {
     timeOffsetString = @"Z";
    } else if (offset == NSUndefinedDateComponent) {
      // unknown offset is rendered as -00:00 per
      // http://www.ietf.org/rfc/rfc3339.txt section 4.3
      timeOffsetString = @"-00:00";
    } else {
      NSString *sign = @"+";
      if (offset < 0) {
        sign = @"-";
        offset = -offset;
      }
      timeOffsetString = [NSString stringWithFormat:@"%@%02ld:%02ld",
        sign, (long)(offset/(60*60)) % 24, (long)(offset / 60) % 60];
    }
    timeString = [NSString stringWithFormat:@"T%02ld:%02ld:%0l2d%@",
      (long)[dateComponents hour], (long)[dateComponents minute],
      (long)[dateComponents second], timeOffsetString];
  }

  // full dateString like "2006-11-17T15:10:46-08:00"
  NSString *dateString = [NSString stringWithFormat:@"%04ld-%02ld-%02ld%@",
    (long)[dateComponents year], (long)[dateComponents month],
    (long)[dateComponents day], timeString];

  return dateString;
}

おまけ

最初に挙げたプログラムを Xcode で Command Line Tool としてビルドし、実行するとコンソールには以下のように出力される。水平線で区切られた 4 行が、testGetDateObject の一回の呼び出しによるもので、最初が引数として与えられた RFC3339 形式の日時文字列、2 行目が getDateObject による変換結果(の NSDate のインスタンス)、3 行目がそのインスタンスをデフォルトのタイムゾーン(ウチではもちろん JST)で評価した結果、最後の 4 行目は同インスタンスを UTC で評価した結果となっている。

2010-11-24 22:06:14.819 ExpApp[24929:a0b] ----------------------------------------
2010-11-24 22:06:14.821 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22T12:34:56Z
2010-11-24 22:06:14.824 ExpApp[24929:a0b] NSDate object: 2010-11-22 21:34:56 +0900
2010-11-24 22:06:14.824 ExpApp[24929:a0b] Default TZ: 2010-11-22 21:34:56 +0900
2010-11-24 22:06:14.825 ExpApp[24929:a0b] UTC: 2010-11-22 12:34:56 +0000
2010-11-24 22:06:14.825 ExpApp[24929:a0b] ----------------------------------------
2010-11-24 22:06:14.825 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22t12:34:56z
2010-11-24 22:06:14.826 ExpApp[24929:a0b] NSDate object: 2010-11-22 21:34:56 +0900
2010-11-24 22:06:14.826 ExpApp[24929:a0b] Default TZ: 2010-11-22 21:34:56 +0900
2010-11-24 22:06:14.826 ExpApp[24929:a0b] UTC: 2010-11-22 12:34:56 +0000
2010-11-24 22:06:14.827 ExpApp[24929:a0b] ----------------------------------------
2010-11-24 22:06:14.827 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22T12:34:56.123Z
2010-11-24 22:06:14.827 ExpApp[24929:a0b] NSDate object: 2010-11-22 21:34:56 +0900
2010-11-24 22:06:14.828 ExpApp[24929:a0b] Default TZ: 2010-11-22 21:34:56 +0900
2010-11-24 22:06:14.828 ExpApp[24929:a0b] UTC: 2010-11-22 12:34:56 +0000
2010-11-24 22:06:14.828 ExpApp[24929:a0b] ----------------------------------------
2010-11-24 22:06:14.829 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22t12:34:56.123z
2010-11-24 22:06:14.829 ExpApp[24929:a0b] NSDate object: 2010-11-22 21:34:56 +0900
2010-11-24 22:06:14.829 ExpApp[24929:a0b] Default TZ: 2010-11-22 21:34:56 +0900
2010-11-24 22:06:14.830 ExpApp[24929:a0b] UTC: 2010-11-22 12:34:56 +0000
2010-11-24 22:06:14.830 ExpApp[24929:a0b] ----------------------------------------
2010-11-24 22:06:14.830 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22T12:34:56+09:00
2010-11-24 22:06:14.831 ExpApp[24929:a0b] NSDate object: 2010-11-22 12:34:56 +0900
2010-11-24 22:06:14.831 ExpApp[24929:a0b] Default TZ: 2010-11-22 12:34:56 +0900
2010-11-24 22:06:14.832 ExpApp[24929:a0b] UTC: 2010-11-22 03:34:56 +0000
2010-11-24 22:06:14.832 ExpApp[24929:a0b] ----------------------------------------
2010-11-24 22:06:14.832 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22t12:34:56+09:00
2010-11-24 22:06:14.833 ExpApp[24929:a0b] NSDate object: 2010-11-22 12:34:56 +0900
2010-11-24 22:06:14.833 ExpApp[24929:a0b] Default TZ: 2010-11-22 12:34:56 +0900
2010-11-24 22:06:14.834 ExpApp[24929:a0b] UTC: 2010-11-22 03:34:56 +0000
2010-11-24 22:06:14.834 ExpApp[24929:a0b] ----------------------------------------
2010-11-24 22:06:14.834 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22T12:34:56.123+09:00
2010-11-24 22:06:14.835 ExpApp[24929:a0b] NSDate object: 2010-11-22 12:34:56 +0900
2010-11-24 22:06:14.836 ExpApp[24929:a0b] Default TZ: 2010-11-22 12:34:56 +0900
2010-11-24 22:06:14.836 ExpApp[24929:a0b] UTC: 2010-11-22 03:34:56 +0000
2010-11-24 22:06:14.836 ExpApp[24929:a0b] ----------------------------------------
2010-11-24 22:06:14.837 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22t12.34.56.123+09:00
2010-11-24 22:06:14.837 ExpApp[24929:a0b] NSDate object: 2010-11-22 12:34:56 +0900
2010-11-24 22:06:14.838 ExpApp[24929:a0b] Default TZ: 2010-11-22 12:34:56 +0900
2010-11-24 22:06:14.838 ExpApp[24929:a0b] UTC: 2010-11-22 03:34:56 +0000
2010-11-24 22:06:14.838 ExpApp[24929:a0b] ----------------------------------------
2010-11-24 22:06:14.839 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22T12:34:56-05:00
2010-11-24 22:06:14.839 ExpApp[24929:a0b] NSDate object: 2010-11-23 02:34:56 +0900
2010-11-24 22:06:14.839 ExpApp[24929:a0b] Default TZ: 2010-11-23 02:34:56 +0900
2010-11-24 22:06:14.840 ExpApp[24929:a0b] UTC: 2010-11-22 17:34:56 +0000
2010-11-24 22:06:14.840 ExpApp[24929:a0b] ----------------------------------------
2010-11-24 22:06:14.840 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22T12:34:56.123-05:00
2010-11-24 22:06:14.841 ExpApp[24929:a0b] NSDate object: 2010-11-23 02:34:56 +0900
2010-11-24 22:06:14.841 ExpApp[24929:a0b] Default TZ: 2010-11-23 02:34:56 +0900
2010-11-24 22:06:14.842 ExpApp[24929:a0b] UTC: 2010-11-22 17:34:56 +0000

参考文献

詳解 Objective-C 2.0
荻原 剛志
ソフトバンククリエイティブ ( 2008-05-28 )
ISBN: 9784797346800

関連リンク

関連記事

0 件のコメント:

コメントを投稿