Extract metadata

This commit is contained in:
Christoph Hagen
2024-01-21 14:32:00 +01:00
parent 8ace8e9319
commit 0088e5df2e
8 changed files with 221 additions and 4 deletions

View File

@@ -0,0 +1,57 @@
import Foundation
import SQLite
struct DBMetadata {
private static let table = Table("metadata_values")
private static let rowKeyId = Expression<Int?>("key_id")
private static let rowObjectId = Expression<Int?>("object_id")
private static let rowValueType = Expression<Int>("value_type")
private static let rowStringValue = Expression<String?>("string_value")
private static let rowNumericalValue = Expression<Double?>("numerical_value")
private static let rowDateValue = Expression<Double?>("date_value")
private static let rowDataValue = Expression<Data?>("data_value")
static func readAll(in database: Connection) throws -> [Self] {
try database.prepare(table).map(Self.init)
}
static func metadata(for workoutId: Int, in database: Connection) throws -> [Self] {
try database.prepare(table.filter(rowObjectId == workoutId)).map(Self.init)
}
let keyId: Int?
let objectId: Int?
let valueType: Int
let string: String?
let number: Double?
let date: Double?
let data: Data?
}
extension DBMetadata {
init(row: Row) {
self.keyId = row[DBMetadata.rowKeyId]
self.objectId = row[DBMetadata.rowObjectId]
self.valueType = row[DBMetadata.rowValueType]
self.string = row[DBMetadata.rowStringValue]
self.number = row[DBMetadata.rowNumericalValue]
self.date = row[DBMetadata.rowDateValue]
self.data = row[DBMetadata.rowDataValue]
}
}

View File

@@ -0,0 +1,21 @@
import Foundation
import SQLite
struct DBMetadataKey {
private static let table = Table("metadata_keys")
private static let rowId = Expression<Int>("ROWID")
private static let rowKey = Expression<String>("key")
static func key(for keyId: Int, in database: Connection) throws -> String {
try database.prepare(table.filter(rowId == keyId).limit(1)).map { $0[rowKey] }.first!
}
static func readAll(in database: Connection) throws -> [ Int: String] {
try database.prepare(table).reduce(into: [:]) { dict, row in
dict[row[rowId]] = row[rowKey]
}
}
}

View File

@@ -21,10 +21,15 @@ final class HealthDatabase: ObservableObject {
func readAllWorkouts() {
do {
let dbWorkouts = try DBWorkout.readAll(in: database)
let metadataKeys = try DBMetadataKey.readAll(in: database)
let workouts = try dbWorkouts.map { entry in
let events = try DBWorkoutEvent.events(for: entry.dataId, in: database)
let activities = try DBWorkoutActivity.activities(for: entry.dataId, in: database)
return Workout(entry: entry, events: events, activities: activities)
let metadata: [String : MetadataValue] = try DBMetadata.metadata(for: entry.dataId, in: database).reduce(into: [:]) { dict, item in
let key = metadataKeys[item.keyId!]!
dict[key] = MetadataValue(entry: item)
}
return Workout(entry: entry, events: events, activities: activities, metadata: metadata)
}
DispatchQueue.main.async {

View File

@@ -0,0 +1,65 @@
import Foundation
enum MetadataValue {
case string(value: String)
case number(value: Double)
case date(value: Date)
case numerical(value: Double, unit: String)
case data(value: Data)
enum ValueType: Int {
/// Uses only the `string_value` column
case string = 0
/// Uses only the `numerical_value` column
case number = 1
/// Uses only the `date_value` column
case date = 2
/// Uses the `string_value` column for the unit, and the `numerical_value` column for the number
case numerical = 3
/// Uses only the `data_value` column
case data = 4
}
}
extension MetadataValue {
init(entry: DBMetadata) {
let valueType = ValueType(rawValue: entry.valueType)!
switch valueType {
case .string:
self = .string(value: entry.string!)
case .number:
self = .number(value: entry.number!)
case .date:
self = .date(value: .init(timeIntervalSinceReferenceDate: entry.date!))
case .numerical:
self = .numerical(value: entry.number!, unit: entry.string!)
case .data:
self = .data(value: entry.data!)
}
}
}
extension MetadataValue: CustomStringConvertible {
var description: String {
switch self {
case .string(let value):
return value
case .number(let value):
return "\(value)"
case .date(let value):
return value.timeAndDateText
case .numerical(let value, let unit):
return String(format: "%.3f %s", value, unit)
case .data(let value):
return value.description
}
}
}

View File

@@ -1,5 +1,5 @@
import Foundation
import Collections
private let df: DateFormatter = {
let df = DateFormatter()
@@ -28,6 +28,8 @@ struct Workout {
let activities: [WorkoutActivity]
let metadata: OrderedDictionary<String, MetadataValue>
var firstActivityDate: Date? {
activities.map { $0.startDate }.min()
}
@@ -51,7 +53,7 @@ struct Workout {
activities.first?.activityType.description ?? "Unknown activity"
}
init(id: Int, totalDistance: Double? = nil, goalType: Int? = nil, goal: Double? = nil, condenserVersion: Int? = nil, condenserDate: Date? = nil, events: [WorkoutEvent] = [], activities: [WorkoutActivity] = []) {
init(id: Int, totalDistance: Double? = nil, goalType: Int? = nil, goal: Double? = nil, condenserVersion: Int? = nil, condenserDate: Date? = nil, events: [WorkoutEvent] = [], activities: [WorkoutActivity] = [], metadata: [String : MetadataValue] = [:]) {
self.id = id
self.totalDistance = totalDistance
self.goalType = goalType
@@ -60,12 +62,13 @@ struct Workout {
self.condenserDate = condenserDate
self.events = events
self.activities = activities
self.metadata = .init(uniqueKeys: metadata.keys, values: metadata.values)
}
}
extension Workout {
init(entry: DBWorkout, events: [DBWorkoutEvent], activities: [DBWorkoutActivity]) {
init(entry: DBWorkout, events: [DBWorkoutEvent], activities: [DBWorkoutActivity], metadata: [String : MetadataValue]) {
self.id = entry.dataId
self.totalDistance = entry.totalDistance
self.goalType = entry.goalType
@@ -74,6 +77,7 @@ extension Workout {
self.condenserDate = entry.condenserDate.map { Date(timeIntervalSinceReferenceDate: $0) }
self.events = events.map(WorkoutEvent.init)
self.activities = activities.map(WorkoutActivity.init)
self.metadata = .init(uniqueKeys: metadata.keys, values: metadata.values)
}
}

View File

@@ -1,4 +1,5 @@
import SwiftUI
import Collections
struct WorkoutDetailView: View {
@@ -37,6 +38,11 @@ struct WorkoutDetailView: View {
}
}
}
if !workout.metadata.isEmpty {
ForEach(workout.metadata.elements, id:\.key) { (key, value) in
DetailRow(key, value: value)
}
}
}
.navigationTitle(workout.typeString)
.navigationDestination(for: WorkoutActivity.self) { activity in
@@ -81,3 +87,8 @@ struct WorkoutDetailView: View {
]))
}
}
extension String: Identifiable {
public var id: Self { self }
}