Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Set context date for NSDataDetector

Suppose today is January 20th, 2014. If I use NSDataDetector to extract a date from the string "tomorrow at 4pm", I'll get 2014-01-21T16:00. Great.

But, suppose I want NSDataDetector to pretend the current date is January 14th, 2014. This way, when I parse "tomorrow at 4pm", I'll get 2014-01-15T16:00. If I change the system time on the device, I get what I want. But, is there a way to specify this programmatically?

Thanks.

like image 920
andros Avatar asked Jan 20 '14 21:01

andros


2 Answers

For testing purposes, you can use a technique called method swizzling. The trick is to replace one of NSDate's methods with one of your own.

If you replace +[NSDate date] with your own implementation, NSDataDetector will consider 'now' to be any time you specify.

Swizzling system class methods in production code is risky. The following example code ignores the Encapsulation of NSDataDetector by taking advantage of knowing that it uses NSDate privately. One of many potential pitfalls would be if the next update to iOS changes the internals of NSDataDetector, your production app may stop working correctly for your end-users unexpectedly.

Add a category to NSDate like this (an aside: if you are building libraries to run on the device, you may need to specify the -all_load linker flag to load categories from libs):

#include <objc/runtime.h>

 @implementation NSDate(freezeDate)

static NSDate *_freezeDate;

// Freeze NSDate to a point in time.
// PROBABLY NOT A GOOD IDEA FOR PRODUCTION CODE
+(void)freezeToDate:(NSDate*)date
{
    if(_freezeDate != nil) [NSDate unfreeze];
    _freezeDate = date;
    Method _original_date_method = class_getClassMethod([NSDate class], @selector(date));
    Method _fake_date_method = class_getClassMethod([self class], @selector(fakeDate));
    method_exchangeImplementations(_original_date_method, _fake_date_method);
}

// Unfreeze NSDate so that now will really be now.
+ (void)unfreeze
{
    if(_freezeDate == nil) return;
    _freezeDate = nil;
    Method _original_date_method = class_getClassMethod([NSDate class], @selector(date));
    Method _fake_date_method = class_getClassMethod([self class], @selector(fakeDate));
    method_exchangeImplementations(_original_date_method, _fake_date_method);
}

+ (NSDate *)fakeDate
{
    return _freezeDate;
}

@end

Here is it being used:

- (void)someTestingFunction:(NSNotification *)aNotification
{
    // Set date to be frozen at a point one week ago from now.
    [NSDate freezeToDate:[NSDate dateWithTimeIntervalSinceNow:(-3600*24*7)]];

    NSString *userInput = @"tomorrow at 7pm";
    NSError *error = nil;
    NSRange range = NSMakeRange(0, userInput.length);
    NSDataDetector *dd = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeDate error:&error];
    [dd enumerateMatchesInString:userInput
                         options:0
                           range:range
                      usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop) {
                          NSLog(@"From one week ago: %@", match);
                      }];

    // Return date to normal
    [NSDate unfreeze];

    [dd enumerateMatchesInString:userInput
                         options:0
                           range:range
                      usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop) {
                          NSLog(@"From now: %@", match);
                      }];

}

Which outputs:

2014-01-20 19:35:57.525 TestObjectiveC2[6167:303] From one week ago: {0, 15}{2014-01-15 03:00:00 +0000}
2014-01-20 19:35:57.526 TestObjectiveC2[6167:303] From now: {0, 15}{2014-01-22 03:00:00 +0000}
like image 197
Phillip Kinkade Avatar answered Sep 27 '22 23:09

Phillip Kinkade


There is no way to do this with NSDataDetector. Please file a bug with Apple to request this functionality.

like image 31
Thomas Deniau Avatar answered Sep 27 '22 21:09

Thomas Deniau