Extract tables, get timezone for samples
This commit is contained in:
parent
77be6d5989
commit
da0e758b35
@ -38,7 +38,7 @@
|
||||
E201EC772B626FC1005B83D3 /* MetadataKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC762B626FC1005B83D3 /* MetadataKey.swift */; };
|
||||
E201EC792B627572005B83D3 /* MetadataKey+SQLite.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC782B627572005B83D3 /* MetadataKey+SQLite.swift */; };
|
||||
E201EC7B2B6275CA005B83D3 /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7A2B6275CA005B83D3 /* Metadata.swift */; };
|
||||
E201EC7D2B62930E005B83D3 /* Sample+SQLite.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7C2B62930E005B83D3 /* Sample+SQLite.swift */; };
|
||||
E201EC7D2B62930E005B83D3 /* SamplesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7C2B62930E005B83D3 /* SamplesTable.swift */; };
|
||||
E201EC7F2B629B4C005B83D3 /* SampleListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7E2B629B4C005B83D3 /* SampleListView.swift */; };
|
||||
E201EC812B631708005B83D3 /* Goal.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC802B631708005B83D3 /* Goal.swift */; };
|
||||
E27BC67A2B5D99AC003A8873 /* LocationSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6792B5D99AC003A8873 /* LocationSample.swift */; };
|
||||
@ -47,8 +47,8 @@
|
||||
E27BC6822B5E762D003A8873 /* LocationSampleDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6812B5E762D003A8873 /* LocationSampleDetailView.swift */; };
|
||||
E27BC6842B5E76A4003A8873 /* Location+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6832B5E76A4003A8873 /* Location+Mock.swift */; };
|
||||
E27BC6862B5FBF0B003A8873 /* Sample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6852B5FBF0B003A8873 /* Sample.swift */; };
|
||||
E27BC6882B5FC220003A8873 /* Sample+Quantity.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6872B5FC220003A8873 /* Sample+Quantity.swift */; };
|
||||
E27BC68A2B5FC255003A8873 /* Sample+Unit.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6892B5FC255003A8873 /* Sample+Unit.swift */; };
|
||||
E27BC6882B5FC220003A8873 /* QuantitySamplesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6872B5FC220003A8873 /* QuantitySamplesTable.swift */; };
|
||||
E27BC68A2B5FC255003A8873 /* UnitStringsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6892B5FC255003A8873 /* UnitStringsTable.swift */; };
|
||||
E27BC68C2B5FC842003A8873 /* ActivitySamplesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC68B2B5FC842003A8873 /* ActivitySamplesView.swift */; };
|
||||
E27BC68E2B5FCBD5003A8873 /* WorkoutEvent+SQLite.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC68D2B5FCBD5003A8873 /* WorkoutEvent+SQLite.swift */; };
|
||||
E27BC6902B5FCEA4003A8873 /* WorkoutActivity+SQLite.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC68F2B5FCEA4003A8873 /* WorkoutActivity+SQLite.swift */; };
|
||||
@ -62,6 +62,9 @@
|
||||
E2FDFF1C2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF1B2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift */; };
|
||||
E2FDFF202B6BE34C0080A7B3 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */; };
|
||||
E2FDFF222B6BE35B0080A7B3 /* EventMetadata.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF212B6BE35B0080A7B3 /* EventMetadata.pb.swift */; };
|
||||
E2FDFF252B6C50A80080A7B3 /* ObjectsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF242B6C50A80080A7B3 /* ObjectsTable.swift */; };
|
||||
E2FDFF272B6C56C70080A7B3 /* DataProvenancesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF262B6C56C70080A7B3 /* DataProvenancesTable.swift */; };
|
||||
E2FDFF292B6D10D60080A7B3 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@ -93,7 +96,7 @@
|
||||
E201EC762B626FC1005B83D3 /* MetadataKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataKey.swift; sourceTree = "<group>"; };
|
||||
E201EC782B627572005B83D3 /* MetadataKey+SQLite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MetadataKey+SQLite.swift"; sourceTree = "<group>"; };
|
||||
E201EC7A2B6275CA005B83D3 /* Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metadata.swift; sourceTree = "<group>"; };
|
||||
E201EC7C2B62930E005B83D3 /* Sample+SQLite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sample+SQLite.swift"; sourceTree = "<group>"; };
|
||||
E201EC7C2B62930E005B83D3 /* SamplesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SamplesTable.swift; sourceTree = "<group>"; };
|
||||
E201EC7E2B629B4C005B83D3 /* SampleListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleListView.swift; sourceTree = "<group>"; };
|
||||
E201EC802B631708005B83D3 /* Goal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Goal.swift; sourceTree = "<group>"; };
|
||||
E27BC6792B5D99AC003A8873 /* LocationSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSample.swift; sourceTree = "<group>"; };
|
||||
@ -102,8 +105,8 @@
|
||||
E27BC6812B5E762D003A8873 /* LocationSampleDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSampleDetailView.swift; sourceTree = "<group>"; };
|
||||
E27BC6832B5E76A4003A8873 /* Location+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Location+Mock.swift"; sourceTree = "<group>"; };
|
||||
E27BC6852B5FBF0B003A8873 /* Sample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sample.swift; sourceTree = "<group>"; };
|
||||
E27BC6872B5FC220003A8873 /* Sample+Quantity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sample+Quantity.swift"; sourceTree = "<group>"; };
|
||||
E27BC6892B5FC255003A8873 /* Sample+Unit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sample+Unit.swift"; sourceTree = "<group>"; };
|
||||
E27BC6872B5FC220003A8873 /* QuantitySamplesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuantitySamplesTable.swift; sourceTree = "<group>"; };
|
||||
E27BC6892B5FC255003A8873 /* UnitStringsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitStringsTable.swift; sourceTree = "<group>"; };
|
||||
E27BC68B2B5FC842003A8873 /* ActivitySamplesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivitySamplesView.swift; sourceTree = "<group>"; };
|
||||
E27BC68D2B5FCBD5003A8873 /* WorkoutEvent+SQLite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkoutEvent+SQLite.swift"; sourceTree = "<group>"; };
|
||||
E27BC68F2B5FCEA4003A8873 /* WorkoutActivity+SQLite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkoutActivity+SQLite.swift"; sourceTree = "<group>"; };
|
||||
@ -115,6 +118,9 @@
|
||||
E2FDFF192B6BB6A40080A7B3 /* HKHealthStore+Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKHealthStore+Interface.swift"; sourceTree = "<group>"; };
|
||||
E2FDFF1B2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKDatabaseFile+Interface.swift"; sourceTree = "<group>"; };
|
||||
E2FDFF212B6BE35B0080A7B3 /* EventMetadata.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventMetadata.pb.swift; sourceTree = "<group>"; };
|
||||
E2FDFF242B6C50A80080A7B3 /* ObjectsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectsTable.swift; sourceTree = "<group>"; };
|
||||
E2FDFF262B6C56C70080A7B3 /* DataProvenancesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProvenancesTable.swift; sourceTree = "<group>"; };
|
||||
E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -199,6 +205,7 @@
|
||||
885002812B5C37B700E7D4DB /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E2FDFF232B6C509D0080A7B3 /* Tables */,
|
||||
E2FDFF212B6BE35B0080A7B3 /* EventMetadata.pb.swift */,
|
||||
E201EC802B631708005B83D3 /* Goal.swift */,
|
||||
E27BC6792B5D99AC003A8873 /* LocationSample.swift */,
|
||||
@ -208,9 +215,6 @@
|
||||
885002A22B5D217600E7D4DB /* MetadataValue.swift */,
|
||||
8850029E2B5D1C7000E7D4DB /* MetadataValue+SQLite.swift */,
|
||||
E27BC6852B5FBF0B003A8873 /* Sample.swift */,
|
||||
E27BC6872B5FC220003A8873 /* Sample+Quantity.swift */,
|
||||
E201EC7C2B62930E005B83D3 /* Sample+SQLite.swift */,
|
||||
E27BC6892B5FC255003A8873 /* Sample+Unit.swift */,
|
||||
8850027E2B5C36A700E7D4DB /* Workout.swift */,
|
||||
8850027A2B5C35BF00E7D4DB /* Workout+SQLite.swift */,
|
||||
E27BC68F2B5FCEA4003A8873 /* WorkoutActivity+SQLite.swift */,
|
||||
@ -232,6 +236,7 @@
|
||||
885002982B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift */,
|
||||
8850029A2B5D16E200E7D4DB /* TimeInterval+Extensions.swift */,
|
||||
E27BC67D2B5E6CE3003A8873 /* Sequence+Extensions.swift */,
|
||||
E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */,
|
||||
);
|
||||
path = Support;
|
||||
sourceTree = "<group>";
|
||||
@ -246,6 +251,18 @@
|
||||
path = API;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E2FDFF232B6C509D0080A7B3 /* Tables */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E2FDFF262B6C56C70080A7B3 /* DataProvenancesTable.swift */,
|
||||
E2FDFF242B6C50A80080A7B3 /* ObjectsTable.swift */,
|
||||
E27BC6872B5FC220003A8873 /* QuantitySamplesTable.swift */,
|
||||
E201EC7C2B62930E005B83D3 /* SamplesTable.swift */,
|
||||
E27BC6892B5FC255003A8873 /* UnitStringsTable.swift */,
|
||||
);
|
||||
path = Tables;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@ -339,6 +356,7 @@
|
||||
885002972B5D157900E7D4DB /* HKWorkoutSessionLocationType+Extensions.swift in Sources */,
|
||||
885002792B5C320400E7D4DB /* Optional+Extensions.swift in Sources */,
|
||||
E201EC792B627572005B83D3 /* MetadataKey+SQLite.swift in Sources */,
|
||||
E2FDFF252B6C50A80080A7B3 /* ObjectsTable.swift in Sources */,
|
||||
885002992B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift in Sources */,
|
||||
E27BC6822B5E762D003A8873 /* LocationSampleDetailView.swift in Sources */,
|
||||
E201EC752B626B19005B83D3 /* Metadata+Mock.swift in Sources */,
|
||||
@ -355,8 +373,8 @@
|
||||
E27BC6842B5E76A4003A8873 /* Location+Mock.swift in Sources */,
|
||||
885002932B5D129300E7D4DB /* ActivityDetailView.swift in Sources */,
|
||||
8850029F2B5D1C7000E7D4DB /* MetadataValue+SQLite.swift in Sources */,
|
||||
E27BC6882B5FC220003A8873 /* Sample+Quantity.swift in Sources */,
|
||||
E201EC7D2B62930E005B83D3 /* Sample+SQLite.swift in Sources */,
|
||||
E27BC6882B5FC220003A8873 /* QuantitySamplesTable.swift in Sources */,
|
||||
E201EC7D2B62930E005B83D3 /* SamplesTable.swift in Sources */,
|
||||
8850027B2B5C35BF00E7D4DB /* Workout+SQLite.swift in Sources */,
|
||||
E27BC6962B5FD61D003A8873 /* WorkoutEvent+Mock.swift in Sources */,
|
||||
E27BC6802B5E74D7003A8873 /* LocationSampleListView.swift in Sources */,
|
||||
@ -366,13 +384,15 @@
|
||||
E201EC7B2B6275CA005B83D3 /* Metadata.swift in Sources */,
|
||||
E27BC67A2B5D99AC003A8873 /* LocationSample.swift in Sources */,
|
||||
885002952B5D147100E7D4DB /* DetailRow.swift in Sources */,
|
||||
E2FDFF292B6D10D60080A7B3 /* String+Extensions.swift in Sources */,
|
||||
E201EC732B626A30005B83D3 /* WorkoutActivity+Mock.swift in Sources */,
|
||||
E2FDFF1A2B6BB6A40080A7B3 /* HKHealthStore+Interface.swift in Sources */,
|
||||
E27BC68A2B5FC255003A8873 /* Sample+Unit.swift in Sources */,
|
||||
E27BC68A2B5FC255003A8873 /* UnitStringsTable.swift in Sources */,
|
||||
8850029D2B5D197300E7D4DB /* EventDetailView.swift in Sources */,
|
||||
E27BC6862B5FBF0B003A8873 /* Sample.swift in Sources */,
|
||||
E27BC67E2B5E6CE3003A8873 /* Sequence+Extensions.swift in Sources */,
|
||||
E27BC68C2B5FC842003A8873 /* ActivitySamplesView.swift in Sources */,
|
||||
E2FDFF272B6C56C70080A7B3 /* DataProvenancesTable.swift in Sources */,
|
||||
E201EC812B631708005B83D3 /* Goal.swift in Sources */,
|
||||
E27BC6942B5FD587003A8873 /* Workout+Mock.swift in Sources */,
|
||||
E27BC68E2B5FCBD5003A8873 /* WorkoutEvent+SQLite.swift in Sources */,
|
||||
|
@ -11,17 +11,28 @@ struct ActivitySamplesView: View {
|
||||
|
||||
@State var samples: [(type: Sample.DataType, samples: [Sample])] = []
|
||||
|
||||
@State var timeZones: [TimeZone] = []
|
||||
|
||||
init(activity: HKWorkoutActivity) {
|
||||
self.activity = activity
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(samples, id: \.0) { entry in
|
||||
NavigationLink {
|
||||
SampleListView(type: entry.type, samples: entry.samples)
|
||||
} label: {
|
||||
DetailRow(entry.type.description, value: entry.samples.count)
|
||||
if !timeZones.isEmpty {
|
||||
Section("Time Zones") {
|
||||
ForEach(timeZones, id: \.identifier) { timeZone in
|
||||
Text(timeZone.debugDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
Section("Samples") {
|
||||
ForEach(samples, id: \.0) { entry in
|
||||
NavigationLink {
|
||||
SampleListView(type: entry.type, samples: entry.samples)
|
||||
} label: {
|
||||
DetailRow(entry.type.description, value: entry.samples.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onAppear(perform: load)
|
||||
@ -39,9 +50,15 @@ struct ActivitySamplesView: View {
|
||||
let ordered = samples
|
||||
.sorted(using: { $0.key.rawValue })
|
||||
.map { (type: $0, samples: $1) }
|
||||
let timeZones: Set<TimeZone> = samples.reduce(into: Set()) { timeZones, sample in
|
||||
timeZones.formUnion(sample.value.compactMap { $0.timeZone })
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.samples = ordered
|
||||
self.timeZones = timeZones.sorted { $0.identifier }
|
||||
}
|
||||
|
||||
} catch {
|
||||
print("Failed to load samples: \(error)")
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ final class HealthDatabase: ObservableObject {
|
||||
|
||||
private let database: Connection
|
||||
|
||||
private let samples: SamplesTable
|
||||
|
||||
@Published
|
||||
var workouts: [Workout] = []
|
||||
|
||||
@ -22,6 +24,7 @@ final class HealthDatabase: ObservableObject {
|
||||
init(fileUrl: URL, database: Connection) {
|
||||
self.fileUrl = fileUrl
|
||||
self.database = database
|
||||
self.samples = .init(database: database)
|
||||
DispatchQueue.global().async {
|
||||
self.readAllWorkouts()
|
||||
}
|
||||
@ -49,13 +52,13 @@ final class HealthDatabase: ObservableObject {
|
||||
}
|
||||
|
||||
func samples(for activity: HKWorkoutActivity) throws -> [Sample.DataType : [Sample]] {
|
||||
try Sample.samples(from: activity.startDate, to: activity.currentEndDate, in: database).reduce(into: [:]) {
|
||||
try samples.samples(from: activity.startDate, to: activity.currentEndDate).reduce(into: [:]) {
|
||||
$0[$1.dataType] = ($0[$1.dataType] ?? []) + [$1]
|
||||
}
|
||||
}
|
||||
|
||||
func sampleCount(for activity: HKWorkoutActivity) throws -> Int {
|
||||
try Sample.sampleCount(from: activity.startDate, to: activity.currentEndDate, in: database)
|
||||
try samples.sampleCount(from: activity.startDate, to: activity.currentEndDate)
|
||||
}
|
||||
|
||||
var activities: [HKWorkoutActivity] {
|
||||
@ -79,6 +82,15 @@ final class HealthDatabase: ObservableObject {
|
||||
convenience init(database: Database) {
|
||||
self.init(fileUrl: .init(filePath: "/"), database: database)
|
||||
}
|
||||
|
||||
func insert(workout: Workout, into store: HKHealthStore) async throws -> HKWorkout? {
|
||||
guard let configuration = workout.activities.first?.workoutConfiguration else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let builder = HKWorkoutBuilder(healthStore: store, configuration: configuration, device: nil)
|
||||
return try await builder.finishWorkout()
|
||||
}
|
||||
}
|
||||
|
||||
private extension HKWorkoutActivity {
|
||||
|
@ -1,23 +0,0 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
extension Sample {
|
||||
|
||||
private static let table = Table("quantity_samples")
|
||||
|
||||
private static let rowDataId = Expression<Int>("data_id")
|
||||
|
||||
// NOTE: Technically optional
|
||||
private static let rowQuantity = Expression<Double>("quantity")
|
||||
|
||||
private static let rowOriginalQuantity = Expression<Double?>("original_quantity")
|
||||
|
||||
/// References `ROW_ID` on table `unit_strings`
|
||||
private static let rowOriginalUnit = Expression<Int?>("original_unit")
|
||||
|
||||
static func quantity(for id: Int, in database: Database) throws -> (quantity: Double, original: Double?, unit: Int?)? {
|
||||
try database.prepare(table.filter(rowDataId == id).limit(1)).map {
|
||||
(quantity: $0[rowQuantity], original: $0[rowOriginalQuantity], unit: $0[rowOriginalUnit])
|
||||
}.first
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
extension Sample {
|
||||
|
||||
private static let table = Table("samples")
|
||||
|
||||
private static let columnDataId = Expression<Int>("data_id")
|
||||
|
||||
// NOTE: Technically optional
|
||||
private static let columnStartDate = Expression<Double>("start_date")
|
||||
|
||||
// NOTE: Technically optional
|
||||
private static let columnEndDate = Expression<Double>("end_date")
|
||||
|
||||
private static let columnDataType = Expression<Int>("data_type")
|
||||
|
||||
static func samples(from start: Date, to end: Date, in database: Database) throws -> [Sample] {
|
||||
let start = start.timeIntervalSinceReferenceDate
|
||||
let end = end.timeIntervalSinceReferenceDate
|
||||
return try database.prepare(table.filter(columnStartDate >= start && columnEndDate <= end)).compactMap { (row: Row) -> Sample? in
|
||||
let dataId = row[columnDataId]
|
||||
guard let quantity = try Sample.quantity(for: dataId, in: database) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let unit = try quantity.unit.map { try Sample.unit(for: $0, in: database) }
|
||||
|
||||
return Sample(
|
||||
startDate: Date(timeIntervalSinceReferenceDate: row[columnStartDate]),
|
||||
endDate: Date(timeIntervalSinceReferenceDate: row[columnEndDate]),
|
||||
dataType: .init(rawValue: row[columnDataType]),
|
||||
quantity: quantity.quantity,
|
||||
originalQuantity: quantity.original,
|
||||
originalUnit: unit)
|
||||
}
|
||||
}
|
||||
|
||||
static func sampleCount(from start: Date, to end: Date, in database: Database) throws -> Int {
|
||||
let start = start.timeIntervalSinceReferenceDate
|
||||
let end = end.timeIntervalSinceReferenceDate
|
||||
return try database.scalar(table.filter(columnStartDate >= start && columnEndDate <= end).count)
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
extension Sample {
|
||||
|
||||
private static let table = Table("unit_strings")
|
||||
|
||||
private static let rowId = Expression<Int>("ROW_ID")
|
||||
|
||||
// - NOTE: Technically optional
|
||||
private static let rowUnitString = Expression<String>("quantity")
|
||||
|
||||
static func unit(for id: Int, in database: Database) throws -> String? {
|
||||
try database.prepare(table.filter(rowId == id).limit(1)).map { row in
|
||||
row[rowUnitString]
|
||||
}.first
|
||||
}
|
||||
}
|
@ -8,12 +8,25 @@ struct Sample {
|
||||
|
||||
let dataType: DataType
|
||||
|
||||
let quantity: Double
|
||||
let quantity: Double?
|
||||
|
||||
let originalQuantity: Double?
|
||||
|
||||
let originalUnit: String?
|
||||
|
||||
let timeZoneName: String?
|
||||
|
||||
var timeZone: TimeZone? {
|
||||
guard let timeZoneName else {
|
||||
return nil
|
||||
}
|
||||
guard let zone = TimeZone(identifier: timeZoneName) else {
|
||||
print("No time zone for '\(timeZoneName)'")
|
||||
return nil
|
||||
}
|
||||
return zone
|
||||
}
|
||||
|
||||
var duration: TimeInterval {
|
||||
endDate.timeIntervalSince(startDate)
|
||||
}
|
||||
@ -25,12 +38,23 @@ struct Sample {
|
||||
return " (\(originalQuantity) \(originalUnit))"
|
||||
|
||||
}
|
||||
|
||||
var quantityText: String {
|
||||
guard let quantity else {
|
||||
return "-"
|
||||
}
|
||||
return "\(quantity)"
|
||||
}
|
||||
|
||||
var dateText: String {
|
||||
startDate.timeAndDateText(in: timeZone ?? .current)
|
||||
}
|
||||
}
|
||||
|
||||
extension Sample: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
"\(startDate.timeAndDateText) (\(Int(duration)) s) \(quantity)\(originalQuantityText)"
|
||||
"\(dateText) (\(Int(duration)) s) \(quantityText)\(originalQuantityText)"
|
||||
}
|
||||
}
|
||||
|
||||
|
266
HealthImport/Model/Tables/DataProvenancesTable.swift
Normal file
266
HealthImport/Model/Tables/DataProvenancesTable.swift
Normal file
@ -0,0 +1,266 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
import HealthKit
|
||||
|
||||
struct DataProvenancesTable {
|
||||
|
||||
private let database: Connection
|
||||
|
||||
init(database: Connection) {
|
||||
self.database = database
|
||||
}
|
||||
|
||||
func create() throws {
|
||||
try database.execute("CREATE TABLE data_provenances (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, sync_provenance INTEGER NOT NULL, origin_product_type TEXT NOT NULL, origin_build TEXT NOT NULL, local_product_type TEXT NOT NULL, local_build TEXT NOT NULL, source_id INTEGER NOT NULL, device_id INTEGER NOT NULL, contributor_id INTEGER NOT NULL, source_version TEXT NOT NULL, tz_name TEXT NOT NULL, origin_major_version INTEGER NOT NULL, origin_minor_version INTEGER NOT NULL, origin_patch_version INTEGER NOT NULL, sync_identity INTEGER NOT NULL, derived_flags INTEGER NOT NULL, UNIQUE(sync_provenance, origin_product_type, origin_build, local_product_type, local_build, source_id, device_id, contributor_id, source_version, tz_name, origin_major_version, origin_minor_version, origin_patch_version, sync_identity))")
|
||||
}
|
||||
|
||||
let table = Table("data_provenances")
|
||||
|
||||
let rowId = Expression<Int>("ROWID")
|
||||
|
||||
let syncProvenance = Expression<Int>("sync_provenance")
|
||||
|
||||
/// Device that created the data (e.g. Watch)
|
||||
let originProductType = Expression<String>("origin_product_type")
|
||||
|
||||
let originBuild = Expression<String>("origin_build")
|
||||
|
||||
/// Device saving the data (e.g. iPhone)
|
||||
let localProductType = Expression<String>("local_product_type")
|
||||
|
||||
let localBuild = Expression<String>("local_build")
|
||||
|
||||
let sourceId = Expression<Int>("source_id")
|
||||
|
||||
let deviceId = Expression<Int>("device_id")
|
||||
|
||||
let contributorId = Expression<Int>("contributor_id")
|
||||
|
||||
let sourceVersion = Expression<String>("source_version")
|
||||
|
||||
let tzName = Expression<String>("tz_name")
|
||||
|
||||
let originMajorVersion = Expression<Int>("origin_major_version")
|
||||
|
||||
let originMinorVersion = Expression<Int>("origin_minor_version")
|
||||
|
||||
let originPatchVersion = Expression<Int>("origin_patch_version")
|
||||
|
||||
let syncIdentity = Expression<Int>("sync_identity")
|
||||
|
||||
let derivedFlags = Expression<Int>("derived_flags")
|
||||
|
||||
func device(for rowId: Int) throws -> HKDevice? {
|
||||
try database.pluck(table.filter(self.rowId == rowId)).map { row in
|
||||
let productType = row[originProductType]
|
||||
return HKDevice(
|
||||
name: nil,
|
||||
manufacturer: "Apple Inc.",
|
||||
model: productTypeToHumanName[productType],
|
||||
hardwareVersion: productType,
|
||||
firmwareVersion: nil,
|
||||
softwareVersion: "\(row[originMajorVersion]).\(row[originMinorVersion]).\(row[originPatchVersion])",
|
||||
localIdentifier: nil,
|
||||
udiDeviceIdentifier: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func timeZoneName(for rowId: Int) throws -> String? {
|
||||
try database.pluck(table.filter(self.rowId == rowId)).map { $0[tzName] }
|
||||
}
|
||||
}
|
||||
|
||||
private let productTypeToHumanName: [String : String] = [
|
||||
"i386" : "iPhone Simulator",
|
||||
"x86_64" : "iPhone Simulator",
|
||||
"arm64" : "iPhone Simulator",
|
||||
"iPhone1,1" : "iPhone",
|
||||
"iPhone1,2" : "iPhone 3G",
|
||||
"iPhone2,1" : "iPhone 3GS",
|
||||
"iPhone3,1" : "iPhone 4",
|
||||
"iPhone3,2" : "iPhone 4 GSM Rev A",
|
||||
"iPhone3,3" : "iPhone 4 CDMA",
|
||||
"iPhone4,1" : "iPhone 4S",
|
||||
"iPhone5,1" : "iPhone 5 (GSM)",
|
||||
"iPhone5,2" : "iPhone 5 (GSM+CDMA)",
|
||||
"iPhone5,3" : "iPhone 5C (GSM)",
|
||||
"iPhone5,4" : "iPhone 5C (Global)",
|
||||
"iPhone6,1" : "iPhone 5S (GSM)",
|
||||
"iPhone6,2" : "iPhone 5S (Global)",
|
||||
"iPhone7,1" : "iPhone 6 Plus",
|
||||
"iPhone7,2" : "iPhone 6",
|
||||
"iPhone8,1" : "iPhone 6s",
|
||||
"iPhone8,2" : "iPhone 6s Plus",
|
||||
"iPhone8,4" : "iPhone SE (GSM)",
|
||||
"iPhone9,1" : "iPhone 7",
|
||||
"iPhone9,2" : "iPhone 7 Plus",
|
||||
"iPhone9,3" : "iPhone 7",
|
||||
"iPhone9,4" : "iPhone 7 Plus",
|
||||
"iPhone10,1" : "iPhone 8",
|
||||
"iPhone10,2" : "iPhone 8 Plus",
|
||||
"iPhone10,3" : "iPhone X Global",
|
||||
"iPhone10,4" : "iPhone 8",
|
||||
"iPhone10,5" : "iPhone 8 Plus",
|
||||
"iPhone10,6" : "iPhone X GSM",
|
||||
"iPhone11,2" : "iPhone XS",
|
||||
"iPhone11,4" : "iPhone XS Max",
|
||||
"iPhone11,6" : "iPhone XS Max Global",
|
||||
"iPhone11,8" : "iPhone XR",
|
||||
"iPhone12,1" : "iPhone 11",
|
||||
"iPhone12,3" : "iPhone 11 Pro",
|
||||
"iPhone12,5" : "iPhone 11 Pro Max",
|
||||
"iPhone12,8" : "iPhone SE 2nd Gen",
|
||||
"iPhone13,1" : "iPhone 12 Mini",
|
||||
"iPhone13,2" : "iPhone 12",
|
||||
"iPhone13,3" : "iPhone 12 Pro",
|
||||
"iPhone13,4" : "iPhone 12 Pro Max",
|
||||
"iPhone14,2" : "iPhone 13 Pro",
|
||||
"iPhone14,3" : "iPhone 13 Pro Max",
|
||||
"iPhone14,4" : "iPhone 13 Mini",
|
||||
"iPhone14,5" : "iPhone 13",
|
||||
"iPhone14,6" : "iPhone SE 3rd Gen",
|
||||
"iPhone14,7" : "iPhone 14",
|
||||
"iPhone14,8" : "iPhone 14 Plus",
|
||||
"iPhone15,2" : "iPhone 14 Pro",
|
||||
"iPhone15,3" : "iPhone 14 Pro Max",
|
||||
"iPhone15,4" : "iPhone 15",
|
||||
"iPhone15,5" : "iPhone 15 Plus",
|
||||
"iPhone16,1" : "iPhone 15 Pro",
|
||||
"iPhone16,2" : "iPhone 15 Pro Max",
|
||||
|
||||
"iPod1,1" : "1st Gen iPod",
|
||||
"iPod2,1" : "2nd Gen iPod",
|
||||
"iPod3,1" : "3rd Gen iPod",
|
||||
"iPod4,1" : "4th Gen iPod",
|
||||
"iPod5,1" : "5th Gen iPod",
|
||||
"iPod7,1" : "6th Gen iPod",
|
||||
"iPod9,1" : "7th Gen iPod",
|
||||
|
||||
"iPad1,1" : "iPad",
|
||||
"iPad1,2" : "iPad 3G",
|
||||
"iPad2,1" : "2nd Gen iPad",
|
||||
"iPad2,2" : "2nd Gen iPad GSM",
|
||||
"iPad2,3" : "2nd Gen iPad CDMA",
|
||||
"iPad2,4" : "2nd Gen iPad New Revision",
|
||||
"iPad3,1" : "3rd Gen iPad",
|
||||
"iPad3,2" : "3rd Gen iPad CDMA",
|
||||
"iPad3,3" : "3rd Gen iPad GSM",
|
||||
"iPad2,5" : "iPad mini",
|
||||
"iPad2,6" : "iPad mini GSM+LTE",
|
||||
"iPad2,7" : "iPad mini CDMA+LTE",
|
||||
"iPad3,4" : "4th Gen iPad",
|
||||
"iPad3,5" : "4th Gen iPad GSM+LTE",
|
||||
"iPad3,6" : "4th Gen iPad CDMA+LTE",
|
||||
"iPad4,1" : "iPad Air (WiFi)",
|
||||
"iPad4,2" : "iPad Air (GSM+CDMA)",
|
||||
"iPad4,3" : "1st Gen iPad Air (China)",
|
||||
"iPad4,4" : "iPad mini Retina (WiFi)",
|
||||
"iPad4,5" : "iPad mini Retina (GSM+CDMA)",
|
||||
"iPad4,6" : "iPad mini Retina (China)",
|
||||
"iPad4,7" : "iPad mini 3 (WiFi)",
|
||||
"iPad4,8" : "iPad mini 3 (GSM+CDMA)",
|
||||
"iPad4,9" : "iPad Mini 3 (China)",
|
||||
"iPad5,1" : "iPad mini 4 (WiFi)",
|
||||
"iPad5,2" : "4th Gen iPad mini (WiFi+Cellular)",
|
||||
"iPad5,3" : "iPad Air 2 (WiFi)",
|
||||
"iPad5,4" : "iPad Air 2 (Cellular)",
|
||||
"iPad6,3" : "iPad Pro (9.7 inch, WiFi)",
|
||||
"iPad6,4" : "iPad Pro (9.7 inch, WiFi+LTE)",
|
||||
"iPad6,7" : "iPad Pro (12.9 inch, WiFi)",
|
||||
"iPad6,8" : "iPad Pro (12.9 inch, WiFi+LTE)",
|
||||
"iPad6,11" : "iPad (2017)",
|
||||
"iPad6,12" : "iPad (2017)",
|
||||
"iPad7,1" : "iPad Pro 2nd Gen (WiFi)",
|
||||
"iPad7,2" : "iPad Pro 2nd Gen (WiFi+Cellular)",
|
||||
"iPad7,3" : "iPad Pro 10.5-inch 2nd Gen",
|
||||
"iPad7,4" : "iPad Pro 10.5-inch 2nd Gen",
|
||||
"iPad7,5" : "iPad 6th Gen (WiFi)",
|
||||
"iPad7,6" : "iPad 6th Gen (WiFi+Cellular)",
|
||||
"iPad7,11" : "iPad 7th Gen 10.2-inch (WiFi)",
|
||||
"iPad7,12" : "iPad 7th Gen 10.2-inch (WiFi+Cellular)",
|
||||
"iPad8,1" : "iPad Pro 11 inch 3rd Gen (WiFi)",
|
||||
"iPad8,2" : "iPad Pro 11 inch 3rd Gen (1TB, WiFi)",
|
||||
"iPad8,3" : "iPad Pro 11 inch 3rd Gen (WiFi+Cellular)",
|
||||
"iPad8,4" : "iPad Pro 11 inch 3rd Gen (1TB, WiFi+Cellular)",
|
||||
"iPad8,5" : "iPad Pro 12.9 inch 3rd Gen (WiFi)",
|
||||
"iPad8,6" : "iPad Pro 12.9 inch 3rd Gen (1TB, WiFi)",
|
||||
"iPad8,7" : "iPad Pro 12.9 inch 3rd Gen (WiFi+Cellular)",
|
||||
"iPad8,8" : "iPad Pro 12.9 inch 3rd Gen (1TB, WiFi+Cellular)",
|
||||
"iPad8,9" : "iPad Pro 11 inch 4th Gen (WiFi)",
|
||||
"iPad8,10" : "iPad Pro 11 inch 4th Gen (WiFi+Cellular)",
|
||||
"iPad8,11" : "iPad Pro 12.9 inch 4th Gen (WiFi)",
|
||||
"iPad8,12" : "iPad Pro 12.9 inch 4th Gen (WiFi+Cellular)",
|
||||
"iPad11,1" : "iPad mini 5th Gen (WiFi)",
|
||||
"iPad11,2" : "iPad mini 5th Gen",
|
||||
"iPad11,3" : "iPad Air 3rd Gen (WiFi)",
|
||||
"iPad11,4" : "iPad Air 3rd Gen",
|
||||
"iPad11,6" : "iPad 8th Gen (WiFi)",
|
||||
"iPad11,7" : "iPad 8th Gen (WiFi+Cellular)",
|
||||
"iPad12,1" : "iPad 9th Gen (WiFi)",
|
||||
"iPad12,2" : "iPad 9th Gen (WiFi+Cellular)",
|
||||
"iPad14,1" : "iPad mini 6th Gen (WiFi)",
|
||||
"iPad14,2" : "iPad mini 6th Gen (WiFi+Cellular)",
|
||||
"iPad13,1" : "iPad Air 4th Gen (WiFi)",
|
||||
"iPad13,2" : "iPad Air 4th Gen (WiFi+Cellular)",
|
||||
"iPad13,4" : "iPad Pro 11 inch 5th Gen",
|
||||
"iPad13,5" : "iPad Pro 11 inch 5th Gen",
|
||||
"iPad13,6" : "iPad Pro 11 inch 5th Gen",
|
||||
"iPad13,7" : "iPad Pro 11 inch 5th Gen",
|
||||
"iPad13,8" : "iPad Pro 12.9 inch 5th Gen",
|
||||
"iPad13,9" : "iPad Pro 12.9 inch 5th Gen",
|
||||
"iPad13,10" : "iPad Pro 12.9 inch 5th Gen",
|
||||
"iPad13,11" : "iPad Pro 12.9 inch 5th Gen",
|
||||
"iPad13,16" : "iPad Air 5th Gen (WiFi)",
|
||||
"iPad13,17" : "iPad Air 5th Gen (WiFi+Cellular)",
|
||||
"iPad13,18" : "iPad 10th Gen",
|
||||
"iPad13,19" : "iPad 10th Gen",
|
||||
"iPad14,3" : "iPad Pro 11 inch 4th Gen",
|
||||
"iPad14,4" : "iPad Pro 11 inch 4th Gen",
|
||||
"iPad14,5" : "iPad Pro 12.9 inch 6th Gen",
|
||||
"iPad14,6" : "iPad Pro 12.9 inch 6th Gen",
|
||||
|
||||
"Watch1,1" : "Apple Watch 38mm case",
|
||||
"Watch1,2" : "Apple Watch 42mm case",
|
||||
"Watch2,6" : "Apple Watch Series 1 38mm case",
|
||||
"Watch2,7" : "Apple Watch Series 1 42mm case",
|
||||
"Watch2,3" : "Apple Watch Series 2 38mm case",
|
||||
"Watch2,4" : "Apple Watch Series 2 42mm case",
|
||||
"Watch3,1" : "Apple Watch Series 3 38mm case (GPS+Cellular)",
|
||||
"Watch3,2" : "Apple Watch Series 3 42mm case (GPS+Cellular)",
|
||||
"Watch3,3" : "Apple Watch Series 3 38mm case (GPS)",
|
||||
"Watch3,4" : "Apple Watch Series 3 42mm case (GPS)",
|
||||
"Watch4,1" : "Apple Watch Series 4 40mm case (GPS)",
|
||||
"Watch4,2" : "Apple Watch Series 4 44mm case (GPS)",
|
||||
"Watch4,3" : "Apple Watch Series 4 40mm case (GPS+Cellular)",
|
||||
"Watch4,4" : "Apple Watch Series 4 44mm case (GPS+Cellular)",
|
||||
"Watch5,1" : "Apple Watch Series 5 40mm case (GPS)",
|
||||
"Watch5,2" : "Apple Watch Series 5 44mm case (GPS)",
|
||||
"Watch5,3" : "Apple Watch Series 5 40mm case (GPS+Cellular)",
|
||||
"Watch5,4" : "Apple Watch Series 5 44mm case (GPS+Cellular)",
|
||||
"Watch5,9" : "Apple Watch SE 40mm case (GPS)",
|
||||
"Watch5,10" : "Apple Watch SE 44mm case (GPS)",
|
||||
"Watch5,11" : "Apple Watch SE 40mm case (GPS+Cellular)",
|
||||
"Watch5,12" : "Apple Watch SE 44mm case (GPS+Cellular)",
|
||||
"Watch6,1" : "Apple Watch Series 6 40mm case (GPS)",
|
||||
"Watch6,2" : "Apple Watch Series 6 44mm case (GPS)",
|
||||
"Watch6,3" : "Apple Watch Series 6 40mm case (GPS+Cellular)",
|
||||
"Watch6,4" : "Apple Watch Series 6 44mm case (GPS+Cellular)",
|
||||
"Watch6,6" : "Apple Watch Series 7 41mm case (GPS)",
|
||||
"Watch6,7" : "Apple Watch Series 7 45mm case (GPS)",
|
||||
"Watch6,8" : "Apple Watch Series 7 41mm case (GPS+Cellular)",
|
||||
"Watch6,9" : "Apple Watch Series 7 45mm case (GPS+Cellular)",
|
||||
"Watch6,10" : "Apple Watch SE 40mm case (GPS)",
|
||||
"Watch6,11" : "Apple Watch SE 44mm case (GPS)",
|
||||
"Watch6,12" : "Apple Watch SE 40mm case (GPS+Cellular)",
|
||||
"Watch6,13" : "Apple Watch SE 44mm case (GPS+Cellular)",
|
||||
"Watch6,14" : "Apple Watch Series 8 41mm case (GPS)",
|
||||
"Watch6,15" : "Apple Watch Series 8 45mm case (GPS)",
|
||||
"Watch6,16" : "Apple Watch Series 8 41mm case (GPS+Cellular)",
|
||||
"Watch6,17" : "Apple Watch Series 8 45mm case (GPS+Cellular)",
|
||||
"Watch6,18" : "Apple Watch Ultra",
|
||||
"Watch7,1" : "Apple Watch Series 9 41mm case (GPS)",
|
||||
"Watch7,2" : "Apple Watch Series 9 45mm case (GPS)",
|
||||
"Watch7,3" : "Apple Watch Series 9 41mm case (GPS+Cellular)",
|
||||
"Watch7,4" : "Apple Watch Series 9 45mm case (GPS+Cellular)",
|
||||
"Watch7,5" : "Apple Watch Ultra 2",
|
||||
]
|
37
HealthImport/Model/Tables/ObjectsTable.swift
Normal file
37
HealthImport/Model/Tables/ObjectsTable.swift
Normal file
@ -0,0 +1,37 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
struct ObjectsTable {
|
||||
|
||||
private let database: Connection
|
||||
|
||||
init(database: Connection) {
|
||||
self.database = database
|
||||
}
|
||||
|
||||
func create() throws {
|
||||
try database.execute("CREATE TABLE objects (data_id INTEGER PRIMARY KEY AUTOINCREMENT, uuid BLOB UNIQUE, provenance INTEGER NOT NULL REFERENCES data_provenances (ROWID) ON DELETE CASCADE, type INTEGER, creation_date REAL)")
|
||||
}
|
||||
|
||||
let table = Table("objects")
|
||||
|
||||
let dataId = Expression<Int>("data_id")
|
||||
|
||||
let uuid = Expression<Data?>("uuid")
|
||||
|
||||
let provenance = Expression<Int>("provenance")
|
||||
|
||||
let type = Expression<Int?>("type")
|
||||
|
||||
let creationDate = Expression<Double?>("creation_date")
|
||||
|
||||
func object(for dataId: Int) throws -> (uuid: UUID, provenance: Int, type: Int, creationDate: Date)? {
|
||||
try database.pluck(table.filter(self.dataId == dataId)).map { row in
|
||||
let uuid = row[uuid]!.asUUID()!
|
||||
let provenance = row[provenance]
|
||||
let type = row[type]!
|
||||
let creationDate = Date(timeIntervalSinceReferenceDate: row[creationDate]!)
|
||||
return (uuid, provenance, type, creationDate)
|
||||
}
|
||||
}
|
||||
}
|
32
HealthImport/Model/Tables/QuantitySamplesTable.swift
Normal file
32
HealthImport/Model/Tables/QuantitySamplesTable.swift
Normal file
@ -0,0 +1,32 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
struct QuantitySamplesTable {
|
||||
|
||||
private let database: Connection
|
||||
|
||||
init(database: Connection) {
|
||||
self.database = database
|
||||
}
|
||||
|
||||
func create() throws {
|
||||
try database.execute("CREATE TABLE quantity_samples (data_id INTEGER PRIMARY KEY, quantity REAL, original_quantity REAL, original_unit INTEGER REFERENCES unit_strings (ROWID) ON DELETE NO ACTION)")
|
||||
}
|
||||
|
||||
let table = Table("quantity_samples")
|
||||
|
||||
let dataId = Expression<Int>("data_id")
|
||||
|
||||
let quantity = Expression<Double?>("quantity")
|
||||
|
||||
let originalQuantity = Expression<Double?>("original_quantity")
|
||||
|
||||
/// References `ROWID` on table `unit_strings`
|
||||
let originalUnit = Expression<Int?>("original_unit")
|
||||
|
||||
func quantity(for id: Int, in database: Database) throws -> (quantity: Double?, original: Double?, unit: Int?) {
|
||||
try database.prepare(table.filter(dataId == id).limit(1)).map {
|
||||
(quantity: $0[quantity], original: $0[originalQuantity], unit: $0[originalUnit])
|
||||
}.first ?? (nil, nil, nil)
|
||||
}
|
||||
}
|
88
HealthImport/Model/Tables/SamplesTable.swift
Normal file
88
HealthImport/Model/Tables/SamplesTable.swift
Normal file
@ -0,0 +1,88 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
struct SamplesTable {
|
||||
|
||||
private let database: Connection
|
||||
|
||||
private let quantitySamples: QuantitySamplesTable
|
||||
|
||||
private let objects: ObjectsTable
|
||||
|
||||
private let dataProvenances: DataProvenancesTable
|
||||
|
||||
private let unitStrings: UnitStringsTable
|
||||
|
||||
init(database: Connection) {
|
||||
self.database = database
|
||||
self.quantitySamples = .init(database: database)
|
||||
self.objects = .init(database: database)
|
||||
self.dataProvenances = .init(database: database)
|
||||
self.unitStrings = .init(database: database)
|
||||
}
|
||||
|
||||
func create() throws {
|
||||
try database.execute("CREATE TABLE samples (data_id INTEGER PRIMARY KEY, start_date REAL, end_date REAL, data_type INTEGER)")
|
||||
}
|
||||
|
||||
private let table = Table("samples")
|
||||
|
||||
private let dataId = Expression<Int>("data_id")
|
||||
|
||||
// NOTE: Technically optional
|
||||
private let startDate = Expression<Double>("start_date")
|
||||
|
||||
// NOTE: Technically optional
|
||||
private let endDate = Expression<Double>("end_date")
|
||||
|
||||
private let dataType = Expression<Int>("data_type")
|
||||
|
||||
func samples(from start: Date, to end: Date) throws -> [Sample] {
|
||||
let start = start.timeIntervalSinceReferenceDate
|
||||
let end = end.timeIntervalSinceReferenceDate
|
||||
|
||||
// Samples: data_id, start_date, end_date, data_type
|
||||
// JOIN quantity_samples on samples.data_id == quantity_samples.data_id
|
||||
// quantity_samples: quantity, original_quantity, original_unit
|
||||
// JOIN objects on samples.data_id == objects.data_id
|
||||
// objects: data_id, uuid, provenance, type, creation_date
|
||||
|
||||
// JOIN data_provenances on objects.provenance == data_provenances.ROWID
|
||||
// SELECT tz_name FROM data_provenances
|
||||
|
||||
let selection = table
|
||||
.select(table[*],
|
||||
quantitySamples.table[*],
|
||||
dataProvenances.table[dataProvenances.tzName],
|
||||
unitStrings.table[unitStrings.unitString])
|
||||
.filter(startDate >= start && endDate <= end)
|
||||
.join(.leftOuter, quantitySamples.table, on: table[dataId] == quantitySamples.table[quantitySamples.dataId])
|
||||
.join(.leftOuter, objects.table, on: table[dataId] == objects.table[objects.dataId])
|
||||
.join(.leftOuter, dataProvenances.table, on: objects.table[objects.provenance] == dataProvenances.table[dataProvenances.rowId])
|
||||
.join(.leftOuter, unitStrings.table, on: quantitySamples.table[quantitySamples.originalUnit] == unitStrings.table[unitStrings.rowId])
|
||||
|
||||
return try database.prepare(selection).map { row in
|
||||
let startDate = Date(timeIntervalSinceReferenceDate: row[startDate])
|
||||
let endDate = Date(timeIntervalSinceReferenceDate: row[endDate])
|
||||
let dataType = Sample.DataType(rawValue: row[dataType])
|
||||
let quantity = row[quantitySamples.quantity]
|
||||
let original = row[quantitySamples.originalQuantity]
|
||||
let unit = row[unitStrings.unitString]
|
||||
let timeZone = row[dataProvenances.tzName].nonEmpty
|
||||
return .init(
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
dataType: dataType,
|
||||
quantity: quantity,
|
||||
originalQuantity: original,
|
||||
originalUnit: unit,
|
||||
timeZoneName: timeZone)
|
||||
}
|
||||
}
|
||||
|
||||
func sampleCount(from start: Date, to end: Date) throws -> Int {
|
||||
let start = start.timeIntervalSinceReferenceDate
|
||||
let end = end.timeIntervalSinceReferenceDate
|
||||
return try database.scalar(table.filter(startDate >= start && endDate <= end).count)
|
||||
}
|
||||
}
|
27
HealthImport/Model/Tables/UnitStringsTable.swift
Normal file
27
HealthImport/Model/Tables/UnitStringsTable.swift
Normal file
@ -0,0 +1,27 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
struct UnitStringsTable {
|
||||
|
||||
private let database: Connection
|
||||
|
||||
init(database: Connection) {
|
||||
self.database = database
|
||||
}
|
||||
|
||||
func create() throws {
|
||||
try database.execute("CREATE TABLE unit_strings (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, unit_string TEXT UNIQUE)")
|
||||
}
|
||||
|
||||
let table = Table("unit_strings")
|
||||
|
||||
let rowId = Expression<Int>("ROWID")
|
||||
|
||||
let unitString = Expression<String?>("unit_string")
|
||||
|
||||
func unit(for id: Int) throws -> String? {
|
||||
try database.pluck(table.filter(rowId == id).limit(1)).map { row in
|
||||
row[unitString]
|
||||
}
|
||||
}
|
||||
}
|
@ -51,7 +51,10 @@ enum WorkoutActivityTable {
|
||||
let start = Date(timeIntervalSinceReferenceDate: row[columnStartDate])
|
||||
let end = Date(timeIntervalSinceReferenceDate: row[columnEndDate])
|
||||
let uuid = row[columnUUID].uuidString
|
||||
let metadata: [String : Any] = [HKMetadataKeyExternalUUID : uuid]
|
||||
|
||||
var metadata: [String : Any] = [ : ]
|
||||
metadata[HKMetadataKeyExternalUUID] = uuid
|
||||
|
||||
// duration: row[columnDuration]
|
||||
// isPrimaryActivity: row[columnIsPrimaryActivity]
|
||||
|
||||
@ -123,19 +126,3 @@ private extension WorkoutActivityTable {
|
||||
try NSKeyedUnarchiver.unarchivedObject(ofClass: HKQuantity.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Data {
|
||||
|
||||
var uuidString: String {
|
||||
let h = Array(self)
|
||||
let parts = [h[0..<4], h[4..<6], h[6..<8], h[8..<10], h[10..<16]]
|
||||
return parts.map { Data($0).hex }.joined(separator: "-")
|
||||
}
|
||||
}
|
||||
|
||||
private extension UUID {
|
||||
|
||||
init?(data: Data) {
|
||||
self.init(uuidString: data.uuidString)
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,37 @@ extension Data {
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
|
||||
func asUUID() -> UUID? {
|
||||
.init(data: self)
|
||||
}
|
||||
|
||||
var uuidString: String? {
|
||||
guard self.count == 16 else {
|
||||
return nil
|
||||
}
|
||||
let h = Array(self)
|
||||
let parts = [h[0..<4], h[4..<6], h[6..<8], h[8..<10], h[10..<16]]
|
||||
return parts.map { Data($0).hex }.joined(separator: "-")
|
||||
}
|
||||
}
|
||||
|
||||
extension UUID {
|
||||
|
||||
init?(data: Data) {
|
||||
guard let uuidString = data.uuidString else {
|
||||
return nil
|
||||
}
|
||||
self.init(uuidString: uuidString)
|
||||
}
|
||||
|
||||
var data: Data? {
|
||||
.init(hex: uuidString.replacingOccurrences(of: "-", with: ""))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Data {
|
||||
|
||||
|
||||
|
@ -65,7 +65,13 @@ extension Date {
|
||||
}
|
||||
|
||||
var timeAndDateText: String {
|
||||
dateFormatter.string(from: self)
|
||||
dateFormatter.timeZone = .current
|
||||
return dateFormatter.string(from: self)
|
||||
}
|
||||
|
||||
func timeAndDateText(in timeZone: TimeZone) -> String {
|
||||
dateFormatter.timeZone = timeZone
|
||||
return dateFormatter.string(from: self)
|
||||
}
|
||||
|
||||
var timeAndDateWithSecondsText: String {
|
||||
|
8
HealthImport/Support/String+Extensions.swift
Normal file
8
HealthImport/Support/String+Extensions.swift
Normal file
@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
|
||||
var nonEmpty: String? {
|
||||
self != "" ? self : nil
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user