jyotish-calc 1.2.0 → 1.3.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.
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Karana (Half-Tithi) Calculations
3
+ * Karana = half of a tithi (6 degrees of moon-sun separation)
4
+ * 60 karanas in a lunar month (11 types cycling through fixed positions)
5
+ */
6
+
7
+ const tithiModule = require('./tithi');
8
+ const utils = require('./utils');
9
+ const { KARANA_NAMES, ONE_TITHI } = require('./constants');
10
+
11
+ /**
12
+ * Get karana number and name
13
+ * There are 11 types of karanas:
14
+ * - 4 Fixed (Shakuni, Chatushpada, Naga, Kimstughna) - occur once each
15
+ * - 7 Repeating (Bava, Balava, Kaulava, Taitila, Garaja, Vanija, Vishti) - repeat 8 times each
16
+ *
17
+ * Distribution in 60 karanas:
18
+ * K1=Kimstughna (1st half of Shukla Pratipada)
19
+ * K2-K57: Bava to Vishti repeating 8 times
20
+ * K58=Shakuni, K59=Chatushpada, K60=Naga
21
+ */
22
+
23
+ /**
24
+ * Get karana type (fixed or repeating) and name
25
+ * @param {number} karanaNo - Karana number (1-60)
26
+ * @returns {object} {number, name, type}
27
+ */
28
+ function getKaranaInfo(karanaNo) {
29
+ // Normalize to 1-60
30
+ const k = ((karanaNo - 1) % 60) + 1;
31
+
32
+ let name, type;
33
+
34
+ if (k === 1) {
35
+ name = 'Kimstughna';
36
+ type = 'fixed';
37
+ } else if (k === 58) {
38
+ name = 'Shakuni';
39
+ type = 'fixed';
40
+ } else if (k === 59) {
41
+ name = 'Chatushpada';
42
+ type = 'fixed';
43
+ } else if (k === 60) {
44
+ name = 'Naga';
45
+ type = 'fixed';
46
+ } else {
47
+ // Repeating karanas (k=2 to k=57)
48
+ // Bava(0), Balava(1), Kaulava(2), Taitila(3), Garaja(4), Vanija(5), Vishti(6)
49
+ const repeatIndex = (k - 2) % 7;
50
+ const repeatNames = ['Bava', 'Balava', 'Kaulava', 'Taitila', 'Garaja', 'Vanija', 'Vishti'];
51
+ name = repeatNames[repeatIndex];
52
+ type = 'repeating';
53
+
54
+ // Vishti (Bhadra) is considered inauspicious
55
+ if (name === 'Vishti') {
56
+ type = 'inauspicious';
57
+ }
58
+ }
59
+
60
+ return { number: k, name, type };
61
+ }
62
+
63
+ /**
64
+ * Get karana from tithi
65
+ * @param {number} tithiNo - Tithi number (1-30)
66
+ * @param {boolean} firstHalf - True for first half of tithi
67
+ * @returns {number} Karana number (1-60)
68
+ */
69
+ function karanaFromTithi(tithiNo, firstHalf = true) {
70
+ // Each tithi has 2 karanas
71
+ const baseKarana = (tithiNo - 1) * 2 + 1;
72
+ return firstHalf ? baseKarana : baseKarana + 1;
73
+ }
74
+
75
+ /**
76
+ * Get karana details for given date/time
77
+ * @param {number} jd - Julian Day Number (local)
78
+ * @param {object} place - {latitude, longitude, timezone}
79
+ * @returns {object} Karana details
80
+ */
81
+ function karana(jd, place) {
82
+ // Get tithi info
83
+ const tithiInfo = tithiModule.tithi(jd, place);
84
+ if (!tithiInfo) return null;
85
+
86
+ const { year, month, day, hour } = utils.jdToDate(jd);
87
+ const currentHours = hour;
88
+
89
+ // Calculate midpoint of tithi (when karana changes)
90
+ const tithiMid = (tithiInfo.startTime + tithiInfo.endTime) / 2;
91
+
92
+ // Determine which half of tithi we're in
93
+ const isFirstHalf = currentHours <= tithiMid || tithiInfo.startTime < 0;
94
+ const karanaNo = karanaFromTithi(tithiInfo.number, isFirstHalf);
95
+ const karanaInfo = getKaranaInfo(karanaNo);
96
+
97
+ // Calculate karana start and end times
98
+ let startTime, endTime;
99
+ if (isFirstHalf) {
100
+ startTime = tithiInfo.startTime;
101
+ endTime = tithiMid;
102
+ } else {
103
+ startTime = tithiMid;
104
+ endTime = tithiInfo.endTime;
105
+ }
106
+
107
+ // Handle negative start time (from previous day)
108
+ if (startTime < 0) {
109
+ startTime = startTime + 24;
110
+ }
111
+
112
+ return {
113
+ number: karanaNo,
114
+ name: karanaInfo.name,
115
+ type: karanaInfo.type,
116
+ startTime,
117
+ endTime,
118
+ startTimeString: utils.formatTime(startTime),
119
+ endTimeString: utils.formatTime(endTime > 24 ? endTime - 24 : endTime) + (endTime > 24 ? ' (+1)' : ''),
120
+ isVishti: karanaInfo.name === 'Vishti',
121
+ tithi: tithiInfo.number,
122
+ halfOfTithi: isFirstHalf ? 'first' : 'second'
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Check if karana is Vishti (Bhadra) - inauspicious
128
+ * @param {number} karanaNo - Karana number
129
+ * @returns {boolean} True if Vishti
130
+ */
131
+ function isVishti(karanaNo) {
132
+ const info = getKaranaInfo(karanaNo);
133
+ return info.name === 'Vishti';
134
+ }
135
+
136
+ /**
137
+ * Get all karanas for a day
138
+ * @param {number} jd - Julian Day Number
139
+ * @param {object} place - {latitude, longitude, timezone}
140
+ * @returns {array} List of karanas for the day
141
+ */
142
+ function karanasForDay(jd, place) {
143
+ const tithiInfo = tithiModule.tithi(jd, place);
144
+ if (!tithiInfo) return [];
145
+
146
+ const karanas = [];
147
+
148
+ // First karana (first half of current tithi)
149
+ const mid = (tithiInfo.startTime + tithiInfo.endTime) / 2;
150
+ const k1 = karanaFromTithi(tithiInfo.number, true);
151
+ const k1Info = getKaranaInfo(k1);
152
+
153
+ if (tithiInfo.startTime < mid && mid < 24) {
154
+ karanas.push({
155
+ number: k1,
156
+ name: k1Info.name,
157
+ startTime: tithiInfo.startTime < 0 ? tithiInfo.startTime + 24 : tithiInfo.startTime,
158
+ endTime: mid
159
+ });
160
+ }
161
+
162
+ // Second karana (second half of current tithi)
163
+ const k2 = karanaFromTithi(tithiInfo.number, false);
164
+ const k2Info = getKaranaInfo(k2);
165
+
166
+ if (mid < tithiInfo.endTime) {
167
+ karanas.push({
168
+ number: k2,
169
+ name: k2Info.name,
170
+ startTime: mid,
171
+ endTime: tithiInfo.endTime > 24 ? tithiInfo.endTime - 24 : tithiInfo.endTime
172
+ });
173
+ }
174
+
175
+ return karanas;
176
+ }
177
+
178
+ module.exports = {
179
+ karana,
180
+ karanaFromTithi,
181
+ getKaranaInfo,
182
+ isVishti,
183
+ karanasForDay,
184
+ KARANA_NAMES
185
+ };
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Lunar Calendar Functions
3
+ * Lunar month, adhika detection, new/full moon, etc.
4
+ */
5
+
6
+ const sunriseSunset = require('./sunrise-sunset');
7
+ const tithiModule = require('./tithi');
8
+ const utils = require('./utils');
9
+ const { LUNAR_MONTH_NAMES, SIDEREAL_YEAR, KALI_YUGA_JD } = require('./constants');
10
+
11
+ /**
12
+ * Get lunar month for given date
13
+ * @param {number} jd - Julian Day Number
14
+ * @param {object} place - {latitude, longitude, timezone}
15
+ * @returns {object} Lunar month details
16
+ */
17
+ function lunarMonth(jd, place) {
18
+ const rise = sunriseSunset.sunrise(jd, place);
19
+ if (!rise) return null;
20
+
21
+ const jdUTC = utils.toUTC(rise.jd, place.timezone);
22
+ const currentTithi = tithiModule.quickTithi(jdUTC);
23
+
24
+ // Find last and next new moons
25
+ const lastNewMoon = tithiModule.newMoon(jdUTC, currentTithi, -1);
26
+ const nextNewMoon = tithiModule.newMoon(jdUTC, currentTithi, +1);
27
+
28
+ // Get solar month (raasi) at each new moon
29
+ const thisSolarMonth = Math.floor(sunriseSunset.solarLongitude(lastNewMoon) / 30);
30
+ const nextSolarMonth = Math.floor(sunriseSunset.solarLongitude(nextNewMoon) / 30);
31
+
32
+ // If both new moons are in same solar month, it's an adhika (leap) month
33
+ const isAdhika = (thisSolarMonth === nextSolarMonth);
34
+
35
+ // Lunar month is named after the solar month at the start
36
+ const lunarMonthIndex = (thisSolarMonth + 1) % 12;
37
+
38
+ // Check for nija (second occurrence after adhika)
39
+ let isNija = false;
40
+ if (!isAdhika) {
41
+ const prevMonthData = lunarMonth(jd - 30, place);
42
+ if (prevMonthData && prevMonthData.index === lunarMonthIndex && prevMonthData.isAdhika) {
43
+ isNija = true;
44
+ }
45
+ }
46
+
47
+ return {
48
+ index: lunarMonthIndex,
49
+ number: lunarMonthIndex + 1, // 1-12
50
+ name: LUNAR_MONTH_NAMES[lunarMonthIndex],
51
+ isAdhika,
52
+ isNija,
53
+ prefix: isAdhika ? 'Adhika ' : (isNija ? 'Nija ' : ''),
54
+ fullName: (isAdhika ? 'Adhika ' : (isNija ? 'Nija ' : '')) + LUNAR_MONTH_NAMES[lunarMonthIndex],
55
+ lastNewMoon: utils.jdToDate(lastNewMoon),
56
+ nextNewMoon: utils.jdToDate(nextNewMoon)
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Get complete lunar date (month + day + year)
62
+ * @param {number} jd - Julian Day Number
63
+ * @param {object} place - {latitude, longitude, timezone}
64
+ * @param {boolean} usePurnimanta - Use Purnimanta system (month ends at full moon)
65
+ * @returns {object} Complete lunar date
66
+ */
67
+ function lunarDate(jd, place, usePurnimanta = false) {
68
+ const month = lunarMonth(jd, place);
69
+ const tithiInfo = tithiModule.tithi(jd, place);
70
+
71
+ if (!month || !tithiInfo) return null;
72
+
73
+ let lunarDay = tithiInfo.number;
74
+ let monthIndex = month.index;
75
+
76
+ // In Purnimanta system, month changes at full moon (tithi 15)
77
+ if (usePurnimanta) {
78
+ if (lunarDay > 15) {
79
+ monthIndex = (monthIndex + 1) % 12;
80
+ lunarDay = (lunarDay - 16) % 30 + 1;
81
+ }
82
+ }
83
+
84
+ // Calculate lunar year (Samvatsara)
85
+ const yearIndex = lunarYearIndex(jd, monthIndex + 1);
86
+
87
+ return {
88
+ month: monthIndex + 1,
89
+ monthName: LUNAR_MONTH_NAMES[monthIndex],
90
+ day: lunarDay,
91
+ tithiName: tithiInfo.name,
92
+ paksha: tithiInfo.paksha,
93
+ year: yearIndex,
94
+ isAdhika: month.isAdhika,
95
+ isNija: month.isNija,
96
+ system: usePurnimanta ? 'Purnimanta' : 'Amanta'
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Calculate lunar year index (0-59 Samvatsara cycle)
102
+ * @param {number} jd - Julian Day Number
103
+ * @param {number} maasaIndex - Lunar month (1-12)
104
+ * @returns {number} Year index in 60-year cycle
105
+ */
106
+ function lunarYearIndex(jd, maasaIndex) {
107
+ const kali = elapsedKaliYears(jd, maasaIndex);
108
+ const kaliBase = 14;
109
+ const kaliStart = 27; // Pramaadhi
110
+
111
+ let adjustedKali = kali;
112
+ if (kali >= 4009) {
113
+ adjustedKali = (kali - kaliBase) % 60;
114
+ }
115
+
116
+ const samvatIndex = (adjustedKali + kaliStart + Math.floor((adjustedKali * 211 - 108) / 18000)) % 60;
117
+ return samvatIndex - 1;
118
+ }
119
+
120
+ /**
121
+ * Calculate elapsed Kali Yuga years
122
+ * @param {number} jd - Julian Day Number
123
+ * @param {number} maasaIndex - Month index (1-12)
124
+ * @returns {number} Kali year number
125
+ */
126
+ function elapsedKaliYears(jd, maasaIndex) {
127
+ const ahar = jd - KALI_YUGA_JD;
128
+ return Math.floor((ahar + (4 - maasaIndex) * 30) / SIDEREAL_YEAR);
129
+ }
130
+
131
+ /**
132
+ * Calculate era years (Kali, Vikrama, Saka)
133
+ * @param {number} jd - Julian Day Number
134
+ * @param {number} maasaIndex - Month index
135
+ * @returns {object} {kali, vikrama, saka}
136
+ */
137
+ function eraYears(jd, maasaIndex) {
138
+ const kali = elapsedKaliYears(jd, maasaIndex);
139
+ const saka = kali - 3179;
140
+ const vikrama = saka + 135;
141
+ return { kali, vikrama, saka };
142
+ }
143
+
144
+ /**
145
+ * Get next lunar month start date
146
+ * @param {number} jd - Julian Day Number
147
+ * @param {object} place - {latitude, longitude, timezone}
148
+ * @param {number} direction - 1 for next, -1 for previous
149
+ * @returns {object} {date, time}
150
+ */
151
+ function nextLunarMonthStart(jd, place, direction = 1) {
152
+ const currentTithi = tithiModule.quickTithi(utils.toUTC(jd, place.timezone));
153
+ const newMoonJD = tithiModule.newMoon(jd, currentTithi, direction);
154
+
155
+ // Get tithi at new moon to find exact start
156
+ const tithiAtNewMoon = tithiModule.tithi(newMoonJD + place.timezone / 24, place);
157
+
158
+ if (!tithiAtNewMoon) return null;
159
+
160
+ const date = utils.jdToDate(newMoonJD + place.timezone / 24);
161
+
162
+ return {
163
+ year: date.year,
164
+ month: date.month,
165
+ day: Math.floor(date.day),
166
+ hours: tithiAtNewMoon.endTime,
167
+ jd: newMoonJD
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Calculate Ritu (season) from lunar month
173
+ * @param {number} monthIndex - Lunar month (0-11 or 1-12)
174
+ * @returns {object} Ritu details
175
+ */
176
+ function ritu(monthIndex) {
177
+ // Normalize to 0-11
178
+ const idx = monthIndex > 0 && monthIndex <= 12 ? monthIndex - 1 : monthIndex;
179
+ const rituIndex = Math.floor(idx / 2);
180
+
181
+ const rituNames = ['Vasanta', 'Grishma', 'Varsha', 'Sharad', 'Hemanta', 'Shishira'];
182
+ const rituDescriptions = [
183
+ 'Spring (Chaitra-Vaishakha)',
184
+ 'Summer (Jyeshtha-Ashadha)',
185
+ 'Monsoon (Shravana-Bhadrapada)',
186
+ 'Autumn (Ashwin-Kartik)',
187
+ 'Pre-winter (Margashirsha-Pausha)',
188
+ 'Winter (Magha-Phalguna)'
189
+ ];
190
+
191
+ return {
192
+ index: rituIndex,
193
+ name: rituNames[rituIndex],
194
+ description: rituDescriptions[rituIndex]
195
+ };
196
+ }
197
+
198
+ module.exports = {
199
+ lunarMonth,
200
+ lunarDate,
201
+ lunarYearIndex,
202
+ elapsedKaliYears,
203
+ eraYears,
204
+ nextLunarMonthStart,
205
+ ritu,
206
+ LUNAR_MONTH_NAMES
207
+ };