Files
bulkhealth/BulkHealthApp/Import/ImportModels.swift
T

232 lines
5.8 KiB
Swift

import Foundation
import HealthKit
struct TemplateField: Identifiable, Hashable {
enum FieldType: String {
case string
case number
case date
case stringArray
}
let id: String
let title: String
let description: String
let targetExplanation: String
let targetExample: String
let targetFormat: String
let targetAllowedValues: String?
let type: FieldType
let isRequired: Bool
}
struct HealthTemplate {
let id: String
let title: String
let localizedHealthTypeName: String
let fields: [TemplateField]
}
struct ImportMapping: Equatable {
var sourceKeyByFieldID: [String: String]
func sourceKey(for fieldID: String) -> String {
sourceKeyByFieldID[fieldID, default: ""]
}
}
enum FieldFormatterType: String, CaseIterable, Identifiable {
case passthrough
case dateISO8601ToISO8601
case dateGermanDayMonthYearToISO8601
case dateGermanDayMonthYearHourMinuteToISO8601
case dateUnixSecondsToISO8601
var id: String { rawValue }
}
struct FieldFormatterConfig: Equatable {
var formatterType: FieldFormatterType
static func `default`(for fieldType: TemplateField.FieldType) -> FieldFormatterConfig {
if fieldType == .date {
return FieldFormatterConfig(formatterType: .dateISO8601ToISO8601)
}
return FieldFormatterConfig(formatterType: .passthrough)
}
}
struct FieldFormattingPreview {
let matched: Int
let total: Int
let examples: [String]
var isPartial: Bool {
total > 0 && matched < total
}
}
struct MoodwellPayload: Decodable {
let mymoods: [MoodwellEntry]
}
struct MoodwellEntry: Decodable, Identifiable {
let arrayOfPhotos: [String]
let moodType: Int
let moodUniqueIdentifier: String
let arrayOfBadEmotions: [String]
let notesString: String?
let arrayOfWeathers: [String]
let createdAt: String
let arrayOfActivities: [String]
let arrayOfGoodEmotions: [String]
var id: String { moodUniqueIdentifier }
var sourceDictionary: [String: ImportValue] {
[
"moodType": .number(Double(moodType)),
"moodUniqueIdentifier": .string(moodUniqueIdentifier),
"arrayOfBadEmotions": .stringArray(arrayOfBadEmotions),
"notesString": notesString.map(ImportValue.string) ?? .null,
"arrayOfWeathers": .stringArray(arrayOfWeathers),
"createdAt": .string(createdAt),
"arrayOfActivities": .stringArray(arrayOfActivities),
"arrayOfGoodEmotions": .stringArray(arrayOfGoodEmotions),
"combinedEmotions": .stringArray(arrayOfGoodEmotions + arrayOfBadEmotions),
"combinedAssociations": .stringArray(arrayOfActivities + arrayOfWeathers.map { "weather:\($0)" })
]
}
}
enum ImportValue: Hashable {
case string(String)
case number(Double)
case stringArray([String])
case date(Date)
case null
func asString() -> String? {
if case let .string(value) = self {
return value
}
return nil
}
func asNumber() -> Double? {
if case let .number(value) = self {
return value
}
return nil
}
func asStringArray() -> [String]? {
if case let .stringArray(value) = self {
return value
}
return nil
}
func asDate() -> Date? {
if case let .date(value) = self {
return value
}
return nil
}
func sourceString() -> String? {
switch self {
case let .string(value):
return value
case let .number(value):
if value.rounded() == value {
return String(Int(value))
}
return String(value)
case let .stringArray(value):
return value.joined(separator: ", ")
case let .date(value):
return value.formatted(date: .abbreviated, time: .shortened)
case .null:
return nil
}
}
func displayString() -> String {
sourceString() ?? "<null>"
}
}
enum ImportFieldFormatterEngine {
static func apply(sourceText: String, config: FieldFormatterConfig) -> String? {
switch config.formatterType {
case .passthrough:
return sourceText
case .dateISO8601ToISO8601:
guard let date = parseISO8601(sourceText.trimmingCharacters(in: .whitespacesAndNewlines)) else {
return nil
}
return iso8601String(from: date)
case .dateGermanDayMonthYearToISO8601:
return convertDate(sourceText, sourcePattern: "dd.MM.yyyy")
case .dateGermanDayMonthYearHourMinuteToISO8601:
return convertDate(sourceText, sourcePattern: "dd.MM.yyyy HH:mm")
case .dateUnixSecondsToISO8601:
guard let seconds = Double(sourceText.trimmingCharacters(in: .whitespacesAndNewlines)) else {
return nil
}
return iso8601String(from: Date(timeIntervalSince1970: seconds))
}
}
private static func convertDate(_ input: String, sourcePattern: String) -> String? {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.calendar = Calendar(identifier: .gregorian)
formatter.dateFormat = sourcePattern
guard let date = formatter.date(from: input.trimmingCharacters(in: .whitespacesAndNewlines)) else {
return nil
}
return iso8601String(from: date)
}
private static func iso8601String(from date: Date) -> String {
let output = ISO8601DateFormatter()
output.formatOptions = [.withInternetDateTime]
return output.string(from: date)
}
private static func parseISO8601(_ input: String) -> Date? {
let withFractional = ISO8601DateFormatter()
withFractional.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = withFractional.date(from: input) {
return date
}
let plain = ISO8601DateFormatter()
plain.formatOptions = [.withInternetDateTime]
return plain.date(from: input)
}
}
struct StateOfMindDraft: Identifiable, Hashable {
let id: String
let date: Date
let kind: HKStateOfMind.Kind
let valence: Double
let labels: [HKStateOfMind.Label]
let associations: [HKStateOfMind.Association]
let metadata: [String: String]
var summaryText: String {
"\(date.formatted(date: .abbreviated, time: .shortened))\(kind == .dailyMood ? "Daily mood" : "Momentary emotion") • valence \(String(format: "%.2f", valence))"
}
}
struct DryRunResult {
let drafts: [StateOfMindDraft]
let warnings: [String]
let errors: [String]
}