From 59c5a2cd7bd17a4417c39869717a94894b2b82d1 Mon Sep 17 00:00:00 2001 From: Theo Spears Date: Mon, 8 Sep 2025 22:47:14 -0700 Subject: [PATCH 1/6] Set precision for HealthKit metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Exercise Minutes and Workout Minutes: 1 decimal place for minute units - Water consumption: 1 decimal place for fluid ounce units - Added shared precision handling to HealthKitMetric protocol 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- BeeKit/HeathKit/HealthKitConfig.swift | 4 ++-- BeeKit/HeathKit/HealthKitMetric.swift | 13 +++++++++++++ BeeKit/HeathKit/QuantityHealthKitMetric.swift | 5 +---- BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift | 6 ++++-- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/BeeKit/HeathKit/HealthKitConfig.swift b/BeeKit/HeathKit/HealthKitConfig.swift index 73ad41fa..d4cce37b 100644 --- a/BeeKit/HeathKit/HealthKitConfig.swift +++ b/BeeKit/HeathKit/HealthKitConfig.swift @@ -15,7 +15,7 @@ public enum HealthKitConfig { // Activity QuantityHealthKitMetric(humanText: "Active energy", databaseString: "activeEnergy", category: .Activity, hkQuantityTypeIdentifier: .activeEnergyBurned, precision: [HKUnit.largeCalorie(): 0]), QuantityHealthKitMetric(humanText: "Cycling distance", databaseString: "cyclingDistance", category: .Activity, hkQuantityTypeIdentifier: .distanceCycling), - QuantityHealthKitMetric(humanText: "Exercise time", databaseString: "exerciseTime", category: .Activity, hkQuantityTypeIdentifier: .appleExerciseTime), + QuantityHealthKitMetric(humanText: "Exercise time", databaseString: "exerciseTime", category: .Activity, hkQuantityTypeIdentifier: .appleExerciseTime, precision: [HKUnit.minute(): 1]), QuantityHealthKitMetric(humanText: "Nike Fuel", databaseString: "nikeFuel", category: .Activity, hkQuantityTypeIdentifier: .nikeFuel), QuantityHealthKitMetric(humanText: "Resting energy", databaseString: "basalEnergy", category: .Activity, hkQuantityTypeIdentifier: .basalEnergyBurned, precision: [HKUnit.largeCalorie(): 0]), StandHoursHealthKitMetric(humanText: "Stand hours", databaseString: "standHour", category: .Activity), @@ -49,7 +49,7 @@ public enum HealthKitConfig { QuantityHealthKitMetric(humanText: "Vitamin D", databaseString: "dietaryVitaminD", category: .Nutrition, hkQuantityTypeIdentifier: .dietaryVitaminD), QuantityHealthKitMetric(humanText: "Vitamin E", databaseString: "dietaryVitaminE", category: .Nutrition, hkQuantityTypeIdentifier: .dietaryVitaminE), QuantityHealthKitMetric(humanText: "Vitamin K", databaseString: "dietaryVitaminK", category: .Nutrition, hkQuantityTypeIdentifier: .dietaryVitaminK), - QuantityHealthKitMetric(humanText: "Water", databaseString: "water", category: .Nutrition, hkQuantityTypeIdentifier: .dietaryWater), + QuantityHealthKitMetric(humanText: "Water", databaseString: "water", category: .Nutrition, hkQuantityTypeIdentifier: .dietaryWater, precision: [HKUnit.fluidOunceUS(): 1]), // Sleep TimeInBedHealthKitMetric(humanText: "Time in bed", databaseString: "timeInBed", category: .Sleep), diff --git a/BeeKit/HeathKit/HealthKitMetric.swift b/BeeKit/HeathKit/HealthKitMetric.swift index d952ac75..42ff23ea 100644 --- a/BeeKit/HeathKit/HealthKitMetric.swift +++ b/BeeKit/HeathKit/HealthKitMetric.swift @@ -20,6 +20,7 @@ public protocol HealthKitMetric { var humanText : String { get } var databaseString : String { get } var category : HealthKitCategory { get } + var precision : [HKUnit: Int] { get } /// The permission required for this connection to read data from HealthKit func permissionType() -> HKObjectType @@ -41,3 +42,15 @@ public protocol HealthKitMetric { /// The units this metric returns its datapoint values in func units(healthStore : HKHealthStore) async throws -> HKUnit } + +extension HealthKitMetric { + var precision: [HKUnit: Int] { return [:] } + + func applyPrecision(value: Double, unit: HKUnit) -> Double { + if let unitPrecision = precision[unit] { + let roundingFactor = pow(10.0, Double(unitPrecision)) + return round(value * roundingFactor) / roundingFactor + } + return value + } +} diff --git a/BeeKit/HeathKit/QuantityHealthKitMetric.swift b/BeeKit/HeathKit/QuantityHealthKitMetric.swift index 6656a382..3ee8d07c 100644 --- a/BeeKit/HeathKit/QuantityHealthKitMetric.swift +++ b/BeeKit/HeathKit/QuantityHealthKitMetric.swift @@ -116,10 +116,7 @@ class QuantityHealthKitMetric : HealthKitMetric { continue } - if let unitPrecision = precision[unit] { - let roundingFactor = pow(10.0, Double(unitPrecision)) - datapointValue = round(datapointValue * roundingFactor) / roundingFactor - } + datapointValue = applyPrecision(value: datapointValue, unit: unit) let id = "apple-health-" + daystamp.description results.append(NewDataPoint(requestid: id, daystamp: daystamp, value: NSNumber(value: datapointValue), comment: "Auto-entered via Apple Health")) diff --git a/BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift b/BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift index ecc4bc7d..86a149ac 100644 --- a/BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift +++ b/BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift @@ -5,6 +5,8 @@ import OSLog public class WorkoutMinutesHealthKitMetric : CategoryHealthKitMetric { private let logger = Logger(subsystem: "com.beeminder.beeminder", category: "WorkoutMinutesHealthKitMetric") let minuteInSeconds = 60.0 + + override var precision: [HKUnit: Int] { return [HKUnit.minute(): 1] } init(humanText: String, databaseString: String, category: HealthKitCategory) { @@ -16,7 +18,7 @@ public class WorkoutMinutesHealthKitMetric : CategoryHealthKitMetric { let samplesOnDay = samples.filter{sample in sample.startDate >= startOfDate} let workouts = samplesOnDay.compactMap({sample in sample as? HKWorkout}) let workoutMinutes = workouts.map{sample in sample.duration / minuteInSeconds}.reduce(0, +) - return Double(workoutMinutes) + return applyPrecision(value: Double(workoutMinutes), unit: HKUnit.minute()) } public override func recentDataPoints(days: Int, deadline: Int, healthStore: HKHealthStore, autodataConfig: [String: Any]) async throws -> [BeeDataPoint] { @@ -40,7 +42,7 @@ public class WorkoutMinutesHealthKitMetric : CategoryHealthKitMetric { let workouts = samples.compactMap { $0 as? HKWorkout } for workout in workouts { - let workoutMinutes = workout.duration / minuteInSeconds + let workoutMinutes = applyPrecision(value: workout.duration / minuteInSeconds, unit: HKUnit.minute()) let workoutDescription = formatWorkoutDescription(workout: workout) let id = "apple-health-workout-\(workout.uuid.uuidString)" From e99da662f0f8a3c41ad5cd2a710bfda48b2af634 Mon Sep 17 00:00:00 2001 From: Theo Spears Date: Mon, 8 Sep 2025 22:52:25 -0700 Subject: [PATCH 2/6] Fix public visibility for precision property MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- BeeKit/HeathKit/HealthKitMetric.swift | 4 ++-- BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BeeKit/HeathKit/HealthKitMetric.swift b/BeeKit/HeathKit/HealthKitMetric.swift index 42ff23ea..f5db1d29 100644 --- a/BeeKit/HeathKit/HealthKitMetric.swift +++ b/BeeKit/HeathKit/HealthKitMetric.swift @@ -44,9 +44,9 @@ public protocol HealthKitMetric { } extension HealthKitMetric { - var precision: [HKUnit: Int] { return [:] } + public var precision: [HKUnit: Int] { return [:] } - func applyPrecision(value: Double, unit: HKUnit) -> Double { + public func applyPrecision(value: Double, unit: HKUnit) -> Double { if let unitPrecision = precision[unit] { let roundingFactor = pow(10.0, Double(unitPrecision)) return round(value * roundingFactor) / roundingFactor diff --git a/BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift b/BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift index 86a149ac..653f1578 100644 --- a/BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift +++ b/BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift @@ -6,7 +6,7 @@ public class WorkoutMinutesHealthKitMetric : CategoryHealthKitMetric { private let logger = Logger(subsystem: "com.beeminder.beeminder", category: "WorkoutMinutesHealthKitMetric") let minuteInSeconds = 60.0 - override var precision: [HKUnit: Int] { return [HKUnit.minute(): 1] } + public override var precision: [HKUnit: Int] { return [HKUnit.minute(): 1] } init(humanText: String, databaseString: String, category: HealthKitCategory) { From d95bcceb01219153743a6e62e6e08e46d9d5b295 Mon Sep 17 00:00:00 2001 From: Theo Spears Date: Mon, 8 Sep 2025 22:53:39 -0700 Subject: [PATCH 3/6] Remove override keyword from precision property MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift b/BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift index 653f1578..9f318eab 100644 --- a/BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift +++ b/BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift @@ -6,7 +6,7 @@ public class WorkoutMinutesHealthKitMetric : CategoryHealthKitMetric { private let logger = Logger(subsystem: "com.beeminder.beeminder", category: "WorkoutMinutesHealthKitMetric") let minuteInSeconds = 60.0 - public override var precision: [HKUnit: Int] { return [HKUnit.minute(): 1] } + public var precision: [HKUnit: Int] { return [HKUnit.minute(): 1] } init(humanText: String, databaseString: String, category: HealthKitCategory) { From b9d181fe6aa06f3eac4c0593d8a2ece3e545f550 Mon Sep 17 00:00:00 2001 From: Theo Spears Date: Mon, 8 Sep 2025 23:02:40 -0700 Subject: [PATCH 4/6] Remove default precision implementation from protocol extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- BeeKit/HeathKit/HealthKitMetric.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/BeeKit/HeathKit/HealthKitMetric.swift b/BeeKit/HeathKit/HealthKitMetric.swift index f5db1d29..969071b8 100644 --- a/BeeKit/HeathKit/HealthKitMetric.swift +++ b/BeeKit/HeathKit/HealthKitMetric.swift @@ -44,8 +44,6 @@ public protocol HealthKitMetric { } extension HealthKitMetric { - public var precision: [HKUnit: Int] { return [:] } - public func applyPrecision(value: Double, unit: HKUnit) -> Double { if let unitPrecision = precision[unit] { let roundingFactor = pow(10.0, Double(unitPrecision)) From f49a5001a40760cde8e6470e783fe7075c3e5b1d Mon Sep 17 00:00:00 2001 From: Theo Spears Date: Mon, 8 Sep 2025 23:05:02 -0700 Subject: [PATCH 5/6] fix method resolution --- BeeKit/HeathKit/CategoryHealthKitMetric.swift | 2 ++ BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/BeeKit/HeathKit/CategoryHealthKitMetric.swift b/BeeKit/HeathKit/CategoryHealthKitMetric.swift index 4d5aa462..2d555e1b 100644 --- a/BeeKit/HeathKit/CategoryHealthKitMetric.swift +++ b/BeeKit/HeathKit/CategoryHealthKitMetric.swift @@ -16,6 +16,8 @@ public class CategoryHealthKitMetric : HealthKitMetric { public let category : HealthKitCategory let hkSampleType : HKSampleType + public var precision: [HKUnit : Int] { [:] } + internal init(humanText: String, databaseString: String, category : HealthKitCategory, hkSampleType: HKSampleType) { self.humanText = humanText self.databaseString = databaseString diff --git a/BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift b/BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift index 9f318eab..653f1578 100644 --- a/BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift +++ b/BeeKit/HeathKit/WorkoutMinutesHealthKitMetric.swift @@ -6,7 +6,7 @@ public class WorkoutMinutesHealthKitMetric : CategoryHealthKitMetric { private let logger = Logger(subsystem: "com.beeminder.beeminder", category: "WorkoutMinutesHealthKitMetric") let minuteInSeconds = 60.0 - public var precision: [HKUnit: Int] { return [HKUnit.minute(): 1] } + public override var precision: [HKUnit: Int] { return [HKUnit.minute(): 1] } init(humanText: String, databaseString: String, category: HealthKitCategory) { From 823eeae31931b3b3a0324ed103f0a6e7e4f7f0a7 Mon Sep 17 00:00:00 2001 From: Theo Spears Date: Mon, 8 Sep 2025 23:06:11 -0700 Subject: [PATCH 6/6] applyPrecision does not need to be pyblic --- BeeKit/HeathKit/HealthKitMetric.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BeeKit/HeathKit/HealthKitMetric.swift b/BeeKit/HeathKit/HealthKitMetric.swift index 969071b8..b0ae5db3 100644 --- a/BeeKit/HeathKit/HealthKitMetric.swift +++ b/BeeKit/HeathKit/HealthKitMetric.swift @@ -44,7 +44,7 @@ public protocol HealthKitMetric { } extension HealthKitMetric { - public func applyPrecision(value: Double, unit: HKUnit) -> Double { + func applyPrecision(value: Double, unit: HKUnit) -> Double { if let unitPrecision = precision[unit] { let roundingFactor = pow(10.0, Double(unitPrecision)) return round(value * roundingFactor) / roundingFactor