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.
- package/README.md +291 -0
- package/changelog.md +0 -0
- package/ephe/seas_18.se1 +0 -0
- package/ephe/seasnam.txt +546077 -0
- package/ephe/sefstars.txt +1580 -0
- package/ephe/seleapsec.txt +6 -0
- package/ephe/semo_18.se1 +0 -0
- package/ephe/seorbel.txt +95 -0
- package/ephe/sepl_18.se1 +0 -0
- package/index.js +4 -0
- package/package.json +28 -0
- package/src/astrology/astrologyConstants.js +55 -0
- package/src/astrology/astrologyService.js +990 -0
- package/src/astrology/retrogradeService.js +267 -0
- package/src/cli/cli.js +1062 -0
- package/src/config/configService.js +401 -0
- package/src/health/fileAnalysis.js +141 -0
- package/src/health/healthAnalysis.js +410 -0
- package/src/health/healthService.js +202 -0
- package/src/main.js +4 -0
- package/src/utils/fileUtils.js +46 -0
- package/src/utils/markdownFormatter.js +161 -0
|
@@ -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,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
|
+
};
|