klio 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,410 @@
1
+ const { getAstrologicalData, calculatePlanetAspects } = require('../astrology/astrologyService');
2
+ const { extractStepData, extractHRVData, extractSleepData, extractSleepGoal } = require('./healthService');
3
+
4
+ // Funktion zur Analyse von Schritten nach Planeten-Zeichen
5
+ async function analyzeStepsByPlanetSign(planetName, healthData, timeLimitDays = null) {
6
+ let stepRecords = extractStepData(healthData);
7
+
8
+ if (stepRecords.length === 0) {
9
+ console.log('Keine Schrittzahldaten zum Analysieren gefunden.');
10
+ return;
11
+ }
12
+
13
+ // Filtere nach Zeitraumbeschränkung, falls angegeben
14
+ if (timeLimitDays) {
15
+ const timeLimitMatch = timeLimitDays.match(/^(\d+)d$/);
16
+ if (timeLimitMatch) {
17
+ const days = parseInt(timeLimitMatch[1]);
18
+ const cutoffDate = new Date();
19
+ cutoffDate.setDate(cutoffDate.getDate() - days);
20
+
21
+ const originalCount = stepRecords.length;
22
+ stepRecords = stepRecords.filter(record => record.date >= cutoffDate);
23
+
24
+ if (stepRecords.length === 0) {
25
+ const originalRecords = extractStepData(healthData); // Hole Originaldaten für ältesten Eintrag
26
+ const oldestDate = originalRecords.length > 0 ? new Date(Math.min(...originalRecords.map(r => r.date.getTime()))).toLocaleDateString() : 'Keine Daten';
27
+ console.log(`⚠️ Keine Schrittaufzeichnungen in den letzten ${days} Tagen gefunden.`);
28
+ console.log(` Verfügbare Daten: ${originalCount} Aufzeichnungen (älteste: ${oldestDate})`);
29
+ console.log(` Tipp: Versuchen Sie einen längeren Zeitraum (z.B. --t 30d oder --t 90d)`);
30
+ return;
31
+ }
32
+
33
+ console.log(`Analysiere ${stepRecords.length} Schrittaufzeichnungen (letzte ${days} Tage)...`);
34
+ } else {
35
+ console.log(`⚠️ Ungültiges Zeitlimit-Format: ${timeLimitDays}. Verwende alle Daten.`);
36
+ console.log(`Analysiere ${stepRecords.length} Schrittaufzeichnungen...`);
37
+ }
38
+ } else {
39
+ console.log(`Analysiere ${stepRecords.length} Schrittaufzeichnungen...`);
40
+ }
41
+
42
+ // Datenqualitätsprüfung
43
+ const totalSteps = stepRecords.reduce((sum, r) => sum + r.steps, 0);
44
+ const avgSteps = totalSteps / stepRecords.length;
45
+ const lowStepRecords = stepRecords.filter(r => r.steps < 1000).length;
46
+ const lowStepPercentage = (lowStepRecords / stepRecords.length * 100).toFixed(1);
47
+
48
+ console.log(`Durchschnittliche Schritte pro Tag: ${avgSteps.toFixed(0)}`);
49
+ console.log(`Tage mit < 1000 Schritten: ${lowStepRecords} (${lowStepPercentage}%)`);
50
+
51
+ // Gruppiere Schritte nach Planeten-Zeichen
52
+ const stepsByPlanetSign = {};
53
+ const planetSignStats = {};
54
+
55
+ for (const record of stepRecords) {
56
+ const dateComponents = {
57
+ year: record.date.getFullYear(),
58
+ month: record.date.getMonth() + 1,
59
+ day: record.date.getDate(),
60
+ hour: record.date.getHours(),
61
+ minute: record.date.getMinutes()
62
+ };
63
+
64
+ // Berechne Planetenposition für dieses Datum
65
+ const planetData = getAstrologicalData(planetName, dateComponents);
66
+ const planetSign = planetData.sign;
67
+
68
+ // Gruppiere nach Planeten-Zeichen
69
+ if (!stepsByPlanetSign[planetSign]) {
70
+ stepsByPlanetSign[planetSign] = [];
71
+ }
72
+ stepsByPlanetSign[planetSign].push(record.steps);
73
+
74
+ // Statistik pro Planeten-Zeichen
75
+ if (!planetSignStats[planetSign]) {
76
+ planetSignStats[planetSign] = {
77
+ count: 0,
78
+ totalSteps: 0,
79
+ minSteps: Infinity,
80
+ maxSteps: 0,
81
+ lowStepDays: 0 // Tage mit < 1000 Schritten
82
+ };
83
+ }
84
+
85
+ planetSignStats[planetSign].count++;
86
+ planetSignStats[planetSign].totalSteps += record.steps;
87
+ planetSignStats[planetSign].minSteps = Math.min(planetSignStats[planetSign].minSteps, record.steps);
88
+ planetSignStats[planetSign].maxSteps = Math.max(planetSignStats[planetSign].maxSteps, record.steps);
89
+
90
+ if (record.steps < 1000) {
91
+ planetSignStats[planetSign].lowStepDays++;
92
+ }
93
+ }
94
+
95
+ // Berechne Durchschnittswerte
96
+ console.log(`\nSchrittstatistik nach ${planetName.charAt(0).toUpperCase() + planetName.slice(1)}zeichen:`);
97
+ console.log('Zeichen | Durchschnitt | Gesamt | Tage | Rang | Aktuell');
98
+ console.log('-------|-------------|--------|-----|-----|--------');
99
+
100
+ const sortedSigns = Object.entries(planetSignStats)
101
+ .sort((a, b) => b[1].totalSteps / b[1].count - a[1].totalSteps / a[1].count); // Absteigend sortieren
102
+
103
+ // Berechne aktuelles Planeten-Zeichen für die Spalte
104
+ const currentPlanetData = getAstrologicalData(planetName);
105
+
106
+ for (let i = 0; i < sortedSigns.length; i++) {
107
+ const [sign, stats] = sortedSigns[i];
108
+ const avgSteps = (stats.totalSteps / stats.count).toFixed(0);
109
+ const totalSteps = stats.totalSteps.toFixed(0);
110
+ const rank = i + 1; // Rang (1 = meisten Schritte)
111
+ const isCurrent = sign === currentPlanetData.sign ? '✓' : '';
112
+
113
+ console.log(`${sign.padEnd(10)} | ${avgSteps.padStart(11)} | ${totalSteps.padStart(6)} | ${stats.count.toString().padStart(3)} | ${rank.toString().padStart(3)} | ${isCurrent}`);
114
+ }
115
+
116
+ // Zeige Analyseergebnis
117
+ console.log(`\n${planetName.charAt(0).toUpperCase() + planetName.slice(1)}zeichen mit den meisten Schritten: ${sortedSigns[0][0]} (∅ ${(sortedSigns[0][1].totalSteps / sortedSigns[0][1].count).toFixed(0)} Schritte, Rang 1)`);
118
+ console.log(`${planetName.charAt(0).toUpperCase() + planetName.slice(1)}zeichen mit den wenigsten Schritten: ${sortedSigns[sortedSigns.length - 1][0]} (∅ ${(sortedSigns[sortedSigns.length - 1][1].totalSteps / sortedSigns[sortedSigns.length - 1][1].count).toFixed(0)} Schritte, Rang ${sortedSigns.length})`)
119
+ }
120
+
121
+ // Funktion zur Analyse von Stress basierend auf HRV und Planeten-Zeichen
122
+ async function analyzeStressByPlanetAspects(planetName, healthData, timeLimitDays = null) {
123
+ let hrvRecords = extractHRVData(healthData);
124
+
125
+ if (hrvRecords.length === 0) {
126
+ console.log('Keine HRV-Daten zum Analysieren gefunden.');
127
+ console.log('Hinweis: HRV (Heart Rate Variability) Daten sind für Stressanalyse erforderlich.');
128
+ return;
129
+ }
130
+
131
+ // Filtere nach Zeitraumbeschränkung, falls angegeben
132
+ if (timeLimitDays) {
133
+ const timeLimitMatch = timeLimitDays.match(/^(\d+)d$/);
134
+ if (timeLimitMatch) {
135
+ const days = parseInt(timeLimitMatch[1]);
136
+ const cutoffDate = new Date();
137
+ cutoffDate.setDate(cutoffDate.getDate() - days);
138
+
139
+ const originalCount = hrvRecords.length;
140
+ hrvRecords = hrvRecords.filter(record => record.date >= cutoffDate);
141
+
142
+ if (hrvRecords.length === 0) {
143
+ const originalRecords = extractHRVData(healthData);
144
+ const oldestDate = originalRecords.length > 0 ? new Date(Math.min(...originalRecords.map(r => r.date.getTime()))).toLocaleDateString() : 'Keine Daten';
145
+ console.log(`⚠️ Keine HRV-Aufzeichnungen in den letzten ${days} Tagen gefunden.`);
146
+ console.log(` Verfügbare Daten: ${originalCount} Aufzeichnungen (älteste: ${oldestDate})`);
147
+ console.log(` Tipp: Versuchen Sie einen längeren Zeitraum (z.B. --t 30d oder --t 90d)`);
148
+ return;
149
+ }
150
+
151
+ console.log(`Analysiere ${hrvRecords.length} HRV-Aufzeichnungen (letzte ${days} Tage)...`);
152
+ } else {
153
+ console.log(`⚠️ Ungültiges Zeitlimit-Format: ${timeLimitDays}. Verwende alle Daten.`);
154
+ console.log(`Analysiere ${hrvRecords.length} HRV-Aufzeichnungen...`);
155
+ }
156
+ } else {
157
+ console.log(`Analysiere ${hrvRecords.length} HRV-Aufzeichnungen...`);
158
+ }
159
+
160
+ // Berechne Stresslevel basierend auf HRV (niedrige HRV = hoher Stress)
161
+ // Wir berechnen einen individuellen Basiswert aus dem Durchschnitt der Daten
162
+ const avgHrvOverall = hrvRecords.reduce((sum, r) => sum + r.hrv, 0) / hrvRecords.length;
163
+ const maxHrvOverall = Math.max(...hrvRecords.map(r => r.hrv));
164
+
165
+ // Stresslevel = 100% * (1 - (HRV - minHRV) / (maxHRV - minHRV))
166
+ // Aber das würde den "relativen" Charakter vielleicht zu sehr strecken.
167
+ // Wir bleiben bei der Relation zum Maximum, aber berechnen den Stress
168
+ // relativ zum Bereich [minHRV, maxHRV]
169
+ const minHrvOverall = Math.min(...hrvRecords.map(r => r.hrv));
170
+ const hrvRange = maxHrvOverall - minHrvOverall;
171
+
172
+ const stressRecords = hrvRecords.map(record => {
173
+ // Wenn alle Werte gleich sind, ist hrvRange 0
174
+ let stressLevel = 0;
175
+ if (hrvRange > 0) {
176
+ stressLevel = 100 * (1 - (record.hrv - minHrvOverall) / hrvRange);
177
+ }
178
+ return {
179
+ date: record.date,
180
+ hrv: record.hrv,
181
+ stressLevel: stressLevel,
182
+ count: record.count
183
+ };
184
+ });
185
+
186
+ // Gruppiere nach Planeten-Zeichen und berechne durchschnittlichen Stress
187
+ const stressByPlanetSign = {};
188
+ const planetSignStats = {};
189
+
190
+ for (const record of stressRecords) {
191
+ const dateComponents = {
192
+ year: record.date.getFullYear(),
193
+ month: record.date.getMonth() + 1,
194
+ day: record.date.getDate(),
195
+ hour: record.date.getHours(),
196
+ minute: record.date.getMinutes()
197
+ };
198
+
199
+ // Berechne Planetenposition für dieses Datum
200
+ const planetData = getAstrologicalData(planetName, dateComponents);
201
+ const planetSign = planetData.sign;
202
+
203
+ // Gruppiere nach Planeten-Zeichen
204
+ if (!stressByPlanetSign[planetSign]) {
205
+ stressByPlanetSign[planetSign] = [];
206
+ }
207
+ stressByPlanetSign[planetSign].push(record.stressLevel);
208
+
209
+ // Statistik pro Planeten-Zeichen
210
+ if (!planetSignStats[planetSign]) {
211
+ planetSignStats[planetSign] = {
212
+ count: 0,
213
+ totalStress: 0,
214
+ minStress: Infinity,
215
+ maxStress: 0,
216
+ avgHRV: 0,
217
+ hrvValues: []
218
+ };
219
+ }
220
+
221
+ planetSignStats[planetSign].count++;
222
+ planetSignStats[planetSign].totalStress += record.stressLevel;
223
+ planetSignStats[planetSign].minStress = Math.min(planetSignStats[planetSign].minStress, record.stressLevel);
224
+ planetSignStats[planetSign].maxStress = Math.max(planetSignStats[planetSign].maxStress, record.stressLevel);
225
+ planetSignStats[planetSign].hrvValues.push(record.hrv);
226
+ }
227
+
228
+ // Berechne Durchschnittswerte
229
+ console.log(`\nStressstatistik nach ${planetName.charAt(0).toUpperCase() + planetName.slice(1)}zeichen:`);
230
+ console.log('Zeichen | Ø Stress | Ø HRV | Tage | Rang | Aktuell');
231
+ console.log('-------|----------|-------|-----|-----|--------');
232
+
233
+ const sortedSigns = Object.entries(planetSignStats)
234
+ .sort((a, b) => (b[1].totalStress / b[1].count) - (a[1].totalStress / a[1].count)); // Absteigend nach Stress
235
+
236
+ // Berechne aktuelles Planeten-Zeichen für die Spalte
237
+ const currentPlanetData = getAstrologicalData(planetName);
238
+
239
+ for (let i = 0; i < sortedSigns.length; i++) {
240
+ const [sign, stats] = sortedSigns[i];
241
+ const avgStress = (stats.totalStress / stats.count).toFixed(1);
242
+ const avgHRV = (stats.hrvValues.reduce((sum, val) => sum + val, 0) / stats.hrvValues.length).toFixed(1);
243
+ const rank = i + 1; // Rang (1 = höchster Stress)
244
+ const isCurrent = sign === currentPlanetData.sign ? '✓' : '';
245
+
246
+ console.log(`${sign.padEnd(10)} | ${avgStress.padStart(8)}% | ${avgHRV.padStart(5)}ms | ${stats.count.toString().padStart(3)} | ${rank.toString().padStart(3)} | ${isCurrent}`);
247
+ }
248
+
249
+ if (planetSignStats[currentPlanetData.sign]) {
250
+ const currentAvgStress = (planetSignStats[currentPlanetData.sign].totalStress / planetSignStats[currentPlanetData.sign].count).toFixed(1);
251
+ const currentAvgHRV = (planetSignStats[currentPlanetData.sign].hrvValues.reduce((sum, val) => sum + val, 0) / planetSignStats[currentPlanetData.sign].hrvValues.length).toFixed(1);
252
+ // Zeige Empfehlung basierend auf aktuellem Stresslevel
253
+ if (currentAvgStress > 70) {
254
+ console.log('\nAktuell könnte eine Phase mit erhöhtem Stress sein.');
255
+ } else if (currentAvgStress > 50) {
256
+ console.log('\nAktuell könnte eine Phase mit moderatem Stress sein.');
257
+ } else {
258
+ console.log('\nAktuell könnte eine Phase mit niedrigem Stress sein.');
259
+ }
260
+ } else {
261
+ console.log(` Keine Stressdaten für das aktuelle ${planetName.charAt(0).toUpperCase() + planetName.slice(1)}zeichen verfügbar.`);
262
+ }
263
+ }
264
+
265
+ // Funktion zur Analyse von Planetenaspekten bei Schlafmangel
266
+ async function analyzePlanetAspectsForSleep(planetName, healthData, sleepGoal = 8) {
267
+ const sleepRecords = extractSleepData(healthData);
268
+
269
+ if (sleepRecords.length === 0) {
270
+ console.log('Keine Schlafdaten zum Analysieren gefunden.');
271
+ return;
272
+ }
273
+
274
+ console.log(`Schlafziel: ${sleepGoal} Stunden`);
275
+
276
+ // Filtere ungültige Datensätze (0 Stunden Schlaf) heraus
277
+ const validSleepRecords = sleepRecords.filter(record => record.duration > 0.1); // Mindestens 6 Minuten
278
+
279
+ // Filtere Nächte mit Schlafmangel (weniger als Ziel)
280
+ const insufficientSleepNights = validSleepRecords.filter(record =>
281
+ record.duration < sleepGoal
282
+ );
283
+
284
+ console.log(`\nNächte mit Schlafmangel (${insufficientSleepNights.length} von ${validSleepRecords.length}):`);
285
+
286
+ // Zeige detaillierte Schlafphasen-Statistik
287
+ console.log('\nSchlafphasen-Statistik (alle Nächte):');
288
+ const phaseStats = {
289
+ 'AsleepCore': 0,
290
+ 'AsleepDeep': 0,
291
+ 'AsleepREM': 0,
292
+ 'Awake': 0
293
+ };
294
+
295
+ let totalSleepTime = 0;
296
+ let totalAwakeTime = 0;
297
+
298
+ for (const record of validSleepRecords) {
299
+ if (record.phases) {
300
+ for (const phase of record.phases) {
301
+ const phaseType = phase.type.replace('HKCategoryValueSleepAnalysis', '');
302
+ phaseStats[phaseType] += phase.duration;
303
+
304
+ if (phaseType === 'Awake') {
305
+ totalAwakeTime += phase.duration;
306
+ } else {
307
+ totalSleepTime += phase.duration;
308
+ }
309
+ }
310
+ }
311
+ }
312
+
313
+ console.log('Durchschnittliche Schlafphasenverteilung:');
314
+ console.log('Tiefschlaf (Core):', phaseStats.AsleepCore.toFixed(2), 'Stunden');
315
+ console.log('Traumschlaf (REM):', phaseStats.AsleepREM.toFixed(2), 'Stunden');
316
+ console.log('Festschlaf (Deep):', phaseStats.AsleepDeep.toFixed(2), 'Stunden');
317
+ console.log('Wachphasen:', phaseStats.Awake.toFixed(2), 'Stunden');
318
+ console.log('Gesamte Schlafzeit:', totalSleepTime.toFixed(2), 'Stunden');
319
+ // Wenn keine Nächte mit Schlafmangel gefunden wurden
320
+ if (insufficientSleepNights.length === 0) {
321
+ if (validSleepRecords.length === 0) {
322
+ console.log('Keine gültigen Schlafdaten gefunden (alle Einträge hatten ≤ 0.1h Schlaf).');
323
+ return;
324
+ }
325
+
326
+ console.log('Glückwunsch! Keine Nächte mit Schlafmangel gefunden.');
327
+ console.log('\nGesamt-Schlafstatistik:');
328
+ console.log('Durchschnittliche Schlafdauer:', (validSleepRecords.reduce((sum, record) => sum + record.duration, 0) / validSleepRecords.length).toFixed(2), 'Stunden');
329
+ console.log('Längste Schlafperiode:', Math.max(...validSleepRecords.map(r => r.duration)).toFixed(2), 'Stunden');
330
+ console.log('Kürzeste Schlafperiode:', Math.min(...validSleepRecords.map(r => r.duration)).toFixed(2), 'Stunden');
331
+
332
+ // Analysiere trotzdem einige Planetenaspekte für allgemeine Muster
333
+ console.log(`\nAllgemeine ${planetName.charAt(0).toUpperCase() + planetName.slice(1)}aspekt-Analyse (alle Nächte):`);
334
+ const generalAspectStats = {};
335
+
336
+ for (const night of sleepRecords.slice(0, Math.min(10, sleepRecords.length))) {
337
+ const midSleepTime = new Date(night.startDate.getTime() +
338
+ (night.endDate.getTime() - night.startDate.getTime()) / 2);
339
+ const planetAspects = calculatePlanetAspects(planetName, midSleepTime);
340
+
341
+ for (const aspect of planetAspects) {
342
+ const aspectKey = `${aspect.type} mit ${aspect.planet}`;
343
+ generalAspectStats[aspectKey] = (generalAspectStats[aspectKey] || 0) + 1;
344
+ }
345
+ }
346
+
347
+ if (Object.keys(generalAspectStats).length > 0) {
348
+ console.log(`Häufige ${planetName.charAt(0).toUpperCase() + planetName.slice(1)}aspekte in den analysierten Nächten:`);
349
+ const sortedGeneralAspects = Object.entries(generalAspectStats)
350
+ .sort((a, b) => b[1] - a[1])
351
+ .slice(0, 5);
352
+
353
+ for (const [aspect, count] of sortedGeneralAspects) {
354
+ console.log(` - ${aspect}: ${count}x`);
355
+ }
356
+ }
357
+
358
+ return;
359
+ }
360
+
361
+ // Analysiere Planetenaspekte für jede Nacht mit Schlafmangel
362
+ const aspectStats = {};
363
+
364
+ for (const night of insufficientSleepNights) {
365
+ const midSleepTime = new Date(night.startDate.getTime() +
366
+ (night.endDate.getTime() - night.startDate.getTime()) / 2);
367
+ const planetAspects = calculatePlanetAspects(planetName, midSleepTime);
368
+
369
+ // Zähle Aspekte
370
+ for (const aspect of planetAspects) {
371
+ const aspectKey = `${aspect.type} mit ${aspect.planet}`;
372
+ aspectStats[aspectKey] = (aspectStats[aspectKey] || 0) + 1;
373
+ }
374
+ }
375
+
376
+ // Zeige Statistik an (Top 10 Planetenaspekte)
377
+ console.log(`\nHäufigste ${planetName.charAt(0).toUpperCase() + planetName.slice(1)}aspekte bei Schlafmangel (Top 10):`);
378
+ console.log('Aspekt | Häufigkeit | Prozent | Heute aktiv');
379
+ console.log('-------|-----------|--------|------------');
380
+
381
+ const totalNights = insufficientSleepNights.length;
382
+ const sortedAspects = Object.entries(aspectStats)
383
+ .sort((a, b) => b[1] - a[1])
384
+ .slice(0, 10); // Begrenzung auf Top 10
385
+
386
+ // Berechne aktuelle Planetenaspekte für den Vergleich
387
+ const currentPlanetAspects = calculatePlanetAspects(planetName, new Date());
388
+ const currentAspectSet = new Set(currentPlanetAspects.map(a => `${a.type} mit ${a.planet}`));
389
+
390
+ for (const [aspect, count] of sortedAspects) {
391
+ const percentage = totalNights > 0 ? ((count / totalNights) * 100).toFixed(1) : 0;
392
+ const isActiveToday = currentAspectSet.has(aspect) ? '✓' : '✗';
393
+ console.log(`${aspect.padEnd(25)} | ${count.toString().padStart(2)} | ${percentage}% | ${isActiveToday}`);
394
+ }
395
+
396
+ if (sortedAspects.length === 0) {
397
+ console.log(`Keine ${planetName.charAt(0).toUpperCase() + planetName.slice(1)}aspekte in Nächten mit Schlafmangel gefunden.`);
398
+ }
399
+
400
+ // Zeige Hinweis über verfügbare Beispielnächte, aber zeige keine Details
401
+ if (insufficientSleepNights.length > 0) {
402
+ console.log(`\n${insufficientSleepNights.length} Nächte mit Schlafmangel gefunden.`);
403
+ }
404
+ }
405
+
406
+ module.exports = {
407
+ analyzeStepsByPlanetSign,
408
+ analyzeStressByPlanetAspects,
409
+ analyzePlanetAspectsForSleep
410
+ };
@@ -0,0 +1,202 @@
1
+ const fs = require('fs');
2
+ const { XMLParser } = require('fast-xml-parser');
3
+
4
+ // Funktion zum Parsen von Apple Health XML Export
5
+ function parseAppleHealthXML(filePath) {
6
+ try {
7
+ const xmlData = fs.readFileSync(filePath, 'utf8');
8
+ const parser = new XMLParser({
9
+ ignoreAttributes: false,
10
+ attributeNamePrefix: '@_',
11
+ parseAttributeValue: true,
12
+ parseNodeValue: true
13
+ });
14
+
15
+ const result = parser.parse(xmlData);
16
+ return result;
17
+ } catch (error) {
18
+ console.error('Fehler beim Parsen der Apple Health XML-Datei:', error.message);
19
+ return null;
20
+ }
21
+ }
22
+
23
+ // Funktion zum Extrahieren von Schrittzahldaten aus Apple Health Export
24
+ function extractStepData(healthData) {
25
+ if (!healthData || !healthData.HealthData || !healthData.HealthData.Record) {
26
+ console.log('Keine Schrittzahldaten in der Apple Health Export-Datei gefunden.');
27
+ return [];
28
+ }
29
+
30
+ const records = Array.isArray(healthData.HealthData.Record)
31
+ ? healthData.HealthData.Record
32
+ : [healthData.HealthData.Record];
33
+
34
+ // Filtere Schrittzahldaten (HKQuantityTypeIdentifierStepCount)
35
+ const stepRecords = records.filter(record =>
36
+ record['@_type'] === 'HKQuantityTypeIdentifierStepCount'
37
+ );
38
+
39
+ // Konsolidiere kurze Aufzeichnungen zu täglichen Summen
40
+ const dailySteps = {};
41
+
42
+ for (const record of stepRecords) {
43
+ const date = new Date(record['@_startDate']);
44
+ const dayKey = date.toISOString().split('T')[0]; // YYYY-MM-DD
45
+
46
+ if (!dailySteps[dayKey]) {
47
+ dailySteps[dayKey] = 0;
48
+ }
49
+
50
+ dailySteps[dayKey] += parseInt(record['@_value']);
51
+ }
52
+
53
+ // Konvertiere zu Array von täglichen Schrittzahldaten
54
+ return Object.entries(dailySteps).map(([dateStr, steps]) => ({
55
+ date: new Date(dateStr),
56
+ steps: steps
57
+ }));
58
+ }
59
+
60
+ // Funktion zum Extrahieren von HRV-Daten aus Apple Health Export
61
+ function extractHRVData(healthData) {
62
+ if (!healthData || !healthData.HealthData || !healthData.HealthData.Record) {
63
+ console.log('Keine HRV-Daten in der Apple Health Export-Datei gefunden.');
64
+ return [];
65
+ }
66
+
67
+ const records = Array.isArray(healthData.HealthData.Record)
68
+ ? healthData.HealthData.Record
69
+ : [healthData.HealthData.Record];
70
+
71
+ // Filtere HRV-Daten (HKQuantityTypeIdentifierHeartRateVariabilitySDNN)
72
+ const hrvRecords = records.filter(record =>
73
+ record['@_type'] === 'HKQuantityTypeIdentifierHeartRateVariabilitySDNN'
74
+ );
75
+
76
+ // Konsolidiere zu täglichen Durchschnittswerten
77
+ const dailyHRV = {};
78
+
79
+ for (const record of hrvRecords) {
80
+ const date = new Date(record['@_startDate']);
81
+ const dayKey = date.toISOString().split('T')[0]; // YYYY-MM-DD
82
+
83
+ if (!dailyHRV[dayKey]) {
84
+ dailyHRV[dayKey] = [];
85
+ }
86
+
87
+ dailyHRV[dayKey].push(parseFloat(record['@_value']));
88
+ }
89
+
90
+ // Berechne tägliche Durchschnittswerte
91
+ return Object.entries(dailyHRV).map(([dateStr, values]) => ({
92
+ date: new Date(dateStr),
93
+ hrv: values.reduce((sum, val) => sum + val, 0) / values.length,
94
+ count: values.length
95
+ }));
96
+ }
97
+
98
+ // Funktion zum Extrahieren von Schlafdaten aus Apple Health Export
99
+ function extractSleepData(healthData) {
100
+ if (!healthData || !healthData.HealthData || !healthData.HealthData.Record) {
101
+ console.log('Keine Schlafdaten in der Apple Health Export-Datei gefunden.');
102
+ return [];
103
+ }
104
+
105
+ const records = Array.isArray(healthData.HealthData.Record)
106
+ ? healthData.HealthData.Record
107
+ : [healthData.HealthData.Record];
108
+
109
+ // Filtere Schlafdaten (HKCategoryTypeIdentifierSleepAnalysis)
110
+ const sleepRecords = records.filter(record =>
111
+ record['@_type'] === 'HKCategoryTypeIdentifierSleepAnalysis' &&
112
+ record['@_value'] !== 'HKCategoryValueSleepAnalysisInBed'
113
+ );
114
+
115
+ // Konsolidiere Schlafphasen zu Schlafperioden
116
+ const sleepPeriods = [];
117
+ let currentPeriod = null;
118
+
119
+ // Sortiere Records nach Startdatum
120
+ sleepRecords.sort((a, b) => new Date(a['@_startDate']) - new Date(b['@_startDate']));
121
+
122
+ for (const record of sleepRecords) {
123
+ const startDate = new Date(record['@_startDate']);
124
+ const endDate = new Date(record['@_endDate']);
125
+ const value = record['@_value'];
126
+
127
+ // Ignoriere kurze Awake-Phasen (kürzer als 5 Minuten)
128
+ if (value === 'HKCategoryValueSleepAnalysisAwake' && (endDate - startDate) < 300000) {
129
+ continue;
130
+ }
131
+
132
+ // Beginne neue Schlafperiode oder erweitere bestehende
133
+ if (!currentPeriod || startDate > new Date(currentPeriod.endDate.getTime() + 30 * 60000)) {
134
+ // Neue Schlafperiode (mehr als 30 Minuten Pause = neue Periode)
135
+ if (currentPeriod) {
136
+ sleepPeriods.push(currentPeriod);
137
+ }
138
+ currentPeriod = {
139
+ startDate: startDate,
140
+ endDate: endDate,
141
+ totalSleepDuration: 0,
142
+ phases: []
143
+ };
144
+ }
145
+
146
+ // Füge Phase hinzu und berechne Schlafzeit
147
+ currentPeriod.endDate = endDate;
148
+ const duration = (endDate - startDate) / (1000 * 60 * 60); // in Stunden
149
+
150
+ // Nur Schlafphasen zählen (nicht Awake)
151
+ if (value !== 'HKCategoryValueSleepAnalysisAwake') {
152
+ currentPeriod.totalSleepDuration += duration;
153
+ }
154
+
155
+ currentPeriod.phases.push({
156
+ type: value,
157
+ startDate: startDate,
158
+ endDate: endDate,
159
+ duration: duration
160
+ });
161
+ }
162
+
163
+ // Füge die letzte Periode hinzu
164
+ if (currentPeriod) {
165
+ sleepPeriods.push(currentPeriod);
166
+ }
167
+
168
+ return sleepPeriods.map(period => ({
169
+ startDate: period.startDate,
170
+ endDate: period.endDate,
171
+ value: 'HKCategoryValueSleepAnalysisAsleep', // Konsolidierter Schlaf
172
+ duration: period.totalSleepDuration,
173
+ phases: period.phases
174
+ }));
175
+ }
176
+
177
+ // Funktion zum Extrahieren von Schlafzieldaten
178
+ function extractSleepGoal(healthData) {
179
+ if (!healthData || !healthData.HealthData || !healthData.HealthData.Record) {
180
+ return 8; // Standard-Schlafziel von 8 Stunden
181
+ }
182
+
183
+ const records = Array.isArray(healthData.HealthData.Record)
184
+ ? healthData.HealthData.Record
185
+ : [healthData.HealthData.Record];
186
+
187
+ // Suche nach Schlafziel-Einträgen
188
+ const sleepGoalRecord = records.find(record =>
189
+ record['@_type'] === 'HKCategoryTypeIdentifierSleepAnalysis' &&
190
+ record['@_value'] === 'HKCategoryValueSleepAnalysisInBed'
191
+ );
192
+
193
+ return sleepGoalRecord ? 8 : 8; // Standardmäßig 8 Stunden
194
+ }
195
+
196
+ module.exports = {
197
+ parseAppleHealthXML,
198
+ extractStepData,
199
+ extractHRVData,
200
+ extractSleepData,
201
+ extractSleepGoal
202
+ };
package/src/main.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Main entry point for the application
4
+ require('./cli/cli');
@@ -0,0 +1,46 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ // Funktion zum Auslesen des Erstellungsdatums einer Datei
5
+ function getFileCreationDate(filePath) {
6
+ try {
7
+ const stats = fs.statSync(filePath);
8
+ // Verwende das Erstellungsdatum (birthtime) oder das Änderungsdatum als Fallback
9
+ return stats.birthtime || stats.mtime;
10
+ } catch (error) {
11
+ console.error(`Fehler beim Lesen der Datei ${filePath}:`, error.message);
12
+ return null;
13
+ }
14
+ }
15
+
16
+ // Funktion zum Parsen von Datum in Jahr, Monat, Tag, Stunde, Minute
17
+ function parseDateToComponents(date) {
18
+ return {
19
+ year: date.getFullYear(),
20
+ month: date.getMonth() + 1, // Monate sind 0-basiert in JavaScript
21
+ day: date.getDate(),
22
+ hour: date.getHours(),
23
+ minute: date.getMinutes()
24
+ };
25
+ }
26
+
27
+ // Funktion zum Lesen aller Dateien in einem Ordner
28
+ function readFilesInFolder(folderPath) {
29
+ try {
30
+ return fs.readdirSync(folderPath)
31
+ .filter(file => {
32
+ const fullPath = path.join(folderPath, file);
33
+ return fs.statSync(fullPath).isFile();
34
+ })
35
+ .map(file => path.join(folderPath, file));
36
+ } catch (error) {
37
+ console.error(`Fehler beim Lesen des Ordners ${folderPath}:`, error.message);
38
+ return [];
39
+ }
40
+ }
41
+
42
+ module.exports = {
43
+ getFileCreationDate,
44
+ parseDateToComponents,
45
+ readFilesInFolder
46
+ };