'use strict'; var astronomia = require('astronomia'); var vsop87Bearth = require('./vsop87Bearth.cjs'); /** * @copyright 2016 commenthol * @license MIT */ const earth = new astronomia.planetposition.Planet(vsop87Bearth.vsop87Bearth); const lunarOffset = astronomia.moonphase.meanLunarMonth / 2; const p = 180 / Math.PI; // Start of Chinese Calendar in 2636 BCE by Chalmers const epochY = -2636; const epoch = new astronomia.julian.CalendarGregorian(epochY, 2, 15).toJDE(); function toYear (jde) { return new astronomia.julian.CalendarGregorian().fromJDE(jde).toYear() } // prevent rounding errors function toFixed (val, e) { return parseFloat(val.toFixed(e), 10) } class CalendarChinese { /** * constructor * * @param {Number|Array|Object} cycle - chinese 60 year cicle; if `{Array}` than `[cycle, year, ..., day]` * @param {Number} year - chinese year of cycle * @param {Number} month - chinese month * @param {Number} leap - `true` if leap month * @param {Number} day - chinese day */ constructor (cycle, year, month, leap, day) { this.set(cycle, year, month, leap, day); this._epochY = epochY; this._epoch = epoch; this._cache = { // cache for results lon: {}, sue: {}, ny: {} }; } /** * set a new chinese date * * @param {Number|Array|Object} cycle - chinese 60 year cicle; if `{Array}` than `[cycle, year, ..., day]` * @param {Number} year - chinese year of cycle * @param {Number} month - chinese month * @param {Number} leap - `true` if leap month * @param {Number} day - chinese day */ set (cycle, year, month, leap, day) { if (cycle instanceof CalendarChinese) { this.cycle = cycle.cycle; this.year = cycle.year; this.month = cycle.month; this.leap = cycle.leap; this.day = cycle.day; } else if (Array.isArray(cycle)) { this.cycle = cycle[0]; this.year = cycle[1]; this.month = cycle[2]; this.leap = cycle[3]; this.day = cycle[4]; } else { this.cycle = cycle; this.year = year; this.month = month; this.leap = leap; this.day = day; } return this } /** * returns chinese date * @returns {Array} */ get () { return [this.cycle, this.year, this.month, this.leap, this.day] } /** * get Gregorian year from Epoch / Cycle * @return {Number} year */ yearFromEpochCycle () { return this._epochY + (this.cycle - 1) * 60 + (this.year - 1) } /** * convert gregorian date to chinese calendar date * * @param {Number} year - (int) year in Gregorian or Julian Calendar * @param {Number} month - (int) * @param {Number} day - needs to be in correct (chinese) timezone * @return {Object} this */ fromGregorian (year, month, day) { const j = this.midnight(new astronomia.julian.CalendarGregorian(year, month, day).toJDE()); if (month === 1 && day <= 20) year--; // chinese new year never starts before 20/01 this._from(j, year); return this } /** * convert date to chinese calendar date * * @param {Date} date - javascript date object * @return {Object} this */ fromDate (date) { const j = this.midnight(new astronomia.julian.CalendarGregorian().fromDate(date).toJDE()); this._from(j, date.getFullYear()); return this } /** * convert JDE to chinese calendar date * * @param {Number} jde - date in JDE * @return {Object} this */ fromJDE (jde) { const j = this.midnight(jde); const gc = new astronomia.julian.CalendarGregorian().fromJDE(j); if (gc.month === 1 && gc.day < 20) gc.year--; // chinese new year never starts before 20/01 this._from(j, gc.year); return this } /** * common conversion from JDE, year to chinese date * * @private * @param {Number} j - date in JDE * @param {Number} year - gregorian year */ _from (j, year) { let ny = this.newYear(year); if (ny > j) { ny = this.newYear(year - 1); } let nm = this.previousNewMoon(j); if (nm < ny) { nm = ny; } const years = 1.5 + (ny - this._epoch) / astronomia.base.BesselianYear; this.cycle = 1 + Math.trunc((years - 1) / 60); this.year = 1 + Math.trunc((years - 1) % 60); this.month = this.inMajorSolarTerm(nm).term; const m = Math.round((nm - ny) / astronomia.moonphase.meanLunarMonth); if (m === 0) { this.month = 1; this.leap = false; } else { this.leap = this.isLeapMonth(nm); } if (m > this.month) { this.month = m; } else if (this.leap) { this.month--; } this.day = 1 + Math.trunc(toFixed(j, 3) - toFixed(nm, 3)); } /** * convert chinese date to gregorian date * * @param {Number} [gyear] - (int) gregorian year * @return {Object} date in gregorian (preleptic) calendar; Timezone is Standard Chinese / Bejing Time * {Number} year - (int) * {Number} month - (int) * {Number} day - (int) */ toGregorian (gyear) { const jde = this.toJDE(gyear); const gc = new astronomia.julian.CalendarGregorian().fromJDE(jde + 0.5); // add 0.5 as day get truncated return { year: gc.year, month: gc.month, day: Math.trunc(gc.day) } } /** * convert chinese date to Date * * @param {Number} [gyear] - (int) gregorian year * @return {Date} javascript date object in gregorian (preleptic) calendar */ toDate (gyear) { const jde = this.toJDE(gyear); return new astronomia.julian.CalendarGregorian().fromJDE(toFixed(jde, 4)).toDate() } /** * convert chinese date to JDE * * @param {Number} [gyear] - (int) gregorian year * @return {Number} date in JDE */ toJDE (gyear) { const years = gyear || this.yearFromEpochCycle(); const ny = this.newYear(years); let nm = ny; if (this.month > 1) { nm = this.previousNewMoon(ny + this.month * 29); const st = this.inMajorSolarTerm(nm).term; const lm = this.isLeapMonth(nm); if (st > this.month) { nm = this.previousNewMoon(nm - 1); } else if (st < this.month || (lm && !this.leap)) { nm = this.nextNewMoon(nm + 1); } } if (this.leap) { nm = this.nextNewMoon(nm + 1); } const jde = nm + this.day - 1; return jde } /** * timeshift to UTC * * @param {CalendarGregorian} gcal - gregorian calendar date * @return {Number} timeshift in fraction of day */ timeshiftUTC (gcal) { if (gcal.toYear() >= 1929) { return 8 / 24 // +8:00:00h Standard China time zone (120° East) } return 1397 / 180 / 24 // +7:45:40h Beijing (116°25´ East) } /** * time/date at midnight - truncate `jde` to actual day * * @param {Number} jde - julian ephemeris day * @return {Number} truncated jde */ midnight (jde) { const gcal = new astronomia.julian.CalendarGregorian().fromJDE(jde); const ts = 0.5 - this.timeshiftUTC(gcal); let mn = Math.trunc(gcal.toJD() - ts) + ts; mn = gcal.fromJD(mn).toJDE(); if (toFixed(jde, 5) === toFixed(mn, 5) + 1) { return jde } return mn } /** * get major solar term `Z1...Z12` for a given date in JDE * * @param {Number} jde - date of new moon * @returns {Number} major solar term part of that month */ inMajorSolarTerm (jde) { const lon = this._cache.lon[jde] || astronomia.solar.apparentVSOP87(earth, jde).lon; this._cache.lon[jde] = lon; const lonDeg = lon * p - 1e-13; const term = (2 + Math.floor(lonDeg / 30)) % 12 + 1; return { term, lon: lonDeg } } /** * Test if date `jde` is inside a leap month * `jde` and previous new moon need to have the same major solar term * * @param {Number} jde - date of new moon * @returns {Boolean} `true` if previous new moon falls into same solar term */ isLeapMonth (jde) { const t1 = this.inMajorSolarTerm(jde); const next = this.nextNewMoon(this.midnight(jde + lunarOffset)); const t2 = this.inMajorSolarTerm(next); const r = (t1.term === t2.term); return r } /** * next new moon since `jde` * * @param {Number} jde - date in julian ephemeris days * @return {Number} jde at midnight */ nextNewMoon (jde) { let nm = this.midnight(astronomia.moonphase.newMoon(toYear(jde))); let cnt = 0; while (nm < jde && cnt++ < 4) { nm = this.midnight(astronomia.moonphase.newMoon(toYear(jde + cnt * lunarOffset))); } return nm } /** * next new moon since `jde` * * @param {Number} jde - date in julian ephemeris days * @return {Number} jde at midnight */ previousNewMoon (jde) { let nm = this.midnight(astronomia.moonphase.newMoon(toYear(jde))); let cnt = 0; while (nm > jde && cnt++ < 4) { nm = this.midnight(astronomia.moonphase.newMoon(toYear(jde - cnt * lunarOffset))); } return nm } /** * chinese new year for a given gregorian year * * @param {Number} gyear - gregorian year (int) * @param {Number} jde at midnight */ newYear (gyear) { gyear = Math.trunc(gyear); if (this._cache.ny[gyear]) return this._cache.ny[gyear] const sue1 = this._cache.sue[gyear - 1] || astronomia.solstice.december2(gyear - 1, earth); const sue2 = this._cache.sue[gyear] || astronomia.solstice.december2(gyear, earth); this._cache.sue[gyear - 1] = sue1; this._cache.sue[gyear] = sue2; const m11n = this.previousNewMoon(this.midnight(sue2 + 1)); const m12 = this.nextNewMoon(this.midnight(sue1 + 1)); const m13 = this.nextNewMoon(this.midnight(m12 + lunarOffset)); this.leapSui = Math.round((m11n - m12) / astronomia.moonphase.meanLunarMonth) === 12; let ny = m13; if (this.leapSui && (this.isLeapMonth(m12) || this.isLeapMonth(m13))) { ny = this.nextNewMoon(this.midnight(m13 + astronomia.moonphase.meanLunarMonth / 2)); } this._cache.ny[gyear] = ny; return ny } /** * get solar term from solar longitude * * @param {Number} term - jiéqì solar term 1 .. 24 * @param {Number} [gyear] - (int) gregorian year * @returns {Number} jde at midnight */ solarTerm (term, gyear) { if (gyear && term <= 3) gyear--; const years = gyear || this.yearFromEpochCycle(); const lon = (((term + 20) % 24) * 15) % 360; let st = astronomia.solstice.longitude(years, earth, lon / p); st = this.midnight(st); return st } /** * get major solar term * * @param {Number} term - zhōngqì solar term Z1 .. Z12 * @param {Number} [gyear] - (int) gregorian year * @returns {Number} jde at midnight */ majorSolarTerm (term, gyear) { return this.solarTerm(term * 2, gyear) } /** * get minor solar term * * @param {Number} term - jiéqì solar term J1 .. J12 * @param {Number} [gyear] - (int) gregorian year * @returns {Number} jde at midnight */ minorSolarTerm (term, gyear) { return this.solarTerm(term * 2 - 1, gyear) } /** * Qı̄ngmíng - Pure brightness Festival * * @param {Number} [gyear] - (int) gregorian year * @returns {Number} jde at midnight */ qingming (gyear) { return this.solarTerm(5, gyear) } } module.exports = CalendarChinese;