GTLRDateTime.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. /* Copyright (c) 2011 Google Inc.
  2. *
  3. * Licensed under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License.
  5. * You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software
  10. * distributed under the License is distributed on an "AS IS" BASIS,
  11. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. * See the License for the specific language governing permissions and
  13. * limitations under the License.
  14. */
  15. //#import <GoogleAPIClientForREST/GTLRDateTime.h>
  16. #import "GTLRDateTime.h"
  17. static NSUInteger const kGTLRDateComponentBits = (NSCalendarUnitYear | NSCalendarUnitMonth
  18. | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute
  19. | NSCalendarUnitSecond);
  20. @interface GTLRDateTime ()
  21. - (void)setFromDate:(NSDate *)date;
  22. - (void)setFromRFC3339String:(NSString *)str;
  23. @property(nonatomic, copy, readwrite) NSDateComponents *dateComponents;
  24. @property(nonatomic, assign, readwrite) NSInteger milliseconds;
  25. @property(nonatomic, strong, readwrite, nullable) NSNumber *offsetMinutes;
  26. @property(nonatomic, assign, readwrite) BOOL hasTime;
  27. @end
  28. @implementation GTLRDateTime {
  29. NSDate *_cachedDate;
  30. NSString *_cachedRFC3339String;
  31. }
  32. // A note about _milliseconds:
  33. // RFC 3339 has support for fractions of a second. NSDateComponents is all
  34. // NSInteger based, so it can't handle a fraction of a second. NSDate is
  35. // built on NSTimeInterval so it has sub-millisecond precision. GTLR takes
  36. // the compromise of supporting the RFC's optional fractional second support
  37. // by maintaining a number of milliseconds past what fits in the
  38. // NSDateComponents. The parsing and string conversions will include
  39. // 3 decimal digits (hence milliseconds). When going to a string, the decimal
  40. // digits are only included if the milliseconds are non zero.
  41. @dynamic date;
  42. @dynamic RFC3339String;
  43. @dynamic stringValue;
  44. @dynamic hasTime;
  45. @synthesize dateComponents = _dateComponents,
  46. milliseconds = _milliseconds,
  47. offsetMinutes = _offsetMinutes;
  48. + (instancetype)dateTimeWithRFC3339String:(NSString *)str {
  49. if (str == nil) return nil;
  50. GTLRDateTime *result = [[self alloc] init];
  51. [result setFromRFC3339String:str];
  52. return result;
  53. }
  54. + (instancetype)dateTimeWithDate:(NSDate *)date {
  55. if (date == nil) return nil;
  56. GTLRDateTime *result = [[self alloc] init];
  57. [result setFromDate:date];
  58. return result;
  59. }
  60. + (instancetype)dateTimeWithDate:(NSDate *)date
  61. offsetMinutes:(NSInteger)offsetMinutes {
  62. GTLRDateTime *result = [self dateTimeWithDate:date];
  63. result.offsetMinutes = @(offsetMinutes);
  64. return result;
  65. }
  66. + (instancetype)dateTimeForAllDayWithDate:(NSDate *)date {
  67. if (date == nil) return nil;
  68. GTLRDateTime *result = [[self alloc] init];
  69. [result setFromDate:date];
  70. result.hasTime = NO;
  71. return result;
  72. }
  73. + (instancetype)dateTimeWithDateComponents:(NSDateComponents *)components {
  74. NSCalendar *cal = components.calendar ?: [self calendar];
  75. NSDate *date = [cal dateFromComponents:components];
  76. return [self dateTimeWithDate:date];
  77. }
  78. - (id)copyWithZone:(NSZone *)zone {
  79. // Object is immutable
  80. return self;
  81. }
  82. - (BOOL)isEqual:(GTLRDateTime *)other {
  83. if (self == other) return YES;
  84. if (![other isKindOfClass:[GTLRDateTime class]]) return NO;
  85. BOOL areDateComponentsEqual = [self.dateComponents isEqual:other.dateComponents];
  86. if (!areDateComponentsEqual) return NO;
  87. NSNumber *offsetMinutes = self.offsetMinutes;
  88. NSNumber *otherOffsetMinutes = other.offsetMinutes;
  89. if ((offsetMinutes == nil) != (otherOffsetMinutes == nil)
  90. || (offsetMinutes.integerValue != otherOffsetMinutes.integerValue)) return NO;
  91. return (self.milliseconds == other.milliseconds);
  92. }
  93. - (NSUInteger)hash {
  94. return [[self date] hash];
  95. }
  96. - (NSString *)description {
  97. return [NSString stringWithFormat:@"%@ %p: {%@}",
  98. [self class], self, self.RFC3339String];
  99. }
  100. - (NSDate *)date {
  101. @synchronized(self) {
  102. if (_cachedDate) return _cachedDate;
  103. }
  104. NSDateComponents *dateComponents = self.dateComponents;
  105. NSTimeInterval extraMillisecondsAsSeconds = 0.0;
  106. NSCalendar *cal = [[self class] calendar];
  107. if (!self.hasTime) {
  108. // We're not keeping track of a time, but NSDate always is based on
  109. // an absolute time. We want to avoid returning an NSDate where the
  110. // calendar date appears different from what was used to create our
  111. // date-time object.
  112. //
  113. // We'll make a copy of the date components, setting the time on our
  114. // copy to noon GMT, since that ensures the date renders correctly for
  115. // any time zone.
  116. NSDateComponents *noonDateComponents = [dateComponents copy];
  117. [noonDateComponents setHour:12];
  118. [noonDateComponents setMinute:0];
  119. [noonDateComponents setSecond:0];
  120. dateComponents = noonDateComponents;
  121. } else {
  122. // Add in the fractional seconds that don't fit into NSDateComponents.
  123. extraMillisecondsAsSeconds = ((NSTimeInterval)self.milliseconds) / 1000.0;
  124. }
  125. NSDate *date = [cal dateFromComponents:dateComponents];
  126. // Add in any milliseconds that didn't fit into the dateComponents.
  127. if (extraMillisecondsAsSeconds > 0.0) {
  128. date = [date dateByAddingTimeInterval:extraMillisecondsAsSeconds];
  129. }
  130. @synchronized(self) {
  131. _cachedDate = date;
  132. }
  133. return date;
  134. }
  135. - (NSString *)stringValue {
  136. return self.RFC3339String;
  137. }
  138. - (NSString *)RFC3339String {
  139. @synchronized(self) {
  140. if (_cachedRFC3339String) return _cachedRFC3339String;
  141. }
  142. NSDateComponents *dateComponents = self.dateComponents;
  143. NSString *timeString = @""; // timeString like "T15:10:46-08:00"
  144. if (self.hasTime) {
  145. NSString *fractionalSecondsString = @"";
  146. if (self.milliseconds > 0.0) {
  147. fractionalSecondsString = [NSString stringWithFormat:@".%03ld", (long)self.milliseconds];
  148. }
  149. // If the dateTime was created from a string with a time offset, render that back in
  150. // and adjust the time.
  151. NSString *offsetStr = @"Z";
  152. NSNumber *offsetMinutes = self.offsetMinutes;
  153. if (offsetMinutes != nil) {
  154. BOOL isNegative = NO;
  155. NSInteger offsetVal = offsetMinutes.integerValue;
  156. if (offsetVal < 0) {
  157. isNegative = YES;
  158. offsetVal = -offsetVal;
  159. }
  160. NSInteger mins = offsetVal % 60;
  161. NSInteger hours = (offsetVal - mins) / 60;
  162. offsetStr = [NSString stringWithFormat:@"%c%02ld:%02ld",
  163. isNegative ? '-' : '+', (long)hours, (long)mins];
  164. // Adjust date components back to account for the offset.
  165. //
  166. // This is the inverse of the adjustment done in setFromRFC3339String:.
  167. if (offsetVal != 0) {
  168. NSDate *adjustedDate =
  169. [self.date dateByAddingTimeInterval:(offsetMinutes.integerValue * 60)];
  170. NSCalendar *calendar = [[self class] calendar];
  171. dateComponents = [calendar components:kGTLRDateComponentBits
  172. fromDate:adjustedDate];
  173. }
  174. }
  175. timeString = [NSString stringWithFormat:@"T%02ld:%02ld:%02ld%@%@",
  176. (long)dateComponents.hour, (long)dateComponents.minute,
  177. (long)dateComponents.second, fractionalSecondsString,
  178. offsetStr];
  179. }
  180. // full dateString like "2006-11-17T15:10:46-08:00"
  181. NSString *dateString = [NSString stringWithFormat:@"%04ld-%02ld-%02ld%@",
  182. (long)dateComponents.year, (long)dateComponents.month,
  183. (long)dateComponents.day, timeString];
  184. @synchronized(self) {
  185. _cachedRFC3339String = dateString;
  186. }
  187. return dateString;
  188. }
  189. - (void)setFromDate:(NSDate *)date {
  190. NSCalendar *cal = [[self class] calendar];
  191. NSDateComponents *components = [cal components:kGTLRDateComponentBits
  192. fromDate:date];
  193. self.dateComponents = components;
  194. // Extract the fractional seconds.
  195. NSTimeInterval asTimeInterval = [date timeIntervalSince1970];
  196. NSTimeInterval worker = asTimeInterval - trunc(asTimeInterval);
  197. self.milliseconds = (NSInteger)round(worker * 1000.0);
  198. }
  199. - (void)setFromRFC3339String:(NSString *)str {
  200. static NSCharacterSet *gDashSet;
  201. static NSCharacterSet *gTSet;
  202. static NSCharacterSet *gColonSet;
  203. static NSCharacterSet *gPlusMinusZSet;
  204. static dispatch_once_t onceToken;
  205. dispatch_once(&onceToken, ^{
  206. gDashSet = [NSCharacterSet characterSetWithCharactersInString:@"-"];
  207. gTSet = [NSCharacterSet characterSetWithCharactersInString:@"Tt "];
  208. gColonSet = [NSCharacterSet characterSetWithCharactersInString:@":"];
  209. gPlusMinusZSet = [NSCharacterSet characterSetWithCharactersInString:@"+-zZ"];
  210. });
  211. NSInteger year = NSDateComponentUndefined;
  212. NSInteger month = NSDateComponentUndefined;
  213. NSInteger day = NSDateComponentUndefined;
  214. NSInteger hour = NSDateComponentUndefined;
  215. NSInteger minute = NSDateComponentUndefined;
  216. NSInteger sec = NSDateComponentUndefined;
  217. NSInteger milliseconds = 0;
  218. double secDouble = -1.0;
  219. NSString* sign = nil;
  220. NSInteger offsetHour = 0;
  221. NSInteger offsetMinute = 0;
  222. if (str.length > 0) {
  223. NSScanner* scanner = [NSScanner scannerWithString:str];
  224. // There should be no whitespace, so no skip characters.
  225. [scanner setCharactersToBeSkipped:nil];
  226. // for example, scan 2006-11-17T15:10:46-08:00
  227. // or 2006-11-17T15:10:46Z
  228. if (// yyyy-mm-dd
  229. [scanner scanInteger:&year] &&
  230. [scanner scanCharactersFromSet:gDashSet intoString:NULL] &&
  231. [scanner scanInteger:&month] &&
  232. [scanner scanCharactersFromSet:gDashSet intoString:NULL] &&
  233. [scanner scanInteger:&day] &&
  234. // Thh:mm:ss
  235. [scanner scanCharactersFromSet:gTSet intoString:NULL] &&
  236. [scanner scanInteger:&hour] &&
  237. [scanner scanCharactersFromSet:gColonSet intoString:NULL] &&
  238. [scanner scanInteger:&minute] &&
  239. [scanner scanCharactersFromSet:gColonSet intoString:NULL] &&
  240. [scanner scanDouble:&secDouble]) {
  241. // At this point we got secDouble, pull it apart.
  242. sec = (NSInteger)secDouble;
  243. double worker = secDouble - ((double)sec);
  244. milliseconds = (NSInteger)round(worker * 1000.0);
  245. // Finish parsing, now the offset info.
  246. if (// Z or +hh:mm
  247. [scanner scanCharactersFromSet:gPlusMinusZSet intoString:&sign] &&
  248. [scanner scanInteger:&offsetHour] &&
  249. [scanner scanCharactersFromSet:gColonSet intoString:NULL] &&
  250. [scanner scanInteger:&offsetMinute]) {
  251. }
  252. }
  253. }
  254. NSDateComponents *dateComponents = [[NSDateComponents alloc] init];
  255. [dateComponents setYear:year];
  256. [dateComponents setMonth:month];
  257. [dateComponents setDay:day];
  258. [dateComponents setHour:hour];
  259. [dateComponents setMinute:minute];
  260. [dateComponents setSecond:sec];
  261. BOOL isMinusOffset = [sign isEqual:@"-"];
  262. if (isMinusOffset || [sign isEqual:@"+"]) {
  263. NSInteger totalOffsetMinutes = ((offsetHour * 60) + offsetMinute) * (isMinusOffset ? -1 : 1);
  264. self.offsetMinutes = @(totalOffsetMinutes);
  265. // Minus offset means Universal time is that many hours and minutes ahead.
  266. //
  267. // This is the inverse of the adjustment done above in RFC3339String.
  268. NSTimeInterval deltaOffsetSeconds = -totalOffsetMinutes * 60;
  269. NSCalendar *calendar = [[self class] calendar];
  270. NSDate *scannedDate = [calendar dateFromComponents:dateComponents];
  271. NSDate *offsetDate = [scannedDate dateByAddingTimeInterval:deltaOffsetSeconds];
  272. dateComponents = [calendar components:kGTLRDateComponentBits
  273. fromDate:offsetDate];
  274. }
  275. self.dateComponents = dateComponents;
  276. self.milliseconds = milliseconds;
  277. }
  278. - (BOOL)hasTime {
  279. NSDateComponents *dateComponents = self.dateComponents;
  280. BOOL hasTime = ([dateComponents hour] != NSDateComponentUndefined
  281. && [dateComponents minute] != NSDateComponentUndefined);
  282. return hasTime;
  283. }
  284. - (void)setHasTime:(BOOL)shouldHaveTime {
  285. // We'll set time values to zero or kUndefinedDateComponent as appropriate.
  286. BOOL hadTime = self.hasTime;
  287. if (shouldHaveTime && !hadTime) {
  288. [_dateComponents setHour:0];
  289. [_dateComponents setMinute:0];
  290. [_dateComponents setSecond:0];
  291. _milliseconds = 0;
  292. } else if (hadTime && !shouldHaveTime) {
  293. [_dateComponents setHour:NSDateComponentUndefined];
  294. [_dateComponents setMinute:NSDateComponentUndefined];
  295. [_dateComponents setSecond:NSDateComponentUndefined];
  296. _milliseconds = 0;
  297. }
  298. }
  299. + (NSCalendar *)calendar {
  300. NSCalendar *cal = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
  301. cal.timeZone = (NSTimeZone * _Nonnull)[NSTimeZone timeZoneWithName:@"Universal"];
  302. return cal;
  303. }
  304. @end