NSPredicate: filtering objects by day of NSDate property
Asked Answered
T

9

126

I have a Core Data model with an NSDate property. I want to filter the database by day. I assume the solution will involve an NSPredicate, but I'm not sure how to put it all together.

I know how to compare the day of two NSDates using NSDateComponents and NSCalendar, but how do I filter it with an NSPredicate?

Perhaps I need to create a category on my NSManagedObject subclass that can return a bare date with just the year, month and day. Then I could compare that in an NSPredicate. Is this your recommendation, or is there something simpler?

Trochlear answered 27/12, 2009 at 6:7 Comment(0)
P
197

Given a NSDate * startDate and endDate and a NSManagedObjectContext * moc:

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(date >= %@) AND (date <= %@)", startDate, endDate];
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:[NSEntityDescription entityForName:@"EntityName" inManagedObjectContext:moc]];
[request setPredicate:predicate];

NSError *error = nil;
NSArray *results = [moc executeFetchRequest:request error:&error];
Photosphere answered 27/12, 2009 at 9:17 Comment(9)
what if we have only one date?Stormy
Looks like you can use ==. Though if you're searching by day, remember that NSDate is a date-and-time -- you might want to use a midnight-to-midnight range.Erminois
Example on how to also setup startDate and endDate, see my answer belowAria
@Erminois can u show me an example how to use a midnight to midnight range. for example: i have 2 same days but different timing. and i want to filter by day(i dont care about the timing). How can i do that?Rotarian
@Shady In the above example, you could set startDate to 2012-09-17 0:00:00 and endDate to 2012-09-18 0:00:00 and the predicate to startDate <= date < endDate. That would catch all times on 2012-09-17.Erminois
@Photosphere I'v tried the exactly same predicate as yours: @"(startDate >= %@) AND (startDate <= %@)", self.minDate, self.maxDate]" but i'm still getting the error! 'Unable to parse the format string "(startDate >= 2014-07-14 23:46:05 +0000) AND (startDate <= 2014-07-19 12:48:19 +0000)"' What am I doing wrong here!?? Is the format wrong??Snowslide
@Snowslide are those NSDates, or strings?Carillon
@Carillon the "startDate" is a NSDate object. What I did wrong was passing a string to the predicate instead of creating te predicate on the fly. I solved it by using NSCompoundPredicate.Snowslide
My dates are in Swift 3 native format (value type Date), so I had to use as NSDate to silence a compiler error.Garnish
A
64

Example on how to also set up startDate and endDate to the above given answer:

...

NSCalendar *calendar = [NSCalendar currentCalendar];
NSDateComponents *components = [calendar components:(NSCalendarUnitYear | NSCalendarUnitMonth ) fromDate:[NSDate date]];
//create a date with these components
NSDate *startDate = [calendar dateFromComponents:components];
[components setMonth:1];
[components setDay:0]; //reset the other components
[components setYear:0]; //reset the other components
NSDate *endDate = [calendar dateByAddingComponents:components toDate:startDate options:0];
predicate = [NSPredicate predicateWithFormat:@"((date >= %@) AND (date < %@)) || (date = nil)",startDate,endDate];

...

Here I was searching for all entries within one month. It's worth to mention, that this example also shows how to search 'nil' date-entires.

Aria answered 16/7, 2011 at 13:10 Comment(0)
I
12

Swift 3.0 extension for Date:

extension Date{

func makeDayPredicate() -> NSPredicate {
    let calendar = Calendar.current
    var components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: self)
    components.hour = 00
    components.minute = 00
    components.second = 00
    let startDate = calendar.date(from: components)
    components.hour = 23
    components.minute = 59
    components.second = 59
    let endDate = calendar.date(from: components)
    return NSPredicate(format: "day >= %@ AND day =< %@", argumentArray: [startDate!, endDate!])
}
}

Then use like:

 let fetchReq = NSFetchRequest(entityName: "MyObject")
 fetchReq.predicate = myDate.makeDayPredicate()
Isoagglutination answered 9/10, 2016 at 13:27 Comment(0)
S
10

In Swift I got something similar to:

        let dateFormatter = NSDateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"
        let date = dateFormatter.dateFromString(predicate)
        let calendar = NSCalendar(calendarIdentifier: NSGregorianCalendar)
        let components = calendar!.components(
            NSCalendarUnit.CalendarUnitYear |
                NSCalendarUnit.CalendarUnitMonth |
                NSCalendarUnit.CalendarUnitDay |
                NSCalendarUnit.CalendarUnitHour |
                NSCalendarUnit.CalendarUnitMinute |
                NSCalendarUnit.CalendarUnitSecond, fromDate: date!)
        components.hour = 00
        components.minute = 00
        components.second = 00
        let startDate = calendar!.dateFromComponents(components)
        components.hour = 23
        components.minute = 59
        components.second = 59
        let endDate = calendar!.dateFromComponents(components)
        predicate = NSPredicate(format: "day >= %@ AND day =< %@", argumentArray: [startDate!, endDate!])

I had a hard time to discover that string interpolation "\(this notation)" doesn't work for comparing dates in NSPredicate.

Salvadorsalvadore answered 27/5, 2015 at 13:36 Comment(1)
Thank you for this answer. Swift 3 version added below.Whidah
W
8

I ported the answer from Glauco Neves to Swift 2.0 and wrapped it inside a function that receives a date and returns the NSPredicate for the corresponding day:

