-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f1fd176
commit a19df71
Showing
30 changed files
with
1,484 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"object": { | ||
"pins": [ | ||
{ | ||
"package": "CombineExt", | ||
"repositoryURL": "https://github.com/CombineCommunity/CombineExt.git", | ||
"state": { | ||
"branch": null, | ||
"revision": "d7b896fa9ca8b47fa7bcde6b43ef9b70bf8c1f56", | ||
"version": "1.8.1" | ||
} | ||
} | ||
] | ||
}, | ||
"version": 1 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// swift-tools-version:5.5 | ||
// The swift-tools-version declares the minimum version of Swift required to build this package. | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "CoreDataRepository", | ||
defaultLocalization: "en", | ||
platforms: [ | ||
.iOS(.v15), | ||
.macOS(.v12), | ||
.tvOS(.v15), | ||
.watchOS(.v8), | ||
], | ||
products: [ | ||
.library( | ||
name: "CoreDataRepository", | ||
targets: ["CoreDataRepository"] | ||
), | ||
], | ||
dependencies: [ | ||
.package(url: "https://github.com/CombineCommunity/CombineExt.git", .upToNextMajor(from: "1.5.1")) | ||
], | ||
targets: [ | ||
.target( | ||
name: "CoreDataRepository", | ||
dependencies: [ | ||
.product(name: "CombineExt", package: "CombineExt"), | ||
], | ||
path: "./Sources" | ||
) | ||
] | ||
) |
251 changes: 251 additions & 0 deletions
251
CoreDataRepository/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,251 @@ | ||
// CoreDataRepository+Aggregate.swift | ||
// CoreDataRepository | ||
// | ||
// | ||
// MIT License | ||
// | ||
// Copyright © 2023 Andrew Roan | ||
|
||
import Combine | ||
import CoreData | ||
|
||
extension CoreDataRepository { | ||
// MARK: Types | ||
|
||
/// The aggregate function to be calculated | ||
public enum AggregateFunction: String { | ||
case count | ||
case sum | ||
case average | ||
case min | ||
case max | ||
} | ||
|
||
// MARK: Private Functions | ||
|
||
private func request( | ||
function: AggregateFunction, | ||
predicate: NSPredicate, | ||
entityDesc: NSEntityDescription, | ||
attributeDesc: NSAttributeDescription, | ||
groupBy: NSAttributeDescription? = nil | ||
) -> NSFetchRequest<NSDictionary> { | ||
let expDesc = NSExpressionDescription.aggregate(function: function, attributeDesc: attributeDesc) | ||
let request = NSFetchRequest<NSDictionary>(entityName: entityDesc.managedObjectClassName) | ||
request.predicate = predicate | ||
request.entity = entityDesc | ||
request.returnsObjectsAsFaults = false | ||
request.resultType = .dictionaryResultType | ||
if function == .count { | ||
request.propertiesToFetch = [attributeDesc.name, expDesc] | ||
} else { | ||
request.propertiesToFetch = [expDesc] | ||
} | ||
|
||
if let groupBy = groupBy { | ||
request.propertiesToGroupBy = [groupBy.name] | ||
} | ||
request.sortDescriptors = [NSSortDescriptor(key: attributeDesc.name, ascending: false)] | ||
return request | ||
} | ||
|
||
/// Calculates aggregate values | ||
/// - Parameters | ||
/// - function: Function | ||
/// - predicate: NSPredicate | ||
/// - entityDesc: NSEntityDescription | ||
/// - attributeDesc: NSAttributeDescription | ||
/// - groupBy: NSAttributeDescription? = nil | ||
/// - Returns | ||
/// - `[[String: Value]]` | ||
/// | ||
private static func aggregate<Value: Numeric>( | ||
context: NSManagedObjectContext, | ||
request: NSFetchRequest<NSDictionary> | ||
) throws -> [[String: Value]] { | ||
let result = try context.fetch(request) | ||
return result as? [[String: Value]] ?? [] | ||
} | ||
|
||
private static func send<Value>( | ||
context: NSManagedObjectContext, | ||
request: NSFetchRequest<NSDictionary> | ||
) async -> Result<[[String: Value]], CoreDataRepositoryError> where Value: Numeric { | ||
await context.performInScratchPad { scratchPad in | ||
do { | ||
let result: [[String: Value]] = try Self.aggregate(context: scratchPad, request: request) | ||
return result | ||
} catch { | ||
throw CoreDataRepositoryError.coreData(error as NSError) | ||
} | ||
} | ||
} | ||
|
||
// MARK: Public Functions | ||
|
||
/// Calculate the count for a fetchRequest | ||
/// - Parameters: | ||
/// - predicate: NSPredicate | ||
/// - entityDesc: NSEntityDescription | ||
/// - Returns | ||
/// - Result<[[String: Value]], CoreDataRepositoryError> | ||
/// | ||
public func count<Value: Numeric>( | ||
predicate: NSPredicate, | ||
entityDesc: NSEntityDescription | ||
) async -> Result<[[String: Value]], CoreDataRepositoryError> { | ||
let _request = NSFetchRequest<NSDictionary>(entityName: entityDesc.name ?? "") | ||
_request.predicate = predicate | ||
_request | ||
.sortDescriptors = | ||
[NSSortDescriptor(key: entityDesc.attributesByName.values.first!.name, ascending: true)] | ||
return await context.performInScratchPad { scratchPad in | ||
do { | ||
let count = try scratchPad.count(for: _request) | ||
return [["countOf\(entityDesc.name ?? "")": Value(exactly: count) ?? Value.zero]] | ||
} catch { | ||
throw CoreDataRepositoryError.coreData(error as NSError) | ||
} | ||
} | ||
} | ||
|
||
/// Calculate the sum for a fetchRequest | ||
/// - Parameters: | ||
/// - predicate: NSPredicate | ||
/// - entityDesc: NSEntityDescription | ||
/// - attributeDesc: NSAttributeDescription | ||
/// - groupBy: NSAttributeDescription? = nil | ||
/// - Returns | ||
/// - Result<[[String: Value]], CoreDataRepositoryError> | ||
/// | ||
public func sum<Value: Numeric>( | ||
predicate: NSPredicate, | ||
entityDesc: NSEntityDescription, | ||
attributeDesc: NSAttributeDescription, | ||
groupBy: NSAttributeDescription? = nil | ||
) async -> Result<[[String: Value]], CoreDataRepositoryError> { | ||
let _request = request( | ||
function: .sum, | ||
predicate: predicate, | ||
entityDesc: entityDesc, | ||
attributeDesc: attributeDesc, | ||
groupBy: groupBy | ||
) | ||
guard entityDesc == attributeDesc.entity else { | ||
return .failure(.propertyDoesNotMatchEntity) | ||
} | ||
return await Self.send(context: context, request: _request) | ||
} | ||
|
||
/// Calculate the average for a fetchRequest | ||
/// - Parameters: | ||
/// - predicate: NSPredicate | ||
/// - entityDesc: NSEntityDescription | ||
/// - attributeDesc: NSAttributeDescription | ||
/// - groupBy: NSAttributeDescription? = nil | ||
/// - Returns | ||
/// - Result<[[String: Value]], CoreDataRepositoryError> | ||
/// | ||
public func average<Value: Numeric>( | ||
predicate: NSPredicate, | ||
entityDesc: NSEntityDescription, | ||
attributeDesc: NSAttributeDescription, | ||
groupBy: NSAttributeDescription? = nil | ||
) async -> Result<[[String: Value]], CoreDataRepositoryError> { | ||
let _request = request( | ||
function: .average, | ||
predicate: predicate, | ||
entityDesc: entityDesc, | ||
attributeDesc: attributeDesc, | ||
groupBy: groupBy | ||
) | ||
guard entityDesc == attributeDesc.entity else { | ||
return .failure(.propertyDoesNotMatchEntity) | ||
} | ||
return await Self.send(context: context, request: _request) | ||
} | ||
|
||
/// Calculate the min for a fetchRequest | ||
/// - Parameters: | ||
/// - predicate: NSPredicate | ||
/// - entityDesc: NSEntityDescription | ||
/// - attributeDesc: NSAttributeDescription | ||
/// - groupBy: NSAttributeDescription? = nil | ||
/// - Returns | ||
/// - Result<[[String: Value]], CoreDataRepositoryError> | ||
/// | ||
public func min<Value: Numeric>( | ||
predicate: NSPredicate, | ||
entityDesc: NSEntityDescription, | ||
attributeDesc: NSAttributeDescription, | ||
groupBy: NSAttributeDescription? = nil | ||
) async -> Result<[[String: Value]], CoreDataRepositoryError> { | ||
let _request = request( | ||
function: .min, | ||
predicate: predicate, | ||
entityDesc: entityDesc, | ||
attributeDesc: attributeDesc, | ||
groupBy: groupBy | ||
) | ||
guard entityDesc == attributeDesc.entity else { | ||
return .failure(.propertyDoesNotMatchEntity) | ||
} | ||
return await Self.send(context: context, request: _request) | ||
} | ||
|
||
/// Calculate the max for a fetchRequest | ||
/// - Parameters: | ||
/// - predicate: NSPredicate | ||
/// - entityDesc: NSEntityDescription | ||
/// - attributeDesc: NSAttributeDescription | ||
/// - groupBy: NSAttributeDescription? = nil | ||
/// - Returns | ||
/// - Result<[[String: Value]], CoreDataRepositoryError> | ||
/// | ||
public func max<Value: Numeric>( | ||
predicate: NSPredicate, | ||
entityDesc: NSEntityDescription, | ||
attributeDesc: NSAttributeDescription, | ||
groupBy: NSAttributeDescription? = nil | ||
) async -> Result<[[String: Value]], CoreDataRepositoryError> { | ||
let _request = request( | ||
function: .max, | ||
predicate: predicate, | ||
entityDesc: entityDesc, | ||
attributeDesc: attributeDesc, | ||
groupBy: groupBy | ||
) | ||
guard entityDesc == attributeDesc.entity else { | ||
return .failure(.propertyDoesNotMatchEntity) | ||
} | ||
return await Self.send(context: context, request: _request) | ||
} | ||
} | ||
|
||
// MARK: Extensions | ||
|
||
extension NSExpression { | ||
/// Convenience initializer for NSExpression that represent an aggregate function on a keypath | ||
fileprivate convenience init( | ||
function: CoreDataRepository.AggregateFunction, | ||
attributeDesc: NSAttributeDescription | ||
) { | ||
let keyPathExp = NSExpression(forKeyPath: attributeDesc.name) | ||
self.init(forFunction: "\(function.rawValue):", arguments: [keyPathExp]) | ||
} | ||
} | ||
|
||
extension NSExpressionDescription { | ||
/// Convenience initializer for NSExpressionDescription that represent the properties to fetch in NSFetchRequest | ||
fileprivate static func aggregate( | ||
function: CoreDataRepository.AggregateFunction, | ||
attributeDesc: NSAttributeDescription | ||
) -> NSExpressionDescription { | ||
let expression = NSExpression(function: function, attributeDesc: attributeDesc) | ||
let expDesc = NSExpressionDescription() | ||
expDesc.expression = expression | ||
expDesc.name = "\(function.rawValue)Of\(attributeDesc.name.capitalized)" | ||
expDesc.expressionResultType = attributeDesc.attributeType | ||
return expDesc | ||
} | ||
} |
Oops, something went wrong.