generated from StanfordSpezi/SpeziTemplateApplication
-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding Time Index To HealthKit Sample Uploads (#34)
# *Adding Time Index To HealthKit Sample Uploads* ## ♻️ Current situation & Problem As we are transitioning the backend to an architecture that doesn't depend on cloud functions and periodic featurization, we are working on efficient data fetching queries in Firebase. Many HealthKit data types are associated with a time _range_, not a timestamp. This complicates Firebase queries since queries like ``` collection.where(filter=("timeRange.start", "<=", queryEnd) .where(filter=FieldFilter("timeRange.end", ">=", queryStart)) ``` are not permitted due to inequality filters on multiple properties. Instead, I am proposing adding the following fields to each HealthKit upload: ``` time.range: bool // whether the sample is a time range or time stamp type // if time.range = false, not .end properties are set time.year.start, time.year.end: int time.month.start, time.month.end: int time.day.start, time.day.end: int time.hour.start, time.hour.end: int time.minute.start, time.minute.end: int time.second.start, time.second.end: int time.dayMin.start, time.dayMin.end: int // how many total minutes have elapsed, between [0, 1439] time.15minBucket.start, time.15minBucket.end // which 15 min bucket the sample falls into, between [0,95] ``` in addition, we will also have ranges for each time index component, such as ``` time.15minBucket.range = [45, 46, 47] // sample falls between 11:15-12:00 ``` which will allow us to make efficient Firebase [array membership queries](https://firebase.google.com/docs/firestore/query-data/queries#array_membership) ## ⚙️ Release Notes * Added a function `constructTimeIndex` in a new `PrismaStandard+TimeIndex.swift` that computes the necessary time fields ## 📚 Documentation N/A ## ✅ Testing N/A ## 📝 Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/CS342/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/CS342/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/CS342/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/CS342/.github/blob/main/CONTRIBUTING.md).
- Loading branch information
Showing
3 changed files
with
135 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
// | ||
// This source file is part of the Stanford Prisma Application based on the Stanford Spezi Template Application project | ||
// | ||
// SPDX-FileCopyrightText: 2023 Stanford University | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
// Created by Matthew Jörke on 2/28/24. | ||
// | ||
|
||
import Foundation | ||
|
||
func constructTimeIndex(startDate: Date, endDate: Date) -> [String: Any?] { | ||
let calendar = Calendar.current | ||
let startComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second, .timeZone], from: startDate) | ||
let endComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second, .timeZone], from: endDate) | ||
let isRange = startDate != endDate | ||
|
||
var timeIndex: [String: Any?] = [ | ||
"range": isRange, | ||
"timezone": startComponents.timeZone, | ||
"datetime.start": startDate.localISOFormat(), | ||
"datetime.end": endDate.localISOFormat() | ||
] | ||
|
||
addTimeIndexComponents(&timeIndex, dateComponents: startComponents, suffix: ".start") | ||
|
||
if isRange { | ||
// only write end date and range if the sample is a range type | ||
addTimeIndexComponents(&timeIndex, dateComponents: endComponents, suffix: ".end") | ||
addTimeIndexRangeComponents(&timeIndex, startComponents: startComponents, endComponents: endComponents) | ||
} | ||
|
||
return timeIndex | ||
} | ||
|
||
func addTimeIndexComponents(_ timeIndex: inout [String: Any?], dateComponents: DateComponents, suffix: String) { | ||
timeIndex["year" + suffix] = dateComponents.year | ||
timeIndex["month" + suffix] = dateComponents.month | ||
timeIndex["day" + suffix] = dateComponents.day | ||
timeIndex["hour" + suffix] = dateComponents.hour | ||
timeIndex["minute" + suffix] = dateComponents.minute | ||
timeIndex["second" + suffix] = dateComponents.second | ||
timeIndex["dayMinute" + suffix] = calculateDayMinute(hour: dateComponents.hour, minute: dateComponents.minute) | ||
timeIndex["15minBucket" + suffix] = calculate15MinBucket(hour: dateComponents.hour, minute: dateComponents.minute) | ||
} | ||
|
||
func addTimeIndexRangeComponents(_ timeIndex: inout [String: Any?], startComponents: DateComponents, endComponents: DateComponents) { | ||
timeIndex["year.range"] = getRange( | ||
start: startComponents.year, | ||
end: endComponents.year, | ||
maxValue: Int.max | ||
) | ||
timeIndex["month.range"] = getRange( | ||
start: startComponents.month, | ||
end: endComponents.month, | ||
maxValue: 12, | ||
startValue: 1 // months are 1-indexed | ||
) | ||
timeIndex["day.range"] = getRange( | ||
start: startComponents.day, | ||
end: endComponents.day, | ||
maxValue: daysInMonth(month: startComponents.month, year: startComponents.year), | ||
startValue: 1 // days are 1-indexed | ||
) | ||
timeIndex["hour.range"] = getRange( | ||
start: startComponents.hour, | ||
end: endComponents.hour, | ||
maxValue: 23 | ||
) | ||
timeIndex["dayMinute.range"] = getRange( | ||
start: calculateDayMinute(hour: startComponents.hour, minute: startComponents.minute), | ||
end: calculateDayMinute(hour: endComponents.hour, minute: endComponents.minute), | ||
maxValue: 1439 | ||
) | ||
timeIndex["15minBucket.range"] = getRange( | ||
start: calculate15MinBucket(hour: startComponents.hour, minute: startComponents.minute), | ||
end: calculate15MinBucket(hour: endComponents.hour, minute: endComponents.minute), | ||
maxValue: 95 | ||
) | ||
|
||
// Minute and second ranges are not likely to be accurate since they often will fill the whole range. | ||
// We will also never query on individual minutes or seconds worth of data. | ||
} | ||
|
||
// swiftlint:disable discouraged_optional_collection | ||
func getRange(start: Int?, end: Int?, maxValue: Int?, startValue: Int = 0) -> [Int]? { | ||
guard let startInt = start, let endInt = end, let maxValueInt = maxValue else { | ||
return nil | ||
} | ||
|
||
if startInt <= endInt { | ||
return Array(startInt...endInt) | ||
} else { | ||
return Array(startInt...maxValueInt) + Array(startValue...endInt) | ||
} | ||
} | ||
|
||
func daysInMonth(month: Int?, year: Int?) -> Int? { | ||
let dateComponents = DateComponents(year: year, month: month) | ||
let calendar = Calendar.current | ||
guard let date = calendar.date(from: dateComponents), | ||
let range = calendar.range(of: .day, in: .month, for: date) else { | ||
return nil // Provide a default value in case of nil | ||
} | ||
return range.count | ||
} | ||
|
||
func calculateDayMinute(hour: Int?, minute: Int?) -> Int? { | ||
guard let hour = hour, let minute = minute else { | ||
return nil | ||
} | ||
return hour * 60 + minute | ||
} | ||
|
||
func calculate15MinBucket(hour: Int?, minute: Int?) -> Int? { | ||
guard let hour = hour, let minute = minute else { | ||
return nil | ||
} | ||
return hour * 4 + minute / 15 | ||
} |