How to use a predicate on NSTimeInterval?
Asked Answered
B

3

9

I would like to get all records for the current month, so I stack two predicates for first date of the month and last day of the month. Since I use CoreData the dates are stored actually as NSTimeInterval.

NSCalendar *calendar = [NSCalendar currentCalendar];

//Get beginning of current month
NSDateComponents *beginningOfCurrentMonthComponents = [calendar components:(NSEraCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit) fromDate:date];
[beginningOfCurrentMonthComponents setDay:1];
NSDate *beginningOfCurrentMonthDate = [calendar dateFromComponents:beginningOfCurrentMonthComponents];

//Set a single month to be added to the current month
NSDateComponents *oneMonth = [[NSDateComponents alloc] init];
[oneMonth setMonth:1];

//determine the last day of this month
NSDate *beginningOfNextMonthDate = [calendar dateByAddingComponents:oneMonth toDate:beginningOfCurrentMonthDate options:0];

NSMutableArray *parr = [NSMutableArray array];
[parr addObject:[NSPredicate predicateWithFormat:@"recordDate >= %d", [beginningOfCurrentMonthDate timeIntervalSince1970]]];
[parr addObject:[NSPredicate predicateWithFormat:@"recordDate < %d", [beginningOfNextMonthDate timeIntervalSince1970]]];

//Give me everything from beginning of this month until end of this month
NSPredicate *predicate = [NSCompoundPredicate andPredicateWithSubpredicates:parr];

return [allRecords filteredArrayUsingPredicate:predicate];

Upon returning the filtered array, it crashes with this error message:

