3

I need a proper way to count how many unique days there are in CoreData objects with a property of type NSDate.

For example, I have the following:

<Object>.date = "2014-05-15 21:29:12 +0000";
<Object>.date = "2014-05-15 21:49:34 +0000";
<Object>.date = "2014-05-16 13:29:23 +0000";
<Object>.date = "2014-05-16 20:49:50 +0000";
<Object>.date = "2014-05-16 22:01:53 +0000";
<Object>.date = "2014-05-20 03:32:12 +0000";
<Object>.date = "2014-05-20 12:45:23 +0000";
<Object>.date = "2014-05-20 14:15:50 +0000";
<Object>.date = "2014-05-20 20:20:05 +0000";

In this case, the result must be 3 because there are 3 different days, 2014-05-15, 2014-05-16 and 2014-05-20

Any way to deal with this problem? I tried with NSPredicate but I did not succeed

Thanks!

mhergon
  • 1,688
  • 1
  • 18
  • 39

5 Answers5

3

That's easy. Let me show you what I'm going to do for it.

Group your results with sort description key. This example helps you to understand how it can be realized.

https://developer.apple.com/library/ios/samplecode/DateSectionTitles/Introduction/Intro.html

And then just calculate these groups.

EDIT:

NSDate+Utils.h

- (NSDate *) dateWithoutTime

NSDate+Utils.m

-(NSDate *) dateWithoutTime
{
    NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    NSDateComponents *components = [calendar components:NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit fromDate:self];
    return [calendar dateFromComponents:components];
}

some file

- (NSUInteger) someObjectsCount
{
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"SomeObject"];

    NSString *key = @"date.dateWithoutTime";

    fetchRequest.sortDescriptors = @[[[NSSortDescriptor alloc] initWithKey:key
                                                                 ascending:YES]];

    NSManagedObjectContext *context;
    context = [(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];

    NSFetchedResultsController *aFetchedResultsController;
    aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
                                                                    managedObjectContext:context
                                                                      sectionNameKeyPath:key
                                                                               cacheName:nil];
    [aFetchedResultsController performFetch:nil];

    return [[aFetchedResultsController sections] count];
}

That's all!

Mazyod
  • 22,319
  • 10
  • 92
  • 157
Mike
  • 481
  • 10
  • 24
  • I don't know where i should add the NSDate+Utils.m, i need the same to get only unique years. – Hollerweger Jun 18 '14 at 08:39
  • Create Utils category with dateWithoutTime method. In your case `NSDateComponents *components = [calendar components:NSYearCalendarUnit fromDate:self];` – Mike Jun 18 '14 at 08:56
  • @Sauvage do you know the description of the technique you used in this line of code NSString *key = @"date.dateWithoutTime"; It seems that somehow the method you created in the category is called here. Does this technique have a name? – aaronium112 Aug 05 '14 at 18:21
  • I'm not sure that it has the name. We divide all objects into sections by dates. And using the method we can make specific value for each. – Mike Aug 05 '14 at 21:08
  • @Sauvage, I just tried to use an extension in Swift: extension NSDate { func dateWithoutTime() -> NSDate { let calendar = NSCalendar(calendarIdentifier: NSCalendar.Identifier.gregorian) let components = calendar!.components([.year, .month, .day], from: self as Date) let resultDate = calendar!.date(from: components)! return resultDate as NSDate }} and got exception:'[<__NSTaggedDate 0xe41b8b7f42500000> valueForUndefinedKey:]: this class is not key value coding-compliant for the key dateWithoutTime.' and the extension method was not called. Thanks. – Ning Jan 03 '18 at 21:38
  • @Ning Maybe it doesn't work for swift extensions. You could try it with objective-c categories. There link above updated in 2k17 by Apple, so it should work I guess. – Mike Jan 04 '18 at 02:44
2

For Swift

One way is to use a set:

 let array = ["15-06-2017", "15-08-2017", "15-06-2017", "14-06-2017", 
              "14-06-2017"]
    let unique = Array(Set(array))
    // ["15-06-2017", "15-08-2017", "14-06-2017"]

You could also create an extension that filters through the array more explicitly:

 extension Array where Element : Equatable {
    var unique: [Element] {
        var uniqueValues: [Element] = []
        forEach { item in
            if !uniqueValues.contains(item) {
                uniqueValues += [item]
            }
        }
        return uniqueValues
    }
}

NOTE

The unique array will be in an unspecified order, and you may need to sort it. Sometimes it's better to just do it yourself by enumerating, you could write an extension.

It might be good to make an extension :

extension Array where Element : Hashable {
    var unique: [Element] {
        return Array(Set(self))
    }
}

There are probably more optimised ways to do what you want, but this way is quick and easy.

Mehsam Saeed
  • 235
  • 2
  • 14
1

As of iOS 8 you can use NSCalendar's startOfDayForDate

Ben Packard
  • 26,102
  • 25
  • 102
  • 183
0

You should have a new attribute called "daydate" which is a date set to midnight from your current date and time.

Every time you create/modify one of your objects, operate like this:

NSDate *date = [NSDate date];

NSCalendar *calendar = [NSCalendar currentCalendar];
[calendar setTimeZone:[NSTimeZone systemTimeZone]];
NSDateComponents *dateComps = [calendar components:NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit fromDate:date];
NSDate *daydate = [calendar dateFromComponents:dateComps];

