| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496 |
- //
- // CarbStore.swift
- // CarbKit
- //
- // Created by Nathan Racklyeft on 1/3/16.
- // Copyright © 2016 Nathan Racklyeft. All rights reserved.
- //
- import Foundation
- import CoreData
- import HealthKit
- import os.log
- public enum CarbStoreResult<T> {
- case success(T)
- case failure(CarbStore.CarbStoreError)
- }
- public enum CarbAbsorptionModel {
- case linear
- case nonlinear
- case adaptiveRateNonlinear
- }
- public protocol CarbStoreDelegate: AnyObject {
- /**
- Informs the delegate that the carb store has updated carb data.
- - Parameter carbStore: The carb store that has updated carb data.
- */
- func carbStoreHasUpdatedCarbData(_ carbStore: CarbStore)
- /**
- Informs the delegate that an internal error occurred.
- - parameter carbStore: The carb store
- - parameter error: The error describing the issue
- */
- func carbStore(_ carbStore: CarbStore, didError error: CarbStore.CarbStoreError)
- }
- /**
- Manages storage, retrieval, and calculation of carbohydrate data.
- There are two tiers of storage:
- * Persistant cache, stored in Core Data, used to ensure access if the app is suspended and re-launched while the Health database
- is protected and to provide data for upload to remote data services. Backfilled from HealthKit data up to observation interval.
- ```
- 0 [max(cacheLength, observationInterval, defaultAbsorptionTimes.slow * 2)]
- |––––––––––––|
- ```
- * HealthKit data, managed by the current application and persisted indefinitely
- ```
- 0
- |––––––––––––––––––--->
- ```
- */
- public final class CarbStore: HealthKitSampleStore {
-
- /// Notification posted when carb entries were changed, either via add/replace/delete methods or from HealthKit
- public static let carbEntriesDidChange = NSNotification.Name(rawValue: "com.loopkit.CarbStore.carbEntriesDidChange")
- public typealias DefaultAbsorptionTimes = (fast: TimeInterval, medium: TimeInterval, slow: TimeInterval)
- public enum CarbStoreError: Error {
- // The store isn't correctly configured for the requested operation
- case notConfigured
- // The health store request returned an error
- case healthStoreError(Error)
- // The core data request returned an error
- case coreDataError(Error)
- // The requested sample can't be modified by this store
- case unauthorized
- // No data was found to match the specified request
- case noData
- init?(error: PersistenceController.PersistenceControllerError?) {
- guard let error = error, case .coreDataError(let coreDataError) = error else {
- return nil
- }
- self = .coreDataError(coreDataError as Error)
- }
- }
- private let carbType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.dietaryCarbohydrates)!
- /// The preferred unit. iOS currently only supports grams for dietary carbohydrates.
- public override var preferredUnit: HKUnit! {
- return super.preferredUnit
- }
- /// A history of recently applied schedule overrides.
- private let overrideHistory: TemporaryScheduleOverrideHistory?
- /// Carbohydrate-to-insulin ratio
- public var carbRatioSchedule: CarbRatioSchedule? {
- get {
- return lockedCarbRatioSchedule.value
- }
- set {
- lockedCarbRatioSchedule.value = newValue
- }
- }
- private let lockedCarbRatioSchedule: Locked<CarbRatioSchedule?>
- /// The carb ratio schedule, applying recent overrides relative to the current moment in time.
- public var carbRatioScheduleApplyingOverrideHistory: CarbRatioSchedule? {
- if let carbRatioSchedule = carbRatioSchedule {
- return overrideHistory?.resolvingRecentCarbRatioSchedule(carbRatioSchedule)
- } else {
- return nil
- }
- }
- /// A trio of default carbohydrate absorption times. Defaults to 2, 3, and 4 hours.
- public let defaultAbsorptionTimes: DefaultAbsorptionTimes
- /// Insulin-to-glucose sensitivity
- public var insulinSensitivitySchedule: InsulinSensitivitySchedule? {
- get {
- return lockedInsulinSensitivitySchedule.value
- }
- set {
- lockedInsulinSensitivitySchedule.value = newValue
- }
- }
- private let lockedInsulinSensitivitySchedule: Locked<InsulinSensitivitySchedule?>
- /// The insulin sensitivity schedule, applying recent overrides relative to the current moment in time.
- public var insulinSensitivityScheduleApplyingOverrideHistory: InsulinSensitivitySchedule? {
- if let insulinSensitivitySchedule = insulinSensitivitySchedule {
- return overrideHistory?.resolvingRecentInsulinSensitivitySchedule(insulinSensitivitySchedule)
- } else {
- return nil
- }
- }
- /// The computed carbohydrate sensitivity schedule based on the insulin sensitivity and carb ratio schedules.
- public var carbSensitivitySchedule: CarbSensitivitySchedule? {
- guard let insulinSensitivitySchedule = insulinSensitivitySchedule, let carbRatioSchedule = carbRatioSchedule else {
- return nil
- }
- return .carbSensitivitySchedule(insulinSensitivitySchedule: insulinSensitivitySchedule, carbRatioSchedule: carbRatioSchedule)
- }
- /// The expected delay in the appearance of glucose effects, accounting for both digestion and sensor lag
- public let delay: TimeInterval
- /// The interval between effect values to use for the calculated timelines.
- public let delta: TimeInterval
- /// The factor by which the entered absorption time can be extended to accomodate slower-than-expected absorption
- public let absorptionTimeOverrun: Double
-
- /// Carb absorption model
- public let carbAbsorptionModel: CarbAbsorptionModel
- /// The interval of carb data to keep in cache
- public let cacheLength: TimeInterval
- /// The interval to observe HealthKit data to populate the cache
- public let observationInterval: TimeInterval
- private let storeEntriesToHealthKit: Bool
- private let cacheStore: PersistenceController
- /// The sync version used for new samples written to HealthKit
- /// Choose a lower or higher sync version if the same sample might be written twice (e.g. from an extension and from an app) for deterministic conflict resolution
- public let syncVersion: Int
- public weak var delegate: CarbStoreDelegate?
- private let queue = DispatchQueue(label: "com.loopkit.CarbStore.queue", qos: .utility)
- private let log = OSLog(category: "CarbStore")
-
- static let healthKitQueryAnchorMetadataKey = "com.loopkit.CarbStore.hkQueryAnchor"
-
- var settings = CarbModelSettings(absorptionModel: PiecewiseLinearAbsorption(), initialAbsorptionTimeOverrun: 1.5, adaptiveAbsorptionRateEnabled: false)
- private let provenanceIdentifier: String
- /**
- Initializes a new instance of the store.
- - returns: A new instance of the store
- */
- public init(
- healthStore: HKHealthStore,
- observeHealthKitSamplesFromOtherApps: Bool = true,
- storeEntriesToHealthKit: Bool = true,
- cacheStore: PersistenceController,
- cacheLength: TimeInterval,
- defaultAbsorptionTimes: DefaultAbsorptionTimes,
- observationInterval: TimeInterval,
- carbRatioSchedule: CarbRatioSchedule? = nil,
- insulinSensitivitySchedule: InsulinSensitivitySchedule? = nil,
- overrideHistory: TemporaryScheduleOverrideHistory? = nil,
- syncVersion: Int = 1,
- absorptionTimeOverrun: Double = 1.5,
- calculationDelta: TimeInterval = 5 /* minutes */ * 60,
- effectDelay: TimeInterval = 10 /* minutes */ * 60,
- carbAbsorptionModel: CarbAbsorptionModel = .nonlinear,
- provenanceIdentifier: String
- ) {
- self.storeEntriesToHealthKit = storeEntriesToHealthKit
- self.cacheStore = cacheStore
- self.defaultAbsorptionTimes = defaultAbsorptionTimes
- self.lockedCarbRatioSchedule = Locked(carbRatioSchedule)
- self.lockedInsulinSensitivitySchedule = Locked(insulinSensitivitySchedule)
- self.overrideHistory = overrideHistory
- self.syncVersion = syncVersion
- self.absorptionTimeOverrun = absorptionTimeOverrun
- self.delta = calculationDelta
- self.delay = effectDelay
- self.cacheLength = cacheLength
- self.observationInterval = observationInterval
- self.carbAbsorptionModel = carbAbsorptionModel
- self.provenanceIdentifier = provenanceIdentifier
-
- let observationEnabled = observationInterval > 0
- super.init(healthStore: healthStore,
- observeHealthKitSamplesFromCurrentApp: true,
- observeHealthKitSamplesFromOtherApps: observeHealthKitSamplesFromOtherApps,
- type: carbType,
- observationStart: Date(timeIntervalSinceNow: -self.observationInterval),
- observationEnabled: observationEnabled)
- // Carb model settings based on the selected absorption model
- switch self.carbAbsorptionModel {
- case .linear:
- self.settings = CarbModelSettings(absorptionModel: LinearAbsorption(), initialAbsorptionTimeOverrun: absorptionTimeOverrun, adaptiveAbsorptionRateEnabled: false)
- case .nonlinear:
- self.settings = CarbModelSettings(absorptionModel: PiecewiseLinearAbsorption(), initialAbsorptionTimeOverrun: absorptionTimeOverrun, adaptiveAbsorptionRateEnabled: false)
- case .adaptiveRateNonlinear:
- self.settings = CarbModelSettings(absorptionModel: PiecewiseLinearAbsorption(), initialAbsorptionTimeOverrun: 1.0, adaptiveAbsorptionRateEnabled: true, adaptiveRateStandbyIntervalFraction: 0.2)
- }
- let semaphore = DispatchSemaphore(value: 0)
- cacheStore.onReady { (error) in
- guard error == nil else {
- semaphore.signal()
- return
- }
-
- cacheStore.fetchAnchor(key: CarbStore.healthKitQueryAnchorMetadataKey) { (anchor) in
- self.queue.async {
- self.queryAnchor = anchor
- if !self.authorizationRequired {
- self.createQuery()
- }
- self.migrateLegacyCarbEntryKeys()
-
- semaphore.signal()
- }
- }
- }
- semaphore.wait()
- }
- // Migrate modifiedCarbEntries and deletedCarbEntryIDs
- private func migrateLegacyCarbEntryKeys() {
- cacheStore.managedObjectContext.performAndWait {
- var changed = false
- for entry in UserDefaults.standard.modifiedCarbEntries ?? [] {
- let object = CachedCarbObject(context: self.cacheStore.managedObjectContext)
- object.create(from: entry)
- changed = true
- }
- // Note: We no longer migrate UserDefaults.standard.deletedCarbEntryIds since we don't have a startDate (only
- // external ID) and CachedCarbObject requires a starDate. This only prevents a deleted carb entry that was previously
- // uploaded to Nightscout, but not yet deleted from Nightscout, from being deleted in Nightscout.
- if changed {
- self.cacheStore.save()
- }
- }
- UserDefaults.standard.purgeLegacyCarbEntryKeys()
- }
- // MARK: - HealthKitSampleStore
-
- override func queryAnchorDidChange() {
- cacheStore.storeAnchor(queryAnchor, key: CarbStore.healthKitQueryAnchorMetadataKey)
- }
- override func processResults(from query: HKAnchoredObjectQuery, added: [HKSample], deleted: [HKDeletedObject], anchor: HKQueryAnchor, completion: @escaping (Bool) -> Void) {
- queue.async {
- guard anchor != self.queryAnchor else {
- self.log.default("Skipping processing results from anchored object query, as anchor was already processed")
- completion(true)
- return
- }
- var changed = false
- var error: CarbStoreError?
- self.cacheStore.managedObjectContext.performAndWait {
- do {
- let date = Date()
- // Add new samples
- if let samples = added as? [HKQuantitySample] {
- for sample in samples {
- if try self.addCarbEntry(for: sample, on: date) {
- self.log.debug("Saved sample %@ into cache from HKAnchoredObjectQuery", sample.uuid.uuidString)
- changed = true
- } else {
- self.log.default("Sample %@ from HKAnchoredObjectQuery already present in cache", sample.uuid.uuidString)
- }
- }
- }
- // Delete deleted samples
- for sample in deleted {
- if try self.deleteCarbEntry(for: sample.uuid, on: date) {
- self.log.debug("Deleted sample %@ from cache from HKAnchoredObjectQuery", sample.uuid.uuidString)
- changed = true
- }
- }
- guard changed else {
- return
- }
- error = CarbStoreError(error: self.cacheStore.save())
- } catch let coreDataError {
- error = .coreDataError(coreDataError)
- }
- }
- if let error = error {
- self.delegate?.carbStore(self, didError: error)
- completion(false)
- return
- }
- if !changed {
- completion(true)
- return
- }
- self.handleUpdatedCarbData()
- completion(true)
- }
- }
- }
- // MARK: - Fetching
- extension CarbStore {
- /// Retrieves carb entries within the specified date range
- ///
- /// - Parameters:
- /// - start: The earliest date of values to retrieve
- /// - end: The latest date of values to retrieve, if provided
- /// - completion: A closure called once the values have been retrieved
- /// - result: An array of carb entries, in chronological order by startDate, or error
- public func getCarbEntries(start: Date? = nil, end: Date? = nil, completion: @escaping (_ result: CarbStoreResult<[StoredCarbEntry]>) -> Void) {
- queue.async {
- completion(self.getCarbEntries(start: start, end: end))
- }
- }
- /// Retrieves carb entries within the specified date range
- ///
- /// - Parameters:
- /// - start: The earliest date of values to retrieve
- /// - end: The latest date of values to retrieve, if provided
- /// - Returns: An array of carb entries, in chronological order by startDate, or error
- private func getCarbEntries(start: Date? = nil, end: Date? = nil) -> CarbStoreResult<[StoredCarbEntry]> {
- dispatchPrecondition(condition: .onQueue(queue))
- var entries: [StoredCarbEntry] = []
- var error: CarbStoreError?
- cacheStore.managedObjectContext.performAndWait {
- do {
- entries = try self.getActiveCachedCarbObjects(start: start, end: end).map { StoredCarbEntry(managedObject: $0) }
- } catch let coreDataError {
- error = .coreDataError(coreDataError)
- }
- }
- if let error = error {
- return .failure(error)
- }
- return .success(entries)
- }
- /// Retrieves active (not superceded, non-delete operation) cached carb objects within the specified date range
- ///
- /// - Parameters:
- /// - start: The earliest date of values to retrieve
- /// - end: The latest date of values to retrieve, if provided
- /// - Returns: An array of cached carb objects
- private func getActiveCachedCarbObjects(start: Date? = nil, end: Date? = nil) throws -> [CachedCarbObject] {
- dispatchPrecondition(condition: .onQueue(queue))
- var predicates = [NSPredicate(format: "operation != %d", Operation.delete.rawValue),
- NSPredicate(format: "supercededDate == NIL")]
- if let start = start {
- predicates.append(NSPredicate(format: "startDate >= %@", start as NSDate))
- }
- if let end = end {
- predicates.append(NSPredicate(format: "startDate < %@", end as NSDate))
- }
- let request: NSFetchRequest<CachedCarbObject> = CachedCarbObject.fetchRequest()
- request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
- request.sortDescriptors = [NSSortDescriptor(key: "startDate", ascending: true)]
- return try self.cacheStore.managedObjectContext.fetch(request)
- }
- /// Retrieves carb entries from HealthKit within the specified date range and interprets their
- /// absorption status based on the provided glucose effect
- ///
- /// - Parameters:
- /// - start: The earliest date of values to retrieve
- /// - end: The latest date of values to retrieve, if provided
- /// - effectVelocities: A timeline of glucose effect velocities, ordered by start date
- /// - completion: A closure calld once the values have been retrieved
- /// - result: An array of carb entries, in chronological order by startDate
- public func getCarbStatus(
- start: Date,
- end: Date? = nil,
- effectVelocities: [GlucoseEffectVelocity]? = nil,
- completion: @escaping (_ result: CarbStoreResult<[CarbStatus<StoredCarbEntry>]>) -> Void
- ) {
- getCarbEntries(start: start, end: end) { (result) in
- switch result {
- case .success(let entries):
- let status = entries.map(
- to: effectVelocities ?? [],
- carbRatio: self.carbRatioScheduleApplyingOverrideHistory,
- insulinSensitivity: self.insulinSensitivityScheduleApplyingOverrideHistory,
- absorptionTimeOverrun: self.absorptionTimeOverrun,
- defaultAbsorptionTime: self.defaultAbsorptionTimes.medium,
- delay: self.delay,
- initialAbsorptionTimeOverrun: self.settings.initialAbsorptionTimeOverrun,
- absorptionModel: self.settings.absorptionModel,
- adaptiveAbsorptionRateEnabled: self.settings.adaptiveAbsorptionRateEnabled,
- adaptiveRateStandbyIntervalFraction: self.settings.adaptiveRateStandbyIntervalFraction
- )
- completion(.success(status))
- case .failure(let error):
- completion(.failure(error))
- }
- }
- }
- }
- // MARK: - Modification
- extension CarbStore {
- public func addCarbEntry(_ entry: NewCarbEntry, completion: @escaping (_ result: CarbStoreResult<StoredCarbEntry>) -> Void) {
- queue.async {
- var storedEntry: StoredCarbEntry?
- var error: CarbStoreError?
- self.cacheStore.managedObjectContext.performAndWait {
- do {
- let syncIdentifier = try self.cacheStore.managedObjectContext.generateUniqueSyncIdentifier()
- let newObject = CachedCarbObject(context: self.cacheStore.managedObjectContext)
- newObject.create(from: entry,
- provenanceIdentifier: self.provenanceIdentifier,
- syncIdentifier: syncIdentifier,
- syncVersion: self.syncVersion)
- if let saveError = CarbStoreError(error: self.cacheStore.save()) {
- error = saveError
- return
- }
- self.saveEntryToHealthKit(newObject)
- storedEntry = StoredCarbEntry(managedObject: newObject)
- } catch let coreDataError {
- error = .coreDataError(coreDataError)
- }
- }
- if let error = error {
- completion(.failure(error))
- return
- }
- completion(.success(storedEntry!))
- self.handleUpdatedCarbData()
- }
- }
- public func replaceCarbEntry(_ oldEntry: StoredCarbEntry, withEntry newEntry: NewCarbEntry, completion: @escaping (_ result: CarbStoreResult<StoredCarbEntry>) -> Void) {
- guard oldEntry.createdByCurrentApp else {
- completion(.failure(.unauthorized))
- return
- }
- queue.async {
- var storedEntry: StoredCarbEntry?
- var error: CarbStoreError?
- self.cacheStore.managedObjectContext.performAndWait {
- do {
- guard let oldObject = try self.cacheStore.managedObjectContext.cachedCarbObjectFromStoredCarbEntry(oldEntry) else {
- error = .noData
- return
- }
- // Use same date for superceding old object and adding new object
- let date = Date()
- oldObject.supercededDate = date
- let newObject = CachedCarbObject(context: self.cacheStore.managedObjectContext)
- newObject.update(from: newEntry, replacing: oldObject, on: date)
- if let saveError = CarbStoreError(error: self.cacheStore.save()) {
- error = saveError
- return
- }
- self.saveEntryToHealthKit(newObject)
- storedEntry = StoredCarbEntry(managedObject: newObject)
- } catch let coreDataError {
- error = .coreDataError(coreDataError)
- }
- }
- if let error = error {
- completion(.failure(error))
- return
- }
- completion(.success(storedEntry!))
- self.handleUpdatedCarbData()
- }
- }
- private func saveEntryToHealthKit(_ object: CachedCarbObject) {
- dispatchPrecondition(condition: .onQueue(queue))
- guard storeEntriesToHealthKit else {
- return
- }
- let quantitySample = object.quantitySample
- var error: Error?
- // Save object to HealthKit, log any errors, but do not fail
- let dispatchGroup = DispatchGroup()
- dispatchGroup.enter()
- self.healthStore.save(quantitySample) { (_, healthKitError) in
- error = healthKitError
- dispatchGroup.leave()
- }
- dispatchGroup.wait()
- if let error = error {
- self.log.error("Error saving HealthKit object: %@", String(describing: error))
- return
- }
- // Update Core Data with the change, log any errors, but do not fail
- object.uuid = quantitySample.uuid
- if let error = self.cacheStore.save() {
- self.log.error("Error updating CachedCarbObject after saving HealthKit object: %@", String(describing: error))
- object.uuid = nil
- }
- }
- public func deleteCarbEntry(_ oldEntry: StoredCarbEntry, completion: @escaping (_ result: CarbStoreResult<Bool>) -> Void) {
- guard oldEntry.createdByCurrentApp else {
- completion(.failure(.unauthorized))
- return
- }
- queue.async {
- var error: CarbStoreError?
- self.cacheStore.managedObjectContext.performAndWait {
- do {
- guard let oldObject = try self.cacheStore.managedObjectContext.cachedCarbObjectFromStoredCarbEntry(oldEntry) else {
- error = .noData
- return
- }
- // Use same date for superceding old object and adding new object; also used for userDeletedDate
- let date = Date()
- oldObject.supercededDate = date
- let newObject = CachedCarbObject(context: self.cacheStore.managedObjectContext)
- newObject.delete(from: oldObject, on: date)
- if let saveError = CarbStoreError(error: self.cacheStore.save()) {
- error = saveError
- return
- }
- self.deleteObjectFromHealthKit(newObject)
- } catch let coreDataError {
- error = .coreDataError(coreDataError)
- }
- }
- if let error = error {
- completion(.failure(error))
- return
- }
- completion(.success(true))
- self.handleUpdatedCarbData()
- }
- }
- private func deleteObjectFromHealthKit(_ object: CachedCarbObject) {
- dispatchPrecondition(condition: .onQueue(queue))
- // If the object does not have a UUID, then it was never saved to HealthKit, so no need to delete
- guard object.uuid != nil else {
- return
- }
- var error: Error?
- // Delete object from HealthKit, log any errors, but do not fail
- let dispatchGroup = DispatchGroup()
- dispatchGroup.enter()
- self.healthStore.deleteObjects(of: self.carbType, predicate: HKQuery.predicateForObject(with: object.uuid!)) { (_, _, healthKitError) in
- error = healthKitError
- dispatchGroup.leave()
- }
- dispatchGroup.wait()
- if let error = error {
- self.log.error("Error deleting HealthKit object: %@", String(describing: error))
- return
- }
- // Update Core Data with the change, log any errors, but do not fail
- object.uuid = nil
- if let error = self.cacheStore.save() {
- self.log.error("Error updating CachedCarbObject after deleting HealthKit object: %@", String(describing: error))
- }
- }
- private func addCarbEntry(for sample: HKQuantitySample, on date: Date) throws -> Bool {
- dispatchPrecondition(condition: .onQueue(queue))
- // Are there any objects matching the UUID?
- let request: NSFetchRequest<CachedCarbObject> = CachedCarbObject.fetchRequest()
- request.predicate = NSPredicate(format: "uuid == %@", sample.uuid as NSUUID)
- request.fetchLimit = 1
- let count = try cacheStore.managedObjectContext.count(for: request)
- guard count == 0 else {
- return false
- }
- // Find all objects being replaced
- let replacedObjects = try fetchRelatedCarbObjects(for: sample)
- // Mark all objects as superceded, as necessary
- replacedObjects.filter({ $0.supercededDate == nil }).forEach({ $0.supercededDate = date })
- // Add an object (create or update) for this UUID
- let object = CachedCarbObject(context: cacheStore.managedObjectContext)
- if let replacedObject = replacedObjects.last {
- object.update(from: sample, replacing: replacedObject, on: date)
- } else {
- object.create(from: sample, on: date)
- }
- return true
- }
- private func deleteCarbEntry(for uuid: UUID, on date: Date) throws -> Bool {
- dispatchPrecondition(condition: .onQueue(queue))
- // Fetch objects matching the UUID, if none found, then nothing to delete, sorted by last seen anchor key
- let request: NSFetchRequest<CachedCarbObject> = CachedCarbObject.fetchRequest()
- request.predicate = NSPredicate(format: "uuid == %@", uuid as NSUUID)
- request.sortDescriptors = [NSSortDescriptor(key: "anchorKey", ascending: true)]
- let objects = try cacheStore.managedObjectContext.fetch(request)
- guard !objects.isEmpty else {
- return false
- }
- // Find all unsuperceded create/update objects, if none found, then nothing to delete
- let supercededObjects = objects.filter { $0.operation != .delete && $0.supercededDate == nil }
- guard !supercededObjects.isEmpty else {
- return false
- }
- // Mark as superceded
- supercededObjects.forEach { $0.supercededDate = date }
- // If we don't yet have a delete object, then add one
- if !objects.contains(where: { $0.operation == .delete }), let supercededObject = supercededObjects.last {
- let object = CachedCarbObject(context: cacheStore.managedObjectContext)
- object.delete(from: supercededObject, on: date)
- }
- return true
- }
- // Fetch all objects that are different versions of the specified sample, using sync identifier
- private func fetchRelatedCarbObjects(for sample: HKQuantitySample) throws -> [CachedCarbObject] {
- dispatchPrecondition(condition: .onQueue(queue))
- guard let syncIdentifier = sample.syncIdentifier else {
- return []
- }
- let request: NSFetchRequest<CachedCarbObject> = CachedCarbObject.fetchRequest()
- request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [NSPredicate(format: "provenanceIdentifier == %@", sample.provenanceIdentifier),
- NSPredicate(format: "syncIdentifier == %@", syncIdentifier)])
- request.sortDescriptors = [NSSortDescriptor(key: "anchorKey", ascending: true)]
- return try cacheStore.managedObjectContext.fetch(request)
- }
- }
- // MARK: - Watch Synchronization
- extension CarbStore {
- /// Get carb objects in main app to deliver to Watch extension
- public func getSyncCarbObjects(start: Date? = nil, end: Date? = nil, completion: @escaping (_ result: CarbStoreResult<[SyncCarbObject]>) -> Void) {
- queue.async {
- var objects: [SyncCarbObject] = []
- var error: CarbStoreError?
- self.cacheStore.managedObjectContext.performAndWait {
- do {
- objects = try self.getActiveCachedCarbObjects(start: start, end: end).map { SyncCarbObject(managedObject: $0) }
- } catch let coreDataError {
- error = .coreDataError(coreDataError)
- }
- }
- if let error = error {
- completion(.failure(error))
- return
- }
- completion(.success(objects))
- }
- }
- /// Store carb objects in Watch extension
- public func setSyncCarbObjects(_ objects: [SyncCarbObject], completion: @escaping (CarbStoreError?) -> Void) {
- queue.async {
- if let error = self.purgeCachedCarbObjectsUnconditionally() {
- completion(error)
- return
- }
- var error: CarbStoreError?
- self.cacheStore.managedObjectContext.performAndWait {
- guard !objects.isEmpty else {
- return
- }
- objects.forEach {
- let object = CachedCarbObject(context: self.cacheStore.managedObjectContext)
- object.update(from: $0)
- }
- error = CarbStoreError(error: self.cacheStore.save())
- }
- completion(error)
- self.handleUpdatedCarbData()
- }
- }
- }
- // MARK: - Cache management
- extension CarbStore {
- public var earliestCacheDate: Date {
- return Date(timeIntervalSinceNow: -cacheLength)
- }
- private func purgeExpiredCachedCarbObjects() {
- purgeCachedCarbObjects(before: earliestCacheDate)
- }
- @discardableResult
- private func purgeCachedCarbObjects(before date: Date) -> CarbStoreError? {
- dispatchPrecondition(condition: .onQueue(queue))
- var error: CarbStoreError?
- cacheStore.managedObjectContext.performAndWait {
- do {
- // Fetch all candidate objects for purge
- let request: NSFetchRequest<CachedCarbObject> = CachedCarbObject.fetchRequest()
- request.predicate = NSPredicate(format: "startDate < %@", date as NSDate)
- let objects = try self.cacheStore.managedObjectContext.fetch(request)
- // Objects can only be purged if all related objects can be purged
- let purgedObjects = try objects.filter { try self.areAllRelatedObjectsPurgable(to: $0, before: date) }
- guard !purgedObjects.isEmpty else {
- return
- }
- // Actually purge
- purgedObjects.forEach { self.cacheStore.managedObjectContext.delete($0) }
- if let saveError = CarbStoreError(error: self.cacheStore.save()) {
- error = saveError
- return
- }
- self.log.info("Purged %d CachedCarbObjects", purgedObjects.count)
- } catch let coreDataError {
- error = .coreDataError(coreDataError)
- }
- }
- if let error = error {
- self.log.error("Unable to purge CachedCarbObjects: %{public}@", String(describing: error))
- return error
- }
- return nil
- }
- public func purgeCachedCarbObjectsUnconditionally(before date: Date, completion: @escaping (CarbStoreError?) -> Void) {
- queue.async {
- if let error = self.purgeCachedCarbObjectsUnconditionally(before: date) {
- completion(error)
- return
- }
- self.handleUpdatedCarbData()
- completion(nil)
- }
- }
- private func purgeCachedCarbObjectsUnconditionally(before date: Date? = nil) -> CarbStoreError? {
- dispatchPrecondition(condition: .onQueue(queue))
- var error: CarbStoreError?
- cacheStore.managedObjectContext.performAndWait {
- do {
- let request: NSFetchRequest<CachedCarbObject> = CachedCarbObject.fetchRequest()
- if let date = date {
- request.predicate = NSPredicate(format: "startDate < %@", date as NSDate)
- }
- let count = try self.cacheStore.managedObjectContext.deleteObjects(matching: request)
- self.log.info("Purged all %d CachedCarbObjects", count)
- } catch let coreDataError {
- self.log.error("Unable to purge all CachedCarbObjects: %{public}@", String(describing: coreDataError))
- error = .coreDataError(coreDataError)
- }
- }
- return error
- }
- private func handleUpdatedCarbData() {
- dispatchPrecondition(condition: .onQueue(queue))
- purgeExpiredCachedCarbObjects()
- NotificationCenter.default.post(name: CarbStore.carbEntriesDidChange, object: self)
- delegate?.carbStoreHasUpdatedCarbData(self)
- }
- private func areAllRelatedObjectsPurgable(to object: CachedCarbObject, before date: Date) throws -> Bool {
- dispatchPrecondition(condition: .onQueue(queue))
- // If no sync identifier, then there are no related objects
- guard let syncIdentifier = object.syncIdentifier else {
- return true
- }
- // Count any that are NOT purgable
- let request: NSFetchRequest<CachedCarbObject> = CachedCarbObject.fetchRequest()
- request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [NSPredicate(format: "provenanceIdentifier == %@", object.provenanceIdentifier),
- NSPredicate(format: "syncIdentifier == %@", syncIdentifier),
- NSPredicate(format: "startDate >= %@", date as NSDate)])
- request.fetchLimit = 1
- return try cacheStore.managedObjectContext.count(for: request) == 0
- }
- }
- // MARK: - Math
- extension CarbStore {
- /// The longest expected absorption time interval for carbohydrates. Defaults to 8 hours.
- public var maximumAbsorptionTimeInterval: TimeInterval {
- return defaultAbsorptionTimes.slow * 2
- }
- /// Retrieves the single carbs on-board value occuring just prior or equal to the specified date
- ///
- /// This operation is performed asynchronously and the completion will be executed on an arbitrary background queue.
- ///
- /// - Parameters:
- /// - date: The date of the value to retrieve
- /// - effectVelocities: A timeline of glucose effect velocities, ordered by start date
- /// - completion: A closure called once the value has been retrieved
- /// - result: The carbs on-board value
- public func carbsOnBoard(at date: Date, effectVelocities: [GlucoseEffectVelocity]? = nil, completion: @escaping (_ result: CarbStoreResult<CarbValue>) -> Void) {
- getCarbsOnBoardValues(start: date.addingTimeInterval(-delta), end: date, effectVelocities: effectVelocities) { (result) in
- switch result {
- case .failure(let error):
- completion(.failure(error))
- case .success(let values):
- guard let value = values.closestPrior(to: date) else {
- // If we have no cob values in the store, and did not encounter an error, return 0
- completion(.success(CarbValue(startDate: date, quantity: HKQuantity(unit: .gram(), doubleValue: 0))))
- return
- }
- completion(.success(value))
- }
- }
- }
- /// Retrieves a timeline of unabsorbed carbohydrates
- ///
- /// This operation is performed asynchronously and the completion will be executed on an arbitrary background queue.
- ///
- /// - Parameters:
- /// - start: The earliest date of values to retrieve
- /// - end: The latest date of values to retrieve, if provided
- /// - effectVelocities: A timeline of glucose effect velocities, ordered by start date
- /// - completion: A closure called once the values have been retrieved
- /// - values: A timeline of carb values, in chronological order
- public func getCarbsOnBoardValues(start: Date, end: Date? = nil, effectVelocities: [GlucoseEffectVelocity]? = nil, completion: @escaping (_ result: CarbStoreResult<[CarbValue]>) -> Void) {
- // To know COB at the requested start date, we need to fetch samples that might still be absorbing
- let foodStart = start.addingTimeInterval(-maximumAbsorptionTimeInterval)
- getCarbEntries(start: foodStart, end: end) { (result) in
- switch result {
- case .failure(let error):
- completion(.failure(error))
- case .success(let entries):
- let carbsOnBoard = self.carbsOnBoard(from: entries, startingAt: start, endingAt: end, effectVelocities: effectVelocities)
- completion(.success(carbsOnBoard))
- }
- }
- }
- /// Computes a timeline of unabsorbed carbohydrates
- /// - Parameters:
- /// - start: The earliest date of values to retrieve
- /// - end: The latest date of values to retrieve, if provided
- /// - effectVelocities: A timeline of glucose effect velocities, ordered by start date
- /// - Returns: A timeline of unabsorbed carbohydrates
- public func carbsOnBoard<Sample: CarbEntry>(
- from samples: [Sample],
- startingAt start: Date,
- endingAt end: Date? = nil,
- effectVelocities: [GlucoseEffectVelocity]? = nil
- ) -> [CarbValue] {
- if let velocities = effectVelocities,
- let carbRatioSchedule = carbRatioScheduleApplyingOverrideHistory,
- let insulinSensitivitySchedule = insulinSensitivityScheduleApplyingOverrideHistory
- {
- return samples.map(
- to: velocities,
- carbRatio: carbRatioSchedule,
- insulinSensitivity: insulinSensitivitySchedule,
- absorptionTimeOverrun: absorptionTimeOverrun,
- defaultAbsorptionTime: defaultAbsorptionTimes.medium,
- delay: delay,
- initialAbsorptionTimeOverrun: settings.initialAbsorptionTimeOverrun,
- absorptionModel: settings.absorptionModel,
- adaptiveAbsorptionRateEnabled: settings.adaptiveAbsorptionRateEnabled,
- adaptiveRateStandbyIntervalFraction: settings.adaptiveRateStandbyIntervalFraction
- ).dynamicCarbsOnBoard(
- from: start,
- to: end,
- defaultAbsorptionTime: defaultAbsorptionTimes.medium,
- absorptionModel: settings.absorptionModel,
- delay: delay,
- delta: delta
- )
- } else {
- return samples.carbsOnBoard(
- from: start,
- to: end,
- defaultAbsorptionTime: defaultAbsorptionTimes.medium,
- absorptionModel: settings.absorptionModel,
- delay: delay,
- delta: delta
- )
- }
- }
- /// Computes the single carbs on-board value occuring just prior or equal to the specified date
- /// - Parameters:
- /// - date: The date of the value to retrieve
- /// - effectVelocities: A timeline of glucose effect velocities, ordered by start date
- /// - Returns: The carbs on-board value
- public func carbsOnBoard<Sample: CarbEntry>(
- from samples: [Sample],
- at date: Date,
- effectVelocities: [GlucoseEffectVelocity]? = nil
- ) throws -> CarbValue {
- let values = carbsOnBoard(from: samples, startingAt: date.addingTimeInterval(-delta), endingAt: date, effectVelocities: effectVelocities)
- guard let value = values.closestPrior(to: date) else {
- throw CarbStoreError.noData
- }
- return value
- }
- /// Retrieves a timeline of effect on blood glucose from carbohydrates
- ///
- /// This operation is performed asynchronously and the completion will be executed on an arbitrary background queue.
- ///
- /// - Parameters:
- /// - start: The earliest date of effects to retrieve
- /// - end: The latest date of effects to retrieve, if provided
- /// - effectVelocities: A timeline of glucose effect velocities, ordered by start date
- /// - completion: A closure called once the effects have been retrieved
- /// - result: An array of effects, in chronological order
- public func getGlucoseEffects(start: Date, end: Date? = nil, effectVelocities: [GlucoseEffectVelocity]? = nil, completion: @escaping(_ result: CarbStoreResult<(entries: [StoredCarbEntry], effects: [GlucoseEffect])>) -> Void) {
- queue.async {
- guard self.carbRatioSchedule != nil, self.insulinSensitivitySchedule != nil else {
- completion(.failure(.notConfigured))
- return
- }
- // To know glucose effects at the requested start date, we need to fetch samples that might still be absorbing
- let foodStart = start.addingTimeInterval(-self.maximumAbsorptionTimeInterval)
-
- self.getCarbEntries(start: foodStart, end: end) { (result) in
- switch result {
- case .failure(let error):
- completion(.failure(error))
- case .success(let entries):
- do {
- let effects = try self.glucoseEffects(of: entries, startingAt: start, endingAt: end, effectVelocities: effectVelocities)
- completion(.success((entries: entries, effects: effects)))
- } catch let error as CarbStoreError {
- completion(.failure(error))
- } catch {
- fatalError()
- }
- }
- }
- }
- }
- /// Computes a timeline of effects on blood glucose from carbohydrates
- /// - Parameters:
- /// - start: The earliest date of effects to retrieve
- /// - end: The latest date of effects to retrieve, if provided
- /// - effectVelocities: A timeline of glucose effect velocities, ordered by start date
- public func glucoseEffects<Sample: CarbEntry>(
- of samples: [Sample],
- startingAt start: Date,
- endingAt end: Date? = nil,
- effectVelocities: [GlucoseEffectVelocity]? = nil
- ) throws -> [GlucoseEffect] {
- guard
- let carbRatioSchedule = carbRatioScheduleApplyingOverrideHistory,
- let insulinSensitivitySchedule = insulinSensitivityScheduleApplyingOverrideHistory
- else {
- throw CarbStoreError.notConfigured
- }
- if let effectVelocities = effectVelocities {
- return samples.map(
- to: effectVelocities,
- carbRatio: carbRatioSchedule,
- insulinSensitivity: insulinSensitivitySchedule,
- absorptionTimeOverrun: absorptionTimeOverrun,
- defaultAbsorptionTime: defaultAbsorptionTimes.medium,
- delay: delay,
- initialAbsorptionTimeOverrun: settings.initialAbsorptionTimeOverrun,
- absorptionModel: settings.absorptionModel,
- adaptiveAbsorptionRateEnabled: settings.adaptiveAbsorptionRateEnabled,
- adaptiveRateStandbyIntervalFraction: settings.adaptiveRateStandbyIntervalFraction
- ).dynamicGlucoseEffects(
- from: start,
- to: end,
- carbRatios: carbRatioSchedule,
- insulinSensitivities: insulinSensitivitySchedule,
- defaultAbsorptionTime: defaultAbsorptionTimes.medium,
- absorptionModel: settings.absorptionModel,
- delay: delay,
- delta: delta
- )
- } else {
- return samples.glucoseEffects(
- from: start,
- to: end,
- carbRatios: carbRatioSchedule,
- insulinSensitivities: insulinSensitivitySchedule,
- defaultAbsorptionTime: defaultAbsorptionTimes.medium,
- absorptionModel: settings.absorptionModel,
- delay: delay,
- delta: delta
- )
- }
- }
- /// Retrieves the total number of recorded carbohydrates for the specified period.
- ///
- /// This operation is performed asynchronously and the completion will be executed on an arbitrary background queue.
- ///
- /// - Parameters:
- /// - start: The earliest date of samples to include.
- /// - completion: A closure called once the value has been retrieved.
- /// - result: The total carbs recorded and the date of the first sample
- public func getTotalCarbs(since start: Date, completion: @escaping (_ result: CarbStoreResult<CarbValue>) -> Void) {
- getCarbEntries(start: start) { (result) in
- switch result {
- case .success(let samples):
- let total = samples.totalCarbs ?? CarbValue(
- startDate: start,
- quantity: HKQuantity(unit: .gram(), doubleValue: 0)
- )
- completion(.success(total))
- case .failure(let error):
- completion(.failure(error))
- }
- }
- }
- }
- // MARK: - Remote Data Service Query
- extension CarbStore {
- public struct QueryAnchor: Equatable, RawRepresentable {
- public typealias RawValue = [String: Any]
- internal var anchorKey: Int64
- public init() {
- self.anchorKey = 0
- }
- public init?(rawValue: RawValue) {
- guard let anchorKey = (rawValue["anchorKey"] ?? rawValue["storedModificationCounter"]) as? Int64 else { // Backwards compatibility with storedModificationCounter
- return nil
- }
- self.anchorKey = anchorKey
- }
- public var rawValue: RawValue {
- var rawValue: RawValue = [:]
- rawValue["anchorKey"] = anchorKey
- return rawValue
- }
- }
- public enum CarbQueryResult {
- case success(QueryAnchor, [SyncCarbObject], [SyncCarbObject], [SyncCarbObject])
- case failure(Error)
- }
- public func executeCarbQuery(fromQueryAnchor queryAnchor: QueryAnchor?, limit: Int, completion: @escaping (CarbQueryResult) -> Void) {
- queue.async {
- var queryAnchor = queryAnchor ?? QueryAnchor()
- var queryCreatedResult = [SyncCarbObject]()
- var queryUpdatedResult = [SyncCarbObject]()
- var queryDeletedResult = [SyncCarbObject]()
- var queryError: Error?
- guard limit > 0 else {
- completion(.success(queryAnchor, [], [], []))
- return
- }
- self.cacheStore.managedObjectContext.performAndWait {
- let storedRequest: NSFetchRequest<CachedCarbObject> = CachedCarbObject.fetchRequest()
- storedRequest.predicate = NSPredicate(format: "anchorKey > %d", queryAnchor.anchorKey)
- storedRequest.sortDescriptors = [NSSortDescriptor(key: "anchorKey", ascending: true)]
- storedRequest.fetchLimit = limit
- do {
- let stored = try self.cacheStore.managedObjectContext.fetch(storedRequest)
- if let anchorKey = stored.max(by: { $0.anchorKey < $1.anchorKey })?.anchorKey {
- queryAnchor.anchorKey = anchorKey
- }
- stored.map({ SyncCarbObject(managedObject: $0) }).forEach {
- switch $0.operation {
- case .create:
- queryCreatedResult.append($0)
- case .update:
- queryUpdatedResult.append($0)
- case .delete:
- queryDeletedResult.append($0)
- }
- }
- } catch let coreDataError {
- queryError = coreDataError
- return
- }
- }
- if let queryError = queryError {
- completion(.failure(queryError))
- return
- }
- completion(.success(queryAnchor, queryCreatedResult, queryUpdatedResult, queryDeletedResult))
- }
- }
- }
- // MARK: - Critical Event Log Export
- extension CarbStore: CriticalEventLog {
- private var exportProgressUnitCountPerObject: Int64 { 1 }
- private var exportFetchLimit: Int { Int(criticalEventLogExportProgressUnitCountPerFetch / exportProgressUnitCountPerObject) }
- public var exportName: String { "Carbs.json" }
- public func exportProgressTotalUnitCount(startDate: Date, endDate: Date? = nil) -> Result<Int64, Error> {
- var result: Result<Int64, Error>?
- self.cacheStore.managedObjectContext.performAndWait {
- do {
- let request: NSFetchRequest<CachedCarbObject> = CachedCarbObject.fetchRequest()
- request.predicate = self.exportDatePredicate(startDate: startDate, endDate: endDate)
- let objectCount = try self.cacheStore.managedObjectContext.count(for: request)
- result = .success(Int64(objectCount) * exportProgressUnitCountPerObject)
- } catch let error {
- result = .failure(error)
- }
- }
- return result!
- }
- public func export(startDate: Date, endDate: Date, to stream: OutputStream, progress: Progress) -> Error? {
- let encoder = JSONStreamEncoder(stream: stream)
- var anchorKey: Int64 = 0
- var fetching = true
- var error: Error?
- while fetching && error == nil {
- self.cacheStore.managedObjectContext.performAndWait {
- do {
- guard !progress.isCancelled else {
- throw CriticalEventLogError.cancelled
- }
- let request: NSFetchRequest<CachedCarbObject> = CachedCarbObject.fetchRequest()
- request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [NSPredicate(format: "anchorKey > %d", anchorKey),
- self.exportDatePredicate(startDate: startDate, endDate: endDate)])
- request.sortDescriptors = [NSSortDescriptor(key: "anchorKey", ascending: true)]
- request.fetchLimit = self.exportFetchLimit
- let objects = try self.cacheStore.managedObjectContext.fetch(request)
- if objects.isEmpty {
- fetching = false
- return
- }
- try encoder.encode(objects)
- anchorKey = objects.last!.anchorKey
- progress.completedUnitCount += Int64(objects.count) * exportProgressUnitCountPerObject
- } catch let fetchError {
- error = fetchError
- }
- }
- }
- if let closeError = encoder.close(), error == nil {
- error = closeError
- }
- return error
- }
- private func exportDatePredicate(startDate: Date, endDate: Date? = nil) -> NSPredicate {
- var addedDatePredicate = NSPredicate(format: "addedDate >= %@", startDate as NSDate)
- var supercededDatePredicate = NSPredicate(format: "supercededDate >= %@", startDate as NSDate)
- if let endDate = endDate {
- addedDatePredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [addedDatePredicate, NSPredicate(format: "addedDate < %@", endDate as NSDate)])
- supercededDatePredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [supercededDatePredicate, NSPredicate(format: "supercededDate < %@", endDate as NSDate)])
- }
- return NSCompoundPredicate(orPredicateWithSubpredicates: [addedDatePredicate, supercededDatePredicate])
- }
- }
- // MARK: - Core Data (Bulk) - TEST ONLY
- extension CarbStore {
- public func addNewCarbEntries(entries: [NewCarbEntry], completion: @escaping (Error?) -> Void) {
- guard !entries.isEmpty else {
- completion(nil)
- return
- }
- queue.async {
- var error: Error?
- self.cacheStore.managedObjectContext.performAndWait {
- do {
- for entry in entries {
- let syncIdentifier = try self.cacheStore.managedObjectContext.generateUniqueSyncIdentifier()
- let object = CachedCarbObject(context: self.cacheStore.managedObjectContext)
- object.create(from: entry,
- provenanceIdentifier: self.provenanceIdentifier,
- syncIdentifier: syncIdentifier,
- on: entry.date)
- }
- error = self.cacheStore.save()
- } catch let coreDataError {
- error = coreDataError
- }
- }
- guard error == nil else {
- completion(error)
- return
- }
- self.log.info("Added %d CachedCarbObjects", entries.count)
- self.delegate?.carbStoreHasUpdatedCarbData(self)
- completion(nil)
- }
- }
- }
- // MARK: - Issue Report
- extension CarbStore {
- /// Generates a diagnostic report about the current state
- ///
- /// This operation is performed asynchronously and the completion will be executed on an arbitrary background queue.
- ///
- /// - parameter completionHandler: A closure called once the report has been generated. The closure takes a single argument of the report string.
- public func generateDiagnosticReport(_ completionHandler: @escaping (_ report: String) -> Void) {
- queue.async {
- var carbAbsorptionModel: String
- switch self.carbAbsorptionModel {
- case .linear:
- carbAbsorptionModel = "Linear"
- case .nonlinear:
- carbAbsorptionModel = "Nonlinear"
- case .adaptiveRateNonlinear:
- carbAbsorptionModel = "Nonlinear with Adaptive Rate for Remaining Carbs"
- }
- var report: [String] = [
- "## CarbStore",
- "",
- "* carbRatioSchedule: \(self.carbRatioSchedule?.debugDescription ?? "")",
- "* carbRatioScheduleApplyingOverrideHistory: \(self.carbRatioScheduleApplyingOverrideHistory?.debugDescription ?? "nil")",
- "* cacheLength: \(self.cacheLength)",
- "* defaultAbsorptionTimes: \(self.defaultAbsorptionTimes)",
- "* observationInterval: \(self.observationInterval)",
- "* insulinSensitivitySchedule: \(self.insulinSensitivitySchedule?.debugDescription ?? "")",
- "* insulinSensitivityScheduleApplyingOverrideHistory: \(self.insulinSensitivityScheduleApplyingOverrideHistory?.debugDescription ?? "nil")",
- "* overrideHistory: \(self.overrideHistory.map(String.init(describing:)) ?? "nil")",
- "* carbSensitivitySchedule: \(self.carbSensitivitySchedule?.debugDescription ?? "nil")",
- "* delay: \(self.delay)",
- "* delta: \(self.delta)",
- "* absorptionTimeOverrun: \(self.absorptionTimeOverrun)",
- "* carbAbsorptionModel: \(carbAbsorptionModel)",
- "* Carb absorption model settings: \(self.settings)",
- super.debugDescription,
- "",
- "cachedCarbEntries:"
- ]
- switch self.getCarbEntries() {
- case .failure(let error):
- report.append("Error: \(error)")
- case .success(let entries):
- report.append("[")
- report.append("\tStoredCarbEntry(uuid, provenanceIdentifier, syncIdentifier, syncVersion, startDate, quantity, foodType, absorptionTime, createdByCurrentApp, userCreatedDate, userUpdatedDate)")
- report.append(entries.map({ (entry) -> String in
- return [
- "\t",
- entry.uuid?.uuidString ?? "",
- entry.provenanceIdentifier,
- entry.syncIdentifier ?? "",
- entry.syncVersion != nil ? String(describing: entry.syncVersion) : "",
- String(describing: entry.startDate),
- String(describing: entry.quantity),
- entry.foodType ?? "",
- String(describing: entry.absorptionTime ?? self.defaultAbsorptionTimes.medium),
- String(describing: entry.createdByCurrentApp),
- entry.userCreatedDate != nil ? String(describing: entry.userCreatedDate) : "",
- entry.userUpdatedDate != nil ? String(describing: entry.userUpdatedDate) : "",
- ].joined(separator: ", ")
- }).joined(separator: "\n"))
- report.append("]")
- report.append("")
- }
- completionHandler(report.joined(separator: "\n"))
- }
- }
- }
- // MARK: - NSManagedObjectContext
- fileprivate extension NSManagedObjectContext {
- func generateUniqueSyncIdentifier() throws -> String {
- while true {
- let syncIdentifier = UUID().uuidString
- let request: NSFetchRequest<CachedCarbObject> = CachedCarbObject.fetchRequest()
- request.predicate = NSPredicate(format: "syncIdentifier == %@", syncIdentifier)
- request.fetchLimit = 1
- if try count(for: request) == 0 {
- return syncIdentifier
- }
- }
- }
- func cachedCarbObjectFromStoredCarbEntry(_ entry: StoredCarbEntry) throws -> CachedCarbObject? {
- guard entry.createdByCurrentApp, let syncIdentifier = entry.syncIdentifier, let syncVersion = entry.syncVersion else {
- return nil
- }
- let request: NSFetchRequest<CachedCarbObject> = CachedCarbObject.fetchRequest()
- request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
- NSPredicate(format: "createdByCurrentApp == YES"),
- NSPredicate(format: "syncIdentifier == %@", syncIdentifier),
- NSPredicate(format: "syncVersion == %d", syncVersion),
- NSPredicate(format: "operation != %d", Operation.delete.rawValue),
- NSPredicate(format: "supercededDate == NIL")
- ])
- request.fetchLimit = 1
- if let object = try fetch(request).first {
- return object
- }
- return try cachedCarbObjectFromStoredCarbEntryDEPRECATED(entry)
- }
- // DEPRECATED: Fallback for pre-syncIdentifier entries, just has UUID from HealthKit
- func cachedCarbObjectFromStoredCarbEntryDEPRECATED(_ entry: StoredCarbEntry) throws -> CachedCarbObject? {
- guard entry.createdByCurrentApp, let uuid = entry.uuid else {
- return nil
- }
- let request: NSFetchRequest<CachedCarbObject> = CachedCarbObject.fetchRequest()
- request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
- NSPredicate(format: "createdByCurrentApp == YES"),
- NSPredicate(format: "uuid == %@", uuid as NSUUID),
- NSPredicate(format: "operation != %d", Operation.delete.rawValue),
- NSPredicate(format: "supercededDate == NIL")
- ])
- request.fetchLimit = 1
- if let object = try fetch(request).first {
- return object
- }
- return nil
- }
- }
|