Core Data group by NSDate without Time - objective-c

I have start Date attribute in core data, and i want to fetch the items along with grouping according to startDate,
But startDate is basically having timeComponent in it, but i want grouping to be based on yyyy-mm-dd,
This is the code i am using
NSError *error = nil;
NSFetchRequest *request = [NSFetchRequest new];
NSManagedObjectContext *context = self.managedObjectContext;
request.entity = [CalendarItem entityInManagedObjectContext:context];
NSExpression *startExpr = [NSExpression expressionForKeyPath:#"start"];
NSExpression *countExpr = [NSExpression expressionForFunction:#"count:" arguments:[NSArray arrayWithObject:startExpr]];
NSExpressionDescription *exprDesc = [[NSExpressionDescription alloc] init];
[exprDesc setExpression:countExpr];
[exprDesc setExpressionResultType:NSInteger64AttributeType];
[exprDesc setName:#"count"];
[request setPropertiesToGroupBy:#[#"start"]];
[request setPropertiesToFetch:[NSArray arrayWithObjects:#"start", exprDesc, nil]];
[request setResultType:NSDictionaryResultType];
NSArray *results = [self.managedObjectContext executeFetchRequest:request error:&error];
This is the Output i am getting:
Printing description of results:
<_PFArray 0x600001e3a700>(
{
count = 1;
start = "2021-09-14 03:30:00 +0000";
},
{
count = 1;
start = "2021-09-14 04:00:00 +0000";
},
{
count = 1;
start = "2021-09-16 09:30:00 +0000";
},
{
count = 1;
start = "2021-11-11 00:00:00 +0000";
},
{
count = 1;
start = "2021-11-11 04:00:00 +0000";
},
{
count = 1;
start = "2021-11-11 06:00:00 +0000";
},
{
count = 1;
start = "2021-11-12 00:00:00 +0000";
}
)
Expected Result:
{
count = 2
start = 2021-09-14
}
{
count = 1
start = 2021-09-16
}
{
count = 3
start = 2021-11-11
}
{
count = 1;
start = "2021-11-12
}

Core Data "date" properties, as you've found, are actually timestamps. They include the time of day, even though they're called "date". You can't tell Core Data to use only part of the value of a date attribute-- it's all or nothing.
To get the kind of grouping you want, you need to create another property where the value is only the value you need instead of a timestamp that includes extra details you don't need. Here, that would be a date where the hour, minute, and second are all set to zero, so that those details don't affect grouping.
One way to do this is to make a new date object with that change and save it in a new property. You could call it trimmedDate. Then any time you set the date on an instance, also set trimmedDate, with code that's something like
NSDate *trimmed = [[NSCalendar currentCalendar] dateBySettingHour:0 minute:0 second:0 ofDate:date options:0];
Then use this new value for grouping.

Related

Core Data: Fetch dictionaries grouped by multiple attributes

I have an NSManagedObject subclass Entry with properties type (int) and date (NSDate). Right now I use the following code to get entries for the current date grouped by type.
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Entry"];
[request setResultType:NSDictionaryResultType];
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath:#"type"];
NSExpression *countExpression = [NSExpression expressionForFunction:#"count:" arguments:#[keyPathExpression]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName:#"entryCountByType"];
[expressionDescription setExpression:countExpression];
[expressionDescription setExpressionResultType:NSInteger64AttributeType];
[request setPropertiesToFetch:#[#"type",expressionDescription]];
[request setPropertiesToGroupBy:#[#"type"]];
[request setPredicate:[NSPredicate predicateWithFormat:#"date == %#", [NSDate date]]];
NSError *error;
NSArray *results = [defaultManagedObjectContext() executeFetchRequest:request error:&error];
Results for a day with 1 entry of type 3 and 1 entry of type 5 are:
<_PFArray 0x15e05b50>(
{
entryCountByType = 1;
type = 3;
},
{
entryCountByType = 1;
type = 5;
}
)
I want to fetch without a date predicate at all and have counts for each type listed by date like so (where 1 day has 2 of type 1 and 1 of type 2, and another day has 3 of type 1 and 2 of type 2):
(
{
date = 6/4/14 00:00:00;
type1 = 2;
type2 = 1;
},
{
date = 6/5/14 00:00:00;
type1 = 3;
type2 = 2;
}
)
Is this possible in the way I'm thinking, that is, with a single fetch request? Doing a fetch for each day individually (about 30 sequential fetches) is really slowing down my app. I've tried adding #"date" to propertiesToGroupBy (after removing the date predicate, of course) but all that does is return a similar result to the first output, just with a date param thrown in so that each type for a day is split out into separate dictionaries.
You are confusing "day" and "date". NSDate is accurate down to the millisecond, so you will likely have only unique dates. You cannot group by a date range.
One way is to store the day as a transient property. The scheme does not matter, but one possibility is e.g. year * 10000 + month * 100 + day.
If you want to dispense with the NSExpressions you could just use a method day in your entity that returns a day string (with NSDateFormatter) and use a NSFetchedResultsController with day as the sectionNameKeyPath to display the grouped data.

Get total step count for every date in HealthKit

What's the best way to get a total step count for every day recorded in HealthKit.
With HKSampleQuery's method initWithSampleType (see below) I can set a start and end date for the query using NSPredicate, but the method returns an array with many HKQuantitySamples per day.
- (instancetype)initWithSampleType:(HKSampleType *)sampleType
predicate:(NSPredicate *)predicate
limit:(NSUInteger)limit
sortDescriptors:(NSArray *)sortDescriptors
resultsHandler:(void (^)(HKSampleQuery *query,
NSArray *results,
NSError *error))resultsHandler
I guess I can query all recorded step counts and go through the array and calculate the total step count for each day, but I'm hoping for an easier solution as there will be thousands of HKSampleQuery objects. Is there a way to have initWithSampleType return a total step count per day?
You should use HKStatisticsCollectionQuery:
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDateComponents *interval = [[NSDateComponents alloc] init];
interval.day = 1;
NSDateComponents *anchorComponents = [calendar components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear
fromDate:[NSDate date]];
anchorComponents.hour = 0;
NSDate *anchorDate = [calendar dateFromComponents:anchorComponents];
HKQuantityType *quantityType = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
// Create the query
HKStatisticsCollectionQuery *query = [[HKStatisticsCollectionQuery alloc] initWithQuantityType:quantityType
quantitySamplePredicate:nil
options:HKStatisticsOptionCumulativeSum
anchorDate:anchorDate
intervalComponents:interval];
// Set the results handler
query.initialResultsHandler = ^(HKStatisticsCollectionQuery *query, HKStatisticsCollection *results, NSError *error) {
if (error) {
// Perform proper error handling here
NSLog(#"*** An error occurred while calculating the statistics: %# ***",error.localizedDescription);
}
NSDate *endDate = [NSDate date];
NSDate *startDate = [calendar dateByAddingUnit:NSCalendarUnitDay
value:-7
toDate:endDate
options:0];
// Plot the daily step counts over the past 7 days
[results enumerateStatisticsFromDate:startDate
toDate:endDate
withBlock:^(HKStatistics *result, BOOL *stop) {
HKQuantity *quantity = result.sumQuantity;
if (quantity) {
NSDate *date = result.startDate;
double value = [quantity doubleValueForUnit:[HKUnit countUnit]];
NSLog(#"%#: %f", date, value);
}
}];
};
[self.healthStore executeQuery:query];
Port to Swift with no dependency to SwiftDate library
let calendar = NSCalendar.current
let interval = NSDateComponents()
interval.day = 1
var anchorComponents = calendar.dateComponents([.day, .month, .year], from: NSDate() as Date)
anchorComponents.hour = 0
let anchorDate = calendar.date(from: anchorComponents)
// Define 1-day intervals starting from 0:00
let stepsQuery = HKStatisticsCollectionQuery(quantityType: stepsCount!, quantitySamplePredicate: nil, options: .cumulativeSum, anchorDate: anchorDate!, intervalComponents: interval as DateComponents)
// Set the results handler
stepsQuery.initialResultsHandler = {query, results, error in
let endDate = NSDate()
let startDate = calendar.date(byAdding: .day, value: -7, to: endDate as Date, wrappingComponents: false)
if let myResults = results{
myResults.enumerateStatistics(from: startDate!, to: endDate as Date) { statistics, stop in
if let quantity = statistics.sumQuantity(){
let date = statistics.startDate
let steps = quantity.doubleValue(for: HKUnit.count())
print("\(date): steps = \(steps)")
//NOTE: If you are going to update the UI do it in the main thread
DispatchQueue.main.async {
//update UI components
}
}
} //end block
} //end if let
}
healthStore?.execute(stepsQuery)
Modified #sebastianr's answer using core Swift classes, for just for testing I am returning only steps for just one day, once you have more days you can create a dictionary of Dates and step count and return it
func getStepCountPerDay(completion:#escaping (_ count: Double)-> Void){
guard let sampleType = HKObjectType.quantityType(forIdentifier: .stepCount)
else {
return
}
let calendar = Calendar.current
var dateComponents = DateComponents()
dateComponents.day = 1
var anchorComponents = calendar.dateComponents([.day, .month, .year], from: Date())
anchorComponents.hour = 0
let anchorDate = calendar.date(from: anchorComponents)
let stepsCumulativeQuery = HKStatisticsCollectionQuery(quantityType: sampleType, quantitySamplePredicate: nil, options: .cumulativeSum, anchorDate: anchorDate!, intervalComponents: dateComponents
)
// Set the results handler
stepsCumulativeQuery.initialResultsHandler = {query, results, error in
let endDate = Date()
let startDate = calendar.date(byAdding: .day, value: 0, to: endDate, wrappingComponents: false)
if let myResults = results{
myResults.enumerateStatistics(from: startDate!, to: endDate as Date) { statistics, stop in
if let quantity = statistics.sumQuantity(){
let date = statistics.startDate
let steps = quantity.doubleValue(for: HKUnit.count())
print("\(date): steps = \(steps)")
completion(steps)
//NOTE: If you are going to update the UI do it in the main thread
DispatchQueue.main.async {
//update UI components
}
}
} //end block
} //end if let
}
HKHealthStore().execute(stepsCumulativeQuery)
}
Here is a translation that currently works for Swift 2.0, using the SwiftDate library.
let type = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)
let startDate = NSDate().beginningOfDay().oneWeekAgo()
let interval = NSDateComponents()
interval.day = 1
let predicate = HKQuery.predicateForSamplesWithStartDate(startDate, endDate: NSDate(), options: .StrictStartDate)
let query = HKStatisticsCollectionQuery(quantityType: type!, quantitySamplePredicate: predicate, options: [.CumulativeSum], anchorDate: NSDate().begginingOfDay(), intervalComponents:interval)
query.initialResultsHandler = { query, results, error in
let endDate = NSDate()
let startDate = NSDate().beginningOfDay().oneWeekAgo()
if let myResults = results{
myResults.enumerateStatisticsFromDate(startDate, toDate: endDate) {
statistics, stop in
if let quantity = statistics.sumQuantity() {
let date = statistics.startDate
let steps = quantity.doubleValueForUnit(HKUnit.countUnit())
print("\(date): steps = \(steps)")
}
}
}
}
healthKitStore.executeQuery(query)
I wrapped mine in a completion block (objective -c). I found what was best was to set the startDate for the query to todays date at midnight. Hope this helps, feel free to copy/paste to get started
-(void)fetchHourlyStepsWithCompletionHandler:(void (^)(NSMutableArray *, NSError *))completionHandler {
NSMutableArray *mutArray = [NSMutableArray new];
NSCalendar *calendar = [NSCalendar autoupdatingCurrentCalendar];
NSDate *startDate = [calendar dateBySettingHour:0 minute:0 second:0 ofDate:[NSDate date] options:0];
NSDate *endDate = [NSDate date]; // Whatever you need in your case
HKQuantityType *type = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
// Your interval: sum by hour
NSDateComponents *intervalComponents = [[NSDateComponents alloc] init];
intervalComponents.hour = 1;
// Example predicate
NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionStrictStartDate];
HKStatisticsCollectionQuery *query = [[HKStatisticsCollectionQuery alloc] initWithQuantityType:type quantitySamplePredicate:predicate options:HKStatisticsOptionCumulativeSum anchorDate:startDate intervalComponents:intervalComponents];
query.initialResultsHandler = ^(HKStatisticsCollectionQuery *query, HKStatisticsCollection *results, NSError *error) {
[results enumerateStatisticsFromDate:startDate toDate:endDate
withBlock:^(HKStatistics *result, BOOL *stop) {
if (!result) {
if (completionHandler) {
completionHandler(nil, error);
}
return;
}
HKQuantity *quantity = result.sumQuantity;
NSDate *startDate = result.startDate;
NSDateFormatter *formatter = [[NSDateFormatter alloc]init];
formatter.dateFormat = #"h a";
NSString *dateString = [formatter stringFromDate:startDate];
double steps = [quantity doubleValueForUnit:[HKUnit countUnit]];
NSDictionary *dict = #{#"steps" : #(steps),
#"hour" : dateString
};
[mutArray addObject:dict];
}];
if (completionHandler) {
completionHandler(mutArray, error);
}
};
[self.healthStore executeQuery:query];
}
With Updated Swift 2.0 & SwiftDate library.
let type = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)
let startDate = NSDate().beginningOfDay
let interval = NSDateComponents()
interval.day = 1
let predicate = HKQuery.predicateForSamplesWithStartDate(startDate, endDate: NSDate(), options: .StrictStartDate)
let query = HKStatisticsCollectionQuery(quantityType: type!, quantitySamplePredicate: predicate, options: [.CumulativeSum], anchorDate: NSDate().beginningOfDay, intervalComponents:interval)
query.initialResultsHandler = { query, results, error in
let endDate = NSDate()
let startDate = NSDate().beginningOfDay
if let myResults = results{
myResults.enumerateStatisticsFromDate(startDate, toDate: endDate) {
statistics, stop in
if let quantity = statistics.sumQuantity() {
let date = statistics.startDate
let steps = quantity.doubleValueForUnit(HKUnit.countUnit())
print("\(date): steps = \(steps)")
}
}
}
}
healthKitStore.executeQuery(query)

Parse date string with NSDateFormatter with month begins from 0

I can't parse this date string: #"2002 0 20", where 0 is January (First month in year is 0, not 1).
Can I use NSDateFormatter to parse this string?
Here http://www.unicode.org/reports/tr35/tr35-31/tr35-dates.html#Parsing_Dates_Times I've read that month should starts on 1.
UPDATE
I need this formatter because I have much data in this format (it is not my data).
I've not found any solution with NSDateFormatter without creating a subclass and overriding format methods.
I don't use NSScan, because it is a too complicated solution, but I think #Andy is right.
I use this code to parse the string:
- (BOOL)getObjectValue:(out __autoreleasing id *)obj forString:(NSString *)string range:
(inout NSRange *)rangep error:(out NSError *__autoreleasing *)error
{
int year = 0;
int month = -1;
int day = -1;
int coutRead = sscanf([string cStringUsingEncoding:NSUTF8StringEncoding], "Date.UTC(%i, %i, %i)", &year, &month, &day);
BOOL result = NO;
if (coutRead == 3)
{
NSDateComponents* components = [[NSDateComponents alloc] init];
components.year = year;
components.month = month + 1;
components.day = day;
*obj = [self.calendar dateFromComponents:components];
result = YES;
}
else
{
obj = 0;
*rangep = NSMakeRange(NSNotFound, 0);
result = NO;
}
return result;
}
Try this:
NSDateFormatter* formatter = [NSDateFormatter new];
formatter.timeZone = [NSTimeZone timeZoneWithName:#"UTC"];
formatter.shortMonthSymbols = #[#"0", #"1", #"2", #"3", #"4", #"5", #"6", #"7", #"8", #"9", #"10", #"11"];
formatter.dateFormat = #"yyyy MMM dd"; // Note 3 Ms for "short month" format
NSDate* theDate = [formatter dateFromString:#"2002 0 20"];
Result:
2002-01-20 00:00:00 +0000
No you can't. Parse manually.
At your disposal:
You have NSString that can split string on substrings array using custom delimiter, white space in your case. For example:
-(NSArray*)componentsSeparatedByString:(NSString *)separator
NSScanner that you can use to read integers directly from string.
Documentation is straightforward and comprehensive.

Core Data comparisons within aggregate functions

Core Data entity that has two properties on it: startDay and startDateTime. Both properties are NSDate objects. startDay has its time components set to midnight, and startDateTime designates a date with a time. Both “calendar days” within startDay and startDateTime are the same.
Here are some objects
(
{
startDateTime = "2014-05-27 08:00:00 +0000";
startDay = "2014-05-27 00:00:00 +0000";
},
{
startDateTime = "2014-05-27 13:00:00 +0000";
startDay = "2014-05-28 00:00:00 +0000";
}
)
Need to get objects grouped by startDay. Need to know if there is event in the "morning" or "afternoon".
Here's how to get results grouped by startDay and have earliest and latest daily events reported.
- (void)fetchEventDays
{
NSExpression *startDateTimeExpression = [NSExpression expressionForKeyPath:#"startDateTime"];
NSExpression *minStartDateTime = [NSExpression expressionForFunction:#"min:" arguments:[NSArray arrayWithObject:startDateTimeExpression]];
NSExpression *maxStartDateTime = [NSExpression expressionForFunction:#"max:" arguments:[NSArray arrayWithObject:startDateTimeExpression]];
NSExpressionDescription *minStartDateTimeExpression = [[NSExpressionDescription alloc] init];
minStartDateTimeExpression.name = #"minEventStartTime";
minStartDateTimeExpression.expression = minStartDateTime;
minStartDateTimeExpression.expressionResultType = NSDateAttributeType;
NSExpressionDescription *maxStartDateTimeExpression = [[NSExpressionDescription alloc] init];
maxStartDateTimeExpression.name = #"maxEventStartTime";
maxStartDateTimeExpression.expression = maxStartDateTime;
maxStartDateTimeExpression.expressionResultType = NSDateAttributeType;
NSManagedObjectContext *context = [RKObjectManager sharedManager].managedObjectStore.mainQueueManagedObjectContext;
NSEntityDescription* entity = [NSEntityDescription entityForName:#"NIModelScheduleData" inManagedObjectContext:context];
NSAttributeDescription* startDayDesc = [entity.attributesByName objectForKey:#"startDay"];
NSFetchRequest* fetch = [[NSFetchRequest alloc] init];
fetch.entity = entity;
fetch.propertiesToFetch = [NSArray arrayWithObjects:startDayDesc, minStartDateTimeExpression, maxStartDateTimeExpression, nil];
fetch.propertiesToGroupBy = [NSArray arrayWithObject:startDayDesc];
fetch.resultType = NSDictionaryResultType;
NSError *error = nil;
NSArray *results = [context executeFetchRequest:fetch error:&error];
NSLog(#"%#", results);
}
That code is returning
(
{
maxEventStartTime = "2014-05-27 13:00:00 +0000";
minEventStartTime = "2014-05-27 08:00:00 +0000";
startDay = "2014-05-27 00:00:00 +0000";
},
{
maxEventStartTime = "2014-05-28 10:00:00 +0000";
minEventStartTime = "2014-05-28 09:00:00 +0000";
startDay = "2014-05-28 00:00:00 +0000";
}
)
It'd be way cooler if it looked like this
(
{
eveningEvent = YES;
morningEvent = YES;
startDay = "2014-05-27 00:00:00 +0000";
},
{
eveningEvent = NO;
morningEvent = YES;
startDay = "2014-05-28 00:00:00 +0000";
}
)
How can I modify my fetch request to get a date comparison into my NSExpression (or NSExpressionDescription?), so that Core Data checks if min/maxEventStartTime are before/after noon, and returns a BOOL instead of the actual NSDate object.

Core data, count and group by

I have an entity with 2 attributes, an NSDate and a boolean value. (this is going to be a large "table")
I need to count all YES and NO values for the boolean between two dates, grouped by days. How can i do this?
The result I'm looking for is
{
totalYes = 10,
totalNo = 5,
date = dd-mm-yyyy
},
{
totalYes = 15,
totalNo = 3,
date = dd-mm-yyyy
},
etc
Thanks
You can try this approach:
1)Get all the enteties with YES,sorted by date
2)Go trough this array and fill array with dictionaries with day value and number of yeses
3)Then, do the ame thing with noe's,adding number of no to that array of dictionaries.
NSFetchRequest *request = [[NSFetchRequest alloc]init];
request.entity = [NSEntityDescription entityForName:#"Day" inManagedObjectContext:context];
request.predicate = [NSPredicate predicateWithFormat:#"yesorno = %#",YES];
NSError *error = nil;
request.sortDescriptors =[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"date" ascending:YES]];
//Here you get all the enteties with YES,sorted by date
NSArray *days = [context executeFetchRequest:request error:&error];
NSMutableArray *arrayOfDates = [NSMutableArray array];
int firstDay = [[[NSCalendar currentCalendar]components:NSDayCalendarUnit fromDate:[[days objectAtIndex:0]date]]day];
//Add the first day dictionary
[arrayOfDates addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:[[days objectAtIndex:0]date],#"Day", nil];
int numberOfYes = 0;
int dayNumber = 0;
for(NSManagedObject *day in days)
{
if( [[[NSCalendar currentCalendar]components:NSDayCalendarUnit fromDate:[day date]]day]>firstDay)
{
//save number of yeses for the previous day,because we are done with it
[[arrayOfDates objectAtIndex:dayNumber]setValue:[NSNumber numberWithInt:numberOfYes] forKey:#"NumberOfYes"];
numberOfYes = 1;
dayNumber++;
firstDay = [[[NSCalendar currentCalendar]components:NSDayCalendarUnit fromDate:[day date]]day];//date with new day
[arrayOfDates addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:[day date],#"Day", nil];//Add this day dictionary to array
}else
{
numberOfYes++;
}
}
//And somrthing similar to No