2013-10-25 19:09:39.702 [3556:a0b] -[__NSCFNumber timeIntervalSinceReferenceDate]: unrecognized selector sent to instance 0x8911440
2013-10-25 19:09:39.704 [3556:a0b] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFNumber timeIntervalSinceReferenceDate]: unrecognized selector sent to instance 0x8911440'
*** First throw call stack:
(
    0   CoreFoundation                      0x01aa85e4 __exceptionPreprocess + 180
    1   libobjc.A.dylib                     0x0182b8b6 objc_exception_throw + 44
    2   CoreFoundation                      0x01b45903 -[NSObject(NSObject) doesNotRecognizeSelector:] + 275
    3   CoreFoundation                      0x01a9890b ___forwarding___ + 1019
    4   CoreFoundation                      0x01a984ee _CF_forwarding_prep_0 + 14
    5   CoreFoundation                      0x01a7e3e3 -[NSDate compare:] + 67
    6   Foundation                          0x014194fe -[NSComparisonPredicateOperator performPrimitiveOperationUsingObject:andObject:] + 408
    7   Foundation                          0x014b03de -[NSPredicateOperator performOperationUsingObject:andObject:] + 306
    8   Foundation                          0x014b016c -[NSComparisonPredicate evaluateWithObject:substitutionVariables:] + 347
    9   Foundation                          0x014299b6 -[NSCompoundPredicateOperator evaluatePredicates:withObject:substitutionVariables:] + 240
    10  Foundation                          0x01429845 -[NSCompoundPredicate evaluateWithObject:substitutionVariables:] + 294
    11  Foundation                          0x014b0009 -[NSPredicate evaluateWithObject:] + 48
    12  Foundation                          0x014aff89 _filterObjectsUsingPredicate + 418
    13  Foundation                          0x014afd42 -[NSArray(NSPredicateSupport) filteredArrayUsingPredicate:] + 328

I used this loop also to indicate that the recordDate truly exists within the array:

for (FTRecord *r in allRecords) {
    NSLog(@"%f", [r recordDate]);
}

2013-10-25 19:09:35.860 [3556:a0b] 1380582000.000000
2013-10-25 19:09:36.556 [3556:a0b] 1380754800.000000
Beaner answered 25/10, 2013 at 18:18 Comment(2)
The stack trace shows that an NSDate object is involved in the predicate. Try changing your NSPredicate to compare recordDate against an NSDate instead of the time interval.Febrifugal
I just double checked everything. But you see that both dates are converted into NSTimeInterval though. I even did what you suggested, by deleting timeIntervalSince1970 and it still crashes with the same message. BTW, I have now added how the dates were created, maybe it helps to track down the problem.Beaner
K
16

You chose the "Use scalar properties for primitive data types" option when creating the managed object subclass, so that the recordDate is represented as NSTimeInterval in the FTRecord class.

But in a predicate like "recordDate >= 123.45", the left-hand side is stored as a NSKeyPathExpression, and that uses valueForKeyPath:@"recordDate" to access the property, which returns an NSDate object. The right-hand side of that predicate is stored as NSConstantValueExpression with a reference to an NSNumber object. Therefore an NSDate is compared with an NSNumber, which leads exactly to the exception that you got.

To fix the problem, you have to compare the property with an NSDate (which is what @rmaddy initially suggested):

[parr addObject:[NSPredicate predicateWithFormat:@"recordDate >= %@", beginningOfCurrentMonthDate]];
[parr addObject:[NSPredicate predicateWithFormat:@"recordDate < %@", beginningOfNextMonthDate]];
NSPredicate *predicate = [NSCompoundPredicate andPredicateWithSubpredicates:parr];

I tested this and it seems to produce the expected result.

Note however that Core Data uses timeIntervalSinceReferenceDate and dateWithTimeIntervalSinceReferenceDate to convert between the scalar values and NSDate, not timeIntervalSince1970.

Kappel answered 25/10, 2013 at 18:45 Comment(4)
Thanks for that. I just double checked, that is true the predicates now won't crash. However the filtered array shows no entry either. Itsa bit messy now. I wonder if I should re-generate the model without using scalar properties for primitive data types. What is your advice here?Beaner
@Kave: See my final remark. I assume that you created the objects using timeIntervalSince1970, but you have to use timeIntervalSinceReferenceDate (compare https://mcmap.net/q/1172426/-core-data-nspredicate-not-returning-records-between-two-dates). If you don't use scalar properties then you get rid of all these conversion problems. But apart from that, I do not have real advice whether scalar properties are better or not.Kappel
Martin you were right.Deleting the app and changing it to timeIntervalSinceReferenceDate fixed the issue. THanks for this great analysis. Eventually I will have to compare timestamps (irrelevant of recordDate) with incoming data from a REST service. There the timestamp will be saved as unix timestamp (1970). That's the reason I chose timeIntervalSince1970 in first place to circumvent incompatibilities. But it seems I can't do that after all. I suppose that would be a new question though.Beaner
@Kave: I have no experience with REST, so yes, a new question would be better.Kappel
C
2

For solution in Swift 3:

let date = NSDate()
let calender = NSCalendar.current
var beginningOfCurrentMonthComponents = calender.dateComponents([.year, .month, .day], from: date as Date)

beginningOfCurrentMonthComponents.day = 1

let beginningOfCurrentMonthDate = calender.date(from: beginningOfCurrentMonthComponents)
let beginningOfNextMonth = calender.date(byAdding: .month, value: 1, to: beginningOfCurrentMonthDate!)

let pred1 = NSPredicate(format: "date >= %@", beginningOfCurrentMonthDate! as NSDate)
let pred2 = NSPredicate(format: "date < %@", beginningOfNextMonth! as NSDate)

let predicate = NSCompoundPredicate.init(andPredicateWithSubpredicates: [pred1, pred2])
Corrective answered 21/9, 2016 at 7:44 Comment(0)
T
0

Will add timeIntervalSince1970 with in a predicate itself. It woul

NSPredicate* predicate = PREDICATE(@"(lastUpdatedTime == nil OR lastUpdatedTime.timeIntervalSince1970 <= %f)",currentDate.timeIntervalSince1970);

Tod answered 27/11, 2015 at 0:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.