Source: calendar.js

/*
 * kollavarsham
 * http://kollavarsham.org
 *
 * Copyright (c) 2014-2023 The Kollavarsham Team
 * Licensed under the MIT license.
 */
/**
 * @module calendar
 */
import { JulianDate } from './dates/julianDate';
import { MathHelper } from './mathHelper';
// TODO: Refactor this out
const samkranti = {
    ahargana: -1,
    Year: -1,
    Month: -1,
    Day: -1,
    Hour: -1,
    Min: -1
};
/**
 *
 *  **INTERNAL/PRIVATE**
 *
 * @class Calendar
 */
export class Calendar {
    constructor(celestial) {
        this.celestial = celestial;
    }
    static get weekdays() {
        return {
            0: { en: 'Monday', ml: 'തിങ്കൾ' },
            1: { en: 'Tuesday', ml: 'ചൊവ്വ' },
            2: { en: 'Wednesday', ml: 'ബുധൻ' },
            3: { en: 'Thursday', ml: 'വ്യാഴം' },
            4: { en: 'Friday', ml: 'വെള്ളി' },
            5: { en: 'Saturday', ml: 'ശനി' },
            6: { en: 'Sunday', ml: 'ഞായർ' }
        };
    }
    static get months() {
        return {
            January: 0,
            February: 1,
            March: 2,
            April: 3,
            May: 4,
            June: 5,
            July: 6,
            August: 7,
            September: 8,
            October: 9,
            November: 10,
            December: 11
        };
    }
    static get naksatras() {
        return {
            [-1]: { saka: '', enMalayalam: '', mlMalayalam: '' },
            0: { saka: 'Asvini', enMalayalam: 'Ashwathi', mlMalayalam: 'അശ്വതി' },
            1: { saka: 'Bharani', enMalayalam: 'Bharani', mlMalayalam: 'ഭരണി' },
            2: { saka: 'Krttika', enMalayalam: 'Karthika', mlMalayalam: 'കാർത്തിക' },
            3: { saka: 'Rohini', enMalayalam: 'Rohini', mlMalayalam: 'രോഹിണി' },
            4: { saka: 'Mrgasira', enMalayalam: 'Makiryam', mlMalayalam: 'മകയിരം' },
            5: { saka: 'Ardra', enMalayalam: 'Thiruvathira', mlMalayalam: 'തിരുവാതിര' },
            6: { saka: 'Punarvasu', enMalayalam: 'Punartham', mlMalayalam: 'പുണർതം' },
            7: { saka: 'Pusya', enMalayalam: 'Pooyam', mlMalayalam: 'പൂയം' },
            8: { saka: 'Aslesa', enMalayalam: 'Aayilyam', mlMalayalam: 'ആയില്യം' },
            9: { saka: 'Magha', enMalayalam: 'Makam', mlMalayalam: 'മകം' },
            10: { saka: 'P-phalguni', enMalayalam: 'Pooram', mlMalayalam: 'പൂരം' },
            11: { saka: 'U-phalguni', enMalayalam: 'Uthram', mlMalayalam: 'ഉത്രം' },
            12: { saka: 'Hasta', enMalayalam: 'Atham', mlMalayalam: 'അത്തം' },
            13: { saka: 'Citra', enMalayalam: 'Chithra', mlMalayalam: 'ചിത്ര' },
            14: { saka: 'Svati', enMalayalam: 'Chothi', mlMalayalam: 'ചോതി' },
            15: { saka: 'Visakha', enMalayalam: 'Vishakham', mlMalayalam: 'വിശാഖം' },
            16: { saka: 'Anuradha', enMalayalam: 'Anizham', mlMalayalam: 'അനിഴം' },
            17: { saka: 'Jyestha', enMalayalam: 'Thrikketta', mlMalayalam: 'തൃക്കേട്ട' },
            18: { saka: 'Mula', enMalayalam: 'Moolam', mlMalayalam: 'മൂലം' },
            19: { saka: 'P-asadha', enMalayalam: 'Pooradam', mlMalayalam: 'പൂരാടം' },
            20: { saka: 'U-asadha', enMalayalam: 'Uthradam', mlMalayalam: 'ഉത്രാടം' },
            21: { saka: 'Sravana', enMalayalam: 'Thiruvonam', mlMalayalam: 'തിരുവോണം' },
            22: { saka: 'Dhanistha', enMalayalam: 'Avittam', mlMalayalam: 'അവിട്ടം' },
            23: { saka: 'Satabhisaj', enMalayalam: 'Chathayam', mlMalayalam: 'ചതയം' },
            24: { saka: 'P-bhadrapada', enMalayalam: 'Poororuttathi', mlMalayalam: 'പൂരുരുട്ടാതി' },
            25: { saka: 'U-bhadrapada', enMalayalam: 'Uthrattathi', mlMalayalam: 'ഉത്രട്ടാതി' },
            26: { saka: 'Revati', enMalayalam: 'Revathi', mlMalayalam: 'രേവതി' },
            27: { saka: 'Asvini', enMalayalam: 'Ashwathi', mlMalayalam: 'അശ്വതി' }
        };
    }
    static timeIntoFractionalDay(date) {
        // TODO: Incorporate this into calculating the multiple-naksatra-per-day (time precision)
        // The year, month and day from the passed in date is discarded and only the time is used.
        // And even from the time information only the hour and minute is used and seconds, milliseconds etc. is discarded
        if (!(date instanceof Date)) {
            throw new Error('Invalid parameter. \'date\' should be an instance of \'Date\'');
        }
        const hour = date.getHours();
        const minute = date.getMinutes();
        return (hour * 60 + minute) / (24 * 60);
    }
    static gregorianDateToJulianDay(date) {
        //  TODO:
        // Annotate all the magic numbers below !
        // There is some explanation here - http://quasar.as.utexas.edu/BillInfo/JulianDatesG.html
        let year = date.getFullYear();
        let month = date.getMonth() + 1;
        const day = date.getDate();
        if (month < 3) {
            year -= 1;
            month += 12;
        }
        let julianDay = MathHelper.truncate(365.25 * year) + MathHelper.truncate(30.59 * (month - 2)) + day + 1721086.5;
        if (year < 0) {
            julianDay -= 1;
            if (year % 4 === 0 && month > 3) {
                julianDay += 1;
            }
        }
        if (julianDay >= 2299160) {
            julianDay += MathHelper.truncate(year / 400) - MathHelper.truncate(year / 100) + 2;
        }
        return julianDay;
    }
    static julianDayToJulianDate(julianDay) {
        const j = MathHelper.truncate(julianDay) + 1402;
        const k = MathHelper.truncate((j - 1) / 1461);
        const l = j - 1461 * k;
        const n = MathHelper.truncate((l - 1) / 365) - MathHelper.truncate(l / 1461);
        const i = l - 365 * n + 30;
        const J = MathHelper.truncate(80 * i / 2447);
        const I = MathHelper.truncate(J / 11);
        const day = i - MathHelper.truncate(2447 * J / 80);
        const month = J + 2 - 12 * I;
        const year = 4 * k + n + I - 4716;
        return new JulianDate(year, month, day);
    }
    static julianDayToGregorianDate(julianDay) {
        const a = julianDay + 68569;
        const b = MathHelper.truncate(a / 36524.25);
        const c = a - MathHelper.truncate(36524.25 * b + 0.75);
        const e = MathHelper.truncate((c + 1) / 365.2425);
        const f = c - MathHelper.truncate(365.25 * e) + 31;
        const g = MathHelper.truncate(f / 30.59);
        const h = MathHelper.truncate(g / 11);
        const day = MathHelper.truncate(f - MathHelper.truncate(30.59 * g) + (julianDay - MathHelper.truncate(julianDay)));
        const month = MathHelper.truncate(g - 12 * h + 2);
        const year = MathHelper.truncate(100 * (b - 49) + e + h);
        const result = new Date(year, month - 1, day);
        if (year > 0 && year <= 99) {
            result.setFullYear(year);
        }
        return result;
    }
    static julianDayToModernDate(julianDay) {
        // Will return JulianDate object for any date before 1st January 1583 AD and Date objects for later dates
        return julianDay < 2299239 ? Calendar.julianDayToJulianDate(julianDay) : Calendar.julianDayToGregorianDate(julianDay);
    }
    static julianDayToAhargana(julianDay) {
        return julianDay - 588465.50;
    }
    static aharganaToJulianDay(ahargana) {
        return 588465.50 + ahargana;
    }
    static kaliToSaka(yearKali) {
        return yearKali - 3179;
    }
    static sakaToKali(yearSaka) {
        return yearSaka + 3179;
    }
    static julianDayToWeekday(julianDay) {
        return Calendar.weekdays[MathHelper.truncate(julianDay + 0.5) % 7];
    }
    static getAdhimasa(lastConjunctionLongitude, nextConjunctionLongitude) {
        const n1 = MathHelper.truncate(lastConjunctionLongitude / 30);
        const n2 = MathHelper.truncate(nextConjunctionLongitude / 30);
        return Math.abs(n1 - n2) < MathHelper.epsilon ? 'Adhika-' : '';
    }
    static getMasaNum(trueSolarLongitude, lastConjunctionLongitude) {
        let masaNum = MathHelper.truncate(trueSolarLongitude / 30) % 12;
        if (masaNum === MathHelper.truncate(lastConjunctionLongitude / 30) % 12) {
            masaNum += 1;
        }
        masaNum = (masaNum + 12) % 12;
        return masaNum;
    }
    static getNaksatra(trueLunarLongitude) {
        return Calendar.naksatras[MathHelper.truncate(trueLunarLongitude * 27 / 360)];
    }
    nextDate(date) {
        // TODO: This looks like a concern of the calling library - But could be exposed as a static utility method (0 usages other than tests)
        date.setUTCDate(date.getUTCDate() + 1);
        return date;
    }
    julianInEngland(julianDay) {
        // TODO: This might be exposed as a static utility method (0 usages other than tests)
        // Gregorian calendar was first introduced in most of Europe in 1582,
        // but it wasn't adopted in England (and so in US and Canada) until 1752
        //
        // - http://www.timeanddate.com/calendar/julian-gregorian-switch.html
        //
        // This returns true between
        //      October 14th, 1582 and September 14th, 1752, both dates exclusive
        return julianDay >= 2299160 && julianDay <= 2361221;
    }
    aharganaToKali(ahargana) {
        return MathHelper.truncate(ahargana * this.celestial.planets.sun.YugaRotation / this.celestial.yuga.CivilDays);
    }
    kaliToAhargana(yearKali, masaNum, tithiDay) {
        const sauraMasas = yearKali * 12 + masaNum; // expired saura masas
        const adhiMasas = MathHelper.truncate(sauraMasas * this.celestial.yuga.Adhimasa / (12 * this.celestial.planets.sun.YugaRotation)); // expired adhimasas
        const candraMasas = sauraMasas + adhiMasas; // expired candra masas
        const tithis = 30 * candraMasas + tithiDay - 1; // expired tithis
        const avamas = MathHelper.truncate(tithis * this.celestial.yuga.Ksayadina / this.celestial.yuga.Tithi); // expired avamas
        return tithis - avamas;
    }
    findSamkranti(leftAhargana, rightAhargana) {
        const width = (rightAhargana - leftAhargana) / 2;
        const centreAhargana = (rightAhargana + leftAhargana) / 2;
        if (width < MathHelper.epsilon) {
            return centreAhargana;
        }
        else {
            let centreTslong = this.celestial.getTrueSolarLongitude(centreAhargana);
            centreTslong -= MathHelper.truncate(centreTslong / 30) * 30;
            if (centreTslong < 5) {
                return this.findSamkranti(leftAhargana, centreAhargana);
            }
            else {
                return this.findSamkranti(centreAhargana, rightAhargana);
            }
        }
    }
    calculateSamkranti(ahargana, desantara) {
        samkranti.ahargana = this.findSamkranti(ahargana, ahargana + 1) + desantara;
        // below line is the fix that Yano-san worked in for Kerala dates - #20140223 cf. try_calculations
        const roundedSamkranti = MathHelper.truncate(samkranti.ahargana) + 0.5;
        const samkrantiModernDate = Calendar.julianDayToModernDate(Calendar.aharganaToJulianDay(roundedSamkranti));
        if (JulianDate.prototype.isPrototypeOf(samkrantiModernDate)) { // eslint-disable-line no-prototype-builtins
            const samkrantiDate = samkrantiModernDate;
            samkranti.Year = samkrantiDate.year;
            samkranti.Month = samkrantiDate.month;
            samkranti.Day = samkrantiDate.date;
        }
        else {
            const samkrantiDate = samkrantiModernDate;
            samkranti.Year = samkrantiDate.getFullYear();
            samkranti.Month = samkrantiDate.getMonth() + 1;
            samkranti.Day = samkrantiDate.getDate();
        }
        const fractionalDay = MathHelper.fractional(samkranti.ahargana) * 24;
        samkranti.Hour = MathHelper.truncate(fractionalDay);
        samkranti.Min = MathHelper.truncate(60 * MathHelper.fractional(fractionalDay));
    }
    isTodaySauraMasaFirst(ahargana, desantara) {
        /*
         //    Definition of the first day
         //    samkranti is between today's 0:00 and 24:00
         //    ==
         //    at 0:00 before 30x, at 24:00 after 30x
         */
        let trueSolarLongitudeToday = this.celestial.getTrueSolarLongitude(ahargana - desantara);
        let trueSolarLongitudeTomorrow = this.celestial.getTrueSolarLongitude(ahargana - desantara + 1);
        trueSolarLongitudeToday -= MathHelper.truncate(trueSolarLongitudeToday / 30) * 30;
        trueSolarLongitudeTomorrow -= MathHelper.truncate(trueSolarLongitudeTomorrow / 30) * 30;
        if (25 < trueSolarLongitudeToday && trueSolarLongitudeTomorrow < 5) { // eslint-disable-line yoda
            this.calculateSamkranti(ahargana, desantara);
            return true;
        }
        return false;
    }
    getSauraMasaAndSauraDivasa(ahargana, desantara) {
        // If today is the first day then 1
        // Otherwise yesterday's + 1
        let month;
        let day;
        ahargana = MathHelper.truncate(ahargana);
        if (this.isTodaySauraMasaFirst(ahargana, desantara)) {
            day = 1;
            const tsLongTomorrow = this.celestial.getTrueSolarLongitude(ahargana + 1);
            month = MathHelper.truncate(tsLongTomorrow / 30) % 12;
            month = (month + 12) % 12;
        }
        else {
            const { sauraMasa, sauraDivasa } = this.getSauraMasaAndSauraDivasa(ahargana - 1, desantara);
            month = sauraMasa;
            day = sauraDivasa + 1;
        }
        return { sauraMasa: month, sauraDivasa: day };
    }
}