8

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.

andros
  • 153
  • 1
  • 6

3 Answers3

2

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}
Community
  • 1
  • 1
Phillip Kinkade
  • 1,382
  • 12
  • 18
  • This is clever, but an extremely bad idea for production code. I worry that your answer does not sufficiently communicate that this is not intended for real use. – mattt Jan 21 '14 at 17:41
  • @mattt that's precisely why I started the answer with "For testing purposes". – Phillip Kinkade Jan 21 '14 at 18:28
  • I understand that. What I'm saying is that "For testing purposes" doesn't go far enough to explain why and under what context this answer is being provided. Given how many people blindly copy-paste code from SO, an answer like this becomes an attractive nuisance. – mattt Jan 21 '14 at 19:31
  • @mattt I've added a warning to the answer and a hard-to-miss code comment in the code to hopefully alert users that may blindly copy just the code. Thanks! – Phillip Kinkade Feb 13 '14 at 19:38
1

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

Thomas Deniau
  • 2,488
  • 1
  • 15
  • 15
0

As far as I can tell, there's no way to do that through NSDataDetector.

That said, it should be pretty straightforward to apply the offset manually—just calculate the NSDateComponenets for the desired offset from an NSCalendar, and then take your detected date from NSDataDetector and add the day date component offset as appropriate.

    NSDate *futureDate = [NSDate dateWithTimeIntervalSinceNow:100000]; // Offset by about a day from now
    __block NSDate *offsetDetectedDate = nil;

    NSString *string = @"Let's have lunch Tomorrow at Noon";
    NSDataDetector *dataDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeDate error:nil];
    [dataDetector enumerateMatchesInString:string options:0 range:NSMakeRange(0, [string length]) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
        if (result.resultType == NSTextCheckingTypeDate) {
            NSDateComponents *offsetDateComponents = [[NSCalendar currentCalendar] components:NSDayCalendarUnit fromDate:result.date toDate:futureDate options:0];
            offsetDetectedDate = [[NSCalendar currentCalendar] dateByAddingUnit:NSDayCalendarUnit value:offsetDateComponents.day toDate:result.date options:0];
            *stop = YES;
        }
    }];

    NSLog(@"%@", offsetDetectedDate);
mattt
  • 19,544
  • 7
  • 73
  • 84
  • 2
    Thanks! This covers some cases, but there are others where the parse is sensitive to the current date/time. For example, "let's have dinner Tuesday night", if today is Monday 1/20, and I want to pretend the context date is Wednesday 1/23. :) – andros Jan 20 '14 at 23:40
  • @andros Well, that's the general approach. You asked about "tomorrow night", specifically. The same would apply for other relative dates. – mattt Jan 21 '14 at 17:38