Skip to main content

astroceleste_engine/vedic/
calendar.rs

1//! The panchanga of a moment: the Vedic day (from sunrise) and its weekday, the five angas
2//! (tithi, nakshatra, yoga, karana, vara) running at the moment with their start and end,
3//! the lunar month and the year.
4//!
5//! The sidereal angles use the Lahiri ayanamsa, the panchanga standard. Months are amanta
6//! (new moon to new moon), named after the sidereal sign the Sun enters during them; a
7//! month with no such ingress is adhika (intercalary) and takes the next month's name. The
8//! year (samvatsara, Shaka and Vikram eras) starts with the month of Chaitra.
9
10use 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
20/// The ayanamsa of the panchanga.
21const AYANAMSA: &str = "lahiri";
22/// Mean sidereal motion of the Moon, degrees per day.
23const MOON_RATE: f64 = 13.176_358;
24
25/// The fifteen tithis of a fortnight; the fifteenth is purnima (full moon) in the bright
26/// half and amavasya (new moon) in the dark half.
27const 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
44/// The weekdays from Sunday, with their lords.
45const 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
85/// The seven movable karanas, which repeat eight times from the second half-tithi.
86const MOVABLE_KARANAS: [&str; 7] = [
87    "bava", "balava", "kaulava", "taitila", "gara", "vanija", "vishti",
88];
89
90/// The lunar months from Chaitra.
91const 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
106/// The seasons, two months each from Chaitra.
107const RITUS: [&str; 6] = [
108    "vasanta", "grishma", "varsha", "sharad", "hemanta", "shishira",
109];
110
111/// The sixty years of the Jupiter cycle, from Prabhava.
112const 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/// One of the angas running at the moment.
176#[derive(Debug, Clone, PartialEq, Serialize)]
177pub struct Anga {
178    /// 1-based, in its cycle (tithi 1-30, nakshatra and yoga 1-27, karana 1-60).
179    pub number: u8,
180    /// Stable code.
181    pub code: &'static str,
182    /// When it began, ISO 8601 UTC to the second.
183    pub start: String,
184    /// When it ends, ISO 8601 UTC to the second.
185    pub end: String,
186}
187
188/// The weekday of the Vedic day, which begins at sunrise.
189#[derive(Debug, Clone, PartialEq, Serialize)]
190pub struct Vara {
191    /// 1 (Sunday) to 7 (Saturday).
192    pub number: u8,
193    /// Stable code, e.g. "ravivara".
194    pub code: &'static str,
195    /// The ruling body.
196    pub lord: &'static str,
197}
198
199/// The lunar month.
200#[derive(Debug, Clone, PartialEq, Serialize)]
201pub struct Masa {
202    /// The amanta month (new moon to new moon).
203    pub amanta: &'static str,
204    /// The purnimanta month (full moon to full moon): the next one in the dark half.
205    pub purnimanta: &'static str,
206    /// Intercalary: no sidereal ingress of the Sun during the month.
207    pub adhika: bool,
208    /// The new moon that begins the amanta month, ISO 8601 UTC to the second.
209    pub start: String,
210    /// The new moon that ends it.
211    pub end: String,
212}
213
214/// The panchanga of a moment and place: [`vedic_calendar`].
215#[derive(Debug, Clone, PartialEq, Serialize)]
216pub struct VedicCalendar {
217    /// The ayanamsa of the sidereal angles: "lahiri".
218    pub ayanamsa: &'static str,
219    /// The sunrise that begins the Vedic day, the sunset and the next sunrise, ISO 8601
220    /// UTC. Where the Sun does not rise or set, half-day defaults stand in.
221    pub sunrise: String,
222    /// Sunset of the Vedic day.
223    pub sunset: String,
224    /// The sunrise that ends the Vedic day.
225    pub next_sunrise: String,
226    /// The weekday of the Vedic day.
227    pub vara: Vara,
228    /// The lunar day: each 12° of the Moon's elongation from the Sun.
229    pub tithi: Anga,
230    /// "shukla" (waxing, tithis 1-15) or "krishna" (waning, 16-30).
231    pub paksha: &'static str,
232    /// The Moon's nakshatra.
233    pub nakshatra: Anga,
234    /// The quarter of the nakshatra the Moon is in, 1 to 4.
235    pub pada: u8,
236    /// The sum of the Sun's and the Moon's sidereal longitudes, in 27 parts.
237    pub yoga: Anga,
238    /// Half a tithi.
239    pub karana: Anga,
240    /// The lunar month.
241    pub masa: Masa,
242    /// The season.
243    pub ritu: &'static str,
244    /// "uttarayana" (the Sun in sidereal Capricorn to Gemini) or "dakshinayana".
245    pub ayana: &'static str,
246    /// The year of the sixty-year cycle, 1 (Prabhava) to 60 (Akshaya), as reckoned in
247    /// southern India (with the Shaka era).
248    pub samvatsara_number: u8,
249    /// Its code.
250    pub samvatsara: &'static str,
251    /// Year of the Shaka era.
252    pub shaka_year: i32,
253    /// Year of the Vikram era.
254    pub vikram_year: i32,
255}
256
257/// The panchanga at `instant` for a place, or `None` when the loaded kernels do not cover
258/// the lunar month around it.
259pub 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
275/// The anga of `f` (degrees growing at about `rate` per day) at `jd`, in parts of `span`:
276/// its 0-based index and the moments it began and ends.
277fn 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    // The Vedic day begins at the sunrise at or before the moment (rise_set allows a
305    // little slack after it).
306    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    // The weekday of the sunrise's date in local mean time.
311    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    // The amanta month, named after the sign the Sun enters during it.
344    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    // The year began with the last Chaitra, which starts in March or April: January and
362    // February, and Magha and Phalguna in March or April, belong to the year before's.
363    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; // 0-based
371
372    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"); // Shaka 1946 = 2024-25
432    }
433}