klio 1.4.9 → 1.5.1

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.
@@ -1,9 +1,9 @@
1
- const swisseph = require('swisseph');
1
+ const swisseph = require('./swissephAdapter');
2
2
  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
 
@@ -50,11 +50,11 @@ function getCurrentTimeInTimezone() {
50
50
  }
51
51
 
52
52
  // Function to load birth data from configuration
53
- function getBirthDataFromConfig() {
53
+ function getBirthDataFromConfig(userId = null) {
54
54
  try {
55
55
  // First try to load the new configuration
56
56
  const { loadConfig } = require('../config/configService');
57
- const config = loadConfig();
57
+ const config = loadConfig(userId);
58
58
 
59
59
  if (config && config.birthData) {
60
60
  // Parse birth date (Format: DD.MM.YYYY)
@@ -78,32 +78,34 @@ function getBirthDataFromConfig() {
78
78
  };
79
79
  }
80
80
 
81
- // If the new configuration was not found, try the old file
82
- const configPath = path.join(__dirname, '../../astrocli-config.json');
83
- if (fs.existsSync(configPath)) {
84
- const configData = fs.readFileSync(configPath, 'utf8');
85
- const oldConfig = JSON.parse(configData);
86
-
87
- if (oldConfig && oldConfig.birthData) {
88
- // Parse birth date (Format: DD.MM.YYYY)
89
- const dateParts = oldConfig.birthData.date.split('.');
90
- const day = parseInt(dateParts[0]);
91
- const month = parseInt(dateParts[1]);
92
- const year = parseInt(dateParts[2]);
93
-
94
- // Parse birth time (Format: HH:MM)
95
- const timeParts = oldConfig.birthData.time.split(':');
96
- const hour = parseInt(timeParts[0]);
97
- const minute = parseInt(timeParts[1]);
81
+ // If the new configuration was not found, try the old file (only for global config)
82
+ if (!userId) {
83
+ const configPath = path.join(__dirname, '../../astrocli-config.json');
84
+ if (fs.existsSync(configPath)) {
85
+ const configData = fs.readFileSync(configPath, 'utf8');
86
+ const oldConfig = JSON.parse(configData);
98
87
 
99
- return {
100
- year: year,
101
- month: month,
102
- day: day,
103
- hour: hour,
104
- minute: minute,
105
- location: oldConfig.birthData.location
106
- };
88
+ if (oldConfig && oldConfig.birthData) {
89
+ // Parse birth date (Format: DD.MM.YYYY)
90
+ const dateParts = oldConfig.birthData.date.split('.');
91
+ const day = parseInt(dateParts[0]);
92
+ const month = parseInt(dateParts[1]);
93
+ const year = parseInt(dateParts[2]);
94
+
95
+ // Parse birth time (Format: HH:MM)
96
+ const timeParts = oldConfig.birthData.time.split(':');
97
+ const hour = parseInt(timeParts[0]);
98
+ const minute = parseInt(timeParts[1]);
99
+
100
+ return {
101
+ year: year,
102
+ month: month,
103
+ day: day,
104
+ hour: hour,
105
+ minute: minute,
106
+ location: oldConfig.birthData.location
107
+ };
108
+ }
107
109
  }
108
110
  }
109
111
  } catch (error) {
@@ -114,14 +116,14 @@ function getBirthDataFromConfig() {
114
116
  }
115
117
 
116
118
  // Function to load person data (including birth data)
117
- function getPersonDataFromConfig(personId = 'birth') {
119
+ function getPersonDataFromConfig(personId = 'birth', userId = null) {
118
120
  try {
119
121
  const { getPersonData } = require('../config/configService');
120
122
 
121
123
  if (personId === 'birth') {
122
- return getBirthDataFromConfig();
124
+ return getBirthDataFromConfig(userId);
123
125
  } else {
124
- return getPersonData(personId);
126
+ return getPersonData(personId, userId);
125
127
  }
126
128
  } catch (error) {
127
129
  console.error(`Error loading data for ${personId}:`, error.message);
@@ -454,12 +456,18 @@ function getAstrologicalData(planetName, customDate = null) {
454
456
  };
455
457
  }
456
458
 
459
+
460
+
457
461
  // Function to identify critical planets
458
462
  function getCriticalPlanets(customDate = null) {
459
463
  const criticalPlanets = [];
460
464
 
461
- // Critical degrees: 0°, 13°, 26° (cardinal degrees) and 29° (anaretic degree)
462
- const criticalDegrees = [0, 13, 26, 29];
465
+ // Critical degrees by sign type:
466
+ // Cardinal signs (Aries, Cancer, Libra, Capricorn): 0°, 13°, 26°
467
+ // Fixed signs (Taurus, Leo, Scorpio, Aquarius): 8-9°, 21-22°
468
+ // Mutable signs (Gemini, Virgo, Sagittarius, Pisces): 4°, 17°
469
+ // Anaretic degree (all signs): 29°
470
+
463
471
  const orb = 1; // Tolerance of 1 degree
464
472
 
465
473
  // Use the specified date or current time
@@ -502,19 +510,55 @@ function getCriticalPlanets(customDate = null) {
502
510
  const degreeInSign = longitude % 30;
503
511
  const signIndex = Math.floor(longitude / 30);
504
512
  const sign = signs[signIndex];
513
+ const signType = signTypes[signIndex];
514
+
515
+ // Determine critical degrees based on sign type
516
+ let isCritical = false;
517
+ let criticalType = '';
505
518
 
506
- // Check if planet is on a critical degree
507
- const isCritical = criticalDegrees.some(criticalDegree => {
508
- return Math.abs(degreeInSign - criticalDegree) <= orb;
509
- });
510
-
519
+ // First check for Anaretic degree (29°) - applies to all signs
520
+ if (degreeInSign >= 28.8) { // 29° with 0.2° orb for precision
521
+ isCritical = true;
522
+ criticalType = 'Anaretic (29°)';
523
+ } else if (signType === 'Cardinal') {
524
+ // Cardinal: exact degrees 0°, 13°, 26° with orb
525
+ isCritical = [0, 13, 26].some(criticalDegree => {
526
+ return Math.abs(degreeInSign - criticalDegree) <= orb;
527
+ });
528
+ criticalType = 'Cardinal (0°, 13°, 26°)';
529
+ } else if (signType === 'Fixed') {
530
+ // Fixed: ranges 8-9° and 21-22°
531
+ isCritical = (degreeInSign >= 8 && degreeInSign <= 9) || (degreeInSign >= 21 && degreeInSign <= 22);
532
+ criticalType = 'Fixed (8-9°, 21-22°)';
533
+ } else if (signType === 'Mutable') {
534
+ // Mutable: exact degrees 4°, 17° with orb
535
+ isCritical = [4, 17].some(criticalDegree => {
536
+ return Math.abs(degreeInSign - criticalDegree) <= orb;
537
+ });
538
+ criticalType = 'Mutable (4°, 17°)';
539
+ }
540
+
511
541
  if (isCritical) {
542
+ // Simple interpretation based on modality challenges
543
+ let interpretation = '';
544
+
545
+ if (criticalType === 'Cardinal (0°, 13°, 26°)') {
546
+ interpretation = 'Tendency to over-express. May push too hard or initiate prematurely.';
547
+ } else if (criticalType === 'Fixed (8-9°, 21-22°)') {
548
+ interpretation = 'Tendency to under-express. May resist change or hold on too tightly.';
549
+ } else if (criticalType === 'Mutable (4°, 17°)') {
550
+ interpretation = 'Path of ambivalence. May struggle with indecision or adapt excessively.';
551
+ } else if (criticalType === 'Anaretic (29°)') {
552
+ interpretation = 'Poised for change. At the end of a cycle, facing transition.';
553
+ }
554
+
512
555
  criticalPlanets.push({
513
556
  name,
514
557
  sign,
515
558
  degree: degreeInSign.toFixed(2),
516
559
  isCritical: true,
517
- criticalType: degreeInSign >= 28.5 ? 'Anaretic (29°)' : 'Cardinal (0°, 13°, 26°)'
560
+ criticalType: criticalType,
561
+ interpretation: interpretation
518
562
  });
519
563
  }
520
564
  }
@@ -642,7 +686,7 @@ function showPlanetAspects(planetName, dateComponents, useBirthData = false, use
642
686
 
643
687
  console.log(`Aspects for ${planetLabel}:`);
644
688
  console.log('================================================================================');
645
- console.log('| Aspect | Planet | Orb | Exact Date/Time |');
689
+ console.log('| Aspect | Planet | Orb |');
646
690
  console.log('================================================================================');
647
691
 
648
692
  if (aspects.length === 0) {
@@ -654,14 +698,8 @@ function showPlanetAspects(planetName, dateComponents, useBirthData = false, use
654
698
  const planetFormatted = planetNameFormatted.padEnd(8, ' ');
655
699
  const orb = aspect.orb.padEnd(4, ' ');
656
700
 
657
- // Calculate exact date and time for this aspect
658
- const exactDateTime = findExactAspectTime(planetName, aspect.planet, aspect.type, dateComponents);
659
- if (exactDateTime) {
660
- const dateTimeStr = `${String(exactDateTime.day).padStart(2, '0')}.${String(exactDateTime.month).padStart(2, '0')}.${exactDateTime.year} ${String(exactDateTime.hour).padStart(2, '0')}:${String(exactDateTime.minute).padStart(2, '0')}`;
661
- console.log(`| ${aspectName} | ${planetFormatted} | ${orb}° | ${dateTimeStr} |`);
662
- } else {
663
- console.log(`| ${aspectName} | ${planetFormatted} | ${orb}° | Calculation failed |`);
664
- }
701
+ // Exact time calculation removed
702
+ console.log(`| ${aspectName} | ${planetFormatted} | ${orb}° |`);
665
703
  });
666
704
  }
667
705
 
@@ -682,7 +720,7 @@ function showPlanetComboAspects(planetNames, dateComponents, useBirthData = fals
682
720
 
683
721
  console.log(`Aspects between ${planetLabels}:`);
684
722
  console.log('================================================================================');
685
- console.log('| Planet 1 | Planet 2 | Aspect | Orb | Exact Date/Time |');
723
+ console.log('| Planet 1 | Planet 2 | Aspect | Orb |');
686
724
  console.log('================================================================================');
687
725
 
688
726
  if (aspects.length === 0) {
@@ -696,14 +734,8 @@ function showPlanetComboAspects(planetNames, dateComponents, useBirthData = fals
696
734
  const aspectName = aspect.type.padEnd(11, ' ');
697
735
  const orb = aspect.orb.padEnd(4, ' ');
698
736
 
699
- // Calculate exact date and time for this aspect
700
- const exactDateTime = findExactAspectTime(aspect.planet1, aspect.planet2, aspect.type, dateComponents);
701
- if (exactDateTime) {
702
- const dateTimeStr = `${String(exactDateTime.day).padStart(2, '0')}.${String(exactDateTime.month).padStart(2, '0')}.${exactDateTime.year} ${String(exactDateTime.hour).padStart(2, '0')}:${String(exactDateTime.minute).padStart(2, '0')}`;
703
- console.log(`| ${planet1Padded} | ${planet2Padded} | ${aspectName} | ${orb}° | ${dateTimeStr} |`);
704
- } else {
705
- console.log(`| ${planet1Padded} | ${planet2Padded} | ${aspectName} | ${orb}° | Calculation failed |`);
706
- }
737
+ // Exact time calculation removed
738
+ console.log(`| ${planet1Padded} | ${planet2Padded} | ${aspectName} | ${orb}° |`);
707
739
  });
708
740
  }
709
741
 
@@ -716,107 +748,7 @@ function showPlanetComboAspects(planetNames, dateComponents, useBirthData = fals
716
748
  }
717
749
  }
718
750
 
719
- // Helper function to calculate exact date and time for an exact aspect
720
- function findExactAspectTime(planet1, planet2, aspectType, startDate) {
721
- const targetAngle = getAspectAngle(aspectType);
722
-
723
- if (targetAngle === null) {
724
- return null;
725
- }
726
-
727
- // We search in a reasonable time window (90 days in the future)
728
- // to find the next exact aspect
729
- // Especially important for slow planets like Saturn, Uranus, Neptune, Pluto
730
- let bestMatch = null;
731
- let bestDistance = Infinity;
732
-
733
- // Search only in the future (0-90 days after start date)
734
- const searchWindow = 90; // days
735
-
736
- // We search in smaller steps (every 2 hours) to get more precise results
737
- const stepsPerDay = 12; // 2-hour steps
738
-
739
- for (let daysOffset = 0; daysOffset <= searchWindow; daysOffset++) {
740
- for (let hourOffset = 0; hourOffset < 24; hourOffset++) { // Every hour
741
- for (let minuteOffset = 0; minuteOffset < 60; minuteOffset += 5) { // Every 5 minutes
742
- const testDate = addDays(startDate, daysOffset);
743
- testDate.hour = hourOffset;
744
- testDate.minute = minuteOffset;
745
-
746
- // Calculate positions of both planets
747
- const planet1Data = getAstrologicalData(planet1, testDate);
748
- const planet2Data = getAstrologicalData(planet2, testDate);
749
-
750
- // Calculate angle between planets
751
- const angleDiff = Math.abs(planet1Data.longitude - planet2Data.longitude) % 360;
752
- const normalizedAngle = Math.min(angleDiff, 360 - angleDiff);
753
- const distanceToTarget = Math.abs(normalizedAngle - targetAngle);
754
-
755
- // Store the best result
756
- if (distanceToTarget < bestDistance) {
757
- bestDistance = distanceToTarget;
758
- bestMatch = {...testDate};
759
- }
760
- }
761
- }
762
- }
763
-
764
- // If we found an aspect close enough to the target, return it
765
- // Stricter tolerance of 0.5° for better accuracy
766
- if (bestMatch && bestDistance <= 0.5) { // Tolerance of 0.5°
767
- return bestMatch;
768
- }
769
-
770
- return null;
771
- }
772
751
 
773
- // Function to find the last exact aspect in the past (for separative phases)
774
- function findLastExactAspectTime(planet1, planet2, aspectType, startDate) {
775
- const targetAngle = getAspectAngle(aspectType);
776
-
777
- if (targetAngle === null) {
778
- return null;
779
- }
780
-
781
- // Search in the past (up to 180 days before start date)
782
- let bestMatch = null;
783
- let bestDistance = Infinity;
784
-
785
- // Search in a time window of 180 days before start date
786
- const searchWindow = 180; // days
787
-
788
- for (let daysOffset = -searchWindow; daysOffset <= 0; daysOffset++) {
789
- for (let hourOffset = 0; hourOffset < 24; hourOffset++) { // Every hour
790
- for (let minuteOffset = 0; minuteOffset < 60; minuteOffset += 5) { // Every 5 minutes
791
- const testDate = addDays(startDate, daysOffset);
792
- testDate.hour = hourOffset;
793
- testDate.minute = minuteOffset;
794
-
795
- // Calculate positions of both planets
796
- const planet1Data = getAstrologicalData(planet1, testDate);
797
- const planet2Data = getAstrologicalData(planet2, testDate);
798
-
799
- // Calculate angle between planets
800
- const angleDiff = Math.abs(planet1Data.longitude - planet2Data.longitude) % 360;
801
- const normalizedAngle = Math.min(angleDiff, 360 - angleDiff);
802
- const distanceToTarget = Math.abs(normalizedAngle - targetAngle);
803
-
804
- // Store the best result
805
- if (distanceToTarget < bestDistance) {
806
- bestDistance = distanceToTarget;
807
- bestMatch = {...testDate};
808
- }
809
- }
810
- }
811
- }
812
-
813
- // If we found an aspect close enough to the target, return it
814
- if (bestMatch && bestDistance <= 0.5) { // Tolerance of 0.5°
815
- return bestMatch;
816
- }
817
-
818
- return null;
819
- }
820
752
 
821
753
  // Function to calculate all active aspects between all planets
822
754
  function getAllActiveAspects(dateComponents) {
@@ -1733,7 +1665,7 @@ function showAllActiveAspects(dateComponents, useBirthData = false) {
1733
1665
 
1734
1666
  console.log('Active Aspects (all planets) - Classified by Precision:');
1735
1667
  console.log('═══════════════════════════════════════════════════════════════════════════');
1736
- console.log('║ Planet 1 │ Planet 2 │ Orb │ Status │ Exact Date/Time ║');
1668
+ console.log('║ Planet 1 │ Planet 2 │ Orb │ Status ║');
1737
1669
  console.log('═══════════════════════════════════════════════════════════════════════════');
1738
1670
 
1739
1671
  if (sortedAspects.length === 0) {
@@ -1784,14 +1716,10 @@ function showAllActiveAspects(dateComponents, useBirthData = false) {
1784
1716
  );
1785
1717
  const statusPadded = phase.padEnd(11, ' ');
1786
1718
 
1787
- // Calculate the exact date and time for this aspect
1788
- const exactDateTime = findExactAspectTime(aspect.planet1, aspect.planet2, aspect.type, dateComponents);
1789
- let dateTimeStr = '-';
1790
- if (exactDateTime) {
1791
- dateTimeStr = `${String(exactDateTime.day).padStart(2, '0')}.${String(exactDateTime.month).padStart(2, '0')}.${exactDateTime.year} ${String(exactDateTime.hour).padStart(2, '0')}:${String(exactDateTime.minute).padStart(2, '0')}`;
1792
- }
1719
+ // Exact time calculation removed
1720
+ const dateTimeStr = '-';
1793
1721
 
1794
- console.log(`║ ${planet1Padded} │ ${planet2Padded} │ ${orb}° │ ${statusPadded} │ ${dateTimeStr} ║`);
1722
+ console.log(`║ ${planet1Padded} │ ${planet2Padded} │ ${orb}° │ ${statusPadded} ║`);
1795
1723
  });
1796
1724
  }
1797
1725
  });
@@ -1904,16 +1832,25 @@ function analyzeSignDistributionSignificance(signDistribution) {
1904
1832
  function parseFilterCriteria(filterString) {
1905
1833
  if (!filterString) return null;
1906
1834
 
1907
- const parts = filterString.split(':');
1908
- if (parts.length !== 2) {
1909
- console.warn('Invalid filter format. Expected "column:value"');
1910
- return null;
1835
+ // Check if multiple filters are provided (comma-separated)
1836
+ const filterConditions = filterString.split(',').map(cond => cond.trim());
1837
+
1838
+ const criteria = [];
1839
+
1840
+ for (const condition of filterConditions) {
1841
+ const parts = condition.split(':');
1842
+ if (parts.length !== 2) {
1843
+ console.warn(`Invalid filter format. Expected "column:value" but got "${condition}"`);
1844
+ return null;
1845
+ }
1846
+
1847
+ criteria.push({
1848
+ column: parts[0].trim(),
1849
+ value: parts[1].trim()
1850
+ });
1911
1851
  }
1912
1852
 
1913
- return {
1914
- column: parts[0].trim(),
1915
- value: parts[1].trim()
1916
- };
1853
+ return criteria.length === 1 ? criteria[0] : criteria;
1917
1854
  }
1918
1855
 
1919
1856
  // Helper function to apply filter to records
@@ -1926,10 +1863,21 @@ function applyFilter(records, filterCriteria) {
1926
1863
 
1927
1864
  if (!parsedCriteria) return records;
1928
1865
 
1929
- return records.filter(record => {
1930
- const recordValue = record[parsedCriteria.column];
1931
- return recordValue && recordValue.toString() === parsedCriteria.value;
1932
- });
1866
+ // Handle both single criteria and multiple criteria
1867
+ if (Array.isArray(parsedCriteria)) {
1868
+ return records.filter(record => {
1869
+ return parsedCriteria.every(criterion => {
1870
+ const recordValue = record[criterion.column];
1871
+ return recordValue && recordValue.toString() === criterion.value;
1872
+ });
1873
+ });
1874
+ } else {
1875
+ // Single criteria (backward compatibility)
1876
+ return records.filter(record => {
1877
+ const recordValue = record[parsedCriteria.column];
1878
+ return recordValue && recordValue.toString() === parsedCriteria.value;
1879
+ });
1880
+ }
1933
1881
  }
1934
1882
 
1935
1883
  function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'koch', analyzeAspects = false, partnerPlanet = null, filterCriteria = null) {
@@ -1939,6 +1887,7 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
1939
1887
 
1940
1888
  // Helper function to process results
1941
1889
  function processResults() {
1890
+
1942
1891
  // Calculate house distribution
1943
1892
  const houseCounts = {};
1944
1893
  results.forEach(result => {
@@ -2004,15 +1953,33 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2004
1953
 
2005
1954
  // Process each row
2006
1955
  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 => {
1956
+ // Look for a column with datetime values, prioritizing specific date formats
1957
+ const datetimeColumns = [];
1958
+
1959
+ // First pass: look for YYYY-MM-DD format (most specific)
1960
+ const yyyyMmDdColumns = Object.keys(data).filter(key => {
2009
1961
  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;
1962
+ return /^\d{4}-\d{2}-\d{2}$/.test(value);
1963
+ });
1964
+
1965
+ // Second pass: look for Unix timestamps (10 or 13 digits)
1966
+ const unixTimestampColumns = Object.keys(data).filter(key => {
1967
+ const value = data[key];
1968
+ return /^\d{10,13}$/.test(value);
1969
+ });
1970
+
1971
+ // Third pass: look for ISO-8601 dates (least specific, as it can match many formats)
1972
+ const isoDateColumns = Object.keys(data).filter(key => {
1973
+ const value = data[key];
1974
+ // Only consider it an ISO date if it's not already matched by more specific patterns
1975
+ if (yyyyMmDdColumns.includes(key) || unixTimestampColumns.includes(key)) {
1976
+ return false;
1977
+ }
1978
+ return moment(value, moment.ISO_8601, true).isValid();
2015
1979
  });
1980
+
1981
+ // Prioritize columns: YYYY-MM-DD first, then Unix timestamps, then ISO dates
1982
+ datetimeColumns.push(...yyyyMmDdColumns, ...unixTimestampColumns, ...isoDateColumns);
2016
1983
 
2017
1984
  if (datetimeColumns.length > 0) {
2018
1985
  const datetimeValue = data[datetimeColumns[0]];
@@ -2023,6 +1990,10 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2023
1990
  // Convert Unix-Timestamp to milliseconds (if 10 digits, multiply by 1000)
2024
1991
  const timestamp = datetimeValue.length === 10 ? parseInt(datetimeValue) * 1000 : parseInt(datetimeValue);
2025
1992
  datetime = moment(timestamp);
1993
+ } else if (/^\d{4}-\d{2}-\d{2}$/.test(datetimeValue)) {
1994
+ // Handle as YYYY-MM-DD date format
1995
+ // Parse as date only, set time to 12:00 (noon) as default
1996
+ datetime = moment(datetimeValue + 'T12:00:00');
2026
1997
  } else {
2027
1998
  // Handle as ISO-8601 date
2028
1999
  datetime = moment(datetimeValue);
@@ -2121,8 +2092,10 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2121
2092
  }
2122
2093
  }
2123
2094
 
2124
- // Check if all operations are completed
2125
- checkCompletion();
2095
+ // Check if all operations are completed (only if no operations were started)
2096
+ if (pendingOperations === 0) {
2097
+ processResults();
2098
+ }
2126
2099
 
2127
2100
  // Helper function to check completion
2128
2101
  function checkCompletion() {
@@ -2139,139 +2112,193 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2139
2112
  }
2140
2113
  } else {
2141
2114
  // 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)
2115
+ try {
2116
+ // Read the file content first
2117
+ const csvData = fs.readFileSync(filePath, 'utf8');
2118
+
2119
+ // Try to auto-detect delimiter by checking first few lines
2120
+ let delimiter = ',';
2121
+ const firstLines = csvData.split('\n').slice(0, 5).join('\n');
2122
+ const commaCount = (firstLines.match(/,/g) || []).length;
2123
+ const semicolonCount = (firstLines.match(/;/g) || []).length;
2124
+
2125
+ // Use semicolon if it appears more frequently than comma
2126
+ if (semicolonCount > commaCount) {
2127
+ delimiter = ';';
2128
+ }
2129
+
2130
+ // Parse the CSV data with detected delimiter
2131
+ const records = parse(csvData, {
2132
+ columns: true,
2133
+ skip_empty_lines: true,
2134
+ delimiter: delimiter
2135
+ });
2136
+
2137
+ // Process each record
2138
+ for (const data of records) {
2139
+ // Apply filter if specified
2140
+ if (filterCriteria) {
2141
+ const parsedCriteria = typeof filterCriteria === 'string'
2142
+ ? parseFilterCriteria(filterCriteria)
2149
2143
  : filterCriteria;
2144
+
2145
+ if (parsedCriteria) {
2146
+ let shouldSkip = false;
2150
2147
 
2151
- if (parsedCriteria) {
2148
+ if (Array.isArray(parsedCriteria)) {
2149
+ // Multiple criteria - all must match
2150
+ shouldSkip = !parsedCriteria.every(criterion => {
2151
+ const recordValue = data[criterion.column];
2152
+ return recordValue && recordValue.toString() === criterion.value;
2153
+ });
2154
+ } else {
2155
+ // Single criteria
2152
2156
  const recordValue = data[parsedCriteria.column];
2153
- if (!recordValue || recordValue.toString() !== parsedCriteria.value) {
2154
- return; // Skip this record
2155
- }
2157
+ shouldSkip = !recordValue || recordValue.toString() !== parsedCriteria.value;
2158
+ }
2159
+
2160
+ if (shouldSkip) {
2161
+ continue; // Skip this record
2156
2162
  }
2157
2163
  }
2164
+ }
2158
2165
 
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
- });
2166
+ // Look for a column with ISO-Datetime values, YYYY-MM-DD dates, or Unix-Timestamps
2167
+ const datetimeColumns = Object.keys(data).filter(key => {
2168
+ const value = data[key];
2169
+ // Check for ISO-8601 date
2170
+ const isISO = moment(value, moment.ISO_8601, true).isValid();
2171
+ // Check for YYYY-MM-DD date format
2172
+ const isYYYYMMDD = /^\d{4}-\d{2}-\d{2}$/.test(value);
2173
+ // Check for Unix-Timestamp (number with 10 or 13 digits)
2174
+ const isUnixTimestamp = /^\d{10,13}$/.test(value);
2175
+ return isISO || isYYYYMMDD || isUnixTimestamp;
2176
+ });
2168
2177
 
2169
- if (datetimeColumns.length > 0) {
2170
- const datetimeValue = data[datetimeColumns[0]];
2171
- let datetime;
2178
+ if (datetimeColumns.length > 0) {
2179
+ const datetimeValue = data[datetimeColumns[0]];
2180
+ let datetime;
2172
2181
 
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
- }
2182
+ // Check if it's a Unix-Timestamp
2183
+ if (/^\d{10,13}$/.test(datetimeValue)) {
2184
+ // Convert Unix-Timestamp to milliseconds (if 10 digits, multiply by 1000)
2185
+ const timestamp = datetimeValue.length === 10 ? parseInt(datetimeValue) * 1000 : parseInt(datetimeValue);
2186
+ datetime = moment(timestamp);
2187
+ } else if (/^\d{4}-\d{2}-\d{2}$/.test(datetimeValue)) {
2188
+ // Handle as YYYY-MM-DD date format
2189
+ // Parse as date only, set time to 12:00 (noon) as default
2190
+ datetime = moment(datetimeValue + 'T12:00:00');
2191
+ } else {
2192
+ // Handle as ISO-8601 date
2193
+ datetime = moment(datetimeValue);
2194
+ }
2182
2195
 
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
- }
2196
+ // Convert the date to the format needed for astrological calculations
2197
+ const dateComponents = {
2198
+ year: datetime.year(),
2199
+ month: datetime.month() + 1, // moment months are 0-based
2200
+ day: datetime.date(),
2201
+ hour: datetime.hour(),
2202
+ minute: datetime.minute()
2203
+ };
2204
+
2205
+ // Calculate the astrological data for the specified planet
2206
+ let astroData;
2207
+ try {
2208
+ astroData = getAstrologicalData(planetName, dateComponents);
2209
+ } catch (error) {
2210
+ // If error, skip this record (e.g., if date is out of range)
2211
+ continue;
2212
+ }
2200
2213
 
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
2214
+ // Increase the counter for pending operations
2215
+ pendingOperations++;
2216
+
2217
+ // Calculate the houses
2218
+ const julianDay = calculateJulianDayUTC(dateComponents, -datetime.utcOffset());
2219
+ calculateHouses(julianDay, getHouseSystemCode(houseSystem), false)
2220
+ .then(houses => {
2221
+ const planetLongitude = parseFloat(astroData.degreeInSign) + (signs.indexOf(astroData.sign) * 30);
2222
+ const house = getPlanetHouse(planetLongitude, houses.house);
2223
+
2224
+ // Berechne Aspekte, falls angefordert
2225
+ let aspects = [];
2226
+ if (analyzeAspects) {
2227
+ try {
2228
+ aspects = calculatePlanetAspects(planetName, dateComponents, true);
2229
+
2230
+ // Filter nach Partner-Planet, falls angegeben
2231
+ if (partnerPlanet) {
2232
+ aspects = aspects.filter(a => a.planet.toLowerCase() === partnerPlanet.toLowerCase());
2220
2233
  }
2234
+ } catch (error) {
2235
+ // Ignoriere Fehler bei der Aspektberechnung für einzelne Datensätze
2221
2236
  }
2237
+ }
2222
2238
 
2223
- const result = {
2224
- datetime: datetimeValue,
2225
- planet: planetName,
2226
- sign: astroData.sign,
2227
- degreeInSign: astroData.degreeInSign,
2228
- house: house
2229
- };
2239
+ const result = {
2240
+ datetime: datetimeValue,
2241
+ planet: planetName,
2242
+ sign: astroData.sign,
2243
+ degreeInSign: astroData.degreeInSign,
2244
+ house: house
2245
+ };
2230
2246
 
2231
- if (analyzeAspects) {
2232
- result.aspects = aspects;
2233
- }
2247
+ if (analyzeAspects) {
2248
+ result.aspects = aspects;
2249
+ }
2250
+
2251
+ results.push(result);
2252
+ checkCompletion();
2253
+ })
2254
+ .catch(error => {
2255
+ console.error('Fehler bei der Hausberechnung:', error);
2256
+
2257
+ // Berechne Aspekte auch bei Hausberechnungsfehler, falls angefordert
2258
+ let aspects = [];
2259
+ if (analyzeAspects) {
2260
+ try {
2261
+ aspects = calculatePlanetAspects(planetName, dateComponents, true);
2234
2262
 
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);
2263
+ // Filter nach Partner-Planet, falls angegeben
2264
+ if (partnerPlanet) {
2265
+ aspects = aspects.filter(a => a.planet.toLowerCase() === partnerPlanet.toLowerCase());
2252
2266
  }
2267
+ } catch (error) {
2268
+ console.error('Fehler bei der Aspektberechnung:', error);
2253
2269
  }
2270
+ }
2254
2271
 
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
- });
2272
+ results.push({
2273
+ datetime: datetimeValue,
2274
+ planet: planetName,
2275
+ sign: astroData.sign,
2276
+ degreeInSign: astroData.degreeInSign,
2277
+ house: 'N/A',
2278
+ ...(analyzeAspects ? {aspects} : {})
2263
2279
  });
2264
- }
2265
- })
2266
- .on('end', () => {
2267
- // Verarbeite die Ergebnisse
2280
+ checkCompletion();
2281
+ });
2282
+ }
2283
+ }
2284
+
2285
+ // Helper function to check completion
2286
+ function checkCompletion() {
2287
+ pendingOperations--;
2288
+ if (pendingOperations === 0) {
2268
2289
  processResults();
2269
- })
2270
- .on('error', (error) => {
2271
- console.error('Fehler beim Lesen der CSV-Datei:', error);
2272
- reject(error);
2273
- });
2274
- }
2290
+ }
2291
+ }
2292
+
2293
+ // Check if all operations are completed (only if no operations were started)
2294
+ if (pendingOperations === 0) {
2295
+ processResults();
2296
+ }
2297
+ } catch (error) {
2298
+ console.error('Fehler beim Lesen der CSV-Datei:', error);
2299
+ reject(error);
2300
+ }
2301
+ }
2275
2302
  })
2276
2303
  }
2277
2304
 
@@ -2812,8 +2839,6 @@ module.exports = {
2812
2839
  getPastAspects,
2813
2840
  getAspectAngle,
2814
2841
  getFutureAspects,
2815
- findExactAspectTime,
2816
- findLastExactAspectTime,
2817
2842
  analyzeCSVWithDatetime,
2818
2843
  detectAspectFigures,
2819
2844
  showAspectFigures,