Given a table of books published, with a date_published column of typeNSAttributeType.DateAttributeType, I would like to know how many books were published by year like this:
Year | Books
-----+------
2013 | 76
2014 | 172
2015 | 155
In plain old SQL this is simple (although it varies slightly by RDBMS):
SELECT DATEPART(yyyy, date_published) AS "Year", COUNT(*) AS "Books"
FROM books
GROUP BY DATEPART(yyyy, date_published)
I'm new to Swift and iOS in general but everything I looked at suggested either pre-computing the year and storing that, or loading all the data and counting it myself. Neither of these approaches suited me as the year is in fact an accounting year (that can vary after storage) and the amount of data is potentially large.
Most approaches revolved around adding a custom attribute to my NSManagedObject. That seems like it's too late to me because the object would not have been loaded into memory at this stage. There were also discussions around NSFetchedResultsController with sectionNameKeyPath's, but again this feels like it's too late in the fetch process. I found NSExpression convoluted so I may well have missed something but it seems like I can't invoke a custom Swift function here. Really, at the end of the day, I expected to find built-in functions for things like DATEPART, DATEADD, DATEDIFF, and I was hoping somebody could point me in the right direction.
As a more concrete example consider the UK tax year which runs 6 April to 5 April. To calculate the tax year I would subtract 3 months and 6 days (to midnight on 5 April). So for a book published on 1 March 2012 I would do the subtraction which would give me 24 November 2011, including 29 days in February for the leap year. From this I simply extract the year part, 2011. So the UK tax year for 1 March 2012 is 2011. I could precompute 2011 and store that in a new column. But then if I move from the UK to Australia the fiscal year changes to July through June. More likely I have a company with a different accounting period than the fiscal year (very likely in the UK). That company then gets taken over by a US group that uses the calendar year, and everyone is happy, except my little app that thinks March 2012 is in 2011.
Here's some boilerplate to get going... with no attempt to group by year:
// The raw date for grouping by - no attempt to extract year
let date = NSExpressionDescription()
date.expression = NSExpression(format: "date_published")
date.name = "date"
date.expressionResultType = .DateAttributeType
// The number of books
let books = NSExpressionDescription()
books.expression = NSExpression(format: "count:(publication_title)")
books.name = "books"
books.expressionResultType = .Integer32AttributeType
// Put a fetch together
let fetch = NSFetchRequest(entityName:"Book")
fetch.resultType = .DictionaryResultType
fetch.propertiesToFetch = [date, books]
fetch.propertiesToGroupBy = [date]
// Execute now
var error: NSError?
if let results = context.executeFetchRequest(fetch,
error: &error) as Array<NSDictionary>? {
for row in results {
let date = row.valueForKey("date") as? NSDate
let books = row.valueForKey("books") as? Int
NSLog("%# %d", date!, books!)
}
} else {
NSLog("Fail!")
}
Thanks for any pointers!
As you're finding, this touches on something that's a weak spot in Core Data's API. It's common to explain that one shouldn't think of Core Data in terms of SQL because it uses a different approach. Dates are where this can be really annoying, because Core Data hides some SQLite capabilities. (They do this at least partly because Core Data isn't a SQLite wrapper, and can work with other, non-SQL storage systems).
The core problem is that Core Data's "Date" type corresponds to an NSDate, and NSDate in turn is just a floating-point number representing the number of seconds since a reference date. It doesn't include year, month, or day. Those values are not even fixed, because the instant in time represented by an NSDate might mean a different date in California as opposed to Japan, for example. The word "date" in these type names is unfortunately misleading.
This is why people generally recommend using extra fields, or at least different data types, for apps using Core Data that need to consider the actual date in some time zone as opposed to a precise moment in time regardless of zone. There isn't a good way to construct a Core Data query that operates on a "Date" field that does what you need. Dealing with this comes down to storing the data you actually need instead of something that just approximates what you need-- except that calling this type "Date" confuses the choice. You don't want a Core Data "Date" type here.
So let's consider one approach to getting the result you need while making SQLite do as much of the work as possible. Let's suppose you replace your date field with an integerDate field that represents the date as an integer (Core Data "Integer 64") using the format yyyyMMDD. Today would be stored as 20151223. In theory this could be done in one step with some NSExpression wizardry, but Core Data doesn't let you group by expressions, so that's out.
Step 1: Get all distinct year values
NSExpression *yearExpression = [NSExpression expressionWithFormat:#"divide:by:(%K,10000)", #"integerDate"];
NSExpressionDescription *yearExpDescription = [[NSExpressionDescription alloc] init];
yearExpDescription.name = #"year";
yearExpDescription.expression = yearExpression;
yearExpDescription.expressionResultType = NSInteger64AttributeType;
NSFetchRequest *distinctYearsRequest = [NSFetchRequest fetchRequestWithEntityName:#"Event"];
distinctYearsRequest.resultType = NSDictionaryResultType;
distinctYearsRequest.returnsDistinctResults = YES;
distinctYearsRequest.propertiesToFetch = #[ yearExpDescription ];
NSError *fetchError = nil;
NSArray *distinctYearsResult = [self.managedObjectContext executeFetchRequest:distinctYearsRequest error:&fetchError];
if (distinctYearsResult != nil) {
NSLog(#"Results by year: %#", distinctYearsResult);
}
NSArray *distinctYears = [distinctYearsResult valueForKey:#"year"];
In the above, yearExpression gets the year portion of integerDate by simple division. When the above completes, distinctYears contains all the years represented by integerDate.
Step 2: Loop through years, getting a count for each:
NSMutableDictionary *countByYear = [NSMutableDictionary dictionary];
for (NSNumber *year in distinctYears) {
NSFetchRequest *countForYearFetch = [NSFetchRequest fetchRequestWithEntityName:#"Event"];
countForYearFetch.resultType = NSDictionaryResultType;
countForYearFetch.propertiesToFetch = #[ yearExpDescription ];
NSExpression *targetYearExpression = [NSExpression expressionForConstantValue:year];
NSPredicate *yearPredicate = [NSComparisonPredicate predicateWithLeftExpression:yearExpression rightExpression:targetYearExpression modifier:NSDirectPredicateModifier type:NSEqualToPredicateOperatorType options:0];
countForYearFetch.predicate = yearPredicate;
NSError *fetchError = nil;
NSUInteger countForYear = [self.managedObjectContext countForFetchRequest:countForYearFetch error:&fetchError];
countByYear[year] = #(countForYear);
}
NSLog(#"Results by year: %#", countByYear);
This does a separate fetch for each year, but keeps the memory overhead low by only fetching the count of results instead of the actual data. When this finishes, countByYear has the number of entries by year, based on the integerDate field.
Having said all this, keep in mind that you do have the option of using SQLite directly instead of using Core Data. PLDatabase will give you an Objective-C style wrapper while still allowing raw SQL queries for everything SQLite can do.
Related
I have an array of objects that has a member that is type Date, and I'm trying to sort the whole array by Date, and it's not sorting correctly.
This is the code I'm using, the name of the array is alarms and the name of the member type Date is time.
alarms.sort(by: { $0.time.compare($1.time) == .orderedAscending })
and whenever I sort it it just doesn't work correctly, and I'm testing it by printing all the values in a for loop.
Can someone help me with the syntax for this?
The compare is a NSDate function. With Date you can just use < operator. For example:
alarms.sort { $0.time < $1.time }
Having said that, compare should work, too, though. I suspect there's some deeper issue here, that perhaps your time values have different dates. You might only be looking at the time portion, but when comparing Date objects, it considers both the date and time. If you only want to look at the time portion, there are several ways of doing that, for example, look at the time interval between time and the start of the day:
let calendar = Calendar.current
alarms.sort {
let elapsed0 = $0.time.timeIntervalSince(calendar.startOfDay(for: $0.time))
let elapsed1 = $1.time.timeIntervalSince(calendar.startOfDay(for: $1.time))
return elapsed0 < elapsed1
}
There are lots of ways to do this, but hopefully this illustrates the idea.
I am using Core Data with an NSFetchedResultsController to display dates in a table view. Upon fetch I want these date objects sorted by the closest upcoming month/day to today while ignoring the year, preferably (if possible) without having to store the fetched results in an array and being forced to perform a separate sort operation on that array. That's the basic idea, the rest is context.
My NSManagedObject subclass Date has 2 attributes:
Date.date -> 1986-02-06 08:00:00 +0000 // the stored date of type Date
Date.equalizedDate -> 02/06 // computed from date ignoring year of type String
If I use NSSortDescriptor(key: "date", ascending: true) on the fetched results controller, then the dates in the table view are sorted by year (not what I want):
Date.date 1978-06-23 07:00:00 +0000 // 1978 = #1
Date.date 1986-02-06 08:00:00 +0000 // 1986 = #2
Date.date 1991-07-26 07:00:00 +0000 // 1991 = #3
If I use NSSortDescriptor(key: "equalizedDate", ascending: true) I can get one step closer by ignoring the year and showing proper ascension but it's not compared to today's date:
Date.date 1986-02-06 08:00:00 +0000 // 02/06 = #1 (February 6th)
Date.date 1978-06-23 07:00:00 +0000 // 06/23 = #2 (June 23rd)
Date.date 1991-07-26 07:00:00 +0000 // 07/26 = #3 (July 26th)
If, for example, today was April 5th, then I would want the fetched results to be fetched and displayed as follows:
// Today is April 5th
Date.date 1978-06-23 07:00:00 +0000 // 06/23 = #1 (June 23rd)
Date.date 1991-07-26 07:00:00 +0000 // 07/26 = #2 (July 26th)
Date.date 1986-02-06 08:00:00 +0000 // 02/06 = #3 (February 6th)
Currently, the only way for me to accomplish this is by storing the fetched results controller results in an array, performing a comparison upon the array, and using the array instead of my fetched results controller for the data source of my table view rows and cells:
// fetchedResults = [Date] <- stored Date objects from fetch
for fetchedDate in fetchedResults {
// formatDateIntoString changes 2015-10-26 05:43:22 +0000 into 10/26
if fetchedDate.equalizedDate < NSDate().formatDateIntoString() {
fetchedResults.removeAtIndex(0)
fetchedResults.append(fetchedDate)
} else {
break
}
}
return fetchedResults
I would prefer to omit this extra layer of logic because it not only eliminates the entire purpose of performance of a fetched results controller but it also gives me wonky troubles and wrong index paths when implementing swipe to delete (link for reference). I know that comparators and selectors don't work for fetched result controllers.
Is there any way to work around this issue?
If you do not care about sections, an easy approach is to keep your sorting for equalizedDate (if you have lots of dates, make sure you set that attribute to be indexed). You may also want to consider changing it to an integer rather than a string.
The array will be sorted from 01/01 to 12/31.
You can then, after the initial fetch, do another quick fetch to find the object closest to today's date.
That object now becomes your point of reference (or startingIndex).
In your FRC delegate, just use the starting index as the offset.
So, when asked to return the objet at index 0, you return a value based on the starting index.
something like...
NSUInteger actualIndex = (index + startingIndex) % count;
It's a little more complicated if you are using sections, but not much.
This way, you don't have to do anything special for sorting. It's also easy to change when the date changes in your app.
I think an easier solution is to simply modify your equalizedDate attribute as follows:
First, make it into a transient property.
Second, make it behave dynamically according to the current date. You can make this attribute very simple, but would then need to write more code to interpret it, or you can try to return something you can use directly.
A simple solution is to return just the offset from todays date (a number from 0 to 365). You would have to add functionality to return the correct date string (or date) based in this number and today's date.
An IMO better solution is to return a date with with a correctly normalized year. You would set the current and future dates to the current year, and all past dates to the next year, also resulting in 365 (366) dates.
You could now easily sort by the transient property, or even use it for sectioning.
I'm trying to create a chart that displays a month's worth of data vs the past month's data broken up by week.
For example viewing the chart today would show July vs June, there would be 5 points on each graph with the following dates:
June: 1-8, 9-15, 16-22, 23-29, 30
July: 1-8, 9-15, 16-22, 23-29, 30-31
The predicate looks like
NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:self.pastStartDate endDate:self.currentEndDate options:HKQueryOptionStrictStartDate];
where self.pastStartDate is the first of last month and self.currentEndDate is the last of the current month.
We then set up the query like so
HKStatisticsCollectionQuery *query = [[HKStatisticsCollectionQuery alloc] initWithQuantityType:quantityType quantitySamplePredicate:predicate options:HKStatisticsOptionDiscreteAverage anchorDate:self.pastStartDate intervalComponents:self.interval];
So for the month, self.interval.day = 7 is set so that we pull a week's worth of data at every interval. The problem is that this interval is not calendar aware, so it doesn't know that the final data point for the last month should only have 1 day, therefore we are getting some overlap.
We have also tried self.interval.weekOfMonth = 1 but again, the interval does not know which month it is in so this is not working either.
Is there a way to create the interval based on the calendar so that it doesn't include more than the end of the month in a given interval?
My data looks like:
objA date1 objnumber2
objB date2 objnumber1
objC date2 objnumber4
objD date2 objnumber3
objE date1 objnumber7
objF date3 objnumber6
objG date1 objnumber5
I am looking for all the objects which are from the Nth last date. The result objects need to be sorted using objnumber (this should be easy using NSSortDescriptor).
So if I specify N=1 (most recent date), I should get [objF] only. (date3 is most recent)
If N=3 (oldest date), I should get sorted [objA, objG, objF]
The sorting part is easy.
My question is do I really need to firstly search for the latest date (using combination of sortdescriptor and nsfetchrequest searchLimit) in the entire data? Then do a second search to find all objects from that date (using nspredicate) and sort it?
Or is there a better way to perform this type of search? How would you generalize this for Nth date instead of last date? That would be a big performance hit no??
NOTE that the date is not known beforehand.
Edit2: okay this is even more complicated since I am using NSDate. So pretty much all the objects have unique dates lol. Gotta throw nsdateformatter in there in the mix too :(
I figured out a solution to my problem.
Instead of using the date, I added a new Int64 NSNumber attribute. Everyday the obj is added, all the objects are tagged with that number for that particular day.
For retriving all objects from Nth day, I firstly do a "fetchlimit 1" nsfetchrequest for that number in a sorted fetchrequest. That gives me the last number.
Now for the Nth last objects, I subtract N from the last number. Then I perform a "nspredicted" nsfetchrequest for all objects with that number attribute. Then I simply just sort the result array.
This resolves my question :D
Looking for experienced advise since I'm newbie to iOS.
I have CoreData entities for "income" and "expenses". I've a tableView to show expenses and another one to show the income. I would like to group the expenses or income by month and display the data (expenses or income) on the tableView, and change the month displayed using toolbar buttons. To show by month seems simple, using an attribute "month" on entities. My question is what's the cleanest way to implement the "monthly display" and adapt the tableView to respond the "next month" and "previous month" buttons?
For instance, I have the tableView showing the February Expenses. When I touch the next button I want to show a tableView with March Expenses.
What kind of approach do you suggest using "months"? This may be quite simple, but for someone who's starting can be quit tough if i take wrong directions. Thanks in advance
The approach I would use would be to have a typedef enum for the month (just for convenience)
typedef enum
{
January = 0,
February,
March,
April,
May,
June,
July,
August,
September,
October,
November,
December
} Month;
Then you would have each entity have an attribute on it which is a number that indicates the month that entity is associated with.
You can then get all the entities associated with a specified month like so:
NSFetchRequest *request= [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:#"<<Your entity name here>>" inManagedObjectContext:managedObjectContext]];
NSPredicate *monthPredicate = [NSPredicate predicateWithFormat:#"month.integerValue == %d", (NSInteger)month];
[request setPredicate:monthPredicate];
that request should get you all the entities associated with the given month. If you want, you could also just save the month as a string. Hope this helps!