klio 1.4.9 → 1.5.0

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 CHANGED
@@ -98,6 +98,7 @@ It's possible to analyze a csv with a column of either ISO date time or unix tim
98
98
  - **Show house and sign distribution of the datetime column**: `klio [planet] --csv <file-path>`
99
99
  - **Show aspect type distribution between two planets:** `klio [planet1] [planet2] --csv <file-path> --a`
100
100
  - **Filter CSV data by column value:** `klio [planet] --csv <file-path> --filter "column:value"` (e.g., `--filter "Item:coffee"`)
101
+ - **Filter CSV data by multiple conditions:** `klio [planet] --csv <file-path> --filter "column1:value1,column2:value2"` (e.g., `--filter "FTR:H,HomeTeam:Liverpool"`)
101
102
  - **Creating a bar chart**: Create a bar chart and save the image to the downloads' folder. The image shows the aspect distribution of your csv datetime values: `klio moon sun --csv /home/user/Downloads/coffee.csv --filter "Item:cookie" --a --title "Eaten cookies during sun-moon aspects"`
102
103
 
103
104
  - The command also returns a Chi-Square.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klio",
3
- "version": "1.4.9",
3
+ "version": "1.5.0",
4
4
  "description": "A CLI for astrological calculations",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -24,6 +24,12 @@ const elements = [
24
24
  'Air', 'Water', 'Fire', 'Earth', 'Air', 'Water'
25
25
  ];
26
26
 
27
+ // Sign types (Cardinal, Fixed, Mutable)
28
+ const signTypes = [
29
+ 'Cardinal', 'Fixed', 'Mutable', 'Cardinal', 'Fixed', 'Mutable',
30
+ 'Cardinal', 'Fixed', 'Mutable', 'Cardinal', 'Fixed', 'Mutable'
31
+ ];
32
+
27
33
  // Decans
28
34
  const decans = [
29
35
  '1st Decan', '2nd Decan', '3rd Decan',
@@ -50,6 +56,7 @@ module.exports = {
50
56
  planets,
51
57
  signs,
52
58
  elements,
59
+ signTypes,
53
60
  decans,
54
61
  dignities
55
62
  };
@@ -3,7 +3,7 @@ const fs = require('fs');
3
3
  const moment = require('moment-timezone');
4
4
  const csvParser = require('csv-parser');
5
5
  const { parse } = require('csv-parse/sync');
6
- const { planets, signs, elements, decans, dignities } = require('./astrologyConstants');
6
+ const { planets, signs, elements, signTypes, decans, dignities } = require('./astrologyConstants');
7
7
  const { loadConfig } = require('../config/configService');
8
8
  const path = require('path');
9
9
 
@@ -454,12 +454,18 @@ function getAstrologicalData(planetName, customDate = null) {
454
454
  };
455
455
  }
456
456
 
457
+
458
+
457
459
  // Function to identify critical planets