myObject.date = date;
myObject.daydate = daydate;

Then you can operate your fetch (2 options).

Option 1:

NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"myObject" inManagedObjectContext:self.managedObjectContext];
[request setEntity:entity];

NSExpression *keyPathExpression = [NSExpression expressionForKeyPath:@"daydate"];
NSExpression *functionExpression = [NSExpression expressionForFunction:@"count:" arguments:@[keyPathExpression]];

NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName:@"count for this daydate:"];
[expressionDescription setExpression:functionExpression];
[expressionDescription setExpressionResultType:NSDoubleAttributeType];

NSAttributeDescription *attributeDesc = (entity.attributesByName)[@"daydate"];

[request setPropertiesToFetch:@[attributeDesc, expressionDescription]];
[request setPropertiesToGroupBy:@[attributeDesc]];
[request setResultType:NSDictionaryResultType];

NSError *error = nil;
NSArray *array = [self.managedObjectContext executeFetchRequest:request error:&error];
NSLog(@"array: %@", array);
NSLog(@"%lu", (unsigned long)array.count);

Option 2:

NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"myObject" inManagedObjectContext:self.managedObjectContext];
[request setEntity:entity];
request.returnsDistinctResults = YES;
request.propertiesToFetch = @[@"daydate"];
request.resultType = NSDictionaryResultType;

NSError *error = nil;
NSArray *array = [self.managedObjectContext executeFetchRequest:request error:&error];
NSLog(@"array: %@", array);
NSLog(@"array count: %lu", (unsigned long)array.count);
Imanou Petit
  • 89,880
  • 29
  • 256
  • 218
  • That's going to break if the user ever travels to a different time zone. `NSDate` always stores a full time, even if you manipulate it so that some values are zero. It's really just a wrapper for `NSTimeInterval`, storing seconds since the reference date. It might be OK if you stuck to UTC instead of the local time zone. – Tom Harrington May 16 '14 at 22:55
0

I have the same issue in an app of mine and I've never found a predicate that would do that for me.

I currently look through all the objects in the entity and calculate the day for each date and then return an array of unique days. I am considering adding a day attribute to my entity but have not yet tested that concept.

Below is the code that I currently use. Note:

  1. Both methods are class methods in a class called Game
  2. The entity in my model is called Game and the attribute I want to convert to unique days is startDateTime
  3. The returned array contains unique days in reverse sorted order (most recent day first)
  4. If you are not concerned with the actual unique days but only the count of unique days the code is trivial to change to return the count of days ([uniqueDays count])

My code:

+ (NSArray *)allGameStartDaysInManagedObjectContext:(NSManagedObjectContext *)moc {
    __block NSArray *gameDates;

    // mod cannot be nil
    NSParameterAssert(moc);

    [moc performBlockAndWait:^{
        NSError *error;

        NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Game"];
        NSArray *game = [moc executeFetchRequest:request error:&error];

        // Check for errors
        if (!game) {
            // Log errors
            NSLog(@"[%@ %@ %d]", NSStringFromClass([self class]), NSStringFromSelector(_cmd), __LINE__);
            NSLog(@"Core Data error: %@", error.localizedDescription);
            NSArray *errors = [[error userInfo] objectForKey:NSDetailedErrorsKey];
            if (errors != nil && errors.count > 0) {
                for (NSError *error in errors) {
                    NSLog(@"  Error: %@", error.userInfo);
                }
            } else {
                NSLog(@"  %@", error.userInfo);
            }
            gameDates = nil;
        } else if (game.count) {
            // Array to hold (at most) all the days of the games in the database
            NSMutableArray *days = [[NSMutableArray alloc] initWithCapacity:game.count];
            for (Game *games in game) {
                // Add only the day to the array
                [days addObject:[Game convertDateTimetoDay:games.startDateTime]];
            }

            // Generate a unique set of dates
            NSSet *uniqueDays = [NSSet setWithArray:days];
            // Create an array from the unique set
            NSMutableArray *uniqueGameDays = [NSMutableArray arrayWithArray:[uniqueDays allObjects]];
            // Create the sort descriptor
            NSSortDescriptor *sortOrder = [NSSortDescriptor sortDescriptorWithKey:@"self" ascending:NO];

            // Sort the array
            NSArray *sortedUniqueGameDays = [uniqueGameDays sortedArrayUsingDescriptors:[NSArray arrayWithObject:sortOrder]];

            gameDates = [sortedUniqueGameDays copy];
        } else {
            gameDates = nil;
        }
    }];

    return gameDates;
}

+ (NSDate *)convertDateTimetoDay:(NSDate *)dateTimeToConvert {

    // Get the year, month and day components (included era although this only applies to BCE)
    NSDateComponents *components = [[NSCalendar currentCalendar] components:NSEraCalendarUnit|NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit fromDate:dateTimeToConvert];
    // Add the date with only the selected components to the array
    return [[[NSCalendar currentCalendar] dateFromComponents:components] dateByAddingTimeInterval:[[NSTimeZone localTimeZone] secondsFromGMT]];
}
Robotic Cat
  • 5,899
  • 4
  • 41
  • 58