1use serde::Serialize;
11
12use super::{nakshatra, NAKSHATRAS, NAKSHATRA_SPAN};
13use crate::almanac::rise_set;
14use crate::ephemeris::KernelSet;
15use crate::error::EngineError;
16use crate::instant::UtcInstant;
17use crate::search::{crossing, instant_of, Sky, ELONGATION_RATE, SUN_RATE, SYNODIC_MONTH};
18use crate::zodiac::ayanamsa_value;
19
20const AYANAMSA: &str = "lahiri";
22const MOON_RATE: f64 = 13.176_358;
24
25const TITHIS: [&str; 14] = [
28 "pratipada",
29 "dvitiya",
30 "tritiya",
31 "chaturthi",
32 "panchami",
33 "shashthi",
34 "saptami",
35 "ashtami",
36 "navami",
37 "dashami",
38 "ekadashi",
39 "dvadashi",
40 "trayodashi",
41 "chaturdashi",
42];
43
44const VARAS: [(&str, &str); 7] = [
46 ("ravivara", "Sun"),
47 ("somavara", "Moon"),
48 ("mangalavara", "Mars"),
49 ("budhavara", "Mercury"),
50 ("guruvara", "Jupiter"),
51 ("shukravara", "Venus"),
52 ("shanivara", "Saturn"),
53];
54
55const YOGAS: [&str; 27] = [
56 "vishkambha",
57 "priti",
58 "ayushman",
59 "saubhagya",
60 "shobhana",
61 "atiganda",
62 "sukarman",
63 "dhriti",
64 "shula",
65 "ganda",
66 "vriddhi",
67 "dhruva",
68 "vyaghata",
69 "harshana",
70 "vajra",
71 "siddhi",
72 "vyatipata",
73 "variyana",
74 "parigha",
75 "shiva",
76 "siddha",
77 "sadhya",
78 "shubha",
79 "shukla",
80 "brahma",
81 "indra",
82 "vaidhriti",
83];
84
85const MOVABLE_KARANAS: [&str; 7] = [
87 "bava", "balava", "kaulava", "taitila", "gara", "vanija", "vishti",
88];
89
90const MASAS: [&str; 12] = [
92 "chaitra",
93 "vaishakha",
94 "jyeshtha",
95 "ashadha",
96 "shravana",
97 "bhadrapada",
98 "ashvina",
99 "kartika",
100 "margashirsha",
101 "pausha",
102 "magha",
103 "phalguna",
104];
105
106const RITUS: [&str; 6] = [
108 "vasanta", "grishma", "varsha", "sharad", "hemanta", "shishira",
109];
110
111const SAMVATSARAS: [&str; 60] = [
113 "prabhava",
114 "vibhava",
115 "shukla",
116 "pramoda",
117 "prajapati",
118 "angirasa",
119 "shrimukha",
120 "bhava",
121 "yuva",
122 "dhatri",
123 "ishvara",
124 "bahudhanya",
125 "pramathi",
126 "vikrama",
127 "vrisha",
128 "chitrabhanu",
129 "svabhanu",
130 "tarana",
131 "parthiva",
132 "vyaya",
133 "sarvajit",
134 "sarvadhari",
135 "virodhi",
136 "vikriti",
137 "khara",
138 "nandana",
139 "vijaya",
140 "jaya",
141 "manmatha",
142 "durmukhi",
143 "hevilambi",
144 "vilambi",
145 "vikari",
146 "sharvari",
147 "plava",
148 "shubhakrit",
149 "shobhakrit",
150 "krodhi",
151 "vishvavasu",
152 "parabhava",
153 "plavanga",
154 "kilaka",
155 "saumya",
156 "sadharana",
157 "virodhakrit",
158 "paridhavi",
159 "pramadi",
160 "ananda",
161 "rakshasa",
162 "nala",
163 "pingala",
164 "kalayukti",
165 "siddharthi",
166 "raudra",
167 "durmati",
168 "dundubhi",
169 "rudhirodgari",
170 "raktakshi",
171 "krodhana",
172 "akshaya",
173];
174
175#[derive(Debug, Clone, PartialEq, Serialize)]
177pub struct Anga {
178 pub number: u8,
180 pub code: &'static str,
182 pub start: String,
184 pub end: String,
186}
187
188#[derive(Debug, Clone, PartialEq, Serialize)]
190pub struct Vara {
191 pub number: u8,
193 pub code: &'static str,
195 pub lord: &'static str,
197}
198
199#[derive(Debug, Clone, PartialEq, Serialize)]
201pub struct Masa {
202 pub amanta: &'static str,
204 pub purnimanta: &'static str,
206 pub adhika: bool,
208 pub start: String,
210 pub end: String,
212}
213
214#[derive(Debug, Clone, PartialEq, Serialize)]
216pub struct VedicCalendar {
217 pub ayanamsa: &'static str,
219 pub sunrise: String,
222 pub sunset: String,
224 pub next_sunrise: String,
226 pub vara: Vara,
228 pub tithi: Anga,
230 pub paksha: &'static str,
232 pub nakshatra: Anga,
234 pub pada: u8,
236 pub yoga: Anga,
238 pub karana: Anga,
240 pub masa: Masa,
242 pub ritu: &'static str,
244 pub ayana: &'static str,
246 pub samvatsara_number: u8,
249 pub samvatsara: &'static str,
251 pub shaka_year: i32,
253 pub vikram_year: i32,
255}
256
257pub fn vedic_calendar(
260 kernels: &KernelSet,
261 instant: UtcInstant,
262 latitude: f64,
263 longitude: f64,
264) -> Result<Option<VedicCalendar>, EngineError> {
265 match calendar(kernels, instant, latitude, longitude) {
266 Err(EngineError::OutOfRange { .. }) => Ok(None),
267 other => other.map(Some),
268 }
269}
270
271fn iso(jd: f64) -> String {
272 instant_of(jd).isoformat()
273}
274
275fn anga(
278 f: impl Fn(f64) -> Result<f64, EngineError>,
279 jd: f64,
280 span: f64,
281 parts: usize,
282 rate: f64,
283) -> Result<(usize, f64, f64), EngineError> {
284 let value = f(jd)?.rem_euclid(360.0);
285 let index = ((value / span) as usize).min(parts - 1);
286 let from = index as f64 * span;
287 let start = crossing(&f, from, jd - (value - from) / rate, rate)?;
288 let end = crossing(&f, from + span, jd + (from + span - value) / rate, rate)?;
289 Ok((index, start, end))
290}
291
292fn calendar(
293 kernels: &KernelSet,
294 instant: UtcInstant,
295 latitude: f64,
296 longitude: f64,
297) -> Result<VedicCalendar, EngineError> {
298 let sky = Sky { kernels };
299 let jd = instant.julian_day();
300 let ayanamsa = |jd: f64| ayanamsa_value(jd, AYANAMSA);
301 let sidereal_sun = |jd: f64| -> Result<f64, EngineError> { Ok(sky.sun(jd)? - ayanamsa(jd)) };
302 let sidereal_moon = |jd: f64| -> Result<f64, EngineError> { Ok(sky.moon(jd)? - ayanamsa(jd)) };
303
304 let (mut rise, mut set, mut next_rise) = rise_set(kernels, jd, latitude, longitude)?;
307 if rise > jd {
308 (rise, set, next_rise) = rise_set(kernels, rise - 0.2, latitude, longitude)?;
309 }
310 let weekday = ((rise + longitude / 360.0 + 1.5).floor() as i64).rem_euclid(7) as usize;
312
313 let (tithi, tithi_start, tithi_end) =
314 anga(|jd| sky.elongation(jd), jd, 12.0, 30, ELONGATION_RATE)?;
315 let paksha = if tithi < 15 { "shukla" } else { "krishna" };
316 let tithi_code = match tithi {
317 14 => "purnima",
318 29 => "amavasya",
319 t => TITHIS[t % 15],
320 };
321
322 let (star, star_start, star_end) = anga(sidereal_moon, jd, NAKSHATRA_SPAN, 27, MOON_RATE)?;
323 let pada = nakshatra(sidereal_moon(jd)?).pada;
324
325 let (yoga, yoga_start, yoga_end) = anga(
326 |jd| Ok(sidereal_sun(jd)? + sidereal_moon(jd)?),
327 jd,
328 NAKSHATRA_SPAN,
329 27,
330 MOON_RATE + SUN_RATE,
331 )?;
332
333 let (karana, karana_start, karana_end) =
334 anga(|jd| sky.elongation(jd), jd, 6.0, 60, ELONGATION_RATE)?;
335 let karana_code = match karana {
336 0 => "kimstughna",
337 57 => "shakuni",
338 58 => "chatushpada",
339 59 => "naga",
340 k => MOVABLE_KARANAS[(k - 1) % 7],
341 };
342
343 let month_start = sky.new_moon_before(jd)?;
345 let month_end = sky.new_moon_near(month_start + SYNODIC_MONTH)?;
346 let sign_at = |jd: f64| -> Result<usize, EngineError> {
347 Ok(((sidereal_sun(jd)?.rem_euclid(360.0) / 30.0) as usize).min(11))
348 };
349 let first_sign = sign_at(month_start)?;
350 let adhika = sign_at(month_end)? == first_sign;
351 let masa = (first_sign + 1) % 12;
352 let purnimanta = if tithi < 15 { masa } else { (masa + 1) % 12 };
353
354 let sun_now = sidereal_sun(jd)?.rem_euclid(360.0);
355 let ayana = if !(90.0..270.0).contains(&sun_now) {
356 "uttarayana"
357 } else {
358 "dakshinayana"
359 };
360
361 let (civil_year, civil_month, ..) = instant.civil();
364 let year_start = if civil_month < 3 || (civil_month <= 4 && masa >= 10) {
365 civil_year - 1
366 } else {
367 civil_year
368 };
369 let shaka_year = year_start - 78;
370 let samvatsara = (shaka_year + 11).rem_euclid(60) as usize; Ok(VedicCalendar {
373 ayanamsa: AYANAMSA,
374 sunrise: iso(rise),
375 sunset: iso(set),
376 next_sunrise: iso(next_rise),
377 vara: Vara {
378 number: weekday as u8 + 1,
379 code: VARAS[weekday].0,
380 lord: VARAS[weekday].1,
381 },
382 tithi: Anga {
383 number: tithi as u8 + 1,
384 code: tithi_code,
385 start: iso(tithi_start),
386 end: iso(tithi_end),
387 },
388 paksha,
389 nakshatra: Anga {
390 number: star as u8 + 1,
391 code: NAKSHATRAS[star],
392 start: iso(star_start),
393 end: iso(star_end),
394 },
395 pada,
396 yoga: Anga {
397 number: yoga as u8 + 1,
398 code: YOGAS[yoga],
399 start: iso(yoga_start),
400 end: iso(yoga_end),
401 },
402 karana: Anga {
403 number: karana as u8 + 1,
404 code: karana_code,
405 start: iso(karana_start),
406 end: iso(karana_end),
407 },
408 masa: Masa {
409 amanta: MASAS[masa],
410 purnimanta: MASAS[purnimanta],
411 adhika,
412 start: iso(month_start),
413 end: iso(month_end),
414 },
415 ritu: RITUS[masa / 2],
416 ayana,
417 samvatsara_number: samvatsara as u8 + 1,
418 samvatsara: SAMVATSARAS[samvatsara],
419 shaka_year,
420 vikram_year: year_start + 57,
421 })
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427
428 #[test]
429 fn karana_and_year_codes() {
430 assert_eq!(MOVABLE_KARANAS[(56 - 1) % 7], "vishti");
431 assert_eq!(SAMVATSARAS[(1946 + 11) % 60], "krodhi"); }
433}