232 lines
5.8 KiB
Swift
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]
|
|
}
|