PageRenderTime 26ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/WebServiceLocalTimeSoap/LocalTime/NSDate+ISO8601Parsing.m

https://gitlab.com/praveenvelanati/ios-demo
Objective C | 655 lines | 494 code | 85 blank | 76 comment | 141 complexity | d962eb3b1d2c92d3b715d8121b1af3b8 MD5 | raw file
  1. /*NSDate+ISO8601Parsing.m
  2. *
  3. *Created by Peter Hosey on 2006-02-20.
  4. *Copyright 2006 Peter Hosey. All rights reserved.
  5. *Modified by Matthew Faupel on 2009-05-06 to use NSDate instead of NSCalendarDate (for iPhone compatibility).
  6. *Modifications copyright 2009 Micropraxis Ltd.
  7. */
  8. #include <ctype.h>
  9. #include <string.h>
  10. #import "NSDate+ISO8601Parsing.h"
  11. #ifndef DEFAULT_TIME_SEPARATOR
  12. # define DEFAULT_TIME_SEPARATOR ':'
  13. #endif
  14. unichar ISO8601ParserDefaultTimeSeparatorCharacter = DEFAULT_TIME_SEPARATOR;
  15. static unsigned read_segment(const unsigned char *str, const unsigned char **next, unsigned *out_num_digits) {
  16. unsigned num_digits = 0U;
  17. unsigned value = 0U;
  18. while(isdigit(*str)) {
  19. value *= 10U;
  20. value += *str - '0';
  21. ++num_digits;
  22. ++str;
  23. }
  24. if(next) *next = str;
  25. if(out_num_digits) *out_num_digits = num_digits;
  26. return value;
  27. }
  28. static unsigned read_segment_4digits(const unsigned char *str, const unsigned char **next, unsigned *out_num_digits) {
  29. unsigned num_digits = 0U;
  30. unsigned value = 0U;
  31. if(isdigit(*str)) {
  32. value += *(str++) - '0';
  33. ++num_digits;
  34. }
  35. if(isdigit(*str)) {
  36. value *= 10U;
  37. value += *(str++) - '0';
  38. ++num_digits;
  39. }
  40. if(isdigit(*str)) {
  41. value *= 10U;
  42. value += *(str++) - '0';
  43. ++num_digits;
  44. }
  45. if(isdigit(*str)) {
  46. value *= 10U;
  47. value += *(str++) - '0';
  48. ++num_digits;
  49. }
  50. if(next) *next = str;
  51. if(out_num_digits) *out_num_digits = num_digits;
  52. return value;
  53. }
  54. static unsigned read_segment_2digits(const unsigned char *str, const unsigned char **next) {
  55. unsigned value = 0U;
  56. if(isdigit(*str))
  57. value += *str - '0';
  58. if(isdigit(*++str)) {
  59. value *= 10U;
  60. value += *(str++) - '0';
  61. }
  62. if(next) *next = str;
  63. return value;
  64. }
  65. //strtod doesn't support ',' as a separator. This does.
  66. static double read_double(const unsigned char *str, const unsigned char **next) {
  67. double value = 0.0;
  68. if(str) {
  69. unsigned int_value = 0;
  70. while(isdigit(*str)) {
  71. int_value *= 10U;
  72. int_value += (*(str++) - '0');
  73. }
  74. value = int_value;
  75. if(((*str == ',') || (*str == '.'))) {
  76. ++str;
  77. register double multiplier, multiplier_multiplier;
  78. multiplier = multiplier_multiplier = 0.1;
  79. while(isdigit(*str)) {
  80. value += (*(str++) - '0') * multiplier;
  81. multiplier *= multiplier_multiplier;
  82. }
  83. }
  84. }
  85. if(next) *next = str;
  86. return value;
  87. }
  88. static BOOL is_leap_year(unsigned year) {
  89. return \
  90. ((year % 4U) == 0U)
  91. && (((year % 100U) != 0U)
  92. || ((year % 400U) == 0U));
  93. }
  94. @implementation NSDate(ISO8601Parsing)
  95. /*Valid ISO 8601 date formats:
  96. *
  97. *YYYYMMDD
  98. *YYYY-MM-DD
  99. *YYYY-MM
  100. *YYYY
  101. *YY //century
  102. * //Implied century: YY is 00-99
  103. * YYMMDD
  104. * YY-MM-DD
  105. * -YYMM
  106. * -YY-MM
  107. * -YY
  108. * //Implied year
  109. * --MMDD
  110. * --MM-DD
  111. * --MM
  112. * //Implied year and month
  113. * ---DD
  114. * //Ordinal dates: DDD is the number of the day in the year (1-366)
  115. *YYYYDDD
  116. *YYYY-DDD
  117. * YYDDD
  118. * YY-DDD
  119. * -DDD
  120. * //Week-based dates: ww is the number of the week, and d is the number (1-7) of the day in the week
  121. *yyyyWwwd
  122. *yyyy-Www-d
  123. *yyyyWww
  124. *yyyy-Www
  125. *yyWwwd
  126. *yy-Www-d
  127. *yyWww
  128. *yy-Www
  129. * //Year of the implied decade
  130. *-yWwwd
  131. *-y-Www-d
  132. *-yWww
  133. *-y-Www
  134. * //Week and day of implied year
  135. * -Wwwd
  136. * -Www-d
  137. * //Week only of implied year
  138. * -Www
  139. * //Day only of implied week
  140. * -W-d
  141. */
  142. + (NSDate *)dateWithString:(NSString *)str strictly:(BOOL)strict timeSeparator:(unichar)timeSep getRange:(out NSRange *)outRange {
  143. NSCalendar *gregorian = [[NSCalendar alloc]
  144. initWithCalendarIdentifier:NSGregorianCalendar];
  145. NSDate *now = [NSDate date];
  146. NSDateComponents *dateComps = [gregorian components: NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit fromDate: now];
  147. unsigned
  148. //Date
  149. year,
  150. month_or_week = 1U,
  151. day = 1U,
  152. //Time
  153. hour = 0U;
  154. NSTimeInterval
  155. minute = 0.0,
  156. second = 0.0;
  157. //Time zone
  158. signed tz_hour = 0;
  159. signed tz_minute = 0;
  160. enum {
  161. monthAndDate,
  162. week,
  163. dateOnly
  164. } dateSpecification = monthAndDate;
  165. if(strict) timeSep = ISO8601ParserDefaultTimeSeparatorCharacter;
  166. NSAssert(timeSep != '\0', @"Time separator must not be NUL.");
  167. BOOL isValidDate = ([str length] > 0U);
  168. NSTimeZone *timeZone = nil;
  169. NSDate *date = nil;
  170. const unsigned char *ch = (const unsigned char *)[str UTF8String];
  171. NSRange range = { 0U, 0U };
  172. const unsigned char *start_of_date;
  173. if(strict && isspace(*ch)) {
  174. range.location = NSNotFound;
  175. isValidDate = NO;
  176. } else {
  177. //Skip leading whitespace.
  178. unsigned i = 0U;
  179. unsigned len = strlen((const char *)ch);
  180. for(; i < len; ++i) {
  181. if(!isspace(ch[i]))
  182. break;
  183. }
  184. range.location = i;
  185. ch += i;
  186. start_of_date = ch;
  187. unsigned segment;
  188. unsigned num_leading_hyphens = 0U, num_digits = 0U;
  189. if(*ch == 'T') {
  190. //There is no date here, only a time. Set the date to now; then we'll parse the time.
  191. isValidDate = isdigit(*++ch);
  192. year = [dateComps year];
  193. month_or_week = [dateComps month];
  194. day = [dateComps day];
  195. } else {
  196. while(*ch == '-') {
  197. ++num_leading_hyphens;
  198. ++ch;
  199. }
  200. segment = read_segment(ch, &ch, &num_digits);
  201. switch(num_digits) {
  202. case 0:
  203. if(*ch == 'W') {
  204. if((ch[1] == '-') && isdigit(ch[2]) && ((num_leading_hyphens == 1U) || ((num_leading_hyphens == 2U) && !strict))) {
  205. year = [dateComps year];
  206. month_or_week = 1U;
  207. ch += 2;
  208. goto parseDayAfterWeek;
  209. } else if(num_leading_hyphens == 1U) {
  210. year = [dateComps year];
  211. goto parseWeekAndDay;
  212. } else
  213. isValidDate = NO;
  214. } else
  215. isValidDate = NO;
  216. break;
  217. case 8: //YYYY MM DD
  218. if(num_leading_hyphens > 0U)
  219. isValidDate = NO;
  220. else {
  221. day = segment % 100U;
  222. segment /= 100U;
  223. month_or_week = segment % 100U;
  224. year = segment / 100U;
  225. }
  226. break;
  227. case 6: //YYMMDD (implicit century)
  228. if(num_leading_hyphens > 0U)
  229. isValidDate = NO;
  230. else {
  231. day = segment % 100U;
  232. segment /= 100U;
  233. month_or_week = segment % 100U;
  234. year = [dateComps year];
  235. year -= (year % 100U);
  236. year += segment / 100U;
  237. }
  238. break;
  239. case 4:
  240. switch(num_leading_hyphens) {
  241. case 0: //YYYY
  242. year = segment;
  243. if(*ch == '-') ++ch;
  244. if(!isdigit(*ch)) {
  245. if(*ch == 'W')
  246. goto parseWeekAndDay;
  247. else
  248. month_or_week = day = 1U;
  249. } else {
  250. segment = read_segment(ch, &ch, &num_digits);
  251. switch(num_digits) {
  252. case 4: //MMDD
  253. day = segment % 100U;
  254. month_or_week = segment / 100U;
  255. break;
  256. case 2: //MM
  257. month_or_week = segment;
  258. if(*ch == '-') ++ch;
  259. if(!isdigit(*ch))
  260. day = 1U;
  261. else
  262. day = read_segment(ch, &ch, NULL);
  263. break;
  264. case 3: //DDD
  265. day = segment % 1000U;
  266. dateSpecification = dateOnly;
  267. if(strict && (day > (365U + is_leap_year(year))))
  268. isValidDate = NO;
  269. break;
  270. default:
  271. isValidDate = NO;
  272. }
  273. }
  274. break;
  275. case 1: //YYMM
  276. month_or_week = segment % 100U;
  277. year = segment / 100U;
  278. if(*ch == '-') ++ch;
  279. if(!isdigit(*ch))
  280. day = 1U;
  281. else
  282. day = read_segment(ch, &ch, NULL);
  283. break;
  284. case 2: //MMDD
  285. day = segment % 100U;
  286. month_or_week = segment / 100U;
  287. year = [dateComps year];
  288. break;
  289. default:
  290. isValidDate = NO;
  291. } //switch(num_leading_hyphens) (4 digits)
  292. break;
  293. case 1:
  294. if(strict) {
  295. //Two digits only - never just one.
  296. if(num_leading_hyphens == 1U) {
  297. if(*ch == '-') ++ch;
  298. if(*++ch == 'W') {
  299. year = [dateComps year];
  300. year -= (year % 10U);
  301. year += segment;
  302. goto parseWeekAndDay;
  303. } else
  304. isValidDate = NO;
  305. } else
  306. isValidDate = NO;
  307. break;
  308. }
  309. case 2:
  310. switch(num_leading_hyphens) {
  311. case 0:
  312. if(*ch == '-') {
  313. //Implicit century
  314. year = [dateComps year];
  315. year -= (year % 100U);
  316. year += segment;
  317. if(*++ch == 'W')
  318. goto parseWeekAndDay;
  319. else if(!isdigit(*ch)) {
  320. goto centuryOnly;
  321. } else {
  322. //Get month and/or date.
  323. segment = read_segment_4digits(ch, &ch, &num_digits);
  324. NSLog(@"(%@) parsing month; segment is %u and ch is %s", str, segment, ch);
  325. switch(num_digits) {
  326. case 4: //YY-MMDD
  327. day = segment % 100U;
  328. month_or_week = segment / 100U;
  329. break;
  330. case 1: //YY-M; YY-M-DD (extension)
  331. if(strict) {
  332. isValidDate = NO;
  333. break;
  334. }
  335. case 2: //YY-MM; YY-MM-DD
  336. month_or_week = segment;
  337. if(*ch == '-') {
  338. if(isdigit(*++ch))
  339. day = read_segment_2digits(ch, &ch);
  340. else
  341. day = 1U;
  342. } else
  343. day = 1U;
  344. break;
  345. case 3: //Ordinal date.
  346. day = segment;
  347. dateSpecification = dateOnly;
  348. break;
  349. }
  350. }
  351. } else if(*ch == 'W') {
  352. year = [dateComps year];
  353. year -= (year % 100U);
  354. year += segment;
  355. parseWeekAndDay: //*ch should be 'W' here.
  356. if(!isdigit(*++ch)) {
  357. //Not really a week-based date; just a year followed by '-W'.
  358. if(strict)
  359. isValidDate = NO;
  360. else
  361. month_or_week = day = 1U;
  362. } else {
  363. month_or_week = read_segment_2digits(ch, &ch);
  364. if(*ch == '-') ++ch;
  365. parseDayAfterWeek:
  366. day = isdigit(*ch) ? read_segment_2digits(ch, &ch) : 1U;
  367. dateSpecification = week;
  368. }
  369. } else {
  370. //Century only. Assume current year.
  371. centuryOnly:
  372. year = segment * 100U + [dateComps year] % 100U;
  373. month_or_week = day = 1U;
  374. }
  375. break;
  376. case 1:; //-YY; -YY-MM (implicit century)
  377. NSLog(@"(%@) found %u digits and one hyphen, so this is either -YY or -YY-MM; segment (year) is %u", str, num_digits, segment);
  378. unsigned current_year = [dateComps year];
  379. unsigned century = (current_year % 100U);
  380. year = segment + (current_year - century);
  381. if(num_digits == 1U) //implied decade
  382. year += century - (current_year % 10U);
  383. if(*ch == '-') {
  384. ++ch;
  385. month_or_week = read_segment_2digits(ch, &ch);
  386. NSLog(@"(%@) month is %u", str, month_or_week);
  387. } else {
  388. month_or_week = 1U;
  389. }
  390. day = 1U;
  391. break;
  392. case 2: //--MM; --MM-DD
  393. year = [dateComps year];
  394. month_or_week = segment;
  395. if(*ch == '-') {
  396. ++ch;
  397. day = read_segment_2digits(ch, &ch);
  398. } else {
  399. day = 1U;
  400. }
  401. break;
  402. case 3: //---DD
  403. year = [dateComps year];
  404. month_or_week = [dateComps month];
  405. day = segment;
  406. break;
  407. default:
  408. isValidDate = NO;
  409. } //switch(num_leading_hyphens) (2 digits)
  410. break;
  411. case 7: //YYYY DDD (ordinal date)
  412. if(num_leading_hyphens > 0U)
  413. isValidDate = NO;
  414. else {
  415. day = segment % 1000U;
  416. year = segment / 1000U;
  417. dateSpecification = dateOnly;
  418. if(strict && (day > (365U + is_leap_year(year))))
  419. isValidDate = NO;
  420. }
  421. break;
  422. case 3: //--DDD (ordinal date, implicit year)
  423. //Technically, the standard only allows one hyphen. But it says that two hyphens is the logical implementation, and one was dropped for brevity. So I have chosen to allow the missing hyphen.
  424. if((num_leading_hyphens < 1U) || ((num_leading_hyphens > 2U) && !strict))
  425. isValidDate = NO;
  426. else {
  427. day = segment;
  428. year = [dateComps year];
  429. dateSpecification = dateOnly;
  430. if(strict && (day > (365U + is_leap_year(year))))
  431. isValidDate = NO;
  432. }
  433. break;
  434. default:
  435. isValidDate = NO;
  436. }
  437. }
  438. if(isValidDate) {
  439. if(isspace(*ch) || (*ch == 'T')) ++ch;
  440. if(isdigit(*ch)) {
  441. hour = read_segment_2digits(ch, &ch);
  442. if(*ch == timeSep) {
  443. ++ch;
  444. if((timeSep == ',') || (timeSep == '.')) {
  445. //We can't do fractional minutes when '.' is the segment separator.
  446. //Only allow whole minutes and whole seconds.
  447. minute = read_segment_2digits(ch, &ch);
  448. if(*ch == timeSep) {
  449. ++ch;
  450. second = read_segment_2digits(ch, &ch);
  451. }
  452. } else {
  453. //Allow a fractional minute.
  454. //If we don't get a fraction, look for a seconds segment.
  455. //Otherwise, the fraction of a minute is the seconds.
  456. minute = read_double(ch, &ch);
  457. second = modf(minute, &minute);
  458. if(second > DBL_EPSILON)
  459. second *= 60.0; //Convert fraction (e.g. .5) into seconds (e.g. 30).
  460. else if(*ch == timeSep) {
  461. ++ch;
  462. second = read_double(ch, &ch);
  463. }
  464. }
  465. }
  466. switch(*ch) {
  467. case 'Z':
  468. timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
  469. break;
  470. case '+':
  471. case '-':;
  472. BOOL negative = (*ch == '-');
  473. if(isdigit(*++ch)) {
  474. //Read hour offset.
  475. segment = *ch - '0';
  476. if(isdigit(*++ch)) {
  477. segment *= 10U;
  478. segment += *(ch++) - '0';
  479. }
  480. tz_hour = (signed)segment;
  481. if(negative) tz_hour = -tz_hour;
  482. //Optional separator.
  483. if(*ch == timeSep) ++ch;
  484. if(isdigit(*ch)) {
  485. //Read minute offset.
  486. segment = *ch - '0';
  487. if(isdigit(*++ch)) {
  488. segment *= 10U;
  489. segment += *ch - '0';
  490. }
  491. tz_minute = segment;
  492. if(negative) tz_minute = -tz_minute;
  493. }
  494. timeZone = [NSTimeZone timeZoneForSecondsFromGMT:(tz_hour * 3600) + (tz_minute * 60)];
  495. }
  496. }
  497. }
  498. }
  499. if(isValidDate) {
  500. if (timeZone != nil)
  501. [gregorian setTimeZone: timeZone];
  502. switch(dateSpecification) {
  503. case monthAndDate:
  504. [dateComps setYear: year];
  505. [dateComps setMonth: month_or_week];
  506. [dateComps setDay: day];
  507. [dateComps setHour: hour];
  508. [dateComps setMinute: minute];
  509. [dateComps setSecond: second];
  510. date = [gregorian dateFromComponents: dateComps];
  511. break;
  512. case week:;
  513. //Adapted from <http://personal.ecu.edu/mccartyr/ISOwdALG.txt>.
  514. //This works by converting the week date into an ordinal date, then letting the next case handle it.
  515. unsigned prevYear = year - 1U;
  516. unsigned YY = prevYear % 100U;
  517. unsigned C = prevYear - YY;
  518. unsigned G = YY + YY / 4U;
  519. unsigned isLeapYear = (((C / 100U) % 4U) * 5U);
  520. unsigned Jan1Weekday = (isLeapYear + G) % 7U;
  521. enum { monday, tuesday, wednesday, thursday/*, friday, saturday, sunday*/ };
  522. day = ((8U - Jan1Weekday) + (7U * (Jan1Weekday > thursday))) + (day - 1U) + (7U * (month_or_week - 2));
  523. case dateOnly: //An "ordinal date".
  524. [dateComps setYear: year];
  525. [dateComps setMonth: 1];
  526. [dateComps setDay: 1];
  527. [dateComps setHour: hour];
  528. [dateComps setMinute: minute];
  529. [dateComps setSecond: second];
  530. date = [gregorian dateFromComponents: dateComps];
  531. [dateComps setYear: 0];
  532. [dateComps setMonth: 0];
  533. [dateComps setDay: (day - 1)];
  534. [dateComps setHour: 0];
  535. [dateComps setMinute: 0];
  536. [dateComps setSecond: 0];
  537. date = [gregorian dateByAddingComponents: dateComps toDate: date options: 0];
  538. break;
  539. }
  540. }
  541. } //if(!(strict && isdigit(ch[0])))
  542. if(outRange) {
  543. if(isValidDate)
  544. range.length = ch - start_of_date;
  545. else
  546. range.location = NSNotFound;
  547. *outRange = range;
  548. }
  549. [gregorian release];
  550. return date;
  551. }
  552. + (NSDate *)dateWithString:(NSString *)str {
  553. return [self dateWithString:str strictly:NO getRange:NULL];
  554. }
  555. + (NSDate *)dateWithString:(NSString *)str strictly:(BOOL)strict {
  556. return [self dateWithString:str strictly:strict getRange:NULL];
  557. }
  558. + (NSDate *)dateWithString:(NSString *)str strictly:(BOOL)strict getRange:(out NSRange *)outRange {
  559. return [self dateWithString:str strictly:strict timeSeparator:ISO8601ParserDefaultTimeSeparatorCharacter getRange:NULL];
  560. }
  561. + (NSDate *)dateWithString:(NSString *)str timeSeparator:(unichar)timeSep getRange:(out NSRange *)outRange {
  562. return [self dateWithString:str strictly:NO timeSeparator:timeSep getRange:outRange];
  563. }
  564. + (NSDate *)dateWithString:(NSString *)str timeSeparator:(unichar)timeSep {
  565. return [self dateWithString:str strictly:NO timeSeparator:timeSep getRange:NULL];
  566. }
  567. + (NSDate *)dateWithString:(NSString *)str getRange:(out NSRange *)outRange {
  568. return [self dateWithString:str strictly:NO timeSeparator:ISO8601ParserDefaultTimeSeparatorCharacter getRange:outRange];
  569. }
  570. @end