相当于 SQL DATEPART 的核心数据

Jos*_*osh 4 core-data ios swift

给定一个已出版的图书表,其中有一date_published列 type NSAttributeType.DateAttributeType,我想知道每年出版了多少本书,如下所示:

Year | Books
-----+------
2013 | 76
2014 | 172
2015 | 155
Run Code Online (Sandbox Code Playgroud)

在普通的旧式 SQL 中,这很简单(尽管根据 RDBMS 略有不同):

SELECT DATEPART(yyyy, date_published) AS "Year", COUNT(*) AS "Books"
FROM books
GROUP BY DATEPART(yyyy, date_published)
Run Code Online (Sandbox Code Playgroud)

总的来说,我对 Swift 和 iOS 很陌生,但我看到的所有内容都建议要么预先计算年份并存储它,要么加载所有数据并自己计算。这些方法都不适合我,因为该年实际上是一个会计年度(存储后可能会有所不同)并且数据量可能很大。

大多数方法都是围绕向我的NSManagedObject. 这对我来说似乎已经太晚了,因为在这个阶段该对象还没有被加载到内存中。也有关于 的讨论NSFetchedResultsControllersectionNameKeyPath但是再次感觉在获取过程中为时已晚。我发现NSExpression很复杂,所以我很可能错过了一些东西,但似乎我无法在这里调用自定义 Swift 函数。真的,在一天结束时,我希望找到 DATEPART、DATEADD、DATEDIFF 等内置函数,并且我希望有人能为我指明正确的方向。

作为一个更具体的例子,考虑英国的纳税年度,即 4 月 6 日至 4 月 5 日。为了计算纳税年度,我会减去 3 个月零 6 天(到 4 月 5 日午夜)。因此,对于 2012 年 3 月 1 日出版的书,我会进行减法,得到 2011 年 11 月 24 日,其中包括闰年 2 月的 29 天。我只是从中提取年份部分 2011 年。因此 2012 年 3 月 1 日的英国纳税年度是 2011 年。我可以预先计算 2011 年并将其存储在新列中。但如果我从英国搬到澳大利亚,财政年度就会更改为七月到六月。我更有可能拥有一家会计期间与财政年度不同的公司(很可能在英国)。然后该公司被一家使用日历年的美国集团接管,每个人都很高兴,除了我的小应用程序认为 2012 年 3 月是 2011 年。

这是一些可以开始的样板......没有尝试按年份分组

// 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!")

}
Run Code Online (Sandbox Code Playgroud)

感谢您的指点!

Tom*_*ton 5

正如您所发现的,这触及了 Core Data API 中的一个弱点。人们通常会解释说,人们不应该从 SQL 的角度来考虑 Core Data,因为它使用了一种不同的方法。日期是真正令人烦恼的地方,因为 Core Data 隐藏了一些 SQLite 功能。(他们这样做至少部分是因为 Core Data 不是 SQLite 包装器,并且可以与其他非 SQL 存储系统一起使用)。

核心问题是 Core Data 的“Date”类型对应于NSDate,而NSDate又只是一个浮点数,表示自参考日期以来的秒数。它不包括年、月或日。这些值甚至不是固定的,因为例如,由 表示的时间NSDate可能意味着加利福尼亚州的日期与日本不同。不幸的是,这些类型名称中的“日期”一词具有误导性。

这就是为什么人们通常建议使用额外的字段,或者至少使用不同的数据类型,对于使用核心数据的应用程序,这些应用程序需要考虑某个时区的实际日期,而不是无论区域如何的精确时间点。没有一种好方法可以构建对“日期”字段进行操作的核心数据查询来满足您的需要。处理这个问题归根结底是存储您实际需要的数据,而不是存储近似您需要的数据——除了将此类型称为“日期”会混淆选择。您不需要此处使用核心数据“日期”类型。

因此,让我们考虑一种方法来获得所需的结果,同时让 SQLite 完成尽可能多的工作。假设您将日期字段替换为integerDate使用以下格式将日期表示为整数(核心数据“Integer 64”)的字段yyyyMMDD。今天将存储为 20151223。理论上,这可以通过一些NSExpression魔法一步完成,但 Core Data 不允许您按表达式进行分组,所以这是不可能的。

第 1 步:获取所有不同的年份值

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"];
Run Code Online (Sandbox Code Playgroud)

上式中,通过简单除法yearExpression得到年份部分integerDate。当上面完成时,distinctYears包含 代表的所有年份integerDate

第 2 步:循环遍历年份,获取每个年份的计数:

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);
Run Code Online (Sandbox Code Playgroud)

这会每年进行一次单独的提取,但通过仅提取结果计数而不是实际数据来保持较低的内存开销。完成后,countByYear将根据字段显示按年份划分的条目数integerDate

说了这么多,请记住,您确实可以选择直接使用 SQLite,而不是使用 Core Data。PLDatabase将为您提供 Objective-C 风格的包装器,同时仍然允许对 SQLite 可以执行的所有操作进行原始 SQL 查询。