/* Copyright (c) 2011 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // // GTLDateTime.m // #import "GTLDateTime.h" @interface GTLDateTime () - (void)setFromDate:(NSDate *)date timeZone:(NSTimeZone *)tz; - (void)setFromRFC3339String:(NSString *)str; @property (nonatomic, retain, readwrite) NSTimeZone *timeZone; @property (nonatomic, copy, readwrite) NSDateComponents *dateComponents; @property (nonatomic, assign, readwrite) NSInteger milliseconds; @property (nonatomic, assign, readwrite) BOOL hasTime; @property (nonatomic, assign, readwrite) NSInteger offsetSeconds; @property (nonatomic, assign, getter=isUniversalTime, readwrite) BOOL universalTime; @end static NSCharacterSet *gDashSet = nil; static NSCharacterSet *gTSet = nil; static NSCharacterSet *gColonSet = nil; static NSCharacterSet *gPlusMinusZSet = nil; static NSMutableDictionary *gCalendarsForTimeZones = nil; @implementation GTLDateTime // A note about milliseconds_: // RFC 3339 has support for fractions of a second. NSDateComponents is all // NSInteger based, so it can't handle a fraction of a second. NSDate is // built on NSTimeInterval so it has sub-millisecond precision. GTL takes // the compromise of supporting the RFC's optional fractional second support // by maintaining a number of milliseconds past what fits in the // NSDateComponents. The parsing and string conversions will include // 3 decimal digits (hence milliseconds). When going to a string, the decimal // digits are only included if the milliseconds are non zero. @dynamic date; @dynamic calendar; @dynamic RFC3339String; @dynamic stringValue; @dynamic timeZone; @dynamic hasTime; @synthesize dateComponents = dateComponents_, milliseconds = milliseconds_, offsetSeconds = offsetSeconds_, universalTime = isUniversalTime_; + (void)initialize { // Note that initialize is guaranteed by the runtime to be called in a // thread-safe manner. if (gDashSet == nil) { gDashSet = [[NSCharacterSet characterSetWithCharactersInString:@"-"] retain]; gTSet = [[NSCharacterSet characterSetWithCharactersInString:@"Tt "] retain]; gColonSet = [[NSCharacterSet characterSetWithCharactersInString:@":"] retain]; gPlusMinusZSet = [[NSCharacterSet characterSetWithCharactersInString:@"+-zZ"] retain]; gCalendarsForTimeZones = [[NSMutableDictionary alloc] init]; } } + (GTLDateTime *)dateTimeWithRFC3339String:(NSString *)str { if (str == nil) return nil; GTLDateTime *result = [[[self alloc] init] autorelease]; [result setFromRFC3339String:str]; return result; } + (GTLDateTime *)dateTimeWithDate:(NSDate *)date timeZone:(NSTimeZone *)tz { if (date == nil) return nil; GTLDateTime *result = [[[self alloc] init] autorelease]; [result setFromDate:date timeZone:tz]; return result; } + (GTLDateTime *)dateTimeForAllDayWithDate:(NSDate *)date { if (date == nil) return nil; GTLDateTime *result = [[[self alloc] init] autorelease]; [result setFromDate:date timeZone:nil]; result.hasTime = NO; return result; } + (GTLDateTime *)dateTimeWithDateComponents:(NSDateComponents *)components { NSCalendar *cal = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease]; NSDate *date = [cal dateFromComponents:components]; #if GTL_IPHONE NSTimeZone *tz = [components timeZone]; #else // NSDateComponents added timeZone: in Mac OS X 10.7. NSTimeZone *tz = nil; if ([components respondsToSelector:@selector(timeZone)]) { tz = [components timeZone]; } #endif return [self dateTimeWithDate:date timeZone:tz]; } - (void)dealloc { [dateComponents_ release]; [timeZone_ release]; [super dealloc]; } - (id)copyWithZone:(NSZone *)zone { // Object is immutable return [self retain]; } // until NSDateComponent implements isEqual, we'll use this - (BOOL)doesDateComponents:(NSDateComponents *)dc1 equalDateComponents:(NSDateComponents *)dc2 { return [dc1 era] == [dc2 era] && [dc1 year] == [dc2 year] && [dc1 month] == [dc2 month] && [dc1 day] == [dc2 day] && [dc1 hour] == [dc2 hour] && [dc1 minute] == [dc2 minute] && [dc1 second] == [dc2 second] && [dc1 week] == [dc2 week] && [dc1 weekday] == [dc2 weekday] && [dc1 weekdayOrdinal] == [dc2 weekdayOrdinal]; } - (BOOL)isEqual:(GTLDateTime *)other { if (self == other) return YES; if (![other isKindOfClass:[GTLDateTime class]]) return NO; BOOL areDateComponentsEqual = [self doesDateComponents:self.dateComponents equalDateComponents:other.dateComponents]; NSTimeZone *tz1 = self.timeZone; NSTimeZone *tz2 = other.timeZone; BOOL areTimeZonesEqual = (tz1 == tz2 || (tz2 && [tz1 isEqual:tz2])); return self.offsetSeconds == other.offsetSeconds && self.isUniversalTime == other.isUniversalTime && self.milliseconds == other.milliseconds && areDateComponentsEqual && areTimeZonesEqual; } - (NSString *)description { return [NSString stringWithFormat:@"%@ %p: {%@}", [self class], self, self.RFC3339String]; } - (NSTimeZone *)timeZone { if (timeZone_) { return timeZone_; } if (self.isUniversalTime) { NSTimeZone *ztz = [NSTimeZone timeZoneWithName:@"Universal"]; return ztz; } NSInteger offsetSeconds = self.offsetSeconds; if (offsetSeconds != NSUndefinedDateComponent) { NSTimeZone *tz = [NSTimeZone timeZoneForSecondsFromGMT:offsetSeconds]; return tz; } return nil; } - (void)setTimeZone:(NSTimeZone *)timeZone { [timeZone_ release]; timeZone_ = [timeZone retain]; if (timeZone) { NSInteger offsetSeconds = [timeZone secondsFromGMTForDate:self.date]; self.offsetSeconds = offsetSeconds; } else { self.offsetSeconds = NSUndefinedDateComponent; } } - (NSCalendar *)calendarForTimeZone:(NSTimeZone *)tz { NSCalendar *cal = nil; @synchronized(gCalendarsForTimeZones) { id tzKey = (tz ? tz : [NSNull null]); cal = [gCalendarsForTimeZones objectForKey:tzKey]; if (cal == nil) { cal = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease]; if (tz) { [cal setTimeZone:tz]; } [gCalendarsForTimeZones setObject:cal forKey:tzKey]; } } return cal; } - (NSCalendar *)calendar { NSTimeZone *tz = self.timeZone; return [self calendarForTimeZone:tz]; } - (NSDate *)date { NSDateComponents *dateComponents = self.dateComponents; NSTimeInterval extraMillisecondsAsSeconds = 0.0; NSCalendar *cal; if (!self.hasTime) { // We're not keeping track of a time, but NSDate always is based on // an absolute time. We want to avoid returning an NSDate where the // calendar date appears different from what was used to create our // date-time object. // // We'll make a copy of the date components, setting the time on our // copy to noon GMT, since that ensures the date renders correctly for // any time zone. NSDateComponents *noonDateComponents = [[dateComponents copy] autorelease]; [noonDateComponents setHour:12]; [noonDateComponents setMinute:0]; [noonDateComponents setSecond:0]; dateComponents = noonDateComponents; NSTimeZone *gmt = [NSTimeZone timeZoneWithName:@"Universal"]; cal = [self calendarForTimeZone:gmt]; } else { cal = self.calendar; // Add in the fractional seconds that don't fit into NSDateComponents. extraMillisecondsAsSeconds = ((NSTimeInterval)self.milliseconds) / 1000.0; } NSDate *date = [cal dateFromComponents:dateComponents]; // Add in any milliseconds that didn't fit into the dateComponents. if (extraMillisecondsAsSeconds > 0.0) { #if GTL_IPHONE || (MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_5) date = [date dateByAddingTimeInterval:extraMillisecondsAsSeconds]; #else date = [date addTimeInterval:extraMillisecondsAsSeconds]; #endif } return date; } - (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]; } NSString *fractionalSecondsString = @""; if (self.milliseconds > 0.0) { fractionalSecondsString = [NSString stringWithFormat:@".%03ld", (long)self.milliseconds]; } timeString = [NSString stringWithFormat:@"T%02ld:%02ld:%02ld%@%@", (long)[dateComponents hour], (long)[dateComponents minute], (long)[dateComponents second], fractionalSecondsString, 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; } - (void)setFromDate:(NSDate *)date timeZone:(NSTimeZone *)tz { NSCalendar *cal = [self calendarForTimeZone:tz]; NSUInteger const kComponentBits = (NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit); NSDateComponents *components = [cal components:kComponentBits fromDate:date]; self.dateComponents = components; // Extract the fractional seconds. NSTimeInterval asTimeInterval = [date timeIntervalSince1970]; NSTimeInterval worker = asTimeInterval - trunc(asTimeInterval); self.milliseconds = (NSInteger)round(worker * 1000.0); self.universalTime = NO; NSInteger offset = NSUndefinedDateComponent; if (tz) { offset = [tz secondsFromGMTForDate:date]; if (offset == 0 && [tz isEqualToTimeZone:[NSTimeZone timeZoneWithName:@"Universal"]]) { self.universalTime = YES; } } self.offsetSeconds = offset; // though offset seconds are authoritative, we'll retain the time zone // since we can't regenerate it reliably from just the offset timeZone_ = [tz retain]; } - (void)setFromRFC3339String:(NSString *)str { NSInteger year = NSUndefinedDateComponent; NSInteger month = NSUndefinedDateComponent; NSInteger day = NSUndefinedDateComponent; NSInteger hour = NSUndefinedDateComponent; NSInteger minute = NSUndefinedDateComponent; NSInteger sec = NSUndefinedDateComponent; NSInteger milliseconds = 0; double secDouble = -1.0; NSString* sign = nil; NSInteger offsetHour = 0; NSInteger offsetMinute = 0; if ([str length] > 0) { NSScanner* scanner = [NSScanner scannerWithString:str]; // There should be no whitespace, so no skip characters. [scanner setCharactersToBeSkipped:nil]; // for example, scan 2006-11-17T15:10:46-08:00 // or 2006-11-17T15:10:46Z if (// yyyy-mm-dd [scanner scanInteger:&year] && [scanner scanCharactersFromSet:gDashSet intoString:NULL] && [scanner scanInteger:&month] && [scanner scanCharactersFromSet:gDashSet intoString:NULL] && [scanner scanInteger:&day] && // Thh:mm:ss [scanner scanCharactersFromSet:gTSet intoString:NULL] && [scanner scanInteger:&hour] && [scanner scanCharactersFromSet:gColonSet intoString:NULL] && [scanner scanInteger:&minute] && [scanner scanCharactersFromSet:gColonSet intoString:NULL] && [scanner scanDouble:&secDouble]) { // At this point we got secDouble, pull it apart. sec = (NSInteger)secDouble; double worker = secDouble - ((double)sec); milliseconds = (NSInteger)round(worker * 1000.0); // Finish parsing, now the offset info. if (// Z or +hh:mm [scanner scanCharactersFromSet:gPlusMinusZSet intoString:&sign] && [scanner scanInteger:&offsetHour] && [scanner scanCharactersFromSet:gColonSet intoString:NULL] && [scanner scanInteger:&offsetMinute]) { } } } NSDateComponents *dateComponents = [[[NSDateComponents alloc] init] autorelease]; [dateComponents setYear:year]; [dateComponents setMonth:month]; [dateComponents setDay:day]; [dateComponents setHour:hour]; [dateComponents setMinute:minute]; [dateComponents setSecond:sec]; self.dateComponents = dateComponents; self.milliseconds = milliseconds; // determine the offset, like from Z, or -08:00:00.0 self.timeZone = nil; NSInteger totalOffset = NSUndefinedDateComponent; self.universalTime = NO; if ([sign caseInsensitiveCompare:@"Z"] == NSOrderedSame) { self.universalTime = YES; totalOffset = 0; } else if (sign != nil) { totalOffset = (60 * offsetMinute) + (60 * 60 * offsetHour); if ([sign isEqual:@"-"]) { if (totalOffset == 0) { // special case: offset of -0.00 means undefined offset totalOffset = NSUndefinedDateComponent; } else { totalOffset *= -1; } } } self.offsetSeconds = totalOffset; } - (BOOL)hasTime { NSDateComponents *dateComponents = self.dateComponents; BOOL hasTime = ([dateComponents hour] != NSUndefinedDateComponent && [dateComponents minute] != NSUndefinedDateComponent); return hasTime; } - (void)setHasTime:(BOOL)shouldHaveTime { // we'll set time values to zero or NSUndefinedDateComponent as appropriate BOOL hadTime = self.hasTime; if (shouldHaveTime && !hadTime) { [dateComponents_ setHour:0]; [dateComponents_ setMinute:0]; [dateComponents_ setSecond:0]; milliseconds_ = 0; offsetSeconds_ = NSUndefinedDateComponent; isUniversalTime_ = NO; } else if (hadTime && !shouldHaveTime) { [dateComponents_ setHour:NSUndefinedDateComponent]; [dateComponents_ setMinute:NSUndefinedDateComponent]; [dateComponents_ setSecond:NSUndefinedDateComponent]; milliseconds_ = 0; offsetSeconds_ = NSUndefinedDateComponent; isUniversalTime_ = NO; self.timeZone = nil; } } @end