func predicateForDayFromDate(date: NSDate) -> NSPredicate {
    let calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)
    let components = calendar!.components([.Year, .Month, .Day, .Hour, .Minute, .Second], fromDate: date)
    components.hour = 00
    components.minute = 00
    components.second = 00
    let startDate = calendar!.dateFromComponents(components)
    components.hour = 23
    components.minute = 59
    components.second = 59
    let endDate = calendar!.dateFromComponents(components)

    return NSPredicate(format: "day >= %@ AND day =< %@", argumentArray: [startDate!, endDate!])
}
Wolk answered 14/12, 2015 at 13:15 Comment(1)
Thank you for this answer. Swift 3 version added below.Whidah
W
6

Adding to Rafael's answer (incredibly useful, thank you!), porting for Swift 3.

func predicateForDayFromDate(date: Date) -> NSPredicate {
    let calendar = Calendar(identifier: Calendar.Identifier.gregorian)
    var components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date)
    components.hour = 00
    components.minute = 00
    components.second = 00
    let startDate = calendar.date(from: components)
    components.hour = 23
    components.minute = 59
    components.second = 59
    let endDate = calendar.date(from: components)

    return NSPredicate(format: "YOUR_DATE_FIELD >= %@ AND YOUR_DATE_FIELD =< %@", argumentArray: [startDate!, endDate!])
}
Whidah answered 10/9, 2016 at 21:29 Comment(0)
T
2

Building on the previous answers, an update and alternative method using Swift 5.x

func predicateForDayUsingDate(_ date: Date) -> NSPredicate {
    
    var calendar = Calendar.current
    calendar.timeZone = NSTimeZone.local
    // following creates exact midnight 12:00:00:000 AM of day
    let startOfDay = calendar.startOfDay(for: date)
    // following creates exact midnight 12:00:00:000 AM of next day
    let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay)!
    
    return NSPredicate(format: "day >= %@ AND day < %@", argumentArray: [startOfDay, endOfDay])
}

If you'd prefer to create the time for endOfDay as 11:59:59 PM, you can instead include...

    let endOfDayLessOneSecond = endOfDay.addingTimeInterval(TimeInterval(-1))

but then you might change the NSPredicate to...

    return NSPredicate(format: "day >= %@ AND day <= %@", argumentArray: [startOfDay, endOfDayLessOneSecond])

...with specific note of the change from day < %@ to day <= %@.

Threatt answered 11/8, 2020 at 5:47 Comment(0)
T
1

I've recently spent some time attempting to solve this same problem and add the following to the list of alternatives to prepare start and end dates (includes updated method for iOS 8 and above)...

NSDate *dateDay = nil;
NSDate *dateDayStart = nil;
NSDate *dateDayNext = nil;

dateDay = <<USER_INPUT>>;

dateDayStart = [[NSCalendar currentCalendar] startOfDayForDate:dateDay];

//  dateDayNext EITHER
dateDayNext = [dateDayStart dateByAddingTimeInterval:(24 * 60 * 60)];

//  dateDayNext OR
NSDateComponents *dateComponentDay = nil;
dateComponentDay = [[NSDateComponents alloc] init];
[dateComponentDay setDay:1];
dateDayNext = [[NSCalendar currentCalendar] dateByAddingComponents:dateComponentDay
                                                            toDate:dateDayStart
                                                           options:NSCalendarMatchNextTime];

...and the NSPredicate for the Core Data NSFetchRequest (as already shown above in other answers)...

[NSPredicate predicateWithFormat:@"(dateAttribute >= %@) AND (dateAttribute < %@)", dateDayStart, dateDayNext]]
Threatt answered 5/4, 2016 at 4:58 Comment(0)
B
0

For me this is worked.

NSCalendar *calendar = [NSCalendar currentCalendar];
    NSDateComponents *components = [calendar components:(NSYearCalendarUnit | NSMonthCalendarUnit ) fromDate:[NSDate date]];
    //create a date with these components
    NSDate *startDate = [calendar dateFromComponents:components];
    [components setMonth:0];
    [components setDay:0]; //reset the other components
    [components setYear:0]; //reset the other components
    NSDate *endDate = [calendar dateByAddingComponents:components toDate:startDate options:0];

    startDate = [NSDate date];
    endDate = [startDate dateByAddingTimeInterval:-(7 * 24 * 60 * 60)];//change here

    NSString *startTimeStamp = [[NSNumber numberWithInt:floor([endDate timeIntervalSince1970])] stringValue];
    NSString *endTimeStamp = [[NSNumber numberWithInt:floor([startDate timeIntervalSince1970])] stringValue];


   NSPredicate *predicate = [NSPredicate predicateWithFormat:@"((paidDate1 >= %@) AND (paidDate1 < %@))",startTimeStamp,endTimeStamp];
    NSLog(@"predicate is %@",predicate);
    totalArr = [completeArray filteredArrayUsingPredicate:predicate];
    [self filterAndPopulateDataBasedonIndex];
    [self.tableviewObj reloadData];
    NSLog(@"result is %@",totalArr);

I have filtered array from current date to 7 days back. I mean I am getting one week data from current date. This should work.

Note: I am converting date which is coming with milli seconds by 1000, and comparing after. Let me know if you need any clarity.

Borreri answered 7/11, 2015 at 10:51 Comment(1)
In your answer completeArray is it having Array of dictionary or dataModel I want to apply on DataModel.Bael

© 2022 - 2024 — McMap. All rights reserved.