458
460
  function getCriticalPlanets(customDate = null) {
459
461
  const criticalPlanets = [];
460
462
 
461
- // Critical degrees: 0°, 13°, 26° (cardinal degrees) and 29° (anaretic degree)
462
- const criticalDegrees = [0, 13, 26, 29];
463
+ // Critical degrees by sign type:
464
+ // Cardinal signs (Aries, Cancer, Libra, Capricorn): 0°, 13°, 26°
465
+ // Fixed signs (Taurus, Leo, Scorpio, Aquarius): 8-9°, 21-22°
466
+ // Mutable signs (Gemini, Virgo, Sagittarius, Pisces): 4°, 17°
467
+ // Anaretic degree (all signs): 29°
468
+
463
469
  const orb = 1; // Tolerance of 1 degree
464
470
 
465
471
  // Use the specified date or current time
@@ -502,19 +508,55 @@ function getCriticalPlanets(customDate = null) {
502
508
  const degreeInSign = longitude % 30;
503
509
  const signIndex = Math.floor(longitude / 30);
504
510
  const sign = signs[signIndex];
511
+ const signType = signTypes[signIndex];
512
+
513
+ // Determine critical degrees based on sign type
514
+ let isCritical = false;
515
+ let criticalType = '';
505
516
 
506
- // Check if planet is on a critical degree
507
- const isCritical = criticalDegrees.some(criticalDegree => {
508
- return Math.abs(degreeInSign - criticalDegree) <= orb;
509
- });
510
-
517
+ // First check for Anaretic degree (29°) - applies to all signs
518
+ if (degreeInSign >= 28.8) { // 29° with 0.2° orb for precision
519
+ isCritical = true;
520
+ criticalType = 'Anaretic (29°)';
521
+ } else if (signType === 'Cardinal') {
522
+ // Cardinal: exact degrees 0°, 13°, 26° with orb
523
+ isCritical = [0, 13, 26].some(criticalDegree => {
524
+ return Math.abs(degreeInSign - criticalDegree) <= orb;
525
+ });
526
+ criticalType = 'Cardinal (0°, 13°, 26°)';
527
+ } else if (signType === 'Fixed') {
528
+ // Fixed: ranges 8-9° and 21-22°
529
+ isCritical = (degreeInSign >= 8 && degreeInSign <= 9) || (degreeInSign >= 21 && degreeInSign <= 22);
530
+ criticalType = 'Fixed (8-9°, 21-22°)';
531
+ } else if (signType === 'Mutable') {
532
+ // Mutable: exact degrees 4°, 17° with orb
533
+ isCritical = [4, 17].some(criticalDegree => {
534
+ return Math.abs(degreeInSign - criticalDegree) <= orb;
535
+ });
536
+ criticalType = 'Mutable (4°, 17°)';
537
+ }
538
+
511
539
  if (isCritical) {
540
+ // Simple interpretation based on modality challenges
541
+ let interpretation = '';
542
+
543
+ if (criticalType === 'Cardinal (0°, 13°, 26°)') {
544
+ interpretation = 'Tendency to over-express. May push too hard or initiate prematurely.';
545
+ } else if (criticalType === 'Fixed (8-9°, 21-22°)') {
546
+ interpretation = 'Tendency to under-express. May resist change or hold on too tightly.';
547
+ } else if (criticalType === 'Mutable (4°, 17°)') {
548
+ interpretation = 'Path of ambivalence. May struggle with indecision or adapt excessively.';
549
+ } else if (criticalType === 'Anaretic (29°)') {
550
+ interpretation = 'Poised for change. At the end of a cycle, facing transition.';
551
+ }
552
+
512
553
  criticalPlanets.push({
513
554
  name,
514
555
  sign,
515
556
  degree: degreeInSign.toFixed(2),
516
557
  isCritical: true,
517
- criticalType: degreeInSign >= 28.5 ? 'Anaretic (29°)' : 'Cardinal (0°, 13°, 26°)'
558
+ criticalType: criticalType,
559
+ interpretation: interpretation
518
560
  });
519
561
  }
520
562
  }
@@ -1904,16 +1946,25 @@ function analyzeSignDistributionSignificance(signDistribution) {
1904
1946
  function parseFilterCriteria(filterString) {
1905
1947
  if (!filterString) return null;
1906
1948
 
1907
- const parts = filterString.split(':');
1908
- if (parts.length !== 2) {
1909
- console.warn('Invalid filter format. Expected "column:value"');
1910
- return null;
1949
+ // Check if multiple filters are provided (comma-separated)
1950
+ const filterConditions = filterString.split(',').map(cond => cond.trim());
1951
+
1952
+ const criteria = [];
1953
+
1954
+ for (const condition of filterConditions) {
1955
+ const parts = condition.split(':');
1956
+ if (parts.length !== 2) {
1957
+ console.warn(`Invalid filter format. Expected "column:value" but got "${condition}"`);
1958
+ return null;
1959
+ }
1960
+
1961
+ criteria.push({
1962
+ column: parts[0].trim(),
1963
+ value: parts[1].trim()
1964
+ });
1911
1965
  }
1912
1966
 
1913
- return {
1914
- column: parts[0].trim(),
1915
- value: parts[1].trim()
1916
- };
1967
+ return criteria.length === 1 ? criteria[0] : criteria;
1917
1968
  }
1918
1969
 
1919
1970
  // Helper function to apply filter to records
@@ -1926,10 +1977,21 @@ function applyFilter(records, filterCriteria) {
1926
1977
 
1927
1978
  if (!parsedCriteria) return records;
1928
1979
 
1929
- return records.filter(record => {
1930
- const recordValue = record[parsedCriteria.column];
1931
- return recordValue && recordValue.toString() === parsedCriteria.value;
1932
- });
1980
+ // Handle both single criteria and multiple criteria
1981
+ if (Array.isArray(parsedCriteria)) {
1982
+ return records.filter(record => {
1983
+ return parsedCriteria.every(criterion => {
1984
+ const recordValue = record[criterion.column];
1985
+ return recordValue && recordValue.toString() === criterion.value;
1986
+ });
1987
+ });
1988
+ } else {
1989
+ // Single criteria (backward compatibility)
1990
+ return records.filter(record => {
1991
+ const recordValue = record[parsedCriteria.column];
1992
+ return recordValue && recordValue.toString() === parsedCriteria.value;
1993
+ });
1994
+ }
1933
1995
  }
1934
1996
 
1935
1997
  function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'koch', analyzeAspects = false, partnerPlanet = null, filterCriteria = null) {
@@ -1939,6 +2001,7 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
1939
2001
 
1940
2002
  // Helper function to process results
1941
2003
  function processResults() {
2004
+
1942
2005
  // Calculate house distribution
1943
2006
  const houseCounts = {};
1944
2007
  results.forEach(result => {
@@ -2004,15 +2067,33 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2004
2067
 
2005
2068
  // Process each row
2006
2069
  for (const data of filteredRecords) {
2007
- // Look for a column with ISO-Datetime values or Unix-Timestamps
2008
- const datetimeColumns = Object.keys(data).filter(key => {
2070
+ // Look for a column with datetime values, prioritizing specific date formats
2071
+ const datetimeColumns = [];
2072
+
2073
+ // First pass: look for YYYY-MM-DD format (most specific)
2074
+ const yyyyMmDdColumns = Object.keys(data).filter(key => {
2009
2075
  const value = data[key];
2010
- // Check for ISO-8601 date
2011
- const isISO = moment(value, moment.ISO_8601, true).isValid();
2012
- // Check for Unix-Timestamp (number with 10 or 13 digits)
2013
- const isUnixTimestamp = /^\d{10,13}$/.test(value);
2014
- return isISO || isUnixTimestamp;
2076
+ return /^\d{4}-\d{2}-\d{2}$/.test(value);
2077
+ });
2078
+
2079
+ // Second pass: look for Unix timestamps (10 or 13 digits)
2080
+ const unixTimestampColumns = Object.keys(data).filter(key => {
2081
+ const value = data[key];
2082
+ return /^\d{10,13}$/.test(value);
2015
2083
  });
2084
+
2085
+ // Third pass: look for ISO-8601 dates (least specific, as it can match many formats)
2086
+ const isoDateColumns = Object.keys(data).filter(key => {
2087
+ const value = data[key];
2088
+ // Only consider it an ISO date if it's not already matched by more specific patterns
2089
+ if (yyyyMmDdColumns.includes(key) || unixTimestampColumns.includes(key)) {
2090
+ return false;
2091
+ }
2092
+ return moment(value, moment.ISO_8601, true).isValid();
2093
+ });
2094
+
2095
+ // Prioritize columns: YYYY-MM-DD first, then Unix timestamps, then ISO dates
2096
+ datetimeColumns.push(...yyyyMmDdColumns, ...unixTimestampColumns, ...isoDateColumns);
2016
2097
 
2017
2098
  if (datetimeColumns.length > 0) {
2018
2099
  const datetimeValue = data[datetimeColumns[0]];
@@ -2023,6 +2104,10 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2023
2104
  // Convert Unix-Timestamp to milliseconds (if 10 digits, multiply by 1000)
2024
2105
  const timestamp = datetimeValue.length === 10 ? parseInt(datetimeValue) * 1000 : parseInt(datetimeValue);
2025
2106
  datetime = moment(timestamp);
2107
+ } else if (/^\d{4}-\d{2}-\d{2}$/.test(datetimeValue)) {
2108
+ // Handle as YYYY-MM-DD date format
2109
+ // Parse as date only, set time to 12:00 (noon) as default
2110
+ datetime = moment(datetimeValue + 'T12:00:00');
2026
2111
  } else {
2027
2112
  // Handle as ISO-8601 date
2028
2113
  datetime = moment(datetimeValue);
@@ -2121,8 +2206,10 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2121
2206
  }
2122
2207
  }
2123
2208
 
2124
- // Check if all operations are completed
2125
- checkCompletion();
2209
+ // Check if all operations are completed (only if no operations were started)
2210
+ if (pendingOperations === 0) {
2211
+ processResults();
2212
+ }
2126
2213
 
2127
2214
  // Helper function to check completion
2128
2215
  function checkCompletion() {
@@ -2139,139 +2226,193 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2139
2226
  }
2140
2227
  } else {
2141
2228
  // Standard file processing for local files
2142
- fs.createReadStream(filePath)
2143
- .pipe(csvParser())
2144
- .on('data', (data) => {
2145
- // Apply filter if specified
2146
- if (filterCriteria) {
2147
- const parsedCriteria = typeof filterCriteria === 'string'
2148
- ? parseFilterCriteria(filterCriteria)
2229
+ try {
2230
+ // Read the file content first
2231
+ const csvData = fs.readFileSync(filePath, 'utf8');
2232
+
2233
+ // Try to auto-detect delimiter by checking first few lines
2234
+ let delimiter = ',';
2235
+ const firstLines = csvData.split('\n').slice(0, 5).join('\n');
2236
+ const commaCount = (firstLines.match(/,/g) || []).length;
2237
+ const semicolonCount = (firstLines.match(/;/g) || []).length;
2238
+
2239
+ // Use semicolon if it appears more frequently than comma
2240
+ if (semicolonCount > commaCount) {
2241
+ delimiter = ';';
2242
+ }
2243
+
2244
+ // Parse the CSV data with detected delimiter
2245
+ const records = parse(csvData, {
2246
+ columns: true,
2247
+ skip_empty_lines: true,
2248
+ delimiter: delimiter
2249
+ });
2250
+
2251
+ // Process each record
2252
+ for (const data of records) {
2253
+ // Apply filter if specified
2254
+ if (filterCriteria) {
2255
+ const parsedCriteria = typeof filterCriteria === 'string'
2256
+ ? parseFilterCriteria(filterCriteria)
2149
2257
  : filterCriteria;
2258
+
2259
+ if (parsedCriteria) {
2260
+ let shouldSkip = false;
2150
2261
 
2151
- if (parsedCriteria) {
2262
+ if (Array.isArray(parsedCriteria)) {
2263
+ // Multiple criteria - all must match
2264
+ shouldSkip = !parsedCriteria.every(criterion => {
2265
+ const recordValue = data[criterion.column];
2266
+ return recordValue && recordValue.toString() === criterion.value;
2267
+ });
2268
+ } else {
2269
+ // Single criteria
2152
2270
  const recordValue = data[parsedCriteria.column];
2153
- if (!recordValue || recordValue.toString() !== parsedCriteria.value) {
2154
- return; // Skip this record
2155
- }
2271
+ shouldSkip = !recordValue || recordValue.toString() !== parsedCriteria.value;
2272
+ }
2273
+
2274
+ if (shouldSkip) {
2275
+ continue; // Skip this record
2156
2276
  }
2157
2277
  }
2278
+ }
2158
2279
 
2159
- // Look for a column with ISO-Datetime values or Unix-Timestamps
2160
- const datetimeColumns = Object.keys(data).filter(key => {
2161
- const value = data[key];
2162
- // Check for ISO-8601 date
2163
- const isISO = moment(value, moment.ISO_8601, true).isValid();
2164
- // Check for Unix-Timestamp (number with 10 or 13 digits)
2165
- const isUnixTimestamp = /^\d{10,13}$/.test(value);
2166
- return isISO || isUnixTimestamp;
2167
- });
2280
+ // Look for a column with ISO-Datetime values, YYYY-MM-DD dates, or Unix-Timestamps
2281
+ const datetimeColumns = Object.keys(data).filter(key => {
2282
+ const value = data[key];
2283
+ // Check for ISO-8601 date
2284
+ const isISO = moment(value, moment.ISO_8601, true).isValid();
2285
+ // Check for YYYY-MM-DD date format
2286
+ const isYYYYMMDD = /^\d{4}-\d{2}-\d{2}$/.test(value);
2287
+ // Check for Unix-Timestamp (number with 10 or 13 digits)
2288
+ const isUnixTimestamp = /^\d{10,13}$/.test(value);
2289
+ return isISO || isYYYYMMDD || isUnixTimestamp;
2290
+ });
2168
2291
 
2169
- if (datetimeColumns.length > 0) {
2170
- const datetimeValue = data[datetimeColumns[0]];
2171
- let datetime;
2292
+ if (datetimeColumns.length > 0) {
2293
+ const datetimeValue = data[datetimeColumns[0]];
2294
+ let datetime;
2172
2295
 
2173
- // Check if it's a Unix-Timestamp
2174
- if (/^\d{10,13}$/.test(datetimeValue)) {
2175
- // Convert Unix-Timestamp to milliseconds (if 10 digits, multiply by 1000)
2176
- const timestamp = datetimeValue.length === 10 ? parseInt(datetimeValue) * 1000 : parseInt(datetimeValue);
2177
- datetime = moment(timestamp);
2178
- } else {
2179
- // Handle as ISO-8601 date
2180
- datetime = moment(datetimeValue);
2181
- }
2296
+ // Check if it's a Unix-Timestamp
2297
+ if (/^\d{10,13}$/.test(datetimeValue)) {
2298
+ // Convert Unix-Timestamp to milliseconds (if 10 digits, multiply by 1000)
2299
+ const timestamp = datetimeValue.length === 10 ? parseInt(datetimeValue) * 1000 : parseInt(datetimeValue);
2300
+ datetime = moment(timestamp);
2301
+ } else if (/^\d{4}-\d{2}-\d{2}$/.test(datetimeValue)) {
2302
+ // Handle as YYYY-MM-DD date format
2303
+ // Parse as date only, set time to 12:00 (noon) as default
2304
+ datetime = moment(datetimeValue + 'T12:00:00');
2305
+ } else {
2306
+ // Handle as ISO-8601 date
2307
+ datetime = moment(datetimeValue);
2308
+ }
2182
2309
 
2183
- // Convert the date to the format needed for astrological calculations
2184
- const dateComponents = {
2185
- year: datetime.year(),
2186
- month: datetime.month() + 1, // moment months are 0-based
2187
- day: datetime.date(),
2188
- hour: datetime.hour(),
2189
- minute: datetime.minute()
2190
- };
2191
-
2192
- // Calculate the astrological data for the specified planet
2193
- let astroData;
2194
- try {
2195
- astroData = getAstrologicalData(planetName, dateComponents);
2196
- } catch (error) {
2197
- // If error, skip this record (e.g., if date is out of range)
2198
- return;
2199
- }
2310
+ // Convert the date to the format needed for astrological calculations
2311
+ const dateComponents = {
2312
+ year: datetime.year(),
2313
+ month: datetime.month() + 1, // moment months are 0-based
2314
+ day: datetime.date(),
2315
+ hour: datetime.hour(),
2316
+ minute: datetime.minute()
2317
+ };
2318
+
2319
+ // Calculate the astrological data for the specified planet
2320
+ let astroData;
2321
+ try {
2322
+ astroData = getAstrologicalData(planetName, dateComponents);
2323
+ } catch (error) {
2324
+ // If error, skip this record (e.g., if date is out of range)
2325
+ continue;
2326
+ }
2327
+
2328
+ // Increase the counter for pending operations
2329
+ pendingOperations++;
2200
2330
 
2201
- // Calculate the houses
2202
- const julianDay = calculateJulianDayUTC(dateComponents, -datetime.utcOffset());
2203
- calculateHouses(julianDay, getHouseSystemCode(houseSystem), false)
2204
- .then(houses => {
2205
- const planetLongitude = parseFloat(astroData.degreeInSign) + (signs.indexOf(astroData.sign) * 30);
2206
- const house = getPlanetHouse(planetLongitude, houses.house);
2207
-
2208
- // Berechne Aspekte, falls angefordert
2209
- let aspects = [];
2210
- if (analyzeAspects) {
2211
- try {
2212
- aspects = calculatePlanetAspects(planetName, dateComponents, true);
2213
-
2214
- // Filter nach Partner-Planet, falls angegeben
2215
- if (partnerPlanet) {
2216
- aspects = aspects.filter(a => a.planet.toLowerCase() === partnerPlanet.toLowerCase());
2217
- }
2218
- } catch (error) {
2219
- // Ignoriere Fehler bei der Aspektberechnung für einzelne Datensätze
2331
+ // Calculate the houses
2332
+ const julianDay = calculateJulianDayUTC(dateComponents, -datetime.utcOffset());
2333
+ calculateHouses(julianDay, getHouseSystemCode(houseSystem), false)
2334
+ .then(houses => {
2335
+ const planetLongitude = parseFloat(astroData.degreeInSign) + (signs.indexOf(astroData.sign) * 30);
2336
+ const house = getPlanetHouse(planetLongitude, houses.house);
2337
+
2338
+ // Berechne Aspekte, falls angefordert
2339
+ let aspects = [];
2340
+ if (analyzeAspects) {
2341
+ try {
2342
+ aspects = calculatePlanetAspects(planetName, dateComponents, true);
2343
+
2344
+ // Filter nach Partner-Planet, falls angegeben
2345
+ if (partnerPlanet) {
2346
+ aspects = aspects.filter(a => a.planet.toLowerCase() === partnerPlanet.toLowerCase());
2220
2347
  }
2348
+ } catch (error) {
2349
+ // Ignoriere Fehler bei der Aspektberechnung für einzelne Datensätze
2221
2350
  }
2351
+ }
2222
2352
 
2223
- const result = {
2224
- datetime: datetimeValue,
2225
- planet: planetName,
2226
- sign: astroData.sign,
2227
- degreeInSign: astroData.degreeInSign,
2228
- house: house
2229
- };
2353
+ const result = {
2354
+ datetime: datetimeValue,
2355
+ planet: planetName,
2356
+ sign: astroData.sign,
2357
+ degreeInSign: astroData.degreeInSign,
2358
+ house: house
2359
+ };
2230
2360
 
2231
- if (analyzeAspects) {
2232
- result.aspects = aspects;
2233
- }
2361
+ if (analyzeAspects) {
2362
+ result.aspects = aspects;
2363
+ }
2364
+
2365
+ results.push(result);
2366
+ checkCompletion();
2367
+ })
2368
+ .catch(error => {
2369
+ console.error('Fehler bei der Hausberechnung:', error);
2370
+
2371
+ // Berechne Aspekte auch bei Hausberechnungsfehler, falls angefordert
2372
+ let aspects = [];
2373
+ if (analyzeAspects) {
2374
+ try {
2375
+ aspects = calculatePlanetAspects(planetName, dateComponents, true);
2234
2376
 
2235
- results.push(result);
2236
- })
2237
- .catch(error => {
2238
- console.error('Fehler bei der Hausberechnung:', error);
2239
-
2240
- // Berechne Aspekte auch bei Hausberechnungsfehler, falls angefordert
2241
- let aspects = [];
2242
- if (analyzeAspects) {
2243
- try {
2244
- aspects = calculatePlanetAspects(planetName, dateComponents, true);
2245
-
2246
- // Filter nach Partner-Planet, falls angegeben
2247
- if (partnerPlanet) {
2248
- aspects = aspects.filter(a => a.planet.toLowerCase() === partnerPlanet.toLowerCase());
2249
- }
2250
- } catch (error) {
2251
- console.error('Fehler bei der Aspektberechnung:', error);
2377
+ // Filter nach Partner-Planet, falls angegeben
2378
+ if (partnerPlanet) {
2379
+ aspects = aspects.filter(a => a.planet.toLowerCase() === partnerPlanet.toLowerCase());
2252
2380
  }
2381
+ } catch (error) {
2382
+ console.error('Fehler bei der Aspektberechnung:', error);
2253
2383
  }
2384
+ }
2254
2385
 
2255
- results.push({
2256
- datetime: datetimeValue,
2257
- planet: planetName,
2258
- sign: astroData.sign,
2259
- degreeInSign: astroData.degreeInSign,
2260
- house: 'N/A',
2261
- ...(analyzeAspects ? {aspects} : {})
2262
- });
2386
+ results.push({
2387
+ datetime: datetimeValue,
2388
+ planet: planetName,
2389
+ sign: astroData.sign,
2390
+ degreeInSign: astroData.degreeInSign,
2391
+ house: 'N/A',
2392
+ ...(analyzeAspects ? {aspects} : {})
2263
2393
  });
2264
- }
2265
- })
2266
- .on('end', () => {
2267
- // Verarbeite die Ergebnisse
2394
+ checkCompletion();
2395
+ });
2396
+ }
2397
+ }
2398
+
2399
+ // Helper function to check completion
2400
+ function checkCompletion() {
2401
+ pendingOperations--;
2402
+ if (pendingOperations === 0) {
2268
2403
  processResults();
2269
- })
2270
- .on('error', (error) => {
2271
- console.error('Fehler beim Lesen der CSV-Datei:', error);
2272
- reject(error);
2273
- });
2274
- }
2404
+ }
2405
+ }
2406
+
2407
+ // Check if all operations are completed (only if no operations were started)
2408
+ if (pendingOperations === 0) {
2409
+ processResults();
2410
+ }
2411
+ } catch (error) {
2412
+ console.error('Fehler beim Lesen der CSV-Datei:', error);
2413
+ reject(error);
2414
+ }
2415
+ }
2275
2416
  })
2276
2417
  }
2277
2418
 
package/src/cli/cli.js CHANGED
@@ -341,6 +341,54 @@ program
341
341
  }
342
342
  }
343
343
 
344
+ // Use custom date if specified (overrides person data)
345
+ if (options.d) {
346
+ // Try to parse the date as DD.MM.YYYY or DD.MM.YYYY HH:MM
347
+ const dateRegex = /^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}))?$/;
348
+ const match = options.d.match(dateRegex);
349
+
350
+ if (match) {
351
+ const day = parseInt(match[1], 10);
352
+ const month = parseInt(match[2], 10);
353
+ const year = parseInt(match[3], 10);
354
+ const hour = match[4] ? parseInt(match[4], 10) : 12; // Default: 12 o'clock
355
+ const minute = match[5] ? parseInt(match[5], 10) : 0; // Default: 0 minutes
356
+
357
+ // Check if the date is valid
358
+ const date = new Date(year, month - 1, day, hour, minute);
359
+ if (date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day) {
360
+ customDate = {
361
+ day: day,
362
+ month: month,
363
+ year: year,
364
+ hour: hour,
365
+ minute: minute
366
+ };
367
+ console.log(`Using custom date: ${day}.${month}.${year} ${hour}:${minute.toString().padStart(2, '0')}`);
368
+ } else {
369
+ console.error('Invalid date:', options.d);
370
+ console.error('💡 Please use the format: DD.MM.YYYY or "DD.MM.YYYY HH:MM" (with quotes for date with time)');
371
+ process.exit(1);
372
+ }
373
+ } else {
374
+ console.error('Invalid date:', options.d);
375
+ console.error('💡 Please use the format: DD.MM.YYYY or "DD.MM.YYYY HH:MM" (with quotes for date with time)');
376
+ process.exit(1);
377
+ }
378
+ }
379
+
380
+ // If no custom date is specified, use current date
381
+ if (!customDate) {
382
+ const currentTime = getCurrentTimeInTimezone();
383
+ customDate = {
384
+ day: currentTime.day,
385
+ month: currentTime.month,
386
+ year: currentTime.year,
387
+ hour: currentTime.hour,
388
+ minute: currentTime.minute
389
+ };
390
+ }
391
+
344
392
  const criticalPlanets = getCriticalPlanets(customDate);
345
393
 
346
394
  if (criticalPlanets.length === 0) {
@@ -348,18 +396,20 @@ program
348
396
  return;
349
397
  }
350
398
 
351
- console.log('========================================================');
352
- console.log('| Planet | Sign | Degree | Type |');
353
- console.log('========================================================');
399
+ console.log('================================================================================================================');
400
+ console.log('| Planet | Sign | Degree | Type | Interpretation |');
401
+ console.log('================================================================================================================');
354
402
 
355
403
  criticalPlanets.forEach(planet => {
356
404
  const planetName = planet.name.charAt(0).toUpperCase() + planet.name.slice(1);
357
405
  const sign = planet.sign.padEnd(10, ' ');
358
406
  const degree = planet.degree.padEnd(5, ' ');
359
- console.log(`| ${planetName.padEnd(8)} | ${sign} | ${degree} | ${planet.criticalType} |`);
407
+ const criticalType = planet.criticalType.padEnd(20, ' ');
408
+ const interpretation = planet.interpretation.padEnd(46, ' ');
409
+ console.log(`| ${planetName.padEnd(8)} | ${sign} | ${degree} | ${criticalType} | ${interpretation} |`);
360
410
  });
361
411
 
362
- console.log('========================================================');
412
+ console.log('================================================================================================================');
363
413
  if (shouldUseBirthData(options)) {
364
414
  console.log('\nThis analysis is based on your birth chart.');
365
415
  } else {