296 lines
8.6 KiB
Objective-C
296 lines
8.6 KiB
Objective-C
#import "XMPPDateTimeProfiles.h"
|
|
#import "NSNumber+XMPP.h"
|
|
|
|
#if ! __has_feature(objc_arc)
|
|
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
|
#endif
|
|
|
|
static NSString * const kXEP0082SharedDateFormatterKey = @"xep0082_shared_date_formatter_key";
|
|
|
|
@interface XMPPDateTimeProfiles (PrivateAPI)
|
|
+ (NSDate *)parseDateTime:(NSString *)dateTimeStr withMandatoryTimeZone:(BOOL)mandatoryTZ;
|
|
@end
|
|
|
|
|
|
@implementation XMPPDateTimeProfiles
|
|
|
|
/**
|
|
* The following acronyms and characters are used from XEP-0082 to represent time-related concepts:
|
|
*
|
|
* CCYY four-digit year portion of Date
|
|
* MM two-digit month portion of Date
|
|
* DD two-digit day portion of Date
|
|
* - ISO 8601 separator among Date portions
|
|
* T ISO 8601 separator between Date and Time
|
|
* hh two-digit hour portion of Time (00 through 23)
|
|
* mm two-digit minutes portion of Time (00 through 59)
|
|
* ss two-digit seconds portion of Time (00 through 59)
|
|
* : ISO 8601 separator among Time portions
|
|
* . ISO 8601 separator between seconds and milliseconds
|
|
* sss fractional second addendum to Time (MAY contain any number of digits)
|
|
* TZD Time Zone Definition (either "Z" for UTC or "(+|-)hh:mm" for a specific time zone)
|
|
*
|
|
**/
|
|
|
|
+ (NSDate *)parseDate:(NSString *)dateStr
|
|
{
|
|
if ([dateStr length] < 10) return nil;
|
|
|
|
// The Date profile defines a date without including the time of day.
|
|
// The lexical representation is as follows:
|
|
//
|
|
// CCYY-MM-DD
|
|
//
|
|
// Example:
|
|
//
|
|
// 1776-07-04
|
|
|
|
NSDateFormatter *df = [self threadDateFormatter];
|
|
[df setFormatterBehavior:NSDateFormatterBehavior10_4]; // Use unicode patterns (as opposed to 10_3)
|
|
[df setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
|
|
[df setDateFormat:@"yyyy-MM-dd"];
|
|
|
|
NSDate *result = [df dateFromString:dateStr];
|
|
|
|
return result;
|
|
}
|
|
|
|
+ (NSDate *)parseTime:(NSString *)timeStr
|
|
{
|
|
// The Time profile is used to specify an instant of time that recurs (e.g., every day).
|
|
// The lexical representation is as follows:
|
|
//
|
|
// hh:mm:ss[.sss][TZD]
|
|
//
|
|
// The Time Zone Definition is optional; if included, it MUST be either UTC (denoted by addition
|
|
// of the character 'Z' to the end of the string) or some offset from UTC (denoted by addition
|
|
// of '[+|-]' and 'hh:mm' to the end of the string).
|
|
//
|
|
// Examples:
|
|
//
|
|
// 16:00:00
|
|
// 16:00:00Z
|
|
// 16:00:00+07:00
|
|
// 16:00:00.123
|
|
// 16:00:00.123Z
|
|
// 16:00:00.123+07:00
|
|
|
|
|
|
// Extract the current day so the result can be on the current day.
|
|
// Why do we bother doing this?
|
|
//
|
|
// First, it is rather intuitive.
|
|
// Second, if we don't we risk being on a date with a conflicting DST (daylight saving time).
|
|
//
|
|
// For example, -0800 instead of the current -0700.
|
|
// This can be rather confusing when printing the result.
|
|
|
|
NSDateFormatter *df = [self threadDateFormatter];
|
|
[df setFormatterBehavior:NSDateFormatterBehavior10_4]; // Use unicode patterns (as opposed to 10_3)
|
|
[df setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
|
|
[df setDateFormat:@"yyyy-MM-dd"];
|
|
|
|
NSString *today = [df stringFromDate:[NSDate date]];
|
|
|
|
NSString *dateTimeStr = [NSString stringWithFormat:@"%@T%@", today, timeStr];
|
|
|
|
return [self parseDateTime:dateTimeStr withMandatoryTimeZone:NO];
|
|
}
|
|
|
|
+ (NSDate *)parseDateTime:(NSString *)dateTimeStr
|
|
{
|
|
// The DateTime profile is used to specify a non-recurring moment in time to an accuracy of seconds (or,
|
|
// optionally, fractions of a second). The format is as follows:
|
|
//
|
|
// CCYY-MM-DDThh:mm:ss[.sss]TZD
|
|
//
|
|
// The Time Zone Definition is mandatory and MUST be either UTC (denoted by addition of the character 'Z'
|
|
// to the end of the string) or some offset from UTC (denoted by addition of '[+|-]' and 'hh:mm' to the
|
|
// end of the string).
|
|
//
|
|
// Examples:
|
|
//
|
|
// 1969-07-21T02:56:15Z
|
|
// 1969-07-20T21:56:15-05:00
|
|
// 1969-07-21T02:56:15.123Z
|
|
// 1969-07-20T21:56:15.123-05:00
|
|
|
|
return [self parseDateTime:dateTimeStr withMandatoryTimeZone:YES];
|
|
}
|
|
|
|
+ (NSDate *)parseDateTime:(NSString *)dateTimeStr withMandatoryTimeZone:(BOOL)mandatoryTZ
|
|
{
|
|
if ([dateTimeStr length] < 19) return nil;
|
|
|
|
// The DateTime profile is used to specify a non-recurring moment in time to an accuracy of seconds (or,
|
|
// optionally, fractions of a second). The format is as follows:
|
|
//
|
|
// CCYY-MM-DDThh:mm:ss[.sss]{TZD}
|
|
//
|
|
// Examples:
|
|
//
|
|
// 1969-07-21T02:56:15
|
|
// 1969-07-21T02:56:15Z
|
|
// 1969-07-20T21:56:15-05:00
|
|
// 1969-07-21T02:56:15.123
|
|
// 1969-07-21T02:56:15.123Z
|
|
// 1969-07-20T21:56:15.123-05:00
|
|
|
|
BOOL hasMilliseconds = NO;
|
|
BOOL hasTimeZoneInfo = NO;
|
|
BOOL hasTimeZoneOffset = NO;
|
|
NSInteger fractionalDigits = 0;
|
|
|
|
if ([dateTimeStr length] > 19)
|
|
{
|
|
unichar c = [dateTimeStr characterAtIndex:19];
|
|
|
|
// Check for optional milliseconds
|
|
if (c == '.')
|
|
{
|
|
hasMilliseconds = YES;
|
|
|
|
// At least one fractional digit?
|
|
if ([dateTimeStr length] < 21) return nil;
|
|
}
|
|
|
|
// Check for optional time zone info, which is at the last char (Z), or the
|
|
// char 6 chars from the end
|
|
if ([dateTimeStr characterAtIndex:[dateTimeStr length] - 1] == 'Z')
|
|
{
|
|
hasTimeZoneInfo = YES;
|
|
hasTimeZoneOffset = NO;
|
|
if (hasMilliseconds)
|
|
{
|
|
// 1969-07-21T02:56:15.1234Z -> 25 - 1 - 20 = 4
|
|
fractionalDigits = [dateTimeStr length] - 1 - 20;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
c = [dateTimeStr characterAtIndex:[dateTimeStr length] - 6];
|
|
if (c == '+' || c == '-')
|
|
{
|
|
hasTimeZoneInfo = YES;
|
|
hasTimeZoneOffset = YES;
|
|
if (hasMilliseconds)
|
|
{
|
|
// 1969-07-21T02:56:15.1234+00:00 -> 30 - 6 - 20 = 4
|
|
fractionalDigits = [dateTimeStr length] - 6 - 20;
|
|
}
|
|
}
|
|
else if (hasMilliseconds)
|
|
{
|
|
// 1969-07-21T02:56:15.1234 -> 24 - 20 = 4
|
|
fractionalDigits = [dateTimeStr length] - 20;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mandatoryTZ && !hasTimeZoneInfo) return nil;
|
|
|
|
NSDateFormatter *df = [self threadDateFormatter];
|
|
[df setFormatterBehavior:NSDateFormatterBehavior10_4]; // Use unicode patterns (as opposed to 10_3)
|
|
[df setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
|
|
[df setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss"];
|
|
|
|
NSDate *result = nil;
|
|
NSString *dateAndTime = [dateTimeStr substringToIndex:19];
|
|
NSString *fraction = fractionalDigits != 0 ? [NSString stringWithFormat:@"0%@", [dateTimeStr substringWithRange:NSMakeRange(19, fractionalDigits + 1)]] : nil;
|
|
|
|
if (hasTimeZoneInfo && !hasTimeZoneOffset)
|
|
{
|
|
[df setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
|
|
result = [df dateFromString:dateAndTime];
|
|
}
|
|
else if (hasTimeZoneInfo && hasTimeZoneOffset)
|
|
{
|
|
NSString *timeZone = [dateTimeStr substringFromIndex:[dateTimeStr length] - 6];
|
|
NSTimeZone *tz = [self parseTimeZoneOffset:timeZone];
|
|
if (tz == nil)
|
|
{
|
|
result = nil;
|
|
}
|
|
else
|
|
{
|
|
[df setTimeZone:tz];
|
|
result = [df dateFromString:dateAndTime];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = [df dateFromString:dateAndTime];
|
|
}
|
|
|
|
if (result && fraction)
|
|
{
|
|
static NSNumberFormatter *numberFormatter = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
numberFormatter = [[NSNumberFormatter alloc] init];
|
|
[numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
|
|
[numberFormatter setDecimalSeparator:@"."];
|
|
});
|
|
|
|
NSTimeInterval fractionInterval = [[numberFormatter numberFromString:fraction] doubleValue];
|
|
NSTimeInterval current = [result timeIntervalSinceReferenceDate];
|
|
result = [NSDate dateWithTimeIntervalSinceReferenceDate:floor(current) + fractionInterval];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
+ (NSTimeZone *)parseTimeZoneOffset:(NSString *)tzo
|
|
{
|
|
// The tzo value is supposed to start with '+' or '-'.
|
|
// Spec says: (+-)hh:mm
|
|
//
|
|
// hh : two-digit hour portion (00 through 23)
|
|
// mm : two-digit minutes portion (00 through 59)
|
|
|
|
if ([tzo length] != 6)
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
NSString *hoursStr = [tzo substringWithRange:NSMakeRange(1, 2)];
|
|
NSString *minutesStr = [tzo substringWithRange:NSMakeRange(4, 2)];
|
|
|
|
NSUInteger hours;
|
|
if (![NSNumber xmpp_parseString:hoursStr intoNSUInteger:&hours])
|
|
return nil;
|
|
|
|
NSUInteger minutes;
|
|
if (![NSNumber xmpp_parseString:minutesStr intoNSUInteger:&minutes])
|
|
return nil;
|
|
|
|
if (hours > 23) return nil;
|
|
if (minutes > 59) return nil;
|
|
|
|
NSInteger secondsOffset = (NSInteger)((hours * 60 * 60) + (minutes * 60));
|
|
|
|
if ([tzo hasPrefix:@"-"])
|
|
{
|
|
secondsOffset = -1 * secondsOffset;
|
|
}
|
|
else if (![tzo hasPrefix:@"+"])
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
return [NSTimeZone timeZoneForSecondsFromGMT:secondsOffset];
|
|
}
|
|
|
|
+ (NSDateFormatter *)threadDateFormatter {
|
|
NSMutableDictionary *currentThreadStorage = [[NSThread currentThread] threadDictionary];
|
|
NSDateFormatter *sharedDateFormatter = currentThreadStorage[kXEP0082SharedDateFormatterKey];
|
|
if (!sharedDateFormatter) {
|
|
sharedDateFormatter = [NSDateFormatter new];
|
|
currentThreadStorage[kXEP0082SharedDateFormatterKey] = sharedDateFormatter;
|
|
}
|
|
|
|
return sharedDateFormatter;
|
|
}
|
|
|
|
@end
|